glimmer-cs-gladiator 0.7.0 → 0.8.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,29 +1,30 @@
1
- $LOAD_PATH.unshift(File.expand_path('..', __FILE__))
2
-
3
- require 'filewatcher'
4
- require 'clipboard'
5
- require 'puts_debuggerer'
6
- require 'views/glimmer/gladiator'
7
-
8
- # Custom Composite Initializer (avoid default margins)
9
- Glimmer::SWT::WidgetProxy::DEFAULT_INITIALIZERS['composite'] = lambda do |composite|
10
- if composite.get_layout.nil?
11
- layout = GridLayout.new
12
- composite.layout = layout
13
- end
14
- end
15
-
16
- # Custom LayoutProxy initialize method (avoid default margins)
17
- module Glimmer
18
- module SWT
19
- class LayoutProxy
20
- def initialize(underscored_layout_name, widget_proxy, args)
21
- @underscored_layout_name = underscored_layout_name
22
- @widget_proxy = widget_proxy
23
- args = SWTProxy.constantify_args(args)
24
- @swt_layout = self.class.swt_layout_class_for(underscored_layout_name).new(*args)
25
- @widget_proxy.swt_widget.setLayout(@swt_layout)
26
- end
27
- end
28
- end
29
- end
1
+ $LOAD_PATH.unshift(File.expand_path('..', __FILE__))
2
+
3
+ require 'glimmer-dsl-swt'
4
+ require 'filewatcher'
5
+ require 'clipboard'
6
+ require 'puts_debuggerer'
7
+ require 'views/glimmer/gladiator'
8
+
9
+ # Custom Composite Initializer (avoid default margins)
10
+ Glimmer::SWT::WidgetProxy::DEFAULT_INITIALIZERS['composite'] = lambda do |composite|
11
+ if composite.get_layout.nil?
12
+ layout = GridLayout.new
13
+ composite.layout = layout
14
+ end
15
+ end
16
+
17
+ # Custom LayoutProxy initialize method (avoid default margins)
18
+ module Glimmer
19
+ module SWT
20
+ class LayoutProxy
21
+ def initialize(underscored_layout_name, widget_proxy, args)
22
+ @underscored_layout_name = underscored_layout_name
23
+ @widget_proxy = widget_proxy
24
+ args = SWTProxy.constantify_args(args)
25
+ @swt_layout = self.class.swt_layout_class_for(underscored_layout_name).new(*args)
26
+ @widget_proxy.swt_widget.setLayout(@swt_layout)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,6 @@
1
+ require_relative '../glimmer-cs-gladiator'
2
+
3
+ include Glimmer
4
+
5
+ local_dir = ENV['LOCAL_DIR'] || '.'
6
+ gladiator(project_dir_path: local_dir).open
@@ -1,112 +1,112 @@
1
- module Glimmer
2
- class Gladiator
3
- class Command
4
- include Glimmer
5
-
6
- class << self
7
- include Glimmer
8
-
9
- def command_history
10
- @command_history ||= {}
11
- end
12
-
13
- def command_history_for(file)
14
- # keeping a first command to make redo support work by remembering next command after undoing all
15
- command_history[file] ||= [Command.new(file)]
16
- end
17
-
18
- def do(file, method = nil, *args, command: nil)
19
- if command.nil?
20
- command ||= Command.new(file, method, *args)
21
- command.previous_command = command_history_for(file).last
22
- unless command_history_for(file).last.method == :change_content! && method == :change_content!
23
- command_history_for(file).last.next_command = command
24
- end
25
- command.do
26
- command_history_for(file) << command unless command_history_for(file).last.method == :change_content! && method == :change_content!
27
- else
28
- command_history_for(file) << command
29
- end
30
- end
31
-
32
- def undo(file)
33
- return if command_history_for(file).size <= 1
34
- command = command_history_for(file).pop
35
- command&.undo
36
- end
37
-
38
- def redo(file)
39
- command = command_history_for(file).last
40
- command&.redo
41
- end
42
-
43
- def clear(file)
44
- command_history[file] = [Command.new(file)]
45
- end
46
- end
47
-
48
- attr_accessor :file, :method, :args, :previous_command, :next_command,
49
- :file_dirty_content, :file_caret_position, :file_selection_count, :previous_file_dirty_content, :previous_file_caret_position, :previous_file_selection_count
50
-
51
- def initialize(file, method = nil, *args)
52
- @file = file
53
- @method = method
54
- @args = args
55
- end
56
-
57
-
58
- def native?
59
- @method.nil?
60
- end
61
-
62
- def do
63
- return if native?
64
- backup
65
- execute
66
- end
67
-
68
- def undo
69
- return if native?
70
- restore
71
- end
72
-
73
- def redo
74
- return if next_command.nil?# || next_command.native?
75
- @file.dirty_content = next_command.file_dirty_content.clone
76
- @file.caret_position = next_command.file_caret_position
77
- @file.selection_count = next_command.file_selection_count
78
- Command.do(next_command.file, command: next_command)
79
- end
80
-
81
- def backup
82
- @previous_file_dirty_content = @file.dirty_content.clone
83
- @previous_file_caret_position = @file.caret_position
84
- @previous_file_selection_count = @file.selection_count
85
- if @method == :change_content!
86
- @previous_file_caret_position = @file.last_caret_position
87
- @previous_file_selection_count = @file.last_selection_count
88
- end
89
- end
90
-
91
- def restore
92
- @file.dirty_content = @previous_file_dirty_content.clone
93
- @file.caret_position = @previous_file_caret_position
94
- @file.selection_count = @previous_file_selection_count
95
- end
96
-
97
- def execute
98
- @file.start_command
99
- @file.send(@method, *@args)
100
- @file.end_command
101
- @file_dirty_content = @file.dirty_content.clone
102
- @file_caret_position = @file.caret_position
103
- @file_selection_count = @file.selection_count
104
- if previous_command.method == :change_content! && @method == :change_content!
105
- previous_command.file_dirty_content = @file_dirty_content
106
- previous_command.file_caret_position = @file_caret_position
107
- previous_command.file_selection_count = @file_selection_count
108
- end
109
- end
110
- end
111
- end
112
- end
1
+ module Glimmer
2
+ class Gladiator
3
+ class Command
4
+ include Glimmer
5
+
6
+ class << self
7
+ include Glimmer
8
+
9
+ def command_history
10
+ @command_history ||= {}
11
+ end
12
+
13
+ def command_history_for(file)
14
+ # keeping a first command to make redo support work by remembering next command after undoing all
15
+ command_history[file] ||= [Command.new(file)]
16
+ end
17
+
18
+ def do(file, method = nil, *args, command: nil)
19
+ if command.nil?
20
+ command ||= Command.new(file, method, *args)
21
+ command.previous_command = command_history_for(file).last
22
+ unless command_history_for(file).last.method == :change_content! && method == :change_content!
23
+ command_history_for(file).last.next_command = command
24
+ end
25
+ command.do
26
+ command_history_for(file) << command unless command_history_for(file).last.method == :change_content! && method == :change_content!
27
+ else
28
+ command_history_for(file) << command
29
+ end
30
+ end
31
+
32
+ def undo(file)
33
+ return if command_history_for(file).size <= 1
34
+ command = command_history_for(file).pop
35
+ command&.undo
36
+ end
37
+
38
+ def redo(file)
39
+ command = command_history_for(file).last
40
+ command&.redo
41
+ end
42
+
43
+ def clear(file)
44
+ command_history[file] = [Command.new(file)]
45
+ end
46
+ end
47
+
48
+ attr_accessor :file, :method, :args, :previous_command, :next_command,
49
+ :file_dirty_content, :file_caret_position, :file_selection_count, :previous_file_dirty_content, :previous_file_caret_position, :previous_file_selection_count
50
+
51
+ def initialize(file, method = nil, *args)
52
+ @file = file
53
+ @method = method
54
+ @args = args
55
+ end
56
+
57
+
58
+ def native?
59
+ @method.nil?
60
+ end
61
+
62
+ def do
63
+ return if native?
64
+ backup
65
+ execute
66
+ end
67
+
68
+ def undo
69
+ return if native?
70
+ restore
71
+ end
72
+
73
+ def redo
74
+ return if next_command.nil?# || next_command.native?
75
+ @file.dirty_content = next_command.file_dirty_content.clone
76
+ @file.caret_position = next_command.file_caret_position
77
+ @file.selection_count = next_command.file_selection_count
78
+ Command.do(next_command.file, command: next_command)
79
+ end
80
+
81
+ def backup
82
+ @previous_file_dirty_content = @file.dirty_content.clone
83
+ @previous_file_caret_position = @file.caret_position
84
+ @previous_file_selection_count = @file.selection_count
85
+ if @method == :change_content!
86
+ @previous_file_caret_position = @file.last_caret_position
87
+ @previous_file_selection_count = @file.last_selection_count
88
+ end
89
+ end
90
+
91
+ def restore
92
+ @file.dirty_content = @previous_file_dirty_content.clone
93
+ @file.caret_position = @previous_file_caret_position
94
+ @file.selection_count = @previous_file_selection_count
95
+ end
96
+
97
+ def execute
98
+ @file.start_command
99
+ @file.send(@method, *@args)
100
+ @file.end_command
101
+ @file_dirty_content = @file.dirty_content.clone
102
+ @file_caret_position = @file.caret_position
103
+ @file_selection_count = @file.selection_count
104
+ if previous_command.method == :change_content! && @method == :change_content!
105
+ previous_command.file_dirty_content = @file_dirty_content
106
+ previous_command.file_caret_position = @file_caret_position
107
+ previous_command.file_selection_count = @file_selection_count
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
@@ -5,6 +5,8 @@ module Glimmer
5
5
  class Dir
