Class: FuzzyFileFinder

Inherits:
Object show all
Defined in:
lib/diakonos/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

Classes: CharacterRun, Directory, FileSystemEntry, TooManyEntries

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(params = {}) ⇒ 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.



105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/diakonos/vendor/fuzzy_file_finder.rb', line 105

def initialize( params = {} )
  @ceiling = params[:ceiling] || 10_000
  @ignores = Array(params[:ignores])

  if params[:directories]
    directories = Array(params[:directories])
    directories << "." if directories.empty?
  else
    directories = ['.']
  end

  @recursive = params[:recursive].nil? ? true : params[:recursive]

  # expand any paths with ~
  root_dirnames = directories.map { |d|
    File.realpath(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? ? "" : "/"))
  @sorted = params[:sorted]

  @files = []
  @directories = {}  # To detect link cycles
  @dirs_with_many = []

  rescan!
end

Instance Attribute Details

#ceilingObject (readonly)

The maximum number of files beneath all roots



93
94
95
# File 'lib/diakonos/vendor/fuzzy_file_finder.rb', line 93

def ceiling
  @ceiling
end

#filesObject (readonly)

The list of files beneath all roots



90
91
92
# File 'lib/diakonos/vendor/fuzzy_file_finder.rb', line 90

def files
  @files
end

#ignoresObject (readonly)

The list of glob patterns to ignore.



99
100
101
# File 'lib/diakonos/vendor/fuzzy_file_finder.rb', line 99

def ignores
  @ignores
end

#rootsObject (readonly)

The roots directory trees to search.



87
88
89
# File 'lib/diakonos/vendor/fuzzy_file_finder.rb', line 87

def roots
  @roots
end

#shared_prefixObject (readonly)

The prefix shared by all roots.



96
97
98
# File 'lib/diakonos/vendor/fuzzy_file_finder.rb', line 96

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.



204
205
206
207
208
209
210
211
212
213
214
215
216
217
# File 'lib/diakonos/vendor/fuzzy_file_finder.rb', line 204

def find(pattern, max=nil)
  results = []

  search(pattern) do |match|
    results << match
    break if max && results.length >= max
  end

  if @sorted
    results.sort_by { |m| m[:path] }
  else
    results
  end
end

#inspectObject

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



220
221
222
# File 'lib/diakonos/vendor/fuzzy_file_finder.rb', line 220

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.



140
141
142
143
# File 'lib/diakonos/vendor/fuzzy_file_finder.rb', line 140

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.



178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# File 'lib/diakonos/vendor/fuzzy_file_finder.rb', line 178

def search(pattern, &block)
  path_parts = pattern.strip.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