nerdtree icon indicating copy to clipboard operation
nerdtree copied to clipboard

[Windows] Node path could have different case if the initial directory is entered in all lower case

Open char101 opened this issue 3 years ago • 10 comments

Self-Diagnosis

  • [x] I have searched the issues for an answer to my question.
  • [ ] I have reviewed the NERDTree documentation. :h NERDTree
  • [ ] I have reviewed the Wiki.
  • [ ] I have searched the web for an answer to my question.

Steps to Reproduce the Issue

  1. Edit a directory using lower case drive name (e.g. tab edit c:\dir)
  2. Try to open a file or directory two level below the initial directory

Current Result (Include screenshots where appropriate.)

Some file and/or directory cannot be opened depending on whether the node path is stored with lower case drive name or upper case drive name.

Expected Result

File and or directory can be opened.

The Cause

  • The initial glob of path names uses the directory name as entered by the user (c:\dir), this results in path names starting with lower case drive name (or whatever case the user entered since path names are case insensitive on Windows)
  • The glob for file names under the subdirectories of the initial directory is done using relative path to cwd (which have been set to c:\dir, but getcwd() return its properly cased name C:\dir), thus when joined it results in different case C:\dir\file
  • This causes mismatch between the stored path names inside the tree nodes.

This is one example if I put call confirm('skipped '.a:path.str().' | '.self.path.str()) in tree_dir_node.vim#L125.

2021-04-25 15_56_34-Window

Although a:path should be under self.path, it is skipped because the drive name cases are different. NERDTree was opened by running tabed e:\projects\pip.

char101 avatar Apr 25 '21 08:04 char101

Temporary fix

diff --git a/lib/nerdtree/creator.vim b/lib/nerdtree/creator.vim
index b9d45dc..53fbc9a 100644
--- a/lib/nerdtree/creator.vim
+++ b/lib/nerdtree/creator.vim
@@ -76,7 +76,8 @@ endfunction
 " FUNCTION: s:Creator.CreateWindowTree(dir) {{{1
 function! s:Creator.CreateWindowTree(dir)
     let creator = s:Creator.New()
-    call creator.createWindowTree(a:dir)
+    exec 'cd '.a:dir
+    call creator.createWindowTree(getcwd())
 endfunction

 " FUNCTION: s:Creator.createWindowTree(dir) {{{1

char101 avatar Apr 25 '21 09:04 char101

@char101, I'm not seeing the error you describe. Putting your call confirm(...) statement in, I'm getting matching case, whether I use :tabe c:\projects or :tabe C:\projects, and directories and files open as expected. Perhaps we have some relevant differences in our Vim or NERDTree settings. Please provide a minimal .vimrc file that will produce the error. Thanks.

PhilRunninger avatar Apr 29 '21 13:04 PhilRunninger

Did you open a file or directory two level under the initial directory?

