henrik-fuzzy_file_finder 1.1.0

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/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
+