glimmer-cs-gladiator 0.8.1 → 0.8.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 11730722d7270481d6ddd3b4ddccf2cea3e4e845896ef78528bb20caf5cdc29a
4
- data.tar.gz: 25f8fa48610d54e44dc6d848fe0d583a6857995fe09d021512e6d63202188e2a
3
+ metadata.gz: 7c2b457e91ebddf9e822a15d20ba6f78957285126d9ba6df7923bfdfa2cd49c2
4
+ data.tar.gz: fb1a55aaf1eb98c4a247dc1f8aead7e0c5c6fff76bf64932540cc6b01528e3f7
5
5
  SHA512:
6
- metadata.gz: 8af3c74194e99143bbefd4bf04b5c1077377264a274cd18075ea0eeebf68d5559a597adc59070e6d978aa4fada54e8e3c915fdd56ccf1a620ce1931d2ed9ad01
7
- data.tar.gz: 3c8e99fac4c80e4ede247883fb9b96c1823b24d69f0f3518abcbcf4a78c9889f78d161b7655a39f67615614d68b766e22504ac2dc1c9840b2a145e6fe3dcfc9e
6
+ metadata.gz: 5967f794ade7c65630b0a8dd34e41d066411a710e6c030d7e07a066e7612199e5651dba95f5bd0dffe36d43dfb906c1c6cc2a85ef03060ed771244b8ad85dbd1
7
+ data.tar.gz: 637a0a898869617472fd13c6cb827d04f316bae498602ae69dbba6eefcee48f7783d1b9aaff5f54a46569c0f0aaa2db40dbee5bf43dd35e0d70bed0bf2c89398
data/CHANGELOG.md CHANGED
@@ -1,7 +1,19 @@
1
1
  # Change Log
2
2
 
3
+ ## 0.8.2
4
+
5
+ - Add a right click menu on text editor area with undo redo | cut, copy, paste, delete | select all
6
+ - Replaced error message_box with a more readable error dialog for running Ruby code
7
+ - Update Ruby Run menu item command to run against the top level binding receiver (ensuring no weird errors when including Glimmer)
8
+ - In app mode, display an "Open Project..." button
9
+ - In app mode, set gladiator icon on initial shell
10
+ - Fix full line selection on Windows (SHIFT+HOME or SHIFT+END)
11
+ - Fix issue with display saving original file before changes when running in app mode, making changes to an open project, and then closing
12
+ - Fix caret position after formatting dirty content (when pasting a string that has extra empty spaces for example)
13
+
3
14
  ## 0.8.1
4
15
 
16
+ - Package Gladiator as a Windows MSI file
5
17
  - Fix issue with HOME and END taking to beginning of file and end of file on Windows instead of beginning of line and end of line
6
18
  - Fix opening first tab on Windows (shows up as blank, but second tab shows up fine)
7
19
  - Fix issue with losing focus on changing tabs on Windows via Windows default tab switching shortcuts of CTRL+PGUP & CTRL+PGDN
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # <img src='https://raw.githubusercontent.com/AndyObtiva/glimmer-cs-gladiator/master/images/glimmer-cs-gladiator-logo.svg' height=85 /> Gladiator 0.8.1 - [Ugliest Text Editor Ever!](https://www.reddit.com/r/ruby/comments/hgve8k/gladiator_glimmer_editor_ugliest_text_editor_ever/)
1
+ # <img src='https://raw.githubusercontent.com/AndyObtiva/glimmer-cs-gladiator/master/images/glimmer-cs-gladiator-logo.svg' height=85 /> Gladiator 0.8.2 - [Ugliest Text Editor Ever!](https://www.reddit.com/r/ruby/comments/hgve8k/gladiator_glimmer_editor_ugliest_text_editor_ever/)
2
2
  ## [<img src="https://raw.githubusercontent.com/AndyObtiva/glimmer/master/images/glimmer-logo-hi-res.png" height=40 /> Glimmer Custom Shell](https://github.com/AndyObtiva/glimmer-dsl-swt#custom-shell-gem)
