henrik-fuzzy_file_finder 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,4 @@
1
+ All code, documentation, and other data distributed with this project
2
+ is released in the PUBLIC DOMAIN, by the author, Jamis Buck. Anyone,
3
+ anywhere, is allowed to use, modify, and/or redistribute any of this
4
+ without restriction.
data/Manifest ADDED
@@ -0,0 +1,4 @@
1
+ lib/fuzzy_file_finder.rb
2
+ LICENSE
3
+ Manifest
4
+ README.rdoc
data/README.rdoc ADDED
@@ -0,0 +1,38 @@
1
+ = FuzzyFileFinder
2
+
3
+ FuzzyFileFinder is a (somewhat improved) implementation of TextMate's "cmd-T" functionality. It allows you to search for a file by specifying a pattern of characters that appear in that file's name. Unlike TextMate, FuzzyFileFinder also lets you match against the file's directory, so you can more easily scope your search.
4
+
5
+ == FEATURES:
6
+
7
+ * Quickly search directory trees for files
8
+ * Avoids accidentally scanning huge directories by implementing a ceiling (default 10,000 entries)
9
+ * Simple highlighting of matches to discover how a pattern matched
10
+
11
+ == SYNOPSIS:
12
+
13
+ In a nutshell:
14
+
15
+ require 'fuzzy_file_finder'
16
+
17
+ finder = FuzzyFileFinder.new # search under current working directory
18
+
19
+ finder.search "app/blogcon" do |match|
20
+ puts "[%5d] %s" % [match[:score] * 10000, match[:highlighted_path]]
21
+ end
22
+
23
+ finder = FuzzyFileFinder.new("/my/project") # search under this directory
24
+
25
+ matches = finder.find("app/blogcon").sort_by { |m| [-m[:score], m[:path] }
26
+ matches.each do |match|
27
+ puts "[%5d] %s" % [match[:score] * 10000, match[:highlighted_path]]
28
+ end
29
+
30
+ See FuzzyFileFinder for more documentation, and links to further information.
31
+
32
+ == INSTALL:
33
+
34
+ * gem install --source=http://gems.github.com jamis-fuzzy_file_finder
35
+
36
+ == LICENSE:
37
+
38
+ All code, documentation, and related materials in this project are released into the PUBLIC DOMAIN. Usage, modification, and distribution are allowed without restriction.
@@ -0,0 +1,33 @@
1
+
2
+ # Gem::Specification for Fuzzy_file_finder-1.1.0
3
+ # Originally generated by Echoe
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = %q{fuzzy_file_finder}
7
+ s.version = "1.1.0"
8
+
9
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
10
+ s.authors = ["Jamis Buck"]
11
+ s.date = %q{2008-10-09}
12
+ s.description = %q{an implementation of TextMate's cmd-T search functionality}
13
+ s.email = %q{jamis@jamisbuck.org}
14
+ s.extra_rdoc_files = ["lib/fuzzy_file_finder.rb", "README.rdoc"]
15
+ s.files = ["lib/fuzzy_file_finder.rb", "LICENSE", "Manifest", "README.rdoc", "fuzzy_file_finder.gemspec"]
16
+ s.has_rdoc = true
17
+ s.homepage = %q{}
18
+ s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Fuzzy_file_finder", "--main", "README.rdoc"]
19
+ s.require_paths = ["lib"]
20
+ s.rubyforge_project = %q{fuzzy_file_finder}
21
+ s.rubygems_version = %q{1.2.0}
22
+ s.summary = %q{an implementation of TextMate's cmd-T search functionality}
23
+
24
+ if s.respond_to? :specification_version then
25
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
26
+ s.specification_version = 2
27
+
28
+ if current_version >= 3 then
29
+ else
30
+ end
31
+ else
32
+ end
33
+ end
@@ -0,0 +1,326 @@
1
+ #--
2
+ # ==================================================================
3
+ # Author: Jamis Buck (jamis@jamisbuck.org)
4
+ # Date: 2008-10-09
5
+ #
6
+ # This file is in the public domain. Usage, modification, and
7
+ # redistribution of this file are unrestricted.
8
+ # ==================================================================
9
+ #++
10
+
11
+ # The "fuzzy" file finder provides a way for searching a directory
12
+ # tree with only a partial name. This is similar to the "cmd-T"
13
+ # feature in TextMate (http://macromates.com).
14
+ #
15
+ # Usage:
16
+ #
17
+ # finder = FuzzyFileFinder.new
18
+ # finder.search("app/blogcon") do |match|
19
+ # puts match[:highlighted_path]
20
+ # end
21
+ #
22
+ # In the above example, all files under the current directory
23
+ # matching "app/blogcon" will be yielded to the block. The given
24
+ # pattern is reduced to a regular expression internally, so that
25
+ # any file that contains those characters in that order (even if
26
+ # there are other characters in between) will match.
27
+ #
28
+ # In other words, "app/blogcon" would match any of the following
29
+ # (parenthesized strings indicate how the match was made):
30
+ #
31
+ # * (app)/controllers/(blog)_(con)troller.rb
32
+ # * lib/c(ap)_(p)ool/(bl)ue_(o)r_(g)reen_(co)loratio(n)
33
+ # * test/(app)/(blog)_(con)troller_test.rb
34
+ #
35
+ # And so forth.
36
+ #
37
+ # To search another directory than the current working directory,
38
+ # pass it to the FuzzyFileFinder initializer:
39
+ #
40
+ # finder = FuzzyFileFinder.new("/my/project")
41
+ #
42
+ class FuzzyFileFinder
43
+ module Version
44
+ MAJOR = 1
45
+ MINOR = 1
46
+ TINY = 0
47
+ STRING = [MAJOR, MINOR, TINY].join(".")
48
+ end
49
+
50
+ # This is the exception that is raised if you try to scan a
51
+ # directory tree with too many entries. By default, a ceiling of
52
+ # 10,000 entries is enforced, but you can change that number via
53
+ # the +ceiling+ parameter to FuzzyFileFinder.new.
54
+ class TooManyEntries < RuntimeError; end
55
+
56
+ # Used internally to represent a run of characters within a
57
+ # match. This is used to build the highlighted version of
58
+ # a file name.
59
+ class CharacterRun < Struct.new(:string, :inside) #:nodoc:
60
+ def to_s
61
+ if inside
62
+ "(#{string})"
63
+ else
64
+ string
65
+ end
66
+ end
67
+ end
68
+
69
+ # Used internally to represent a file within the directory tree.
70
+ class FileSystemEntry #:nodoc:
71
+ attr_reader :name
72
+
73
+ def initialize(name)
74
+ @name = name
75
+ end
76
+
77
+ def directory?
78
+ false
79
+ end
80
+ end
81
+
82
+ # Used internally to represent a subdirectory within the directory
83
+ # tree.
84
+ class Directory < FileSystemEntry
85
+ attr_reader :children
86
+
87
+ def initialize(name)
88
+ @children = []
89
+ super
90
+ end
91
+
92
+ def directory?
93
+ true
94
+ end
95
+ end
96
+
97
+ # The root of the directory tree to search.
98
+ attr_reader :root
99
+
100
+ # The maximum number of files and directories (combined).
101
+ attr_reader :ceiling
102
+
103
+ # The number of directories beneath +root+
104
+ attr_reader :directory_count
105
+
106
+ # The number of files beneath +root+
107
+ attr_reader :file_count
108
+
109
+ # Initializes a new FuzzyFileFinder. This will scan the
110
+ # given +directory+, using +ceiling+ as the maximum number
111
+ # of entries to scan. If there are more than +ceiling+ entries
112
+ # a TooManyEntries exception will be raised.
113
+ def initialize(directory=".", ceiling=10_000)
114
+ # Work with directory as "." so the full path is not searched
115
+ @given_root = directory
116
+ @root = Directory.new('.')
117
+ @ceiling = ceiling
118
+ rescan!
119
+ end
120
+
121
+ # Rescans the subtree. If the directory contents every change,
122
+ # you'll need to call this to force the finder to be aware of
123
+ # the changes.
124
+ def rescan!
125
+ Dir.chdir(@given_root) do
126
+ @abs_root = Dir.pwd
127
+ root.children.clear
128
+ @file_count = 0
129
+ @directory_count = 0
130
+ follow_tree(root.name, root)
131
+ end
132
+ end
133
+
134
+ # Takes the given +pattern+ (which must be a string) and searches
135
+ # all files beneath +root+, yielding each match.
136
+ #
137
+ # +pattern+ is interpreted thus:
138
+ #
139
+ # * "foo" : look for any file with the characters 'f', 'o', and 'o'
140
+ # in its basename (discounting directory names). The characters
141
+ # must be in that order.
142
+ # * "foo/bar" : look for any file with the characters 'b', 'a',
143
+ # and 'r' in its basename (discounting directory names). Also,
144
+ # any successful match must also have at least one directory
145
+ # element matching the characters 'f', 'o', and 'o' (in that
146
+ # order.
147
+ # * "foo/bar/baz" : same as "foo/bar", but matching two
148
+ # directory elements in addition to a file name of "baz".
149
+ #
150
+ # Each yielded match will be a hash containing the following keys:
151
+ #
152
+ # * :absolute_path refers to the full, absolute path to the file
153
+ # * :path refers to the path to the file relative to the search
154
+ # directory
155
+ # * :directory refers to the directory of the file
156
+ # * :name refers to the name of the file (without directory)
157
+ # * :highlighted_directory refers to the directory of the file with
158
+ # matches highlighted in parentheses.
159
+ # * :highlighted_name refers to the name of the file with matches
160
+ # highlighted in parentheses
161
+ # * :highlighted_path refers to the relative path of the file with
162
+ # matches highlighted in parentheses
163
+ # * :abbr refers to an abbreviated form of :highlighted_path, where
164
+ # path segments without matches are compressed to just their first
165
+ # character.
166
+ # * :score refers to a value between 0 and 1 indicating how closely
167
+ # the file matches the given pattern. A score of 1 means the
168
+ # pattern matches the file exactly.
169
+ def search(pattern, &block)
170
+ pattern.strip!
171
+ path_parts = pattern.split("/")
172
+ path_parts.push "" if pattern[-1,1] == "/"
173
+
174
+ file_name_part = path_parts.pop || ""
175
+
176
+ if path_parts.any?
177
+ path_regex_raw = "^(.*?)" + path_parts.map { |part| make_pattern(part) }.join("(.*?/.*?)") + "(.*?)$"
178
+ path_regex = Regexp.new(path_regex_raw, Regexp::IGNORECASE)
179
+ end
180
+
181
+ file_regex_raw = "^(.*?)" << make_pattern(file_name_part) << "(.*)$"
182
+ file_regex = Regexp.new(file_regex_raw, Regexp::IGNORECASE)
183
+
184
+ do_search(path_regex, path_parts.length, file_regex, root, &block)
185
+ end
186
+
187
+ # Takes the given +pattern+ (which must be a string, formatted as
188
+ # described in #search), and returns up to +max+ matches in an
189
+ # Array. If +max+ is nil, all matches will be returned.
190
+ def find(pattern, max=nil)
191
+ results = []
192
+ search(pattern) do |match|
193
+ results << match
194
+ break if max && results.length >= max
195
+ end
196
+ return results
197
+ end
198
+
199
+ # Displays the finder object in a sane, non-explosive manner.
200
+ def inspect #:nodoc:
201
+ "#<%s:0x%x root=%s, files=%d, directories=%d>" % [self.class.name, object_id, root.name.inspect, file_count, directory_count]
202
+ end
203
+
204
+ private
205
+
206
+ # Processes the given +path+ into the given +directory+ object,
207
+ # recursively following subdirectories in a depth-first manner.
208
+ def follow_tree(path, directory)
209
+ Dir.entries(path).each do |entry|
210
+ next if entry[0,1] == "."
211
+ raise TooManyEntries if file_count + directory_count > ceiling
212
+
213
+ full = path == "." ? entry : File.join(path, entry)
214
+ if File.directory?(full)
215
+ @directory_count += 1
216
+ subdir = Directory.new(full)
217
+ directory.children << subdir
218
+ follow_tree(full, subdir)
219
+ else
220
+ @file_count += 1
221
+ directory.children << FileSystemEntry.new(entry)
222
+ end
223
+ end
224
+ end
225
+
226
+ # Takes the given pattern string "foo" and converts it to a new
227
+ # string "(f)([^/]*?)(o)([^/]*?)(o)" that can be used to create
228
+ # a regular expression.
229
+ def make_pattern(pattern)
230
+ pattern = pattern.split(//)
231
+ pattern << "" if pattern.empty?
232
+
233
+ pattern.inject("") do |regex, character|
234
+ regex << "([^/]*?)" if regex.length > 0
235
+ regex << "(" << Regexp.escape(character) << ")"
236
+ end
237
+ end
238
+
239
+ # Given a MatchData object +match+ and a number of "inside"
240
+ # segments to support, compute both the match score and the
241
+ # highlighted match string. The "inside segments" refers to how
242
+ # many patterns were matched in this one match. For a file name,
243
+ # this will always be one. For directories, it will be one for
244
+ # each directory segment in the original pattern.
245
+ def build_match_result(match, inside_segments)
246
+ runs = []
247
+ inside_chars = total_chars = 0
248
+ match.captures.each_with_index do |capture, index|
249
+ if capture.length > 0
250
+ # odd-numbered captures are matches inside the pattern.
251
+ # even-numbered captures are matches between the pattern's elements.
252
+ inside = index % 2 != 0
253
+
254
+ total_chars += capture.gsub(%r(/), "").length # ignore '/' delimiters
255
+ inside_chars += capture.length if inside
256
+
257
+ if runs.last && runs.last.inside == inside
258
+ runs.last.string << capture
259
+ else
260
+ runs << CharacterRun.new(capture, inside)
261
+ end
262
+ end
263
+ end
264
+
265
+ # Determine the score of this match.
266
+ # 1. fewer "inside runs" (runs corresponding to the original pattern)
267
+ # is better.
268
+ # 2. better coverage of the actual path name is better
269
+
270
+ inside_runs = runs.select { |r| r.inside }
271
+ run_ratio = inside_runs.length.zero? ? 1 : inside_segments / inside_runs.length.to_f
272
+
273
+ char_ratio = total_chars.zero? ? 1 : inside_chars.to_f / total_chars
274
+
275
+ score = run_ratio * char_ratio
276
+
277
+ return { :score => score, :result => runs.join }
278
+ end
279
+
280
+ # Do the actual search, recursively. +path_regex+ is either nil,
281
+ # or a regular expression to match against directory names. The
282
+ # +path_segments+ parameter is an integer indicating how many
283
+ # directory segments there were in the original pattern. The
284
+ # +file_regex+ is a regular expression to match against the file
285
+ # name, +under+ is a Directory object to search. Matches are
286
+ # yielded.
287
+ def do_search(path_regex, path_segments, file_regex, under, &block)
288
+ # If a path_regex is present, match the current directory against
289
+ # it and, if there is a match, compute the score and highlighted
290
+ # result.
291
+ path_match = path_regex && under.name.match(path_regex)
292
+
293
+ if path_match
294
+ path_match_result = build_match_result(path_match, path_segments)
295
+ path_match_score = path_match_result[:score]
296
+ path_match_result = path_match_result[:result]
297
+ else
298
+ path_match_score = 1
299
+ end
300
+
301
+ # For each child of the directory, search under subdirectories, or
302
+ # match files.
303
+ under.children.each do |entry|
304
+ full = under == root ? entry.name : File.join(under.name, entry.name)
305
+ if entry.directory?
306
+ do_search(path_regex, path_segments, file_regex, entry, &block)
307
+ elsif (path_regex.nil? || path_match) && file_match = entry.name.match(file_regex)
308
+ match_result = build_match_result(file_match, 1)
309
+ highlighted_directory = path_match_result || under.name
310
+ full_match_result = File.join(highlighted_directory, match_result[:result])
311
+ abbr = File.join(highlighted_directory.gsub(/[^\/]+/) { |m| m.index("(") ? m : m[0,1] }, match_result[:result])
312
+
313
+ result = { :path => full,
314
+ :absolute_path => File.join(@abs_root, full),
315
+ :abbr => abbr,
316
+ :directory => under.name,
317
+ :name => entry.name,
318
+ :highlighted_directory => highlighted_directory,
319
+ :highlighted_name => match_result[:result],
320
+ :highlighted_path => full_match_result,
321
+ :score => path_match_score * match_result[:score] }
322
+ yield result
323
+ end
324
+ end
325
+ end
326
+ end
metadata ADDED
@@ -0,0 +1,63 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: henrik-fuzzy_file_finder
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jamis Buck
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-10-09 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: an implementation of TextMate's cmd-T search functionality
17
+ email: jamis@jamisbuck.org
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - lib/fuzzy_file_finder.rb
24
+ - README.rdoc
25
+ files:
26
+ - lib/fuzzy_file_finder.rb
27
+ - LICENSE
28
+ - Manifest
29
+ - README.rdoc
30
+ - fuzzy_file_finder.gemspec
31
+ has_rdoc: true
32
+ homepage: ""
33
+ post_install_message:
34
+ rdoc_options:
35
+ - --line-numbers
36
+ - --inline-source
37
+ - --title
38
+ - Fuzzy_file_finder
39
+ - --main
40
+ - README.rdoc
41
+ require_paths:
42
+ - lib
43
+ required_ruby_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: "0"
48
+ version:
49
+ required_rubygems_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: "0"
54
+ version:
55
+ requirements: []
56
+
57
+ rubyforge_project: fuzzy_file_finder
58
+ rubygems_version: 1.2.0
59
+ signing_key:
60
+ specification_version: 2
61
+ summary: an implementation of TextMate's cmd-T search functionality
62
+ test_files: []
63
+