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
@@ -0,0 +1,318 @@
1
+ # Scientific calculator widget embeddable inline in a text buffer.
2
+ #
3
+ # Enable this module in Preferences → Modules → "Scientific Calculator".
4
+ #
5
+ # Usage (once enabled):
6
+ # :insert_calculator action — inserts the widget at the cursor position
7
+ #
8
+ # The widget occupies a single U+FFFC (object replacement) character in the
9
+ # Ruby buffer, keeping GTK/Ruby buffer offsets aligned.
10
+ # Press ↩ inside the calculator to insert the current result after the widget.
11
+
12
+ class VmaCalculator < Gtk::Box
13
+ BUTTON_LAYOUT = [
14
+ # label row col w h
15
+ ["sin", 0, 0, 1, 1],
16
+ ["cos", 0, 1, 1, 1],
17
+ ["tan", 0, 2, 1, 1],
18
+ ["log", 0, 3, 1, 1],
19
+ ["ln", 0, 4, 1, 1],
20
+ ["√x", 1, 0, 1, 1],
21
+ ["x²", 1, 1, 1, 1],
22
+ ["xʸ", 1, 2, 1, 1],
23
+ ["π", 1, 3, 1, 1],
24
+ ["e", 1, 4, 1, 1],
25
+ ["C", 2, 0, 1, 1],
26
+ ["±", 2, 1, 1, 1],
27
+ ["%", 2, 2, 1, 1],
28
+ ["⌫", 2, 3, 1, 1],
29
+ ["↩", 2, 4, 1, 1],
30
+ ["7", 3, 0, 1, 1],
31
+ ["8", 3, 1, 1, 1],
32
+ ["9", 3, 2, 1, 1],
33
+ ["÷", 3, 3, 1, 1],
34
+ ["(", 3, 4, 1, 1],
35
+ ["4", 4, 0, 1, 1],
36
+ ["5", 4, 1, 1, 1],
37
+ ["6", 4, 2, 1, 1],
38
+ ["×", 4, 3, 1, 1],
39
+ [")", 4, 4, 1, 1],
40
+ ["1", 5, 0, 1, 1],
41
+ ["2", 5, 1, 1, 1],
42
+ ["3", 5, 2, 1, 1],
43
+ ["-", 5, 3, 1, 1],
44
+ ["=", 5, 4, 2, 1],
45
+ ["0", 6, 0, 2, 1],
46
+ [".", 6, 2, 1, 1],
47
+ ["+", 6, 3, 1, 1],
48
+ ].freeze
49
+
50
+ def initialize(anchor_pos, bufo)
51
+ super(:vertical, 2)
52
+ @anchor_pos = anchor_pos
53
+ @bufo = bufo
54
+ @expression = ""
55
+ @last_result = nil
56
+ @variables = {} # name => value, updated on every assignment
57
+ # Persistent binding for eval so that assigned variables (e.g. x = 5)
58
+ # survive between calculations. Scoped to self so deg2rad and Math are accessible.
59
+ @eval_binding = instance_eval { binding }
60
+
61
+ self.margin_top = 4
62
+ self.margin_bottom = 4
63
+ self.margin_start = 4
64
+ self.margin_end = 4
65
+
66
+ setup_ui
67
+ end
68
+
69
+ private
70
+
71
+ def setup_ui
72
+ @expr_label = Gtk::Label.new("")
73
+ @expr_label.xalign = 1.0
74
+ @expr_label.add_css_class("dim-label")
75
+ append(@expr_label)
76
+
77
+ @display = Gtk::Entry.new
78
+ @display.editable = true
79
+ @display.xalign = 1.0
80
+ @display.text = "0"
81
+ @display.add_css_class("monospace")
82
+ @display.width_chars = 20
83
+ append(@display)
84
+
85
+ # Variable assignment row: [→ var [__name__] [Assign]]
86
+ var_row = Gtk::Box.new(:horizontal, 4)
87
+ var_row.margin_top = 4
88
+
89
+ var_arrow = Gtk::Label.new("→ var")
90
+ var_arrow.add_css_class("dim-label")
91
+
92
+ @var_name_entry = Gtk::Entry.new
93
+ @var_name_entry.placeholder_text = "name"
94
+ @var_name_entry.width_chars = 8
95
+ @var_name_entry.hexpand = true
96
+ track_entry_focus(@var_name_entry)
97
+ track_entry_focus(@display)
98
+
99
+ assign_btn = Gtk::Button.new(label: "Assign")
100
+ assign_btn.signal_connect("clicked") { assign_variable }
101
+
102
+ var_row.append(var_arrow)
103
+ var_row.append(@var_name_entry)
104
+ var_row.append(assign_btn)
105
+ append(var_row)
106
+
107
+ # Flow of variable buttons; clicking one appends the name to the expression.
108
+ @var_buttons_box = Gtk::FlowBox.new
109
+ @var_buttons_box.selection_mode = :none
110
+ @var_buttons_box.column_spacing = 2
111
+ @var_buttons_box.row_spacing = 2
112
+ @var_buttons_box.margin_top = 2
113
+ @var_buttons_box.homogeneous = false
114
+ append(@var_buttons_box)
115
+
116
+ grid = Gtk::Grid.new
117
+ grid.row_spacing = 2
118
+ grid.column_spacing = 2
119
+ grid.margin_top = 4
120
+ append(grid)
121
+
122
+ BUTTON_LAYOUT.each do |label, row, col, w, h|
123
+ btn = Gtk::Button.new(label: label)
124
+ btn.width_request = 42
125
+ btn.height_request = 36
126
+ btn.signal_connect("clicked") { on_button(label) }
127
+ btn.add_css_class("suggested-action") if %w[= ↩].include?(label)
128
+ btn.add_css_class("destructive-action") if %w[C ⌫].include?(label)
129
+ grid.attach(btn, col, row, w, h)
130
+ end
131
+ end
132
+
133
+ def on_button(label)
134
+ case label
135
+ when "C"
136
+ @expression = ""
137
+ show_display("0")
138
+ when "⌫"
139
+ @expression = @expression[0..-2] || ""
140
+ show_display(@expression.empty? ? "0" : @expression)
141
+ when "=" then evaluate
142
+ when "↩" then insert_result
143
+ when "sin" then apply_unary("Math.sin(deg2rad(%s))")
144
+ when "cos" then apply_unary("Math.cos(deg2rad(%s))")
145
+ when "tan" then apply_unary("Math.tan(deg2rad(%s))")
146
+ when "log" then apply_unary("Math.log10(%s)")
147
+ when "ln" then apply_unary("Math.log(%s)")
148
+ when "√x" then apply_unary("Math.sqrt(%s)")
149
+ when "x²"
150
+ @expression = "(#{@expression})**2"
151
+ evaluate
152
+ when "xʸ"
153
+ @expression += "**"
154
+ show_display(@expression)
155
+ when "π"
156
+ @expression += Math::PI.to_s
157
+ show_display(@expression)
158
+ when "e"
159
+ @expression += Math::E.to_s
160
+ show_display(@expression)
161
+ when "±"
162
+ @expression = @expression.start_with?("-") ? @expression[1..] : "-#{@expression}"
163
+ show_display(@expression.empty? ? "0" : @expression)
164
+ when "%"
165
+ @expression = "(#{@expression})/100.0"
166
+ evaluate
167
+ when "÷"
168
+ @expression += "/"
169
+ show_display(@expression)
170
+ when "×"
171
+ @expression += "*"
172
+ show_display(@expression)
173
+ else
174
+ @expression += label
175
+ show_display(@expression)
176
+ end
177
+ end
178
+
179
+ def apply_unary(template)
180
+ @expression = template % "(#{@expression}).to_f"
181
+ evaluate
182
+ end
183
+
184
+ def evaluate
185
+ # Pick up any text the user typed directly into the display field.
186
+ @expression = @display.text
187
+ expr = @expression
188
+ # Coerce bare integer literals to floats so that expressions like (1/6)**3
189
+ # use floating-point division instead of integer division.
190
+ # Lookbehind (?<![.\d]) and lookahead (?![.\d]) ensure digits that are
191
+ # already part of a float literal (e.g. the 53 in 1.53) are left alone.
192
+ eval_expr = expr.gsub(/(?<![.\d])(\d+)(?![.\d])/, '\1.0')
193
+ result = eval(eval_expr, @eval_binding)
194
+ @last_result = result.is_a?(Numeric) ? result.to_f : nil
195
+ formatted = result.is_a?(Float) && result == result.floor && result.abs < 1e15 ?
196
+ result.to_i.to_s : result.round(5).to_s
197
+ @expr_label.text = expr
198
+ @expression = formatted
199
+ show_display(formatted)
200
+ append_trail("#{expr} = #{formatted}")
201
+ # If the expression was a typed assignment (e.g. "x = 5"), record the variable.
202
+ if (m = expr.match(/\A\s*([a-zA-Z_]\w*)\s*=(?!=)/)) && @last_result
203
+ record_variable(m[1], @last_result)
204
+ end
205
+ rescue => e
206
+ @expr_label.text = @expression
207
+ show_display("Error")
208
+ @expression = ""
209
+ end
210
+
211
+ # Assign the current display value to the variable name typed in @var_name_entry.
212
+ # Attach a focus controller to an entry so the vimamsa key handler steps
213
+ # aside while the entry is active and resumes when it loses focus.
214
+ def track_entry_focus(entry)
215
+ fc = Gtk::EventControllerFocus.new
216
+ entry.add_controller(fc)
217
+ fc.signal_connect("enter") { vma.gui.notify_entry_focus(true) }
218
+ fc.signal_connect("leave") { vma.gui.notify_entry_focus(false) }
219
+ end
220
+
221
+ def assign_variable
222
+ name = @var_name_entry.text.strip
223
+ return if name.empty? || @last_result.nil?
224
+ return unless name.match?(/\A[a-zA-Z_]\w*\z/) # must be a valid Ruby identifier
225
+ eval("#{name} = #{@last_result}", @eval_binding)
226
+ record_variable(name, @last_result)
227
+ append_trail("#{name} = #{@last_result}")
228
+ @expr_label.text = "#{name} assigned"
229
+ rescue => e
230
+ @expr_label.text = "Error"
231
+ end
232
+
233
+ # Store a variable name/value and refresh the variable list widget.
234
+ def record_variable(name, value)
235
+ @variables[name] = value
236
+ rebuild_var_list
237
+ end
238
+
239
+ # Rebuild variable buttons from @variables — one compact button per name.
240
+ def rebuild_var_list
241
+ while (child = @var_buttons_box.first_child)
242
+ @var_buttons_box.remove(child)
243
+ end
244
+ @variables.each_key do |name|
245
+ btn = Gtk::Button.new(label: name)
246
+ btn.signal_connect("clicked") do
247
+ @expression += name
248
+ show_display(@expression)
249
+ end
250
+ @var_buttons_box.append(btn)
251
+ end
252
+ end
253
+
254
+ # Insert one calculation line into the buffer just above the widget.
255
+ # Each insertion shifts the widget down, so @anchor_pos is updated to follow it.
256
+ def append_trail(text)
257
+ line = "#{text}\n"
258
+ @bufo.insert_txt_at(line, @anchor_pos)
259
+ @anchor_pos += line.length
260
+ # Flush Ruby buffer deltas to the GTK view so the text appears immediately.
261
+ @bufo.view.handle_deltas
262
+ end
263
+
264
+ def show_display(text)
265
+ @display.text = text
266
+ end
267
+
268
+ # Insert the current result into the buffer immediately after the widget.
269
+ def insert_result
270
+ return if @last_result.nil?
271
+ result_str = (@last_result == @last_result.floor && @last_result.abs < 1e15) ?
272
+ @last_result.to_i.to_s : @last_result.to_s
273
+ @bufo.insert_txt_at(result_str, @anchor_pos + 1)
274
+ end
275
+
276
+ def deg2rad(deg)
277
+ deg * Math::PI / 180.0
278
+ end
279
+ end
280
+
281
+ # Insert a VmaCalculator widget at the current cursor position.
282
+ # U+FFFC is inserted in the Ruby buffer as a placeholder so that
283
+ # Ruby and GTK buffer offsets stay aligned (both have exactly one
284
+ # character at the anchor position).
285
+ def insert_calculator_in_buffer
286
+ view = vma.gui.view
287
+ bufo = vma.buf
288
+ return if view.nil? || bufo.nil?
289
+
290
+ pos = bufo.pos
291
+
292
+ # Step 1: insert the placeholder in the Ruby buffer and flush it to GTK.
293
+ bufo.insert_txt_at("\uFFFC", pos)
294
+ view.handle_deltas
295
+
296
+ # Step 2: the GTK buffer now has a plain U+FFFC at pos.
297
+ # Replace it with a real child anchor, then attach the widget.
298
+ iter = view.buffer.get_iter_at(:offset => pos)
299
+ if iter && iter.char == "\uFFFC"
300
+ iter2 = view.buffer.get_iter_at(:offset => pos + 1)
301
+ view.buffer.delete(iter, iter2)
302
+ iter = view.buffer.get_iter_at(:offset => pos)
303
+ anchor = view.buffer.create_child_anchor(iter)
304
+ calc = VmaCalculator.new(pos, bufo)
305
+ view.add_child_at_anchor(calc, anchor)
306
+ calc.show
307
+ end
308
+ end
309
+
310
+ def calculator_init
311
+ reg_act(:insert_calculator, proc { insert_calculator_in_buffer }, "Insert scientific calculator widget at cursor")
312
+ vma.gui.menu.add_module_action(:insert_calculator, "Insert Calculator")
313
+ end
314
+
315
+ def calculator_disable
316
+ unreg_act(:insert_calculator)
317
+ vma.gui.menu.remove_module_action(:insert_calculator)
318
+ end
@@ -0,0 +1,3 @@
1
+ def calculator_info
2
+ { name: "Scientific Calculator", no_restart: true }
3
+ end
@@ -0,0 +1,140 @@
1
+ class VmaTerminal < Gtk::Box
2
+ def initialize(anchor_pos, bufo)
3
+ super(:vertical, 0)
4
+ @anchor_pos = anchor_pos
5
+ @bufo = bufo
6
+
7
+ @term = Vte::Terminal.new
8
+ @term.hexpand = true
9
+ @term.vexpand = true
10
+ @term.set_size_request(800, 400)
11
+ @term.focusable = true
12
+
13
+ @term.signal_connect("realize") do
14
+ view_width = @bufo.view.allocated_width
15
+ w = view_width > 50 ? view_width - 50 : view_width
16
+ @term.set_size_request(w, 400)
17
+ end
18
+
19
+ click = Gtk::GestureClick.new
20
+ click.signal_connect("pressed") do
21
+ vma.gui.instance_variable_set(:@kbd_passthrough, true)
22
+ @term.grab_focus
23
+ end
24
+ @term.add_controller(click)
25
+
26
+ append(@term)
27
+
28
+ bar = Gtk::Box.new(:horizontal, 4)
29
+ bar.margin_start = 4
30
+ btn = Gtk::Button.new(label: "Copy to buffer")
31
+ btn.signal_connect("clicked") { copy_to_buffer }
32
+ bar.append(btn)
33
+ append(bar)
34
+
35
+ spawn_shell
36
+ end
37
+
38
+ private
39
+
40
+ def spawn_shell
41
+ # @term.spawn_async(
42
+ # Vte::PtyFlags::DEFAULT,
43
+ # nil,
44
+ # [ENV["SHELL"] || "/bin/bash"],
45
+ # GLib::Spawn::DEFAULT,
46
+ # # -1
47
+ # )
48
+
49
+ # @term.spawn_async(
50
+ # Vte::PtyFlags::DEFAULT,
51
+ # # [ENV["SHELL"] || "/bin/bash"],
52
+ # "/bin/bash",
53
+ # [],
54
+ # [],
55
+ @term.spawn
56
+ # @term.spawn_async(
57
+ # "/home/sjs/",
58
+ # [],
59
+ # [],
60
+ # GLib::Spawn::DEFAULT,
61
+ # )
62
+ # [ENV["SHELL"] || "/bin/bash"],
63
+ # "/bin/bash",
64
+
65
+
66
+ # g_spawn_async (
67
+ # const gchar* working_directory,
68
+ # gchar** argv,
69
+ # gchar** envp,
70
+ # GSpawnFlags flags,
71
+ # GSpawnChildSetupFunc child_setup,
72
+ # gpointer user_data,
73
+ # GPid* child_pid,
74
+ # GError** error
75
+ # )
76
+
77
+ # GLib::Spawn::DEFAULT,
78
+ # -1
79
+ # )
80
+
81
+ end
82
+
83
+ def copy_to_buffer
84
+ cols = @term.column_count
85
+ adj = @term.vadjustment
86
+ visible_rows = adj.page_size.to_i
87
+ first_row = adj.upper.to_i - visible_rows
88
+ last_row = adj.upper.to_i
89
+ text = @term.get_text_range_format(:text, first_row, 0, last_row, cols - 1)
90
+ .first
91
+ .gsub(/\s+$/, "")
92
+ .rstrip
93
+ return if text.empty?
94
+
95
+ insert_pos = @anchor_pos + 1
96
+ @bufo.insert_txt_at("\n" + text + "\n", insert_pos)
97
+ @bufo.view.handle_deltas
98
+ end
99
+ end
100
+
101
+ def insert_terminal_in_buffer
102
+ view = vma.gui.view
103
+ bufo = vma.buf
104
+ return if view.nil? || bufo.nil?
105
+
106
+ pos = bufo.pos
107
+
108
+ bufo.insert_txt_at("\uFFFC", pos)
109
+ view.handle_deltas
110
+
111
+ iter = view.buffer.get_iter_at(:offset => pos)
112
+ if iter && iter.char == "\uFFFC"
113
+ iter2 = view.buffer.get_iter_at(:offset => pos + 1)
114
+ view.buffer.delete(iter, iter2)
115
+ iter = view.buffer.get_iter_at(:offset => pos)
116
+ anchor = view.buffer.create_child_anchor(iter)
117
+ widget = VmaTerminal.new(pos, bufo)
118
+
119
+ char_w = view.pango_context.get_metrics(nil, nil).approximate_char_width / Pango::SCALE
120
+ columns = char_w > 0 ? (view.allocated_width / char_w) - 4 : 80
121
+ # require "pry";binding.pry
122
+ # widget.instance_variable_get(:@term).set_columns(columns)
123
+ widget.instance_variable_get(:@term).set_size(columns,20)
124
+
125
+ view.add_child_at_anchor(widget, anchor)
126
+ widget.show
127
+ end
128
+ end
129
+
130
+ def terminal_init
131
+ require "vte4"
132
+ reg_act(:insert_terminal, proc { insert_terminal_in_buffer }, "Insert embedded terminal at cursor")
133
+ add_keys "terminal", { "C , t" => :insert_terminal }
134
+ vma.gui.menu.add_module_action(:insert_terminal, "Insert Terminal")
135
+ end
136
+
137
+ def terminal_disable
138
+ unreg_act(:insert_terminal)
139
+ vma.gui.menu.remove_module_action(:insert_terminal)
140
+ end
@@ -0,0 +1,3 @@
1
+ def terminal_info
2
+ { name: "Embedded Terminal (VTE)", no_restart: true }
3
+ end
data/run_tests.rb ADDED
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env ruby
2
+ $stdout.sync = true
3
+ $stderr.sync = true
4
+ # run_tests.rb — Discover and run all vimamsa tests, write a report.
5
+ #
6
+ # Usage:
7
+ # ruby run_tests.rb [REPORT_FILE] default report: test_report.txt
8
+ #
9
+ # For headless / CI use:
10
+ # xvfb-run ruby run_tests.rb
11
+
12
+ require "open3"
13
+
14
+ SCRIPT_DIR = File.expand_path("..", __FILE__)
15
+ REPORT = File.expand_path(ARGV.first || "test_report.txt", SCRIPT_DIR)
16
+
17
+ # ── Discover test files ───────────────────────────────────────────────────────
18
+ test_files = Dir.glob(File.join(SCRIPT_DIR, "tests", "test_*.rb")).sort
19
+
20
+ if test_files.empty?
21
+ warn "ERROR: No test files found in tests/"
22
+ exit 1
23
+ end
24
+
25
+ # ── Guard: need a display ─────────────────────────────────────────────────────
26
+ unless ENV["DISPLAY"] || ENV["WAYLAND_DISPLAY"]
27
+ xvfb = `which xvfb-run 2>/dev/null`.strip
28
+ if xvfb.empty?
29
+ warn "ERROR: No display found (DISPLAY/WAYLAND_DISPLAY unset) and xvfb-run not available."
30
+ warn " Install xvfb or run from a desktop session."
31
+ exit 1
32
+ end
33
+ puts "No display detected — re-running under xvfb-run."
34
+ exec("xvfb-run", "-a", RbConfig.ruby, __FILE__, *ARGV)
35
+ end
36
+
37
+ # ── Print header ──────────────────────────────────────────────────────────────
38
+ puts "=== Vimamsa test runner ==="
39
+ puts "Running #{test_files.size} test file(s):"
40
+ test_files.each { |f| puts " #{File.basename(f)}" }
41
+ puts ""
42
+
43
+ # ── Run tests ─────────────────────────────────────────────────────────────────
44
+ # All files loaded in a single vimamsa session (one GTK startup).
45
+ # stdout → streamed live and captured for the report
46
+ # stderr → suppressed (GTK/GLib noise)
47
+ cmd = [RbConfig.ruby, "exe/run_tests.rb", "--test", *test_files]
48
+ raw = ""
49
+ IO.popen([*cmd, err: File::NULL], chdir: SCRIPT_DIR) do |io|
50
+ io.each_line do |line|
51
+ print line
52
+ raw << line
53
+ end
54
+ end
55
+ puts ""
56
+
57
+ # ── Parse summary ─────────────────────────────────────────────────────────────
58
+ summary_line = raw.lines.grep(/^Results:/).last.to_s
59
+ passed = summary_line[/(\d+) passed/, 1].to_i
60
+ failed = summary_line[/(\d+) failed/, 1].to_i
61
+ errors = raw.lines.count { |l| l.match?(/^\s+ERROR\s/) }
62
+
63
+ timestamp = Time.now.strftime("%Y-%m-%d %H:%M:%S")
64
+
65
+ # ── Write report ──────────────────────────────────────────────────────────────
66
+ File.open(REPORT, "w") do |f|
67
+ f.puts "Vimamsa Test Report — #{timestamp}"
68
+ f.puts "=" * 52
69
+ f.puts ""
70
+ f.puts "Test files (#{test_files.size}):"
71
+ test_files.each { |tf| f.puts " #{File.basename(tf)}" }
72
+ f.puts ""
73
+ f.puts "-" * 52
74
+ f.puts raw
75
+ f.puts "-" * 52
76
+ f.puts ""
77
+ f.puts "Summary"
78
+ f.puts "-" * 52
79
+ f.puts " Passed : #{passed}"
80
+ f.puts " Failed : #{failed}"
81
+ f.puts " Errors : #{errors}"
82
+ f.puts "\n ALL TESTS PASSED" if failed + errors == 0
83
+ f.puts ""
84
+ end
85
+
86
+ puts "Report written to: #{REPORT}"
87
+ puts " Passed: #{passed} Failed: #{failed} Errors: #{errors}"
88
+
89
+ exit(failed + errors == 0 ? 0 : 1)
data/styles/dark.xml CHANGED
@@ -128,7 +128,7 @@
128
128
 