3
3
  [![Gem Version](https://badge.fury.io/rb/glimmer-cs-gladiator.svg)](http://badge.fury.io/rb/glimmer-cs-gladiator)
4
4
 
@@ -119,7 +119,9 @@ Gladiator currently supports the following text editing features (including keyb
119
119
 
120
120
  [Download Gladiator Mac DMG Installer](https://www.dropbox.com/s/uklftb8q16czgo6/Gladiator-0.8.1.dmg?dl=1)
121
121
 
122
- [Download Gladiator Windows MSI Installer](https://www.dropbox.com/s/uuvo5h6golzmr82/Gladiator-0.8.1.msi?dl=1)
122
+ [Download Gladiator Windows MSI Installer](https://www.dropbox.com/s/edvbgsrjbdwc8v8/Gladiator-0.8.2.msi?dl=1)
123
+
124
+ The packaged version starts with a dialog asking you what project to open. Gladiator does not fully show up until you have selected a project directory.
123
125
 
124
126
  Otherwise, if you prefer a command line version, then follow the Setup Instructions below.
125
127
 
@@ -147,19 +149,14 @@ Run (`jruby -S bundle` or `bundle` directly if you have [RVM](https://rvm.io/)):
147
149
  jruby -S bundle
148
150
  ```
149
151
 
150
- Afterwards, to ensure system wide availablility of the `gladiator` command, run this command in an environment that has JRuby:
152
+ Afterwards, if you are using [RVM](https://rvm.io/) and want to ensure system wide availablility of the `gladiator` command across Ruby versions, run this command in an environment that has JRuby (not needed without [RVM](https://rvm.io/)):
151
153
 
152
154
  ```
153
155
  gladiator-setup
154
- ```
155
-
156
- Finally, start a new terminal session or source .gladiator_source:
157
-
158
- ```
159
156
  source ~/.gladiator_source
160
157
  ```
161
158
 
162
- You should be able to run `gladiator` from anywhere now, even cross-rubies in [RVM](https://rvm.io).
159
+ You should be able to run `gladiator` from anywhere now.
163
160
 
164
161
  ## Usage
165
162
 
@@ -195,7 +192,7 @@ To reuse Gladiator as a Glimmer Custom Shell inside another Glimmer application,
195
192
  following to the application's `Gemfile`:
196
193
 
197
194
  ```
198
- gem 'glimmer-cs-gladiator', '>= 0.8.1'
195
+ gem 'glimmer-cs-gladiator', '>= 0.8.2'
199
196
  ```
200
197
 
201
198
  Run:
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.8.1
1
+ 0.8.2
@@ -2,16 +2,16 @@
2
2
  # DO NOT EDIT THIS FILE DIRECTLY
3
3
  # Instead, edit Jeweler::Tasks in rakefile, and run 'rake gemspec'
4
4
  # -*- encoding: utf-8 -*-
5
- # stub: glimmer-cs-gladiator 0.8.1 ruby lib
5
+ # stub: glimmer-cs-gladiator 0.8.2 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "glimmer-cs-gladiator".freeze
9
- s.version = "0.8.1"
9
+ s.version = "0.8.2"
10
10
 
11
11
  s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
12
12
  s.require_paths = ["lib".freeze]
13
13
  s.authors = ["Andy Maleh".freeze]
14
- s.date = "2021-02-07"
14
+ s.date = "2021-02-08"
15
15
  s.description = "Gladiator (short for Glimmer Editor) is a Glimmer sample project under on-going development. It is not intended to be a full-fledged editor by any means, yet mostly a fun educational exercise in using Glimmer to build a text editor. Gladiator is also a personal tool for shaping an editor exactly the way I like. I leave building truly professional text editors to software tooling experts who would hopefully use Glimmer one day.".freeze
16
16
  s.email = "andy.am@gmail.com".freeze
17
17
  s.executables = ["glimmer-cs-gladiator".freeze, "gladiator".freeze, "gladiator-setup".freeze]
@@ -36,6 +36,7 @@ Gem::Specification.new do |s|
36
36
  "lib/models/glimmer/gladiator/dir.rb",
37
37
  "lib/models/glimmer/gladiator/file.rb",
38
38
  "lib/views/glimmer/gladiator.rb",
39
+ "lib/views/glimmer/gladiator/file_edit_menu.rb",
39
40
  "lib/views/glimmer/gladiator/file_explorer_tree.rb",
40
41
  "lib/views/glimmer/gladiator/file_lookup_list.rb",
41
42
  "lib/views/glimmer/gladiator/gladiator_menu_bar.rb",
@@ -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
@@ -1,662 +1,713 @@
1
- module Glimmer
2
- class Gladiator
3
- class File
4
- include Glimmer
5
-
6
- attr_accessor :line_numbers_content, :line_number, :find_text, :replace_text, :top_pixel, :display_path, :case_sensitive, :caret_position, :selection_count, :last_caret_position, :last_selection_count, :line_position
7
- attr_reader :name, :path, :project_dir
8
-
9
- def initialize(path='', project_dir=nil)
10
- raise "Not a file path: #{path}" if path.nil? || (!path.empty? && !::File.file?(path))
11
- @project_dir = project_dir
12
- @command_history = []
13
- @name = path.empty? ? 'Scratchpad' : ::File.basename(path)
14
- self.path = ::File.expand_path(path) unless path.empty?
15
- @top_pixel = 0
16
- @caret_position = 0
17
- @selection_count = 0
18
- @last_selection_count = 0
19
- @line_number = 1
20
- @init = nil
21
- end
22
-
23
- def language
24
- # TODO consider using Rouge::Lexer.guess_by_filename instead and perhaps guess_by_source when it fails
25
- extension = path.split('.').last if path.to_s.include?('.')
26
- return 'ruby' if scratchpad?
27
- return 'ruby' if path.to_s.end_with?('Gemfile') || path.to_s.end_with?('Rakefile')
28
- return 'ruby' if dirty_content.start_with?('#!/usr/bin/env ruby') || dirty_content.start_with?('#!/usr/bin/env jruby')
29
- return 'yaml' if path.to_s.end_with?('Gemfile.lock')
30
- return 'shell' if extension.nil? && path.to_s.include?('/bin/')
31
- case extension
32
- # TODO extract case statement to an external config file
33
- when 'rb'
34
- 'ruby'
35
- when 'md', 'markdown'
36
- 'markdown'
37
- when 'js', 'es6'
38
- 'javascript'
39
- when 'json'
40
- 'json'
41
- when 'yaml'
42
- 'yaml'
43
- when 'html'
44
- 'html'
45
- when 'h', 'c'
46
- 'c'
47
- when 'hs'
48
- 'haskell'
49
- when 'gradle'
50
- 'gradle'
51
- when 'cpp'
52
- 'cpp'
53
- when 'css'
54
- 'css'
55
- when 'java'
56
- 'java'
57
- when 'jsp'
58
- 'jsp'
59
- when 'plist'
60
- 'plist'
61
- when 'haml'
62
- 'haml'
63
- when 'xml'
64
- 'xml'
65
- when 'ini'
66
- 'ini'
67
- when 'pl'
68
- 'perl'
69
- when 'tcl'
70
- 'tcl'
71
- when 'sass'
72
- 'sass'
73
- when 'scss'
74
- 'scss'
75
- when 'sql'
76
- 'sql'
77
- when 'sh'
78
- 'shell'
79
- when 'vue'
80
- 'vue'
81
- when 'txt', nil
82
- 'plain_text'
83
- end
84
- end
85
-
86
- def init_content
87
- unless @init
88
- @init = true
89
- begin
90
- # test read dirty content
91
- observe(self, :dirty_content) do
92
- line_count = lines.empty? ? 1 : lines.size
93
- lines_text_size = [line_count.to_s.size, 4].max
94
- old_top_pixel = top_pixel
95
- self.line_numbers_content = line_count.times.map {|n| (' ' * (lines_text_size - (n+1).to_s.size)) + (n+1).to_s }.join("\n")
96
- self.top_pixel = old_top_pixel
97
- end
98
- the_dirty_content = read_dirty_content
99
- the_dirty_content.split("\n") # test that it is not a binary file (crashes to rescue block otherwise)
100
- self.dirty_content = the_dirty_content
101
- observe(self, :caret_position) do |new_caret_position|
102
- update_line_number_from_caret_position(new_caret_position)
103
- end
104
- observe(self, :line_number) do |new_line_number|
105
- line_index = line_number - 1
106
- new_caret_position = caret_position_for_line_index(line_index)
107
- current_caret_position = caret_position
108
- line_index_for_new_caret_position = line_index_for_caret_position(new_caret_position)
109
- line_index_for_current_caret_position = line_index_for_caret_position(current_caret_position)
110
- self.caret_position = new_caret_position unless (current_caret_position && line_index_for_new_caret_position == line_index_for_current_caret_position)
111
- end
112
- rescue # in case of a binary file
113
- stop_filewatcher
114
- end
115
- end
116
- end
117
-
118
- def update_line_number_from_caret_position(new_caret_position)
119
- new_line_number = line_index_for_caret_position(caret_position) + 1
120
- current_line_number = line_number
121
- unless (current_line_number && current_line_number == new_line_number)
122
- self.line_number = new_line_number
123
- # TODO check if the following line is needed
124
- self.line_position = caret_position - caret_position_for_line_index(line_number - 1) + 1
125
- end
126
- end
127
-
128
- def path=(the_path)
129
- @path = the_path
130
- generate_display_path
131
- end
132
-
133
- def generate_display_path
134
- return if @path.empty?
135
- @display_path = @path.sub(project_dir.path, '').sub(/^\//, '')
136
- end
137
-
138
- def name=(the_name)
139
- new_path = path.sub(/#{Regexp.escape(@name)}$/, the_name) unless scratchpad?
140
- @name = the_name
141
- if !scratchpad? && ::File.exist?(path)
142
- FileUtils.mv(path, new_path)
143
- self.path = new_path
144
- end
145
- end
146
-
147
- def scratchpad?
148
- path.to_s.empty?
149
- end
150
-
151
- def backup_properties
152
- [:find_text, :replace_text, :case_sensitive, :top_pixel, :caret_position, :selection_count].reduce({}) do |hash, property|
153
- hash.merge(property => send(property))
154
- end
155
- end
156
-
157
- def restore_properties(properties_hash)
158
- return if properties_hash[:caret_position] == 0 && properties_hash[:selection_count] == 0 && properties_hash[:find_text].nil? && properties_hash[:replace_text].nil? && properties_hash[:top_pixel] == 0 && properties_hash[:case_sensitive].nil?
159
- properties_hash.each do |property, value|
160
- send("#{property}=", value)
161
- end
162
- end
163
-
164
- def caret_position=(value)
165
- @last_caret_position = @caret_position
166
- @caret_position = value
167
- end
168
-
169
- def selection_count=(value)
170
- #@last_selection_count = @selection_count
171
- @selection_count = value
172
- @last_selection_count = @selection_count
173
- end
174
-
175
- def dirty_content
176
- init_content
177
- @dirty_content
178
- end
179
-
180
- def dirty_content=(the_content)
181
- # TODO set partial dirty content by line(s) for enhanced performance
182
- @dirty_content = the_content
183
- old_caret_position = caret_position
184
- old_top_pixel = top_pixel
185
-
186
- notify_observers(:content)
187
- if @formatting_dirty_content_for_writing
188
- self.caret_position = old_caret_position
189
- self.top_pixel = old_top_pixel
190
- end
191
- end
192
-
193
- def content
194
- dirty_content
195
- end
196
-
197
- # to use for widget data-binding
198
- def content=(value)
199
- value = value.gsub("\t", ' ')
200
- if dirty_content != value
201
- Command.do(self, :change_content!, value)
202
- end
203
- end
204
-
205
- def change_content!(value)
206
- self.dirty_content = value
207
- update_line_number_from_caret_position(caret_position)
208
- end
209
-
210
- def start_command
211
- @commmand_in_progress = true
212
- end
213
-
214
- def end_command
215
- @commmand_in_progress = false
216
- end
217
-
218
- def command_in_progress?
219
- @commmand_in_progress
220
- end
221
-
222
- def close
223
- stop_filewatcher
224
- remove_all_observers
225
- initialize(path, project_dir)
226
- Command.clear(self)
227
- end
228
-
229
- def read_dirty_content
230
- path.empty? ? '' : ::File.read(path)
231
- end
232
-
233
- def start_filewatcher
234
- return if scratchpad?
235
- @filewatcher = Filewatcher.new(@path)
236
- @thread = Thread.new(@filewatcher) do |fw|
237
- fw.watch do |filename, event|
238
- async_exec do
239
- begin
240
- self.dirty_content = read_dirty_content if read_dirty_content != dirty_content
241
- rescue StandardError, Errno::ENOENT
242
- # in case of a binary file
243
- stop_filewatcher
244
- end
245
- end
246
- end
247
- end
248
- end
249
-
250
- def stop_filewatcher
251
- @filewatcher&.stop
252
- end
253
-
254
- def write_dirty_content
255
- # TODO write partial dirty content by line(s) for enhanced performance
256
- return if scratchpad? || !::File.exist?(path) || !::File.exists?(path) || read_dirty_content == dirty_content
257
- format_dirty_content_for_writing!
258
- ::File.write(path, dirty_content)
259
- rescue StandardError, ArgumentError => e
260
- puts "Error in writing dirty content for #{path}"
261
- puts e.full_message
262
- end
263
-
264
- def format_dirty_content_for_writing!
265
- return if @commmand_in_progress
266
- # TODO f ix c ar e t pos it ion after formatting dirty content (diff?)
267
- new_dirty_content = dirty_content.to_s.split("\n").map {|line| line.strip.empty? ? line : line.rstrip }.join("\n")
268
- new_dirty_content = "#{new_dirty_content.gsub("\r\n", "\n").gsub("\r", "\n").sub(/\n+\z/, '')}\n"
269
- if new_dirty_content != self.dirty_content
270
- @formatting_dirty_content_for_writing = true
271
- self.dirty_content = new_dirty_content
272
- @formatting_dirty_content_for_writing = false
273
- end
274
- end
275
-
276
- def write_raw_dirty_content
277
- return if scratchpad? || !::File.exist?(path)
278
- ::File.write(path, dirty_content) if ::File.exists?(path)
279
- rescue => e
280
- puts "Error in writing raw dirty content for #{path}"
281
- puts e.full_message
282
- end
283
-
284
- def current_line_indentation
285
- current_line.to_s.match(/^(\s+)/).to_a[1].to_s
286
- end
287
-
288
- def current_line
289
- lines[line_number - 1]
290
- end
291
-
292
- def delete!
293
- FileUtils.rm(path) unless scratchpad?
294
- end
295
-
296
- def prefix_new_line!
297
- the_lines = lines
298
- the_lines[line_number-1...line_number-1] = [current_line_indentation]
299
- self.dirty_content = the_lines.join("\n")
300
- self.caret_position = caret_position_for_line_index(line_number-1) + current_line_indentation.size
301
- self.selection_count = 0
302
- end
303
-
304
- def insert_new_line!
305
- the_lines = lines
306
- the_lines[line_number...line_number] = [current_line_indentation]
307
- self.dirty_content = the_lines.join("\n")
308
- self.caret_position = caret_position_for_line_index(line_number) + current_line_indentation.size
309
- self.selection_count = 0
310
- end
311
-
312
- def comment_line!
313
- old_lines = lines
314
- return if old_lines.size < 1
315
- old_selection_count = self.selection_count
316
- old_caret_position = self.caret_position
317
- old_caret_position_line_index = line_index_for_caret_position(old_caret_position)
318
- old_caret_position_line_caret_position = caret_position_for_line_index(old_caret_position_line_index)
319
- old_end_caret_line_index = end_caret_position_line_index(caret_position, selection_count)
320
- new_lines = lines
321
- delta = 0
322
- line_indices_for_selection(caret_position, selection_count).reverse.each do | the_line_index |
323
- delta = 0
324
- the_line = old_lines[the_line_index]
325
- return if the_line.nil?
326
- if the_line.strip.start_with?('# ')
327
- new_lines[the_line_index] = the_line.sub(/# /, '')
328
- delta -= 2
329
- elsif the_line.strip.start_with?('#')
330
- new_lines[the_line_index] = the_line.sub(/#/, '')
331
- delta -= 1
332
- else
333
- new_lines[the_line_index] = "# #{the_line}"
334
- delta += 2
335
- end
336
- end
337
- self.dirty_content = new_lines.join("\n")
338
- if old_selection_count.to_i > 0
339
- self.caret_position = caret_position_for_line_index(old_caret_position_line_index)
340
- self.selection_count = (caret_position_for_line_index(old_end_caret_line_index + 1) - self.caret_position)
341
- else
342
- new_caret_position = old_caret_position + delta
343
- new_caret_position = [new_caret_position, old_caret_position_line_caret_position].max
344
- self.caret_position = new_caret_position
345
- self.selection_count = 0
346
- end
347
- end
348
-
349
- def indent!
350
- new_lines = lines
351
- old_lines = lines
352
- return if old_lines.size < 1
353
- old_selection_count = self.selection_count
354
- old_caret_position = self.caret_position
355
- old_caret_position_line_index = line_index_for_caret_position(old_caret_position)
356
- old_caret_position_line_caret_position = caret_position_for_line_index(old_caret_position_line_index)
357
- old_end_caret_line_index = end_caret_position_line_index(caret_position, selection_count)
358
- delta = 2
359
- line_indices_for_selection(caret_position, selection_count).each do |the_line_index|
360
- the_line = old_lines[the_line_index]
361
- new_lines[the_line_index] = " #{the_line}"
362
- end
363
- old_caret_position = self.caret_position
364
- self.dirty_content = new_lines.join("\n")
365
- if old_selection_count.to_i > 0
366
- self.caret_position = caret_position_for_line_index(old_caret_position_line_index)
367
- self.selection_count = (caret_position_for_line_index(old_end_caret_line_index + 1) - self.caret_position)
368
- else
369
- self.caret_position = old_caret_position + delta
370
- self.selection_count = 0
371
- end
372
- end
373
-
374
- def outdent!
375
- new_lines = lines
376
- old_lines = lines
377
- return if old_lines.size < 1
378
- old_selection_count = self.selection_count
379
- old_caret_position = self.caret_position
380
- old_caret_position_line_index = line_index_for_caret_position(old_caret_position)
381
- old_caret_position_line_caret_position = caret_position_for_line_index(old_caret_position_line_index)
382
- old_end_caret_line_index = end_caret_position_line_index(caret_position, selection_count)
383
- delta = 0
384
- line_indices_for_selection(caret_position, selection_count).each do |the_line_index|
385
- the_line = old_lines[the_line_index]
386
- if the_line.to_s.start_with?(' ')
387
- new_lines[the_line_index] = the_line.sub(/ /, '')
388
- delta = -2
389
- elsif the_line&.start_with?(' ')
390
- new_lines[the_line_index] = the_line.sub(/ /, '')
391
- delta = -1
392
- end
393
- end
394
- self.dirty_content = new_lines.join("\n")
395
- if old_selection_count.to_i > 0
396
- self.caret_position = caret_position_for_line_index(old_caret_position_line_index)
397
- self.selection_count = (caret_position_for_line_index(old_end_caret_line_index + 1) - self.caret_position)
398
- else
399
- new_caret_position = old_caret_position + delta
400
- new_caret_position = [new_caret_position, old_caret_position_line_caret_position].max
401
- self.caret_position = new_caret_position
402
- self.selection_count = 0
403
- end
404
- end
405
-
406
- def kill_line!
407
- new_lines = lines
408
- return if new_lines.size < 1
409
- line_indices = line_indices_for_selection(caret_position, selection_count)
410
- new_lines = new_lines[0...line_indices.first] + new_lines[(line_indices.last+1)...new_lines.size]
411
- old_caret_position = self.caret_position
412
- old_line_index = self.line_number - 1
413
- line_position = line_position_for_caret_position(old_caret_position)
414
- self.dirty_content = "#{new_lines.join("\n")}\n"
415
- self.caret_position = caret_position_for_line_index(old_line_index) + [line_position, lines[old_line_index].to_s.size].min
416
- self.selection_count = 0
417
- end
418
-
419
- def duplicate_line!
420
- new_lines = lines
421
- old_lines = lines
422
- return if old_lines.size < 1
423
- old_selection_count = self.selection_count
424
- old_caret_position = self.caret_position
425
- old_caret_position_line_index = line_index_for_caret_position(old_caret_position)
426
- old_caret_position_line_caret_position = caret_position_for_caret_position_start_of_line(old_caret_position_line_index)
427
- old_end_caret_line_index = end_caret_position_line_index(caret_position, selection_count)
428
- the_line_indices = line_indices_for_selection(caret_position, selection_count)
429
- the_lines = lines_for_selection(caret_position, selection_count)
430
- delta = the_lines.join("\n").size + 1
431
- the_lines.each_with_index do |the_line, i|
432
- new_lines.insert(the_line_indices.first + i, the_line)
433
- end
434
- self.dirty_content = new_lines.join("\n")
435
- if old_selection_count.to_i > 0
436
- self.caret_position = caret_position_for_line_index(old_caret_position_line_index)
437
- self.selection_count = (caret_position_for_line_index(old_end_caret_line_index + 1) - self.caret_position)
438
- else
439
- self.caret_position = old_caret_position + delta
440
- self.selection_count = 0
441
- end
442
- end
443
-
444
- def find_next
445
- return if find_text.to_s.empty?
446
- all_lines = lines
447
- the_line_index = line_index_for_caret_position(caret_position)
448
- line_position = line_position_for_caret_position(caret_position)
449
- found = found_text?(caret_position)
450
- 2.times do |i|
451
- rotation = the_line_index
452
- all_lines.rotate(rotation).each_with_index do |the_line, the_index|
453
- the_index = (the_index + rotation)%all_lines.size
454
- start_position = 0
455
- start_position = line_position + find_text.to_s.size if i == 0 && the_index == the_line_index && found_text?(caret_position)
456
- text_to_find_in = the_line[start_position..-1]
457
- occurrence_index = case_sensitive ? text_to_find_in&.index(find_text.to_s) : text_to_find_in&.downcase&.index(find_text.to_s.downcase)
458
- if occurrence_index
459
- self.caret_position = caret_position_for_line_index(the_index) + start_position + occurrence_index
460
- self.selection_count = find_text.to_s.size
461
- return
462
- end
463
- end
464
- end
465
- end
466
-
467
- def find_previous
468
- return if find_text.to_s.empty?
469
- all_lines = lines
470
- the_line_index = line_index_for_caret_position(caret_position)
471
- line_position = line_position_for_caret_position(caret_position)
472
- 2.times do |i|
473
- rotation = - the_line_index - 1 + all_lines.size
474
- all_lines.reverse.rotate(rotation).each_with_index do |the_line, the_index|
475
- the_index = all_lines.size - 1 - (the_index + rotation)%all_lines.size
476
- if the_index == the_line_index
477
- start_position = i > 0 ? 0 : (the_line.size - line_position)
478
- else
479
- start_position = 0
480
- end
481
- text_to_find_in = the_line.downcase.reverse[start_position...the_line.size].to_s
482
- occurrence_index = text_to_find_in.index(find_text.to_s.downcase.reverse)
483
- if occurrence_index
484
- self.caret_position = caret_position_for_line_index(the_index) + (the_line.size - (start_position + occurrence_index + find_text.to_s.size))
485
- self.selection_count = find_text.to_s.size
486
- return
487
- end
488
- end
489
- end
490
- end
491
-
492
- def ensure_find_next
493
- return if find_text.to_s.empty? || dirty_content.to_s.strip.size < 1
494
- find_next unless found_text?(self.caret_position)
495
- end
496
-
497
- def found_text?(caret_position)
498
- dirty_content[caret_position.to_i, find_text.to_s.size].to_s.downcase == find_text.to_s.downcase
499
- end
500
-
501
- def replace_next!
502
- return if find_text.to_s.empty? || dirty_content.to_s.strip.size < 1
503
- ensure_find_next
504
- new_dirty_content = dirty_content
505
- new_dirty_content[caret_position, find_text.size] = replace_text.to_s
506
- self.dirty_content = new_dirty_content
507
- find_next
508
- find_next if replace_text.to_s.include?(find_text) && !replace_text.to_s.start_with?(find_text)
509
- end
510
-
511
- def page_up
512
- self.selection_count = 0
513
- self.line_number = [(self.line_number - 15), 1].max
514
- end
515
-
516
- def page_down
517
- self.selection_count = 0
518
- self.line_number = [(self.line_number + 15), lines.size].min
519
- end
520
-
521
- def home
522
- self.selection_count = 0
523
- self.line_number = 1
524
- end
525
-
526
- def end
527
- self.selection_count = 0
528
- self.line_number = lines.size
529
- end
530
-
531
- def start_of_line
532
- self.caret_position = caret_position_for_line_index(self.line_number - 1)
533
- end
534
-
535
- def end_of_line
536
- self.caret_position = caret_position_for_line_index(self.line_number) - 1
537
- end
538
-
539
- def move_up!
540
- old_lines = lines
541
- return if old_lines.size < 2
542
- old_selection_count = self.selection_count
543
- old_caret_position = self.caret_position
544
- old_caret_position_line_caret_position = caret_position_for_caret_position_start_of_line(old_caret_position)
545
- old_caret_position_line_position = old_caret_position - old_caret_position_line_caret_position
546
- old_end_caret_line_index = end_caret_position_line_index(caret_position, selection_count)
547
- new_lines = lines
548
- the_line_indices = line_indices_for_selection(caret_position, selection_count)
549
- the_lines = lines_for_selection(caret_position, selection_count)
550
- new_line_index = [the_line_indices.first - 1, 0].max
551
- new_lines[the_line_indices.first..the_line_indices.last] = []
552
- new_lines[new_line_index...new_line_index] = the_lines
553
- self.dirty_content = new_lines.join("\n")
554
- self.caret_position = caret_position_for_line_index(new_line_index) + [old_caret_position_line_position, new_lines[new_line_index].size].min
555
- self.selection_count = old_selection_count.to_i if old_selection_count.to_i > 0
556
- end
557
-
558
- def move_down!
559
- old_lines = lines
560
- return if old_lines.size < 2
561
- old_selection_count = self.selection_count
562
- old_caret_position = self.caret_position
563
- old_caret_position_line_caret_position = caret_position_for_caret_position_start_of_line(old_caret_position)
564
- old_caret_position_line_position = old_caret_position - old_caret_position_line_caret_position
565
- old_end_caret_line_index = end_caret_position_line_index(caret_position, selection_count)
566
- new_lines = lines
567
- the_line_indices = line_indices_for_selection(caret_position, selection_count)
568
- the_lines = lines_for_selection(caret_position, selection_count)
569
- new_line_index = [the_line_indices.first + 1, new_lines.size - 1].min
570
- new_lines[the_line_indices.first..the_line_indices.last] = []
571
- new_lines[new_line_index...new_line_index] = the_lines
572
- self.dirty_content = new_lines.join("\n")
573
- self.caret_position = caret_position_for_line_index(new_line_index) + [old_caret_position_line_position, new_lines[new_line_index].size].min
574
- self.selection_count = old_selection_count.to_i if old_selection_count.to_i > 0
575
- end
576
-
577
- def run
578
- if scratchpad?
579
- eval content
580
- else
581
- write_dirty_content
582
- load path
583
- end
584
- end
585
-
586
- def lines
587
- need_padding = dirty_content.to_s.end_with?("\n")
588
- splittable_content = need_padding ? "#{dirty_content} " : dirty_content
589
- the_lines = splittable_content.split("\n")
590
- the_lines[-1] = the_lines[-1].strip if need_padding
591
- the_lines
592
- end
593
-
594
- def line_for_caret_position(caret_position)
595
- lines[line_index_for_caret_position(caret_position.to_i)]
596
- end
597
-
598
- def line_index_for_caret_position(caret_position)
599
- dirty_content[0...caret_position.to_i].count("\n")
600
- end
601
-
602
- def caret_position_for_line_index(line_index)
603
- cp = lines[0...line_index].join("\n").size
604
- cp += 1 if line_index > 0
605
- cp
606
- end
607
-
608
- def caret_position_for_caret_position_start_of_line(caret_position)
609
- caret_position_for_line_index(line_index_for_caret_position(caret_position))
610
- end
611
-
612
- # position within line containing "caret position" (e.g. for caret position 5 in 1st line, they match as 5, for 15 in line 2 with line 1 having 10 characters, line position is 4)
613
- # TODO consider renaming to line_character_position_for_caret_position
614
- def line_position_for_caret_position(caret_position)
615
- caret_position = caret_position.to_i
616
- caret_position - caret_position_for_caret_position_start_of_line(caret_position)
617
- end
618
-
619
- def line_caret_positions_for_selection(caret_position, selection_count)
620
- line_indices = line_indices_for_selection(caret_position, selection_count)
621
- line_caret_positions = line_indices.map { |line_index| caret_position_for_line_index(line_index) }.to_a
622
- end
623
-
624
- def end_caret_position_line_index(caret_position, selection_count)
625
- end_caret_position = caret_position + selection_count.to_i
626
- end_caret_position -= 1 if dirty_content[end_caret_position - 1] == "\n"
627
- end_line_index = line_index_for_caret_position(end_caret_position)
628
- end
629
-
630
- def lines_for_selection(caret_position, selection_count)
631
- line_indices = line_indices_for_selection(caret_position, selection_count)
632
- lines[line_indices.first..line_indices.last]
633
- end
634
-
635
- def line_indices_for_selection(caret_position, selection_count)
636
- start_line_index = line_index_for_caret_position(caret_position)
637
- if selection_count.to_i > 0
638
- end_line_index = end_caret_position_line_index(caret_position, selection_count)
639
- else
640
- end_line_index = start_line_index
641
- end
642
- (start_line_index..end_line_index).to_a
643
- end
644
-
645
- def children
646
- []
647
- end
648
-
649
- def to_s
650
- path
651
- end
652
-
653
- def eql?(other)
654
- self.path.eql?(other&.path)
655
- end
656
-
657
- def hash
658
- self.path.hash
659
- end
660
- end
661
- end
662
- end
1
+ module Glimmer
2
+ class Gladiator
3
+ class File
4
+ include Glimmer
5
+
6
+ attr_accessor :line_numbers_content, :line_number, :find_text, :replace_text, :top_pixel, :display_path, :case_sensitive, :caret_position, :selection_count, :last_caret_position, :last_selection_count, :line_position
7
+ attr_reader :name, :path, :project_dir
8
+
9
+ def initialize(path='', project_dir=nil)
10
+ raise "Not a file path: #{path}" if path.nil? || (!path.empty? && !::File.file?(path))
11
+ @project_dir = project_dir
12
+ @command_history = []
13
+ @name = path.empty? ? 'Scratchpad' : ::File.basename(path)
14
+ self.path = ::File.expand_path(path) unless path.empty?
15
+ @top_pixel = 0
16
+ @caret_position = 0
17
+ @selection_count = 0
18
+ @last_selection_count = 0
19
+ @line_number = 1
20
+ @init = nil
21
+ end
22
+
23
+ def language
24
+ # TODO consider using Rouge::Lexer.guess_by_filename instead and perhaps guess_by_source when it fails
25
+ extension = path.split('.').last if path.to_s.include?('.')
26
+ return 'ruby' if scratchpad?
27
+ return 'ruby' if path.to_s.end_with?('Gemfile') || path.to_s.end_with?('Rakefile')
28
+ return 'ruby' if dirty_content.start_with?('#!/usr/bin/env ruby') || dirty_content.start_with?('#!/usr/bin/env jruby')
29
+ return 'yaml' if path.to_s.end_with?('Gemfile.lock')
30
+ return 'shell' if extension.nil? && path.to_s.include?('/bin/')
31
+ case extension
32
+ # TODO extract case statement to an external config file
33
+ when 'rb'
34
+ 'ruby'
35
+ when 'md', 'markdown'
36
+ 'markdown'
37
+ when 'js', 'es6'
38
+ 'javascript'
39
+ when 'json'
40
+ 'json'
41
+ when 'yaml'
42
+ 'yaml'
43
+ when 'html'
44
+ 'html'
45
+ when 'h', 'c'
46
+ 'c'
47
+ when 'hs'
48
+ 'haskell'
49
+ when 'gradle'
50
+ 'gradle'
51
+ when 'cpp'
52
+ 'cpp'
53
+ when 'css'
54
+ 'css'
55
+ when 'java'
56
+ 'java'
57
+ when 'jsp'
58
+ 'jsp'
59
+ when 'plist'
60
+ 'plist'
61
+ when 'haml'
62
+ 'haml'
63
+ when 'xml'
64
+ 'xml'
65
+ when 'ini'
66
+ 'ini'
67
+ when 'pl'
68
+ 'perl'
69
+ when 'tcl'
70
+ 'tcl'
71
+ when 'sass'
72
+ 'sass'
73
+ when 'scss'
74
+ 'scss'
75
+ when 'sql'
76
+ 'sql'
77
+ when 'sh'
78
+ 'shell'
79
+ when 'vue'
80
+ 'vue'
81
+ when 'txt', nil
82
+ 'plain_text'
83
+ end
84
+ end
85
+
86
+ def init_content
87
+ unless @init
88
+ @init = true
89
+ begin
90
+ # test read dirty content
91
+ observe(self, :dirty_content) do
92
+ line_count = lines.empty? ? 1 : lines.size
93
+ lines_text_size = [line_count.to_s.size, 4].max
94
+ old_top_pixel = top_pixel
95
+ self.line_numbers_content = line_count.times.map {|n| (' ' * (lines_text_size - (n+1).to_s.size)) + (n+1).to_s }.join("\n")
96
+ self.top_pixel = old_top_pixel
97
+ end
98
+ the_dirty_content = read_dirty_content
99
+ the_dirty_content.split("\n") # test that it is not a binary file (crashes to rescue block otherwise)
100
+ self.dirty_content = the_dirty_content
101
+ observe(self, :caret_position) do |new_caret_position|
102
+ update_line_number_from_caret_position(new_caret_position)
103
+ end
104
+ observe(self, :line_number) do |new_line_number|
105
+ line_index = line_number - 1
106
+ new_caret_position = caret_position_for_line_index(line_index)
107
+ current_caret_position = caret_position
108
+ line_index_for_new_caret_position = line_index_for_caret_position(new_caret_position)
109
+ line_index_for_current_caret_position = line_index_for_caret_position(current_caret_position)
110
+ self.caret_position = new_caret_position unless (current_caret_position && line_index_for_new_caret_position == line_index_for_current_caret_position)
111
+ end
112
+ rescue # in case of a binary file
113
+ stop_filewatcher
114
+ end
115
+ end
116
+ end
117
+
118
+ def update_line_number_from_caret_position(new_caret_position)
119
+ new_line_number = line_index_for_caret_position(caret_position) + 1
120
+ current_line_number = line_number
121
+ unless (current_line_number && current_line_number == new_line_number)
122
+ self.line_number = new_line_number
123
+ # TODO check if the following line is needed
124
+ self.line_position = caret_position - caret_position_for_line_index(line_number - 1) + 1
125
+ end
126
+ end
127
+
128
+ def path=(the_path)
129
+ @path = the_path
130
+ generate_display_path
131
+ end
132
+
133
+ def generate_display_path
134
+ return if @path.empty?
135
+ @display_path = @path.sub(project_dir.path, '').sub(/^\//, '')
136
+ end
137
+
138
+ def name=(the_name)
139
+ new_path = path.sub(/#{Regexp.escape(@name)}$/, the_name) unless scratchpad?
140
+ @name = the_name
141
+ if !scratchpad? && ::File.exist?(path)
142
+ FileUtils.mv(path, new_path)
143
+ self.path = new_path
144
+ end
145
+ end
146
+
147
+ def scratchpad?
148
+ path.to_s.empty?
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
+ format_dirty_content_for_writing!(force: true) if value.to_s.include?("\r\n")
208
+ update_line_number_from_caret_position(caret_position)
209
+ end
210
+
211
+ def start_command
212
+ @commmand_in_progress = true
213
+ end
214
+
215
+ def end_command
216
+ @commmand_in_progress = false
217
+ end
218
+
219
+ def command_in_progress?
220
+ @commmand_in_progress
221
+ end
222
+
223
+ def close
224
+ stop_filewatcher
225
+ remove_all_observers
226
+ initialize(path, project_dir)
227
+ Command.clear(self)
228
+ end
229
+
230
+ def read_dirty_content
231
+ path.empty? ? '' : ::File.read(path)
232
+ end
233
+
234
+ def start_filewatcher
235
+ return if scratchpad?
236
+ @filewatcher = Filewatcher.new(@path)
237
+ @thread = Thread.new(@filewatcher) do |fw|
238
+ fw.watch do |filename, event|
239
+ async_exec do
240
+ begin
241
+ self.dirty_content = read_dirty_content if read_dirty_content != dirty_content
242
+ rescue StandardError, Errno::ENOENT
243
+ # in case of a binary file
244
+ stop_filewatcher
245
+ end
246
+ end
247
+ end
248
+ end
249
+ end
250
+
251
+ def stop_filewatcher
252
+ @filewatcher&.stop
253
+ end
254
+
255
+ def write_dirty_content
256
+ # TODO write partial dirty content by line(s) for enhanced performance
257
+ return if scratchpad? || !::File.exist?(path) || !::File.exists?(path) || read_dirty_content == dirty_content
258
+ format_dirty_content_for_writing!
259
+ ::File.write(path, dirty_content)
260
+ rescue StandardError, ArgumentError => e
261
+ puts "Error in writing dirty content for #{path}"
262
+ puts e.full_message
263
+ end
264
+
265
+ def format_dirty_content_for_writing!(force: false)
266
+ return if !force && @commmand_in_progress
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
+ caret_position_diff = dirty_content.to_s.size - new_dirty_content.size
272
+ self.dirty_content = new_dirty_content
273
+ self.caret_position = caret_position - caret_position_diff
274
+ @formatting_dirty_content_for_writing = false
275
+ end
276
+ end
277
+
278
+ def write_raw_dirty_content
279
+ return if scratchpad? || !::File.exist?(path)
280
+ ::File.write(path, dirty_content) if ::File.exists?(path)
281
+ rescue => e
282
+ puts "Error in writing raw dirty content for #{path}"
283
+ puts e.full_message
284
+ end
285
+
286
+ def current_line_indentation
287
+ current_line.to_s.match(/^(\s+)/).to_a[1].to_s
288
+ end
289
+
290
+ def current_line
291
+ lines[line_number - 1]
292
+ end
293
+
294
+ def delete!
295
+ FileUtils.rm(path) unless scratchpad?
296
+ end
297
+
298
+ def prefix_new_line!
299
+ the_lines = lines
300
+ the_lines[line_number-1...line_number-1] = [current_line_indentation]
301
+ self.dirty_content = the_lines.join("\n")
302
+ self.caret_position = caret_position_for_line_index(line_number-1) + current_line_indentation.size
303
+ self.selection_count = 0
304
+ end
305
+
306
+ def insert_new_line!
307
+ the_lines = lines
308
+ the_lines[line_number...line_number] = [current_line_indentation]
309
+ self.dirty_content = the_lines.join("\n")
310
+ self.caret_position = caret_position_for_line_index(line_number) + current_line_indentation.size
311
+ self.selection_count = 0
312
+ end
313
+
314
+ def comment_line!
315
+ old_lines = lines
316
+ return if old_lines.size < 1
317
+ old_selection_count = self.selection_count
318
+ old_caret_position = self.caret_position
319
+ old_caret_position_line_index = line_index_for_caret_position(old_caret_position)
320
+ old_caret_position_line_caret_position = caret_position_for_line_index(old_caret_position_line_index)
321
+ old_end_caret_line_index = end_caret_position_line_index(caret_position, selection_count)
322
+ new_lines = lines
323
+ delta = 0
324
+ line_indices_for_selection(caret_position, selection_count).reverse.each do | the_line_index |
325
+ delta = 0
326
+ the_line = old_lines[the_line_index]
327
+ return if the_line.nil?
328
+ if the_line.strip.start_with?('# ')
329
+ new_lines[the_line_index] = the_line.sub(/# /, '')
330
+ delta -= 2
331
+ elsif the_line.strip.start_with?('#')
332
+ new_lines[the_line_index] = the_line.sub(/#/, '')
333
+ delta -= 1
334
+ else
335
+ new_lines[the_line_index] = "# #{the_line}"
336
+ delta += 2
337
+ end
338
+ end
339
+ self.dirty_content = new_lines.join("\n")
340
+ if old_selection_count.to_i > 0
341
+ self.caret_position = caret_position_for_line_index(old_caret_position_line_index)
342
+ self.selection_count = (caret_position_for_line_index(old_end_caret_line_index + 1) - self.caret_position)
343
+ else
344
+ new_caret_position = old_caret_position + delta
345
+ new_caret_position = [new_caret_position, old_caret_position_line_caret_position].max
346
+ self.caret_position = new_caret_position
347
+ self.selection_count = 0
348
+ end
349
+ end
350
+
351
+ def indent!
352
+ new_lines = lines
353
+ old_lines = lines
354
+ return if old_lines.size < 1
355
+ old_selection_count = self.selection_count
356
+ old_caret_position = self.caret_position
357
+ old_caret_position_line_index = line_index_for_caret_position(old_caret_position)
358
+ old_caret_position_line_caret_position = caret_position_for_line_index(old_caret_position_line_index)
359
+ old_end_caret_line_index = end_caret_position_line_index(caret_position, selection_count)
360
+ delta = 2
361
+ line_indices_for_selection(caret_position, selection_count).each do |the_line_index|
362
+ the_line = old_lines[the_line_index]
363
+ new_lines[the_line_index] = " #{the_line}"
364
+ end
365
+ old_caret_position = self.caret_position
366
+ self.dirty_content = new_lines.join("\n")
367
+ if old_selection_count.to_i > 0
368
+ self.caret_position = caret_position_for_line_index(old_caret_position_line_index)
369
+ self.selection_count = (caret_position_for_line_index(old_end_caret_line_index + 1) - self.caret_position)
370
+ else
371
+ self.caret_position = old_caret_position + delta
372
+ self.selection_count = 0
373
+ end
374
+ end
375
+
376
+ def outdent!
377
+ new_lines = lines
378
+ old_lines = lines
379
+ return if old_lines.size < 1
380
+ old_selection_count = self.selection_count
381
+ old_caret_position = self.caret_position
382
+ old_caret_position_line_index = line_index_for_caret_position(old_caret_position)
383
+ old_caret_position_line_caret_position = caret_position_for_line_index(old_caret_position_line_index)
384
+ old_end_caret_line_index = end_caret_position_line_index(caret_position, selection_count)
385
+ delta = 0
386
+ line_indices_for_selection(caret_position, selection_count).each do |the_line_index|
387
+ the_line = old_lines[the_line_index]
388
+ if the_line.to_s.start_with?(' ')
389
+ new_lines[the_line_index] = the_line.sub(/ /, '')
390
+ delta = -2
391
+ elsif the_line&.start_with?(' ')
392
+ new_lines[the_line_index] = the_line.sub(/ /, '')
393
+ delta = -1
394
+ end
395
+ end
396
+ self.dirty_content = new_lines.join("\n")
397
+ if old_selection_count.to_i > 0
398
+ self.caret_position = caret_position_for_line_index(old_caret_position_line_index)
399
+ self.selection_count = (caret_position_for_line_index(old_end_caret_line_index + 1) - self.caret_position)
400
+ else
401
+ new_caret_position = old_caret_position + delta
402
+ new_caret_position = [new_caret_position, old_caret_position_line_caret_position].max
403
+ self.caret_position = new_caret_position
404
+ self.selection_count = 0
405
+ end
406
+ end
407
+
408
+ def kill_line!
409
+ new_lines = lines
410
+ return if new_lines.size < 1
411
+ line_indices = line_indices_for_selection(caret_position, selection_count)
412
+ new_lines = new_lines[0...line_indices.first] + new_lines[(line_indices.last+1)...new_lines.size]
413
+ old_caret_position = self.caret_position
414
+ old_line_index = self.line_number - 1
415
+ line_position = line_position_for_caret_position(old_caret_position)
416
+ self.dirty_content = "#{new_lines.join("\n")}\n"
417
+ self.caret_position = caret_position_for_line_index(old_line_index) + [line_position, lines[old_line_index].to_s.size].min
418
+ self.selection_count = 0
419
+ end
420
+
421
+ def duplicate_line!
422
+ new_lines = lines
423
+ old_lines = lines
424
+ return if old_lines.size < 1
425
+ old_selection_count = self.selection_count
426
+ old_caret_position = self.caret_position
427
+ old_caret_position_line_index = line_index_for_caret_position(old_caret_position)
428
+ old_caret_position_line_caret_position = caret_position_for_caret_position_start_of_line(old_caret_position_line_index)
429
+ old_end_caret_line_index = end_caret_position_line_index(caret_position, selection_count)
430
+ the_line_indices = line_indices_for_selection(caret_position, selection_count)
431
+ the_lines = lines_for_selection(caret_position, selection_count)
432
+ delta = the_lines.join("\n").size + 1
433
+ the_lines.each_with_index do |the_line, i|
434
+ new_lines.insert(the_line_indices.first + i, the_line)
435
+ end
436
+ self.dirty_content = new_lines.join("\n")
437
+ if old_selection_count.to_i > 0
438
+ self.caret_position = caret_position_for_line_index(old_caret_position_line_index)
439
+ self.selection_count = (caret_position_for_line_index(old_end_caret_line_index + 1) - self.caret_position)
440
+ else
441
+ self.caret_position = old_caret_position + delta
442
+ self.selection_count = 0
443
+ end
444
+ end
445
+
446
+ def find_next
447
+ return if find_text.to_s.empty?
448
+ all_lines = lines
449
+ the_line_index = line_index_for_caret_position(caret_position)
450
+ line_position = line_position_for_caret_position(caret_position)
451
+ found = found_text?(caret_position)
452
+ 2.times do |i|
453
+ rotation = the_line_index
454
+ all_lines.rotate(rotation).each_with_index do |the_line, the_index|
455
+ the_index = (the_index + rotation)%all_lines.size
456
+ start_position = 0
457
+ start_position = line_position + find_text.to_s.size if i == 0 && the_index == the_line_index && found_text?(caret_position)
458
+ text_to_find_in = the_line[start_position..-1]
459
+ occurrence_index = case_sensitive ? text_to_find_in&.index(find_text.to_s) : text_to_find_in&.downcase&.index(find_text.to_s.downcase)
460
+ if occurrence_index
461
+ self.caret_position = caret_position_for_line_index(the_index) + start_position + occurrence_index
462
+ self.selection_count = find_text.to_s.size
463
+ return
464
+ end
465
+ end
466
+ end
467
+ end
468
+
469
+ def find_previous
470
+ return if find_text.to_s.empty?
471
+ all_lines = lines
472
+ the_line_index = line_index_for_caret_position(caret_position)
473
+ line_position = line_position_for_caret_position(caret_position)
474
+ 2.times do |i|
475
+ rotation = - the_line_index - 1 + all_lines.size
476
+ all_lines.reverse.rotate(rotation).each_with_index do |the_line, the_index|
477
+ the_index = all_lines.size - 1 - (the_index + rotation)%all_lines.size
478
+ if the_index == the_line_index
479
+ start_position = i > 0 ? 0 : (the_line.size - line_position)
480
+ else
481
+ start_position = 0
482
+ end
483
+ text_to_find_in = the_line.downcase.reverse[start_position...the_line.size].to_s
484
+ occurrence_index = text_to_find_in.index(find_text.to_s.downcase.reverse)
485
+ if occurrence_index
486
+ self.caret_position = caret_position_for_line_index(the_index) + (the_line.size - (start_position + occurrence_index + find_text.to_s.size))
487
+ self.selection_count = find_text.to_s.size
488
+ return
489
+ end
490
+ end
491
+ end
492
+ end
493
+
494
+ def ensure_find_next
495
+ return if find_text.to_s.empty? || dirty_content.to_s.strip.size < 1
496
+ find_next unless found_text?(self.caret_position)
497
+ end
498
+
499
+ def found_text?(caret_position)
500
+ dirty_content[caret_position.to_i, find_text.to_s.size].to_s.downcase == find_text.to_s.downcase
501
+ end
502
+
503
+ def replace_next!
504
+ return if find_text.to_s.empty? || dirty_content.to_s.strip.size < 1
505
+ ensure_find_next
506
+ new_dirty_content = dirty_content
507
+ new_dirty_content[caret_position, find_text.size] = replace_text.to_s
508
+ self.dirty_content = new_dirty_content
509
+ find_next
510
+ find_next if replace_text.to_s.include?(find_text) && !replace_text.to_s.start_with?(find_text)
511
+ end
512
+
513
+ def page_up
514
+ self.selection_count = 0
515
+ self.line_number = [(self.line_number - 15), 1].max
516
+ end
517
+
518
+ def page_down
519
+ self.selection_count = 0
520
+ self.line_number = [(self.line_number + 15), lines.size].min
521
+ end
522
+
523
+ def home
524
+ self.line_number = 1
525
+ self.selection_count = 0
526
+ end
527
+
528
+ def end
529
+ self.line_number = lines.size
530
+ self.selection_count = 0
531
+ end
532
+
533
+ def start_of_line
534
+ self.caret_position = caret_position_for_line_index(self.line_number - 1)
535
+ self.selection_count = 0
536
+ end
537
+
538
+ def end_of_line
539
+ self.caret_position = caret_position_for_line_index(self.line_number) - 1
540
+ self.selection_count = 0
541
+ end
542
+
543
+ def select_to_start_of_line
544
+ old_caret_position = caret_position
545
+ self.caret_position = caret_position_for_line_index(self.line_number - 1)
546
+ self.selection_count = old_caret_position - caret_position
547
+ end
548
+
549
+ def select_to_end_of_line
550
+ self.caret_position = selection_count == 0 ? caret_position : caret_position + selection_count
551
+ self.selection_count = (caret_position_for_line_index(self.line_number) - 1) - caret_position
552
+ end
553
+
554
+ def delete!
555
+ new_dirty_content = dirty_content
556
+ new_dirty_content[caret_position...(caret_position + selection_count)] = ''
557
+ old_caret_position = caret_position
558
+ self.dirty_content = new_dirty_content
559
+ self.caret_position = old_caret_position
560
+ end
561
+
562
+ def cut!
563
+ Clipboard.copy(selected_text)
564
+ delete!
565
+ end
566
+
567
+ def copy
568
+ Clipboard.copy(selected_text)
569
+ end
570
+
571
+ def paste!
572
+ new_dirty_content = dirty_content
573
+ pasted_text = Clipboard.paste
574
+ new_dirty_content[caret_position...(caret_position + selection_count)] = pasted_text
575
+ old_caret_position = caret_position
576
+ self.dirty_content = new_dirty_content
577
+ self.caret_position = old_caret_position + pasted_text.to_s.size
578
+ self.selection_count = 0
579
+ end
580
+
581
+ def select_all
582
+ self.caret_position = 0
583
+ self.selection_count = dirty_content.to_s.size
584
+ end
585
+
586
+ def selected_text
587
+ dirty_content.to_s[caret_position, selection_count]
588
+ end
589
+
590
+ def move_up!
591
+ old_lines = lines
592
+ return if old_lines.size < 2
593
+ old_selection_count = self.selection_count
594
+ old_caret_position = self.caret_position
595
+ old_caret_position_line_caret_position = caret_position_for_caret_position_start_of_line(old_caret_position)
596
+ old_caret_position_line_position = old_caret_position - old_caret_position_line_caret_position
597
+ old_end_caret_line_index = end_caret_position_line_index(caret_position, selection_count)
598
+ new_lines = lines
599
+ the_line_indices = line_indices_for_selection(caret_position, selection_count)
600
+ the_lines = lines_for_selection(caret_position, selection_count)
601
+ new_line_index = [the_line_indices.first - 1, 0].max
602
+ new_lines[the_line_indices.first..the_line_indices.last] = []
603
+ new_lines[new_line_index...new_line_index] = the_lines
604
+ self.dirty_content = new_lines.join("\n")
605
+ self.caret_position = caret_position_for_line_index(new_line_index) + [old_caret_position_line_position, new_lines[new_line_index].size].min
606
+ self.selection_count = old_selection_count.to_i if old_selection_count.to_i > 0
607
+ end
608
+
609
+ def move_down!
610
+ old_lines = lines
611
+ return if old_lines.size < 2
612
+ old_selection_count = self.selection_count
613
+ old_caret_position = self.caret_position
614
+ old_caret_position_line_caret_position = caret_position_for_caret_position_start_of_line(old_caret_position)
615
+ old_caret_position_line_position = old_caret_position - old_caret_position_line_caret_position
616
+ old_end_caret_line_index = end_caret_position_line_index(caret_position, selection_count)
617
+ new_lines = lines
618
+ the_line_indices = line_indices_for_selection(caret_position, selection_count)
619
+ the_lines = lines_for_selection(caret_position, selection_count)
620
+ new_line_index = [the_line_indices.first + 1, new_lines.size - 1].min
621
+ new_lines[the_line_indices.first..the_line_indices.last] = []
622
+ new_lines[new_line_index...new_line_index] = the_lines
623
+ self.dirty_content = new_lines.join("\n")
624
+ self.caret_position = caret_position_for_line_index(new_line_index) + [old_caret_position_line_position, new_lines[new_line_index].size].min
625
+ self.selection_count = old_selection_count.to_i if old_selection_count.to_i > 0
626
+ end
627
+
628
+ def run
629
+ if scratchpad?
630
+ TOPLEVEL_BINDING.receiver.send(:eval, content)
631
+ else
632
+ write_dirty_content
633
+ load path
634
+ end
635
+ end
636
+
637
+ def lines
638
+ need_padding = dirty_content.to_s.end_with?("\n")
639
+ splittable_content = need_padding ? "#{dirty_content} " : dirty_content
640
+ the_lines = splittable_content.split("\n")
641
+ the_lines[-1] = the_lines[-1].strip if need_padding
642
+ the_lines
643
+ end
644
+
645
+ def line_for_caret_position(caret_position)
646
+ lines[line_index_for_caret_position(caret_position.to_i)]
647
+ end
648
+
649
+ def line_index_for_caret_position(caret_position)
650
+ dirty_content[0...caret_position.to_i].count("\n")
651
+ end
652
+
653
+ def caret_position_for_line_index(line_index)
654
+ cp = lines[0...line_index].join("\n").size
655
+ cp += 1 if line_index > 0
656
+ cp
657
+ end
658
+
659
+ def caret_position_for_caret_position_start_of_line(caret_position)
660
+ caret_position_for_line_index(line_index_for_caret_position(caret_position))
661
+ end
662
+
663
+ # 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)
664
+ # TODO consider renaming to line_character_position_for_caret_position
665
+ def line_position_for_caret_position(caret_position)
666
+ caret_position = caret_position.to_i
667
+ caret_position - caret_position_for_caret_position_start_of_line(caret_position)
668
+ end
669
+
670
+ def line_caret_positions_for_selection(caret_position, selection_count)
671
+ line_indices = line_indices_for_selection(caret_position, selection_count)
672
+ line_caret_positions = line_indices.map { |line_index| caret_position_for_line_index(line_index) }.to_a
673
+ end
674
+
675
+ def end_caret_position_line_index(caret_position, selection_count)
676
+ end_caret_position = caret_position + selection_count.to_i
677
+ end_caret_position -= 1 if dirty_content[end_caret_position - 1] == "\n"
678
+ end_line_index = line_index_for_caret_position(end_caret_position)
679
+ end
680
+
681
+ def lines_for_selection(caret_position, selection_count)
682
+ line_indices = line_indices_for_selection(caret_position, selection_count)
683
+ lines[line_indices.first..line_indices.last]
684
+ end
685
+
686
+ def line_indices_for_selection(caret_position, selection_count)
687
+ start_line_index = line_index_for_caret_position(caret_position)
688
+ if selection_count.to_i > 0
689
+ end_line_index = end_caret_position_line_index(caret_position, selection_count)
690
+ else
691
+ end_line_index = start_line_index
692
+ end
693
+ (start_line_index..end_line_index).to_a
694
+ end
695
+
696
+ def children
697
+ []
698
+ end
699
+
700
+ def to_s
701
+ path
702
+ end
703
+
704
+ def eql?(other)
705
+ self.path.eql?(other&.path)
706
+ end
707
+
708
+ def hash
709
+ self.path.hash
710
+ end
711
+ end
712
+ end
713
+ end