ruvim 0.4.0 → 0.6.0

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 (113) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +53 -4
  3. data/README.md +15 -6
  4. data/Rakefile +7 -0
  5. data/benchmark/cext_compare.rb +165 -0
  6. data/benchmark/chunked_load.rb +256 -0
  7. data/benchmark/file_load.rb +140 -0
  8. data/benchmark/hotspots.rb +178 -0
  9. data/docs/binding.md +3 -2
  10. data/docs/command.md +81 -9
  11. data/docs/done.md +23 -0
  12. data/docs/spec.md +105 -19
  13. data/docs/todo.md +9 -0
  14. data/docs/tutorial.md +9 -1
  15. data/docs/vim_diff.md +13 -0
  16. data/ext/ruvim/extconf.rb +5 -0
  17. data/ext/ruvim/ruvim_ext.c +519 -0
  18. data/lib/ruvim/app.rb +217 -2778
  19. data/lib/ruvim/browser.rb +104 -0
  20. data/lib/ruvim/buffer.rb +39 -28
  21. data/lib/ruvim/command_invocation.rb +2 -2
  22. data/lib/ruvim/completion_manager.rb +708 -0
  23. data/lib/ruvim/dispatcher.rb +14 -8
  24. data/lib/ruvim/display_width.rb +91 -45
  25. data/lib/ruvim/editor.rb +64 -81
  26. data/lib/ruvim/ex_command_registry.rb +3 -1
  27. data/lib/ruvim/gh/link.rb +207 -0
  28. data/lib/ruvim/git/blame.rb +16 -6
  29. data/lib/ruvim/git/branch.rb +20 -5
  30. data/lib/ruvim/git/grep.rb +107 -0
  31. data/lib/ruvim/git/handler.rb +42 -1
  32. data/lib/ruvim/global_commands.rb +175 -35
  33. data/lib/ruvim/highlighter.rb +4 -13
  34. data/lib/ruvim/key_handler.rb +1510 -0
  35. data/lib/ruvim/keymap_manager.rb +7 -7
  36. data/lib/ruvim/lang/base.rb +5 -0
  37. data/lib/ruvim/lang/c.rb +116 -0
  38. data/lib/ruvim/lang/cpp.rb +107 -0
  39. data/lib/ruvim/lang/csv.rb +4 -1
  40. data/lib/ruvim/lang/diff.rb +2 -0
  41. data/lib/ruvim/lang/dockerfile.rb +36 -0
  42. data/lib/ruvim/lang/elixir.rb +85 -0
  43. data/lib/ruvim/lang/erb.rb +30 -0
  44. data/lib/ruvim/lang/go.rb +83 -0
  45. data/lib/ruvim/lang/html.rb +34 -0
  46. data/lib/ruvim/lang/javascript.rb +83 -0
  47. data/lib/ruvim/lang/json.rb +6 -0
  48. data/lib/ruvim/lang/lua.rb +76 -0
  49. data/lib/ruvim/lang/makefile.rb +36 -0
  50. data/lib/ruvim/lang/markdown.rb +3 -4
  51. data/lib/ruvim/lang/ocaml.rb +77 -0
  52. data/lib/ruvim/lang/perl.rb +91 -0
  53. data/lib/ruvim/lang/python.rb +85 -0
  54. data/lib/ruvim/lang/registry.rb +102 -0
  55. data/lib/ruvim/lang/ruby.rb +7 -0
  56. data/lib/ruvim/lang/rust.rb +95 -0
  57. data/lib/ruvim/lang/scheme.rb +5 -0
  58. data/lib/ruvim/lang/sh.rb +76 -0
  59. data/lib/ruvim/lang/sql.rb +52 -0
  60. data/lib/ruvim/lang/toml.rb +36 -0
  61. data/lib/ruvim/lang/tsv.rb +4 -1
  62. data/lib/ruvim/lang/typescript.rb +53 -0
  63. data/lib/ruvim/lang/yaml.rb +62 -0
  64. data/lib/ruvim/rich_view/table_renderer.rb +3 -3
  65. data/lib/ruvim/rich_view.rb +14 -7
  66. data/lib/ruvim/screen.rb +126 -72
  67. data/lib/ruvim/stream/file_load.rb +85 -0
  68. data/lib/ruvim/stream/follow.rb +40 -0
  69. data/lib/ruvim/stream/git.rb +43 -0
  70. data/lib/ruvim/stream/run.rb +74 -0
  71. data/lib/ruvim/stream/stdin.rb +55 -0
  72. data/lib/ruvim/stream.rb +35 -0
  73. data/lib/ruvim/stream_mixer.rb +394 -0
  74. data/lib/ruvim/terminal.rb +18 -4
  75. data/lib/ruvim/text_metrics.rb +84 -65
  76. data/lib/ruvim/version.rb +1 -1
  77. data/lib/ruvim/window.rb +5 -5
  78. data/lib/ruvim.rb +23 -6
  79. data/test/app_command_test.rb +382 -0
  80. data/test/app_completion_test.rb +43 -19
  81. data/test/app_dot_repeat_test.rb +27 -3
  82. data/test/app_ex_command_test.rb +154 -0
  83. data/test/app_motion_test.rb +13 -12
  84. data/test/app_register_test.rb +2 -1
  85. data/test/app_scenario_test.rb +15 -10
  86. data/test/app_startup_test.rb +70 -27
  87. data/test/app_text_object_test.rb +2 -1
  88. data/test/app_unicode_behavior_test.rb +3 -2
  89. data/test/browser_test.rb +88 -0
  90. data/test/buffer_test.rb +24 -0
  91. data/test/cli_test.rb +63 -0
  92. data/test/command_invocation_test.rb +33 -0
  93. data/test/config_dsl_test.rb +47 -0
  94. data/test/dispatcher_test.rb +74 -4
  95. data/test/ex_command_registry_test.rb +106 -0
  96. data/test/follow_test.rb +20 -21
  97. data/test/gh_link_test.rb +141 -0
  98. data/test/git_blame_test.rb +96 -17
  99. data/test/git_grep_test.rb +64 -0
  100. data/test/highlighter_test.rb +125 -0
  101. data/test/indent_test.rb +137 -0
  102. data/test/input_screen_integration_test.rb +1 -1
  103. data/test/keyword_chars_test.rb +85 -0
  104. data/test/lang_test.rb +634 -0
  105. data/test/markdown_renderer_test.rb +5 -5
  106. data/test/on_save_hook_test.rb +12 -8
  107. data/test/render_snapshot_test.rb +78 -0
  108. data/test/rich_view_test.rb +42 -42
  109. data/test/run_command_test.rb +307 -0
  110. data/test/screen_test.rb +68 -5
  111. data/test/stream_test.rb +165 -0
  112. data/test/window_test.rb +59 -0
  113. metadata +52 -2
