diakonos 0.8.6 → 0.8.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -12,7 +12,7 @@ class TextMark
12
12
  end
13
13
 
14
14
  def to_s
15
- return "(#{start_row},#{start_col})-(#{end_row},#{end_col}) #{formatting}"
15
+ "(#{start_row},#{start_col})-(#{end_row},#{end_col}) #{formatting}"
16
16
  end
17
17
  end
18
18
 
@@ -0,0 +1,353 @@
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 matching "app/blogcon" will be
23
+ # yielded to the block. The given pattern is reduced to a regular
24
+ # expression internally, so that any file that contains those
25
+ # characters in that order (even if there are other characters
26
+ # 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
+ class FuzzyFileFinder
37
+ module Version
38
+ MAJOR = 1
39
+ MINOR = 0
40
+ TINY = 4
41
+ STRING = [MAJOR, MINOR, TINY].join(".")
42
+ end
43
+
44
+ # This is the exception that is raised if you try to scan a
45
+ # directory tree with too many entries. By default, a ceiling of
46
+ # 10,000 entries is enforced, but you can change that number via
47
+ # the +ceiling+ parameter to FuzzyFileFinder.new.
48
+ class TooManyEntries < RuntimeError; end
49
+
50
+ # Used internally to represent a run of characters within a
51
+ # match. This is used to build the highlighted version of
52
+ # a file name.
53
+ class CharacterRun < Struct.new(:string, :inside) #:nodoc:
54
+ def to_s
55
+ if inside
56
+ "(#{string})"
57
+ else
58
+ string
59
+ end
60
+ end
61
+ end
62
+
63
+ # Used internally to represent a file within the directory tree.
64
+ class FileSystemEntry #:nodoc:
65
+ attr_reader :parent
66
+ attr_reader :name
67
+
68
+ def initialize(parent, name)
69
+ @parent = parent
70
+ @name = name
71
+ end
72
+
73
+ def path
74
+ File.join(parent.name, name)
75
+ end
76
+ end
77
+
78
+ # Used internally to represent a subdirectory within the directory
79
+ # tree.
80
+ class Directory #:nodoc:
81
+ attr_reader :name
82
+
83
+ def initialize(name, is_root=false)
84
+ @name = name
85
+ @is_root = is_root
86
+ end
87
+
88
+ def root?
89
+ is_root
90
+ end
91
+ end
92
+
93
+ # The roots directory trees to search.
94
+ attr_reader :roots
95
+
96
+ # The list of files beneath all +roots+
97
+ attr_reader :files
98
+
99
+ # The maximum number of files beneath all +roots+
100
+ attr_reader :ceiling
101
+
102
+ # The prefix shared by all +roots+.
103
+ attr_reader :shared_prefix
104
+
105
+ # The list of glob patterns to ignore.
106
+ attr_reader :ignores
107
+
108
+ # Initializes a new FuzzyFileFinder. This will scan the
109
+ # given +directories+, using +ceiling+ as the maximum number
110
+ # of entries to scan. If there are more than +ceiling+ entries
111
+ # a TooManyEntries exception will be raised.
112
+ def initialize(directories=['.'], ceiling=10_000, ignores=nil)
113
+ directories = Array(directories)
114
+ directories << "." if directories.empty?
115
+
116
+ # expand any paths with ~
117
+ root_dirnames = directories.map { |d| File.expand_path(d) }.select { |d| File.directory?(d) }.uniq
118
+
119
+ @roots = root_dirnames.map { |d| Directory.new(d, true) }
120
+ @shared_prefix = determine_shared_prefix
121
+ @shared_prefix_re = Regexp.new("^#{Regexp.escape(shared_prefix)}" + (shared_prefix.empty? ? "" : "/"))
122
+
123
+ @files = []
124
+ @ceiling = ceiling
125
+
126
+ @ignores = Array(ignores)
127
+
128
+ rescan!
129
+ end
130
+
131
+ # Rescans the subtree. If the directory contents every change,
132
+ # you'll need to call this to force the finder to be aware of
133
+ # the changes.
134
+ def rescan!
135
+ @files.clear
136
+ roots.each { |root| follow_tree(root) }
137
+ end
138
+
139
+ # Takes the given +pattern+ (which must be a string) and searches
140
+ # all files beneath +root+, yielding each match.
141
+ #
142
+ # +pattern+ is interpreted thus:
143
+ #
144
+ # * "foo" : look for any file with the characters 'f', 'o', and 'o'
145
+ # in its basename (discounting directory names). The characters
146
+ # must be in that order.
147
+ # * "foo/bar" : look for any file with the characters 'b', 'a',
148
+ # and 'r' in its basename (discounting directory names). Also,
149
+ # any successful match must also have at least one directory
150
+ # element matching the characters 'f', 'o', and 'o' (in that
151
+ # order.
152
+ # * "foo/bar/baz" : same as "foo/bar", but matching two
153
+ # directory elements in addition to a file name of "baz".
154
+ #
155
+ # Each yielded match will be a hash containing the following keys:
156
+ #
157
+ # * :path refers to the full path to the file
158
+ # * :directory refers to the directory of the file
159
+ # * :name refers to the name of the file (without directory)
160
+ # * :highlighted_directory refers to the directory of the file with
161
+ # matches highlighted in parentheses.
162
+ # * :highlighted_name refers to the name of the file with matches
163
+ # highlighted in parentheses
164
+ # * :highlighted_path refers to the full path of the file with
165
+ # matches highlighted in parentheses
166
+ # * :abbr refers to an abbreviated form of :highlighted_path, where
167
+ # path segments without matches are compressed to just their first
168
+ # character.
169
+ # * :score refers to a value between 0 and 1 indicating how closely
170
+ # the file matches the given pattern. A score of 1 means the
171
+ # pattern matches the file exactly.
172
+ def search(pattern, &block)
173
+ pattern.strip!
174
+ path_parts = pattern.split("/")
175
+ path_parts.push "" if pattern[-1,1] == "/"
176
+
177
+ file_name_part = path_parts.pop || ""
178
+
179
+ if path_parts.any?
180
+ path_regex_raw = "^(.*?)" + path_parts.map { |part| make_pattern(part) }.join("(.*?/.*?)") + "(.*?)$"
181
+ path_regex = Regexp.new(path_regex_raw, Regexp::IGNORECASE)
182
+ end
183
+
184
+ file_regex_raw = "^(.*?)" << make_pattern(file_name_part) << "(.*)$"
185
+ file_regex = Regexp.new(file_regex_raw, Regexp::IGNORECASE)
186
+
187
+ path_matches = {}
188
+ files.each do |file|
189
+ path_match = match_path(file.parent, path_matches, path_regex, path_parts.length)
190
+ next if path_match[:missed]
191
+
192
+ match_file(file, file_regex, path_match, &block)
193
+ end
194
+ end
195
+
196
+ # Takes the given +pattern+ (which must be a string, formatted as
197
+ # described in #search), and returns up to +max+ matches in an
198
+ # Array. If +max+ is nil, all matches will be returned.
199
+ def find(pattern, max=nil)
200
+ results = []
201
+ search(pattern) do |match|
202
+ results << match
203
+ break if max && results.length >= max
204
+ end
205
+ return results
206
+ end
207
+
208
+ # Displays the finder object in a sane, non-explosive manner.
209
+ def inspect #:nodoc:
210
+ "#<%s:0x%x roots=%s, files=%d>" % [self.class.name, object_id, roots.map { |r| r.name.inspect }.join(", "), files.length]
211
+ end
212
+
213
+ private
214
+
215
+ # Recursively scans +directory+ and all files and subdirectories
216
+ # beneath it, depth-first.
217
+ def follow_tree(directory)
218
+ Dir.entries(directory.name).each do |entry|
219
+ next if entry[0,1] == "."
220
+ raise TooManyEntries if files.length > ceiling
221
+
222
+ full = File.join(directory.name, entry)
223
+
224
+ if File.directory?(full)
225
+ follow_tree(Directory.new(full))
226
+ elsif !ignore?(full.sub(@shared_prefix_re, ""))
227
+ files.push(FileSystemEntry.new(directory, entry))
228
+ end
229
+ end
230
+ end
231
+
232
+ # Returns +true+ if the given name matches any of the ignore
233
+ # patterns.
234
+ def ignore?(name)
235
+ ignores.any? { |pattern| File.fnmatch(pattern, name) }
236
+ end
237
+
238
+ # Takes the given pattern string "foo" and converts it to a new
239
+ # string "(f)([^/]*?)(o)([^/]*?)(o)" that can be used to create
240
+ # a regular expression.
241
+ def make_pattern(pattern)
242
+ pattern = pattern.split(//)
243
+ pattern << "" if pattern.empty?
244
+
245
+ pattern.inject("") do |regex, character|
246
+ regex << "([^/]*?)" if regex.length > 0
247
+ regex << "(" << Regexp.escape(character) << ")"
248
+ end
249
+ end
250
+
251
+ # Given a MatchData object +match+ and a number of "inside"
252
+ # segments to support, compute both the match score and the
253
+ # highlighted match string. The "inside segments" refers to how
254
+ # many patterns were matched in this one match. For a file name,
255
+ # this will always be one. For directories, it will be one for
256
+ # each directory segment in the original pattern.
257
+ def build_match_result(match, inside_segments)
258
+ runs = []
259
+ inside_chars = total_chars = 0
260
+ match.captures.each_with_index do |capture, index|
261
+ if capture.length > 0
262
+ # odd-numbered captures are matches inside the pattern.
263
+ # even-numbered captures are matches between the pattern's elements.
264
+ inside = index % 2 != 0
265
+
266
+ total_chars += capture.gsub(%r(/), "").length # ignore '/' delimiters
267
+ inside_chars += capture.length if inside
268
+
269
+ if runs.last && runs.last.inside == inside
270
+ runs.last.string << capture
271
+ else
272
+ runs << CharacterRun.new(capture, inside)
273
+ end
274
+ end
275
+ end
276
+
277
+ # Determine the score of this match.
278
+ # 1. fewer "inside runs" (runs corresponding to the original pattern)
279
+ # is better.
280
+ # 2. better coverage of the actual path name is better
281
+
282
+ inside_runs = runs.select { |r| r.inside }
283
+ run_ratio = inside_runs.length.zero? ? 1 : inside_segments / inside_runs.length.to_f
284
+
285
+ char_ratio = total_chars.zero? ? 1 : inside_chars.to_f / total_chars
286
+
287
+ score = run_ratio * char_ratio
288
+
289
+ return { :score => score, :result => runs.join }
290
+ end
291
+
292
+ # Match the given path against the regex, caching the result in +path_matches+.
293
+ # If +path+ is already cached in the path_matches cache, just return the cached
294
+ # value.
295
+ def match_path(path, path_matches, path_regex, path_segments)
296
+ return path_matches[path] if path_matches.key?(path)
297
+
298
+ name_with_slash = path.name + "/" # add a trailing slash for matching the prefix
299
+ matchable_name = name_with_slash.sub(@shared_prefix_re, "")
300
+ matchable_name.chop! # kill the trailing slash
301
+
302
+ if path_regex
303
+ match = matchable_name.match(path_regex)
304
+
305
+ path_matches[path] =
306
+ match && build_match_result(match, path_segments) ||
307
+ { :score => 1, :result => matchable_name, :missed => true }
308
+ else
309
+ path_matches[path] = { :score => 1, :result => matchable_name }
310
+ end
311
+ end
312
+
313
+ # Match +file+ against +file_regex+. If it matches, yield the match
314
+ # metadata to the block.
315
+ def match_file(file, file_regex, path_match, &block)
316
+ if file_match = file.name.match(file_regex)
317
+ match_result = build_match_result(file_match, 1)
318
+ full_match_result = path_match[:result].empty? ? match_result[:result] : File.join(path_match[:result], match_result[:result])
319
+ shortened_path = path_match[:result].gsub(/[^\/]+/) { |m| m.index("(") ? m : m[0,1] }
320
+ abbr = shortened_path.empty? ? match_result[:result] : File.join(shortened_path, match_result[:result])
321
+
322
+ result = { :path => file.path,
323
+ :abbr => abbr,
324
+ :directory => file.parent.name,
325
+ :name => file.name,
326
+ :highlighted_directory => path_match[:result],
327
+ :highlighted_name => match_result[:result],
328
+ :highlighted_path => full_match_result,
329
+ :score => path_match[:score] * match_result[:score] }
330
+ yield result
331
+ end
332
+ end
333
+
334
+ def determine_shared_prefix
335
+ # the common case: if there is only a single root, then the entire
336
+ # name of the root is the shared prefix.
337
+ return roots.first.name if roots.length == 1
338
+
339
+ split_roots = roots.map { |root| root.name.split(%r{/}) }
340
+ segments = split_roots.map { |root| root.length }.max
341
+ master = split_roots.pop
342
+
343
+ segments.times do |segment|
344
+ if !split_roots.all? { |root| root[segment] == master[segment] }
345
+ return master[0,segment].join("/")
346
+ end
347
+ end
348
+
349
+ # shouldn't ever get here, since we uniq the root list before
350
+ # calling this method, but if we do, somehow...
351
+ return roots.first.name
352
+ end
353
+ end
@@ -5,16 +5,16 @@ require 'diakonos'
5
5
 
6
6
  class TC_Buffer < Test::Unit::TestCase
7
7
  SAMPLE_FILE = File.dirname( File.expand_path( __FILE__ ) ) + '/sample-file.rb'
8
-
8
+
9
9
  def setup
10
- @d = Diakonos::Diakonos.new [ '-e', 'quit' ]
10
+ @d = Diakonos::Diakonos.new [ '-e', 'quit', '--test', ]
11
11
  @d.start
12
12
  end
13
-
13
+
14
14
  def teardown
15
15
  system "reset"
16
16
  end
17
-
17
+
18
18
  def test_selected_text
19
19
  @d.openFile( SAMPLE_FILE )
20
20
  b = Diakonos::Buffer.new( @d, SAMPLE_FILE, SAMPLE_FILE )
@@ -33,7 +33,7 @@ class TC_Buffer < Test::Unit::TestCase
33
33
  @d.clipboard.clip
34
34
  )
