Class: FuzzyFileFinder

Inherits:
Object
  • Object
show all
Defined in:
lib/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, ignores = nil) ⇒ 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.



112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/fuzzy_file_finder.rb', line 112

def initialize(directories=['.'], ceiling=10_000, ignores=nil)
  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

  @ignores = Array(ignores)

  rescan!
end

Instance Attribute Details

#ceilingObject (readonly)

The maximum number of files beneath all roots



100
101
102
# File 'lib/fuzzy_file_finder.rb', line 100

def ceiling
  @ceiling
end

#filesObject (readonly)

The list of files beneath all roots



97
98
99
# File 'lib/fuzzy_file_finder.rb', line 97

def files
  @files
end

#ignoresObject (readonly)

The list of glob patterns to ignore.



106
107
108
# File 'lib/fuzzy_file_finder.rb', line 106

def ignores
  @ignores
end

#rootsObject (readonly)

The roots directory trees to search.



94
95
96
# File 'lib/fuzzy_file_finder.rb', line 94

def roots
  @roots
end

#shared_prefixObject (readonly)

The prefix shared by all roots.



103
104
105
# File 'lib/fuzzy_file_finder.rb', line 103

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.



199
200
201
202
203
204
205
206
# File 'lib/fuzzy_file_finder.rb', line 199

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.



209
210
211
# File 'lib/fuzzy_file_finder.rb', line 209

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.



134
135
136
137
# File 'lib/fuzzy_file_finder.rb', line 134

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.



172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/fuzzy_file_finder.rb', line 172

def search(pattern, &block)
  pattern.gsub!(" ", "")
  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