ruvim 0.3.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 (129) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +68 -7
  3. data/README.md +30 -7
  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 +18 -1
  10. data/docs/command.md +156 -10
  11. data/docs/config.md +10 -2
  12. data/docs/done.md +23 -0
  13. data/docs/spec.md +162 -25
  14. data/docs/todo.md +9 -0
  15. data/docs/tutorial.md +33 -1
  16. data/docs/vim_diff.md +31 -8
  17. data/ext/ruvim/extconf.rb +5 -0
  18. data/ext/ruvim/ruvim_ext.c +519 -0
  19. data/lib/ruvim/app.rb +246 -2525
  20. data/lib/ruvim/browser.rb +104 -0
  21. data/lib/ruvim/buffer.rb +43 -20
  22. data/lib/ruvim/cli.rb +6 -0
  23. data/lib/ruvim/command_invocation.rb +2 -2
  24. data/lib/ruvim/completion_manager.rb +708 -0
  25. data/lib/ruvim/dispatcher.rb +14 -8
  26. data/lib/ruvim/display_width.rb +91 -45
  27. data/lib/ruvim/editor.rb +74 -80
  28. data/lib/ruvim/ex_command_registry.rb +3 -1
  29. data/lib/ruvim/file_watcher.rb +243 -0
  30. data/lib/ruvim/gh/link.rb +207 -0
  31. data/lib/ruvim/git/blame.rb +255 -0
  32. data/lib/ruvim/git/branch.rb +112 -0
  33. data/lib/ruvim/git/commit.rb +102 -0
  34. data/lib/ruvim/git/diff.rb +129 -0
  35. data/lib/ruvim/git/grep.rb +107 -0
  36. data/lib/ruvim/git/handler.rb +125 -0
  37. data/lib/ruvim/git/log.rb +41 -0
  38. data/lib/ruvim/git/status.rb +103 -0
  39. data/lib/ruvim/global_commands.rb +351 -77
  40. data/lib/ruvim/highlighter.rb +4 -11
  41. data/lib/ruvim/input.rb +1 -0
  42. data/lib/ruvim/key_handler.rb +1510 -0
  43. data/lib/ruvim/keymap_manager.rb +7 -7
  44. data/lib/ruvim/lang/base.rb +5 -0
  45. data/lib/ruvim/lang/c.rb +116 -0
  46. data/lib/ruvim/lang/cpp.rb +107 -0
  47. data/lib/ruvim/lang/csv.rb +4 -1
  48. data/lib/ruvim/lang/diff.rb +43 -0
  49. data/lib/ruvim/lang/dockerfile.rb +36 -0
  50. data/lib/ruvim/lang/elixir.rb +85 -0
  51. data/lib/ruvim/lang/erb.rb +30 -0
  52. data/lib/ruvim/lang/go.rb +83 -0
  53. data/lib/ruvim/lang/html.rb +34 -0
  54. data/lib/ruvim/lang/javascript.rb +83 -0
  55. data/lib/ruvim/lang/json.rb +40 -0
  56. data/lib/ruvim/lang/lua.rb +76 -0
  57. data/lib/ruvim/lang/makefile.rb +36 -0
  58. data/lib/ruvim/lang/markdown.rb +3 -4
  59. data/lib/ruvim/lang/ocaml.rb +77 -0
  60. data/lib/ruvim/lang/perl.rb +91 -0
  61. data/lib/ruvim/lang/python.rb +85 -0
  62. data/lib/ruvim/lang/registry.rb +102 -0
  63. data/lib/ruvim/lang/ruby.rb +7 -0
  64. data/lib/ruvim/lang/rust.rb +95 -0
  65. data/lib/ruvim/lang/scheme.rb +5 -0
  66. data/lib/ruvim/lang/sh.rb +76 -0
  67. data/lib/ruvim/lang/sql.rb +52 -0
  68. data/lib/ruvim/lang/toml.rb +36 -0
  69. data/lib/ruvim/lang/tsv.rb +4 -1
  70. data/lib/ruvim/lang/typescript.rb +53 -0
  71. data/lib/ruvim/lang/yaml.rb +62 -0
  72. data/lib/ruvim/rich_view/json_renderer.rb +131 -0
  73. data/lib/ruvim/rich_view/jsonl_renderer.rb +57 -0
  74. data/lib/ruvim/rich_view/table_renderer.rb +3 -3
  75. data/lib/ruvim/rich_view.rb +30 -7
  76. data/lib/ruvim/screen.rb +135 -84
  77. data/lib/ruvim/stream/file_load.rb +85 -0
  78. data/lib/ruvim/stream/follow.rb +40 -0
  79. data/lib/ruvim/stream/git.rb +43 -0
  80. data/lib/ruvim/stream/run.rb +74 -0
  81. data/lib/ruvim/stream/stdin.rb +55 -0
  82. data/lib/ruvim/stream.rb +35 -0
  83. data/lib/ruvim/stream_mixer.rb +394 -0
  84. data/lib/ruvim/terminal.rb +18 -4
  85. data/lib/ruvim/text_metrics.rb +84 -65
  86. data/lib/ruvim/version.rb +1 -1
  87. data/lib/ruvim/window.rb +5 -5
  88. data/lib/ruvim.rb +31 -4
  89. data/test/app_command_test.rb +382 -0
  90. data/test/app_completion_test.rb +65 -16
  91. data/test/app_dot_repeat_test.rb +27 -3
  92. data/test/app_ex_command_test.rb +154 -0
  93. data/test/app_motion_test.rb +13 -12
  94. data/test/app_register_test.rb +2 -1
  95. data/test/app_scenario_test.rb +182 -8
  96. data/test/app_startup_test.rb +70 -27
  97. data/test/app_text_object_test.rb +2 -1
  98. data/test/app_unicode_behavior_test.rb +3 -2
  99. data/test/browser_test.rb +88 -0
  100. data/test/buffer_test.rb +24 -0
  101. data/test/cli_test.rb +77 -0
  102. data/test/clipboard_test.rb +67 -0
  103. data/test/command_invocation_test.rb +33 -0
  104. data/test/command_line_test.rb +118 -0
  105. data/test/config_dsl_test.rb +134 -0
  106. data/test/dispatcher_test.rb +74 -4
  107. data/test/display_width_test.rb +41 -0
  108. data/test/ex_command_registry_test.rb +106 -0
  109. data/test/file_watcher_test.rb +197 -0
  110. data/test/follow_test.rb +198 -0
  111. data/test/gh_link_test.rb +141 -0
  112. data/test/git_blame_test.rb +792 -0
  113. data/test/git_grep_test.rb +64 -0
  114. data/test/highlighter_test.rb +169 -0
  115. data/test/indent_test.rb +223 -0
  116. data/test/input_screen_integration_test.rb +1 -1
  117. data/test/keyword_chars_test.rb +85 -0
  118. data/test/lang_test.rb +634 -0
  119. data/test/markdown_renderer_test.rb +5 -5
  120. data/test/on_save_hook_test.rb +12 -8
  121. data/test/render_snapshot_test.rb +78 -0
  122. data/test/rich_view_test.rb +279 -23
  123. data/test/run_command_test.rb +307 -0
  124. data/test/screen_test.rb +68 -5
  125. data/test/search_option_test.rb +19 -0
  126. data/test/stream_test.rb +165 -0
  127. data/test/test_helper.rb +9 -0
  128. data/test/window_test.rb +59 -0
  129. metadata +68 -2
