jamis-fuzzy_file_finder 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,10 +1,10 @@
1
1
 
2
- # Gem::Specification for Fuzzy_file_finder-1.0.1
2
+ # Gem::Specification for Fuzzy_file_finder-1.0.2
3
3
  # Originally generated by Echoe
4
4
 
5
5
  Gem::Specification.new do |s|
6
6
  s.name = %q{fuzzy_file_finder}
7
- s.version = "1.0.1"
7
+ s.version = "1.0.2"
8
8
 
9
9
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
10
10
  s.authors = ["Jamis Buck"]
@@ -37,7 +37,7 @@ class FuzzyFileFinder
37
37
  module Version
38
38
  MAJOR = 1
39
39
  MINOR = 0
40
- TINY = 1
40
+ TINY = 2
41
41
  STRING = [MAJOR, MINOR, TINY].join(".")
42
42
  end
43
43
 
@@ -62,56 +62,64 @@ class FuzzyFileFinder
62
62
 
63
63
  # Used internally to represent a file within the directory tree.
64
64
  class FileSystemEntry #:nodoc:
65
+ attr_reader :parent
65
66
  attr_reader :name
66
67
 
67
- def initialize(name)
68
+ def initialize(parent, name)
69
+ @parent = parent
68
70
  @name = name
69
71
  end
70
72
 
71
- def directory?
72
- false
73
+ def path
74
+ File.join(parent.name, name)
73
75
  end
74
76
  end
75
77
 
76
78
  # Used internally to represent a subdirectory within the directory
77
79
  # tree.
78
- class Directory < FileSystemEntry
79
- attr_reader :children
80
+ class Directory #:nodoc:
81
+ attr_reader :name
80
82
 
81
- def initialize(name)
82
- @children = []
83
- super
83
+ def initialize(name, is_root=false)
84
+ @name = name
85
+ @is_root = is_root
84
86
  end
85
87
 
86
- def directory?
87
- true
88
+ def root?
89
+ is_root
88
90
  end
89
91
  end
90
92
 
91
93
  # The roots directory trees to search.
92
94
  attr_reader :roots
93
95
 
94
- # The maximum number of files and directories (combined).
95
- attr_reader :ceiling
96
+ # The list of files beneath all +roots+
97
+ attr_reader :files
96
98
 
97
- # The number of directories beneath all +roots+
98
- attr_reader :directory_count
99
+ # The maximum number of files beneath all +roots+
100
+ attr_reader :ceiling
99
101
 
100
- # The number of files beneath all +roots+
101
- attr_reader :file_count
102
+ # The prefix shared by all +roots+.
103
+ attr_reader :shared_prefix
102
104
 
103
105
  # Initializes a new FuzzyFileFinder. This will scan the
104
- # given +directory+, using +ceiling+ as the maximum number
106
+ # given +directories+, using +ceiling+ as the maximum number
105
107
  # of entries to scan. If there are more than +ceiling+ entries
106
108
  # a TooManyEntries exception will be raised.
107
109
  def initialize(directories=['.'], ceiling=10_000)
108
- directories = directories.any? ? directories : ['.']
110
+ directories = Array(directories)
111
+ directories << "." if directories.empty?
109
112
 
110
113
  # expand any paths with ~
111
- root_dirnames = directories.map { |d| File.expand_path(d) }.select { |d| File.directory?(d) }
114
+ root_dirnames = directories.map { |d| File.expand_path(d) }.select { |d| File.directory?(d) }.uniq
115
+
116
+ @roots = root_dirnames.map { |d| Directory.new(d, true) }
117
+ @shared_prefix = determine_shared_prefix
118
+ @shared_prefix_re = Regexp.new("^#{Regexp.escape(shared_prefix)}" + (shared_prefix.empty? ? "" : "/"))
112
119
 
113
- @roots = root_dirnames.map { |d| Directory.new(d) }
120
+ @files = []
114
121
  @ceiling = ceiling
