vimamsa 0.1.22 → 0.1.24

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.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.dockerignore +32 -0
  3. data/Dockerfile +45 -0
  4. data/README.md +2 -2
  5. data/custom_example.rb +38 -9
  6. data/docker_cmd.sh +7 -0
  7. data/exe/run_tests.rb +23 -0
  8. data/img/screenshot1.png +0 -0
  9. data/img/screenshot2.png +0 -0
  10. data/lib/vimamsa/actions.rb +8 -0
  11. data/lib/vimamsa/buffer.rb +165 -53
  12. data/lib/vimamsa/buffer_changetext.rb +68 -14
  13. data/lib/vimamsa/buffer_cursor.rb +9 -3
  14. data/lib/vimamsa/buffer_list.rb +14 -28
  15. data/lib/vimamsa/buffer_manager.rb +1 -1
  16. data/lib/vimamsa/conf.rb +33 -1
  17. data/lib/vimamsa/diff_buffer.rb +185 -0
  18. data/lib/vimamsa/editor.rb +149 -80
  19. data/lib/vimamsa/file_finder.rb +6 -2
  20. data/lib/vimamsa/gui.rb +330 -135
  21. data/lib/vimamsa/gui_dialog.rb +2 -0
  22. data/lib/vimamsa/gui_file_panel.rb +94 -0
  23. data/lib/vimamsa/gui_form_generator.rb +4 -2
  24. data/lib/vimamsa/gui_func_panel.rb +127 -0
  25. data/lib/vimamsa/gui_image.rb +2 -4
  26. data/lib/vimamsa/gui_menu.rb +54 -1
  27. data/lib/vimamsa/gui_select_window.rb +18 -6
  28. data/lib/vimamsa/gui_settings.rb +486 -0
  29. data/lib/vimamsa/gui_sourceview.rb +196 -8
  30. data/lib/vimamsa/gui_text.rb +0 -22
  31. data/lib/vimamsa/hyper_plain_text.rb +1 -0
  32. data/lib/vimamsa/key_actions.rb +54 -31
  33. data/lib/vimamsa/key_binding_tree.rb +154 -8
  34. data/lib/vimamsa/key_bindings_vimlike.rb +48 -35
  35. data/lib/vimamsa/langservp.rb +161 -7
  36. data/lib/vimamsa/macro.rb +54 -7
  37. data/lib/vimamsa/main.rb +1 -0
  38. data/lib/vimamsa/rbvma.rb +5 -0
  39. data/lib/vimamsa/string_util.rb +56 -0
  40. data/lib/vimamsa/test_framework.rb +137 -0
  41. data/lib/vimamsa/util.rb +3 -36
  42. data/lib/vimamsa/version.rb +1 -1
  43. data/modules/calculator/calculator.rb +318 -0
  44. data/modules/calculator/calculator_info.rb +3 -0
  45. data/modules/terminal/terminal.rb +140 -0
  46. data/modules/terminal/terminal_info.rb +3 -0
  47. data/run_tests.rb +89 -0
  48. data/styles/dark.xml +1 -1
  49. data/styles/molokai_edit.xml +2 -2
  50. data/tests/key_bindings.rb +2 -0
  51. data/tests/test_basic_editing.rb +86 -0
  52. data/tests/test_copy_paste.rb +88 -0
  53. data/tests/test_key_bindings.rb +152 -0
  54. data/tests/test_module_interface.rb +98 -0
  55. data/tests/test_undo.rb +201 -0
  56. data/vimamsa.gemspec +6 -5
  57. metadata +52 -14
@@ -22,7 +22,14 @@ class Buffer < String
22
22
  else
23
23
  @deltas << delta
24
24
  end
25
- @edit_history << delta
25
+
26
+ unless @macro_group_active
27
+ if @last_delta_time && (Time.now - @last_delta_time) > cnf.undo.group_threshold!
28
+ new_undo_group
29
+ end
30
+ end
31
+ @current_group << delta
32
+ @last_delta_time = Time.now
26
33
  if self[-1] != "\n"
27
34
  add_delta([self.size, INSERT, 1, "\n"], true)
28
35
  end
@@ -81,19 +88,19 @@ class Buffer < String
81
88
  @clipboard_paste_running = true
