glimmer-cs-gladiator 0.4.1 → 0.5.4

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: 22778fafc962d8b91e8901b2a70ca645b5cfd5a3dbcf089efa688aecaf643924
4
- data.tar.gz: '0190bf73da9810409ef4cd6f392847c9186761b8e3e71d8094e0e82becfe4600'
3
+ metadata.gz: d31e7dd5189317eaeb3a91a175a050ffaaeb4e82d6c8709167477d26dd7bf237
4
+ data.tar.gz: 370a540665a9d7f3d08154f1e98530521f72e9a720f35fa83f982747e2e5bf5e
5
5
  SHA512:
6
- metadata.gz: 513bedaec5b4d2ec4285b8ab32663099207dc7638588b31d3007b6acf87dcb5c1b03d276d9900f485ea1da1fcd2a3307e0463331caa5e3a87efe00c63632f17d
7
- data.tar.gz: f0fec7f0f3183602e7c287f7edb777b367df756ac76e6e095091821328cc7891623b03f8b6428c24502f8fc90e6aefe5a8b591029c42b2a569a3e115dc037ee8
6
+ metadata.gz: 7af2ad9407c442ad6634a843ad3b5fc6cac47dadbd84ec0dc0b19f8d38b858950648a951383be811f040232ab426cbf0dffc1ad1d003a33a6a3cefd07d6c6832
7
+ data.tar.gz: a9d30dc68de3f7de8fa25c9860c57885892e4ceeea734c63bc4fd2b257736afc0e1b0c7a062d03af93991d03d05907dbfbe058adcf6e13d695ce524167ce5aef
data/README.md CHANGED
@@ -1,20 +1,21 @@
1
- # <img src='https://raw.githubusercontent.com/AndyObtiva/glimmer-cs-gladiator/master/images/glimmer-cs-gladiator-logo.svg' height=85 /> Gladiator 0.4.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.5.4 - [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
 
5
5
  ![Gladiator](images/glimmer-gladiator.png)
6
6
 