122
+
115
123
  rescan!
116
124
  end
117
125
 
@@ -119,10 +127,8 @@ class FuzzyFileFinder
119
127
  # you'll need to call this to force the finder to be aware of
120
128
  # the changes.
121
129
  def rescan!
122
- roots.each { |r| r.children.clear }
123
- @file_count = 0
124
- @directory_count = 0
125
- roots.each { |r| follow_tree(r.name, r) }
130
+ @files.clear
131
+ roots.each { |root| follow_tree(root) }
126
132
  end
127
133
 
128
134
  # Takes the given +pattern+ (which must be a string) and searches
@@ -173,8 +179,12 @@ class FuzzyFileFinder
173
179
  file_regex_raw = "^(.*?)" << make_pattern(file_name_part) << "(.*)$"
174
180
  file_regex = Regexp.new(file_regex_raw, Regexp::IGNORECASE)
175
181
 
176
- roots.each do |root|
177
- do_search(path_regex, path_parts.length, file_regex, root, &block)
182
+ path_matches = {}
183
+ files.each do |file|
184
+ path_match = match_path(file.parent, path_matches, path_regex, path_parts.length)
185
+ next if path_match[:missed]
186
+
187
+ match_file(file, file_regex, path_match, &block)
178
188
  end
179
189
  end
180
190
 
@@ -192,27 +202,24 @@ class FuzzyFileFinder
192
202
 
193
203
  # Displays the finder object in a sane, non-explosive manner.
194
204
  def inspect #:nodoc:
195
- "#<%s:0x%x roots=%s, files=%d, directories=%d>" % [self.class.name, object_id, roots.map { |r| r.name.inspect }.join(", "), file_count, directory_count]
205
+ "#<%s:0x%x roots=%s, files=%d>" % [self.class.name, object_id, roots.map { |r| r.name.inspect }.join(", "), files.length]
196
206
  end
197
207
 
198
208
  private
199
209
 
200
- # Processes the given +path+ into the given +directory+ object,
201
- # recursively following subdirectories in a depth-first manner.
202
- def follow_tree(path, directory)
203
- Dir.entries(path).each do |entry|
210
+ # Recursively scans +directory+ and all files and subdirectories
211
+ # beneath it, depth-first.
212
+ def follow_tree(directory)
213
+ Dir.entries(directory.name).each do |entry|
204
214
  next if entry[0,1] == "."
205
- raise TooManyEntries if file_count + directory_count > ceiling
215
+ raise TooManyEntries if files.length > ceiling
216
+
217
+ full = File.join(directory.name, entry)
206
218
 
207
- full = path == "." ? entry : File.join(path, entry)
208
219
  if File.directory?(full)
209
- @directory_count += 1
210
- subdir = Directory.new(full)
211
- directory.children << subdir
212
- follow_tree(full, subdir)
220
+ follow_tree(Directory.new(full))
213
221
  else
214
- @file_count += 1
215
- directory.children << FileSystemEntry.new(entry)
222
+ files.push(FileSystemEntry.new(directory, entry))
216
223
  end
217
224
  end
218
225
  end
@@ -271,52 +278,62 @@ class FuzzyFileFinder
271
278
  return { :score => score, :result => runs.join }
272
279
  end
273
280
 
