ruvim 0.1.0 → 0.3.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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +4 -0
  3. data/AGENTS.md +84 -0
  4. data/CLAUDE.md +1 -0
  5. data/docs/binding.md +29 -0
  6. data/docs/command.md +101 -0
  7. data/docs/config.md +203 -84
  8. data/docs/done.md +21 -0
  9. data/docs/lib_cleanup_report.md +79 -0
  10. data/docs/plugin.md +13 -15
  11. data/docs/spec.md +195 -33
  12. data/docs/todo.md +183 -10
  13. data/docs/tutorial.md +1 -1
  14. data/docs/vim_diff.md +94 -171
  15. data/lib/ruvim/app.rb +1543 -172
  16. data/lib/ruvim/buffer.rb +35 -1
  17. data/lib/ruvim/cli.rb +12 -3
  18. data/lib/ruvim/clipboard.rb +2 -0
  19. data/lib/ruvim/command_invocation.rb +3 -1
  20. data/lib/ruvim/command_line.rb +2 -0
  21. data/lib/ruvim/command_registry.rb +2 -0
  22. data/lib/ruvim/config_dsl.rb +2 -0
  23. data/lib/ruvim/config_loader.rb +21 -5
  24. data/lib/ruvim/context.rb +2 -7
  25. data/lib/ruvim/dispatcher.rb +153 -13
  26. data/lib/ruvim/display_width.rb +28 -2
  27. data/lib/ruvim/editor.rb +622 -69
  28. data/lib/ruvim/ex_command_registry.rb +2 -0
  29. data/lib/ruvim/global_commands.rb +1386 -114
  30. data/lib/ruvim/highlighter.rb +16 -21
  31. data/lib/ruvim/input.rb +52 -29
  32. data/lib/ruvim/keymap_manager.rb +83 -0
  33. data/lib/ruvim/keyword_chars.rb +48 -0
  34. data/lib/ruvim/lang/base.rb +25 -0
  35. data/lib/ruvim/lang/csv.rb +18 -0
  36. data/lib/ruvim/lang/json.rb +18 -0
  37. data/lib/ruvim/lang/markdown.rb +170 -0
  38. data/lib/ruvim/lang/ruby.rb +236 -0
  39. data/lib/ruvim/lang/scheme.rb +44 -0
  40. data/lib/ruvim/lang/tsv.rb +19 -0
  41. data/lib/ruvim/rich_view/markdown_renderer.rb +248 -0
  42. data/lib/ruvim/rich_view/table_renderer.rb +176 -0
  43. data/lib/ruvim/rich_view.rb +93 -0
  44. data/lib/ruvim/screen.rb +851 -119
  45. data/lib/ruvim/terminal.rb +18 -1
  46. data/lib/ruvim/text_metrics.rb +28 -0
  47. data/lib/ruvim/version.rb +2 -2
  48. data/lib/ruvim/window.rb +37 -10
  49. data/lib/ruvim.rb +15 -0
  50. data/test/app_completion_test.rb +174 -0
  51. data/test/app_dot_repeat_test.rb +13 -0
  52. data/test/app_motion_test.rb +110 -2
  53. data/test/app_scenario_test.rb +998 -0
  54. data/test/app_startup_test.rb +197 -0
  55. data/test/arglist_test.rb +113 -0
  56. data/test/buffer_test.rb +49 -30
  57. data/test/config_loader_test.rb +37 -0
  58. data/test/dispatcher_test.rb +438 -0
  59. data/test/display_width_test.rb +18 -0
  60. data/test/editor_register_test.rb +23 -0
  61. data/test/fixtures/render_basic_snapshot.txt +7 -8
  62. data/test/fixtures/render_basic_snapshot_nonumber.txt +1 -2
  63. data/test/fixtures/render_unicode_scrolled_snapshot.txt +6 -7
  64. data/test/highlighter_test.rb +121 -0
  65. data/test/indent_test.rb +201 -0
  66. data/test/input_screen_integration_test.rb +65 -14
  67. data/test/markdown_renderer_test.rb +279 -0
  68. data/test/on_save_hook_test.rb +150 -0
  69. data/test/rich_view_test.rb +478 -0
  70. data/test/screen_test.rb +470 -0
  71. data/test/window_test.rb +26 -0
  72. metadata +37 -2
@@ -1,4 +1,5 @@
1
1
  require_relative "test_helper"
2
+ require "tmpdir"
2
3
 