35
35
  end
36
-
36
+
37
37
  def test_replace
38
38
  @d.openFile SAMPLE_FILE
39
39
  b = Diakonos::Buffer.new( @d, SAMPLE_FILE, SAMPLE_FILE )
@@ -5,9 +5,9 @@ require 'diakonos'
5
5
 
6
6
  class TC_Diakonos < Test::Unit::TestCase
7
7
  def setup
8
- $diakonos = Diakonos::Diakonos.new
8
+ $diakonos = Diakonos::Diakonos.new [ '--test', ]
9
9
  end
10
-
10
+
11
11
  def test_true
12
12
  assert true
13
13
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: diakonos
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.6
4
+ version: 0.8.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pistos
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2008-11-02 01:00:00 -04:00
12
+ date: 2009-02-22 00:00:00 -05:00
13
13
  default_executable:
14
14
  dependencies: []
15
15
 
@@ -31,25 +31,38 @@ files:
31
31
  - diakonos-256-colour.conf
32
32
  - bin/diakonos
33
33
  - lib/diakonos.rb
34
- - lib/diakonos/bignum.rb
34
+ - lib/diakonos/display.rb
35
+ - lib/diakonos/object.rb
36
+ - lib/diakonos/window.rb
37
+ - lib/diakonos/finding.rb
38
+ - lib/diakonos/readline.rb
39
+ - lib/diakonos/fixnum.rb
40
+ - lib/diakonos/keying.rb
41
+ - lib/diakonos/enumerable.rb
42
+ - lib/diakonos/buffer.rb
43
+ - lib/diakonos/array.rb
35
44
  - lib/diakonos/bookmark.rb
