diakonos 0.8.11 → 0.9.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (111) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG +439 -0
  3. data/LICENCE.md +675 -0
  4. data/bin/diakonos +6 -0
  5. data/diakonos-256-colour.conf +220 -0
  6. data/diakonos.conf +1802 -0
  7. data/help/about-help.dhf +31 -0
  8. data/help/clipboard.dhf +45 -0
  9. data/help/close-file.dhf +6 -0
  10. data/help/code-block-navigation.dhf +16 -0
  11. data/help/column-markers.dhf +15 -0
  12. data/help/config.dhf +69 -0
  13. data/help/cursor-stack.dhf +19 -0
  14. data/help/delete.dhf +41 -0
  15. data/help/extensions.dhf +125 -0
  16. data/help/file-type.dhf +24 -0
  17. data/help/key-mapping.dhf +127 -0
  18. data/help/line-numbers.dhf +22 -0
  19. data/help/macros.dhf +27 -0
  20. data/help/new-file.dhf +6 -0
  21. data/help/open-file.dhf +21 -0
  22. data/help/quit.dhf +7 -0
  23. data/help/resizing.dhf +19 -0
  24. data/help/ruby.dhf +17 -0
  25. data/help/save-file.dhf +10 -0
  26. data/help/scripting.dhf +92 -0
  27. data/help/search.dhf +103 -0
  28. data/help/shell.dhf +60 -0
  29. data/help/speed.dhf +23 -0
  30. data/help/support.dhf +15 -0
  31. data/help/switch-buffers.dhf +15 -0
  32. data/help/tabs.dhf +36 -0
  33. data/help/undo.dhf +9 -0
  34. data/help/uninstall.dhf +18 -0
  35. data/help/welcome.dhf +32 -0
  36. data/help/word-wrap.dhf +17 -0
  37. data/lib/diakonos/about.rb +70 -0
  38. data/lib/diakonos/bookmark.rb +46 -0
  39. data/lib/diakonos/buffer/bookmarking.rb +47 -0
  40. data/lib/diakonos/buffer/cursor.rb +335 -0
  41. data/lib/diakonos/buffer/delete.rb +170 -0
  42. data/lib/diakonos/buffer/display.rb +362 -0
  43. data/lib/diakonos/buffer/file.rb +157 -0
  44. data/lib/diakonos/buffer/indentation.rb +175 -0
  45. data/lib/diakonos/buffer/searching.rb +552 -0
  46. data/lib/diakonos/buffer/selection.rb +360 -0
  47. data/lib/diakonos/buffer/undo.rb +73 -0
  48. data/lib/diakonos/buffer-hash.rb +60 -0
  49. data/lib/diakonos/buffer-management.rb +59 -0
  50. data/lib/diakonos/buffer.rb +698 -0
  51. data/lib/diakonos/clipboard-klipper-dbus.rb +62 -0
  52. data/lib/diakonos/clipboard-klipper.rb +62 -0
  53. data/lib/diakonos/clipboard-osx.rb +59 -0
  54. data/lib/diakonos/clipboard-xclip.rb +60 -0
  55. data/lib/diakonos/clipboard.rb +47 -0
  56. data/lib/diakonos/config-file.rb +67 -0
  57. data/lib/diakonos/config.rb +382 -0
  58. data/lib/diakonos/core-ext/enumerable.rb +15 -0
  59. data/lib/diakonos/core-ext/hash.rb +60 -0
  60. data/lib/diakonos/core-ext/object.rb +6 -0
  61. data/lib/diakonos/core-ext/regexp.rb +6 -0
  62. data/lib/diakonos/core-ext/string.rb +122 -0
  63. data/lib/diakonos/ctag.rb +28 -0
  64. data/lib/diakonos/cursor.rb +27 -0
  65. data/lib/diakonos/display/format.rb +75 -0
  66. data/lib/diakonos/display.rb +336 -0
  67. data/lib/diakonos/extension-set.rb +49 -0
  68. data/lib/diakonos/extension.rb +34 -0
  69. data/lib/diakonos/finding.rb +40 -0
  70. data/lib/diakonos/functions/basics.rb +34 -0
  71. data/lib/diakonos/functions/bookmarking.rb +61 -0
  72. data/lib/diakonos/functions/buffers.rb +491 -0
  73. data/lib/diakonos/functions/clipboard.rb +70 -0
  74. data/lib/diakonos/functions/cursor.rb +264 -0
  75. data/lib/diakonos/functions/grepping.rb +83 -0
  76. data/lib/diakonos/functions/indentation.rb +71 -0
  77. data/lib/diakonos/functions/readline.rb +93 -0
  78. data/lib/diakonos/functions/search.rb +179 -0
  79. data/lib/diakonos/functions/selection.rb +98 -0
  80. data/lib/diakonos/functions/sessions.rb +78 -0
  81. data/lib/diakonos/functions/shell.rb +250 -0
  82. data/lib/diakonos/functions/tags.rb +65 -0
  83. data/lib/diakonos/functions/text-manipulation.rb +196 -0
  84. data/lib/diakonos/functions-deprecated.rb +77 -0
  85. data/lib/diakonos/functions.rb +292 -0
  86. data/lib/diakonos/grep.rb +98 -0
  87. data/lib/diakonos/help.rb +47 -0
  88. data/lib/diakonos/hooks.rb +13 -0
  89. data/lib/diakonos/installation.rb +19 -0
  90. data/lib/diakonos/interaction-handler.rb +216 -0
  91. data/lib/diakonos/interaction.rb +52 -0
  92. data/lib/diakonos/key-map.rb +62 -0
  93. data/lib/diakonos/keying.rb +442 -0
  94. data/lib/diakonos/line-mover.rb +42 -0
  95. data/lib/diakonos/list.rb +59 -0
  96. data/lib/diakonos/logging.rb +27 -0
  97. data/lib/diakonos/mode.rb +17 -0
  98. data/lib/diakonos/mouse.rb +18 -0
  99. data/lib/diakonos/number-fitter.rb +11 -0
  100. data/lib/diakonos/range.rb +31 -0
  101. data/lib/diakonos/readline/functions.rb +82 -0
  102. data/lib/diakonos/readline.rb +223 -0
  103. data/lib/diakonos/search.rb +58 -0
  104. data/lib/diakonos/sessions.rb +257 -0
  105. data/lib/diakonos/sized-array.rb +48 -0
  106. data/lib/diakonos/text-mark.rb +19 -0
  107. data/lib/diakonos/vendor/fuzzy_file_finder.rb +386 -0
  108. data/lib/diakonos/version.rb +25 -0
  109. data/lib/diakonos/window.rb +43 -0
  110. data/lib/diakonos.rb +592 -0
  111. metadata +160 -68