82
89
  Thread.new {
83
90
  text = `xclip -selection c -o`
84
- paste_finish(text, at, register)
91
+ GLib::Idle.add { paste_finish(text, at, register); false }
85
92
  }
86
93
  return nil
87
94
  end
88
95
 
89
96
  # Start asynchronous read of system clipboard
90
- def paste_start(at, register)
97
+ def paste_start(at, register, overwrite: false)
91
98
  @clipboard_paste_running = true
92
99
 
93
- if true or running_wayland? and !GLib::Version::or_later?(2, 79, 0)
94
- return paste_start_xclip(at, register)
95
- end
96
-
100
+ # if true or running_wayland? and !GLib::Version::or_later?(2, 79, 0)
101
+ # return paste_start_xclip(at, register)
102
+ # end
103
+
97
104
  clipboard = vma.gui.window.display.clipboard
98
105
  clipboard.read_text_async do |_clipboard, result|
99
106
  begin
@@ -101,13 +108,13 @@ class Buffer < String
101
108
  rescue Gio::IOError::NotSupported
102
109
  debug Gio::IOError::NotSupported
103
110
  else
104
- paste_finish(text, at, register)
111
+ paste_finish(text, at, register, overwrite: overwrite)
105
112
  end
106
113
  end
107
114
  return nil
108
115
  end
109
116
 
110
- def paste_finish(text, at, register)
117
+ def paste_finish(text, at, register, overwrite: false)
111
118
  debug "PASTE: #{text}"
112
119
 
113
120
  # If we did not put this text to clipboard
@@ -121,9 +128,25 @@ class Buffer < String
121
128
 
122
129
  return if text == ""
123
130
 
124
- if @paste_lines
131
+ cursor_at_start = cnf.paste.cursor_at_start!
132
+
133
+ if overwrite && !@paste_lines
134
+ # Delete as many chars forward as we are about to insert, stopping before newline
135
+ line_end = current_line_range.last - 1 # position of last char before \n
136
+ n = [text.size, [line_end - @pos + 1, 0].max].min
137
+ add_delta([@pos, DELETE, n], true) if n > 0
138
+ insert_txt_at(text, @pos)
139
+ set_pos(cursor_at_start ? @pos : @pos + text.size - 1)
140
+ elsif @paste_lines
125
141
  debug "PASTE LINES"
126
- put_to_new_next_line(text)
142
+ if at == BEFORE
143
+ l = current_line_range()
144
+ insert_txt_at(text, l.begin)
145
+ set_pos(l.begin)
146
+ else
147
+ put_to_new_next_line(text)
148
+ set_pos(@pos) if cursor_at_start
149
+ end
127
150
  else
128
151
  debug "PASTE !LINES"
129
152
  if at_end_of_buffer? or at_end_of_line? or at == BEFORE
@@ -132,12 +155,23 @@ class Buffer < String
132
155
  pos = @pos + 1
133
156
  end
134
157
  insert_txt_at(text, pos)
135
- set_pos(pos + text.size)
158
+ set_pos(cursor_at_start ? pos : pos + text.size)
136
159
  end
137
160
  set_pos(@pos)
161
+ vma.buf.view.after_action # redraw
138
162
  @clipboard_paste_running = false
139
163
  end
140
164
 
165
+ def paste_over(at = AFTER, register = nil)
166
+ if vma.macro.running_macro
167
+ text = vma.clipboard.get()
168
+ paste_finish(text, at, register, overwrite: true)
169
+ else
170
+ paste_start(at, register, overwrite: true)
171
+ end
172
+ return true
173
+ end
174
+
141
175
  def paste(at = AFTER, register = nil)
142
176
  # Macro's don't work with asynchronous call using GTK
143
177
  # TODO: implement as synchronous?
@@ -152,6 +186,22 @@ class Buffer < String
152
186
  return true
153
187
  end
154
188
 
189
+ def increment_current_word()
190
+ debug "increment_current_word", 2
191
+ p = @pos
192
+ return if !is_legal_pos(p)
193
+ (word, range) = get_word_in_pos(p, boundary: :word2)
194
+ if word.match(/(true|false)/i)
195
+ rep = flip_true_false(word)
196
+ else
197
+ num = word.to_i
198
+ num += 1
199
+ rep = num.to_s
200
+ end
201
+ debug [word, range].to_s, 2
202
+ replace_range(range, rep)
203
+ end
204
+
155
205
  def complete_current_word(rep)