36
45
  - lib/diakonos/buffer-hash.rb
46
+ - lib/diakonos/buffer-management.rb
47
+ - lib/diakonos/clipboard.rb
37
48
  - lib/diakonos/ctag.rb
38
- - lib/diakonos/enumerable.rb
39
- - lib/diakonos/fixnum.rb
40
- - lib/diakonos/hash.rb
49
+ - lib/diakonos/grep.rb
50
+ - lib/diakonos/hooks.rb
41
51
  - lib/diakonos/keycode.rb
42
- - lib/diakonos/object.rb
52
+ - lib/diakonos/list.rb
53
+ - lib/diakonos/logging.rb
43
54
  - lib/diakonos/regexp.rb
55
+ - lib/diakonos/sessions.rb
44
56
  - lib/diakonos/sized-array.rb
45
57
  - lib/diakonos/text-mark.rb
46
- - lib/diakonos/window.rb
47
- - lib/diakonos/array.rb
48
- - lib/diakonos/clipboard.rb
49
- - lib/diakonos/finding.rb
50
- - lib/diakonos/readline.rb
58
+ - lib/diakonos/bignum.rb
59
+ - lib/diakonos/hash.rb
60
+ - lib/diakonos/interaction.rb
51
61
  - lib/diakonos/string.rb
52
- - lib/diakonos/buffer.rb
62
+ - lib/diakonos/functions.rb
63
+ - lib/diakonos/help.rb
64
+ - lib/diakonos/config.rb
65
+ - lib/diakonos/vendor/fuzzy_file_finder.rb
53
66
  has_rdoc: false
54
67
  homepage: http://purepistos.net/diakonos
55
68
  post_install_message:
@@ -78,9 +91,9 @@ specification_version: 2
78
91
  summary: A Linux editor for the masses.
79
92
  test_files:
80
93
  - test/clipboard-test.rb
81
- - test/diakonos-test.rb
82
94
  - test/hash-test.rb
83
95
  - test/regexp-test.rb
84
96
  - test/sizedarray-test.rb
85
97
  - test/string-test.rb
86
98
  - test/buffer-test.rb
99
+ - test/diakonos-test.rb