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
data/lib/vimamsa/macro.rb CHANGED
@@ -26,22 +26,51 @@ class Macro
26
26
  attr_reader :running_macro
27
27
  attr_accessor :recorded_macros, :recording, :named_macros, :last_macro
28
28
 
29
+ NAMED_MACROS_FILE = "named_macros.json"
30
+
29
31
  def initialize()
30
32
  @recording = false
31
- # @recorded_macros = {}
32
33
  @current_recording = []
33
34
  @current_name = nil
34
35
  @last_macro = "a"
35
36
  @running_macro = false
36
37
 
37
- #TODO:
38
38
  @recorded_macros = vma.marshal_load("macros", {})
39
- @named_macros = vma.marshal_load("named_macros", {})
39
+ @named_macros = load_named_macros
40
40
  vma.hook.register(:shutdown, self.method("save"))
41
41
  end
42
42
 
43
+ def named_macros_path
44
+ get_dot_path(NAMED_MACROS_FILE)
45
+ end
46
+
47
+ # Save named macros as JSON immediately — called automatically after name_macro.
48
+ def save_named_macros
49
+ require "json"
50
+ File.write(named_macros_path, JSON.pretty_generate(@named_macros))
51
+ rescue => e
52
+ error("Failed to save named macros: #{e}")
53
+ end
54
+
55
+ # Load named macros from JSON. Falls back to Marshal data from older versions.
56
+ def load_named_macros
57
+ require "json"
58
+ path = named_macros_path
59
+ if File.exist?(path)
60
+ data = JSON.parse(File.read(path))
61
+ # JSON keys are always strings; action lists are arrays of strings — correct types
62
+ return data
63
+ end
64
+ # Fallback: migrate from old Marshal-based storage
65
+ vma.marshal_load("named_macros", {})
66
+ rescue => e
67
+ error("Failed to load named macros: #{e}")
68
+ {}
69
+ end
70
+
43
71
  def save()
44
72
  vma.marshal_save("macros", @recorded_macros)
73
+ # named_macros are kept current via save_named_macros; save a Marshal copy as backup
45
74
  vma.marshal_save("named_macros", @named_macros)
46
75
  end
47
76
 
@@ -56,17 +85,31 @@ class Macro
56
85
  $macro_search_list = l
57
86
  $select_keys = ["h", "l", "f", "d", "s", "a", "g", "z"]
58
87
 
88
+ delete_cb = proc { |name, refresh|
89
+ Gui.confirm("Delete macro '#{name}'?", proc {
90
+ vma.macro.delete_named_macro(name)
91
+ refresh.call
92
+ })
93
+ }
94
+
59
95
  gui_select_update_window(l, $select_keys.collect { |x| x.upcase },
60
96
  "gui_find_macro_select_callback",
61
- "gui_find_macro_update_callback")
97
+ "gui_find_macro_update_callback",
98
+ { delete_callback: delete_cb })
99
+ end
100
+
101
+ def delete_named_macro(name)
102
+ @named_macros.delete(name)
103
+ save_named_macros
104
+ message("Macro '#{name}' deleted")
62
105
  end
63
106
 
64
107
  def name_macro(name, id = nil)
65
108
  debug "NAME MACRO #{name}"
66
- if id.nil?
67
- id = @last_macro
68
- end
109
+ id = @last_macro if id.nil?
69
110
  @named_macros[name] = @recorded_macros[id].clone
111
+ save_named_macros
112
+ message("Macro '#{name}' saved")
70
113
  end
71
114
 
72
115
  def start_recording(name)
@@ -122,6 +165,8 @@ class Macro
122
165
  isok = true
123
166
  # if acts.kind_of?(Array) and acts.any?
124
167
  if acts.any?
168
+ vma.buf&.new_undo_group
169
+ vma.buf&.instance_variable_set(:@macro_group_active, true)
125
170
  @running_macro = true
126
171
  # TODO:needed?
127
172
  # set_last_command({ method: vma.macro.method("run_macro"), params: [name] })
@@ -138,6 +183,8 @@ class Macro
138
183
  end
139
184
  end
140
185
  @running_macro = false
186
+ vma.buf&.instance_variable_set(:@macro_group_active, false)
187
+ vma.buf&.new_undo_group
141
188
  buf.set_pos(buf.pos)
