glimmer-cs-gladiator 0.8.1 → 0.9.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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