156
206
  debug "complete_current_word", 2
157
207
  p = @pos - 1
@@ -189,7 +239,11 @@ class Buffer < String
189
239
  (startpos, endpos) = get_visual_mode_range2
190
240
  delete_range(startpos, endpos, x)
191
241
  @pos = [@pos, @selection_start].min
192
- end_visual_mode
242
+ if vma.kbd.get_mode == :visual
243
+ end_visual_mode
244
+ else
245
+ end_selection
246
+ end
193
247
  #return
194
248
 
195
249
  # Delete current char
@@ -217,7 +271,7 @@ class Buffer < String
217
271
 
218
272
  def delete_range(startpos, endpos, x = nil)
219
273
  s = self[startpos..endpos]
220
- if startpos == endpos or s == ""
274
+ if startpos > endpos or s == ""
221
275
  return
222
276
  end
223
277
  if x == :append
@@ -1,6 +1,5 @@
1
1
  # Buffer operations related to cursor position, e.g. moving the cursor (backward, forward, next line etc.)
2
2
  class Buffer < String
3
-
4
3
  def line(lpos)
5
4
  if @line_ends.size == 0
6
5
  return self
@@ -30,15 +29,22 @@ class Buffer < String
30
29
  def refresh_cursor
31
30
  self.view.set_cursor_pos(@pos)
32
31
  end
33
-
32
+
34
33
  def set_pos(new_pos)
34
+ # If user interacts with the buffer, we consider that the buffer has been accessed
35
+ # And navigation has ended
36
+ if new_pos != @pos
37
+ update_access_time
38
+ bufs.reset_navigation
39
+ end
35
40
  if new_pos >= self.size
36
41
  @pos = self.size - 1 # TODO:??right side of last char
37
42
  elsif new_pos >= 0
38
43
  @pos = new_pos
39
44
  end
45
+
40
46
  self.view.set_cursor_pos(pos)
41
- # gui_set_cursor_pos(@id, @pos)
47
+ # gui_set_cursor_pos(@id, @pos)
42
48
  calculate_line_and_column_pos
43
49
 
44
50
  check_if_modified_outside
@@ -20,12 +20,11 @@ def load_buffer_list()
20
20
  end
21
21
 
22
22
  class BufferList
23
- attr_reader :current_buf, :last_dir, :last_file, :buffer_history
23
+ attr_reader :current_buf, :last_dir, :last_file
24
24
  attr_accessor :list
25
25
 
26
26
  def initialize()
27
27
  @last_dir = File.expand_path(".")
28
- @buffer_history = []
29
28
  super
30
29
  @current_buf = 0
31
30
  @list = []
@@ -42,25 +41,14 @@ class BufferList
42
41
  vma.gui.set_current_buffer(vma.buf.id) #TODO: handle elswhere?
43
42
  # vma.buf.view.set_cursor_pos(vma.buf.pos) #TODO: handle elswhere?
44
43
  update_last_dir(_buf)
44
+ vma.gui.file_panel_refresh
45
45
  end
46
46
 
47
47
  def add(_buf)
48
- @buffer_history << _buf.id
49
- # @navigation_idx = _buf.id #TODO:??
50
48
  @list << _buf
51
49
  @h[_buf.id] = _buf
52
50
  end
53
51
 
54
- #NOTE: unused. enable?
55
- # def switch()
56
- # debug "SWITCH BUF. bufsize:#{self.size}, curbuf: #{@current_buf}"
57
- # @current_buf += 1
58
- # @current_buf = 0 if @current_buf >= self.size
59
- # m = method("switch")
60
- # set_last_command({ method: m, params: [] })
61
- # set_current_buffer(@current_buf)
62
- # end
63
-
64
52
  def slist
65
53
  # TODO: implement using heap/priorityque
66
54
  @list.sort_by! { |x| x.access_time }
@@ -72,6 +60,13 @@ class BufferList
72
60
  end
73
61
  end
74
62
 