@@ -0,0 +1,307 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "test_helper"
4
+
5
+ class RunCommandTest < Minitest::Test
6
+ TerminalStub = Struct.new(:winsize) do
7
+ def write(_data) = nil
8
+ end
9
+
10
+ def setup
11
+ @app = RuVim::App.new(clean: true)
12
+ @editor = @app.instance_variable_get(:@editor)
13
+ @key_handler = @app.instance_variable_get(:@key_handler)
14
+ @editor.materialize_intro_buffer!
15
+ end
16
+
17
+ def feed(*keys)
18
+ keys.each { |k| @key_handler.handle(k) }
19
+ end
20
+
21
+ def buf
22
+ @editor.current_buffer
23
+ end
24
+
25
+ def win
26
+ @editor.current_window
27
+ end
28
+
29
+ # --- runprg option ---
30
+
31
+ def test_runprg_option_exists
32
+ assert_nil @editor.get_option("runprg")
33
+ end
34
+
35
+ def test_runprg_set_via_ex
36
+ feed(*":set runprg=ruby\\ -w\\ %".chars, :enter)
37
+ assert_equal "ruby -w %", @editor.get_option("runprg")
38
+ end
39
+
40
+ # --- % expansion ---
41
+
42
+ def test_expand_run_command_replaces_percent_with_filename
43
+ buf.instance_variable_set(:@path, "/tmp/test.rb")
44
+ gc = RuVim::GlobalCommands.instance
45
+ result = gc.send(:expand_run_command, "ruby -w %", buf)
46
+ assert_equal "ruby -w /tmp/test.rb", result
47
+ end
48
+
49
+ def test_expand_run_command_without_percent
50
+ gc = RuVim::GlobalCommands.instance
51
+ result = gc.send(:expand_run_command, "echo hello", buf)
52
+ assert_equal "echo hello", result
53
+ end
54
+
55
+ def test_expand_run_command_percent_with_no_path_raises
56
+ gc = RuVim::GlobalCommands.instance
57
+ assert_raises(RuVim::CommandError) do
58
+ gc.send(:expand_run_command, "ruby %", buf)
59
+ end
60
+ end
61
+
62
+ # --- :run with args ---
63
+
64
+ def test_run_with_args_creates_shell_output_buffer
65
+ feed(*":run echo hello".chars, :enter)
66
+ output_buf = @editor.buffers.values.find { |b| b.name == "[Shell Output]" }
67
+ assert output_buf, "Expected [Shell Output] buffer to be created"
68
+ assert output_buf.readonly?
69
+ end
70
+
71
+ def test_run_opens_in_horizontal_split
72
+ feed(*":run echo hello".chars, :enter)
73
+ # Should have 2 windows after split
74
+ leaves = @editor.send(:tree_leaves, @editor.layout_tree)
75
+ assert_equal 2, leaves.length, "Expected 2 windows (source + output)"
76
+ # Current window should show the output buffer
77
+ output_buf = @editor.buffers.values.find { |b| b.name == "[Shell Output]" }
78
+ assert_equal output_buf.id, @editor.current_window.buffer_id
79
+ end
80
+
81
+ def test_run_reuses_existing_output_window
82
+ feed(*":run echo first".chars, :enter)
83
+ leaves_after_first = @editor.send(:tree_leaves, @editor.layout_tree)
84
+
85
+ # Focus source window and run again
86
+ feed(*":wincmd w".chars, :enter)
87
+ feed(*":run echo second".chars, :enter)
88
+
89
+ leaves_after_second = @editor.send(:tree_leaves, @editor.layout_tree)
90
+ assert_equal leaves_after_first.length, leaves_after_second.length, "Should not create additional splits"
91
+ end
92
+
93
+ def test_run_reuses_shell_output_buffer
94
+ feed(*":run echo first".chars, :enter)
95
+ first_buf = @editor.buffers.values.find { |b| b.name == "[Shell Output]" }
96
+ first_id = first_buf.id
97
+
98
+ # Switch to source window and run again
99
+ feed(*":wincmd w".chars, :enter)
100
+ feed(*":run echo second".chars, :enter)
101
+ second_buf = @editor.buffers.values.find { |b| b.name == "[Shell Output]" }
102
+
103
+ assert_equal first_id, second_buf.id, "Should reuse the same [Shell Output] buffer"
104
+ end
105
+
106
+ # --- :run without args uses runprg ---
107
+
108
+ def test_run_no_args_uses_runprg
109
+ buf.instance_variable_set(:@path, "/tmp/test.rb")
110
+ buf.options["runprg"] = "echo %"
111
+ feed(*":run".chars, :enter)
112
+ output_buf = @editor.buffers.values.find { |b| b.name == "[Shell Output]" }
113
+ assert output_buf, "Expected [Shell Output] buffer to be created"
114
+ end
115
+
116
+ def test_run_no_args_no_runprg_no_history_shows_error
117
+ feed(*":run".chars, :enter)
118
+ assert_match(/runprg/, @editor.message.to_s)
119
+ end
120
+
121
+ # --- per-buffer run history ---
122
+
123
+ def test_run_remembers_last_command_per_buffer
124
+ buf.instance_variable_set(:@path, "/tmp/test.rb")
125
+ feed(*":run echo test1".chars, :enter)
126
+
127
+ # Switch back and run again without args
128
+ feed(*":bprev".chars, :enter)
129
+ feed(*":run".chars, :enter)
130
+
131
+ output_buf = @editor.buffers.values.find { |b| b.name == "[Shell Output]" }
132
+ assert output_buf
133
+ end
134
+
135
+ # --- filetype default runprg ---
136
+
137
+ def test_ruby_filetype_sets_default_runprg
138
+ buf.instance_variable_set(:@path, "/tmp/test.rb")
139
+ @editor.send(:assign_filetype, buf, "ruby")
140
+ assert_equal "ruby -w %", buf.options["runprg"]
141
+ end
142
+
143
+ def test_python_filetype_sets_default_runprg
144
+ buf.instance_variable_set(:@path, "/tmp/test.py")
145
+ @editor.send(:assign_filetype, buf, "python")
146
+ assert_equal "python3 %", buf.options["runprg"]
147
+ end
148
+
149
+ # --- Ctrl-C stops run stream ---
150
+
151
+ def test_ctrl_c_on_run_output_buffer_stops_stream
152
+ feed(*":run echo hello".chars, :enter)
153
+ output_buf = @editor.buffers.values.find { |b| b.name == "[Shell Output]" }
154
+ assert output_buf
155
+ assert output_buf.stream.stop_handler, "Expected stream to have a stop_handler"
156
+
157
+ @editor.stream_stop_or_cancel!
158
+ assert_includes @editor.message.to_s, "stopped"
159
+ end
160
+
161
+ # --- auto-save before :run ---
162
+
163
+ def test_run_auto_saves_modified_buffer
164
+ Tempfile.create(["ruvim-run-save", ".rb"]) do |f|
165
+ f.write("original\n")
166
+ f.flush
167
+
168
+ app = RuVim::App.new(path: f.path, clean: true)
169
+ editor = app.instance_variable_get(:@editor)
170
+ kh = app.instance_variable_get(:@key_handler)
171
+ buf = editor.current_buffer
172
+
173
+ # Simulate editing
174
+ buf.lines[0] = "modified"
175
+ buf.modified = true
176
+
177
+ ":run echo ok".chars.each { |k| kh.handle(k) }
178
+ kh.handle(:enter)
179
+
180
+ # File should be saved
181
+ assert_equal "modified", File.read(f.path)
182
+ assert_equal false, buf.modified?
183
+ end
184
+ end
185
+
186
+ def test_run_does_not_save_unmodified_buffer
187
+ Tempfile.create(["ruvim-run-nosave", ".rb"]) do |f|
188
+ f.write("original\n")
189
+ f.flush
190
+ mtime_before = File.mtime(f.path)
191
+
192
+ app = RuVim::App.new(path: f.path, clean: true)
193
+ kh = app.instance_variable_get(:@key_handler)
194
+
195
+ sleep 0.01 # ensure mtime would differ if written
196
+ ":run echo ok".chars.each { |k| kh.handle(k) }
197
+ kh.handle(:enter)
198
+
199
+ assert_equal mtime_before, File.mtime(f.path)
200
+ end
201
+ end
202
+
203
+ # --- streaming output arrives incrementally ---
204
+
205
+ def test_run_streams_output_incrementally
206
+ sh = @app.instance_variable_get(:@stream_mixer)
207
+ # Run a command that prints lines with small delays
208
+ cmd = "ruby -e 'STDOUT.sync=true; 3.times{puts _1; sleep 0.1}'"
209
+ feed(*":run #{cmd}".chars, :enter)
210
+ output_buf = @editor.buffers.values.find { |b| b.name == "[Shell Output]" }
211
+ assert output_buf, "Expected [Shell Output] buffer"
212
+
213
+ # Wait for at least one line to arrive (but not necessarily all)
214
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 5
215
+ loop do
216
+ sh.drain_events!
217
+ break if output_buf.lines.any? { |l| l.include?("0") }
218
+ if Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
219
+ flunk "Timed out waiting for streamed output; lines=#{output_buf.lines.inspect}"
220
+ end
221
+ sleep 0.05
222
+ end
223
+
224
+ # Should have received at least the first line while command is still running
225
+ assert_includes output_buf.lines, "0"
226
+ ensure
227
+ output_buf&.stream&.stop!
228
+ end
229
+
230
+ # --- status line shows run command ---
231
+
232
+ def test_ctrl_c_key_stops_running_stream
233
+ sh = @app.instance_variable_get(:@stream_mixer)
234
+ feed(*":run sleep 10".chars, :enter)
235
+ output_buf = @editor.buffers.values.find { |b| b.name == "[Shell Output]" }
236
+ assert output_buf, "Expected [Shell Output] buffer"
237
+ assert_equal :live, output_buf.stream.state, "Buffer should be streaming"
238
+ assert output_buf.stream.stop_handler, "Buffer should have stop handler"
239
+
240
+ # Simulate Ctrl-C key press
241
+ feed(:ctrl_c)
242
+ sh.drain_events!
243
+
244
+ assert_equal :closed, output_buf.stream.state, "Stream should be stopped after Ctrl-C"
245
+ ensure
246
+ output_buf&.stream&.stop! rescue nil
247
+ end
248
+
249
+ # --- stream_status shows correct label ---
250
+
251
+ def test_run_output_stream_status_live
252
+ feed(*":run sleep 10".chars, :enter)
253
+ output_buf = @editor.buffers.values.find { |b| b.name == "[Shell Output]" }
254
+ assert_equal "run", output_buf.stream_status
255
+ ensure
256
+ output_buf&.stream&.stop! rescue nil
257
+ end
258
+
259
+ def test_run_output_stream_status_exit
260
+ feed(*":run echo done".chars, :enter)
261
+ output_buf = @editor.buffers.values.find { |b| b.name == "[Shell Output]" }
262
+ sh = @app.instance_variable_get(:@stream_mixer)
263
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 5
264
+ loop do
265
+ sh.drain_events!
266
+ break if output_buf.stream.state == :closed
267
+ flunk "Timed out" if Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
268
+ sleep 0.02
269
+ end
270
+ assert_match(/\Arun\/exit 0\z/, output_buf.stream_status)
271
+ end
272
+
273
+ def test_stdin_stream_status_live
274
+ r, w = IO.pipe
275
+ buf = @editor.current_buffer
276
+ buf.stream = RuVim::Stream::Stdin.new(io: r, buffer_id: buf.id, queue: Queue.new) {}
277
+ assert_equal "stdin", buf.stream_status
278
+ ensure
279
+ buf&.stream&.stop!
280
+ w&.close rescue nil
281
+ end
282
+
283
+ def test_stdin_stream_status_closed
284
+ r, w = IO.pipe
285
+ buf = @editor.current_buffer
286
+ buf.stream = RuVim::Stream::Stdin.new(io: r, buffer_id: buf.id, queue: Queue.new) {}
287
+ buf.stream.stop!
288
+ assert_equal "stdin/EOF", buf.stream_status
289
+ ensure
290
+ w&.close rescue nil
291
+ end
292
+
293
+ def test_run_stores_command_on_output_buffer
294
+ feed(*":run echo hello".chars, :enter)
295
+ output_buf = @editor.buffers.values.find { |b| b.name == "[Shell Output]" }
296
+ assert_equal "echo hello", output_buf.stream_command
297
+ end
298
+
299
+ def test_status_line_includes_run_command
300
+ feed(*":run echo hello".chars, :enter)
301
+ term = TerminalStub.new([6, 80])
302
+ screen = RuVim::Screen.new(terminal: term)
303
+ line = screen.send(:status_line, @editor, 80)
304
+ assert_includes line, "echo hello"
305
+ assert_includes line, "[run]"
306
+ end
307
+ end
data/test/screen_test.rb CHANGED
@@ -104,14 +104,18 @@ class ScreenTest < Minitest::Test
104
104
  def test_status_line_shows_stream_state_for_stdin_buffer