142
189
  # TODO: Should be a better way to trigger this. Sometimes need to wait for GTK to process things before updating the cursor.
143
190
  run_as_idle proc { vma.buf.refresh_cursor; vma.buf.refresh_cursor }, delay: 0.15
data/lib/vimamsa/main.rb CHANGED
@@ -45,6 +45,7 @@ def buflist()
45
45
  end
46
46
 
47
47
  require "vimamsa/editor.rb"
48
+ require "vimamsa/diff_buffer"
48
49
 
49
50
  $vma = Editor.new
50
51
  def vma()
data/lib/vimamsa/rbvma.rb CHANGED
@@ -16,6 +16,7 @@ require "cgi"
16
16
  require "uri"
17
17
  require "vimamsa/conf"
18
18
  require "vimamsa/util"
19
+ require "vimamsa/string_util"
19
20
  # exit!
20
21
  require "vimamsa/main"
21
22
  require "vimamsa/terminal"
@@ -30,6 +31,9 @@ require "vimamsa/gui_form_generator"
30
31
  require "vimamsa/gui_text"
31
32
  require "vimamsa/gui_menu"
32
33
  require "vimamsa/gui_dialog"
34
+ require "vimamsa/gui_settings"
35
+ require "vimamsa/gui_file_panel"
36
+ require "vimamsa/gui_func_panel"
33
37
  require "vimamsa/gui_select_window"
34
38
  require "vimamsa/gui_sourceview"
35
39
  require "vimamsa/gui_sourceview_autocomplete"
@@ -46,6 +50,7 @@ require "vimamsa/buffer_manager"
46
50
  require "vimamsa/constants"
47
51
  require "vimamsa/debug"
48
52
  require "vimamsa/tests"
53
+ require "vimamsa/test_framework"
49
54
  require "vimamsa/easy_jump"
50
55
  require "vimamsa/encrypt"
51
56
  require "vimamsa/file_finder"