63
+ def print_by_access_time
64
+ slist.reverse.each_with_index do |b, i|
65
+ name = b.fname || "(untitled)"
66
+ puts "#{i + 1}. #{b.access_time.strftime('%H:%M:%S')} #{name}"
67
+ end
68
+ end
69
+
75
70
  def get_last_visited_id
76
71
  last_buf = nil
77
72
  for i in 0..(slist.size - 1)
@@ -83,9 +78,6 @@ class BufferList
83
78
 
84
79
  def switch_to_last_buf()
85
80
  debug "SWITCH TO LAST BUF:"
86
- # debug @buffer_history
87
- # last_buf = @buffer_history[-2]
88
-
89
81
  last_buf = slist[-2]
90
82
  if !last_buf.nil?
91
83
  set_current_buffer(last_buf.id)
@@ -107,17 +99,6 @@ class BufferList
107
99
  return @h[id]
108
100
  end
109
101
 
110
- def add_buf_to_history(buf_idx)
111
- if @list.include?(buf_idx)
112
- @buffer_history << @buf_idx
113
- @navigation_idx = 0
114
- # compact_buf_history()
115
- else
116
- debug "buffer_list, no such id:#{buf_idx}"
117
- return
118
- end
119
- end
120
-
121
102
  def add_current_buf_to_history()
122
103
  @h[@current_buf].update_access_time
123
104
  end
@@ -143,7 +124,10 @@ class BufferList
143
124
 
144
125
  bu.set_active # TODO
145
126
  bu.update_access_time if update_history
127
+ reset_navigation if update_history
146
128
  vma.gui.set_current_buffer(idx)
129
+ vma.gui.file_panel_refresh
130
+ vma.gui.func_panel_refresh
147
131
 
148
132
  #TODO: delete?
149
133
  # if !vma.buf.mode_stack.nil? and vma.kbd.get_scope != :editor #TODO
@@ -255,6 +239,8 @@ class BufferList
255
239
 
256
240
  @list.delete(@h[idx])
257
241
  @h.delete(idx)
242
+ gui_close_buffer(idx)
243
+ vma.gui.file_panel_refresh
258
244
 
259
245
  if auto_open
260
246
  @current_buf = get_last_non_active_buffer
@@ -60,7 +60,7 @@ class BufferManager
60
60
  @@cur = self
61
61
  @header = []
62
62
  @header << "Current buffers:"
63
- @header << "keys: <enter> to select, <c> to close buffer, <x> exit"
63
+ @header << "keys: <enter> (or <double click>) to select, <c> to close buffer, <x> exit"
64
64
  @header << "=" * 40
65
65
 
66
66
  s = ""
data/lib/vimamsa/conf.rb CHANGED
@@ -13,7 +13,7 @@ $confh = Hash.new { |h, k| h[k] = Hash.new(&h.default_proc) }
13
13
  # => $confh = {:foo=>{:bar=>{:baz=>3}}}
14
14
  def set(_id, val)
15
15
  a = $confh
16
- id = _id.to_a
16
+ id = _id.to_a.dup
17
17
  last = id.pop
18
18
  for x in id
19
19
  a = a[x]
@@ -103,6 +103,28 @@ def cnf()
103
103
  return $vimamsa_conf
104
104
  end
105
105
 
106
+ # Read a config value by key path array.
107
+ # Uses the cnf accessor chain so key types match cnf.xxx! reads.
108
+ # Example:
109
+ # cnf_get([:tab, :width]) # => 2
110
+ # cnf_get([:style_scheme]) # => "molokai_edit"
111
+ def cnf_get(key)
112
+ obj = cnf
113
+ key[0..-2].each { |k| obj = obj.public_send(k) }
114
+ obj.public_send(:"#{key.last}!")
115
+ end
116
+
117
+ # Write a config value by key path array.
118
+ # Uses the cnf accessor chain so the value is readable via cnf.xxx!
119
+ # Example:
120
+ # cnf_set([:tab, :width], 4)
121
+ # cnf_set([:style_scheme], "soviet_cockpit")
122
+ def cnf_set(key, val)
123
+ obj = cnf
124
+ key[0..-2].each { |k| obj = obj.public_send(k) }
125
+ obj.public_send(:"#{key.last}=", val)
126
+ end
127
+
106
128
  cnf.indent_based_on_last_line = true