6
6
  include Glimmer
7
7
  include Glimmer::DataBinding::ObservableModel
8
+
9
+ IGNORE_PATHS = ['.gladiator', '.git', 'coverage', 'packages', 'node_modules', 'tmp', 'vendor', 'pkg', 'dist']
8
10
 
9
11
  attr_accessor :selected_child, :filter, :children, :filtered_path_options, :filtered_path, :display_path, :ignore_paths
10
12
  attr_reader :name, :parent, :path
@@ -36,7 +38,7 @@ module Glimmer
36
38
  end
37
39
  self.path = ::File.expand_path(path)
38
40
  @name = ::File.basename(::File.expand_path(path))
39
- @ignore_paths = ['.gladiator', '.git', 'coverage', 'packages', 'node_modules', 'tmp', 'vendor']
41
+ @ignore_paths = IGNORE_PATHS
40
42
  self.filtered_path_options = []
41
43
  end
42
44
 
@@ -72,8 +74,10 @@ module Glimmer
72
74
  def retrieve_children
73
75
  @children = ::Dir.glob(::File.join(@path, '*')).reject do |p|
74
76
  # TODO make sure to configure ignore_paths in a preferences dialog
75
- project_dir.ignore_paths.reduce(false) do |result, ignore_path|
76
- result || p.include?(ignore_path)
77
+ if project_dir == self
78
+ project_dir.ignore_paths.any? do |ignore_path|
79
+ p.include?(ignore_path)
80
+ end
77
81
  end