@@ -8,11 +8,12 @@ class AppScenarioTest < Minitest::Test
8
8
  @app = RuVim::App.new(clean: true)
9
9
  @editor = @app.instance_variable_get(:@editor)
10
10
  @dispatcher = @app.instance_variable_get(:@dispatcher)
11
+ @key_handler = @app.instance_variable_get(:@key_handler)
11
12
  @editor.materialize_intro_buffer!
12
13
  end
13
14
 
14
15
  def feed(*keys)
15
- keys.each { |k| @app.send(:handle_key, k) }
16
+ keys.each { |k| @key_handler.handle(k) }
16
17
  end
17
18
 
18
19
  def test_insert_edit_search_and_delete_scenario
@@ -353,7 +354,7 @@ class AppScenarioTest < Minitest::Test
353
354
  @editor.current_buffer.modifiable = false
354
355
  @editor.current_buffer.readonly = true
355
356
 
356
- @app.send(:handle_key, "x")
357
+ @key_handler.handle("x")
357
358
 
358
359
  assert_equal ["hello"], @editor.current_buffer.lines
359
360
  assert_match(/not modifiable/i, @editor.message)
@@ -364,7 +365,7 @@ class AppScenarioTest < Minitest::Test
364
365
  @editor.current_buffer.modifiable = false
365
366
  @editor.current_buffer.readonly = true
366
367
 
367
- @app.send(:handle_key, "i")
368
+ @key_handler.handle("i")
368
369
 
369
370
  assert_equal :normal, @editor.mode
370
371
  assert_equal ["hello"], @editor.current_buffer.lines
@@ -373,15 +374,16 @@ class AppScenarioTest < Minitest::Test
373
374
 
374
375
  def test_normal_ctrl_c_stops_stdin_stream_via_default_binding
375
376
  stream = StringIO.new("hello\n")
376
- @app.instance_variable_set(:@stdin_stream_source, stream)
377
- @app.send(:prepare_stdin_stream_buffer!)
377
+ sh = @app.instance_variable_get(:@stream_mixer)
378
+ sh.prepare_stdin_stream_buffer!(stream)
379
+ sh.start_pending_stdin!
378
380
 
379
- @app.send(:handle_key, :ctrl_c)
381
+ @key_handler.handle(:ctrl_c)
380
382
 