@@ -0,0 +1,56 @@
1
+ def flip_true_false(str)
2
+ str.gsub(/\b(true|false)\b/i) do |match|
3
+ if match.match?(/\Atrue\z/i)
4
+ replacement = "false"
5
+ else
6
+ replacement = "true"
7
+ end
8
+
9
+ # preserve casing style
10
+ if match == match.upcase
11
+ replacement.upcase
12
+ elsif match[0] == match[0].upcase
13
+ replacement.capitalize
14
+ else
15
+ replacement.downcase
16
+ end
17
+ end
18
+ end
19
+
20
+ def to_camel_case(str)
21
+ words = str.split(/\W+/) # Split the input string into words
22
+ camel_case_words = words.map.with_index do |word, index|
23
+ index == 0 ? word.downcase : word.capitalize
24
+ end
25
+ camel_case_words.join
26
+ end
27
+
28
+ # Get all indexes for start of matching regexp
29
+ def scan_indexes(txt, regex)
30
+ # indexes = txt.enum_for(:scan, regex).map { Regexp.last_match.begin(0) + 1 }
31
+ indexes = txt.enum_for(:scan, regex).map { Regexp.last_match.begin(0) }
32
+ return indexes
33
+ end
34
+
35
+ def is_path(s)
36
+ m = s.match(/(~[a-z]*)?\/.*\//)
37
+ if m != nil
38
+ return true
39
+ end
40
+ return false
41
+ end
42
+
43
+
44
+ def sanitize_input(str)
45
+ if str.encoding != Encoding::UTF_8
46
+ str = text.encode(Encoding::UTF_8)
47
+ end
48
+ str.gsub!(/\r\n/, "\n")
49
+ return str
50
+ end
51
+
52
+ def is_url(s)
53
+ return s.match(/(https?|file):\/\/.*/) != nil
54
+ end
55
+
56
+
@@ -0,0 +1,137 @@
1
+ # VmaTest — integration test base class
2
+ #
3
+ # Tests run inside the live GTK app (headless or normal) after vma.start.
4
+ # Each test method gets a fresh empty buffer.
5
+ #
6
+ # Two ways to drive the editor:
7
+ # act(:action_name) — execute a registered action directly
8
+ # act("buf.insert_txt(…)") — eval a string action
9
+ # keys("i h e l l o esc") — simulate a space-separated key sequence
10
+ #
11
+ # Assertions raise VmaTestFailure on mismatch (caught by the runner).
12
+ #
13
+ # Usage:
14
+ # Run all tests: run_vma_tests
15
+ # Run one class: run_vma_tests(MyTests)
16
+
17
+ class VmaTestFailure < StandardError; end
18
+
19
+ class VmaTest
20
+ attr_reader :failures, :passes
21
+
22
+ # Subclasses define test_* methods.
23
+ # Each receives a fresh buffer and the kbd in :command mode.
24
+ def run_all
25
+ @passes = 0
26
+ @failures = []
27
+ test_methods = self.class.instance_methods(false)
28
+ .select { |m| m.to_s.start_with?("test_") }
29
+ .sort
30
+
31
+ test_methods.each do |m|
32
+ _setup_test
33
+ begin
34
+ send(m)
35
+ @passes += 1
36
+ puts " PASS #{self.class}##{m}"
37
+ rescue VmaTestFailure => e
38
+ @failures << "#{self.class}##{m}: #{e.message}"
39
+ puts " FAIL #{self.class}##{m}: #{e.message}"
40
+ rescue => e
41
+ @failures << "#{self.class}##{m}: #{e.class}: #{e.message}"
42
+ puts " ERROR #{self.class}##{m}: #{e.class}: #{e.message}"
43
+ puts e.backtrace.first(5).join("\n")
44
+ end
45
+ end
46
+ end
47
+
48
+ # ── Actions ──────────────────────────────────────────────────────────────
49
+
50
+ # Execute one action (symbol, string, or proc)
51
+ def act(action)
52
+ exec_action(action)
53
+ drain_idle
54
+ end
55
+
56
+ # Simulate a space-separated key sequence in the current mode.
57
+ # E.g.: keys("i h e l l o esc")
58
+ # Special tokens: ctrl-x, alt-x, shift-X, esc, enter, backspace, tab, space
59
+ def keys(seq)
60
+ seq.split.each do |k|
61
+ vma.kbd.match_key_conf(k, nil, :key_press)
62
+ # Emit a key_release for modifier-only keys so state resets correctly
63
+ vma.kbd.match_key_conf(k + "!", nil, :key_release) if %w[ctrl alt shift].include?(k)
64
+ end
65
+ drain_idle
66
+ end
67
+
68
+ # ── Assertions ───────────────────────────────────────────────────────────
69
+
70
+ def assert_buf(expected, msg = nil)
71
+ actual = vma.buf.to_s
72
+ return if actual == expected
73
+ raise VmaTestFailure, (msg || "buffer mismatch\n expected: #{expected.inspect}\n actual: #{actual.inspect}")
74
+ end
75
+
76
+ def assert_pos(lpos, cpos, msg = nil)
77
+ al = vma.buf.lpos
78
+ ac = vma.buf.cpos
79
+ return if al == lpos && ac == cpos
80
+ raise VmaTestFailure, (msg || "position mismatch: expected line #{lpos} col #{cpos}, got line #{al} col #{ac}")
81
+ end
82
+
83
+ def assert_mode(expected_mode, msg = nil)
84
+ actual = vma.kbd.get_mode
85
+ return if actual == expected_mode
86
+ raise VmaTestFailure, (msg || "mode mismatch: expected #{expected_mode.inspect}, got #{actual.inspect}")
87
+ end
88
+
89
+ def assert_eq(expected, actual, msg = nil)
90
+ return if expected == actual
91
+ raise VmaTestFailure, (msg || "expected #{expected.inspect}, got #{actual.inspect}")
92
+ end
93
+
94
+ def assert(cond, msg = "assertion failed")
95
+ raise VmaTestFailure, msg unless cond
96
+ end
97
+
98
+ private
99
+
100
+ def _setup_test
101
+ # Fresh buffer, command mode
102
+ b = create_new_buffer("\n", "test", true)
103
+ vma.kbd.set_mode(:command) rescue nil
104
+ drain_idle
105
+ end
106
+
107
+ # Let any GLib::Idle callbacks run before we check state
108
+ def drain_idle
109
+ 20.times { GLib::MainContext.default.iteration(false) }
110
+ end
111
+ end
112
+
113
+ # ── Runner ───────────────────────────────────────────────────────────────────
114
+
115
+ def run_vma_tests(*classes)
116
+ classes = ObjectSpace.each_object(Class)
117
+ .select { |c| c < VmaTest }
118
+ .sort_by(&:name) if classes.empty?
119
+
120
+ total_pass = 0
121
+ total_fail = []
122
+
123
+ classes.each do |klass|
124
+ puts "\n#{klass}"
125
+ t = klass.new
126
+ t.run_all
127
+ total_pass += t.passes
128
+ total_fail.concat(t.failures)
129
+ end
130
+
131
+ puts "\n#{"=" * 50}"
132
+ puts "Results: #{total_pass} passed, #{total_fail.size} failed"
133
+ total_fail.each { |f| puts " FAIL: #{f}" }
134
+ puts "=" * 50
135
+
136
+ total_fail.empty?
137
+ end
data/lib/vimamsa/util.rb CHANGED
@@ -4,7 +4,9 @@ VOWELS = %w(a e i o u)
4
4
  CONSONANTS = %w(b c d f g h j k l m n p q r s t v w x y z)
5
5
 
6
6
  def draw_cursor_bug_workaround()
7
- DelayExecutioner.exec(id: :bug_workaround_draw_cursor, wait: 1.0, callable: proc { vma.gui.view.draw_cursor(); debug ":bug_workaround_draw_cursor"; false })
7
+ if !vma.gui.nil?
8
+ DelayExecutioner.exec(id: :bug_workaround_draw_cursor, wait: 1.0, callable: proc { vma.gui.view.draw_cursor(); debug ":bug_workaround_draw_cursor"; false })
9
+ end
8
10
  end
9
11
 
10
12
  def running_wayland?
@@ -24,14 +26,6 @@ def tilde_path(abspath)
24
26
  abspath.sub(/^#{Regexp.escape(userhome)}\//, "~/")
25
27
  end
26
28
 
27
- def to_camel_case(str)
28
- words = str.split(/\W+/) # Split the input string into words
29
- camel_case_words = words.map.with_index do |word, index|
30
- index == 0 ? word.downcase : word.capitalize
31
- end
32
- camel_case_words.join
33
- end
34
-
35
29
  def generate_password(length)
36
30
  password = ""
37
31
  while password.size < length
@@ -53,13 +47,6 @@ def generate_password_to_buf(length)
53
47
  vma.buf.insert_txt(passw)
54
48
  end
55
49
 
56
- # Get all indexes for start of matching regexp
57
- def scan_indexes(txt, regex)
58
- # indexes = txt.enum_for(:scan, regex).map { Regexp.last_match.begin(0) + 1 }
59
- indexes = txt.enum_for(:scan, regex).map { Regexp.last_match.begin(0) }
60
- return indexes
61
- end
62
-
63
50
  def file_mime_type(fpath)
64
51
  fpath = File.expand_path(fpath)
65
52
  return nil if !File.readable?(fpath)
@@ -322,18 +309,6 @@ def read_file(text, path)
322
309
  return content
323
310
  end
324
311
 
325
- def sanitize_input(str)
326
- if str.encoding != Encoding::UTF_8
327
- str = text.encode(Encoding::UTF_8)
328
- end
329
- str.gsub!(/\r\n/, "\n")
330
- return str
331
- end
332
-
333
- def is_url(s)
334
- return s.match(/(https?|file):\/\/.*/) != nil
335
- end
336
-
337
312
  def expand_if_existing(fpath)
338
313
  return nil if fpath.class != String
339
314
  fpath = File.expand_path(fpath)
@@ -386,11 +361,3 @@ end
386
361
  # #TODO: check contents of file
387
362
  # return true
388
363
  # end
389
-
390
- def is_path(s)
391
- m = s.match(/(~[a-z]*)?\/.*\//)
392
- if m != nil
393
- return true
394
- end
395
- return false
396
- end
@@ -1,3 +1,3 @@
1
1
  module Vimamsa
2
- VERSION = "0.1.22"
2
+ VERSION = "0.1.24"
3
3
  end