glimmer-cs-gladiator 0.7.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,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