105
105
  editor = RuVim::Editor.new
106
106
  buf = editor.add_virtual_buffer(kind: :stream, name: "[stdin]", lines: [""], readonly: true, modifiable: false)
107
- buf.stream_state = :closed
107
+ r, w = IO.pipe
108
+ buf.stream = RuVim::Stream::Stdin.new(io: r, buffer_id: buf.id, queue: Queue.new) {}
109
+ buf.stream.stop!
108
110
  editor.add_window(buffer_id: buf.id)
109
111
 
110
112
  term = TerminalStub.new([6, 60])
111
113
  screen = RuVim::Screen.new(terminal: term)
112
114
  line = screen.send(:status_line, editor, 60)
113
115
 
114
- assert_includes line, "[stdin/closed]"
116
+ assert_includes line, "[stdin/EOF]"
117
+ ensure
118
+ w&.close rescue nil
115
119
  end
116
120
 
117
121
  def test_render_uses_dim_and_brighter_line_number_gutter_colors
@@ -380,7 +384,7 @@ class ScreenTest < Minitest::Test
380
384
  # Create rows where raw lines differ in length but formatted lines are aligned
381
385
  buf.replace_all_lines!(["Short\tSecond\tThird", "LongerField\tB\tC"])
382
386
  buf.options["filetype"] = "tsv"