129
129
  <style name="def:title" scale="2.0" bold="true" />
130
130
  <style name="def:hyperlink" foreground="lightblue" bold="true" />
131
- <style name="def:heading0" scale="5.0" bold="true" foreground="heading" />
131
+ <style name="def:heading0" scale="2.0" bold="true" foreground="heading" />
132
132
  <style name="def:heading1" scale="1.75" bold="true" foreground="heading" />
133
133
  <style name="def:heading2" scale="1.5" bold="true" foreground="heading" />
134
134
  <style name="def:heading3" scale="1.25" bold="true" foreground="heading" />
@@ -31,7 +31,7 @@
31
31
  <style foreground="#66D9EF" name="def:type"/>
32
32
  <!--style background="#272822" foreground="#F8F8F2" name="text"/-->
33
33
  <style background="#000000" foreground="#F8F8F2" name="text"/>
34
- <style foreground="#75715E" name="def:comment"/>
34
+ <style foreground="#b0b0b0" name="def:comment"/>
35
35
  <style background="#3E3D32" name="current-line"/>
36
36
  <style background="#222222" foreground="#F8F8F2" name="text"/>
37
37
  <!--style foreground="#465457" name="def:comment"/-->
@@ -40,7 +40,7 @@
40
40
 
41
41
  <style name="def:title" scale="2.0" bold="true" />