107
129
  cnf.extensions_to_open = [".txt", ".h", ".c", ".cpp", ".hpp", ".rb", ".inc", ".php", ".sh", ".m", ".gd", ".js", ".py"]
108
130
  cnf.default_search_extensions = ["txt", "rb"]
@@ -112,6 +134,8 @@ cnf.lsp.enabled = false
112
134
  cnf.fexp.experimental = false
113
135
  cnf.experimental = false
114
136
 
137
+ cnf.kbd.show_prev_action = true
138
+
115
139
  cnf.tab.width = 2
116
140
  cnf.tab.to_spaces_default = false
117
141
  cnf.tab.to_spaces_languages = ["c", "java", "ruby", "hyperplaintext", "php"]
@@ -129,4 +153,12 @@ cnf.startup_file=false
129
153
 
130
154
  cnf.macro.animation_delay = 0.02
131
155
 
156
+ cnf.undo.group_threshold = 1.8 # seconds of inactivity before starting a new undo group
157
+
158
+ cnf.paste.cursor_at_start = false
159
+
160
+ cnf.style_scheme = "molokai_edit"
161
+ cnf.color_contrast = 1.0
162
+ cnf.color_brightness = 0.0
163
+
132
164
 
@@ -0,0 +1,185 @@
1
+ # Map a "line number in a unified diff output" to the corresponding
2
+ # line in the new/changed file (the + side), together with the file it belongs to.
3
+ #
4
+ # Handles multi-file diffs: each --- / +++ pair sets the active file; each @@
5
+ # hunk header resets the line counters. Walking hunk lines:
6
+ # ' ' => old++, new++
7
+ # '-' => old++
8
+ # '+' => new++
9
+ #
10
+ # Returns [new_path, old_path, line_no] or nil for deleted / unmappable lines.
11
+ #
12
+ class DiffLineMapper
13
+ HUNK_RE = /^@@\s+-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s+@@/
14
+
15
+ def initialize(diff_text)
16
+ @lines = diff_text.lines
17
+ end
18
+
19
+ # Given a 1-based line number in the diff output, return:
20
+ # - [new_path, old_path, Integer]: raw +++ path, raw --- path, 1-based new-file line
21
+ # - nil: if the diff line is a deletion ('-') or cannot be mapped
22
+ def new_line_for_diff_lineno(diff_lineno)
23
+ raise ArgumentError, "diff line number must be >= 1" if diff_lineno.to_i < 1
24
+ idx = diff_lineno.to_i - 1
25
+ return nil if idx >= @lines.length
26
+
27
+ old_path = nil
28
+ new_path = nil
29
+ old = nil
30
+ new_ = nil
31
+ in_hunk = false
32
+
33
+ @lines.each_with_index do |line, i|
34
+ # File headers reset hunk state and record current file paths.
35
+ # These appear outside hunks, but guard against malformed diffs too.
36
+ if line.start_with?('--- ')
37
+ old_path = line[4..].split("\t").first.strip
38
+ in_hunk = false
39
+ old = new_ = nil
40
+ next
41
+ end
42
+
43
+ if line.start_with?('+++ ')
44
+ new_path = line[4..].split("\t").first.strip
45
+ in_hunk = false
46
+ old = new_ = nil
47
+ next
48
+ end
49
+
50
+ if (m = line.match(HUNK_RE))
51
+ old = m[1].to_i
52
+ new_ = m[3].to_i
53
+ in_hunk = true
54
+ next
55
+ end
56
+
57
+ next unless in_hunk
58
+
59
+ if i == idx
60
+ return nil unless new_
61
+ case line.getbyte(0)
62
+ when '+'.ord then return [new_path, old_path, new_]
63
+ when ' '.ord then return [new_path, old_path, new_]
64
+ when '-'.ord then return nil
65
+ else return nil
66
+ end
67
+ end
68
+
69
+ next unless old && new_
70
+
71
+ case line.getbyte(0)
72
+ when ' '.ord then old += 1; new_ += 1
73
+ when '-'.ord then old += 1
74
+ when '+'.ord then new_ += 1
75
+ end
76
+ end
77
+
78
+ nil
79
+ end
80
+ end
81
+
82
+ def diff_buffer_init()
83
+ return if @diff_buffer_init_done
84
+ @diff_buffer_init_done = true
85
+ vma.kbd.add_minor_mode("diffview", :diffview, :command)
86
+ bindkey "diffview enter", :diff_buffer_jump_to_source
87
+ end
88
+
89
+ def diff_buffer()
90
+ return if !if_cmd_exists("diff")
91
+ diff_buffer_init
92
+ orig_path = vma.buf.fname
93
+ infile = Tempfile.new("in")
94
+ infile.write(vma.buf.to_s)
95
+ infile.flush
96
+ bufstr = run_cmd("diff -uw '#{orig_path}' #{infile.path}")
97
+ infile.close; infile.unlink
98
+ create_new_file(nil, bufstr)
99
+ gui_set_file_lang(vma.buf.id, "diff")
100
+ vma.kbd.set_mode(:diffview)
101
+ end
102
+
103
+ def diff_buffer_jump_to_source()
104
+ mapper = DiffLineMapper.new(vma.buf.to_s)
105
+ cur_lpos = vma.buf.lpos + 1
106
+ result = mapper.new_line_for_diff_lineno(cur_lpos)
107
+
108
+ if result.nil?
109
+ message("No source line for this position")
110
+ return
111
+ end
112
+
113
+ new_path, old_path, to_line = result
114
+ orig_path = resolve_diff_path(new_path, old_path)
115
+
116
+ if orig_path.nil? || !File.exist?(orig_path)
117
+ message("Could not find file: #{new_path || old_path}")
118
+ return
119
+ end
120
+
121
+ jump_to_file(orig_path, to_line)
122
+ center_on_current_line
123
+ end
124
+
125
+ # Resolve a +++ / --- path pair to an absolute filesystem path.
126
+ # Prefers new_path (the post-change file); falls back to old_path
127
+ # when new_path is /dev/null or a temp file that no longer exists.
128
+ def resolve_diff_path(new_path, old_path)
129
+ git_root = `git rev-parse --show-toplevel 2>/dev/null`.strip
130
+
131
+ expand = lambda do |path|
132
+ return nil if path.nil? || path == "/dev/null"
133
+ # git diff uses "a/" / "b/" prefixes for old/new sides
134
+ if path.start_with?("b/") || path.start_with?("a/")
135
+ rel = path[2..]
136
+ return git_root.empty? ? File.expand_path(rel) : File.join(git_root, rel)
137
+ end
138
+ path.start_with?("/") ? path : File.expand_path(path)
139
+ end
140
+
141
+ path = expand.call(new_path)
142
+ return path if path && File.exist?(path)
143
+
144
+ expand.call(old_path)
145
+ end
146
+
147
+ def git_diff_w()
148
+ return if !if_cmd_exists("git")
149
+ diff_buffer_init
150
+
151
+ dir = vma.buf.fname ? File.dirname(vma.buf.fname) : Dir.pwd
152
+ git_root = `git -C #{Shellwords.escape(dir)} rev-parse --show-toplevel 2>/dev/null`.strip
153
+ if git_root.empty?
154
+ message("Not a git repository")
155
+ return
156
+ end
157
+
158
+ bufstr = run_cmd("git -C #{Shellwords.escape(git_root)} diff -w")
159
+ if bufstr.strip.empty?
160
+ message("git diff -w: no changes")
161
+ return
162
+ end
163
+
164
+ create_new_file(nil, bufstr)
165
+ gui_set_file_lang(vma.buf.id, "diff")
166
+ vma.kbd.set_mode(:diffview)
167
+ end
168
+
169
+ def git_diff_buffer()
170
+ return if !if_cmd_exists("git")
171
+ diff_buffer_init
172
+ fname = vma.buf.fname
173
+ if fname.nil?
174
+ message("Buffer has no file")
175
+ return
176
+ end
177
+ bufstr = run_cmd("git diff -w -- #{Shellwords.escape(fname)}")
178
+ if bufstr.strip.empty?
179
+ message("git diff: no changes")
180
+ return
181
+ end
182
+ create_new_file(nil, bufstr)
183
+ gui_set_file_lang(vma.buf.id, "diff")
184
+ vma.kbd.set_mode(:diffview)
185
+ end