383
- RuVim::RichView.open!(editor, format: "tsv")
387
+ RuVim::RichView.open!(editor, format: :tsv)
384
388
 
385
389
  # Move cursor to end of line (like pressing $)
386
390
  win.cursor_y = 0
@@ -420,7 +424,7 @@ class ScreenTest < Minitest::Test
420
424
  "緑\tキウイフルーツジャム\t静岡"
421
425
  ])
422
426
  buf.options["filetype"] = "tsv"
423
- RuVim::RichView.open!(editor, format: "tsv")
427
+ RuVim::RichView.open!(editor, format: :tsv)
424
428
 
425
429
  # Move to end of line to trigger horizontal scroll
426
430
  win.cursor_y = 2
@@ -503,7 +507,7 @@ class ScreenTest < Minitest::Test
503
507
  win = editor.add_window(buffer_id: buf.id)
504
508
  buf.replace_all_lines!(["A\tB\tC", "DD\tEE\tFF"])
505
509
  buf.options["filetype"] = "tsv"
506
- RuVim::RichView.open!(editor, format: "tsv")
510
+ RuVim::RichView.open!(editor, format: :tsv)
507
511
 
508
512
  # Move cursor to end of raw line
509
513
  win.cursor_y = 0
@@ -590,4 +594,63 @@ class ScreenTest < Minitest::Test
590
594
  # win2 + win3 heights + separator should equal text_rows
