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.
- data/fuzzy_file_finder.gemspec +2 -2
- data/lib/fuzzy_file_finder.rb +99 -82
- metadata +1 -1
data/fuzzy_file_finder.gemspec
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
|
2
|
-
# Gem::Specification for Fuzzy_file_finder-1.0.
|
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.
|
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"]
|
data/lib/fuzzy_file_finder.rb
CHANGED
@@ -37,7 +37,7 @@ class FuzzyFileFinder
|
|
37
37
|
module Version
|
38
38
|
MAJOR = 1
|
39
39
|
MINOR = 0
|
40
|
-
TINY =
|
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
|
72
|
-
|
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
|
79
|
-
attr_reader :
|
80
|
+
class Directory #:nodoc:
|
81
|
+
attr_reader :name
|
80
82
|
|
81
|
-
def initialize(name)
|
82
|
-
@
|
83
|
-
|
83
|
+
def initialize(name, is_root=false)
|
84
|
+
@name = name
|
85
|
+
@is_root = is_root
|
84
86
|
end
|
85
87
|
|
86
|
-
def
|
87
|
-
|
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
|
95
|
-
attr_reader :
|
96
|
+
# The list of files beneath all +roots+
|
97
|
+
attr_reader :files
|
96
98
|
|
97
|
-
# The number of
|
98
|
-
attr_reader :
|
99
|
+
# The maximum number of files beneath all +roots+
|
100
|
+
attr_reader :ceiling
|
99
101
|
|
100
|
-
# The
|
101
|
-
attr_reader :
|
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 +
|
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
|
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
|
-
@
|
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
|
-
|
123
|
-
|
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
|
-
|
177
|
-
|
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
|
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
|
-
#
|
201
|
-
#
|
202
|
-
def follow_tree(
|
203
|
-
Dir.entries(
|
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
|
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
|
-
|
210
|
-
subdir = Directory.new(full)
|
211
|
-
directory.children << subdir
|
212
|
-
follow_tree(full, subdir)
|
220
|
+
follow_tree(Directory.new(full))
|
213
221
|
else
|
214
|
-
|
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
|
-
#
|
275
|
-
#
|
276
|
-
#
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
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
|
-
|
296
|
+
path_matches[path] = { :score => 1, :result => matchable_name }
|
293
297
|
end
|
298
|
+
end
|
294
299
|
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
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
|