3
4
  class DispatcherTest < Minitest::Test
4
5
  def setup
@@ -32,6 +33,53 @@ class DispatcherTest < Minitest::Test
32
33
  assert_includes body, "Write current buffer"
33
34
  end
34
35
 
36
+ def test_dispatch_ex_bindings_lists_buffer_filetype_and_app_layers
37
+ keymaps = @app.instance_variable_get(:@keymaps)
38
+ @editor.current_buffer.options["filetype"] = "ruby"
39
+ keymaps.bind_buffer(@editor.current_buffer.id, "Q", "ui.clear_message")
40
+ keymaps.bind_filetype("ruby", "K", "cursor.up", mode: :normal)
41
+
42
+ @dispatcher.dispatch_ex(@editor, "bindings")
43
+
44
+ assert_equal "[Bindings]", @editor.message
45
+ assert_equal :help, @editor.current_buffer.kind
46
+ body = @editor.current_buffer.lines.join("\n")
47
+ assert_includes body, "Layer: buffer"
48
+ assert_includes body, "Layer: filetype"
49
+ assert_includes body, "Layer: app"
50
+ assert_operator body.index("Layer: buffer"), :<, body.index("Layer: filetype")
51
+ assert_operator body.index("Layer: filetype"), :<, body.index("Layer: app")
52
+ assert_includes body, "Q"
53
+ assert_includes body, "K"
54
+ assert_includes body, "gg"
55
+ assert_includes body, "f"
56
+ assert_includes body, "normal.find_char_forward_start"
57
+ assert_includes body, "Move to start of buffer"
58
+ assert_includes body, "Start char find forward"
59
+ assert_includes body, "Clear message"
60
+ end
61
+
62
+ def test_dispatch_ex_bindings_sort_command_sorts_within_group_by_command_id
63
+ keymaps = @app.instance_variable_get(:@keymaps)
64
+ keymaps.bind_buffer(@editor.current_buffer.id, "K", "ui.clear_message")
65
+ keymaps.bind_buffer(@editor.current_buffer.id, "Q", "cursor.up")
66
+
67
+ @dispatcher.dispatch_ex(@editor, "bindings sort=command")
68
+
69
+ body = @editor.current_buffer.lines.join("\n")
70
+ assert_includes body, "Sort: command"
71
+
72
+ buffer_section = body.split("Layer: buffer", 2).last
73
+ refute_nil buffer_section
74
+ buffer_section = buffer_section.split("Layer: app", 2).first.to_s
75
+
76
+ up_idx = buffer_section.index("cursor.up")
77
+ clear_idx = buffer_section.index("ui.clear_message")
78
+ refute_nil up_idx
79
+ refute_nil clear_idx
80
+ assert_operator up_idx, :<, clear_idx
81
+ end
82
+
35
83
  def test_dispatch_ex_command_and_ruby
36
84
  @dispatcher.dispatch_ex(@editor, "command Hi help")
37
85
  assert_equal "Defined :Hi", @editor.message
@@ -43,6 +91,49 @@ class DispatcherTest < Minitest::Test
43
91
  assert_equal "ruby: 3", @editor.message
44
92
  end
45
93
 
94
+ def test_dispatch_ex_commands_shows_description_and_bound_keys
95
+ keymaps = @app.instance_variable_get(:@keymaps)
96
+ keymaps.bind(:normal, "K", "editor.buffer_next")
97
+
98
+ @dispatcher.dispatch_ex(@editor, "commands")
99
+
100
+ assert_equal "[Commands]", @editor.message
101
+ assert_equal :help, @editor.current_buffer.kind
102
+ body = @editor.current_buffer.lines.join("\n")
103
+ assert_includes body, "bnext"
104
+ assert_includes body, "Next buffer"
105
+ assert_includes body, "keys: K"
106
+ end
107
+
108
+ def test_dispatch_ex_ruby_captures_stdout_and_stderr_into_virtual_buffer
109
+ @dispatcher.dispatch_ex(@editor, "ruby STDOUT.puts(%q[out]); STDERR.puts(%q[err]); 42")
110
+
111
+ assert_equal "[Ruby Output]", @editor.message
112
+ assert_equal :help, @editor.current_buffer.kind
113
+ body = @editor.current_buffer.lines.join("\n")
114
+ assert_includes body, "[stdout]"
115
+ assert_includes body, "out"
116
+ assert_includes body, "[stderr]"
117
+ assert_includes body, "err"
118
+ assert_includes body, "[result]"
119
+ assert_includes body, "42"
120
+ end
121
+
122
+ def test_dispatch_ex_shell_captures_stdout_and_stderr_into_virtual_buffer
123
+ @dispatcher.dispatch_ex(@editor, "!echo out; echo err 1>&2")
124
+
125
+ assert_equal "[Shell Output]", @editor.message
126
+ assert_equal :help, @editor.current_buffer.kind
127
+ body = @editor.current_buffer.lines.join("\n")
128
+ assert_includes body, "[command]"
129
+ assert_includes body, "echo out; echo err 1>&2"
130
+ assert_includes body, "[stdout]"
131
+ assert_includes body, "out"
132
+ assert_includes body, "[stderr]"
133
+ assert_includes body, "err"
134
+ assert_includes body, "[status]"
135
+ end
136
+
46
137
  def test_dispatch_ex_set_commands