591
595
  assert_equal text_rows, rects[win2.id][:height] + rects[win3.id][:height] + 1
592
596
  end
597
+
598
+ def test_cursor_hidden_in_normal_mode
599
+ editor = RuVim::Editor.new
600
+ buf = editor.add_empty_buffer
601
+ editor.add_window(buffer_id: buf.id)
602
+ editor.enter_normal_mode
603
+
604
+ term = TerminalStub.new([8, 20])
605
+ screen = RuVim::Screen.new(terminal: term)
606
+ screen.render(editor)
607
+
608
+ output = term.writes.join
609
+ # Normal mode hides terminal cursor (cell rendering handles visibility)
610
+ refute_includes output, "\e[?25h", "Normal mode should hide terminal cursor"
611
+ end
612
+
613
+ def test_cursor_bar_in_insert_mode
614
+ editor = RuVim::Editor.new
615
+ buf = editor.add_empty_buffer
616
+ editor.add_window(buffer_id: buf.id)
617
+ editor.enter_insert_mode
618
+
619
+ term = TerminalStub.new([8, 20])
620
+ screen = RuVim::Screen.new(terminal: term)
621
+ screen.render(editor)
622
+
623
+ output = term.writes.join
624
+ assert_includes output, "\e[6 q", "Insert mode should set steady bar cursor"
625
+ assert_includes output, "\e[?25h", "Insert mode should show terminal cursor"
626
+ end
627
+
628
+ def test_cursor_hidden_in_visual_mode
629
+ editor = RuVim::Editor.new
630
+ buf = editor.add_empty_buffer
631
+ editor.add_window(buffer_id: buf.id)
632
+ editor.enter_visual(:visual_char)
633
+
634
+ term = TerminalStub.new([8, 20])
635
+ screen = RuVim::Screen.new(terminal: term)
636
+ screen.render(editor)
637
+
638
+ output = term.writes.join
639
+ refute_includes output, "\e[?25h", "Visual mode should hide terminal cursor"
640
+ end
641
+
642
+ def test_cursor_bar_in_command_line_mode
643
+ editor = RuVim::Editor.new
644
+ buf = editor.add_empty_buffer
645
+ editor.add_window(buffer_id: buf.id)
646
+ editor.enter_command_line_mode
647
+
648
+ term = TerminalStub.new([8, 20])
649
+ screen = RuVim::Screen.new(terminal: term)
650
+ screen.render(editor)
651
+
652
+ output = term.writes.join
653
+ assert_includes output, "\e[6 q", "Command-line mode should set steady bar cursor"
654
+ assert_includes output, "\e[?25h", "Command-line mode should show terminal cursor"
655
+ end
593
656
  end
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "test_helper"
4
+
5
+ class StreamTest < Minitest::Test
6
+ def make_queue
7
+ Queue.new
8
+ end
9
+
10
+ def noop
11
+ -> {}
12
+ end
13
+
14
+ # --- Stream::Stdin ---
15
+
16
+ def test_stdin_starts_live
17
+ r, w = IO.pipe
18
+ s = RuVim::Stream::Stdin.new(io: r, buffer_id: 1, queue: make_queue, &noop)
19
+ assert_equal :live, s.state
20
+ assert_equal "stdin", s.status
21
+ ensure
22
+ s&.stop!
23
+ w&.close rescue nil
24
+ end
25
+
26
+ def test_stdin_status_closed
27
+ r, w = IO.pipe
28
+ s = RuVim::Stream::Stdin.new(io: r, buffer_id: 1, queue: make_queue, &noop)
29
+ s.stop!
30
+ assert_equal :closed, s.state
31
+ assert_equal "stdin/EOF", s.status
32
+ ensure
33
+ w&.close rescue nil
34
+ end
35
+
36
+ def test_stdin_command_is_nil
37
+ r, w = IO.pipe
38
+ s = RuVim::Stream::Stdin.new(io: r, buffer_id: 1, queue: make_queue, &noop)
39
+ assert_nil s.command
40
+ ensure
41
+ s&.stop!
42
+ w&.close rescue nil
43
+ end
44
+
45
+ # --- Stream::Run ---
46
+
47
+ def test_run_starts_live
48
+ q = make_queue
49
+ s = RuVim::Stream::Run.new(command: "echo hello", buffer_id: 1, queue: q, &noop)
50
+ assert_equal :live, s.state
51
+ assert_equal "run", s.status
52
+ assert_equal "echo hello", s.command
53
+ ensure
54
+ s&.stop!
55
+ end
56
+
57
+ def test_run_status_closed_with_exit
58
+ q = make_queue
59
+ s = RuVim::Stream::Run.new(command: "echo hello", buffer_id: 1, queue: q, &noop)
60
+ s.stop!
61
+ s.exit_status = Struct.new(:exitstatus).new(0)
62
+ assert_equal "run/exit 0", s.status
63
+ end
64
+
65
+ def test_run_status_error
66
+ q = make_queue
67
+ s = RuVim::Stream::Run.new(command: "echo hello", buffer_id: 1, queue: q, &noop)
68
+ s.state = :error
69
+ assert_equal "run/error", s.status
70
+ ensure
71
+ s&.stop!
72
+ end
73
+
74
+ # --- Stream::Follow ---
75
+
76
+ def test_follow_starts_live
77
+ Tempfile.create(["stream_test", ".txt"]) do |f|
78
+ f.write("test\n"); f.flush
79
+ s = RuVim::Stream::Follow.new(path: f.path, buffer_id: 1, queue: make_queue, &noop)
80
+ assert_equal :live, s.state
81
+ assert_match(/\Afollow/, s.status)
82
+ assert_nil s.command
83
+ ensure
84
+ s&.stop!
85
+ end
86
+ end
87
+
88
+ def test_follow_stop_clears_state
89
+ Tempfile.create(["stream_test", ".txt"]) do |f|
90
+ f.write("test\n"); f.flush
91
+ s = RuVim::Stream::Follow.new(path: f.path, buffer_id: 1, queue: make_queue, &noop)
92
+ s.stop!
93
+ assert_nil s.state
94
+ assert_nil s.status
95
+ end
96
+ end
97
+
98
+ # --- Stream::FileLoad ---
99
+
100
+ def test_file_load_starts_live
101
+ Tempfile.create(["stream_test", ".txt"]) do |f|
102
+ f.write("data\n"); f.flush
103
+ io = File.open(f.path, "rb")
104
+ s = RuVim::Stream::FileLoad.new(io: io, file_size: f.size, buffer_id: 1, queue: make_queue, &noop)
105
+ assert_equal :live, s.state
106
+ assert_equal "load", s.status
107
+ ensure
108
+ s&.stop!
109
+ end
110
+ end
111
+
112
+ def test_file_load_status_error
113
+ Tempfile.create(["stream_test", ".txt"]) do |f|
114
+ f.write("data\n"); f.flush
115
+ io = File.open(f.path, "rb")
116
+ s = RuVim::Stream::FileLoad.new(io: io, file_size: f.size, buffer_id: 1, queue: make_queue, &noop)
117
+ s.state = :error
118
+ assert_equal "load/error", s.status
119
+ ensure
120
+ s&.stop!
121
+ end
122
+ end
123
+
124
+ # --- Stream::Git ---
125
+
126
+ def test_git_status_always_nil
127
+ q = make_queue
128
+ s = RuVim::Stream::Git.new(cmd: ["echo", "test"], root: ".", buffer_id: 1, queue: q, &noop)
129
+ assert_nil s.status
130
+ ensure
131
+ s&.stop!
132
+ end
133
+
134
+ # --- common ---
135
+
136
+ def test_live_predicate
137
+ q = make_queue
138
+ s = RuVim::Stream::Run.new(command: "sleep 10", buffer_id: 1, queue: q, &noop)
139
+ assert s.live?
140
+ s.stop!
141
+ refute s.live?
142
+ end
143
+
144
+ def test_buffer_stream_status_delegates
145
+ q = make_queue
146
+ app = RuVim::App.new(clean: true)
147
+ editor = app.instance_variable_get(:@editor)
148
+ editor.materialize_intro_buffer!
149
+ buf = editor.current_buffer
150
+ buf.stream = RuVim::Stream::Run.new(command: "echo test", buffer_id: buf.id, queue: q, &noop)
151
+ assert_equal "run", buf.stream_status
152
+ assert_equal "echo test", buf.stream_command
153
+ ensure
154
+ buf&.stream&.stop!
155
+ end
156
+
157
+ def test_buffer_stream_status_nil_without_stream
158
+ app = RuVim::App.new(clean: true)
159
+ editor = app.instance_variable_get(:@editor)
160
+ editor.materialize_intro_buffer!
161
+ buf = editor.current_buffer
162
+ assert_nil buf.stream_status
163
+ assert_nil buf.stream_command
164
+ end
165
+ end
data/test/window_test.rb CHANGED
@@ -31,6 +31,65 @@ class WindowTest < Minitest::Test
31
31
  assert_operator win.col_offset, :>, 0
