neo-tree.nvim
neo-tree.nvim copied to clipboard
BUG: Can not open file insde App router with parentheses in its name. (Windows only)
Did you check docs and existing issues?
- [X] I have read all the docs.
- [X] I have searched the existing issues.
- [X] I have searched the existing discussions.
Neovim Version (nvim -v)
0.9.5
Operating System / Version
Win 10
Describe the Bug
Can not open file insde App router with parentheses in its name. (Windows only)
Screenshots, Traceback
Steps to Reproduce
- Open the app router project
- Open page.tsx in side folder has () wrap
Expected Behavior
Can open the file
Your Configuration
-- DO NOT change the paths and don't remove the colorscheme
local root = vim.fn.fnamemodify("./.repro", ":p")
-- set stdpaths to use .repro
for _, name in ipairs({ "config", "data", "state", "cache" }) do
vim.env[("XDG_%s_HOME"):format(name:upper())] = root .. "/" .. name
end
-- bootstrap lazy
local lazypath = root .. "/plugins/lazy.nvim"
if not vim.loop.fs_stat(lazypath) then
vim.fn.system({ "git", "clone", "--filter=blob:none", "https://github.com/folke/lazy.nvim.git", lazypath, })
end
vim.opt.runtimepath:prepend(lazypath)
-- install plugins
local plugins = {
"folke/tokyonight.nvim",
-- add any other plugins here
}
local neotree_config = {
"nvim-neo-tree/neo-tree.nvim",
dependencies = { "MunifTanjim/nui.nvim", "nvim-tree/nvim-web-devicons", "nvim-lua/plenary.nvim" },
cmd = { "Neotree" },
keys = {
{ "<Leader>e", "<Cmd>Neotree<CR>" }, -- change or remove this line if relevant.
},
opts = {
-- Your config here
-- ...
},
}
table.insert(plugins, neotree_config)
require("lazy").setup(plugins, {
root = root .. "/plugins",
})
vim.cmd.colorscheme("tokyonight")
-- add anything else here
What is your neo-tree version?
I don't think this is just parenthisis that is hurting you, notice that the entire path just falls over when it hits ] in your path.
I haven't looked at the path escaping code in quite a while, I know it has morphed since I last touched it. It likely needs a bit more robustness to handle some of that weirdness
What is your neo-tree version?
I'm using the latest one 3.25
I don't think this is just parenthisis that is hurting you, notice that the entire path just falls over when it hits
]in your path.I haven't looked at the path escaping code in quite a while, I know it has morphed since I last touched it. It likely needs a bit more robustness to handle some of that weirdness
I found that I have same issue with this https://github.com/nvim-neo-tree/neo-tree.nvim/issues/889
Pinging @bwpge
Ah Windows paths, the gift that keeps on giving 😂
@pysan3 I will take a look in the morning
For anyone who wants to take a look in the meantime, my best initial guess is we'll probably just have to add braces to this escape logic (totally guessing though): https://github.com/nvim-neo-tree/neo-tree.nvim/blob/25bfdbe802eb913276bb83874b043be57bd70347/lua/neo-tree/utils/init.lua#L1058
According to my comment https://github.com/nvim-neo-tree/neo-tree.nvim/pull/1353#issuecomment-1986851233 [ should work with the builtin vim commands (eg :edit). It may be that the same rule does not apply to a lua function we call somewhere?
I haven't been able to test so I might be completely wrong and forgive me I can't do anything until this weekends at the earliest :'(
Reproduced the issue on my machine, looks like somewhere the parenthesis is not getting escaped (trying to open baz.txt or other.txt):
[Neo-tree ERROR] X:\test\[foo](bar) : ENOENT: no such file or directory: X:\test\[foo](bar)
[Neo-tree ERROR] X:\test\[foo](bar)\qux : ENOENT: no such file or directory: X:\test\[foo](bar)\qux
Using the following test directory:
test
├── [foo]
│ └── (bar)
│ ├── baz.txt
│ └── qux
│ └── other.txt
├── stylua.toml
└── test.lua
Edit: What I find odd though is the escape_path_for_cmd function is correctly escaping the paren (threw in a debug message before returning):
DEBUG: using escaped path `X:\test\[foo]\\(bar)\qux\other.txt`
As a funny side note, nice to know that even established tools like zoxide have the same struggles with Windows paths. Wasn't able to change directory to make the test files...
After doing some testing, it seems specifically the [foo] (square bracket) segment followed by (bar) (parenthesized) segment causes this super odd problem. I can't even directly open the file by typing :e X:\test\[foo]\\(bar)\baz.txt, it's almost like Neovim loses the path separator somewhere else in the pipeline. But this doesn't happen with something like X:\test\\(foo)\\(bar)\baz.txt.
Test files:
test
├── (foo)
│ ├── (bar)
│ │ └── baz.txt <- ok
│ └── foo.txt <- ok
├── [foo]
│ ├── (bar)
│ │ ├── baz.txt <- ERROR
│ │ └── qux
│ │ └── other.txt <- ERROR
│ ├── [bar]
│ │ └── baz.txt <- ok
│ └── some.txt <- ok
I tried using 3 backslashes to open those files that errored and that actually worked, e.g., :e X:\test\[foo]\\\(bar)\qux\other.txt. I'll try some more testing this afternoon to figure out exactly when this problem happens (is it just [...]\(...), or do other weird combinations cause the same effect?).
Just to restate, this seems to be a really specific Windows path problem with Neovim (not Neo-tree) that doesn't follow any of the other established rules about escaping, since I can't even manually open those types of paths without using a triple backslash (or using a / separator, which unfortunately breaks other plugins when we do that).
Thank you so much for your deep dive @bwpge !
With my pathlib.nvim I force all path separators to be forward regardless of the OS when passing to a vim command (eg :edit).
https://github.com/pysan3/pathlib.nvim/blob/main/lua/pathlib/base.lua#L1209-L1220
(self._raw_paths contains the elements of the path: { "folder", "foo.txt" } -> "folder/foo.txt").
Do you think we should rather take this route instead? This won't raise any errors with escaping.
The / separator actually goes back to my original contribution to Neo-tree, unfortunately this causes problems with other plugins (see: https://github.com/nvim-lualine/lualine.nvim/issues/1183 for example).
I know it's not Neo-tree's responsibility to make every other plugin in Neovim happy, but it introduces a really subtle change that is really difficult to pinpoint for users. Essentially, a lot of plugins do their own path parsing and may rely on the platform separator (e.g., package.config:sub(1, 1)), so if you open a file (e.g., :e) using / AND an absolute path (such as X:/foo/bar.txt) it changes the buffer name on Windows. So if a plugin is trying to do some path stuff with the buffer name, it may trip on itself if we use a / instead of \.
Again, not really a Neo-tree problem per se, but it breaks other popular plugins. I've also run into some weird buffer behavior with LSP refactors like renaming a symbol, where it opens a buffer with the same name but different path separators.
On the bright side, this is what I'm testing now and seems to be working for weird paths, including something like X:\[foo]\(bar)\&baz.txt (I found (bar)\\&baz.txt has the same problem with dropping the path separator):
M.escape_path_for_cmd = function(path)
local escaped_path = vim.fn.fnameescape(path)
if M.is_windows then
local evil = "[%(%)%^&;`]"
escaped_path = escaped_path:gsub("\\" .. evil, "\\%1")
escaped_path = escaped_path:gsub("(" .. evil .. ")\\\\(" .. evil .. ")", "%1\\\\\\%2")
-- special case for #1448
escaped_path = escaped_path:gsub("%]\\\\%(", "]\\\\\\(")
vim.notify("DEBUG: using escaped path `" .. escaped_path .. "`")
end
return escaped_path
end
Just wanted to check in, haven't forgotten about this issue just had a crazy schedule this week. Hope to get a PR sent in tonight or tomorrow night.
From some more testing, I've come to the conclusion that a square bracket or backtick in the path makes other problematic punctuation (the ones we discovered like &, ^, etc.) require an extra escape character.
I'm using the following script to verify logic:
local punc = { "()", "[]", "^", "&", ";", "`" }
-- testing implementation for this function
local function escape_path_for_cmd(path)
local escaped_path = vim.fn.fnameescape(path)
if true then
local need_extra_esc = path:find("[%[%]`]")
local esc = need_extra_esc and "\\\\" or "\\"
escaped_path = escaped_path:gsub("\\[%(%)%^&;]", esc .. "%1")
-- backticks get escaped by fnameescape and always need an extra
-- separator if they are the first character in a path segment
escaped_path = escaped_path:gsub("\\\\`", "\\%1")
end
return escaped_path
end
-- makes a value like `[foo]` from part='foo' and p='[]'
local function make_part(part, p)
local open = p:sub(1, 1)
local close = p:sub(2, 2)
if close == "" then
close = open
end
return (open .. part .. close)
end
-- create nested directories with leading/trailing punctuation
local dirs = {}
for _, p1 in ipairs(punc) do
for _, p2 in ipairs(punc) do
local part1 = make_part("foo", p1)
local part2 = make_part("bar", p2)
local path = string.format(".\\tests\\%s\\%s", part1, part2)
vim.fn.mkdir(path, "p")
table.insert(dirs, path)
end
end
-- try to write files with weird path
for _, d in ipairs(dirs) do
local p = escape_path_for_cmd(d .. "\\bar.txt")
local ok, err = pcall(vim.cmd, "silent w " .. p)
if not ok then
print("fail:", p, "->", err)
else
print("wrote:", p)
end
end
This solution above solves the problem for all paths tested:
wrote: .\tests\\(foo)\\(bar)\bar.txt
wrote: .\tests\\\(foo)\[bar]\bar.txt
wrote: .\tests\\(foo)\\^bar^\bar.txt
wrote: .\tests\\(foo)\\&bar&\bar.txt
wrote: .\tests\\(foo)\\;bar;\bar.txt
wrote: .\tests\\\(foo)\\\`bar\`\bar.txt
wrote: .\tests\[foo]\\\(bar)\bar.txt
wrote: .\tests\[foo]\[bar]\bar.txt
wrote: .\tests\[foo]\\\^bar^\bar.txt
wrote: .\tests\[foo]\\\&bar&\bar.txt
wrote: .\tests\[foo]\\\;bar;\bar.txt
wrote: .\tests\[foo]\\\`bar\`\bar.txt
wrote: .\tests\\^foo^\\(bar)\bar.txt
wrote: .\tests\\\^foo^\[bar]\bar.txt
wrote: .\tests\\^foo^\\^bar^\bar.txt
wrote: .\tests\\^foo^\\&bar&\bar.txt
wrote: .\tests\\^foo^\\;bar;\bar.txt
wrote: .\tests\\\^foo^\\\`bar\`\bar.txt
wrote: .\tests\\&foo&\\(bar)\bar.txt
wrote: .\tests\\\&foo&\[bar]\bar.txt
wrote: .\tests\\&foo&\\^bar^\bar.txt
wrote: .\tests\\&foo&\\&bar&\bar.txt
wrote: .\tests\\&foo&\\;bar;\bar.txt
wrote: .\tests\\\&foo&\\\`bar\`\bar.txt
wrote: .\tests\\;foo;\\(bar)\bar.txt
wrote: .\tests\\\;foo;\[bar]\bar.txt
wrote: .\tests\\;foo;\\^bar^\bar.txt
wrote: .\tests\\;foo;\\&bar&\bar.txt
wrote: .\tests\\;foo;\\;bar;\bar.txt
wrote: .\tests\\\;foo;\\\`bar\`\bar.txt
wrote: .\tests\\\`foo\`\\\(bar)\bar.txt
wrote: .\tests\\\`foo\`\[bar]\bar.txt
wrote: .\tests\\\`foo\`\\\^bar^\bar.txt
wrote: .\tests\\\`foo\`\\\&bar&\bar.txt
wrote: .\tests\\\`foo\`\\\;bar;\bar.txt
wrote: .\tests\\\`foo\`\\\`bar\`\bar.txt
I have some things to take care of today but later I'm going to test all the same punctuation above on the beginning filename (I found that this creates some additional problems, so I want to test that separately).