47
138
  @dispatcher.dispatch_ex(@editor, "set number")
48
139
  assert_equal true, @editor.current_window.options["number"]
@@ -121,4 +212,351 @@ class DispatcherTest < Minitest::Test
121
212
  @dispatcher.dispatch_ex(@editor, "lnext")
122
213
  assert_equal 1, @editor.current_window.cursor_y
123
214
  end
215
+
216
+ def test_hidden_option_allows_buffer_switch_without_bang
217
+ @editor.materialize_intro_buffer!
218
+ @editor.current_buffer.replace_all_lines!(["x"])
219
+ @editor.current_buffer.modified = true
220
+ other = @editor.add_empty_buffer(path: "other.txt")
221
+ @editor.set_option("hidden", true, scope: :global)
222
+
223
+ @dispatcher.dispatch_ex(@editor, "buffer #{other.id}")
224
+
225
+ assert_equal other.id, @editor.current_buffer.id
226
+ refute @editor.message_error?
227
+ end
228
+
229
+ def test_autowrite_saves_current_buffer_before_buffer_switch
230
+ Dir.mktmpdir("ruvim-autowrite") do |dir|
231
+ path = File.join(dir, "a.txt")
232
+ File.write(path, "old\n")
233
+ @editor.materialize_intro_buffer!
234
+ @editor.current_buffer.path = path
235
+ @editor.current_buffer.replace_all_lines!(["new"])
236
+ @editor.current_buffer.modified = true
237
+ other = @editor.add_empty_buffer(path: "other.txt")
238
+ @editor.set_option("autowrite", true, scope: :global)
239
+
240
+ @dispatcher.dispatch_ex(@editor, "buffer #{other.id}")
241
+
242
+ assert_equal other.id, @editor.current_buffer.id
243
+ assert_equal "new", File.read(path).strip
244
+ end
245
+ end
246
+
247
+ def test_bdelete_deletes_current_buffer_and_switches_to_another
248
+ @editor.materialize_intro_buffer!
249
+ first = @editor.current_buffer
250
+ other = @editor.add_empty_buffer(path: "other.txt")
251
+ @dispatcher.dispatch_ex(@editor, "buffer #{other.id}")
252
+ assert_equal other.id, @editor.current_buffer.id
253
+
254
+ @dispatcher.dispatch_ex(@editor, "bd")
255
+
256
+ assert_equal first.id, @editor.current_buffer.id
257
+ refute @editor.buffers.key?(other.id)
258
+ assert_equal "buffer #{other.id} deleted", @editor.message
259
+ end
260
+
261
+ def test_bdelete_rejects_modified_buffer_without_bang
262
+ @editor.materialize_intro_buffer!
263
+ @editor.current_buffer.replace_all_lines!(["x"])
264
+ @editor.current_buffer.modified = true
265
+
266
+ @dispatcher.dispatch_ex(@editor, "bd")
267
+
268
+ assert_equal true, @editor.message_error?
269
+ assert_match(/No write since last change/, @editor.message)
270
+ assert @editor.buffers.key?(@editor.current_buffer.id)
271
+ end
272
+
273
+ def test_bdelete_bang_deletes_modified_buffer
274
+ @editor.materialize_intro_buffer!
275
+ first = @editor.current_buffer
276
+ other = @editor.add_empty_buffer(path: "other.txt")
277
+ @dispatcher.dispatch_ex(@editor, "buffer #{other.id}")
278
+ @editor.current_buffer.replace_all_lines!(["dirty"])
279
+ @editor.current_buffer.modified = true
280
+
281
+ @dispatcher.dispatch_ex(@editor, "bd!")
282
+
283
+ assert_equal first.id, @editor.current_buffer.id
284
+ refute @editor.buffers.key?(other.id)
285
+ refute @editor.message_error?
286
+ end
287
+
288
+ def test_tabs_lists_all_tabpages
289
+ @editor.tabnew(path: nil)
290
+ @editor.tabnew(path: nil)
291
+ assert_equal 3, @editor.tabpage_count
292
+
293
+ @dispatcher.dispatch_ex(@editor, "tabs")
294
+
295
+ lines = @editor.hit_enter_lines
296
+ refute_nil lines, "tabs should produce multiline output"
297
+ joined = lines.join("\n")
298
+ assert_includes joined, "Tab page 1"
299
+ assert_includes joined, "Tab page 2"
300
+ assert_includes joined, "Tab page 3"
301
+ end
302
+
303
+ def test_splitbelow_and_splitright_change_insertion_side
304
+ @editor.set_option("splitbelow", false, scope: :global)
305
+ first = @editor.current_window_id
306
+ @dispatcher.dispatch_ex(@editor, "split")
307
+ assert_equal @editor.window_order[0], @editor.current_window_id
308
+ assert_equal first, @editor.window_order[1]
309
+
310
+ @editor.set_option("splitright", false, scope: :global)
311
+ @editor.current_window_id = first
312
+ @dispatcher.dispatch_ex(@editor, "vsplit")
313
+ idx = @editor.window_order.index(@editor.current_window_id)
314
+ assert_equal 1, idx
315
+ end
316
+
317
+ # --- Range parser tests ---
318
+
319
+ def test_parse_range_percent
320
+ @editor.materialize_intro_buffer!
321
+ @editor.current_buffer.replace_all_lines!(["a", "b", "c", "d", "e"])
322
+ result = @dispatcher.parse_range("%", @editor)
323
+ assert_equal 0, result[:range_start]
324
+ assert_equal 4, result[:range_end]
325
+ assert_equal "", result[:rest]
326
+ end
327
+
328
+ def test_parse_range_numeric_pair
329
+ @editor.materialize_intro_buffer!
330
+ @editor.current_buffer.replace_all_lines!(["a", "b", "c", "d", "e"])
331
+ result = @dispatcher.parse_range("1,5", @editor)
332
+ assert_equal 0, result[:range_start]
333
+ assert_equal 4, result[:range_end]
334
+ assert_equal "", result[:rest]
335
+ end
336
+
337
+ def test_parse_range_dot_and_dollar
338
+ @editor.materialize_intro_buffer!
339
+ @editor.current_buffer.replace_all_lines!(["a", "b", "c", "d", "e"])
340
+ @editor.current_window.cursor_y = 2
341
+
342
+ result = @dispatcher.parse_range(".,$", @editor)
343
+ assert_equal 2, result[:range_start]
344
+ assert_equal 4, result[:range_end]
345
+ assert_equal "", result[:rest]
346
+ end
347
+
348
+ def test_parse_range_with_offset
349
+ @editor.materialize_intro_buffer!
350
+ @editor.current_buffer.replace_all_lines!(["a", "b", "c", "d", "e"])
351
+ @editor.current_window.cursor_y = 1
352
+
353
+ result = @dispatcher.parse_range(".+2,$-1", @editor)
354
+ assert_equal 3, result[:range_start]
355
+ assert_equal 3, result[:range_end]
356
+ assert_equal "", result[:rest]
357
+ end
358
+
359
+ def test_parse_range_single_address
360
+ @editor.materialize_intro_buffer!
361
+ @editor.current_buffer.replace_all_lines!(["a", "b", "c"])
362
+ result = @dispatcher.parse_range("2", @editor)
363
+ assert_equal 1, result[:range_start]
364
+ assert_equal 1, result[:range_end]
365
+ assert_equal "", result[:rest]
366
+ end
367
+
368
+ def test_parse_range_returns_nil_for_no_range
369
+ @editor.materialize_intro_buffer!
370
+ result = @dispatcher.parse_range("help", @editor)
371
+ assert_nil result
372
+ end
373
+
374
+ def test_parse_range_with_rest
375
+ @editor.materialize_intro_buffer!
376
+ @editor.current_buffer.replace_all_lines!(["a", "b", "c", "d", "e"])
377
+ result = @dispatcher.parse_range("%s/a/b/g", @editor)
378
+ assert_equal 0, result[:range_start]
379
+ assert_equal 4, result[:range_end]
380
+ assert_equal "s/a/b/g", result[:rest]
381
+ end
382
+
383
+ def test_parse_range_mark_address
384
+ @editor.materialize_intro_buffer!
385
+ @editor.current_buffer.replace_all_lines!(["a", "b", "c", "d", "e"])
386
+ @editor.current_window.cursor_y = 1
387
+ @editor.set_mark("a")
388
+ @editor.current_window.cursor_y = 3
389
+ @editor.set_mark("b")
390
+
391
+ result = @dispatcher.parse_range("'a,'b", @editor)
392
+ assert_equal 1, result[:range_start]
393
+ assert_equal 3, result[:range_end]
394
+ assert_equal "", result[:rest]
395
+ end
396
+
397
+ # --- Substitute parser tests ---
398
+
399
+ def test_parse_substitute_basic
400
+ result = @dispatcher.parse_substitute("s/foo/bar/")
401
+ assert_equal "foo", result[:pattern]
402
+ assert_equal "bar", result[:replacement]
403
+ assert_equal "", result[:flags_str]
404
+ end
405
+
406
+ def test_parse_substitute_with_flags
407
+ result = @dispatcher.parse_substitute("s/foo/bar/gi")
408
+ assert_equal "foo", result[:pattern]
409
+ assert_equal "bar", result[:replacement]
410
+ assert_equal "gi", result[:flags_str]
411
+ end
412
+
413
+ def test_parse_substitute_returns_nil_for_non_substitute
414
+ assert_nil @dispatcher.parse_substitute("help")
415
+ assert_nil @dispatcher.parse_substitute("set number")
416
+ end
417
+
418
+ # --- Substitute with range integration ---
419
+
420
+ def test_substitute_with_range
421
+ @editor.materialize_intro_buffer!
422
+ @editor.current_buffer.replace_all_lines!(["foo", "foo", "foo", "foo", "foo"])
423
+
424
+ @dispatcher.dispatch_ex(@editor, "1,3s/foo/bar/")
425
+
426
+ assert_equal "bar", @editor.current_buffer.line_at(0)
427
+ assert_equal "bar", @editor.current_buffer.line_at(1)
428
+ assert_equal "bar", @editor.current_buffer.line_at(2)
429
+ assert_equal "foo", @editor.current_buffer.line_at(3)
430
+ assert_equal "foo", @editor.current_buffer.line_at(4)
431
+ end
432
+
433
+ def test_substitute_percent_range
434
+ @editor.materialize_intro_buffer!
435
+ @editor.current_buffer.replace_all_lines!(["foo", "bar", "foo"])
436
+
437
+ @dispatcher.dispatch_ex(@editor, "%s/foo/baz/")
438
+
439
+ assert_equal "baz", @editor.current_buffer.line_at(0)
440
+ assert_equal "bar", @editor.current_buffer.line_at(1)
441
+ assert_equal "baz", @editor.current_buffer.line_at(2)
442
+ end
443
+
444
+ def test_substitute_ignore_case_flag
445
+ @editor.materialize_intro_buffer!
446
+ @editor.current_buffer.replace_all_lines!(["FOO", "foo", "Foo"])
447
+
448
+ @dispatcher.dispatch_ex(@editor, "%s/foo/bar/gi")
449
+
450
+ assert_equal "bar", @editor.current_buffer.line_at(0)
451
+ assert_equal "bar", @editor.current_buffer.line_at(1)
452
+ assert_equal "bar", @editor.current_buffer.line_at(2)
453
+ end
454
+
455
+ def test_substitute_count_only_flag
456
+ @editor.materialize_intro_buffer!
457
+ @editor.current_buffer.replace_all_lines!(["foo", "bar", "foo baz foo"])
458
+
459
+ @dispatcher.dispatch_ex(@editor, "%s/foo/x/gn")
460
+
461
+ # n flag: count only, no changes
462
+ assert_equal "foo", @editor.current_buffer.line_at(0)
463
+ assert_equal "foo baz foo", @editor.current_buffer.line_at(2)
464
+ assert_match(/3 match/, @editor.message)
465
+ end
466
+
467
+ def test_substitute_no_range_defaults_to_whole_buffer
468
+ @editor.materialize_intro_buffer!
469
+ @editor.current_buffer.replace_all_lines!(["aaa", "bbb", "aaa"])
470
+
471
+ @dispatcher.dispatch_ex(@editor, "s/aaa/zzz/")
472
+
473
+ assert_equal "zzz", @editor.current_buffer.line_at(0)
474
+ assert_equal "bbb", @editor.current_buffer.line_at(1)
475
+ assert_equal "zzz", @editor.current_buffer.line_at(2)
476
+ end
477
+
478
+ # --- :d (delete lines) tests ---
479
+
480
+ def test_delete_lines_with_range
481
+ @editor.materialize_intro_buffer!
482
+ @editor.current_buffer.replace_all_lines!(["a", "b", "c", "d", "e"])
483
+
484
+ @dispatcher.dispatch_ex(@editor, "2,4d")
485
+
486
+ assert_equal 2, @editor.current_buffer.line_count
487
+ assert_equal "a", @editor.current_buffer.line_at(0)
488
+ assert_equal "e", @editor.current_buffer.line_at(1)
489
+ assert_match(/3 line/, @editor.message)
490
+ end
491
+
492
+ def test_delete_lines_without_range_deletes_current
493
+ @editor.materialize_intro_buffer!
494
+ @editor.current_buffer.replace_all_lines!(["a", "b", "c"])
495
+ @editor.current_window.cursor_y = 1
496
+
497
+ @dispatcher.dispatch_ex(@editor, "d")
498
+
499
+ assert_equal 2, @editor.current_buffer.line_count
500
+ assert_equal "a", @editor.current_buffer.line_at(0)
501
+ assert_equal "c", @editor.current_buffer.line_at(1)
502
+ end
503
+
504
+ # --- :y (yank lines) tests ---
505
+
506
+ def test_yank_lines_with_range
507
+ @editor.materialize_intro_buffer!
508
+ @editor.current_buffer.replace_all_lines!(["a", "b", "c", "d", "e"])
509
+
510
+ @dispatcher.dispatch_ex(@editor, "1,3y")
511
+
512
+ assert_match(/3 line/, @editor.message)
513
+ # Buffer unchanged
514
+ assert_equal 5, @editor.current_buffer.line_count
515
+ end
516
+
517
+ def test_yank_lines_without_range_yanks_current
518
+ @editor.materialize_intro_buffer!
519
+ @editor.current_buffer.replace_all_lines!(["a", "b", "c"])
520
+ @editor.current_window.cursor_y = 1
521
+
522
+ @dispatcher.dispatch_ex(@editor, "y")
523
+
524
+ assert_match(/1 line/, @editor.message)
525
+ assert_equal 3, @editor.current_buffer.line_count
526
+ end
527
+
528
+ # --- :grep tests ---
529
+
530
+ def test_grep_populates_quickfix
531
+ Dir.mktmpdir("ruvim-grep") do |dir|
532
+ File.write(File.join(dir, "a.txt"), "hello world\ngoodbye\n")
533
+ File.write(File.join(dir, "b.txt"), "hello again\n")
534
+
535
+ @editor.materialize_intro_buffer!
536
+ @dispatcher.dispatch_ex(@editor, "grep hello #{File.join(dir, '*.txt')}")
537
+
538
+ assert_operator @editor.quickfix_items.length, :>=, 2
539
+ refute @editor.message_error?
540
+ end
541
+ end
542
+
543
+ def test_grep_no_matches_shows_error
544
+ @editor.materialize_intro_buffer!
545
+ @dispatcher.dispatch_ex(@editor, "grep ZZZZUNMATCHABLE /dev/null")
546
+
547
+ assert @editor.message_error?
548
+ end
549
+
550
+ def test_lgrep_populates_location_list
551
+ Dir.mktmpdir("ruvim-lgrep") do |dir|
552
+ File.write(File.join(dir, "c.txt"), "alpha\nbeta\nalpha\n")
553
+
554
+ @editor.materialize_intro_buffer!
555
+ wid = @editor.current_window_id
556
+ @dispatcher.dispatch_ex(@editor, "lgrep alpha #{File.join(dir, 'c.txt')}")
557
+
558
+ assert_operator @editor.location_items(wid).length, :>=, 2
559
+ refute @editor.message_error?
560
+ end
561
+ end
124
562
  end