32
32
  end
33
33
 
34
+ def test_move_left
35
+ buffer = RuVim::Buffer.new(id: 1, lines: ["abcde"])
36
+ win = RuVim::Window.new(id: 1, buffer_id: 1)
37
+ win.cursor_x = 3
38
+
39
+ win.move_left(buffer)
40
+ assert_equal 2, win.cursor_x
41
+
42
+ win.move_left(buffer, 2)
43
+ assert_equal 0, win.cursor_x
44
+
45
+ # Should not go below 0
46
+ win.move_left(buffer)
47
+ assert_equal 0, win.cursor_x
48
+ end
49
+
50
+ def test_move_right
51
+ buffer = RuVim::Buffer.new(id: 1, lines: ["abcde"])
52
+ win = RuVim::Window.new(id: 1, buffer_id: 1)
53
+ win.cursor_x = 0
54
+
55
+ win.move_right(buffer)
56
+ assert_equal 1, win.cursor_x
57
+
58
+ win.move_right(buffer, 2)
59
+ assert_equal 3, win.cursor_x
60
+
61
+ # Should not go beyond line length
62
+ win.move_right(buffer, 10)
63
+ assert_equal 5, win.cursor_x
64
+ end
65
+
66
+ def test_move_left_with_multibyte
67
+ buffer = RuVim::Buffer.new(id: 1, lines: ["ab日本c"])
68
+ win = RuVim::Window.new(id: 1, buffer_id: 1)
69
+ win.cursor_x = 4 # on "c"
70
+
71
+ win.move_left(buffer)
72
+ assert_equal 3, win.cursor_x # on "本"
73
+
74
+ win.move_left(buffer)
75
+ assert_equal 2, win.cursor_x # on "日"
76
+ end
77
+
78
+ def test_move_up
79
+ buffer = RuVim::Buffer.new(id: 1, lines: ["abc", "def", "ghi"])
80
+ win = RuVim::Window.new(id: 1, buffer_id: 1)
81
+ win.cursor_y = 2
82
+ win.cursor_x = 1
83
+
84
+ win.move_up(buffer)
85
+ assert_equal 1, win.cursor_y
86
+ assert_equal 1, win.cursor_x
87
+
88
+ # Should not go below 0
89
+ win.move_up(buffer, 5)
90
+ assert_equal 0, win.cursor_y
91
+ end
92
+
34
93
  def test_move_down_preserves_preferred_column_across_empty_line
35
94
  long = "x" * 80
36
95
  buffer = RuVim::Buffer.new(id: 1, lines: [long, "", long])