@@ -0,0 +1,386 @@
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
+ # This is the exception that is raised if you try to scan a
38
+ # directory tree with too many entries. By default, a ceiling of
39
+ # 10,000 entries is enforced, but you can change that number via
40
+ # the +ceiling+ parameter to FuzzyFileFinder.new.
41
+ class TooManyEntries < RuntimeError; end
42
+
43
+ # Used internally to represent a run of characters within a
44
+ # match. This is used to build the highlighted version of
45
+ # a file name.
46
+ class CharacterRun < Struct.new(:string, :inside) #:nodoc:
47
+ def to_s
48
+ if inside
49
+ "(#{string})"
50
+ else
51
+ string
52
+ end
53
+ end
54
+ end
55
+
56
+ # Used internally to represent a file within the directory tree.
57
+ class FileSystemEntry #:nodoc:
58
+ attr_reader :parent
59
+ attr_reader :name
60
+
61
+ def initialize(parent, name)
62
+ @parent = parent
63
+ @name = name
64
+ end
65
+
66
+ def path
67
+ File.join(parent.name, name)
68
+ end
69
+ end
70
+
71
+ # Used internally to represent a subdirectory within the directory
72
+ # tree.
73
+ class Directory #:nodoc:
74
+ attr_reader :name
75
+
76
+ def initialize(name, is_root=false)
77
+ @name = name
78
+ @is_root = is_root
79
+ end
80
+
81
+ def root?
82
+ is_root
83
+ end
84
+ end
85
+
86
+ # The roots directory trees to search.
87
+ attr_reader :roots
88
+
89
+ # The list of files beneath all +roots+
90
+ attr_reader :files
91
+
92
+ # The maximum number of files beneath all +roots+
93
+ attr_reader :ceiling
94
+
95
+ # The prefix shared by all +roots+.
96
+ attr_reader :shared_prefix
97
+
98
+ # The list of glob patterns to ignore.
99
+ attr_reader :ignores
100
+
101
+ # Initializes a new FuzzyFileFinder. This will scan the
102
+ # given +directories+, using +ceiling+ as the maximum number
103
+ # of entries to scan. If there are more than +ceiling+ entries
104
+ # a TooManyEntries exception will be raised.
105
+ def initialize( params = {} )
106
+ @ceiling = params[:ceiling] || 10_000
107
+ @ignores = Array(params[:ignores])
108
+
109
+ if params[:directories]
110
+ directories = Array(params[:directories])
111
+ directories << "." if directories.empty?
112
+ else
113
+ directories = ['.']
114
+ end
115
+
116
+ @recursive = params[:recursive].nil? ? true : params[:recursive]
117
+
118
+ # expand any paths with ~
119
+ root_dirnames = directories.map { |d|
120
+ File.realpath(d)
121
+ }.select { |d|
122
+ File.directory?(d)
123
+ }.uniq
124
+
125
+ @roots = root_dirnames.map { |d| Directory.new(d, true) }
126
+ @shared_prefix = determine_shared_prefix
127
+ @shared_prefix_re = Regexp.new("^#{Regexp.escape(shared_prefix)}" + (shared_prefix.empty? ? "" : "/"))
128
+ @sorted = params[:sorted]
129
+
130
+ @files = []
131
+ @directories = {} # To detect link cycles
132
+ @dirs_with_many = []
133
+
134
+ rescan!
135
+ end
136
+
137
+ # Rescans the subtree. If the directory contents every change,
138
+ # you'll need to call this to force the finder to be aware of
139
+ # the changes.
140
+ def rescan!
141
+ @files.clear
142
+ roots.each { |root| follow_tree(root) }
143
+ end
144
+
145
+ # Takes the given +pattern+ (which must be a string) and searches
146
+ # all files beneath +root+, yielding each match.
147
+ #
148
+ # +pattern+ is interpreted thus:
149
+ #
150
+ # * "foo" : look for any file with the characters 'f', 'o', and 'o'
151
+ # in its basename (discounting directory names). The characters
152
+ # must be in that order.
153
+ # * "foo/bar" : look for any file with the characters 'b', 'a',
154
+ # and 'r' in its basename (discounting directory names). Also,
155
+ # any successful match must also have at least one directory
156
+ # element matching the characters 'f', 'o', and 'o' (in that
157
+ # order.
158
+ # * "foo/bar/baz" : same as "foo/bar", but matching two
159
+ # directory elements in addition to a file name of "baz".
160
+ #
161
+ # Each yielded match will be a hash containing the following keys:
162
+ #
163
+ # * :path refers to the full path to the file
164
+ # * :directory refers to the directory of the file
165
+ # * :name refers to the name of the file (without directory)
166
+ # * :highlighted_directory refers to the directory of the file with
167
+ # matches highlighted in parentheses.
168
+ # * :highlighted_name refers to the name of the file with matches
169
+ # highlighted in parentheses
170
+ # * :highlighted_path refers to the full path of the file with
171
+ # matches highlighted in parentheses
172
+ # * :abbr refers to an abbreviated form of :highlighted_path, where
173
+ # path segments without matches are compressed to just their first
174
+ # character.
175
+ # * :score refers to a value between 0 and 1 indicating how closely
176
+ # the file matches the given pattern. A score of 1 means the
177
+ # pattern matches the file exactly.
178
+ def search(pattern, &block)
179
+ path_parts = pattern.strip.split("/")
180
+ path_parts.push "" if pattern[-1,1] == "/"
181
+
182
+ file_name_part = path_parts.pop || ""
183
+
184
+ if path_parts.any?
185
+ path_regex_raw = "^(.*?)" + path_parts.map { |part| make_pattern(part) }.join("(.*?/.*?)") + "(.*?)$"
186
+ path_regex = Regexp.new(path_regex_raw, Regexp::IGNORECASE)
187
+ end
188
+
189
+ file_regex_raw = "^(.*?)" << make_pattern(file_name_part) << "(.*)$"
190
+ file_regex = Regexp.new(file_regex_raw, Regexp::IGNORECASE)
191
+
192
+ path_matches = {}
193
+ files.each do |file|
194
+ path_match = match_path(file.parent, path_matches, path_regex, path_parts.length)
195
+ next if path_match[:missed]
196
+
197
+ match_file(file, file_regex, path_match, &block)
198
+ end
199
+ end
200
+
201
+ # Takes the given +pattern+ (which must be a string, formatted as
202
+ # described in #search), and returns up to +max+ matches in an
203
+ # Array. If +max+ is nil, all matches will be returned.
204
+ def find(pattern, max=nil)
205
+ results = []
206
+
207
+ search(pattern) do |match|
208
+ results << match
209
+ break if max && results.length >= max
210
+ end
211
+
212
+ if @sorted
213
+ results.sort_by { |m| m[:path] }
214
+ else
215
+ results
216
+ end
217
+ end
218
+
219
+ # Displays the finder object in a sane, non-explosive manner.
220
+ def inspect #:nodoc:
221
+ "#<%s:0x%x roots=%s, files=%d>" % [self.class.name, object_id, roots.map { |r| r.name.inspect }.join(", "), files.length]
222
+ end
223
+
224
+ private
225
+
226
+ # Recursively scans +directory+ and all files and subdirectories
227
+ # beneath it, depth-first.
228
+ def follow_tree(directory)
229
+ real_dir = File.realpath(directory.name)
230
+ if ! @directories[real_dir]
231
+ @directories[real_dir] = true
232
+
233
+ Dir.entries(directory.name)
234
+ .tap { |_entries|
235
+ if _entries.length > ceiling/10
236
+ @dirs_with_many << [_entries.length, directory.name]
237
+ $diakonos.log "[#{self.class}] Many dir entries: #{_entries.length} in #{directory.name}"
238
+ end
239
+ }.each do |entry|
240
+ next if entry[0,1] == "."
241
+ if files.length > ceiling
242
+ raise TooManyEntries.new(%{
243
+ Directories with many entries:
244
+
245
+ #{@dirs_with_many.map { |d| d.join("\t") }.join("\n")}
246
+
247
+ })
248
+ end
249
+
250
+ full = File.join(directory.name, entry)
251
+ next if ignore?(full)
252
+
253
+ if File.directory?(full)
254
+ if @recursive
255
+ follow_tree(Directory.new(full))
256
+ end
257
+ else
258
+ files.push(FileSystemEntry.new(directory, entry))
259
+ end
260
+ end
261
+ end
262
+ end
263
+
264
+ # Returns +true+ if the given name matches any of the ignore
265
+ # patterns.
266
+ def ignore?(name)
267
+ n = name.sub(@shared_prefix_re, "")
268
+ ignores.any? { |pattern| File.fnmatch(pattern, n) }
269
+ end
270
+
271
+ # Takes the given pattern string "foo" and converts it to a new
272
+ # string "(f)([^/]*?)(o)([^/]*?)(o)" that can be used to create
273
+ # a regular expression.
274
+ def make_pattern(pattern)
275
+ pattern = pattern.split(//)
276
+ pattern << "" if pattern.empty?
277
+
278
+ pattern.inject("") do |regex, character|
279
+ regex << "([^/]*?)" if regex.length > 0
280
+ regex << "(" << Regexp.escape(character) << ")"
281
+ end
282
+ end
283
+
284
+ # Given a MatchData object +match+ and a number of "inside"
285
+ # segments to support, compute both the match score and the
286
+ # highlighted match string. The "inside segments" refers to how
287
+ # many patterns were matched in this one match. For a file name,
288
+ # this will always be one. For directories, it will be one for
289
+ # each directory segment in the original pattern.
290
+ def build_match_result(match, inside_segments)
291
+ runs = []
292
+ inside_chars = total_chars = 0
293
+ match.captures.each_with_index do |capture, index|
294
+ if capture.length > 0
295
+ # odd-numbered captures are matches inside the pattern.
296
+ # even-numbered captures are matches between the pattern's elements.
297
+ inside = index % 2 != 0
298
+
299
+ total_chars += capture.gsub(%r(/), "").length # ignore '/' delimiters
300
+ inside_chars += capture.length if inside
301
+
302
+ if runs.last && runs.last.inside == inside
303
+ runs.last.string << capture
304
+ else
305
+ runs << CharacterRun.new(capture, inside)
306
+ end
307
+ end
308
+ end
309
+
310
+ # Determine the score of this match.
311
+ # 1. fewer "inside runs" (runs corresponding to the original pattern)
312
+ # is better.
313
+ # 2. better coverage of the actual path name is better
314
+
315
+ inside_runs = runs.select { |r| r.inside }
316
+ run_ratio = inside_runs.length.zero? ? 1 : inside_segments / inside_runs.length.to_f
317
+
318
+ char_ratio = total_chars.zero? ? 1 : inside_chars.to_f / total_chars
319
+
320
+ score = run_ratio * char_ratio
321
+
322
+ return { :score => score, :result => runs.join }
323
+ end
324
+
325
+ # Match the given path against the regex, caching the result in +path_matches+.
326
+ # If +path+ is already cached in the path_matches cache, just return the cached
327
+ # value.
328
+ def match_path(path, path_matches, path_regex, path_segments)
329
+ return path_matches[path] if path_matches.key?(path)
330
+
331
+ name_with_slash = path.name + "/" # add a trailing slash for matching the prefix
332
+ matchable_name = name_with_slash.sub(@shared_prefix_re, "")
333
+ matchable_name.chop! # kill the trailing slash
334
+
335
+ if path_regex
336
+ match = matchable_name.match(path_regex)
337
+
338
+ path_matches[path] =
339
+ match && build_match_result(match, path_segments) ||
340
+ { :score => 1, :result => matchable_name, :missed => true }
341
+ else
342
+ path_matches[path] = { :score => 1, :result => matchable_name }
343
+ end
344
+ end
345
+
346
+ # Match +file+ against +file_regex+. If it matches, yield the match
347
+ # metadata to the block.
348
+ def match_file(file, file_regex, path_match, &block)
349
+ if file_match = file.name.match(file_regex)
350
+ match_result = build_match_result(file_match, 1)
351
+ full_match_result = path_match[:result].empty? ? match_result[:result] : File.join(path_match[:result], match_result[:result])
352
+ shortened_path = path_match[:result].gsub(/[^\/]+/) { |m| m.index("(") ? m : m[0,1] }
353
+ abbr = shortened_path.empty? ? match_result[:result] : File.join(shortened_path, match_result[:result])
354
+
355
+ result = { :path => file.path,
356
+ :abbr => abbr,
357
+ :directory => file.parent.name,
358
+ :name => file.name,
359
+ :highlighted_directory => path_match[:result],
360
+ :highlighted_name => match_result[:result],
361
+ :highlighted_path => full_match_result,
362
+ :score => path_match[:score] * match_result[:score] }
363
+ yield result
364
+ end
365
+ end
366
+
367
+ def determine_shared_prefix
368
+ # the common case: if there is only a single root, then the entire
369
+ # name of the root is the shared prefix.
370
+ return roots.first.name if roots.length == 1
371
+
372
+ split_roots = roots.map { |root| root.name.split(%r{/}) }
373
+ segments = split_roots.map { |root| root.length }.max
374
+ master = split_roots.pop
375
+
376
+ segments.times do |segment|
377
+ if !split_roots.all? { |root| root[segment] == master[segment] }
378
+ return master[0,segment].join("/")
379
+ end
380
+ end
381
+
382
+ # shouldn't ever get here, since we uniq the root list before
383
+ # calling this method, but if we do, somehow...
384
+ return roots.first.name
385
+ end
386
+ end
@@ -0,0 +1,25 @@
1
+ module Diakonos
2
+ VERSION = '0.9.9'
3
+ LAST_MODIFIED = '2023-03-07'
4
+
5
+ def self.parse_version( s )
6
+ if s
7
+ s.split( '.' ).map { |part| part.to_i }.extend( Comparable )
8
+ end
9
+ end
10
+
11
+ def self.check_ruby_version
12
+ ruby_version = parse_version( RUBY_VERSION )
13
+ if ruby_version < [ 2, 1 ]
14
+ $stderr.puts "This version of Diakonos (#{Diakonos::VERSION}) requires Ruby 2.1 or higher."
15
+ if ruby_version >= [ 2, 0 ]
16
+ $stderr.puts "Version 0.9.5 is the last version of Diakonos which can run under Ruby 2.0."
17
+ elsif ruby_version >= [ 1, 9 ]
18
+ $stderr.puts "Version 0.9.2 is the last version of Diakonos which can run under Ruby 1.9."
19
+ elsif ruby_version >= [ 1, 8 ]
20
+ $stderr.puts "Version 0.8.9 is the last version of Diakonos which can run under Ruby 1.8."
21
+ end
22
+ exit 1
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,43 @@
1
+ module Diakonos
2
+ class Window < ::Curses::Window
3
+
4
+ if $diakonos.testing
5
+
6
+ def initialize( *args )
7
+ # Setup some variables to keep track of a fake cursor
8
+ @row, @col = 0, 0
9
+ super
10
+ Curses::close_screen
11
+ end
12
+
13
+ def refresh
14
+ # Don't refresh when testing
15
+ end
16
+
17
+ def setpos( row, col )
18
+ @row, @col = row, col
19
+ end
20
+
21
+ def addstr( str )
22
+ @col += str.length
23
+ end
24
+
25
+ def curx
26
+ @col
27
+ end
28
+
29
+ def cury
30
+ @row
31
+ end
32
+
33
+ def attrset( *args )
34
+ # noop
35
+ end
36
+
37
+ def getch
38
+ $keystrokes.shift
39
+ end
40
+ end
41
+
42
+ end
43
+ end