@@ -0,0 +1,18 @@
1
+ require_relative "test_helper"
2
+
3
+ class DisplayWidthTest < Minitest::Test
4
+ def test_ambiguous_width_cache_tracks_env_changes
5
+ prev = ENV["RUVIM_AMBIGUOUS_WIDTH"]
6
+ ENV["RUVIM_AMBIGUOUS_WIDTH"] = nil
7
+ assert_equal 1, RuVim::DisplayWidth.cell_width("Ω")
8
+
9
+ ENV["RUVIM_AMBIGUOUS_WIDTH"] = "2"
10
+ assert_equal 2, RuVim::DisplayWidth.cell_width("Ω")
11
+ ensure
12
+ if prev.nil?
13
+ ENV.delete("RUVIM_AMBIGUOUS_WIDTH")
14
+ else
15
+ ENV["RUVIM_AMBIGUOUS_WIDTH"] = prev
16
+ end
17
+ end
18
+ end
@@ -1,4 +1,5 @@
1
1
  require_relative "test_helper"
2
+ require "tmpdir"
2
3
 
3
4
  class EditorRegisterTest < Minitest::Test
4
5
  def test_named_register_and_append_register
@@ -61,4 +62,26 @@ class EditorRegisterTest < Minitest::Test
61
62
  buffer = editor.add_empty_buffer(path: "/tmp/example.rb")
