glimmer-cs-gladiator 0.8.1 → 0.9.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.
@@ -1,662 +1,729 @@
1
- module Glimmer
2
- class Gladiator
3
- class File
4
- include Glimmer
5
-
6
- attr_accessor :line_numbers_content, :line_number, :find_text, :replace_text, :top_pixel, :display_path, :case_sensitive, :caret_position, :selection_count, :last_caret_position, :last_selection_count, :line_position
7
- attr_reader :name, :path, :project_dir
8
-
9
- def initialize(path='', project_dir=nil)
10
- raise "Not a file path: #{path}" if path.nil? || (!path.empty? && !::File.file?(path))
11
- @project_dir = project_dir
12
- @command_history = []
13
- @name = path.empty? ? 'Scratchpad' : ::File.basename(path)
14
- self.path = ::File.expand_path(path) unless path.empty?
15
- @top_pixel = 0
16
- @caret_position = 0
17
- @selection_count = 0
18
- @last_selection_count = 0
19
- @line_number = 1
20
- @init = nil
21
- end
22
-
23
- def language
24
- # TODO consider using Rouge::Lexer.guess_by_filename instead and perhaps guess_by_source when it fails
25
- extension = path.split('.').last if path.to_s.include?('.')
26
- return 'ruby' if scratchpad?
27
- return 'ruby' if path.to_s.end_with?('Gemfile') || path.to_s.end_with?('Rakefile')
28
- return 'ruby' if dirty_content.start_with?('#!/usr/bin/env ruby') || dirty_content.start_with?('#!/usr/bin/env jruby')
29
- return 'yaml' if path.to_s.end_with?('Gemfile.lock')
30
- return 'shell' if extension.nil? && path.to_s.include?('/bin/')
31
- case extension
32
- # TODO extract case statement to an external config file
33
- when 'rb'
34
- 'ruby'
35
- when 'md', 'markdown'
36
- 'markdown'
37
- when 'js', 'es6'
38
- 'javascript'
39
- when 'json'
40
- 'json'
41
- when 'yaml'
42
- 'yaml'
43
- when 'html'
44
- 'html'
45
- when 'h', 'c'
46
- 'c'
47
- when 'hs'
48
- 'haskell'
49
- when 'gradle'
50
- 'gradle'
51
- when 'cpp'
52
- 'cpp'
53
- when 'css'
54
- 'css'
55
- when 'java'
56
- 'java'
57
- when 'jsp'
58
- 'jsp'
59
- when 'plist'
60
- 'plist'
61
- when 'haml'
62
- 'haml'
63
- when 'xml'
64
- 'xml'
65
- when 'ini'
66
- 'ini'
67
- when 'pl'
68
- 'perl'
69
- when 'tcl'
70
- 'tcl'
71
- when 'sass'
72
- 'sass'
73
- when 'scss'
74
- 'scss'
75
- when 'sql'
76
- 'sql'
77
- when 'sh'
78
- 'shell'
79
- when 'vue'
80
- 'vue'
81
- when 'txt', nil
82
- 'plain_text'
83
- end
84
- end
85
-
86
- def init_content
87
- unless @init
88
- @init = true
89
- begin
90
- # test read dirty content
91
- observe(self, :dirty_content) do
92
- line_count = lines.empty? ? 1 : lines.size
93
- lines_text_size = [line_count.to_s.size, 4].max
94
- old_top_pixel = top_pixel
95
- self.line_numbers_content = line_count.times.map {|n| (' ' * (lines_text_size - (n+1).to_s.size)) + (n+1).to_s }.join("\n")
96
- self.top_pixel = old_top_pixel
97
- end
98
- the_dirty_content = read_dirty_content
99
- the_dirty_content.split("\n") # test that it is not a binary file (crashes to rescue block otherwise)
100
- self.dirty_content = the_dirty_content
101
- observe(self, :caret_position) do |new_caret_position|
102
- update_line_number_from_caret_position(new_caret_position)
103
- end
104
- observe(self, :line_number) do |new_line_number|
105
- line_index = line_number - 1
106
- new_caret_position = caret_position_for_line_index(line_index)
107
- current_caret_position = caret_position
108
- line_index_for_new_caret_position = line_index_for_caret_position(new_caret_position)
109
- line_index_for_current_caret_position = line_index_for_caret_position(current_caret_position)
110
- self.caret_position = new_caret_position unless (current_caret_position && line_index_for_new_caret_position == line_index_for_current_caret_position)
111
- end
112
- rescue # in case of a binary file
113
- stop_filewatcher
114
- end
115
- end
116
- end
117
-
118
- def update_line_number_from_caret_position(new_caret_position)
119
- new_line_number = line_index_for_caret_position(caret_position) + 1
120
- current_line_number = line_number
121
- unless (current_line_number && current_line_number == new_line_number)
122
- self.line_number = new_line_number
123
- # TODO check if the following line is needed
124
- self.line_position = caret_position - caret_position_for_line_index(line_number - 1) + 1
125
- end
126
- end
127
-
128
- def path=(the_path)
129
- @path = the_path
130
- generate_display_path
131
- end
132
-
133
- def generate_display_path
134
- return if @path.empty?
135
- @display_path = @path.sub(project_dir.path, '').sub(/^\//, '')
136
- end
137
-
138
- def name=(the_name)
139
- new_path = path.sub(/#{Regexp.escape(@name)}$/, the_name) unless scratchpad?
140
- @name = the_name
141
- if !scratchpad? && ::File.exist?(path)
142
- FileUtils.mv(path, new_path)
143
- self.path = new_path
144
- end
145
- end
146
-
147
- def scratchpad?
148
- path.to_s.empty?
149
- end
150
-
151
- def backup_properties
152
- [:find_text, :replace_text, :case_sensitive, :top_pixel, :caret_position, :selection_count].reduce({}) do |hash, property|
153
- hash.merge(property => send(property))
154
- end
155
- end
156
-
157
- def restore_properties(properties_hash)
158
- return if properties_hash[:caret_position] == 0 && properties_hash[:selection_count] == 0 && properties_hash[:find_text].nil? && properties_hash[:replace_text].nil? && properties_hash[:top_pixel] == 0 && properties_hash[:case_sensitive].nil?
159
- properties_hash.each do |property, value|
160
- send("#{property}=", value)
161
- end
162
- end
163
-
164
- def caret_position=(value)
165
- @last_caret_position = @caret_position
166
- @caret_position = value
167
- end
168
-
169
- def selection_count=(value)
170
- #@last_selection_count = @selection_count
171
- @selection_count = value
172
- @last_selection_count = @selection_count
173
- end
174
-
175
- def dirty_content
176
- init_content
177
- @dirty_content
178
- end
179
-
180
- def dirty_content=(the_content)
181
- # TODO set partial dirty content by line(s) for enhanced performance
182
- @dirty_content = the_content
183
- old_caret_position = caret_position
184
- old_top_pixel = top_pixel
185
-
186
- notify_observers(:content)
187
- if @formatting_dirty_content_for_writing
188
- self.caret_position = old_caret_position
189
- self.top_pixel = old_top_pixel
190
- end
191
- end
192
-
193
- def content
194
- dirty_content
195
- end
196
-
197
- # to use for widget data-binding
198
- def content=(value)
199
- value = value.gsub("\t", ' ')
200
- if dirty_content != value
201
- Command.do(self, :change_content!, value)
202
- end
203
- end
204
-
205
- def change_content!(value)
206
- self.dirty_content = value
207
- update_line_number_from_caret_position(caret_position)
208
- end
209
-
210
- def start_command
211
- @commmand_in_progress = true
212
- end
213
-
214
- def end_command
215
- @commmand_in_progress = false
216
- end
217
-
218
- def command_in_progress?
219
- @commmand_in_progress
220
- end
221
-
222
- def close
223
- stop_filewatcher
224
- remove_all_observers
225
- initialize(path, project_dir)
226
- Command.clear(self)
227
- end
228
-
229
- def read_dirty_content
230
- path.empty? ? '' : ::File.read(path)
231
- end
232
-
233
- def start_filewatcher
234
- return if scratchpad?
235
- @filewatcher = Filewatcher.new(@path)
236
- @thread = Thread.new(@filewatcher) do |fw|
237
- fw.watch do |filename, event|
238
- async_exec do
239
- begin
240
- self.dirty_content = read_dirty_content if read_dirty_content != dirty_content
241
- rescue StandardError, Errno::ENOENT
242
- # in case of a binary file
243
- stop_filewatcher
244
- end
245
- end
246
- end
247
- end
248
- end
249
-
250
- def stop_filewatcher
251
- @filewatcher&.stop
252
- end
253
-
254
- def write_dirty_content
255
- # TODO write partial dirty content by line(s) for enhanced performance
256
- return if scratchpad? || !::File.exist?(path) || !::File.exists?(path) || read_dirty_content == dirty_content
257
- format_dirty_content_for_writing!
258
- ::File.write(path, dirty_content)
259
- rescue StandardError, ArgumentError => e
260
- puts "Error in writing dirty content for #{path}"
261
- puts e.full_message
262
- end
263
-
264
- def format_dirty_content_for_writing!
265
- return if @commmand_in_progress
266
- # TODO f ix c ar e t pos it ion after formatting dirty content (diff?)
267
- new_dirty_content = dirty_content.to_s.split("\n").map {|line| line.strip.empty? ? line : line.rstrip }.join("\n")
268
- new_dirty_content = "#{new_dirty_content.gsub("\r\n", "\n").gsub("\r", "\n").sub(/\n+\z/, '')}\n"
269
- if new_dirty_content != self.dirty_content
270
- @formatting_dirty_content_for_writing = true
271
- self.dirty_content = new_dirty_content
272
- @formatting_dirty_content_for_writing = false
273
- end
274
- end
275
-
276
- def write_raw_dirty_content
277
- return if scratchpad? || !::File.exist?(path)
278
- ::File.write(path, dirty_content) if ::File.exists?(path)
279
- rescue => e
280
- puts "Error in writing raw dirty content for #{path}"
281
- puts e.full_message
282
- end
283
-
284
- def current_line_indentation
285
- current_line.to_s.match(/^(\s+)/).to_a[1].to_s
286
- end
287
-
288
- def current_line
289
- lines[line_number - 1]
290
- end
291
-
292
- def delete!
293
- FileUtils.rm(path) unless scratchpad?
294
- end
295
-
296
- def prefix_new_line!
297
- the_lines = lines
298
- the_lines[line_number-1...line_number-1] = [current_line_indentation]
299
- self.dirty_content = the_lines.join("\n")
300
- self.caret_position = caret_position_for_line_index(line_number-1) + current_line_indentation.size
301
- self.selection_count = 0
302
- end
303
-
304
- def insert_new_line!
305
- the_lines = lines
306
- the_lines[line_number...line_number] = [current_line_indentation]
307
- self.dirty_content = the_lines.join("\n")
308
- self.caret_position = caret_position_for_line_index(line_number) + current_line_indentation.size
309
- self.selection_count = 0
310
- end
311
-
312
- def comment_line!
313
- old_lines = lines
314
- return if old_lines.size < 1
315
- old_selection_count = self.selection_count
316
- old_caret_position = self.caret_position
317
- old_caret_position_line_index = line_index_for_caret_position(old_caret_position)
318
- old_caret_position_line_caret_position = caret_position_for_line_index(old_caret_position_line_index)
319
- old_end_caret_line_index = end_caret_position_line_index(caret_position, selection_count)
320
- new_lines = lines
321
- delta = 0
322
- line_indices_for_selection(caret_position, selection_count).reverse.each do | the_line_index |
323
- delta = 0
324
- the_line = old_lines[the_line_index]
325
- return if the_line.nil?
326
- if the_line.strip.start_with?('# ')
327
- new_lines[the_line_index] = the_line.sub(/# /, '')
328
- delta -= 2
329
- elsif the_line.strip.start_with?('#')
330
- new_lines[the_line_index] = the_line.sub(/#/, '')
331
- delta -= 1
332
- else
333
- new_lines[the_line_index] = "# #{the_line}"
334
- delta += 2
335
- end
336
- end
337
- self.dirty_content = new_lines.join("\n")
338
- if old_selection_count.to_i > 0
339
- self.caret_position = caret_position_for_line_index(old_caret_position_line_index)
340
- self.selection_count = (caret_position_for_line_index(old_end_caret_line_index + 1) - self.caret_position)
341
- else
342
- new_caret_position = old_caret_position + delta
343
- new_caret_position = [new_caret_position, old_caret_position_line_caret_position].max
344
- self.caret_position = new_caret_position
345
- self.selection_count = 0
346
- end
347
- end
348
-
349
- def indent!
350
- new_lines = lines
351
- old_lines = lines
352
- return if old_lines.size < 1
353
- old_selection_count = self.selection_count
354
- old_caret_position = self.caret_position
355
- old_caret_position_line_index = line_index_for_caret_position(old_caret_position)
356
- old_caret_position_line_caret_position = caret_position_for_line_index(old_caret_position_line_index)
357
- old_end_caret_line_index = end_caret_position_line_index(caret_position, selection_count)
358
- delta = 2
359
- line_indices_for_selection(caret_position, selection_count).each do |the_line_index|
360
- the_line = old_lines[the_line_index]
361
- new_lines[the_line_index] = " #{the_line}"
362
- end
363
- old_caret_position = self.caret_position
364
- self.dirty_content = new_lines.join("\n")
365
- if old_selection_count.to_i > 0
366
- self.caret_position = caret_position_for_line_index(old_caret_position_line_index)
367
- self.selection_count = (caret_position_for_line_index(old_end_caret_line_index + 1) - self.caret_position)
368
- else
369
- self.caret_position = old_caret_position + delta
370
- self.selection_count = 0
371
- end
372
- end
373
-
374
- def outdent!
375
- new_lines = lines
376
- old_lines = lines
377
- return if old_lines.size < 1
378
- old_selection_count = self.selection_count
379
- old_caret_position = self.caret_position
380
- old_caret_position_line_index = line_index_for_caret_position(old_caret_position)
381
- old_caret_position_line_caret_position = caret_position_for_line_index(old_caret_position_line_index)
382
- old_end_caret_line_index = end_caret_position_line_index(caret_position, selection_count)
383
- delta = 0
384
- line_indices_for_selection(caret_position, selection_count).each do |the_line_index|
385
- the_line = old_lines[the_line_index]
386
- if the_line.to_s.start_with?(' ')
387
- new_lines[the_line_index] = the_line.sub(/ /, '')
388
- delta = -2
389
- elsif the_line&.start_with?(' ')
390
- new_lines[the_line_index] = the_line.sub(/ /, '')
391
- delta = -1
392
- end
393
- end
394
- self.dirty_content = new_lines.join("\n")
395
- if old_selection_count.to_i > 0
396
- self.caret_position = caret_position_for_line_index(old_caret_position_line_index)
397
- self.selection_count = (caret_position_for_line_index(old_end_caret_line_index + 1) - self.caret_position)
398
- else
399
- new_caret_position = old_caret_position + delta
400
- new_caret_position = [new_caret_position, old_caret_position_line_caret_position].max
401
- self.caret_position = new_caret_position
402
- self.selection_count = 0
403
- end
404
- end
405
-
406
- def kill_line!
407
- new_lines = lines
408
- return if new_lines.size < 1
409
- line_indices = line_indices_for_selection(caret_position, selection_count)
410
- new_lines = new_lines[0...line_indices.first] + new_lines[(line_indices.last+1)...new_lines.size]
411
- old_caret_position = self.caret_position
412
- old_line_index = self.line_number - 1
413
- line_position = line_position_for_caret_position(old_caret_position)
414
- self.dirty_content = "#{new_lines.join("\n")}\n"
415
- self.caret_position = caret_position_for_line_index(old_line_index) + [line_position, lines[old_line_index].to_s.size].min
416
- self.selection_count = 0
417
- end
418
-
419
- def duplicate_line!
420
- new_lines = lines
421
- old_lines = lines
422
- return if old_lines.size < 1
423
- old_selection_count = self.selection_count
424
- old_caret_position = self.caret_position
425
- old_caret_position_line_index = line_index_for_caret_position(old_caret_position)
426
- old_caret_position_line_caret_position = caret_position_for_caret_position_start_of_line(old_caret_position_line_index)
427
- old_end_caret_line_index = end_caret_position_line_index(caret_position, selection_count)
428
- the_line_indices = line_indices_for_selection(caret_position, selection_count)
429
- the_lines = lines_for_selection(caret_position, selection_count)
430
- delta = the_lines.join("\n").size + 1
431
- the_lines.each_with_index do |the_line, i|
432
- new_lines.insert(the_line_indices.first + i, the_line)
433
- end
434
- self.dirty_content = new_lines.join("\n")
435
- if old_selection_count.to_i > 0
436
- self.caret_position = caret_position_for_line_index(old_caret_position_line_index)
437
- self.selection_count = (caret_position_for_line_index(old_end_caret_line_index + 1) - self.caret_position)
438
- else
439
- self.caret_position = old_caret_position + delta
440
- self.selection_count = 0
441
- end
442
- end
443
-
444
- def find_next
445
- return if find_text.to_s.empty?
446
- all_lines = lines
447
- the_line_index = line_index_for_caret_position(caret_position)
448
- line_position = line_position_for_caret_position(caret_position)
449
- found = found_text?(caret_position)
450
- 2.times do |i|
451
- rotation = the_line_index
452
- all_lines.rotate(rotation).each_with_index do |the_line, the_index|
453
- the_index = (the_index + rotation)%all_lines.size
454
- start_position = 0
455
- start_position = line_position + find_text.to_s.size if i == 0 && the_index == the_line_index && found_text?(caret_position)
456
- text_to_find_in = the_line[start_position..-1]
457
- occurrence_index = case_sensitive ? text_to_find_in&.index(find_text.to_s) : text_to_find_in&.downcase&.index(find_text.to_s.downcase)
458
- if occurrence_index
459
- self.caret_position = caret_position_for_line_index(the_index) + start_position + occurrence_index
460
- self.selection_count = find_text.to_s.size
461
- return
462
- end
463
- end
464
- end
465
- end
466
-
467
- def find_previous
468
- return if find_text.to_s.empty?
469
- all_lines = lines
470
- the_line_index = line_index_for_caret_position(caret_position)
471
- line_position = line_position_for_caret_position(caret_position)
472
- 2.times do |i|
473
- rotation = - the_line_index - 1 + all_lines.size
474
- all_lines.reverse.rotate(rotation).each_with_index do |the_line, the_index|
475
- the_index = all_lines.size - 1 - (the_index + rotation)%all_lines.size
476
- if the_index == the_line_index
477
- start_position = i > 0 ? 0 : (the_line.size - line_position)
478
- else
479
- start_position = 0
480
- end
481
- text_to_find_in = the_line.downcase.reverse[start_position...the_line.size].to_s
482
- occurrence_index = text_to_find_in.index(find_text.to_s.downcase.reverse)
483
- if occurrence_index
484
- self.caret_position = caret_position_for_line_index(the_index) + (the_line.size - (start_position + occurrence_index + find_text.to_s.size))
485
- self.selection_count = find_text.to_s.size
486
- return
487
- end
488
- end
489
- end
490
- end
491
-
492
- def ensure_find_next
493
- return if find_text.to_s.empty? || dirty_content.to_s.strip.size < 1
494
- find_next unless found_text?(self.caret_position)
495
- end
496
-
497
- def found_text?(caret_position)
498
- dirty_content[caret_position.to_i, find_text.to_s.size].to_s.downcase == find_text.to_s.downcase
499
- end
500
-
501
- def replace_next!
502
- return if find_text.to_s.empty? || dirty_content.to_s.strip.size < 1
503
- ensure_find_next
504
- new_dirty_content = dirty_content
505
- new_dirty_content[caret_position, find_text.size] = replace_text.to_s
506
- self.dirty_content = new_dirty_content
507
- find_next
508
- find_next if replace_text.to_s.include?(find_text) && !replace_text.to_s.start_with?(find_text)
509
- end
510
-
511
- def page_up
512
- self.selection_count = 0
513
- self.line_number = [(self.line_number - 15), 1].max
514
- end
515
-
516
- def page_down
517
- self.selection_count = 0
518
- self.line_number = [(self.line_number + 15), lines.size].min
519
- end
520
-
521
- def home
522
- self.selection_count = 0
523
- self.line_number = 1
524
- end
525
-
526
- def end
527
- self.selection_count = 0
528
- self.line_number = lines.size
529
- end
530
-
531
- def start_of_line
532
- self.caret_position = caret_position_for_line_index(self.line_number - 1)
533
- end
534
-
535
- def end_of_line
536
- self.caret_position = caret_position_for_line_index(self.line_number) - 1
537
- end
538
-
539
- def move_up!
540
- old_lines = lines
541
- return if old_lines.size < 2
542
- old_selection_count = self.selection_count
543
- old_caret_position = self.caret_position
544
- old_caret_position_line_caret_position = caret_position_for_caret_position_start_of_line(old_caret_position)
545
- old_caret_position_line_position = old_caret_position - old_caret_position_line_caret_position
546
- old_end_caret_line_index = end_caret_position_line_index(caret_position, selection_count)
547
- new_lines = lines
548
- the_line_indices = line_indices_for_selection(caret_position, selection_count)
549
- the_lines = lines_for_selection(caret_position, selection_count)
550
- new_line_index = [the_line_indices.first - 1, 0].max
551
- new_lines[the_line_indices.first..the_line_indices.last] = []
552
- new_lines[new_line_index...new_line_index] = the_lines
553
- self.dirty_content = new_lines.join("\n")
554
- self.caret_position = caret_position_for_line_index(new_line_index) + [old_caret_position_line_position, new_lines[new_line_index].size].min
555
- self.selection_count = old_selection_count.to_i if old_selection_count.to_i > 0
556
- end
557
-
558
- def move_down!
559
- old_lines = lines
560
- return if old_lines.size < 2
561
- old_selection_count = self.selection_count
562
- old_caret_position = self.caret_position
563
- old_caret_position_line_caret_position = caret_position_for_caret_position_start_of_line(old_caret_position)
564
- old_caret_position_line_position = old_caret_position - old_caret_position_line_caret_position
565
- old_end_caret_line_index = end_caret_position_line_index(caret_position, selection_count)
566
- new_lines = lines
567
- the_line_indices = line_indices_for_selection(caret_position, selection_count)
568
- the_lines = lines_for_selection(caret_position, selection_count)
569
- new_line_index = [the_line_indices.first + 1, new_lines.size - 1].min
570
- new_lines[the_line_indices.first..the_line_indices.last] = []
571
- new_lines[new_line_index...new_line_index] = the_lines
572
- self.dirty_content = new_lines.join("\n")
573
- self.caret_position = caret_position_for_line_index(new_line_index) + [old_caret_position_line_position, new_lines[new_line_index].size].min
574
- self.selection_count = old_selection_count.to_i if old_selection_count.to_i > 0
575
- end
576
-
577
- def run
578
- if scratchpad?
579
- eval content
580
- else
581
- write_dirty_content
582
- load path
583
- end
584
- end
585
-
586
- def lines
587
- need_padding = dirty_content.to_s.end_with?("\n")
588
- splittable_content = need_padding ? "#{dirty_content} " : dirty_content
589
- the_lines = splittable_content.split("\n")
590
- the_lines[-1] = the_lines[-1].strip if need_padding
591
- the_lines
592
- end
593
-
594
- def line_for_caret_position(caret_position)
595
- lines[line_index_for_caret_position(caret_position.to_i)]
596
- end
597
-
598
- def line_index_for_caret_position(caret_position)
599
- dirty_content[0...caret_position.to_i].count("\n")
600
- end
601
-
602
- def caret_position_for_line_index(line_index)
603
- cp = lines[0...line_index].join("\n").size
604
- cp += 1 if line_index > 0
605
- cp
606
- end
607
-
608
- def caret_position_for_caret_position_start_of_line(caret_position)
609
- caret_position_for_line_index(line_index_for_caret_position(caret_position))
610
- end
611
-
612
- # position within line containing "caret position" (e.g. for caret position 5 in 1st line, they match as 5, for 15 in line 2 with line 1 having 10 characters, line position is 4)
613
- # TODO consider renaming to line_character_position_for_caret_position
614
- def line_position_for_caret_position(caret_position)
615
- caret_position = caret_position.to_i
616
- caret_position - caret_position_for_caret_position_start_of_line(caret_position)
617
- end
618
-
619
- def line_caret_positions_for_selection(caret_position, selection_count)
620
- line_indices = line_indices_for_selection(caret_position, selection_count)
621
- line_caret_positions = line_indices.map { |line_index| caret_position_for_line_index(line_index) }.to_a
622
- end
623
-
624
- def end_caret_position_line_index(caret_position, selection_count)
625
- end_caret_position = caret_position + selection_count.to_i
626
- end_caret_position -= 1 if dirty_content[end_caret_position - 1] == "\n"
627
- end_line_index = line_index_for_caret_position(end_caret_position)
628
- end
629
-
630
- def lines_for_selection(caret_position, selection_count)
631
- line_indices = line_indices_for_selection(caret_position, selection_count)
632
- lines[line_indices.first..line_indices.last]
633
- end
634
-
635
- def line_indices_for_selection(caret_position, selection_count)
636
- start_line_index = line_index_for_caret_position(caret_position)
637
- if selection_count.to_i > 0
638
- end_line_index = end_caret_position_line_index(caret_position, selection_count)
639
- else
640
- end_line_index = start_line_index
641
- end
642
- (start_line_index..end_line_index).to_a
643
- end
644
-
645
- def children
646
- []
647
- end
648
-
649
- def to_s
650
- path
651
- end
652
-
653
- def eql?(other)
654
- self.path.eql?(other&.path)
655
- end
656
-
657
- def hash
658
- self.path.hash
659
- end
660
- end
661
- end
662
- end
1
+ module Glimmer
2
+ class Gladiator
3
+ class File
4
+ include Glimmer
5
+
6
+ attr_accessor :line_numbers_content, :line_number, :find_text, :replace_text, :top_pixel, :display_path, :case_sensitive, :caret_position, :selection_count, :last_caret_position, :last_selection_count, :line_position
7
+ attr_reader :name, :path, :project_dir
8
+
9
+ def initialize(path='', project_dir=nil)
10
+ raise "Not a file path: #{path}" if path.nil? || (!path.empty? && !::File.file?(path))
11
+ @project_dir = project_dir
12
+ @command_history = []
13
+ @name = path.empty? ? 'Scratchpad' : ::File.basename(path)
14
+ self.path = ::File.expand_path(path) unless path.empty?
15
+ @top_pixel = 0
16
+ @caret_position = 0
17
+ @selection_count = 0
18
+ @last_selection_count = 0
19
+ @line_number = 1
20
+ @init = nil
21
+ end
22
+
23
+ def language
24
+ # TODO consider using Rouge::Lexer.guess_by_filename instead and perhaps guess_by_source when it fails
25
+ extension = path.split('.').last if path.to_s.include?('.')
26
+ return 'ruby' if scratchpad?
27
+ return 'ruby' if path.to_s.end_with?('Gemfile') || path.to_s.end_with?('Rakefile')
28
+ return 'ruby' if dirty_content.start_with?('#!/usr/bin/env ruby') || dirty_content.start_with?('#!/usr/bin/env jruby')
29
+ return 'yaml' if path.to_s.end_with?('Gemfile.lock')
30
+ return 'shell' if extension.nil? && path.to_s.include?('/bin/')
31
+ case extension
32
+ # TODO extract case statement to an external config file
33
+ when 'rb'
34
+ 'ruby'
35
+ when 'md', 'markdown'
36
+ 'markdown'
37
+ when 'js', 'es6'
38
+ 'javascript'
39
+ when 'json'
40
+ 'json'
41
+ when 'yaml'
42
+ 'yaml'
43
+ when 'html'
44
+ 'html'
45
+ when 'h', 'c'
46
+ 'c'
47
+ when 'hs'
48
+ 'haskell'
49
+ when 'gradle'
50
+ 'gradle'
51
+ when 'cpp'
52
+ 'cpp'
53
+ when 'css'
54
+ 'css'
55
+ when 'java'
56
+ 'java'
57
+ when 'jsp'
58
+ 'jsp'
59
+ when 'plist'
60
+ 'plist'
61
+ when 'haml'
62
+ 'haml'
63
+ when 'xml'
64
+ 'xml'
65
+ when 'ini'
66
+ 'ini'
67
+ when 'pl'
68
+ 'perl'
69
+ when 'tcl'
70
+ 'tcl'
71
+ when 'sass'
72
+ 'sass'
73
+ when 'scss'
74
+ 'scss'
75
+ when 'sql'
76
+ 'sql'
77
+ when 'sh'
78
+ 'shell'
79
+ when 'vue'
80
+ 'vue'
81
+ when 'txt', nil
82
+ 'plain_text'
83
+ end
84
+ end
85
+
86
+ def init_content
87
+ unless @init
88
+ @init = true
89
+ begin
90
+ # test read dirty content
91
+ observe(self, :dirty_content) do
92
+ line_count = lines.empty? ? 1 : lines.size
93
+ lines_text_size = [line_count.to_s.size, 4].max
94
+ old_top_pixel = top_pixel
95
+ self.line_numbers_content = line_count.times.map {|n| (' ' * (lines_text_size - (n+1).to_s.size)) + (n+1).to_s }.join("\n")
96
+ self.top_pixel = old_top_pixel
97
+ end
98
+ the_dirty_content = read_dirty_content
99
+ the_dirty_content.split("\n") # test that it is not a binary file (crashes to rescue block otherwise)
100
+ self.dirty_content = the_dirty_content
101
+ observe(self, :caret_position) do |new_caret_position|
102
+ update_line_number_from_caret_position(new_caret_position)
103
+ end
104
+ observe(self, :line_number) do |new_line_number|
105
+ line_index = line_number - 1
106
+ new_caret_position = caret_position_for_line_index(line_index)
107
+ current_caret_position = caret_position
108
+ line_index_for_new_caret_position = line_index_for_caret_position(new_caret_position)
109
+ line_index_for_current_caret_position = line_index_for_caret_position(current_caret_position)
110
+ self.caret_position = new_caret_position unless (current_caret_position && line_index_for_new_caret_position == line_index_for_current_caret_position)
111
+ end
112
+ rescue # in case of a binary file
113
+ stop_filewatcher
114
+ end
115
+ end
116
+ end
117
+
118
+ def update_line_number_from_caret_position(new_caret_position)
119
+ new_line_number = line_index_for_caret_position(caret_position) + 1
120
+ current_line_number = line_number
121
+ unless (current_line_number && current_line_number == new_line_number)
122
+ self.line_number = new_line_number
123
+ # TODO check if the following line is needed
124
+ self.line_position = caret_position - caret_position_for_line_index(line_number - 1) + 1
125
+ end
126
+ end
127
+
128
+ def path=(the_path)
129
+ @path = the_path
130
+ generate_display_path
131
+ end
132
+
133
+ def generate_display_path
134
+ return if @path.empty?
135
+ @display_path = @path.sub(project_dir.path, '').sub(/^\//, '')
136
+ end
137
+
138
+ def name=(the_name)
139
+ new_path = path.sub(/#{Regexp.escape(@name)}$/, the_name) unless scratchpad?
140
+ @name = the_name
141
+ if !scratchpad? && ::File.exist?(path)
142
+ FileUtils.mv(path, new_path)
143
+ self.path = new_path
144
+ end
145
+ end
146
+
147
+ def scratchpad?
148
+ path.to_s.empty? # TODO consider updating to set the .gladiator-scratchpad path directly
149
+ end
150
+
151
+ def scratchpad_file
152
+ scratchpad_file = ::File.join(project_dir.path, '.gladiator-scratchpad')
153
+ ::File.write(scratchpad_file, content)
154
+ scratchpad_file
155
+ end
156
+
157
+ def backup_properties
158
+ [:find_text, :replace_text, :case_sensitive, :top_pixel, :caret_position, :selection_count].reduce({}) do |hash, property|
159
+ hash.merge(property => send(property))
160
+ end
161
+ end
162
+
163
+ def restore_properties(properties_hash)
164
+ return if properties_hash[:caret_position] == 0 && properties_hash[:selection_count] == 0 && properties_hash[:find_text].nil? && properties_hash[:replace_text].nil? && properties_hash[:top_pixel] == 0 && properties_hash[:case_sensitive].nil?
165
+ properties_hash.each do |property, value|
166
+ send("#{property}=", value)
167
+ end
168
+ end
169
+
170
+ def caret_position=(value)
171
+ @last_caret_position = @caret_position
172
+ @caret_position = value
173
+ end
174
+
175
+ def selection_count=(value)
176
+ #@last_selection_count = @selection_count
177
+ @selection_count = value
178
+ @last_selection_count = @selection_count
179
+ end
180
+
181
+ def dirty_content
182
+ init_content
183
+ @dirty_content
184
+ end
185
+
186
+ def dirty_content=(the_content)
187
+ # TODO set partial dirty content by line(s) for enhanced performance
188
+ @dirty_content = the_content
189
+ old_caret_position = caret_position
190
+ old_top_pixel = top_pixel
191
+
192
+ notify_observers(:content)
193
+ if @formatting_dirty_content_for_writing
194
+ self.caret_position = old_caret_position
195
+ self.top_pixel = old_top_pixel
196
+ end
197
+ end
198
+
199
+ def content
200
+ dirty_content
201
+ end
202
+
203
+ # to use for widget data-binding
204
+ def content=(value)
205
+ value = value.gsub("\t", ' ')
206
+ if dirty_content != value
207
+ Command.do(self, :change_content!, value)
208
+ end
209
+ end
210
+
211
+ def change_content!(value)
212
+ self.dirty_content = value
213
+ format_dirty_content_for_writing!(force: true) if value.to_s.include?("\r\n")
214
+ update_line_number_from_caret_position(caret_position)
215
+ end
216
+
217
+ def start_command
218
+ @commmand_in_progress = true
219
+ end
220
+
221
+ def end_command
222
+ @commmand_in_progress = false
223
+ end
224
+
225
+ def command_in_progress?
226
+ @commmand_in_progress
227
+ end
228
+
229
+ def close
230
+ @closed = true
231
+ stop_filewatcher
232
+ remove_all_observers
233
+ initialize(path, project_dir)
234
+ Command.clear(self)
235
+ end
236
+
237
+ def closed?
238
+ @closed
239
+ end
240
+
241
+ def read_dirty_content
242
+ path.empty? ? '' : ::File.read(path)
243
+ end
244
+
245
+ def start_filewatcher
246
+ return if scratchpad?
247
+ @filewatcher ||= Filewatcher.new(@path)
248
+ @filewatcher_thread ||= Thread.new(@filewatcher) do |fw|
249
+ fw.watch do |filename, event|
250
+ async_exec do
251
+ begin
252
+ self.dirty_content = read_dirty_content if read_dirty_content != dirty_content
253
+ rescue StandardError, Errno::ENOENT
254
+ # in case of a binary file
255
+ stop_filewatcher
256
+ end
257
+ end
258
+ end
259
+ end
260
+ end
261
+
262
+ def stop_filewatcher
263
+ @filewatcher&.stop
264
+ @filewatcher_thread&.kill
265
+ @filewatcher_thread = nil
266
+ @filewatcher&.finalize
267
+ @filewatcher = nil
268
+ end
269
+
270
+ def write_dirty_content
271
+ # TODO write partial dirty content by line(s) for enhanced performance
272
+ return if scratchpad? || !::File.exist?(path) || !::File.exists?(path) || read_dirty_content == dirty_content
273
+ format_dirty_content_for_writing!
274
+ ::File.write(path, dirty_content)
275
+ rescue StandardError, ArgumentError => e
276
+ puts "Error in writing dirty content for #{path}"
277
+ puts e.full_message
278
+ end
279
+
280
+ def format_dirty_content_for_writing!(force: false)
281
+ return if !force && @commmand_in_progress
282
+ new_dirty_content = dirty_content.to_s.split("\n").map {|line| line.strip.empty? ? line : line.rstrip }.join("\n")
283
+ new_dirty_content = "#{new_dirty_content.gsub("\r\n", "\n").gsub("\r", "\n").sub(/\n+\z/, '')}\n"
284
+ if new_dirty_content != self.dirty_content
285
+ @formatting_dirty_content_for_writing = true
286
+ caret_position_diff = dirty_content.to_s.size - new_dirty_content.size
287
+ self.dirty_content = new_dirty_content
288
+ self.caret_position = caret_position - caret_position_diff
289
+ @formatting_dirty_content_for_writing = false
290
+ end
291
+ end
292
+
293
+ def write_raw_dirty_content
294
+ return if scratchpad? || !::File.exist?(path)
295
+ ::File.write(path, dirty_content) if ::File.exists?(path)
296
+ rescue => e
297
+ puts "Error in writing raw dirty content for #{path}"
298
+ puts e.full_message
299
+ end
300
+
301
+ def current_line_indentation
302
+ current_line.to_s.match(/^(\s+)/).to_a[1].to_s
303
+ end
304
+
305
+ def current_line
306
+ lines[line_number - 1]
307
+ end
308
+
309
+ def delete!
310
+ FileUtils.rm(path) unless scratchpad?
311
+ end
312
+
313
+ def prefix_new_line!
314
+ the_lines = lines
315
+ the_lines[line_number-1...line_number-1] = [current_line_indentation]
316
+ self.dirty_content = the_lines.join("\n")
317
+ self.caret_position = caret_position_for_line_index(line_number-1) + current_line_indentation.size
318
+ self.selection_count = 0
319
+ end
320
+
321
+ def insert_new_line!
322
+ the_lines = lines
323
+ the_lines[line_number...line_number] = [current_line_indentation]
324
+ self.dirty_content = the_lines.join("\n")
325
+ self.caret_position = caret_position_for_line_index(line_number) + current_line_indentation.size
326
+ self.selection_count = 0
327
+ end
328
+
329
+ def comment_line!
330
+ old_lines = lines
331
+ return if old_lines.size < 1
332
+ old_selection_count = self.selection_count
333
+ old_caret_position = self.caret_position
334
+ old_caret_position_line_index = line_index_for_caret_position(old_caret_position)
335
+ old_caret_position_line_caret_position = caret_position_for_line_index(old_caret_position_line_index)
336
+ old_end_caret_line_index = end_caret_position_line_index(caret_position, selection_count)
337
+ new_lines = lines
338
+ delta = 0
339
+ line_indices_for_selection(caret_position, selection_count).reverse.each do | the_line_index |
340
+ delta = 0
341
+ the_line = old_lines[the_line_index]
342
+ return if the_line.nil?
343
+ if the_line.strip.start_with?('# ')
344
+ new_lines[the_line_index] = the_line.sub(/# /, '')
345
+ delta -= 2
346
+ elsif the_line.strip.start_with?('#')
347
+ new_lines[the_line_index] = the_line.sub(/#/, '')
348
+ delta -= 1
349
+ else
350
+ new_lines[the_line_index] = "# #{the_line}"
351
+ delta += 2
352
+ end
353
+ end
354
+ self.dirty_content = new_lines.join("\n")
355
+ if old_selection_count.to_i > 0
356
+ self.caret_position = caret_position_for_line_index(old_caret_position_line_index)
357
+ self.selection_count = (caret_position_for_line_index(old_end_caret_line_index + 1) - self.caret_position)
358
+ else
359
+ new_caret_position = old_caret_position + delta
360
+ new_caret_position = [new_caret_position, old_caret_position_line_caret_position].max
361
+ self.caret_position = new_caret_position
362
+ self.selection_count = 0
363
+ end
364
+ end
365
+
366
+ def indent!
367
+ new_lines = lines
368
+ old_lines = lines
369
+ return if old_lines.size < 1
370
+ old_selection_count = self.selection_count
371
+ old_caret_position = self.caret_position
372
+ old_caret_position_line_index = line_index_for_caret_position(old_caret_position)
373
+ old_caret_position_line_caret_position = caret_position_for_line_index(old_caret_position_line_index)
374
+ old_end_caret_line_index = end_caret_position_line_index(caret_position, selection_count)
375
+ delta = 2
376
+ line_indices_for_selection(caret_position, selection_count).each do |the_line_index|
377
+ the_line = old_lines[the_line_index]
378
+ new_lines[the_line_index] = " #{the_line}"
379
+ end
380
+ old_caret_position = self.caret_position
381
+ self.dirty_content = new_lines.join("\n")
382
+ if old_selection_count.to_i > 0
383
+ self.caret_position = caret_position_for_line_index(old_caret_position_line_index)
384
+ self.selection_count = (caret_position_for_line_index(old_end_caret_line_index + 1) - self.caret_position)
385
+ else
386
+ self.caret_position = old_caret_position + delta
387
+ self.selection_count = 0
388
+ end
389
+ end
390
+
391
+ def outdent!
392
+ new_lines = lines
393
+ old_lines = lines
394
+ return if old_lines.size < 1
395
+ old_selection_count = self.selection_count
396
+ old_caret_position = self.caret_position
397
+ old_caret_position_line_index = line_index_for_caret_position(old_caret_position)
398
+ old_caret_position_line_caret_position = caret_position_for_line_index(old_caret_position_line_index)
399
+ old_end_caret_line_index = end_caret_position_line_index(caret_position, selection_count)
400
+ delta = 0
401
+ line_indices_for_selection(caret_position, selection_count).each do |the_line_index|
402
+ the_line = old_lines[the_line_index]
403
+ if the_line.to_s.start_with?(' ')
404
+ new_lines[the_line_index] = the_line.sub(/ /, '')
405
+ delta = -2
406
+ elsif the_line&.start_with?(' ')
407
+ new_lines[the_line_index] = the_line.sub(/ /, '')
408
+ delta = -1
409
+ end
410
+ end
411
+ self.dirty_content = new_lines.join("\n")
412
+ if old_selection_count.to_i > 0
413
+ self.caret_position = caret_position_for_line_index(old_caret_position_line_index)
414
+ self.selection_count = (caret_position_for_line_index(old_end_caret_line_index + 1) - self.caret_position)
415
+ else
416
+ new_caret_position = old_caret_position + delta
417
+ new_caret_position = [new_caret_position, old_caret_position_line_caret_position].max
418
+ self.caret_position = new_caret_position
419
+ self.selection_count = 0
420
+ end
421
+ end
422
+
423
+ def kill_line!
424
+ new_lines = lines
425
+ return if new_lines.size < 1
426
+ line_indices = line_indices_for_selection(caret_position, selection_count)
427
+ new_lines = new_lines[0...line_indices.first] + new_lines[(line_indices.last+1)...new_lines.size]
428
+ old_caret_position = self.caret_position
429
+ old_line_index = self.line_number - 1
430
+ line_position = line_position_for_caret_position(old_caret_position)
431
+ self.dirty_content = "#{new_lines.join("\n")}\n"
432
+ self.caret_position = caret_position_for_line_index(old_line_index) + [line_position, lines[old_line_index].to_s.size].min
433
+ self.selection_count = 0
434
+ end
435
+
436
+ def duplicate_line!
437
+ new_lines = lines
438
+ old_lines = lines
439
+ return if old_lines.size < 1
440
+ old_selection_count = self.selection_count
441
+ old_caret_position = self.caret_position
442
+ old_caret_position_line_index = line_index_for_caret_position(old_caret_position)
443
+ old_caret_position_line_caret_position = caret_position_for_caret_position_start_of_line(old_caret_position_line_index)
444
+ old_end_caret_line_index = end_caret_position_line_index(caret_position, selection_count)
445
+ the_line_indices = line_indices_for_selection(caret_position, selection_count)
446
+ the_lines = lines_for_selection(caret_position, selection_count)
447
+ delta = the_lines.join("\n").size + 1
448
+ the_lines.each_with_index do |the_line, i|
449
+ new_lines.insert(the_line_indices.first + i, the_line)
450
+ end
451
+ self.dirty_content = new_lines.join("\n")
452
+ if old_selection_count.to_i > 0
453
+ self.caret_position = caret_position_for_line_index(old_caret_position_line_index)
454
+ self.selection_count = (caret_position_for_line_index(old_end_caret_line_index + 1) - self.caret_position)
455
+ else
456
+ self.caret_position = old_caret_position + delta
457
+ self.selection_count = 0
458
+ end
459
+ end
460
+
461
+ def find_next
462
+ return if find_text.to_s.empty?
463
+ all_lines = lines
464
+ the_line_index = line_index_for_caret_position(caret_position)
465
+ line_position = line_position_for_caret_position(caret_position)
466
+ found = found_text?(caret_position)
467
+ 2.times do |i|
468
+ rotation = the_line_index
469
+ all_lines.rotate(rotation).each_with_index do |the_line, the_index|
470
+ the_index = (the_index + rotation)%all_lines.size
471
+ start_position = 0
472
+ start_position = line_position + find_text.to_s.size if i == 0 && the_index == the_line_index && found_text?(caret_position)
473
+ text_to_find_in = the_line[start_position..-1]
474
+ occurrence_index = case_sensitive ? text_to_find_in&.index(find_text.to_s) : text_to_find_in&.downcase&.index(find_text.to_s.downcase)
475
+ if occurrence_index
476
+ self.caret_position = caret_position_for_line_index(the_index) + start_position + occurrence_index
477
+ self.selection_count = find_text.to_s.size
478
+ return
479
+ end
480
+ end
481
+ end
482
+ end
483
+
484
+ def find_previous
485
+ return if find_text.to_s.empty?
486
+ all_lines = lines
487
+ the_line_index = line_index_for_caret_position(caret_position)
488
+ line_position = line_position_for_caret_position(caret_position)
489
+ 2.times do |i|
490
+ rotation = - the_line_index - 1 + all_lines.size
491
+ all_lines.reverse.rotate(rotation).each_with_index do |the_line, the_index|
492
+ the_index = all_lines.size - 1 - (the_index + rotation)%all_lines.size
493
+ if the_index == the_line_index
494
+ start_position = i > 0 ? 0 : (the_line.size - line_position)
495
+ else
496
+ start_position = 0
497
+ end
498
+ text_to_find_in = the_line.downcase.reverse[start_position...the_line.size].to_s
499
+ occurrence_index = text_to_find_in.index(find_text.to_s.downcase.reverse)
500
+ if occurrence_index
501
+ self.caret_position = caret_position_for_line_index(the_index) + (the_line.size - (start_position + occurrence_index + find_text.to_s.size))
502
+ self.selection_count = find_text.to_s.size
503
+ return
504
+ end
505
+ end
506
+ end
507
+ end
508
+
509
+ def ensure_find_next
510
+ return if find_text.to_s.empty? || dirty_content.to_s.strip.size < 1
511
+ find_next unless found_text?(self.caret_position)
512
+ end
513
+
514
+ def found_text?(caret_position)
515
+ dirty_content[caret_position.to_i, find_text.to_s.size].to_s.downcase == find_text.to_s.downcase
516
+ end
517
+
518
+ def replace_next!
519
+ return if find_text.to_s.empty? || dirty_content.to_s.strip.size < 1
520
+ ensure_find_next
521
+ new_dirty_content = dirty_content
522
+ new_dirty_content[caret_position, find_text.size] = replace_text.to_s
523
+ self.dirty_content = new_dirty_content
524
+ find_next
525
+ find_next if replace_text.to_s.include?(find_text) && !replace_text.to_s.start_with?(find_text)
526
+ write_dirty_content
527
+ end
528
+
529
+ def page_up
530
+ self.line_number = [(self.line_number - 15), 1].max
531
+ self.selection_count = 0
532
+ end
533
+
534
+ def page_down
535
+ self.line_number = [(self.line_number + 15), lines.size].min
536
+ self.selection_count = 0
537
+ end
538
+
539
+ def home
540
+ self.line_number = 1
541
+ self.selection_count = 0
542
+ end
543
+
544
+ def end
545
+ self.line_number = lines.size
546
+ self.selection_count = 0
547
+ end
548
+
549
+ def start_of_line
550
+ self.caret_position = caret_position_for_line_index(self.line_number - 1)
551
+ self.selection_count = 0
552
+ end
553
+
554
+ def end_of_line
555
+ self.caret_position = caret_position_for_line_index(self.line_number) - 1
556
+ self.selection_count = 0
557
+ end
558
+
559
+ def select_to_start_of_line
560
+ old_caret_position = caret_position
561
+ self.caret_position = caret_position_for_line_index(self.line_number - 1)
562
+ self.selection_count = old_caret_position - caret_position
563
+ end
564
+
565
+ def select_to_end_of_line
566
+ self.caret_position = selection_count == 0 ? caret_position : caret_position + selection_count
567
+ self.selection_count = (caret_position_for_line_index(self.line_number) - 1) - caret_position
568
+ end
569
+
570
+ def delete!
571
+ new_dirty_content = dirty_content
572
+ new_dirty_content[caret_position...(caret_position + selection_count)] = ''
573
+ old_caret_position = caret_position
574
+ self.dirty_content = new_dirty_content
575
+ self.caret_position = old_caret_position
576
+ end
577
+
578
+ def cut!
579
+ Clipboard.copy(selected_text)
580
+ delete!
581
+ end
582
+
583
+ def copy
584
+ Clipboard.copy(selected_text)
585
+ end
586
+
587
+ def paste!
588
+ new_dirty_content = dirty_content
589
+ pasted_text = Clipboard.paste
590
+ new_dirty_content[caret_position...(caret_position + selection_count)] = pasted_text
591
+ old_caret_position = caret_position
592
+ self.dirty_content = new_dirty_content
593
+ self.caret_position = old_caret_position + pasted_text.to_s.size
594
+ self.selection_count = 0
595
+ end
596
+
597
+ def select_all
598
+ self.caret_position = 0
599
+ self.selection_count = dirty_content.to_s.size
600
+ end
601
+
602
+ def selected_text
603
+ dirty_content.to_s[caret_position, selection_count]
604
+ end
605
+
606
+ def move_up!
607
+ old_lines = lines
608
+ return if old_lines.size < 2
609
+ old_selection_count = self.selection_count
610
+ old_caret_position = self.caret_position
611
+ old_caret_position_line_caret_position = caret_position_for_caret_position_start_of_line(old_caret_position)
612
+ old_caret_position_line_position = old_caret_position - old_caret_position_line_caret_position
613
+ old_end_caret_line_index = end_caret_position_line_index(caret_position, selection_count)
614
+ new_lines = lines
615
+ the_line_indices = line_indices_for_selection(caret_position, selection_count)
616
+ the_lines = lines_for_selection(caret_position, selection_count)
617
+ new_line_index = [the_line_indices.first - 1, 0].max
618
+ new_lines[the_line_indices.first..the_line_indices.last] = []
619
+ new_lines[new_line_index...new_line_index] = the_lines
620
+ self.dirty_content = new_lines.join("\n")
621
+ self.caret_position = caret_position_for_line_index(new_line_index) + [old_caret_position_line_position, new_lines[new_line_index].size].min
622
+ self.selection_count = old_selection_count.to_i if old_selection_count.to_i > 0
623
+ end
624
+
625
+ def move_down!
626
+ old_lines = lines
627
+ return if old_lines.size < 2
628
+ old_selection_count = self.selection_count
629
+ old_caret_position = self.caret_position
630
+ old_caret_position_line_caret_position = caret_position_for_caret_position_start_of_line(old_caret_position)
631
+ old_caret_position_line_position = old_caret_position - old_caret_position_line_caret_position
632
+ old_end_caret_line_index = end_caret_position_line_index(caret_position, selection_count)
633
+ new_lines = lines
634
+ the_line_indices = line_indices_for_selection(caret_position, selection_count)
635
+ the_lines = lines_for_selection(caret_position, selection_count)
636
+ new_line_index = [the_line_indices.first + 1, new_lines.size - 1].min
637
+ new_lines[the_line_indices.first..the_line_indices.last] = []
638
+ new_lines[new_line_index...new_line_index] = the_lines
639
+ self.dirty_content = new_lines.join("\n")
640
+ self.caret_position = caret_position_for_line_index(new_line_index) + [old_caret_position_line_position, new_lines[new_line_index].size].min
641
+ self.selection_count = old_selection_count.to_i if old_selection_count.to_i > 0
642
+ end
643
+
644
+ def run
645
+ if scratchpad?
646
+ load scratchpad_file
647
+ else
648
+ write_dirty_content
649
+ load path
650
+ end
651
+ end
652
+
653
+ def lines
654
+ need_padding = dirty_content.to_s.end_with?("\n")
655
+ splittable_content = need_padding ? "#{dirty_content} " : dirty_content
656
+ the_lines = splittable_content.split("\n")
657
+ the_lines[-1] = the_lines[-1].strip if need_padding
658
+ the_lines
659
+ end
660
+
661
+ def line_for_caret_position(caret_position)
662
+ lines[line_index_for_caret_position(caret_position.to_i)]
663
+ end
664
+
665
+ def line_index_for_caret_position(caret_position)
666
+ dirty_content[0...caret_position.to_i].count("\n")
667
+ end
668
+
669
+ def caret_position_for_line_index(line_index)
670
+ cp = lines[0...line_index].join("\n").size
671
+ cp += 1 if line_index > 0
672
+ cp
673
+ end
674
+
675
+ def caret_position_for_caret_position_start_of_line(caret_position)
676
+ caret_position_for_line_index(line_index_for_caret_position(caret_position))
677
+ end
678
+
679
+ # position within line containing "caret position" (e.g. for caret position 5 in 1st line, they match as 5, for 15 in line 2 with line 1 having 10 characters, line position is 4)
680
+ # TODO consider renaming to line_character_position_for_caret_position
681
+ def line_position_for_caret_position(caret_position)
682
+ caret_position = caret_position.to_i
683
+ caret_position - caret_position_for_caret_position_start_of_line(caret_position)
684
+ end
685
+
686
+ def line_caret_positions_for_selection(caret_position, selection_count)
687
+ line_indices = line_indices_for_selection(caret_position, selection_count)
688
+ line_caret_positions = line_indices.map { |line_index| caret_position_for_line_index(line_index) }.to_a
689
+ end
690
+
691
+ def end_caret_position_line_index(caret_position, selection_count)
692
+ end_caret_position = caret_position + selection_count.to_i
693
+ end_caret_position -= 1 if dirty_content[end_caret_position - 1] == "\n"
694
+ end_line_index = line_index_for_caret_position(end_caret_position)
695
+ end
696
+
697
+ def lines_for_selection(caret_position, selection_count)
698
+ line_indices = line_indices_for_selection(caret_position, selection_count)
699
+ lines[line_indices.first..line_indices.last]
700
+ end
701
+
702
+ def line_indices_for_selection(caret_position, selection_count)
703
+ start_line_index = line_index_for_caret_position(caret_position)
704
+ if selection_count.to_i > 0
705
+ end_line_index = end_caret_position_line_index(caret_position, selection_count)
706
+ else
707
+ end_line_index = start_line_index
708
+ end
709
+ (start_line_index..end_line_index).to_a
710
+ end
711
+
712
+ def children
713
+ []
714
+ end
715
+
716
+ def to_s
717
+ path
718
+ end
719
+
720
+ def eql?(other)
721
+ self.path.eql?(other&.path)
722
+ end
723
+
724
+ def hash
725
+ self.path.hash
726
+ end
727
+ end
728
+ end
729
+ end