274
- # Do the actual search, recursively. +path_regex+ is either nil,
275
- # or a regular expression to match against directory names. The
276
- # +path_segments+ parameter is an integer indicating how many
277
- # directory segments there were in the original pattern. The
278
- # +file_regex+ is a regular expression to match against the file
279
- # name, +under+ is a Directory object to search. Matches are
280
- # yielded.
281
- def do_search(path_regex, path_segments, file_regex, under, &block)
282
- # If a path_regex is present, match the current directory against
283
- # it and, if there is a match, compute the score and highlighted
284
- # result.
285
- path_match = path_regex && under.name.match(path_regex)
286
-
287
- if path_match
288
- path_match_result = build_match_result(path_match, path_segments)
289
- path_match_score = path_match_result[:score]
290
- path_match_result = path_match_result[:result]
281
+ # Match the given path against the regex, caching the result in +path_matches+.
282
+ # If +path+ is already cached in the path_matches cache, just return the cached
283
+ # value.
284
+ def match_path(path, path_matches, path_regex, path_segments)
285
+ return path_matches[path] if path_matches.key?(path)
286
+
287
+ matchable_name = path.name.sub(@shared_prefix_re, "")
288
+
289
+ if path_regex
290
+ match = matchable_name.match(path_regex)
291
+
292
+ path_matches[path] =
293
+ match && build_match_result(match, path_segments) ||
294
+ { :score => 1, :result => matchable_name, :missed => true }
291
295
  else
292
- path_match_score = 1
296
+ path_matches[path] = { :score => 1, :result => matchable_name }
293
297
  end
298
+ end
294
299
 
295
- # determine whether +under+ is one of our root objects or not
296
- is_root = roots.any? { |root| root == under }
297
-
298
- # For each child of the directory, search under subdirectories, or
299
- # match files.
300
- under.children.each do |entry|
301
- full = is_root ? entry.name : File.join(under.name, entry.name)
302
- if entry.directory?
303
- do_search(path_regex, path_segments, file_regex, entry, &block)
304
- elsif (path_regex.nil? || path_match) && file_match = entry.name.match(file_regex)
305
- match_result = build_match_result(file_match, 1)
306
- highlighted_directory = path_match_result || under.name
307
- full_match_result = File.join(highlighted_directory, match_result[:result])
308
- abbr = File.join(highlighted_directory.gsub(/[^\/]+/) { |m| m.index("(") ? m : m[0,1] }, match_result[:result])
309
-
310
- result = { :path => full,
311
- :abbr => abbr,
312
- :directory => under.name,
313
- :name => entry.name,
314
- :highlighted_directory => highlighted_directory,
315
- :highlighted_name => match_result[:result],
316
- :highlighted_path => full_match_result,
317
- :score => path_match_score * match_result[:score] }
318
- yield result
300
+ # Match +file+ against +file_regex+. If it matches, yield the match
301
+ # metadata to the block.
302
+ def match_file(file, file_regex, path_match, &block)
303
+ if file_match = file.name.match(file_regex)
304
+ match_result = build_match_result(file_match, 1)
305
+ full_match_result = File.join(path_match[:result], match_result[:result])
306
+ abbr = File.join(path_match[:result].gsub(/[^\/]+/) { |m| m.index("(") ? m : m[0,1] }, match_result[:result])
307
+
308
+ result = { :path => file.path,
309
+ :abbr => abbr,
310
+ :directory => file.parent.name,
311
+ :name => file.name,
312
+ :highlighted_directory => path_match[:result],
313
+ :highlighted_name => match_result[:result],
314
+ :highlighted_path => full_match_result,
315
+ :score => path_match[:score] * match_result[:score] }
316
+ yield result
317
+ end
318
+ end
319
+
320
+ def determine_shared_prefix
321
+ # the common case: if there is only a single root, then the entire
322
+ # name of the root is the shared prefix.
323
+ return roots.first.name if roots.length == 1
324
+
325
+ split_roots = roots.map { |root| root.name.split(%r{/}) }
326
+ segments = split_roots.map { |root| root.length }.max
327
+ master = split_roots.pop
328
+
329
+ segments.times do |segment|
330
+ if !split_roots.all? { |root| root[segment] == master[segment] }
331
+ return master[0,segment].join("/")
319
332
  end
320
333
  end
334
+
335
+ # shouldn't ever get here, since we uniq the root list before
336
+ # calling this method, but if we do, somehow...
337
+ return roots.first.name
321
338
  end
322
339
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jamis-fuzzy_file_finder
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jamis Buck