381
- assert_equal :closed, @editor.current_buffer.stream_state
383
+ assert_equal :closed, @editor.current_buffer.stream.state
382
384
  assert_equal :normal, @editor.mode
383
385
  assert_equal true, stream.closed?
384
- assert_match(/\[stdin\] closed/, @editor.message)
386
+ assert_match(/stopped/, @editor.message)
385
387
  end
386
388
 
387
389
  def test_ctrl_z_calls_terminal_suspend
@@ -392,6 +394,7 @@ class AppScenarioTest < Minitest::Test
392
394
  end
393
395
  terminal_stub.define_singleton_method(:suspend_calls) { @suspend_calls }
394
396
  @app.instance_variable_set(:@terminal, terminal_stub)
397
+ @app.instance_variable_get(:@key_handler).instance_variable_set(:@terminal, terminal_stub)
395
398
 
396
399
  feed("i", "a", :ctrl_z)
397
400
 
@@ -404,6 +407,7 @@ class AppScenarioTest < Minitest::Test
404
407
  terminal_stub = Object.new
405
408
  terminal_stub.define_singleton_method(:suspend_for_tstp) {}
406
409
  @app.instance_variable_set(:@terminal, terminal_stub)
410
+ @app.instance_variable_get(:@key_handler).instance_variable_set(:@terminal, terminal_stub)
407
411
 
408
412
  screen_stub = Object.new
409
413
  screen_stub.instance_variable_set(:@invalidated, false)
@@ -412,6 +416,7 @@ class AppScenarioTest < Minitest::Test
412
416
  end
413
417
  screen_stub.define_singleton_method(:invalidated?) { @invalidated }
414
418
  @app.instance_variable_set(:@screen, screen_stub)
419
+ @app.instance_variable_get(:@key_handler).instance_variable_set(:@screen, screen_stub)
415
420
 
416
421
  feed(:ctrl_z)
417
422
 
@@ -695,6 +700,28 @@ class AppScenarioTest < Minitest::Test
695
700
  assert_equal :normal, @editor.mode
696
701
  end
697
702
 
703
+ def test_paste_batch_suppresses_autoindent
704
+ @editor.current_buffer.replace_all_lines!([" hello"])
705
+ @editor.current_window.cursor_y = 0
706
+ @editor.current_window.cursor_x = 0
707
+
708
+ # Normal enter in insert mode should autoindent
709
+ feed("A", :enter, :escape)
710
+ assert_equal [" hello", " "], @editor.current_buffer.lines
711
+
712
+ # Simulate paste batch: autoindent should be suppressed
713
+ @editor.current_buffer.replace_all_lines!([" hello"])
714
+ @editor.current_window.cursor_y = 0
715
+ @editor.current_window.cursor_x = 0
716
+ feed("A")
717
+ @app.instance_variable_get(:@key_handler).paste_batch = true
718
+ feed(:enter, *"world".chars)
719
+ @app.instance_variable_get(:@key_handler).paste_batch = false
720
+ feed(:escape)
721
+
722
+ assert_equal [" hello", "world"], @editor.current_buffer.lines
723
+ end
724
+
698
725
  def test_batch_insert_stops_on_escape
699
726
  @editor.current_buffer.replace_all_lines!([""])
700
727
  # Escape exits insert mode; subsequent keys are normal-mode commands
@@ -1072,4 +1099,151 @@ class AppScenarioTest < Minitest::Test
1072
1099
  assert_equal :hsplit, tree[:type]
1073
1100
  assert_equal 3, tree[:children].length
1074
1101
  end