7
- (Now, slightly less ugly with Ruby syntax highlighting colors)
8
-
9
- Gladiator (short for Glimmer Editor) is a [Glimmer DSL for SWT](https://github.com/AndyObtiva/glimmer-dsl-swt) sample project under on-going development that demonstrates how to build a text editor in Ruby using [Glimmer DSL for SWT](https://github.com/AndyObtiva/glimmer-dsl-swt) (JRuby Desktop Development GUI Library).
7
+ Gladiator (short for Glimmer Editor) is a [Glimmer DSL for SWT](https://github.com/AndyObtiva/glimmer-dsl-swt) sample beta project under on-going development that demonstrates how to build a text editor in Ruby using [Glimmer DSL for SWT](https://github.com/AndyObtiva/glimmer-dsl-swt) (JRuby Desktop Development GUI Library).
10
8
  It is not intended to be a full-fledged editor by any means, yet mostly a fun educational exercise in using [Glimmer](https://github.com/AndyObtiva/glimmer).
11
- Gladiator is also a personal tool for shaping an editor exactly the way I like, with all the keyboard shortcuts I prefer.
9
+ Gladiator is also a personal tool for shaping an editor exactly the way I like, with all the keyboard shortcuts I prefer.
12
10
  I leave building truly professional text editors to software tooling experts who would hopefully use [Glimmer](https://github.com/AndyObtiva/glimmer) one day. Otherwise, I have been happily using Gladiator to develop all my [open-source projects](https://github.com/AndyObtiva) since May of 2020.
13
11
 
14
12
  ## Features
15
13
 
16
14
  Gladiator currently supports the following text editing features (including keyboard shortcuts with Mac CMD=CTRL on Windows/Linux):
17
15
  - Ruby Syntax Highlighting with colors
16
+ - Open Project from File Menu (CMD+SHIFT+P)
17
+ - Scratchpad for running arbitrary Ruby/Glimmer code without saving to disk (CMD+SHIFT+S)
18
+ - Run Ruby code (CMD+SHIFT+R)
18
19
  - File explorer navigation with context menu to open file, rename, delete, add new file, add new directory, or refresh tree (CMD+T)
19
20
  - File lookup by name ignoring slashes, underscores, and dots to ease lookup (CMD+R)
20
21
  - Watch open file for external changes to automatically refresh in editor
@@ -24,7 +25,7 @@ Gladiator currently supports the following text editing features (including keyb
24
25
  - Jump to Line (CMD+L)
25
26
  - Multiple tab support (CMD+SHIFT+[ & CMD+SHIFT+] for tab navigation. CMD+1-9 to jump to a specific tab)
26
27
  - Remember opened tabs, caret position, top line, window size, and window location
27
- - Autosave on focus out/quit/open new file
28
+ - Autosave on focus out/quit/open new file
28
29
  - Duplicate Line(s)/selection (CMD+D)
29
30
  - Kill Line(s)/selection (CMD+K)
30
31
  - Move line/selection up (CMD+UP)
@@ -32,15 +33,14 @@ Gladiator currently supports the following text editing features (including keyb
32
33
  - Comment/Uncomment line/selection (CMD+/)
33
34
  - Indent/Unindent line/selection (CMD+] & CMD+[)
34
35
  - Insert/Prefix New Line (CMD+ENTER & CMD+SHIFT+ENTER)
35
- - Drag and Drop Text Editor Split Screen (drag a file from File Tree or File Lookup List, and it splits the screen)
36
- - Run current Ruby file via Run Menu (CMD+SHIFT+R)
37
- - Change Split Orientation to Horizontal/Vertical via View Menu (CMD+SHIFT+O)
36
+ - Split Pane & Change Split Orientation to Horizontal/Vertical via View Menu (CMD+SHIFT+O)
37
+ - Drag and Drop Text Editor Split Screen (drag a file from File Tree or File Lookup List, and it splits the pane)
38
38
 
39
39
  ## Platforms
40
40
 
41
41
  - Mac: Gladiator works best on the Mac. This is the platform it is most used on and receives the most maintenance for.
42
- - Windows: Gladiator works OK on Windows, but has some annoying bugs.
43
- - Linux: Gladiator works with very bad handicaps on Linux (performing text editing operations causes scroll jitter)
42
+ - Windows: Gladiator works OK on Windows, but has some annoying bugs. Contributers could help fix.
43
+ - Linux: Gladiator works with handicaps on Linux (performing some text editing operations causes scroll jitter). Contributers could help fix.
44
44
 
45
45
  ## Pre-requisites
46
46
 
@@ -78,6 +78,12 @@ You may run the `gladiator` command to bring up the text editor in the project d
78
78
  gladiator
79
79
  ```
80
80
 
81
+ On Linux, you may need to run with extra memory via this command instead:
82
+
83
+ ```
84
+ gladiator -J-Xmx1200M
85
+ ```
86
+
81
87
  On Windows, you may need to run with extra memory via this command instead:
82
88
 
83
89
  ```
@@ -100,11 +106,11 @@ bin/gladiator relative-or-absolute-path/to/project
100
106
 
101
107
  ### Glimmer Custom Shell Reuse
102
108
 
103
- To reuse Gladiator as a Glimmer Custom Shell inside another Glimmer application, add the
109
+ To reuse Gladiator as a Glimmer Custom Shell inside another Glimmer application, add the
104
110
  following to the application's `Gemfile`:
105
111
 
106
112
  ```
107
- gem 'glimmer-cs-gladiator', '~> 0.4.1'
113
+ gem 'glimmer-cs-gladiator', '~> 0.5.4'
108
114
  ```
109
115
 
110
116
  Run:
@@ -117,7 +123,7 @@ And, then instantiate the Gladiator [custom shell](https://github.com/AndyObtiva
117
123
 
118
124
  ## Env Var Options
119
125
 
120
- Gladiator opens with the current directory as the root by default.
126
+ Gladiator opens with the current directory as the root by default.
121
127
  If you would like to open another directory, set `LOCAL_DIR` environment variable.
122
128
 
123
129
  Example:
@@ -133,13 +139,9 @@ Opens Gladiator with "/Users/User/code" as the root directory.
133
139
  Gladiator automatically saves configuration data in a `.gladiator` file at the directory it is run from.
134
140
 
135
141
  It currently remembers:
136
- - Last opened file
137
- - Caret position
138
- - Top line position
139
- - Window size
140
- - Opened tabs
141
- - Split tabs
142
- - Ignore Paths
142
+ - Last opened files (in both split panes if split)
143
+ - Window size and position
144
+ - Ignore Paths (default: '.gladiator', '.git', 'coverage', 'packages', 'node_modules', 'tmp', 'vendor')
143
145
 
144
146
  ## Gotcha
145
147
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.4.1
1
+ 0.5.4
@@ -2,4 +2,5 @@ require_relative '../lib/glimmer-cs-gladiator'
2
2
 
3
3
  include Glimmer
4
4
 
5
- gladiator.open
5
+ local_dir = ENV['LOCAL_DIR'] || '.'
6
+ gladiator(project_dir_path: local_dir).open
@@ -23,7 +23,7 @@ module Glimmer
23
23
  args = SWTProxy.constantify_args(args)
24
24
  @swt_layout = self.class.swt_layout_class_for(underscored_layout_name).new(*args)
25
25
  @widget_proxy.swt_widget.setLayout(@swt_layout)
26
- end
26
+ end
27
27
  end
28
28
  end
29
29
  end
@@ -1,6 +1,6 @@
1
1
  module Glimmer
2
2
  class Gladiator
3
- class Command
3
+ class Command
4
4
  class << self
5
5
  include Glimmer
6
6
 
@@ -10,10 +10,10 @@ module Glimmer
10
10
 
11
11
  def command_history_for(file)
12
12
  # keeping a first command to make redo support work by remembering next command after undoing all
13
- command_history[file] ||= [Command.new(file)]
13
+ command_history[file] ||= [Command.new(file)]
14
14
  end
15
15
 
16
- def do(file, method = nil, command: nil)
16
+ def do(file, method = nil, command: nil)
17
17
  command ||= Command.new(file, method)
18
18
  command_history_for(file)&.last&.next_command = command
19
19
  command.do
@@ -21,7 +21,7 @@ module Glimmer
21
21
  end
22
22
 
23
23
  def undo(file)
24
- return if command_history_for(file).size <= 1
24
+ return if command_history_for(file).size <= 1
25
25
  command = command_history_for(file).pop
26
26
  command&.undo
27
27
  end
@@ -4,39 +4,49 @@ module Glimmer
4
4
  class Gladiator
5
5
  class Dir
6
6
  include Glimmer
7
+ include Glimmer::DataBinding::ObservableModel
7
8
 
8
- REFRESH_DELAY = 7
9
+ attr_accessor :selected_child, :filter, :children, :filtered_path_options, :filtered_path, :display_path, :ignore_paths
10
+ attr_reader :name, :parent, :path
11
+ attr_writer :all_children
9
12
 
10
- class << self
11
- def local_dir
12
- unless @local_dir
13
- @local_dir = new(ENV['LOCAL_DIR'] || '.', true)
14
- # @local_dir.refresh
15
- @filewatcher = Filewatcher.new(@local_dir.path)
16
- @thread = Thread.new(@filewatcher) do |fw|
13
+ def initialize(path, project_dir = nil)
14
+ @project_dir = project_dir
15
+ if is_local_dir
16
+ @filewatcher = Filewatcher.new(path)
17
+ Thread.new(@filewatcher) do |fw|
18
+ begin
17
19
  fw.watch do |filename, event|
18
- if @last_update.nil? || (Time.now.to_f - @last_update) > REFRESH_DELAY
19
- @local_dir.refresh if !filename.include?('new_file') && !@local_dir.selected_child_path_history.include?(filename) && filename != @local_dir.selected_child_path
20
+ # TODO do fine grained processing of events for enhanced performance (e.g. dir refresh vs file change)
21
+ # TODO do fine grained file change only without a refresh delay for enhanced performance
22
+ begin
23
+ if !@refresh_in_progress && !filename.include?('new_file') && (event != :updated || find_child_file(filename).nil?)
24
+ Thread.new {
25
+ refresh
26
+ }
27
+ end
28
+ rescue => e
29
+ puts e.full_message
20
30
  end
21
- @last_update = Time.now.to_f
22
31
  end
32
+ rescue => e
33
+ puts e.full_message
23
34
  end
24
35
  end
25
- @local_dir
26
- end
27
- end
28
-
29
- attr_accessor :selected_child, :filter, :children, :filtered_path_options, :filtered_path, :display_path, :ignore_paths
30
- attr_reader :name, :parent, :path, :is_local_dir
31
- attr_writer :all_children
32
-
33
- def initialize(path, is_local_dir = false)
34
- @is_local_dir = is_local_dir
36
+ end
35
37
  self.path = ::File.expand_path(path)
36
38
  @name = ::File.basename(::File.expand_path(path))
37
- @ignore_paths = ['packages', 'tmp', 'vendor']
39
+ @ignore_paths = ['.gladiator', '.git', 'coverage', 'packages', 'node_modules', 'tmp', 'vendor']
38
40
  self.filtered_path_options = []
39
41
  end
42
+
43
+ def is_local_dir
44
+ @project_dir.nil?
45
+ end
46
+
47
+ def project_dir
48
+ @project_dir || self
49
+ end
40
50
 
41
51
  def path=(the_path)
42
52
  @path = the_path
@@ -44,7 +54,7 @@ module Glimmer
44
54
  end
45
55
 
46
56
  def generate_display_path
47
- is_local_dir ? path : @display_path = @path.sub(Dir.local_dir.path, '').sub(/^\//, '')
57
+ is_local_dir ? path : @display_path = @path.sub(project_dir.path, '').sub(/^\//, '')
48
58
  end
49
59
 
50
60
  def name=(the_name)
@@ -61,20 +71,36 @@ module Glimmer
61
71
 
62
72
  def retrieve_children
63
73
  @children = ::Dir.glob(::File.join(@path, '*')).reject do |p|
64
- # TODO make sure to configure ignore_paths in a preferences dialog
65
- Dir.local_dir.ignore_paths.reduce(false) do |result, ignore_path|
74
+ # TODO make sure to configure ignore_paths in a preferences dialog
75
+ project_dir.ignore_paths.reduce(false) do |result, ignore_path|
66
76
  result || p.include?(ignore_path)
67
77
  end
68
- end.map do |p|
69
- ::File.file?(p) ? Gladiator::File.new(p) : Gladiator::Dir.new(p)
70
- end.sort_by do |c|
71
- c.path.to_s.downcase
72
- end.sort_by do |c|
78
+ end.map do |p|
79
+ ::File.file?(p) ? File.new(p, project_dir) : Dir.new(p, project_dir)
80
+ end.sort_by do |c|
81
+ c.path.to_s.downcase
82
+ end.sort_by do |c|
73
83
  c.class.name
74
84
  end.each do |child|
75
85
  child.retrieve_children if child.is_a?(Dir)
76
86
  end
77
87
  end
88
+
89
+ def find_child_file(child_path)
90
+ depth_first_search_file(self, child_path)
91
+ end
92
+
93
+ def depth_first_search_file(dir, file_path)
94
+ dir.children.each do |child|
95
+ if child.is_a?(File)
96
+ return child if child.path.include?(file_path)
97
+ else
98
+ result = depth_first_search_file(child, file_path)
99
+ return result unless result.nil?
100
+ end
101
+ end
102
+ nil
103
+ end
78
104
 
79
105
  def selected_child_path_history
80
106
  @selected_child_path_history ||= []
@@ -90,6 +116,7 @@ module Glimmer
90
116
 
91
117
  def refresh(async: true, force: false)
92
118
  return if @refresh_paused && !force
119
+ @refresh_in_progress = true
93
120
  retrieve_children
94
121
  collect_all_children
95
122
  refresh_operation = lambda do
@@ -101,11 +128,12 @@ module Glimmer
101
128
  else
102
129
  sync_exec(&refresh_operation)
103
130
  end
131
+ @refresh_in_progress = false
104
132
  end
105
133
 
106
134
  def filter=(value)
107
135
  if value.to_s.empty?
108
- @filter = nil
136
+ @filter = nil
109
137
  else
110
138
  @filter = value
111
139
  end
@@ -117,8 +145,8 @@ module Glimmer
117
145
  def filtered
118
146
  return if filter.nil?
119
147
  children_files = !@last_filter.to_s.empty? && filter.downcase.start_with?(@last_filter.downcase) ? @last_filtered : all_children_files
120
- children_files.select do |child|
121
- child_path = child.path.to_s.sub(Dir.local_dir.path, '')
148
+ children_files.select do |child|
149
+ child_path = child.path.to_s.sub(project_dir.path, '')
122
150
  child_path.downcase.include?(filter.downcase) ||
123
151
  child_path.downcase.gsub(/[_\/.-]/, '').include?(filter.downcase.gsub(/[_\/.-]/, ''))
124
152
  end.sort_by {|c| c.path.to_s.downcase}
@@ -141,14 +169,19 @@ module Glimmer
141
169
  end
142
170
 
143
171
  def selected_child_path=(selected_path)
144
- full_selected_path = selected_path.include?(Dir.local_dir.path) ? selected_path : ::File.join(Dir.local_dir.path, selected_path)
145
- return if selected_path.nil? ||
146
- ::Dir.exist?(full_selected_path) ||
172
+ return (project_dir.selected_child = nil) if selected_path.nil?
173
+ # scratchpad scenario
174
+ if selected_path.empty? # Scratchpad
175
+ @selected_child&.write_dirty_content
176
+ return (self.selected_child = File.new)
177
+ end
178
+ full_selected_path = selected_path.include?(project_dir.path) ? selected_path : ::File.join(project_dir.path, selected_path)
179
+ return if ::Dir.exist?(full_selected_path) ||
147
180
  (selected_child && selected_child.path == full_selected_path)
148
181
  selected_path = full_selected_path
149
182
  if ::File.file?(selected_path)
150
183
  @selected_child&.write_dirty_content
151
- new_child = Gladiator::File.new(selected_path)
184
+ new_child = find_child_file(selected_path)
152
185
  begin
153
186
  unless new_child.dirty_content.nil?
154
187
  self.selected_child&.stop_filewatcher
@@ -168,6 +201,13 @@ module Glimmer
168
201
  @selected_child&.path
169
202
  end
170
203
 
204
+ def selected_child=(new_child)
205
+ return if selected_child == new_child
206
+ file_properties = @selected_child&.backup_properties if @selected_child == new_child
207
+ @selected_child = new_child
208
+ @selected_child.restore_properties(file_properties) if file_properties
209
+ end
210
+
171
211
  def delete!
172
212
  FileUtils.rm_rf(path)
173
213
  end
@@ -183,11 +223,7 @@ module Glimmer
183
223
  def hash
184
224
  self.path.hash
185
225
  end
186
- end
226
+ end
187
227
  end
188
228
  end
189
-
190
- at_exit do
191
- Glimmer::Gladiator::Dir.local_dir.selected_child&.write_raw_dirty_content
192
- end
193
229
 
@@ -1,43 +1,56 @@
1
- require 'models/glimmer/gladiator/dir'
2
-
3
1
  module Glimmer
4
2
  class Gladiator
5
3
  class File
6
4
  include Glimmer
7
-
8
- attr_accessor :dirty_content, :line_numbers_content, :selection, :line_number, :find_text, :replace_text, :top_pixel, :display_path, :case_sensitive
9
- attr_reader :name, :path
10
5
 
11
- def initialize(path)
12
- raise "Not a file path: #{path}" unless ::File.file?(path)
6
+ attr_accessor :line_numbers_content, :selection, :line_number, :find_text, :replace_text, :top_pixel, :display_path, :case_sensitive
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
13
12
  @command_history = []
14
- @name = ::File.basename(path)
15
- self.path = ::File.expand_path(path)
13
+ @name = path.empty? ? 'Scratchpad' : ::File.basename(path)
14
+ self.path = ::File.expand_path(path) unless path.empty?
16
15
  @top_pixel = 0
17
16
  @selection_count = 0
18
17
  @selection = Point.new(0, 0 + @selection_count)
19
- read_dirty_content = ::File.read(path)
20
- begin
21
- # test read dirty content
22
- read_dirty_content.split("\n")
23
- observe(self, :dirty_content) do
24
- lines_text_size = lines.size.to_s.size
25
- self.line_numbers_content = lines.size.times.map {|n| (' ' * (lines_text_size - (n+1).to_s.size)) + (n+1).to_s }.join("\n")
26
- end
27
- @line_number = 1
28
- self.dirty_content = read_dirty_content
29
- observe(self, :selection) do
30
- self.line_number = line_index_for_caret_position(caret_position) + 1
31
- end
32
- observe(self, :line_number) do
33
- if line_number
34
- line_index = line_number - 1
35
- new_caret_position = caret_position_for_line_index(line_index)
36
- self.caret_position = new_caret_position unless self.caret_position && line_index_for_caret_position(new_caret_position) == line_index_for_caret_position(caret_position)
18
+ @line_number = 1
19
+ end
20
+
21
+ def init_content
22
+ unless @init
23
+ @init = true
24
+ begin
25
+ # test read dirty content
26
+ observe(self, :dirty_content) do
27
+ lines_text_size = lines.size.to_s.size
28
+ old_top_pixel = top_pixel
29
+ self.line_numbers_content = lines.size.times.map {|n| (' ' * (lines_text_size - (n+1).to_s.size)) + (n+1).to_s }.join("\n")
30
+ self.top_pixel = old_top_pixel
37
31
  end
32
+ the_dirty_content = read_dirty_content
33
+ the_dirty_content.split("\n") # test that it is not a binary file (crashes to rescue block otherwise)
34
+ self.dirty_content = the_dirty_content
35
+ observe(self, :selection) do
36
+ new_line_number = line_index_for_caret_position(caret_position) + 1
37
+ async_exec {
38
+ self.line_number = new_line_number
39
+ }
40
+ end
41
+ observe(self, :line_number) do
42
+ if line_number
43
+ line_index = line_number - 1
44
+ new_caret_position = caret_position_for_line_index(line_index)
45
+ current_caret_position = self.caret_position
46
+ async_exec {
47
+ self.caret_position = new_caret_position unless current_caret_position && line_index_for_caret_position(new_caret_position) == line_index_for_caret_position(current_caret_position)
48
+ }
49
+ end
50
+ end
51
+ rescue # in case of a binary file
52
+ stop_filewatcher
38
53
  end
39
- rescue
40
- # no op in case of a binary file
41
54
  end
42
55
  end
43
56
 
@@ -45,15 +58,34 @@ module Glimmer
45
58
  @path = the_path
46
59
  generate_display_path
47
60
  end
48
-
61
+
49
62
  def generate_display_path
50
- @display_path = @path.sub(Dir.local_dir.path, '').sub(/^\//, '')
63
+ return if @path.empty?
64
+ @display_path = @path.sub(project_dir.path, '').sub(/^\//, '')
51
65
  end
52
66
 
67
+ def scratchpad?
68
+ path.to_s.empty?
69
+ end
70
+
71
+ def backup_properties
72
+ [:find_text, :replace_text, :case_sensitive, :top_pixel, :caret_position, :selection_count].reduce({}) do |hash, property|
73
+ hash.merge(property => send(property))
74
+ end
75
+ end
76
+
77
+ def restore_properties(properties_hash)
78
+ 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?
79
+ properties_hash.each do |property, value|
80
+ send("#{property}=", value)
81
+ end
82
+ end
83
+
53
84
  # to use for widget data-binding
54
85
  def content=(value)
55
86
  value = value.gsub("\t", ' ')
56
87
  if dirty_content != value
88
+ # TODO fix this command recording hack by truly recording every text change as a proper command (add process_key command, paste command, cut command, etc...)
57
89
  Command.do(self) # record a native (OS-widget) operation
58
90
  self.dirty_content = value
59
91
  end
@@ -68,7 +100,7 @@ module Glimmer
68
100
  self.selection = Point.new(value, value + selection_count.to_i)
69
101
  self.top_pixel = old_top_pixel
70
102
  end
71
-
103
+
72
104
  def caret_position
73
105
  selection.x
74
106
  end
@@ -76,63 +108,85 @@ module Glimmer
76
108
  def selection_count
77
109
  selection.y - selection.x
78
110
  end
79
-
111
+
80
112
  def selection_count=(value)
81
113
  self.selection = Point.new(caret_position, caret_position + value.to_i)
82
- end
83
-
114
+ end
115
+
84
116
  def name=(the_name)
85
- new_path = path.sub(/#{Regexp.escape(@name)}$/, the_name)
117
+ new_path = path.sub(/#{Regexp.escape(@name)}$/, the_name) unless scratchpad?
86
118
  @name = the_name
87
- if ::File.exists?(path)
119
+ if !scratchpad? && ::File.exist?(path)
88
120
  FileUtils.mv(path, new_path)
89
121
  self.path = new_path
90
122
  end
91
123
  end
92
124
 
125
+ def dirty_content
126
+ init_content
127
+ @dirty_content
128
+ end
129
+
93
130
  def dirty_content=(the_content)
94
- @dirty_content = the_content if ::File.exist?(path)
131
+ # TODO set partial dirty content by line(s) for enhanced performance
132
+ @dirty_content = the_content
133
+ old_caret_position = caret_position
134
+ old_top_pixel = top_pixel
95
135
  notify_observers(:content)
136
+ if @formatting_dirty_content_for_writing
137
+ self.caret_position = old_caret_position
138
+ self.selection_count = 0
139
+ self.top_pixel = old_top_pixel
140
+ end
141
+ end
142
+
143
+ def read_dirty_content
144
+ path.empty? ? '' : ::File.read(path)
96
145
  end
97
-
146
+
98
147
  def start_filewatcher
148
+ return if scratchpad?
99
149
  @filewatcher = Filewatcher.new(@path)
100
- @thread = Thread.new(@filewatcher) do |fw|
150
+ @thread = Thread.new(@filewatcher) do |fw|
101
151
  fw.watch do |filename, event|
102
- begin
103
- read_dirty_content = ::File.read(path)
104
- # test read dirty content
105
- read_dirty_content.split("\n")
106
- async_exec do
152
+ async_exec do
153
+ begin
107
154
  self.dirty_content = read_dirty_content if read_dirty_content != dirty_content
155
+ rescue StandardError, Errno::ENOENT
156
+ # in case of a binary file
157
+ stop_filewatcher
108
158
  end
109
- rescue
110
- # no op in case of a binary file
111
159
  end
112
160
  end
113
161
  end
114
162
  end
115
-
163
+
116
164
  def stop_filewatcher
117
165
  @filewatcher&.stop
118
166
  end
119
167
 
120
- def format_dirty_content_for_writing!
121
- new_dirty_content = "#{dirty_content.gsub("\r\n", "\n").gsub("\r", "\n").sub(/\n+\z/, '')}\n"
122
- self.dirty_content = new_dirty_content if new_dirty_content != self.dirty_content
123
- end
124
-
125
168
  def write_dirty_content
126
- return unless ::File.exist?(path)
169
+ # TODO write partial dirty content by line(s) for enhanced performance
170
+ return if scratchpad? || !::File.exist?(path) || !::File.exists?(path) || read_dirty_content == dirty_content
127
171
  format_dirty_content_for_writing!
128
- ::File.write(path, dirty_content) if ::File.exists?(path)
129
- rescue => e
172
+ ::File.write(path, dirty_content)
173
+ rescue StandardError, ArgumentError => e
130
174
  puts "Error in writing dirty content for #{path}"
131
175
  puts e.full_message
132
176
  end
133
-
177
+
178
+ def format_dirty_content_for_writing!
179
+ new_dirty_content = dirty_content.split("\n").map {|line| line.strip.empty? ? line : line.rstrip }.join("\n")
180
+ new_dirty_content = "#{new_dirty_content.gsub("\r\n", "\n").gsub("\r", "\n").sub(/\n+\z/, '')}\n"
181
+ if new_dirty_content != self.dirty_content
182
+ @formatting_dirty_content_for_writing = true
183
+ self.dirty_content = new_dirty_content
184
+ @formatting_dirty_content_for_writing = false
185
+ end
186
+ end
187
+
134
188
  def write_raw_dirty_content
135
- return unless ::File.exist?(path)
189
+ return if scratchpad? || !::File.exist?(path)
136
190
  ::File.write(path, dirty_content) if ::File.exists?(path)
137
191
  rescue => e
138
192
  puts "Error in writing raw dirty content for #{path}"
@@ -143,19 +197,20 @@ module Glimmer
143
197
  current_line.to_s.match(/^(\s+)/).to_a[1].to_s
144
198
  end
145
199
 
146
- def current_line
200
+ def current_line
147
201
  lines[line_number - 1]
148
202
  end
149
203
 
150
204
  def delete!
151
- FileUtils.rm(path)
205
+ FileUtils.rm(path) unless scratchpad?
152
206
  end
153
-
207
+
154
208
  def prefix_new_line!
155
209
  the_lines = lines
156
210
  the_lines[line_number-1...line_number-1] = [current_line_indentation]
157
211
  self.dirty_content = the_lines.join("\n")
158
212
  self.caret_position = caret_position_for_line_index(line_number-1) + current_line_indentation.size
213
+ self.selection_count = 0
159
214
  end
160
215
 
161
216
  def insert_new_line!
@@ -163,8 +218,9 @@ module Glimmer
163
218
  the_lines[line_number...line_number] = [current_line_indentation]
164
219
  self.dirty_content = the_lines.join("\n")
165
220
  self.caret_position = caret_position_for_line_index(line_number) + current_line_indentation.size
221
+ self.selection_count = 0
166
222
  end
167
-
223
+
168
224
  def comment_line!
169
225
  old_lines = lines
170
226
  return if old_lines.size < 1
@@ -190,7 +246,7 @@ module Glimmer
190
246
  delta += 2
191
247
  end
192
248
  end
193
- self.dirty_content = new_lines.join("\n")
249
+ self.dirty_content = new_lines.join("\n")
194
250
  if old_selection_count.to_i > 0
195
251
  self.caret_position = caret_position_for_line_index(old_caret_position_line_index)
196
252
  self.selection_count = (caret_position_for_line_index(old_end_caret_line_index + 1) - self.caret_position)
@@ -198,9 +254,10 @@ module Glimmer
198
254
  new_caret_position = old_caret_position + delta
199
255
  new_caret_position = [new_caret_position, old_caret_position_line_caret_position].max
200
256
  self.caret_position = new_caret_position
257
+ self.selection_count = 0
201
258
  end
202
259
  end
203
-
260
+
204
261
  def indent!
205
262
  new_lines = lines
206
263
  old_lines = lines
@@ -216,16 +273,16 @@ module Glimmer
216
273
  new_lines[the_line_index] = " #{the_line}"
217
274
  end
218
275
  old_caret_position = self.caret_position
219
- self.dirty_content = new_lines.join("\n")
276
+ self.dirty_content = new_lines.join("\n")
220
277
  if old_selection_count.to_i > 0
221
- caret_position_value = caret_position_for_line_index(old_caret_position_line_index)
222
- selection_count_value = (caret_position_for_line_index(old_end_caret_line_index + 1) - caret_position_value)
223
- self.selection = Point.new(caret_position_value, caret_position_value + selection_count_value)
278
+ self.caret_position = caret_position_for_line_index(old_caret_position_line_index)
279
+ self.selection_count = (caret_position_for_line_index(old_end_caret_line_index + 1) - self.caret_position)
224
280
  else
225
281
  self.caret_position = old_caret_position + delta
282
+ self.selection_count = 0
226
283
  end
227
284
  end
228
-
285
+
229
286
  def outdent!
230
287
  new_lines = lines
231
288
  old_lines = lines
@@ -246,18 +303,18 @@ module Glimmer
246
303
  delta = -1
247
304
  end
248
305
  end
249
- self.dirty_content = new_lines.join("\n")
306
+ self.dirty_content = new_lines.join("\n")
250
307
  if old_selection_count.to_i > 0
251
- caret_position_value = caret_position_for_line_index(old_caret_position_line_index)
252
- selection_count_value = (caret_position_for_line_index(old_end_caret_line_index + 1) - caret_position_value)
253
- self.selection = Point.new(caret_position_value, caret_position_value + selection_count_value)
308
+ self.caret_position = caret_position_for_line_index(old_caret_position_line_index)
309
+ self.selection_count = (caret_position_for_line_index(old_end_caret_line_index + 1) - self.caret_position)
254
310
  else
255
311
  new_caret_position = old_caret_position + delta
256
312
  new_caret_position = [new_caret_position, old_caret_position_line_caret_position].max
257
313
  self.caret_position = new_caret_position
314
+ self.selection_count = 0
258
315
  end
259
316
  end
260
-
317
+
261
318
  def kill_line!
262
319
  new_lines = lines
263
320
  return if new_lines.size < 1
@@ -270,7 +327,7 @@ module Glimmer
270
327
  self.caret_position = caret_position_for_line_index(old_line_index) + [line_position, lines[old_line_index].to_s.size].min
271
328
  self.selection_count = 0
272
329
  end
273
-
330
+
274
331
  def duplicate_line!
275
332
  new_lines = lines
276
333
  old_lines = lines
@@ -292,11 +349,12 @@ module Glimmer
292
349
  self.selection_count = (caret_position_for_line_index(old_end_caret_line_index + 1) - self.caret_position)
293
350
  else
294
351
  self.caret_position = old_caret_position + delta
352
+ self.selection_count = 0
295
353
  end
296
354
  end
297
-
355
+
298
356
  def find_next
299
- return if find_text.to_s.empty?
357
+ return if find_text.to_s.empty?
300
358
  all_lines = lines
301
359
  the_line_index = line_index_for_caret_position(caret_position)
302
360
  line_position = line_position_for_caret_position(caret_position)
@@ -312,13 +370,12 @@ module Glimmer
312
370
  if occurrence_index
313
371
  self.caret_position = caret_position_for_line_index(the_index) + start_position + occurrence_index
314
372
  self.selection_count = find_text.to_s.size
315
- self.selection = Point.new(self.caret_position, self.caret_position + self.selection_count)
316
373
  return
317
374
  end
318
375
  end
319
376
  end
320
377
  end
321
-
378
+
322
379
  def find_previous
323
380
  return if find_text.to_s.empty?
324
381
  all_lines = lines
@@ -343,7 +400,7 @@ module Glimmer
343
400
  end
344
401
  end
345
402
  end
346
-
403
+
347
404
  def ensure_find_next
348
405
  return if find_text.to_s.empty? || dirty_content.to_s.strip.size < 1
349
406
  find_next unless found_text?(self.caret_position)
@@ -352,37 +409,45 @@ module Glimmer
352
409
  def found_text?(caret_position)
353
410
  dirty_content[caret_position.to_i, find_text.to_s.size].to_s.downcase == find_text.to_s.downcase
354
411
  end
355
-
412
+
356
413
  def replace_next!
357
414
  return if find_text.to_s.empty? || dirty_content.to_s.strip.size < 1
358
415
  ensure_find_next
359
416
  new_dirty_content = dirty_content
360
417
  new_dirty_content[caret_position, find_text.size] = replace_text.to_s
361
418
  self.dirty_content = new_dirty_content
362
- find_next
419
+ find_next
363
420
  find_next if replace_text.to_s.include?(find_text) && !replace_text.to_s.start_with?(find_text)
364
421
  end
365
-
422
+
366
423
  def page_up
367
424
  self.selection_count = 0
368
425
  self.line_number = [(self.line_number - 15), 1].max
369
426
  end
370
-
427
+
371
428
  def page_down
372
429
  self.selection_count = 0
373
430
  self.line_number = [(self.line_number + 15), lines.size].min
374
431
  end
375
-
432
+
376
433
  def home
377
434
  self.selection_count = 0
378
435
  self.line_number = 1
379
436
  end
380
-
437
+
381
438
  def end
382
439
  self.selection_count = 0
383
440
  self.line_number = lines.size
384
441
  end
385
-
442
+
443
+ def start_of_line
444
+ self.caret_position = caret_position_for_line_index(self.line_number - 1)
445
+ end
446
+
447
+ def end_of_line
448
+ self.caret_position = caret_position_for_line_index(self.line_number) - 1
449
+ end
450
+
386
451
  def move_up!
387
452
  old_lines = lines
388
453
  return if old_lines.size < 2
@@ -401,7 +466,7 @@ module Glimmer
401
466
  self.caret_position = caret_position_for_line_index(new_line_index) + [old_caret_position_line_position, new_lines[new_line_index].size].min
402
467
  self.selection_count = old_selection_count.to_i if old_selection_count.to_i > 0
403
468
  end
404
-
469
+
405
470
  def move_down!
406
471
  old_lines = lines
407
472
  return if old_lines.size < 2
@@ -420,25 +485,38 @@ module Glimmer
420
485
  self.caret_position = caret_position_for_line_index(new_line_index) + [old_caret_position_line_position, new_lines[new_line_index].size].min
421
486
  self.selection_count = old_selection_count.to_i if old_selection_count.to_i > 0
422
487
  end
423
-
488
+
489
+ def run
490
+ begin
491
+ if scratchpad?
492
+ eval content
493
+ else
494
+ write_dirty_content
495
+ load path
496
+ end
497
+ rescue SyntaxError, StandardError => e
498
+ puts e.full_message
499
+ end
500
+ end
501
+
424
502
  def lines
425
503
  dirty_content.split("\n")
426
504
  end
427
-
505
+
428
506
  def line_for_caret_position(caret_position)
429
507
  lines[line_index_for_caret_position(caret_position.to_i)]
430
508
  end
431
-
509
+
432
510
  def line_index_for_caret_position(caret_position)
433
511
  dirty_content[0...caret_position.to_i].count("\n")
434
512
  end
435
-
513
+
436
514
  def caret_position_for_line_index(line_index)
437
515
  cp = lines[0...line_index].join("\n").size
438
516
  cp += 1 if line_index > 0
439
517
  cp
440
518
  end
441
-
519
+
442
520
  def caret_position_for_caret_position_start_of_line(caret_position)
443
521
  caret_position_for_line_index(line_index_for_caret_position(caret_position))
444
522
  end
@@ -449,23 +527,23 @@ module Glimmer
449
527
  caret_position = caret_position.to_i
450
528
  caret_position - caret_position_for_caret_position_start_of_line(caret_position)
451
529
  end
452
-
530
+
453
531
  def line_caret_positions_for_selection(caret_position, selection_count)
454
532
  line_indices = line_indices_for_selection(caret_position, selection_count)
455
533
  line_caret_positions = line_indices.map { |line_index| caret_position_for_line_index(line_index) }.to_a
456
534
  end
457
-
535
+
458
536
  def end_caret_position_line_index(caret_position, selection_count)
459
537
  end_caret_position = caret_position + selection_count.to_i
460
538
  end_caret_position -= 1 if dirty_content[end_caret_position - 1] == "\n"
461
539
  end_line_index = line_index_for_caret_position(end_caret_position)
462
540
  end
463
-
541
+
464
542
  def lines_for_selection(caret_position, selection_count)
465
543
  line_indices = line_indices_for_selection(caret_position, selection_count)
466
544
  lines[line_indices.first..line_indices.last]
467
545
  end
468
-
546
+
469
547
  def line_indices_for_selection(caret_position, selection_count)
470
548
  start_line_index = line_index_for_caret_position(caret_position)
471
549
  if selection_count.to_i > 0
@@ -475,22 +553,22 @@ module Glimmer
475
553
  end
476
554
  (start_line_index..end_line_index).to_a
477
555
  end
478
-
556
+
479
557
  def children
480
558
  []
481
559
  end
482
-
560
+
483
561
  def to_s
484
562
  path
485
563
  end
486
-
564
+
487
565
  def eql?(other)
488
566
  self.path.eql?(other&.path)
489
567
  end
490
-
568
+
491
569
  def hash
492
570
  self.path.hash
493
571
  end
494
- end
572
+ end
495
573
  end
496
574
  end