42
42
  <style name="def:hyperlink" foreground="lightblue" bold="true" />
43
- <style name="def:heading0" scale="5.0" bold="true" foreground="heading" />
43
+ <style name="def:heading0" scale="2.0" bold="true" foreground="heading" />
44
44
  <style name="def:heading1" scale="1.75" bold="true" foreground="heading" />
45
45
  <style name="def:heading2" scale="1.5" bold="true" foreground="heading" />
46
46
  <style name="def:heading3" scale="1.25" bold="true" foreground="heading" />
@@ -0,0 +1,2 @@
1
+ require "lib/vimamsa/key_binding_tree.rb"
2
+ require "minitest/autorun"
@@ -0,0 +1,86 @@
1
+ class TestBasicEditing < VmaTest
2
+
3
+ def test_insert_text
4
+ act 'buf.insert_txt("hello")'
5
+ assert_buf "hello\n"
6
+ end
7
+
8
+ def test_delete_char_forward
9
+ act 'buf.insert_txt("abcde")'
10
+ act :jump_to_start_of_buffer
11
+ act :delete_char_forward
12
+ act :delete_char_forward
13
+ assert_buf "cde\n"
14
+ end
15
+
16
+ def test_delete_backward
17
+ act 'buf.insert_txt("abcde")'
18
+ act "buf.delete(BACKWARD_CHAR)"
19
+ act "buf.delete(BACKWARD_CHAR)"
20
+ assert_buf "abc\n"
21
+ end
22
+
23
+ def test_undo_redo
24
+ act 'buf.insert_txt("hello")'
25
+ assert_buf "hello\n"
26
+ act :undo
27
+ assert_buf "\n"
28
+ act :redo
29
+ assert_buf "hello\n"
30
+ end
31
+
32
+ def test_new_line
33
+ act 'buf.insert_txt("first")'
34
+ act 'buf.insert_txt("\n")'
35
+ act 'buf.insert_txt("second")'
36
+ assert_buf "first\nsecond\n"
37
+ end
38
+
39
+ def test_jump_start_end
40
+ act 'buf.insert_txt("hello")'
41
+ act :jump_to_start_of_buffer
42
+ assert_pos 0, 0
43
+ act :jump_to_end_of_buffer
44
+ assert_pos 0, 5
45
+ end
46
+
47
+ def test_copy_paste
48
+ #Note: Buffer contains a single "\n" by default
49
+ act 'buf.insert_txt("hello\n")'
50
+ act :jump_to_start_of_buffer
51
+ act :copy_cur_line
52
+ act :jump_to_end_of_buffer
53
+ act :paste_after_cursor
54
+ assert_buf "hello\n\nhello\n"
55
+ end
56
+
57
+ def test_delete_line
58
+ act 'buf.insert_txt("first\n")'
59
+ act 'buf.insert_txt("second")'
60
+ act :jump_to_start_of_buffer
61
+ act :delete_line
62
+ assert_buf "second\n"
63
+ end
64
+
65
+ end
66
+
67
+ class TestKeySequences < VmaTest
68
+
69
+ def test_insert_mode_via_keys
70
+ # 'i' enters insert mode in command mode
71
+ keys "i"
72
+ assert_mode :insert
73
+ keys "esc"
74
+ assert_mode :command
75
+ end
76
+
77
+ def test_type_and_escape
78
+ keys "i"
79
+ # Type individual characters
80
+ "hello".each_char { |c| keys c }
81
+ keys "esc"
82
+ assert_buf "hello\n"
83
+ assert_mode :command
84
+ end
85
+
86
+ end