1102
+
1103
+ # --- search filter (g/) ---
1104
+
1105
+ def test_filter_creates_buffer_with_matching_lines
1106
+ @editor.current_buffer.replace_all_lines!(["apple", "banana", "apricot", "cherry"])
1107
+ feed("/", "a", "p", :enter) # search for "ap"
1108
+ feed("g", "/")
1109
+
1110
+ buf = @editor.current_buffer
1111
+ assert_equal :filter, buf.kind
1112
+ assert_equal ["apple", "apricot"], buf.lines
1113
+ end
1114
+
1115
+ def test_filter_enter_jumps_to_original_line_and_closes_filter
1116
+ @editor.current_buffer.replace_all_lines!(["apple", "banana", "apricot", "cherry"])
1117
+ original_buf_id = @editor.current_buffer.id
1118
+ feed("/", "a", "p", :enter)
1119
+ feed("g", "/")
1120
+
1121
+ # Move to second match line ("apricot", originally line 2)
1122
+ feed("j")
1123
+ feed(:enter)
1124
+
1125
+ assert_equal original_buf_id, @editor.current_buffer.id
1126
+ assert_equal 2, @editor.current_window.cursor_y
1127
+ end
1128
+
1129
+ def test_filter_quit_returns_to_previous_buffer
1130
+ @editor.current_buffer.replace_all_lines!(["apple", "banana", "apricot"])
1131
+ original_buf_id = @editor.current_buffer.id
1132
+ feed("/", "a", "p", :enter)
1133
+ feed("g", "/")
1134
+
1135
+ assert_equal :filter, @editor.current_buffer.kind
1136
+ feed(":", "q", :enter)
1137
+
1138
+ assert_equal original_buf_id, @editor.current_buffer.id
1139
+ end
1140
+
1141
+ def test_filter_recursive_filtering
1142
+ @editor.current_buffer.replace_all_lines!(["apple pie", "apricot jam", "apple sauce", "cherry"])
1143
+ feed("/", "a", "p", :enter)
1144
+ feed("g", "/")
1145
+
1146
+ assert_equal ["apple pie", "apricot jam", "apple sauce"], @editor.current_buffer.lines
1147
+
1148
+ # Search within filter and filter again
1149
+ feed("/", "p", "l", "e", :enter)
1150
+ feed("g", "/")
1151
+
1152
+ assert_equal ["apple pie", "apple sauce"], @editor.current_buffer.lines
1153
+
1154
+ # Enter jumps to original buffer
1155
+ feed("j") # "apple sauce" - originally line 2 of buffer
1156
+ feed(:enter)
1157
+
1158
+ assert_equal 2, @editor.current_window.cursor_y
1159
+ end
1160
+
1161
+ def test_filter_inherits_filetype
1162
+ @editor.current_buffer.replace_all_lines!(["a\tb", "c\td", "a\te"])
1163
+ @editor.current_buffer.options["filetype"] = "tsv"
1164
+ feed("/", "a", :enter)
1165
+ feed("g", "/")
1166
+
1167
+ assert_equal "tsv", @editor.current_buffer.options["filetype"]
1168
+ end
1169
+
1170
+ def test_filter_without_search_pattern_shows_error
1171
+ @editor.current_buffer.replace_all_lines!(["apple", "banana"])
1172
+ feed("g", "/")
1173
+
1174
+ assert @editor.message_error?
1175
+ end
1176
+
1177
+ def test_filter_quit_restores_cursor_position
1178
+ @editor.current_buffer.replace_all_lines!(["aaa", "bbb", "aab", "ccc", "aac"])
1179
+ original_buf_id = @editor.current_buffer.id
1180
+ feed("/", "a", "a", :enter)
1181
+ # Search moves cursor to line 0 (first match)
1182
+ feed("n")
1183
+ # Now on line 2 ("aab")
1184
+ assert_equal 2, @editor.current_window.cursor_y
1185
+ feed("g", "/")
1186
+
1187
+ assert_equal :filter, @editor.current_buffer.kind
1188
+ feed(":", "q", :enter)
1189
+
1190
+ assert_equal original_buf_id, @editor.current_buffer.id
1191
+ assert_equal 2, @editor.current_window.cursor_y
1192
+ end
1193
+
1194
+ def test_filter_ex_command
1195
+ @editor.current_buffer.replace_all_lines!(["apple", "banana", "apricot"])
1196
+ feed("/", "a", "p", :enter)
1197
+ feed(":", "f", "i", "l", "t", "e", "r", :enter)
1198
+
1199
+ assert_equal :filter, @editor.current_buffer.kind
1200
+ assert_equal ["apple", "apricot"], @editor.current_buffer.lines
1201
+ end
1202
+
1203
+ # --- dG / dgg / yG / ygg / cG / cgg ---
1204
+
1205
+ def test_dG_deletes_from_cursor_to_end
1206
+ @editor.current_buffer.replace_all_lines!(["aa", "bb", "cc", "dd", "ee"])
1207
+ @editor.current_window.cursor_y = 2
1208
+ feed("d", "G")
1209
+
1210
+ assert_equal ["aa", "bb"], @editor.current_buffer.lines
1211
+ end
1212
+
1213
+ def test_dgg_deletes_from_cursor_to_start
1214
+ @editor.current_buffer.replace_all_lines!(["aa", "bb", "cc", "dd", "ee"])
1215
+ @editor.current_window.cursor_y = 2
1216
+ feed("d", "g", "g")
1217
+
1218
+ assert_equal ["dd", "ee"], @editor.current_buffer.lines
1219
+ end
1220
+
1221
+ def test_yG_yanks_from_cursor_to_end
1222
+ @editor.current_buffer.replace_all_lines!(["aa", "bb", "cc", "dd"])
1223
+ @editor.current_window.cursor_y = 1
1224
+ feed("y", "G")
1225
+
1226
+ reg = @editor.get_register('"')&.fetch(:text, "")
1227
+ assert_includes reg, "bb"
1228
+ assert_includes reg, "dd"
1229
+ end
1230
+
1231
+ def test_ygg_yanks_from_cursor_to_start
1232
+ @editor.current_buffer.replace_all_lines!(["aa", "bb", "cc", "dd"])
1233
+ @editor.current_window.cursor_y = 2
1234
+ feed("y", "g", "g")
1235
+
1236
+ reg = @editor.get_register('"')&.fetch(:text, "")
1237
+ assert_includes reg, "aa"
1238
+ assert_includes reg, "cc"
1239
+ end
1240
+
1241
+ def test_cG_changes_from_cursor_to_end
1242
+ @editor.current_buffer.replace_all_lines!(["aa", "bb", "cc", "dd"])
1243
+ @editor.current_window.cursor_y = 2
1244
+ feed("c", "G")
1245
+
1246
+ assert_equal ["aa", "bb", ""], @editor.current_buffer.lines
1247
+ assert_equal :insert, @editor.mode
1248
+ end
1075
1249
  end
