Class: FuzzyFileFinder

Inherits:
Object
  • Object
show all
Defined in:
lib/ver/vendor/fuzzy_file_finder.rb

Overview

The “fuzzy” file finder provides a way for searching a directory tree with only a partial name. This is similar to the “cmd-T” feature in TextMate (macromates.com).

Usage:

finder = FuzzyFileFinder.new
finder.search("app/blogcon") do |match|
  puts match[:highlighted_path]
end

In the above example, all files matching “app/blogcon” will be yielded to the block. The given pattern is reduced to a regular expression internally, so that any file that contains those characters in that order (even if there are other characters in between) will match.

In other words, “app/blogcon” would match any of the following (parenthesized strings indicate how the match was made):

  • (app)/controllers/(blog)_(con)troller.rb

  • lib/c(ap)_(p)ool/(bl)ue_(o)r_(g)reen_(co)loratio(n)

  • test/(app)/(blog)_(con)troller_test.rb

And so forth.

Defined Under Namespace

Modules: Version Classes: CharacterRun, Directory, FileSystemEntry, TooManyEntries

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(directories = ['.'], ceiling = 10_000) ⇒ FuzzyFileFinder

Initializes a new FuzzyFileFinder. This will scan the given directories, using ceiling as the maximum number of entries to scan. If there are more than ceiling entries a TooManyEntries exception will be raised.



110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/ver/vendor/fuzzy_file_finder.rb', line 110

def initialize(directories=['.'], ceiling=10_000)
  directories = Array(directories)
  directories << "." if directories.empty?

  # expand any paths with ~
  root_dirnames = directories.map { |d| File.expand_path(d) }.select { |d| File.directory?(d) }.uniq

  @roots = root_dirnames.map { |d| Directory.new(d, true) }
  @shared_prefix = determine_shared_prefix
  @shared_prefix_re = Regexp.new("^#{Regexp.escape(shared_prefix)}" + (shared_prefix.empty? ? "" : "/"))

  @files = []
  @ceiling = ceiling

  rescan!
end

Instance Attribute Details

#ceilingObject (readonly)

The maximum number of files beneath all roots



101
102
103
# File 'lib/ver/vendor/fuzzy_file_finder.rb', line 101

def ceiling
  @ceiling
end

#filesObject (readonly)

The list of files beneath all roots



98
99
100
# File 'lib/ver/vendor/fuzzy_file_finder.rb', line 98

def files
  @files
end

#rootsObject (readonly)

The roots directory trees to search.



95
96
97
# File 'lib/ver/vendor/fuzzy_file_finder.rb', line 95

def roots
  @roots
end

#shared_prefixObject (readonly)

The prefix shared by all roots.



104
105
106
# File 'lib/ver/vendor/fuzzy_file_finder.rb', line 104

def shared_prefix
  @shared_prefix
end

Instance Method Details

#find(pattern, max = nil) ⇒ Object

Takes the given pattern (which must be a string, formatted as described in #search), and returns up to max matches in an Array. If max is nil, all matches will be returned.



195
196
197
198
199
200
201
202
# File 'lib/ver/vendor/fuzzy_file_finder.rb', line 195

def find(pattern, max=nil)
  results = []
  search(pattern) do |match|
    results << match
    break if max && results.length >= max
  end
  return results
end

#inspectObject

Displays the finder object in a sane, non-explosive manner.



205
206
207
# File 'lib/ver/vendor/fuzzy_file_finder.rb', line 205

def inspect #:nodoc:
  "#<%s:0x%x roots=%s, files=%d>" % [self.class.name, object_id, roots.map { |r| r.name.inspect }.join(", "), files.length]
end

#rescan!Object

Rescans the subtree. If the directory contents every change, you’ll need to call this to force the finder to be aware of the changes.



130
131
132
133
# File 'lib/ver/vendor/fuzzy_file_finder.rb', line 130

def rescan!
  @files.clear
  roots.each { |root| follow_tree(root) }
end

#search(pattern, &block) ⇒ Object

Takes the given pattern (which must be a string) and searches all files beneath root, yielding each match.

pattern is interpreted thus:

  • “foo” : look for any file with the characters ‘f’, ‘o’, and ‘o’ in its basename (discounting directory names). The characters must be in that order.

  • “foo/bar” : look for any file with the characters ‘b’, ‘a’, and ‘r’ in its basename (discounting directory names). Also, any successful match must also have at least one directory element matching the characters ‘f’, ‘o’, and ‘o’ (in that order.

  • “foo/bar/baz” : same as “foo/bar”, but matching two directory elements in addition to a file name of “baz”.

Each yielded match will be a hash containing the following keys:

  • :path refers to the full path to the file

  • :directory refers to the directory of the file

  • :name refers to the name of the file (without directory)

  • :highlighted_directory refers to the directory of the file with matches highlighted in parentheses.

  • :highlighted_name refers to the name of the file with matches highlighted in parentheses

  • :highlighted_path refers to the full path of the file with matches highlighted in parentheses

  • :abbr refers to an abbreviated form of :highlighted_path, where path segments without matches are compressed to just their first character.

  • :score refers to a value between 0 and 1 indicating how closely the file matches the given pattern. A score of 1 means the pattern matches the file exactly.



168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
# File 'lib/ver/vendor/fuzzy_file_finder.rb', line 168

def search(pattern, &block)
  pattern.strip!
  path_parts = pattern.split("/")
  path_parts.push "" if pattern[-1,1] == "/"

  file_name_part = path_parts.pop || ""

  if path_parts.any?
    path_regex_raw = "^(.*?)" + path_parts.map { |part| make_pattern(part) }.join("(.*?/.*?)") + "(.*?)$"
    path_regex = Regexp.new(path_regex_raw, Regexp::IGNORECASE)
  end

  file_regex_raw = "^(.*?)" << make_pattern(file_name_part) << "(.*)$"
  file_regex = Regexp.new(file_regex_raw, Regexp::IGNORECASE)

  path_matches = {}
  files.each do |file|
    path_match = match_path(file.parent, path_matches, path_regex, path_parts.length)
    next if path_match[:missed]

    match_file(file, file_regex, path_match, &block)
  end
end