To demonstrate the problem you also can run echo glob("c:\projects\*') the results should have lowercase prefix. That itself is a cause for case comparison mismatch. Because like I wrote before, the initial file list is retrieved using glob() but for subsequent directory expansion the list is retrieved using the combination of getcwd() and relative path.

I can't find any relevan vim settings that affects the glob result case.

char101 avatar Apr 29 '21 13:04 char101

I tried it again and it does not always happen. This is a sample directory structure where the file cannot be opened:

C:\ (or any drive)
  projects\
    pip\
      test\
        a.txt

Load nerdtree using ed c:\projects\pip (without ending slash). Then try to edit a.txt.

2021-04-29 21_00_33-Window

char101 avatar Apr 29 '21 13:04 char101

It seems like this behavior is affected by autochdir.

If I don't set autochdir the problem does not happen.

If I set autochdir, and run ed c:\projects\pip, then cwd is set to C:\projects and s:TreeDirNode._glob uses relative path.

If I set autochdir, and run ed c:\projects\pip\, then cwd is set to C:\projects\pip but the problem does not happen.

char101 avatar Apr 29 '21 14:04 char101

So like I wrote, this is caused by s:TreeDirNode._glob which uses absolute or relative path depending on whether the path is under cwd or not. And that causes case mismatch, because glob('c\projects\pip\*') != getcwd().glob('pip\*') if cwd is c:\projects.

char101 avatar Apr 29 '21 14:04 char101

The problem can also be fixed by not trying to make glob return relative path

diff --git a/lib/nerdtree/tree_dir_node.vim b/lib/nerdtree/tree_dir_node.vim
index f5f7682..3b1823c 100644
--- a/lib/nerdtree/tree_dir_node.vim
+++ b/lib/nerdtree/tree_dir_node.vim
@@ -273,16 +273,18 @@ function! s:TreeDirNode._glob(pattern, all)

     " Construct a path specification such that globpath() will return
     " relative pathnames, if possible.
-    if self.path.str() ==# getcwd()
-        let l:pathSpec = ','
-    else
-        let l:pathSpec = escape(fnamemodify(self.path.str({'format': 'Glob'}), ':.'), ',')
-
-        " On Windows, the drive letter may be removed by "fnamemodify()".
-        if nerdtree#runningWindows() && l:pathSpec[0] == nerdtree#slash()
-            let l:pathSpec = self.path.drive . l:pathSpec
-        endif
-    endif
+    " if self.path.str() ==# getcwd()
+    "     let l:pathSpec = ','
+    " else
+    "     let l:pathSpec = escape(fnamemodify(self.path.str({'format': 'Glob'}), ':.'), ',')
+    "
+    "     " On Windows, the drive letter may be removed by "fnamemodify()".
+    "     if nerdtree#runningWindows() && l:pathSpec[0] == nerdtree#slash()
+    "         let l:pathSpec = self.path.drive . l:pathSpec
+    "     endif
+    " endif
+
+    let l:pathSpec = self.path.str({'format': 'Glob'})

     let l:globList = []

char101 avatar Apr 29 '21 14:04 char101

This comparison in function! s:TreeDirNode._glob(pattern, all) could also be problematic if self.path.str() is lowercase.

if self.path.str() ==# getcwd()

char101 avatar Apr 29 '21 14:04 char101

Which version of Vim are you using? I'm on Neovim, and I don't see any of the issues you're describing. Maybe it's been fixed in Neovim.

I don't want to abandon the relative paths because I believe they're needed for g:NERDTreeRespectWildIgnore to work. Try the change below and see if it solves the issue. It ignores the case of the first character (a drive letter or /), but respects everything else. Then again, what happens if you try something like this, where the rest of the path differs in case from what was created?

C:\>mkdir foobar
C:\>mkdir foobar\SNAFU
C:\>mkdir foobar\SNAFU\boHIca
C:\>mkdir foobar\SNAFU\TarFu
C:\>dir > foobar\SNAFU\boHIca\dir.txt
C:\>vim c:\FOObar
diff --git a/lib/nerdtree/tree_dir_node.vim b/lib/nerdtree/tree_dir_node.vim
index f5f7682..de117ac 100644
--- a/lib/nerdtree/tree_dir_node.vim
+++ b/lib/nerdtree/tree_dir_node.vim
@@ -273,7 +273,8 @@ function! s:TreeDirNode._glob(pattern, all)

     " Construct a path specification such that globpath() will return
     " relative pathnames, if possible.
-    if self.path.str() ==# getcwd()
+    if self.path.str()[0] ==? getcwd()[0] && self.path.str()[1:] ==# getcwd()[1:]
         let l:pathSpec = ','
     else
         let l:pathSpec = escape(fnamemodify(self.path.str({'format': 'Glob'}), ':.'), ',')

PhilRunninger avatar May 07 '21 18:05 PhilRunninger

I am using GVim 8.2.2825.

I feel that you don't get the idea of what I was trying to explain here.

The problem is not with using relative path (although the relative path detection does not always work, based on whether that path ends with \ or not, as I explained above).

The idea is:

If you have a directory and files in all uppercase:

C:\DIRECTORY
  A.TXT
  B.TXT
  SUBDIRECTORY\
    AA.TXT
    BB.TXT

And you run this in vim: echo glob('c:\directory\*') you get the output

c:\directory\A.TXT
c:\directory\B.TXT
c:\directory\SUBDIRECTORY

I also tried this in neovim, the output is the same.

Now if I browse to C:\DIRECTORY\SUBDIRECTORY in nerdtree, it uses relative globbing combined with getcwd() by cd-ing to C:\DIRECTORY\SUBDIRECTORY first.

Now what is the value of getcwd(): C:\DIRECTORY\SUBDIRECTORY

What is the value of glob('*')

AA.TXT
BB.TXT

What is the value when the paths as combined

C;\DIRECTORY\SUBDIRECTORY\AA.TXT
C:\DIRECTORY\SUBDIRECTORY\BB.TXT

What is the path value of its parent node?

c:\directory\SUBDIRECTORY

And now when trying to open C:\DIRECTORY\SUBDIRECTORY\AA.TXT, it checks whether C:\DIRECTORY\SUBDIRECTORY\AA.TXT is under c:\directory\SUBDIRECTORY which should be TRUE but instead it is FALSE because the case differs.

The solution is not to abandon relative path if relative path is required. You need to normalize the initial directory case to its real case. One way is by cd-ing to the directory and then retrieving the directory name using getcwd().

char101 avatar May 08 '21 02:05 char101

I am using GVim 8.2.2825.

I feel that you don't get the idea of what I was trying to explain here.

The problem is not with using relative path (although the relative path detection does not always work, based on whether that path ends with \ or not, as I explained above).

The idea is:

If you have a directory and files in all uppercase:

C:\DIRECTORY
  A.TXT
  B.TXT
  SUBDIRECTORY\
    AA.TXT
    BB.TXT

And you run this in vim: echo glob('c:\directory\*') you get the output

c:\directory\A.TXT
c:\directory\B.TXT
c:\directory\SUBDIRECTORY

I also tried this in neovim, the output is the same.

Now if I browse to C:\DIRECTORY\SUBDIRECTORY in nerdtree, it uses relative globbing combined with getcwd() by cd-ing to C:\DIRECTORY\SUBDIRECTORY first.

Now what is the value of getcwd(): C:\DIRECTORY\SUBDIRECTORY

What is the value of glob('*')

AA.TXT
BB.TXT

What is the value when the paths as combined

C;\DIRECTORY\SUBDIRECTORY\AA.TXT
C:\DIRECTORY\SUBDIRECTORY\BB.TXT

What is the path value of its parent node?

c:\directory\SUBDIRECTORY

And now when trying to open C:\DIRECTORY\SUBDIRECTORY\AA.TXT, it checks whether C:\DIRECTORY\SUBDIRECTORY\AA.TXT is under c:\directory\SUBDIRECTORY which should be TRUE but instead it is FALSE because the case differs.

The solution is not to abandon relative path if relative path is required. You need to normalize the initial directory case to its real case. One way is by cd-ing to the directory and then retrieving the directory name using getcwd().

I've checked this example with the latest version of NERDTree, It seems that it has been fixed after the #1387 PR.

rzvxa avatar Feb 05 '24 18:02 rzvxa