@@ -185,6 +185,46 @@ class AppStartupTest < Minitest::Test
185
185
  assert_match(/Restricted mode/, editor.message)
186
186
  end
187
187
 
188
+ def test_restricted_mode_disables_ex_grep
189
+ app = RuVim::App.new(clean: true, restricted: true)
190
+ editor = app.instance_variable_get(:@editor)
191
+ dispatcher = app.instance_variable_get(:@dispatcher)
192
+
193
+ dispatcher.dispatch_ex(editor, "grep pattern file.txt")
194
+
195
+ assert_match(/Restricted mode/, editor.message)
196
+ end
197
+
198
+ def test_restricted_mode_disables_ex_lgrep
199
+ app = RuVim::App.new(clean: true, restricted: true)
200
+ editor = app.instance_variable_get(:@editor)
201
+ dispatcher = app.instance_variable_get(:@dispatcher)
202
+
203
+ dispatcher.dispatch_ex(editor, "lgrep pattern file.txt")
204
+
205
+ assert_match(/Restricted mode/, editor.message)
206
+ end
207
+
208
+ def test_restricted_mode_disables_ex_git
209
+ app = RuVim::App.new(clean: true, restricted: true)
210
+ editor = app.instance_variable_get(:@editor)
211
+ dispatcher = app.instance_variable_get(:@dispatcher)
212
+
213
+ dispatcher.dispatch_ex(editor, "git status")
214
+
215
+ assert_match(/Restricted mode/, editor.message)
216
+ end
217
+
218
+ def test_restricted_mode_disables_ex_gh
219
+ app = RuVim::App.new(clean: true, restricted: true)
220
+ editor = app.instance_variable_get(:@editor)
221
+ dispatcher = app.instance_variable_get(:@dispatcher)
222
+
223
+ dispatcher.dispatch_ex(editor, "gh link")
224
+
225
+ assert_match(/Restricted mode/, editor.message)
226
+ end
227
+
188
228
  def test_verbose_logs_startup_and_startup_ex_actions
189
229
  log = StringIO.new
190
230
  app = RuVim::App.new(
@@ -260,10 +300,11 @@ class AppStartupTest < Minitest::Test
260
300
  )
261
301
  editor = app.instance_variable_get(:@editor)
262
302
 
303
+ sh = app.instance_variable_get(:@stream_mixer)
304
+ buf = editor.current_buffer
263
305
  20.times do
264
- app.send(:drain_stream_events!)
265
- thread = app.instance_variable_get(:@stream_reader_thread)
266
- break unless thread&.alive?
306
+ sh.drain_events!
307
+ break unless buf.stream.thread&.alive?
267
308
  sleep 0.005
268
309
  end
269
310
 
@@ -272,11 +313,11 @@ class AppStartupTest < Minitest::Test
272
313
  assert_equal "[stdin]", buf.display_name
273
314
  assert_equal true, buf.readonly?
274
315
  assert_equal false, buf.modifiable?
275
- assert_includes [:live, :closed], buf.stream_state
276
- assert_equal ["line1", "line2", ""], buf.lines
316
+ assert_includes [:live, :closed], buf.stream.state
317
+ assert_equal ["line1", "line2"], buf.lines
277
318
  assert_match(/\[stdin\] (follow|EOF)/, editor.message)
278
319
  ensure
279
- app&.send(:shutdown_stream_reader!)
320
+ app&.instance_variable_get(:@stream_mixer)&.shutdown!
280
321
  end
281
322
 
282
323
  def test_large_file_threshold_uses_async_loader
@@ -290,28 +331,28 @@ class AppStartupTest < Minitest::Test
290
331
  ENV["RUVIM_ASYNC_FILE_THRESHOLD_BYTES"] = "1"
291
332
  app = RuVim::App.new(clean: true)
292
333
  editor = app.instance_variable_get(:@editor)
334
+ sh = app.instance_variable_get(:@stream_mixer)
293
335
 
294
336
  buf = editor.open_path(f.path)
295
337
  assert_match(/loading/i, editor.message)
296
- assert_includes [:live, :closed], buf.loading_state
338
+ assert_includes [:live, :closed], buf.stream&.state
297
339
  assert_equal false, buf.modifiable?
298
340
 
299
341
  100.times do
