≡

wincent.dev

  • Products
  • Blog
  • Wiki
  • Issues
You are viewing an historical archive of past issues. Please report new issues to the appropriate project issue tracker on GitHub.
Home » Issues » Bug #1555

Bug #1555: wildignore support has unhandled edge cases

Kind bug
Product Command-T
When Created 2010-05-11T05:47:23Z, updated 2010-05-18T06:24:35Z
Status closed
Reporter anonymous
Tags no tags

Description

wildignore tries to match a filename in a few different ways (see :help autocmd-patterns). Command-t currently matches it using fnmatch and basename(path). This makes it impossible to match against patterns with a dirname. (/foo/*/bar, etc). Here's another solution that I like; rather than emulate vim's behavior, it just uses vim do to the work.

From 141bf2e1b6b61eab49aabff16d8683c10a7e25c0 Mon Sep 17 00:00:00 2001
From: Mike Lundy <mike@fluffypenguin.org>
Date: Tue, 11 May 2010 02:24:53 -0700
Subject: [PATCH] make Scanner.path_excluded use wildignore

fnmatch is a lossy replacement for vim's internal wildcard match. I
reimplemented it by using wildignore directly (via expand) to perform the
exclusion.
---
 ruby/command-t/controller.rb |    3 +--
 ruby/command-t/scanner.rb    |    7 ++-----
 ruby/vim.rb                  |    5 +++++
 3 files changed, 8 insertions(+), 7 deletions(-)

diff --git a/ruby/command-t/controller.rb b/ruby/command-t/controller.rb
index 51277ce..3b93c19 100644
--- a/ruby/command-t/controller.rb
+++ b/ruby/command-t/controller.rb
@@ -148,8 +148,7 @@ module CommandT
         :max_depth              => get_number('g:CommandTMaxDepth'),
         :always_show_dot_files  => get_bool('g:CommandTAlwaysShowDotFiles'),
         :never_show_dot_files   => get_bool('g:CommandTNeverShowDotFiles'),
-        :scan_dot_directories   => get_bool('g:CommandTScanDotDirectories'),
-        :excludes               => get_string('&wildignore')
+        :scan_dot_directories   => get_bool('g:CommandTScanDotDirectories')
     end
 
     def get_number name
diff --git a/ruby/command-t/scanner.rb b/ruby/command-t/scanner.rb
index a48d070..92dd3c8 100644
--- a/ruby/command-t/scanner.rb
+++ b/ruby/command-t/scanner.rb
@@ -31,7 +31,6 @@ module CommandT
       @max_depth            = options[:max_depth] || 15
       @max_files            = options[:max_files] || 10_000
       @scan_dot_directories = options[:scan_dot_directories] || false
-      @excludes             = (options[:excludes] || '*.o,*.obj,.git').split(',')
     end
 
     def paths
@@ -61,16 +60,14 @@ module CommandT
   private
 
     def path_excluded? path
-      @excludes.any? do |pattern|
-        File.fnmatch pattern, path, File::FNM_DOTMATCH
-      end
+      VIM::expand(path).empty?
     end
 
     def add_paths_for_directory dir, accumulator
       Dir.foreach(dir) do |entry|
         next if ['.', '..'].include?(entry)
         path = File.join(dir, entry)
-        unless path_excluded?(entry)
+        unless path_excluded?(path)
           if File.file?(path)
             @files += 1
             raise FileLimitExceeded if @files > @max_files
diff --git a/ruby/vim.rb b/ruby/vim.rb
index 48214eb..9d26464 100644
--- a/ruby/vim.rb
+++ b/ruby/vim.rb
@@ -38,4 +38,9 @@ module VIM
   def self.escape_for_single_quotes str
     str.gsub "'", "''"
   end
+
+  def self.expand fn
+    cmd = 'expand(\'' + VIM::escape_for_single_quotes(fn) + '\')'
+    VIM.evaluate(cmd)
+  end
 end # module VIM
-- 
1.7.1

Comments

  1. Greg Hurrell 2010-05-11T06:01:06Z

    Any idea of what the performance impact of this is on really large directory hierarchies? (ie. crank up the scan depth and max file limit and then see how it performs on an enormous hierarchy.)

  2. Greg Hurrell 2010-05-11T12:37:36Z

    I've had a bit of a play around with this and I like the basic idea of it. It seems to handle most of the usage scenarios mentioned in ticket #1542:

    • existing simple file globs like *.o and .git
    • patterns with leading **

    Notably it doesn't handle patterns of the form vendor/rails/** like the person who posted that ticket was originally requesting. This is because internally, we're working with absolute paths at that point and /absolute/path/to/vendor/rails/foo is not considered by VIM as a match. Instead, you would need to specify a pattern like **/vendor/rails/**.

    One change required, however. It appears that on at least some versions of VIM the Ruby support doesn't know how to turn "NULL" results into an empty string, which means that you'll get a "NULL pointer given" exception raised every time you test an excluded path. From Googling I see that this isn't something that happens to affect just the platform I'm using (MacVim).

    So this is the tweaked version I'm testing, which uses empty(exclude('...')) rather than just a naked exclude('...'):

    diff --git a/ruby/command-t/controller.rb b/ruby/command-t/controller.rb
    index 51277ce..3b93c19 100644
    --- a/ruby/command-t/controller.rb
    +++ b/ruby/command-t/controller.rb
    @@ -148,8 +148,7 @@ module CommandT
             :max_depth              => get_number('g:CommandTMaxDepth'),
             :always_show_dot_files  => get_bool('g:CommandTAlwaysShowDotFiles'),
             :never_show_dot_files   => get_bool('g:CommandTNeverShowDotFiles'),
    -        :scan_dot_directories   => get_bool('g:CommandTScanDotDirectories'),
    -        :excludes               => get_string('&wildignore')
    +        :scan_dot_directories   => get_bool('g:CommandTScanDotDirectories')
         end
     
         def get_number name
    diff --git a/ruby/command-t/scanner.rb b/ruby/command-t/scanner.rb
    index a48d070..c773d72 100644
    --- a/ruby/command-t/scanner.rb
    +++ b/ruby/command-t/scanner.rb
    @@ -31,7 +31,6 @@ module CommandT
           @max_depth            = options[:max_depth] || 15
           @max_files            = options[:max_files] || 10_000
           @scan_dot_directories = options[:scan_dot_directories] || false
    -      @excludes             = (options[:excludes] || '*.o,*.obj,.git').split(',')
         end
     
         def paths
    @@ -61,16 +60,15 @@ module CommandT
       private
     
         def path_excluded? path
    -      @excludes.any? do |pattern|
    -        File.fnmatch pattern, path, File::FNM_DOTMATCH
    -      end
    +      path = Vim.escape_for_single_quotes path
    +      VIM.evaluate("empty(expand('#{path}'))").to_i == 1
         end
     
         def add_paths_for_directory dir, accumulator
           Dir.foreach(dir) do |entry|
             next if ['.', '..'].include?(entry)
             path = File.join(dir, entry)
    -        unless path_excluded?(entry)
    +        unless path_excluded?(path)
               if File.file?(path)
                 @files += 1
                 raise FileLimitExceeded if @files > @max_files
  3. Greg Hurrell 2010-05-11T16:12:08Z

    And here's my entry into the commit log essay contest:

    commit bbf296b775d5d9220fae28a34111117312bfcaa7
    Author: Greg Hurrell <greg@hurrell.net>
    Date:   Tue May 11 21:56:57 2010 +0200
    
        Bring wildignore behavior closer to VIM's own behaviour
        
        In commit f314c1e6 we tried to make Command-T react to the 'wildignore'
        setting in way that more closely matches VIM's own behaviour by
        using the builtin expand() function which takes into account
        'wildignore'.
        
        While this works quite well, there are still some discrepancies from
        VIM's actual behaviour.
        
        For example, while patterns like this work as expected with no
        surprises:
        
          *.o
          .git
          **/build
        
        A pattern like this:
        
          vendor/rails/**
        
        Will work differently in Command-T than it does in the rest of VIM.
        Specifically, if I type this in VIM:
        
          :e vendor/rails/<tab>
        
        Then VIM's autocomplete won't allow me to drill down into the directory
        because it is excluded by the 'wildignore' setting.
        
        Command-T, on the other hand, will allow me to see the contents of that
        directory. This is because internally, at the time the file names are
        checked, they are almost always absolute paths, because the default
        starting directory is ":pwd" which is itself provided by VIM as an
        absolute path. As such, VIM's expand() function checks to see if:
        
          /absolute/path/to/vendor/rails/
        
        Matches against the 'wildignore' pattern of:
        
          vendor/rails/**
        
        And decides that it does not match.
        
        As a work around, a user could specify a pattern like this:
        
          **/vendor/rails/**
        
        But it is a bit ugly because it doesn't fit well with the behaviour of
        VIM itself.
        
        The fix, then, is to not pass absolute paths into the expand() function,
        but to pass paths relative to the starting directory.
        
        In 99% of cases, the starting directory is the ":pwd", so the behaviour
        should then be identical to VIM's own behaviour.
        
        In the cases where the user has passed in an override for the starting
        directory (either relative or absolute), then the behaviour will diverge
        slightly from VIM's while still hopefully being consistent and intuitive
        from the user's perspective.
        
        For example, if :pwd is the HOME directory and the user invokes
        Command-T with:
        
          :CommandT path/to/some/rails/project
        
        Then a 'wildignore' which includes this pattern:
        
          vendor/rails/**
        
        Will exclude all files under:
        
          path/to/some/rails/prokect/vendor/rails/
        
        Which is probably what the user expects. For comparison, typing:
        
          :e path/to/some/rails/project/vendor/rails/<tab>
        
        Will autocomplete despite the 'wildignore' settings, which is consistent
        with the standard VIM behaviour as described previously.
        
        Signed-off-by: Greg Hurrell <greg@hurrell.net>
    
    diff --git a/ruby/command-t/scanner.rb b/ruby/command-t/scanner.rb
    index c773d72..1868e95 100644
    --- a/ruby/command-t/scanner.rb
    +++ b/ruby/command-t/scanner.rb
    @@ -21,13 +21,15 @@
     # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
     # POSSIBILITY OF SUCH DAMAGE.
     
    +require 'pathname'
    +
     module CommandT
       # Reads the current directory recursively for the paths to all regular files.
       class Scanner
         class FileLimitExceeded < ::RuntimeError; end
     
         def initialize path = Dir.pwd, options = {}
    -      @path                 = path
    +      @path                 = path ? Pathname.new(path) : nil
           @max_depth            = options[:max_depth] || 15
           @max_files            = options[:max_files] || 10_000
           @scan_dot_directories = options[:scan_dot_directories] || false
    @@ -39,7 +41,7 @@ module CommandT
             @paths = []
             @depth = 0
             @files = 0
    -        @prefix_len = @path.chomp('/').length
    +        @prefix_len = @path.to_s.chomp('/').length
             add_paths_for_directory @path, @paths
           rescue FileLimitExceeded
           end
    @@ -51,8 +53,9 @@ module CommandT
         end
     
         def path= str
    -      if @path != str
    -        @path = str
    +      pathname = str ? Pathname.new(str) : nil
    +      if @path != pathname
    +        @path = pathname
             flush
           end
         end
    @@ -60,6 +63,7 @@ module CommandT
       private
     
         def path_excluded? path
    +      path = path.relative_path_from(@path).to_s
           path = Vim.escape_for_single_quotes path
           VIM.evaluate("empty(expand('#{path}'))").to_i == 1
         end
    @@ -67,13 +71,13 @@ module CommandT
         def add_paths_for_directory dir, accumulator
           Dir.foreach(dir) do |entry|
             next if ['.', '..'].include?(entry)
    -        path = File.join(dir, entry)
    +        path = dir + entry
             unless path_excluded?(path)
    -          if File.file?(path)
    +          if path.file?
                 @files += 1
                 raise FileLimitExceeded if @files > @max_files
    -            accumulator << path[@prefix_len + 1..-1]
    -          elsif File.directory?(path)
    +            accumulator << path.to_s[@prefix_len + 1..-1]
    +          elsif path.directory?
                 next if @depth >= @max_depth
                 next if (entry.match(/\A\./) && !@scan_dot_directories)
                 @depth += 1
  4. anonymous 2010-05-11T20:18:48Z

    I'm going to rethink this a bit (going through VIM:: makes it rather hard to test).

  5. anonymous 2010-05-11T20:20:35Z

    oops, ignore my last post (and this one). I forgot to reload the page before I posted it.

  6. Greg Hurrell 2010-05-12T10:18:55Z

    Performance of the Pathname-based code is a bit sucky, based on my subjective testing. Gonna try replacing it with something simpler.

  7. Greg Hurrell 2010-05-12T12:13:42Z

    This replaces the Pathname-based code with a much faster "inline" alternative:

    diff --git a/ruby/command-t/scanner.rb b/ruby/command-t/scanner.rb
    index 1868e95..c14668d 100644
    --- a/ruby/command-t/scanner.rb
    +++ b/ruby/command-t/scanner.rb
    @@ -21,15 +21,13 @@
     # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
     # POSSIBILITY OF SUCH DAMAGE.
     
    -require 'pathname'
    -
     module CommandT
       # Reads the current directory recursively for the paths to all regular files.
       class Scanner
         class FileLimitExceeded < ::RuntimeError; end
     
         def initialize path = Dir.pwd, options = {}
    -      @path                 = path ? Pathname.new(path) : nil
    +      @path                 = path
           @max_depth            = options[:max_depth] || 15
           @max_files            = options[:max_files] || 10_000
           @scan_dot_directories = options[:scan_dot_directories] || false
    @@ -41,7 +39,7 @@ module CommandT
             @paths = []
             @depth = 0
             @files = 0
    -        @prefix_len = @path.to_s.chomp('/').length
    +        @prefix_len = @path.chomp('/').length
             add_paths_for_directory @path, @paths
           rescue FileLimitExceeded
           end
    @@ -53,9 +51,8 @@ module CommandT
         end
     
         def path= str
    -      pathname = str ? Pathname.new(str) : nil
    -      if @path != pathname
    -        @path = pathname
    +      if @path != str
    +        @path = str
             flush
           end
         end
    @@ -63,7 +60,8 @@ module CommandT
       private
     
         def path_excluded? path
    -      path = path.relative_path_from(@path).to_s
    +      # first strip common prefix (@path) from path
    +      path = path[(@prefix_len + 1)..-1]
           path = Vim.escape_for_single_quotes path
           VIM.evaluate("empty(expand('#{path}'))").to_i == 1
         end
    @@ -71,13 +69,13 @@ module CommandT
         def add_paths_for_directory dir, accumulator
           Dir.foreach(dir) do |entry|
             next if ['.', '..'].include?(entry)
    -        path = dir + entry
    +        path = File.join(dir, entry)
             unless path_excluded?(path)
    -          if path.file?
    +          if File.file?(path)
                 @files += 1
                 raise FileLimitExceeded if @files > @max_files
    -            accumulator << path.to_s[@prefix_len + 1..-1]
    -          elsif path.directory?
    +            accumulator << path[@prefix_len + 1..-1]
    +          elsif File.directory?(path)
                 next if @depth >= @max_depth
                 next if (entry.match(/\A\./) && !@scan_dot_directories)
                 @depth += 1
  8. Greg Hurrell 2010-05-12T14:36:43Z

    Ok, I've now done enough testing of this that it's at the point where I feel ok with pushing it out.

    Still not at the point where I feel comfortable cutting a release, but at least it's out there in the public eye now.

  9. Greg Hurrell 2010-05-18T06:24:35Z

    Status changed:

    • From: new
    • To: closed
Add a comment

Comments are now closed for this issue.

  • contact
  • legal

Menu

  • Blog
  • Wiki
  • Issues
  • Snippets