78
82
  end.map do |p|
79
83
  ::File.file?(p) ? File.new(p, project_dir) : Dir.new(p, project_dir)
@@ -93,7 +97,7 @@ module Glimmer
93
97
  def depth_first_search_file(dir, file_path)
94
98
  dir.children.each do |child|
95
99
  if child.is_a?(File)
96
- return child if child.path.include?(file_path)
100
+ return child if child.path.include?(file_path.to_s)
97
101
  else
98
102
  result = depth_first_search_file(child, file_path)
99
103
  return result unless result.nil?
@@ -1,599 +1,662 @@
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 init_content
24
- unless @init
25
- @init = true
26
- begin
27
- # test read dirty content
28
- observe(self, :dirty_content) do
29
- line_count = lines.empty? ? 1 : lines.size
30
- lines_text_size = [line_count.to_s.size, 4].max
31
- old_top_pixel = top_pixel
32
- self.line_numbers_content = line_count.times.map {|n| (' ' * (lines_text_size - (n+1).to_s.size)) + (n+1).to_s }.join("\n")
33
- self.top_pixel = old_top_pixel
34
- end
35
- the_dirty_content = read_dirty_content
36
- the_dirty_content.split("\n") # test that it is not a binary file (crashes to rescue block otherwise)
37
- self.dirty_content = the_dirty_content
38
- observe(self, :caret_position) do |new_caret_position|
39
- update_line_number_from_caret_position(new_caret_position)
40
- end
41
- observe(self, :line_number) do |new_line_number|
42
- line_index = line_number - 1
43
- new_caret_position = caret_position_for_line_index(line_index)
44
- current_caret_position = caret_position
45
- line_index_for_new_caret_position = line_index_for_caret_position(new_caret_position)
46
- line_index_for_current_caret_position = line_index_for_caret_position(current_caret_position)
47
- self.caret_position = new_caret_position unless (current_caret_position && line_index_for_new_caret_position == line_index_for_current_caret_position)
48
- end
49
- rescue # in case of a binary file
50
- stop_filewatcher
51
- end
52
- end
53
- end
54
-
55
- def update_line_number_from_caret_position(new_caret_position)
56
- new_line_number = line_index_for_caret_position(caret_position) + 1
57
- current_line_number = line_number
58
- unless (current_line_number && current_line_number == new_line_number)
59
- self.line_number = new_line_number
60
- # TODO check if the following line is needed
61
- self.line_position = caret_position - caret_position_for_line_index(line_number - 1) + 1
62
- end
63
- end
64
-
65
- def path=(the_path)
66
- @path = the_path
67
- generate_display_path
68
- end
69
-
70
- def generate_display_path
71
- return if @path.empty?
72
- @display_path = @path.sub(project_dir.path, '').sub(/^\//, '')
73
- end
74
-
75
- def name=(the_name)
76
- new_path = path.sub(/#{Regexp.escape(@name)}$/, the_name) unless scratchpad?
77
- @name = the_name
78
- if !scratchpad? && ::File.exist?(path)
79
- FileUtils.mv(path, new_path)
80
- self.path = new_path
81
- end
82
- end
83
-
84
- def scratchpad?
85
- path.to_s.empty?
86
- end
87
-
88
- def backup_properties
89
- [:find_text, :replace_text, :case_sensitive, :top_pixel, :caret_position, :selection_count].reduce({}) do |hash, property|
90
- hash.merge(property => send(property))
91
- end
92
- end
93
-
94
- def restore_properties(properties_hash)
95
- 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?
96
- properties_hash.each do |property, value|
97
- send("#{property}=", value)
98
- end
99
- end
100
-
101
- def caret_position=(value)
102
- @last_caret_position = @caret_position
103
- @caret_position = value
104
- end
105
-
106
- def selection_count=(value)
107
- #@last_selection_count = @selection_count
108
- @selection_count = value
109
- @last_selection_count = @selection_count
110
- end
111
-
112
- def dirty_content
113
- init_content
114
- @dirty_content
115
- end
116
-
117
- def dirty_content=(the_content)
118
- # TODO set partial dirty content by line(s) for enhanced performance
119
- @dirty_content = the_content
120
- old_caret_position = caret_position
121
- old_top_pixel = top_pixel
122
-
123
- notify_observers(:content)
124
- if @formatting_dirty_content_for_writing
125
- self.caret_position = old_caret_position
126
- self.top_pixel = old_top_pixel
127
- end
128
- end
129
-
130
- def content
131
- dirty_content
132
- end
133
-
134
- # to use for widget data-binding
135
- def content=(value)
136
- value = value.gsub("\t", ' ')
137
- if dirty_content != value
138
- Command.do(self, :change_content!, value)
139
- end
140
- end
141
-
142
- def change_content!(value)
143
- self.dirty_content = value
144
- update_line_number_from_caret_position(caret_position)
145
- end
146
-
147
- def start_command
148
- @commmand_in_progress = true
149
- end
150
-
151
- def end_command
152
- @commmand_in_progress = false
153
- end
154
-
155
- def command_in_progress?
156
- @commmand_in_progress
157
- end
158
-
159
- def close
160
- stop_filewatcher
161
- remove_all_observers
162
- initialize(path, project_dir)
163
- Command.clear(self)
164
- end
165
-
166
- def read_dirty_content
167
- path.empty? ? '' : ::File.read(path)
168
- end
169
-
170
- def start_filewatcher
171
- return if scratchpad?
172
- @filewatcher = Filewatcher.new(@path)
173
- @thread = Thread.new(@filewatcher) do |fw|
174
- fw.watch do |filename, event|
175
- async_exec do
176
- begin
177
- self.dirty_content = read_dirty_content if read_dirty_content != dirty_content
178
- rescue StandardError, Errno::ENOENT
179
- # in case of a binary file
180
- stop_filewatcher
181
- end
182
- end
183
- end
184
- end
185
- end
186
-
187
- def stop_filewatcher
188
- @filewatcher&.stop
189
- end
190
-
191
- def write_dirty_content
192
- # TODO write partial dirty content by line(s) for enhanced performance
193
- return if scratchpad? || !::File.exist?(path) || !::File.exists?(path) || read_dirty_content == dirty_content
194
- format_dirty_content_for_writing!
195
- ::File.write(path, dirty_content)
196
- rescue StandardError, ArgumentError => e
197
- puts "Error in writing dirty content for #{path}"
198
- puts e.full_message
199
- end
200
-
201
- def format_dirty_content_for_writing!
202
- return if @commmand_in_progress
203
- # TODO f ix c ar e t pos it ion after formatting dirty content (diff?)
204
- new_dirty_content = dirty_content.to_s.split("\n").map {|line| line.strip.empty? ? line : line.rstrip }.join("\n")
205
- new_dirty_content = "#{new_dirty_content.gsub("\r\n", "\n").gsub("\r", "\n").sub(/\n+\z/, '')}\n"
206
- if new_dirty_content != self.dirty_content
207
- @formatting_dirty_content_for_writing = true
208
- self.dirty_content = new_dirty_content
209
- @formatting_dirty_content_for_writing = false
210
- end
211
- end
212
-
213
- def write_raw_dirty_content
214
- return if scratchpad? || !::File.exist?(path)
215
- ::File.write(path, dirty_content) if ::File.exists?(path)
216
- rescue => e
217
- puts "Error in writing raw dirty content for #{path}"
218
- puts e.full_message
219
- end
220
-
221
- def current_line_indentation
222
- current_line.to_s.match(/^(\s+)/).to_a[1].to_s
223
- end
224
-
225
- def current_line
226
- lines[line_number - 1]
227
- end
228
-
229
- def delete!
230
- FileUtils.rm(path) unless scratchpad?
231
- end
232
-
233
- def prefix_new_line!
234
- the_lines = lines
235
- the_lines[line_number-1...line_number-1] = [current_line_indentation]
236
- self.dirty_content = the_lines.join("\n")
237
- self.caret_position = caret_position_for_line_index(line_number-1) + current_line_indentation.size
238
- self.selection_count = 0
239
- end
240
-
241
- def insert_new_line!
242
- the_lines = lines
243
- the_lines[line_number...line_number] = [current_line_indentation]
244
- self.dirty_content = the_lines.join("\n")
245
- self.caret_position = caret_position_for_line_index(line_number) + current_line_indentation.size
246
- self.selection_count = 0
247
- end
248
-
249
- def comment_line!
250
- old_lines = lines
251
- return if old_lines.size < 1
252
- old_selection_count = self.selection_count
253
- old_caret_position = self.caret_position
254
- old_caret_position_line_index = line_index_for_caret_position(old_caret_position)
255
- old_caret_position_line_caret_position = caret_position_for_line_index(old_caret_position_line_index)
256
- old_end_caret_line_index = end_caret_position_line_index(caret_position, selection_count)
257
- new_lines = lines
258
- delta = 0
259
- line_indices_for_selection(caret_position, selection_count).reverse.each do | the_line_index |
260
- delta = 0
261
- the_line = old_lines[the_line_index]
262
- return if the_line.nil?
263
- if the_line.strip.start_with?('# ')
264
- new_lines[the_line_index] = the_line.sub(/# /, '')
265
- delta -= 2
266
- elsif the_line.strip.start_with?('#')
267
- new_lines[the_line_index] = the_line.sub(/#/, '')
268
- delta -= 1
269
- else
270
- new_lines[the_line_index] = "# #{the_line}"
271
- delta += 2
272
- end
273
- end
274
- self.dirty_content = new_lines.join("\n")
275
- if old_selection_count.to_i > 0
276
- self.caret_position = caret_position_for_line_index(old_caret_position_line_index)
277
- self.selection_count = (caret_position_for_line_index(old_end_caret_line_index + 1) - self.caret_position)
278
- else
279
- new_caret_position = old_caret_position + delta
280
- new_caret_position = [new_caret_position, old_caret_position_line_caret_position].max
281
- self.caret_position = new_caret_position
282
- self.selection_count = 0
283
- end
284
- end
285
-
286
- def indent!
287
- new_lines = lines
288
- old_lines = lines
289
- return if old_lines.size < 1
290
- old_selection_count = self.selection_count
291
- old_caret_position = self.caret_position
292
- old_caret_position_line_index = line_index_for_caret_position(old_caret_position)
293
- old_caret_position_line_caret_position = caret_position_for_line_index(old_caret_position_line_index)
294
- old_end_caret_line_index = end_caret_position_line_index(caret_position, selection_count)
295
- delta = 2
296
- line_indices_for_selection(caret_position, selection_count).each do |the_line_index|
297
- the_line = old_lines[the_line_index]
298
- new_lines[the_line_index] = " #{the_line}"
299
- end
300
- old_caret_position = self.caret_position
301
- self.dirty_content = new_lines.join("\n")
302
- if old_selection_count.to_i > 0
303
- self.caret_position = caret_position_for_line_index(old_caret_position_line_index)
304
- self.selection_count = (caret_position_for_line_index(old_end_caret_line_index + 1) - self.caret_position)
305
- else
306
- self.caret_position = old_caret_position + delta
307
- self.selection_count = 0
308
- end
309
- end
310
-
311
- def outdent!
312
- new_lines = lines
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
- delta = 0
321
- line_indices_for_selection(caret_position, selection_count).each do |the_line_index|
322
- the_line = old_lines[the_line_index]
323
- if the_line.to_s.start_with?(' ')
324
- new_lines[the_line_index] = the_line.sub(/ /, '')
325
- delta = -2
326
- elsif the_line&.start_with?(' ')
327
- new_lines[the_line_index] = the_line.sub(/ /, '')
328
- delta = -1
329
- end
330
- end
331
- self.dirty_content = new_lines.join("\n")
332
- if old_selection_count.to_i > 0
333
- self.caret_position = caret_position_for_line_index(old_caret_position_line_index)
334
- self.selection_count = (caret_position_for_line_index(old_end_caret_line_index + 1) - self.caret_position)
335
- else
336
- new_caret_position = old_caret_position + delta
337
- new_caret_position = [new_caret_position, old_caret_position_line_caret_position].max
338
- self.caret_position = new_caret_position
339
- self.selection_count = 0
340
- end
341
- end
342
-
343
- def kill_line!
344
- new_lines = lines
345
- return if new_lines.size < 1
346
- line_indices = line_indices_for_selection(caret_position, selection_count)
347
- new_lines = new_lines[0...line_indices.first] + new_lines[(line_indices.last+1)...new_lines.size]
348
- old_caret_position = self.caret_position
349
- old_line_index = self.line_number - 1
350
- line_position = line_position_for_caret_position(old_caret_position)
351
- self.dirty_content = "#{new_lines.join("\n")}\n"
352
- self.caret_position = caret_position_for_line_index(old_line_index) + [line_position, lines[old_line_index].to_s.size].min
353
- self.selection_count = 0
354
- end
355
-
356
- def duplicate_line!
357
- new_lines = lines
358
- old_lines = lines
359
- return if old_lines.size < 1
360
- old_selection_count = self.selection_count
361
- old_caret_position = self.caret_position
362
- old_caret_position_line_index = line_index_for_caret_position(old_caret_position)
363
- old_caret_position_line_caret_position = caret_position_for_caret_position_start_of_line(old_caret_position_line_index)
364
- old_end_caret_line_index = end_caret_position_line_index(caret_position, selection_count)
365
- the_line_indices = line_indices_for_selection(caret_position, selection_count)
366
- the_lines = lines_for_selection(caret_position, selection_count)
367
- delta = the_lines.join("\n").size + 1
368
- the_lines.each_with_index do |the_line, i|
369
- new_lines.insert(the_line_indices.first + i, the_line)
370
- end
371
- self.dirty_content = new_lines.join("\n")
372
- if old_selection_count.to_i > 0
373
- self.caret_position = caret_position_for_line_index(old_caret_position_line_index)
374
- self.selection_count = (caret_position_for_line_index(old_end_caret_line_index + 1) - self.caret_position)
375
- else
376
- self.caret_position = old_caret_position + delta
377
- self.selection_count = 0
378
- end
379
- end
380
-
381
- def find_next
382
- return if find_text.to_s.empty?
383
- all_lines = lines
384
- the_line_index = line_index_for_caret_position(caret_position)
385
- line_position = line_position_for_caret_position(caret_position)
386
- found = found_text?(caret_position)
387
- 2.times do |i|
388
- rotation = the_line_index
389
- all_lines.rotate(rotation).each_with_index do |the_line, the_index|
390
- the_index = (the_index + rotation)%all_lines.size
391
- start_position = 0
392
- start_position = line_position + find_text.to_s.size if i == 0 && the_index == the_line_index && found_text?(caret_position)
393
- text_to_find_in = the_line[start_position..-1]
394
- occurrence_index = case_sensitive ? text_to_find_in&.index(find_text.to_s) : text_to_find_in&.downcase&.index(find_text.to_s.downcase)
395
- if occurrence_index
396
- self.caret_position = caret_position_for_line_index(the_index) + start_position + occurrence_index
397
- self.selection_count = find_text.to_s.size
398
- return
399
- end
400
- end
401
- end
402
- end
403
-
404
- def find_previous
405
- return if find_text.to_s.empty?
406
- all_lines = lines
407
- the_line_index = line_index_for_caret_position(caret_position)
408
- line_position = line_position_for_caret_position(caret_position)
409
- 2.times do |i|
410
- rotation = - the_line_index - 1 + all_lines.size
411
- all_lines.reverse.rotate(rotation).each_with_index do |the_line, the_index|
412
- the_index = all_lines.size - 1 - (the_index + rotation)%all_lines.size
413
- if the_index == the_line_index
414
- start_position = i > 0 ? 0 : (the_line.size - line_position)
415
- else
416
- start_position = 0
417
- end
418
- text_to_find_in = the_line.downcase.reverse[start_position...the_line.size].to_s
419
- occurrence_index = text_to_find_in.index(find_text.to_s.downcase.reverse)
420
- if occurrence_index
421
- self.caret_position = caret_position_for_line_index(the_index) + (the_line.size - (start_position + occurrence_index + find_text.to_s.size))
422
- self.selection_count = find_text.to_s.size
423
- return
424
- end
425
- end
426
- end
427
- end
428
-
429
- def ensure_find_next
430
- return if find_text.to_s.empty? || dirty_content.to_s.strip.size < 1
431
- find_next unless found_text?(self.caret_position)
432
- end
433
-
434
- def found_text?(caret_position)
435
- dirty_content[caret_position.to_i, find_text.to_s.size].to_s.downcase == find_text.to_s.downcase
436
- end
437
-
438
- def replace_next!
439
- return if find_text.to_s.empty? || dirty_content.to_s.strip.size < 1
440
- ensure_find_next
441
- new_dirty_content = dirty_content
442
- new_dirty_content[caret_position, find_text.size] = replace_text.to_s
443
- self.dirty_content = new_dirty_content
444
- find_next
445
- find_next if replace_text.to_s.include?(find_text) && !replace_text.to_s.start_with?(find_text)
446
- end
447
-
448
- def page_up
449
- self.selection_count = 0
450
- self.line_number = [(self.line_number - 15), 1].max
451
- end
452
-
453
- def page_down
454
- self.selection_count = 0
455
- self.line_number = [(self.line_number + 15), lines.size].min
456
- end
457
-
458
- def home
459
- self.selection_count = 0
460
- self.line_number = 1
461
- end
462
-
463
- def end
464
- self.selection_count = 0
465
- self.line_number = lines.size
466
- end
467
-
468
- def start_of_line
469
- self.caret_position = caret_position_for_line_index(self.line_number - 1)
470
- end
471
-
472
- def end_of_line
473
- self.caret_position = caret_position_for_line_index(self.line_number) - 1
474
- end
475
-
476
- def move_up!
477
- old_lines = lines
478
- return if old_lines.size < 2
479
- old_selection_count = self.selection_count
480
- old_caret_position = self.caret_position
481
- old_caret_position_line_caret_position = caret_position_for_caret_position_start_of_line(old_caret_position)
482
- old_caret_position_line_position = old_caret_position - old_caret_position_line_caret_position
483
- old_end_caret_line_index = end_caret_position_line_index(caret_position, selection_count)
484
- new_lines = lines
485
- the_line_indices = line_indices_for_selection(caret_position, selection_count)
486
- the_lines = lines_for_selection(caret_position, selection_count)
487
- new_line_index = [the_line_indices.first - 1, 0].max
488
- new_lines[the_line_indices.first..the_line_indices.last] = []
489
- new_lines[new_line_index...new_line_index] = the_lines
490
- self.dirty_content = new_lines.join("\n")
491
- self.caret_position = caret_position_for_line_index(new_line_index) + [old_caret_position_line_position, new_lines[new_line_index].size].min
492
- self.selection_count = old_selection_count.to_i if old_selection_count.to_i > 0
493
- end
494
-
495
- def move_down!
496
- old_lines = lines
497
- return if old_lines.size < 2
498
- old_selection_count = self.selection_count
499
- old_caret_position = self.caret_position
500
- old_caret_position_line_caret_position = caret_position_for_caret_position_start_of_line(old_caret_position)
501
- old_caret_position_line_position = old_caret_position - old_caret_position_line_caret_position
502
- old_end_caret_line_index = end_caret_position_line_index(caret_position, selection_count)
503
- new_lines = lines
504
- the_line_indices = line_indices_for_selection(caret_position, selection_count)
505
- the_lines = lines_for_selection(caret_position, selection_count)
506
- new_line_index = [the_line_indices.first + 1, new_lines.size - 1].min
507
- new_lines[the_line_indices.first..the_line_indices.last] = []
508
- new_lines[new_line_index...new_line_index] = the_lines
509
- self.dirty_content = new_lines.join("\n")
510
- self.caret_position = caret_position_for_line_index(new_line_index) + [old_caret_position_line_position, new_lines[new_line_index].size].min
511
- self.selection_count = old_selection_count.to_i if old_selection_count.to_i > 0
512
- end
513
-
514
- def run
515
- if scratchpad?
516
- eval content
517
- else
518
- write_dirty_content
519
- load path
520
- end
521
- end
522
-
523
- def lines
524
- need_padding = dirty_content.to_s.end_with?("\n")
525
- splittable_content = need_padding ? "#{dirty_content} " : dirty_content
526
- the_lines = splittable_content.split("\n")
527
- the_lines[-1] = the_lines[-1].strip if need_padding
528
- the_lines
529
- end
530
-
531
- def line_for_caret_position(caret_position)
532
- lines[line_index_for_caret_position(caret_position.to_i)]
533
- end
534
-
535
- def line_index_for_caret_position(caret_position)
536
- dirty_content[0...caret_position.to_i].count("\n")
537
- end
538
-
539
- def caret_position_for_line_index(line_index)
540
- cp = lines[0...line_index].join("\n").size
541
- cp += 1 if line_index > 0
542
- cp
543
- end
544
-
545
- def caret_position_for_caret_position_start_of_line(caret_position)
546
- caret_position_for_line_index(line_index_for_caret_position(caret_position))
547
- end
548
-
549
- # 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)
550
- # TODO consider renaming to line_character_position_for_caret_position
551
- def line_position_for_caret_position(caret_position)
552
- caret_position = caret_position.to_i
553
- caret_position - caret_position_for_caret_position_start_of_line(caret_position)
554
- end
555
-
556
- def line_caret_positions_for_selection(caret_position, selection_count)
557
- line_indices = line_indices_for_selection(caret_position, selection_count)
558
- line_caret_positions = line_indices.map { |line_index| caret_position_for_line_index(line_index) }.to_a
559
- end
560
-
561
- def end_caret_position_line_index(caret_position, selection_count)
562
- end_caret_position = caret_position + selection_count.to_i
563
- end_caret_position -= 1 if dirty_content[end_caret_position - 1] == "\n"
564
- end_line_index = line_index_for_caret_position(end_caret_position)
565
- end
566
-
567
- def lines_for_selection(caret_position, selection_count)
568
- line_indices = line_indices_for_selection(caret_position, selection_count)
569
- lines[line_indices.first..line_indices.last]
570
- end
571
-
572
- def line_indices_for_selection(caret_position, selection_count)
573
- start_line_index = line_index_for_caret_position(caret_position)
574
- if selection_count.to_i > 0
575
- end_line_index = end_caret_position_line_index(caret_position, selection_count)
576
- else
577
- end_line_index = start_line_index
578
- end
579
- (start_line_index..end_line_index).to_a
580
- end
581
-
582
- def children
583
- []
584
- end
585
-
586
- def to_s
587
- path
588
- end
589
-
590
- def eql?(other)
591
- self.path.eql?(other&.path)
592
- end
593
-
594
- def hash
595
- self.path.hash
596
- end
597
- end
598
- end
599
- 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?
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