300
- app.send(:drain_stream_events!)
301
- break if buf.loading_state != :live
302
- state = app.instance_variable_get(:@async_file_loads)[buf.id]
303
- break unless state && state[:thread]&.alive?
342
+ sh.drain_events!
343
+ break if buf.stream&.state != :live
344
+ break unless buf.stream.is_a?(RuVim::Stream::FileLoad) && buf.stream.thread&.alive?
304
345
  sleep 0.005
305
346
  end
306
- app.send(:drain_stream_events!)
347
+ sh.drain_events!
307
348
 
308
- assert_equal :closed, buf.loading_state
349
+ assert_equal :closed, buf.stream&.state
309
350
  assert_equal true, buf.modifiable?
310
351
  assert_equal %w[a b c], buf.lines
311
352
  assert_match(/#{Regexp.escape(f.path)}/, editor.message)
312
353
  ensure
313
354
  ENV["RUVIM_ASYNC_FILE_THRESHOLD_BYTES"] = prev
314
- app&.send(:shutdown_background_readers!)
355
+ app&.instance_variable_get(:@stream_mixer)&.shutdown!
315
356
  end
316
357
  end
317
358
  end
@@ -329,25 +370,26 @@ class AppStartupTest < Minitest::Test
329
370
  ENV["RUVIM_ASYNC_FILE_PREFIX_BYTES"] = "4"
330
371
  app = RuVim::App.new(clean: true)
331
372
  editor = app.instance_variable_get(:@editor)
373
+ sh = app.instance_variable_get(:@stream_mixer)
332
374
 
333
375
  buf = editor.open_path(f.path)
334
- assert_equal :live, buf.loading_state
335
- assert_equal ["ab", "c"], buf.lines
376
+ assert_equal :live, buf.stream&.state
377
+ assert_equal ["ab", ""], buf.lines
336
378
  assert_match(/showing first/i, editor.message)
337
379
 
338
380
  100.times do
339
- app.send(:drain_stream_events!)
340
- break if buf.loading_state != :live
381
+ sh.drain_events!
382
+ break if buf.stream&.state != :live
341
383
  sleep 0.005
342
384
  end
343
- app.send(:drain_stream_events!)
385
+ sh.drain_events!
344
386
 
345
- assert_equal :closed, buf.loading_state
387
+ assert_equal :closed, buf.stream&.state
346
388
  assert_equal %w[ab cd ef], buf.lines
347
389
  ensure
348
390
  ENV["RUVIM_ASYNC_FILE_THRESHOLD_BYTES"] = prev_async
349
391
  ENV["RUVIM_ASYNC_FILE_PREFIX_BYTES"] = prev_prefix
350
- app&.send(:shutdown_background_readers!)
392
+ app&.instance_variable_get(:@stream_mixer)&.shutdown!
351
393
  end
352
394
  end
353
395
  end
@@ -360,16 +402,17 @@ class AppStartupTest < Minitest::Test
360
402
  ENV["HOME"] = dir
361
403
 
362
404
  app1 = RuVim::App.new(clean: true)
363
- app1.send(:push_command_line_history, ":", "set number")
364
- app1.send(:push_command_line_history, "/", "foo")
365
- app1.send(:push_command_line_history, "?", "bar")
366
- app1.send(:save_command_line_history!)
405
+ completion1 = app1.instance_variable_get(:@completion)
406
+ completion1.push_history(":", "set number")
407
+ completion1.push_history("/", "foo")
408
+ completion1.push_history("?", "bar")
409
+ completion1.save_history!
367
410
 
368
411
  path = File.join(dir, "ruvim", "history.json")
369
412
  assert_equal true, File.file?(path)
370
413
 
371
414
  app2 = RuVim::App.new(clean: true)
372
- hist = app2.instance_variable_get(:@cmdline_history)
415
+ hist = app2.instance_variable_get(:@completion).instance_variable_get(:@cmdline_history)
373
416
  assert_equal ["set number"], hist[":"]
374
417
  assert_equal ["foo"], hist["/"]
375
418
  assert_equal ["bar"], hist["?"]
@@ -387,7 +430,7 @@ class AppStartupTest < Minitest::Test
387
430
  ENV["HOME"] = dir
388
431
 
389
432
  app = RuVim::App.new(clean: true)
390
- assert_equal File.join(dir, ".ruvim", "history.json"), app.send(:command_line_history_file_path)
433
+ assert_equal File.join(dir, ".ruvim", "history.json"), app.instance_variable_get(:@completion).send(:history_file_path)
391
434
  ensure
392
435
  ENV["XDG_STATE_HOME"] = prev_xdg
393
436
  ENV["HOME"] = prev_home
@@ -4,13 +4,14 @@ class AppTextObjectTest < Minitest::Test
4
4
  def setup
5
5
  @app = RuVim::App.new(clean: true)
6
6
  @editor = @app.instance_variable_get(:@editor)
7
+ @key_handler = @app.instance_variable_get(:@key_handler)
7
8
  @editor.materialize_intro_buffer!
8
9
  @buffer = @editor.current_buffer
9
10
  @win = @editor.current_window
10
11
  end
11
12
 
12
13
  def press(*keys)
13
- keys.each { |k| @app.send(:handle_normal_key, k) }
14
+ keys.each { |k| @key_handler.send(:handle_normal_key, k) }
14
15
  end
15
16
 
16
17
  def test_delete_inside_square_brackets
@@ -4,17 +4,18 @@ class AppUnicodeBehaviorTest < Minitest::Test
4
4
  def setup
5
5
  @app = RuVim::App.new(clean: true)
6
6
  @editor = @app.instance_variable_get(:@editor)
7
+ @key_handler = @app.instance_variable_get(:@key_handler)
7
8
  @editor.materialize_intro_buffer!
8
9
  @buffer = @editor.current_buffer
9
10
  @win = @editor.current_window
10
11
  end
11
12
 
12
13
  def press_normal(*keys)
13
- keys.each { |k| @app.send(:handle_normal_key, k) }
14
+ keys.each { |k| @key_handler.send(:handle_normal_key, k) }
14
15
  end
15
16
 
16
17
  def press(*keys)
17
- keys.each { |k| @app.send(:handle_key, k) }
18
+ keys.each { |k| @key_handler.handle(k) }
18
19
  end
19
20
 
20
21
  def test_word_motions_on_japanese_text_do_not_break_character_boundaries
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "test_helper"
4
+
5
+ class BrowserTest < Minitest::Test
6
+ def test_wsl_mount_point_default
7
+ assert_equal "/mnt/", RuVim::Browser.wsl_mount_point(config: nil)
8
+ end
9
+
10
+ def test_wsl_mount_point_from_config
11
+ config = <<~CONF
12
+ [automount]
13
+ root = /win/
14
+ CONF
15
+ assert_equal "/win/", RuVim::Browser.wsl_mount_point(config: config)
16
+ end
17
+
18
+ def test_wsl_mount_point_with_missing_trailing_slash
19
+ config = <<~CONF
20
+ [automount]
21
+ root = /win
22
+ CONF
23
+ assert_equal "/win/", RuVim::Browser.wsl_mount_point(config: config)
24
+ end
25
+
26
+ def test_wsl_mount_point_ignores_commented_line
27
+ config = <<~CONF
28
+ [automount]
29
+ # root = /old/
30
+ root = /new/
31
+ CONF
32
+ assert_equal "/new/", RuVim::Browser.wsl_mount_point(config: config)
33
+ end
34
+
35
+ def test_detect_backend_returns_a_hash_or_nil
36
+ result = RuVim::Browser.detect_backend
37
+ if result
38
+ assert_kind_of Symbol, result[:type]
39
+ end
40
+ end
41
+
42
+ def test_powershell_path_uses_mount_point
43
+ path = RuVim::Browser.powershell_path("/mnt/")
44
+ assert_equal "/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe", path
45
+ end
46
+
47
+ # --- URL validation ---
48
+
49
+ def test_valid_url_https
50
+ assert RuVim::Browser.valid_url?("https://github.com/user/repo")
51
+ end
52
+
53
+ def test_valid_url_http
54
+ assert RuVim::Browser.valid_url?("http://example.com")
55
+ end
56
+
57
+ def test_invalid_url_file
58
+ refute RuVim::Browser.valid_url?("file:///etc/passwd")
59
+ end
60
+
61
+ def test_invalid_url_javascript
62
+ refute RuVim::Browser.valid_url?("javascript:alert(1)")
63
+ end
64
+
65
+ def test_invalid_url_empty
66
+ refute RuVim::Browser.valid_url?("")
67
+ end
68
+
69
+ def test_invalid_url_nil
70
+ refute RuVim::Browser.valid_url?(nil)
71
+ end
72
+
73
+ # --- PowerShell encoded command ---
74
+
75
+ def test_powershell_encoded_command_uses_encoded_flag
76
+ cmd = RuVim::Browser.powershell_encoded_command("/ps.exe", "https://github.com/user/repo")
77
+ assert_equal "/ps.exe", cmd[0]
78
+ assert_includes cmd, "-EncodedCommand"
79
+ refute_includes cmd, "-Command"
80
+ end
81
+
82
+ def test_powershell_encoded_command_escapes_single_quotes
83
+ cmd = RuVim::Browser.powershell_encoded_command("/ps.exe", "https://example.com/it's")
84
+ encoded = cmd.last
85
+ decoded = encoded.unpack1("m0").force_encoding("UTF-16LE").encode("UTF-8")
86
+ assert_includes decoded, "it''s"
87
+ end
88
+ end
data/test/buffer_test.rb CHANGED
@@ -41,6 +41,30 @@ class BufferTest < Minitest::Test
41
41
  end
42
42
  end
43
43
 
44
+ def test_from_file_rejects_non_regular_file
45
+ Dir.mktmpdir do |dir|
46
+ fifo_path = File.join(dir, "test_fifo")
47
+ system("mkfifo", fifo_path)
48
+ assert File.exist?(fifo_path), "FIFO should exist"
49
+
50
+ err = assert_raises(RuVim::CommandError) do
51
+ RuVim::Buffer.from_file(id: 1, path: fifo_path)
52
+ end
53
+ assert_match(/Not a regular file/, err.message)
54
+ end
55
+ end
56
+
57
+ def test_reload_rejects_non_regular_file
58
+ b = RuVim::Buffer.new(id: 1, path: "/dev/null")
59
+ # /dev/null exists but is not a regular file on Linux
60
+ if !File.file?("/dev/null") && File.exist?("/dev/null")
61
+ err = assert_raises(RuVim::CommandError) do
62
+ b.reload_from_file!("/dev/null")
63
+ end
64
+ assert_match(/Not a regular file/, err.message)
65
+ end
66
+ end
67
+
44
68
  def test_utf8_file_is_loaded_as_utf8_text_not_binary_bytes
45
69
  Dir.mktmpdir do |dir|
46
70
  path = File.join(dir, "ruvim_utf8_test.txt")
data/test/cli_test.rb CHANGED
@@ -153,6 +153,20 @@ class CLITest < Minitest::Test
153
153
  assert_equal "", err.string
154
154
  end
155
155
 
156
+ def test_parse_follow_option
157
+ opts = RuVim::CLI.parse(["-f", "log.txt"])
158
+
159
+ assert_equal ["log.txt"], opts.files
160
+ assert_equal true, opts.follow
161
+ end
162
+
163
+ def test_help_mentions_follow_option
164
+ out = StringIO.new
165
+ err = StringIO.new
166
+ RuVim::CLI.run(["--help"], stdout: out, stderr: err, stdin: StringIO.new)
167
+ assert_match(/-f\s+Open file in follow mode/, out.string)
168
+ end
169
+
156
170
  def test_run_returns_error_for_missing_config_file
157
171
  out = StringIO.new
158
172
  err = StringIO.new
@@ -162,4 +176,67 @@ class CLITest < Minitest::Test
162
176
  assert_equal 2, code
163
177
  assert_match(/config file not found/, err.string)
164
178
  end
179
+
180
+ def test_parse_double_dash_stops_options
181
+ opts = RuVim::CLI.parse(["--", "-R", "--help"])
182
+ assert_equal ["-R", "--help"], opts.files
183
+ assert_equal false, opts.readonly
184
+ assert_equal false, opts.show_help
185
+ end
186
+
187
+ def test_parse_unknown_option_raises
188
+ assert_raises(RuVim::CLI::ParseError) do
189
+ RuVim::CLI.parse(["--unknown-flag"])
190
+ end
191
+ end
192
+
193
+ def test_parse_startuptime_missing_arg_raises
194
+ assert_raises(RuVim::CLI::ParseError) do
195
+ RuVim::CLI.parse(["--startuptime"])
196
+ end
197
+ end
198
+
199
+ def test_parse_cmd_missing_arg_raises
200
+ assert_raises(RuVim::CLI::ParseError) do
201
+ RuVim::CLI.parse(["--cmd"])
202
+ end
203
+ end
204
+
205
+ def test_parse_q_missing_arg_raises
206
+ assert_raises(RuVim::CLI::ParseError) do
207
+ RuVim::CLI.parse(["-q"])
208
+ end
209
+ end
210
+
211
+ def test_parse_u_missing_arg_raises
212
+ assert_raises(RuVim::CLI::ParseError) do
213
+ RuVim::CLI.parse(["-u"])
214
+ end
215
+ end
216
+
217
+ def test_parse_c_missing_arg_raises
218
+ assert_raises(RuVim::CLI::ParseError) do
219
+ RuVim::CLI.parse(["-c"])
220
+ end
221
+ end
222
+
223
+ def test_parse_combined_u_value
224
+ opts = RuVim::CLI.parse(["-u/tmp/myrc.rb"])
225
+ assert_equal "/tmp/myrc.rb", opts.config_path
226
+ end
227
+
228
+ def test_parse_verbose_without_level
229
+ opts = RuVim::CLI.parse(["--verbose"])
230
+ assert_equal 1, opts.verbose_level
231
+ end
232
+
233
+ def test_parse_multiple_files
234
+ opts = RuVim::CLI.parse(["a.txt", "b.txt", "c.txt"])
235
+ assert_equal ["a.txt", "b.txt", "c.txt"], opts.files
236
+ end
237
+
238
+ def test_parse_plus_ex_command
239
+ opts = RuVim::CLI.parse(["+set number", "file.txt"])
240
+ assert_equal [{ type: :ex, value: "set number" }], opts.startup_actions
241
+ end
165
242
  end