62
63
  assert_equal "ruby", buffer.options["filetype"]
63
64
  end
65
+
66
+ def test_detect_filetype_from_shebang_when_extension_is_missing
67
+ editor = RuVim::Editor.new
68
+ Dir.mktmpdir("ruvim-shebang-ft") do |dir|
69
+ path = File.join(dir, "script")
70
+ File.binwrite(path, "#!/usr/bin/env ruby\nputs :ok\n")
71
+
72
+ buffer = editor.add_buffer_from_file(path)
73
+ assert_equal "ruby", buffer.options["filetype"]
74
+ end
75
+ end
76
+
77
+ def test_detect_filetype_from_env_shebang_with_dash_s
78
+ editor = RuVim::Editor.new
79
+ Dir.mktmpdir("ruvim-shebang-envs") do |dir|
80
+ path = File.join(dir, "tool")
81
+ File.binwrite(path, "#!/usr/bin/env -S python3 -u\nprint('ok')\n")
82
+
83
+ buffer = editor.add_buffer_from_file(path)
84
+ assert_equal "python", buffer.options["filetype"]
85
+ end
86
+ end
64
87
  end
@@ -1,8 +1,7 @@
1
- 1 # title
2
- 2
3
- 3 foo
4
- 4 bar 日本語 編集
5
- 5 baz
6
- ~
7
- ~
8
- -- NORMAL -- t1/1 w 1:1
1
+ 1 # title
2
+ 2
3
+ 3 foo
4
+ 4 bar 日本語 編集
5
+ 5 baz
6
+ ~
7
+ -- NORMAL -- [No Na 1:1
@@ -4,5 +4,4 @@ foo
4
4
  bar 日本語 編集
5
5
  baz
6
6
  ~
7
- ~
8
- -- NORMAL -- t1/1 w 1:1
7
+ -- NORMAL -- [No Na 1:1
@@ -1,7 +1,6 @@
1
- 1 # title
2
- 2
3
- 3 foo
4
- 4 bar 日本語 編集
5
- 5 baz
6
- 6 qux
7
- -- NORMAL -- t1 4:5
1
+ 1 # title
2
+ 2
3
+ 3 foo
4
+ 4 bar 日本語 編集
5
+ 5 baz
6
+ -- NORMAL -- [N 4:5
@@ -13,4 +13,125 @@ class HighlighterTest < Minitest::Test
13
13
  assert_equal "\e[36m", cols[1] # key chars
14
14
  assert_equal "\e[33m", cols[6] # number start
15
15
  end
16
+
17
+ def test_ruby_highlighter_marks_instance_variables_and_constants
18
+ cols = RuVim::Highlighter.color_columns("ruby", "@x = Foo")
19
+ assert_equal "\e[93m", cols[0] # @x
20
+ assert_equal "\e[96m", cols[5] # F
21
+ end
22
+
23
+ # --- Markdown ---
24
+
25
+ def test_markdown_heading_h1
26
+ cols = RuVim::Highlighter.color_columns("markdown", "# Hello")
27
+ refute_empty cols
28
+ assert_equal "\e[1;33m", cols[0] # bold yellow for H1
29
+ assert_equal "\e[1;33m", cols[6] # entire line colored
30
+ end
31
+
32
+ def test_markdown_heading_h2
33
+ cols = RuVim::Highlighter.color_columns("markdown", "## Section")
34
+ assert_equal "\e[1;36m", cols[0] # bold cyan for H2
35
+ end
36
+
37
+ def test_markdown_heading_h3_to_h6
38
+ colors = {
39
+ 3 => "\e[1;32m",
40
+ 4 => "\e[1;35m",
41
+ 5 => "\e[1;34m",
42
+ 6 => "\e[1;90m"
43
+ }
44
+ colors.each do |level, expected_color|
45
+ line = "#{"#" * level} Title"
46
+ cols = RuVim::Highlighter.color_columns("markdown", line)
47
+ assert_equal expected_color, cols[0], "H#{level} should use correct color"
48
+ end
49
+ end
50
+
51
+ def test_markdown_fence_line
52
+ cols = RuVim::Highlighter.color_columns("markdown", "```ruby")
53
+ refute_empty cols
54
+ assert_equal "\e[90m", cols[0] # dim
55
+ end
56
+
57
+ def test_markdown_hr
58
+ cols = RuVim::Highlighter.color_columns("markdown", "---")
59
+ refute_empty cols
60
+ assert_equal "\e[90m", cols[0] # dim
61
+ end
62
+
63
+ def test_markdown_block_quote_marker
64
+ cols = RuVim::Highlighter.color_columns("markdown", "> quoted text")
65
+ assert_equal "\e[36m", cols[0] # cyan for >
66
+ end
67
+
68
+ def test_markdown_inline_bold
69
+ cols = RuVim::Highlighter.color_columns("markdown", "hello **bold** world")
70
+ # ** markers and content should be bold
71
+ assert_equal "\e[1m", cols[6] # first *
72
+ assert_equal "\e[1m", cols[13] # last *
73
+ end
74
+
75
+ def test_markdown_inline_code
76
+ cols = RuVim::Highlighter.color_columns("markdown", "use `foo()` here")
77
+ assert_equal "\e[33m", cols[4] # backtick
78
+ end
79
+
80
+ def test_markdown_empty_line
81
+ cols = RuVim::Highlighter.color_columns("markdown", "")
82
+ assert_empty cols
83
+ end
84
+
85
+ def test_markdown_plain_text
86
+ cols = RuVim::Highlighter.color_columns("markdown", "plain text")
87
+ assert_empty cols
88
+ end
89
+
90
+ # --- Scheme ---
91
+
92
+ def test_scheme_keyword_define
93
+ cols = RuVim::Highlighter.color_columns("scheme", "(define x 42)")
94
+ assert_equal "\e[36m", cols[1] # "define" keyword
95
+ assert_equal "\e[36m", cols[6] # end of "define"
96
+ end
97
+
98
+ def test_scheme_keyword_lambda
99
+ cols = RuVim::Highlighter.color_columns("scheme", "(lambda (x) x)")
100
+ assert_equal "\e[36m", cols[1] # "lambda"
101
+ end
102
+
103
+ def test_scheme_string
104
+ cols = RuVim::Highlighter.color_columns("scheme", '(display "hello")')
105
+ assert_equal "\e[32m", cols[9] # opening quote
106
+ assert_equal "\e[32m", cols[15] # closing quote
107
+ end
108
+
109
+ def test_scheme_number
110
+ cols = RuVim::Highlighter.color_columns("scheme", "(+ 1 2.5)")
111
+ assert_equal "\e[33m", cols[3] # "1"
112
+ assert_equal "\e[33m", cols[5] # "2"
113
+ end
114
+
115
+ def test_scheme_boolean
116
+ cols = RuVim::Highlighter.color_columns("scheme", "(if #t #f)")
117
+ assert_equal "\e[35m", cols[4] # "#t"
118
+ assert_equal "\e[35m", cols[7] # "#f"
119
+ end
120
+
121
+ def test_scheme_comment
122
+ cols = RuVim::Highlighter.color_columns("scheme", "; this is a comment")
123
+ assert_equal "\e[90m", cols[0] # ";"
124
+ assert_equal "\e[90m", cols[18] # end of comment
125
+ end
126
+
127
+ def test_scheme_char_literal
128
+ cols = RuVim::Highlighter.color_columns("scheme", '#\a #\space')
129
+ assert_equal "\e[32m", cols[0] # "#\a"
130
+ assert_equal "\e[32m", cols[4] # "#\space"
131
+ end
132
+
133
+ def test_scheme_empty_line
134
+ cols = RuVim::Highlighter.color_columns("scheme", "")
135
+ assert_empty cols
136
+ end
16
137
  end