glimmer-cs-gladiator 0.8.1 → 0.8.2

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