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
@@ -0,0 +1,67 @@
1
+ require_relative "test_helper"
2
+
3
+ class ClipboardTest < Minitest::Test
4
+ def setup
5
+ RuVim::Clipboard.reset_backend!
6
+ end
7
+
8
+ def teardown
9
+ RuVim::Clipboard.reset_backend!
10
+ end
11
+
12
+ def test_available_returns_true_when_backend_exists
13
+ RuVim::Clipboard.instance_variable_set(:@backend, { write: %w[echo], read: %w[echo] })
14
+ assert RuVim::Clipboard.available?
15
+ end
16
+
17
+ def test_available_returns_false_when_no_backend
18
+ RuVim::Clipboard.instance_variable_set(:@backend, false)
19
+ refute RuVim::Clipboard.available?
20
+ end
21
+
22
+ def test_read_returns_nil_when_no_read_cmd
23
+ RuVim::Clipboard.instance_variable_set(:@backend, { write: nil, read: nil })
24
+ assert_nil RuVim::Clipboard.read
25
+ end
26
+
27
+ def test_read_returns_output_on_success
28
+ RuVim::Clipboard.instance_variable_set(:@backend, { write: %w[true], read: %w[echo hello] })
29
+ result = RuVim::Clipboard.read
30
+ assert_equal "hello\n", result
31
+ end
32
+
33
+ def test_read_returns_nil_on_command_failure
34
+ RuVim::Clipboard.instance_variable_set(:@backend, { write: %w[true], read: %w[false] })
35
+ assert_nil RuVim::Clipboard.read
36
+ end
37
+
38
+ def test_write_returns_false_when_no_write_cmd
39
+ RuVim::Clipboard.instance_variable_set(:@backend, { write: nil, read: nil })
40
+ refute RuVim::Clipboard.write("test")
41
+ end
42
+
43
+ def test_write_sends_text_to_command
44
+ RuVim::Clipboard.instance_variable_set(:@backend, { write: %w[cat], read: %w[true] })
45
+ assert RuVim::Clipboard.write("hello")
46
+ end
47
+
48
+ def test_pbcopy_backend_format
49
+ expected = { write: %w[pbcopy], read: %w[pbpaste] }
50
+ assert_equal expected, RuVim::Clipboard.pbcopy_backend
51
+ end
52
+
53
+ def test_wayland_backend_format
54
+ expected = { write: %w[wl-copy], read: %w[wl-paste -n] }
55
+ assert_equal expected, RuVim::Clipboard.wayland_backend
56
+ end
57
+
58
+ def test_xclip_backend_format
59
+ expected = { write: %w[xclip -selection clipboard -in], read: %w[xclip -selection clipboard -out] }
60
+ assert_equal expected, RuVim::Clipboard.xclip_backend
61
+ end
62
+
63
+ def test_xsel_backend_format
64
+ expected = { write: %w[xsel --clipboard --input], read: %w[xsel --clipboard --output] }
65
+ assert_equal expected, RuVim::Clipboard.xsel_backend
66
+ end
67
+ end
@@ -0,0 +1,33 @@
1
+ require_relative "test_helper"
2
+
3
+ class CommandInvocationTest < Minitest::Test
4
+ def test_bang_defaults_to_false
5
+ inv = RuVim::CommandInvocation.new(id: "test")
6
+ assert_equal false, inv.bang
7
+ end
8
+
9
+ def test_bang_true
10
+ inv = RuVim::CommandInvocation.new(id: "test", bang: true)
11
+ assert_equal true, inv.bang
12
+ end
13
+
14
+ def test_bang_false
15
+ inv = RuVim::CommandInvocation.new(id: "test", bang: false)
16
+ assert_equal false, inv.bang
17
+ end
18
+
19
+ def test_argv_defaults_to_empty_array
20
+ inv = RuVim::CommandInvocation.new(id: "test")
21
+ assert_equal [], inv.argv
22
+ end
23
+
24
+ def test_kwargs_defaults_to_empty_hash
25
+ inv = RuVim::CommandInvocation.new(id: "test")
26
+ assert_equal({}, inv.kwargs)
27
+ end
28
+
29
+ def test_count_defaults_to_nil
30
+ inv = RuVim::CommandInvocation.new(id: "test")
31
+ assert_nil inv.count
32
+ end
33
+ end
@@ -0,0 +1,118 @@
1
+ require_relative "test_helper"
2
+
3
+ class CommandLineTest < Minitest::Test
4
+ def setup
5
+ @cl = RuVim::CommandLine.new
6
+ end
7
+
8
+ def test_initial_state
9
+ assert_equal ":", @cl.prefix
10
+ assert_equal "", @cl.text
11
+ assert_equal 0, @cl.cursor
12
+ end
13
+
14
+ def test_reset_with_custom_prefix
15
+ @cl.reset(prefix: "/")
16
+ assert_equal "/", @cl.prefix
17
+ assert_equal "", @cl.text
18
+ assert_equal 0, @cl.cursor
19
+ end
20
+
21
+ def test_insert_appends_at_cursor
22
+ @cl.insert("abc")
23
+ assert_equal "abc", @cl.text
24
+ assert_equal 3, @cl.cursor
25
+ end
26
+
27
+ def test_insert_at_middle
28
+ @cl.insert("ac")
29
+ @cl.instance_variable_set(:@cursor, 1)
30
+ @cl.insert("b")
31
+ assert_equal "abc", @cl.text
32
+ assert_equal 2, @cl.cursor
33
+ end
34
+
35
+ def test_backspace_at_zero_does_nothing
36
+ @cl.backspace
37
+ assert_equal "", @cl.text
38
+ assert_equal 0, @cl.cursor
39
+ end
40
+
41
+ def test_backspace_deletes_character
42
+ @cl.insert("abc")
43
+ @cl.backspace
44
+ assert_equal "ab", @cl.text
45
+ assert_equal 2, @cl.cursor
46
+ end
47
+
48
+ def test_move_left
49
+ @cl.insert("abc")
50
+ @cl.move_left
51
+ assert_equal 2, @cl.cursor
52
+ end
53
+
54
+ def test_move_left_at_zero_stays
55
+ @cl.move_left
56
+ assert_equal 0, @cl.cursor
57
+ end
58
+
59
+ def test_move_right
60
+ @cl.insert("abc")
61
+ @cl.instance_variable_set(:@cursor, 1)
62
+ @cl.move_right
63
+ assert_equal 2, @cl.cursor
64
+ end
65
+
66
+ def test_move_right_at_end_stays
67
+ @cl.insert("abc")
68
+ @cl.move_right
69
+ assert_equal 3, @cl.cursor
70
+ end
71
+
72
+ def test_content_includes_prefix
73
+ @cl.insert("hello")
74
+ assert_equal ":hello", @cl.content
75
+ end
76
+
77
+ def test_content_with_custom_prefix
78
+ @cl.reset(prefix: "/")
79
+ @cl.insert("search")
80
+ assert_equal "/search", @cl.content
81
+ end
82
+
83
+ def test_clear_resets_text_and_cursor
84
+ @cl.insert("hello")
85
+ @cl.clear
86
+ assert_equal "", @cl.text
87
+ assert_equal 0, @cl.cursor
88
+ assert_equal ":", @cl.prefix
89
+ end
90
+
91
+ def test_replace_text
92
+ @cl.insert("old")
93
+ @cl.replace_text("new text")
94
+ assert_equal "new text", @cl.text
95
+ assert_equal 8, @cl.cursor
96
+ end
97
+
98
+ def test_replace_span_end_cursor
99
+ @cl.insert("hello world")
100
+ @cl.replace_span(6, 11, "ruby")
101
+ assert_equal "hello ruby", @cl.text
102
+ assert_equal 10, @cl.cursor
103
+ end
104
+
105
+ def test_replace_span_start_cursor
106
+ @cl.insert("hello world")
107
+ @cl.replace_span(6, 11, "ruby", cursor_at: :start)
108
+ assert_equal "hello ruby", @cl.text
109
+ assert_equal 6, @cl.cursor
110
+ end
111
+
112
+ def test_replace_span_integer_cursor
113
+ @cl.insert("hello world")
114
+ @cl.replace_span(6, 11, "ruby", cursor_at: 8)
115
+ assert_equal "hello ruby", @cl.text
116
+ assert_equal 8, @cl.cursor
117
+ end
118
+ end
@@ -75,4 +75,138 @@ class ConfigDSLTest < Minitest::Test
75
75
  assert_equal :match, match.status
76
76
  assert match.invocation.id.start_with?("user.keymap.global.")
77
77
  end
78
+
79
+ def test_nmap_with_command_id_string
80
+ @command_registry.register("test.cmd", call: ->(_ctx, **) {}, desc: "test", source: :builtin)
81
+ @dsl.nmap("T", "test.cmd")
82
+ match = @keymaps.resolve(:normal, ["T"])
83
+ assert_equal :match, match.status
84
+ assert_equal "test.cmd", match.invocation.id
85
+ end
86
+
87
+ def test_nmap_without_command_id_or_block_raises
88
+ # ConfigDSL < BasicObject, so raise becomes NoMethodError for ::ArgumentError
89
+ assert_raises(NoMethodError, ArgumentError) { @dsl.nmap("T") }
90
+ end
91
+
92
+ def test_imap_without_block_or_id_raises
93
+ assert_raises(NoMethodError, ArgumentError) { @dsl.imap("T") }
94
+ end
95
+
96
+ def test_command_registers_user_command
97
+ @dsl.command("my.cmd", desc: "custom") { |_ctx, **| }
98
+ spec = @command_registry.fetch("my.cmd")
99
+ assert_equal :user, spec.source
100
+ assert_equal "custom", spec.desc
101
+ end
102
+
103
+ def test_command_without_block_raises
104
+ assert_raises(NoMethodError, ArgumentError) { @dsl.command("my.cmd") }
105
+ end
106
+
107
+ def test_nmap_with_filetype
108
+ dsl = RuVim::ConfigDSL.new(
109
+ command_registry: @command_registry,
110
+ ex_registry: @ex_registry,
111
+ keymaps: @keymaps,
112
+ command_host: RuVim::GlobalCommands.instance,
113
+ filetype: "ruby"
114
+ )
115
+ dsl.nmap("K", desc: "ft test") { |_ctx, **| }
116
+ editor = fresh_editor
117
+ editor.current_buffer.options["filetype"] = "ruby"
118
+ match = @keymaps.resolve_with_context(:normal, ["K"], editor: editor)
119
+ assert_equal :match, match.status
120
+ end
121
+
122
+ def test_set_option_requires_editor
123
+ assert_raises(NoMethodError, ArgumentError) { @dsl.set("number") }
124
+ end
125
+
126
+ def test_set_boolean_option
127
+ editor = fresh_editor
128
+ dsl = RuVim::ConfigDSL.new(
129
+ command_registry: @command_registry,
130
+ ex_registry: @ex_registry,
131
+ keymaps: @keymaps,
132
+ command_host: RuVim::GlobalCommands.instance,
133
+ editor: editor
134
+ )
135
+ dsl.set("number")
136
+ assert editor.get_option("number")
137
+ end
138
+
139
+ def test_set_no_prefix_disables_option
140
+ editor = fresh_editor
141
+ dsl = RuVim::ConfigDSL.new(
142
+ command_registry: @command_registry,
143
+ ex_registry: @ex_registry,
144
+ keymaps: @keymaps,
145
+ command_host: RuVim::GlobalCommands.instance,
146
+ editor: editor
147
+ )
148
+ dsl.set("number")
149
+ dsl.set("nonumber")
150
+ refute editor.get_option("number")
151
+ end
152
+
153
+ def test_set_with_value
154
+ editor = fresh_editor
155
+ dsl = RuVim::ConfigDSL.new(
156
+ command_registry: @command_registry,
157
+ ex_registry: @ex_registry,
158
+ keymaps: @keymaps,
159
+ command_host: RuVim::GlobalCommands.instance,
160
+ editor: editor
161
+ )
162
+ dsl.set("tabstop=4")
163
+ assert_equal 4, editor.get_option("tabstop")
164
+ end
165
+
166
+ def test_ex_command_registers_and_allows_re_register
167
+ dsl = RuVim::ConfigDSL.new(
168
+ command_registry: @command_registry,
169
+ ex_registry: RuVim::ExCommandRegistry.instance,
170
+ keymaps: @keymaps,
171
+ command_host: RuVim::GlobalCommands.instance
172
+ )
173
+ saved_specs = RuVim::ExCommandRegistry.instance.instance_variable_get(:@specs).dup
174
+ saved_lookup = RuVim::ExCommandRegistry.instance.instance_variable_get(:@lookup).dup
175
+
176
+ dsl.ex_command("testusercmd", desc: "test") { |_ctx, **| }
177
+ assert RuVim::ExCommandRegistry.instance.registered?("testusercmd")
178
+
179
+ # Re-registering should not raise (unregisters first)
180
+ dsl.ex_command("testusercmd", desc: "test v2") { |_ctx, **| }
181
+ assert RuVim::ExCommandRegistry.instance.registered?("testusercmd")
182
+ ensure
183
+ RuVim::ExCommandRegistry.instance.instance_variable_set(:@specs, saved_specs)
184
+ RuVim::ExCommandRegistry.instance.instance_variable_set(:@lookup, saved_lookup)
185
+ end
186
+
187
+ def test_setlocal_sets_local_scope
188
+ editor = fresh_editor
189
+ dsl = RuVim::ConfigDSL.new(
190
+ command_registry: @command_registry,
191
+ ex_registry: @ex_registry,
192
+ keymaps: @keymaps,
193
+ command_host: RuVim::GlobalCommands.instance,
194
+ editor: editor
195
+ )
196
+ dsl.setlocal("number")
197
+ assert editor.get_option("number")
198
+ end
199
+
200
+ def test_setglobal_sets_global_scope
201
+ editor = fresh_editor
202
+ dsl = RuVim::ConfigDSL.new(
203
+ command_registry: @command_registry,
204
+ ex_registry: @ex_registry,
205
+ keymaps: @keymaps,
206
+ command_host: RuVim::GlobalCommands.instance,
207
+ editor: editor
208
+ )
209
+ dsl.setglobal("ignorecase")
210
+ assert editor.get_option("ignorecase")
211
+ end
78
212
  end
@@ -119,19 +119,89 @@ class DispatcherTest < Minitest::Test
119
119
  assert_includes body, "42"
120
120
  end
121
121
 
122
- def test_dispatch_ex_shell_captures_stdout_and_stderr_into_virtual_buffer
122
+ def test_dispatch_ex_shell_uses_shell_executor_when_available
123
+ executed_command = nil
124
+ fake_status = Struct.new(:exitstatus).new(0)
125
+ @editor.shell_executor = ->(cmd) { executed_command = cmd; fake_status }
126
+
127
+ @dispatcher.dispatch_ex(@editor, "!echo hello")
128
+
129
+ assert_equal "echo hello", executed_command
130
+ assert_equal "shell exit 0", @editor.message
131
+ end
132
+
133
+ def test_dispatch_ex_shell_falls_back_to_capture_without_executor
134
+ @editor.shell_executor = nil
123
135
  @dispatcher.dispatch_ex(@editor, "!echo out; echo err 1>&2")
124
136
 
125
137
  assert_equal "[Shell Output]", @editor.message
126
138
  assert_equal :help, @editor.current_buffer.kind
127
139
  body = @editor.current_buffer.lines.join("\n")
128
- assert_includes body, "[command]"
129
- assert_includes body, "echo out; echo err 1>&2"
130
140
  assert_includes body, "[stdout]"
131
141
  assert_includes body, "out"
132
142
  assert_includes body, "[stderr]"
133
143
  assert_includes body, "err"
134
- assert_includes body, "[status]"
144
+ end
145
+
146
+ def test_dispatch_ex_read_file_inserts_after_cursor_line
147
+ @editor.materialize_intro_buffer!
148
+ Dir.mktmpdir do |dir|
149
+ path = File.join(dir, "data.txt")
150
+ File.write(path, "aaa\nbbb\n")
151
+ @editor.current_buffer.replace_all_lines!(%w[line1 line2 line3])
152
+ @editor.current_window.cursor_y = 1 # on line2
153
+
154
+ @dispatcher.dispatch_ex(@editor, "r #{path}")
155
+
156
+ assert_equal %w[line1 line2 aaa bbb line3], @editor.current_buffer.lines
157
+ end
158
+ end
159
+
160
+ def test_dispatch_ex_read_with_range_inserts_after_specified_line
161
+ @editor.materialize_intro_buffer!
162
+ Dir.mktmpdir do |dir|
163
+ path = File.join(dir, "data.txt")
164
+ File.write(path, "inserted\n")
165
+ @editor.current_buffer.replace_all_lines!(%w[line1 line2 line3])
166
+
167
+ @dispatcher.dispatch_ex(@editor, "2r #{path}")
168
+
169
+ assert_equal %w[line1 line2 inserted line3], @editor.current_buffer.lines
170
+ end
171
+ end
172
+
173
+ def test_dispatch_ex_read_shell_command_inserts_output
174
+ @editor.materialize_intro_buffer!
175
+ @editor.current_buffer.replace_all_lines!(%w[line1 line2])
176
+ @editor.current_window.cursor_y = 0
177
+
178
+ @dispatcher.dispatch_ex(@editor, "r !echo hello")
179
+
180
+ assert_equal %w[line1 hello line2], @editor.current_buffer.lines
181
+ end
182
+
183
+ def test_dispatch_ex_write_to_shell_command
184
+ @editor.materialize_intro_buffer!
185
+ @editor.current_buffer.replace_all_lines!(%w[hello world])
186
+
187
+ Dir.mktmpdir do |dir|
188
+ outfile = File.join(dir, "out.txt")
189
+ @dispatcher.dispatch_ex(@editor, "w !cat > #{outfile}")
190
+
191
+ assert_equal "hello\nworld\n", File.read(outfile)
192
+ end
193
+ end
194
+
195
+ def test_dispatch_ex_write_range_to_shell_command
196
+ @editor.materialize_intro_buffer!
197
+ @editor.current_buffer.replace_all_lines!(%w[aaa bbb ccc])
198
+
199
+ Dir.mktmpdir do |dir|
200
+ outfile = File.join(dir, "out.txt")
201
+ @dispatcher.dispatch_ex(@editor, "2,3w !cat > #{outfile}")
202
+
203
+ assert_equal "bbb\nccc\n", File.read(outfile)
204
+ end
135
205
  end
136
206
 
137
207
  def test_dispatch_ex_set_commands
@@ -15,4 +15,45 @@ class DisplayWidthTest < Minitest::Test
15
15
  ENV["RUVIM_AMBIGUOUS_WIDTH"] = prev
16
16
  end
17
17
  end
18
+
19
+ def test_zero_codepoint_returns_zero
20
+ assert_equal 0, RuVim::DisplayWidth.uncached_codepoint_width(0)
21
+ end
22
+
23
+ def test_expand_tabs_basic
24
+ assert_equal " hello", RuVim::DisplayWidth.expand_tabs("\thello", tabstop: 2)
25
+ end
26
+
27
+ def test_expand_tabs_mid_column
28
+ assert_equal "a hello", RuVim::DisplayWidth.expand_tabs("a\thello", tabstop: 2)
29
+ end
30
+
31
+ def test_expand_tabs_with_tabstop_4
32
+ assert_equal " hello", RuVim::DisplayWidth.expand_tabs("\thello", tabstop: 4)
33
+ end
34
+
35
+ def test_expand_tabs_preserves_non_tab_chars
36
+ assert_equal "hello", RuVim::DisplayWidth.expand_tabs("hello", tabstop: 2)
37
+ end
38
+
39
+ def test_expand_tabs_with_start_col
40
+ result = RuVim::DisplayWidth.expand_tabs("\tx", tabstop: 4, start_col: 1)
41
+ assert_equal " x", result
42
+ end
43
+
44
+ def test_wide_codepoint_cjk
45
+ assert_equal 2, RuVim::DisplayWidth.cell_width("漢")
46
+ end
47
+
48
+ def test_wide_codepoint_fullwidth_form
49
+ assert_equal 2, RuVim::DisplayWidth.cell_width("A")
50
+ end
51
+
52
+ def test_combining_mark_returns_zero
53
+ assert_equal 0, RuVim::DisplayWidth.cell_width("\u0300")
54
+ end
55
+
56
+ def test_zero_width_joiner_returns_zero
57
+ assert_equal 0, RuVim::DisplayWidth.cell_width("\u200D")
58
+ end
18
59
  end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "test_helper"
4
+
5
+ class ExCommandRegistryTest < Minitest::Test
6
+ def setup
7
+ @registry = RuVim::ExCommandRegistry.instance
8
+ @saved_specs = @registry.instance_variable_get(:@specs).dup
9
+ @saved_lookup = @registry.instance_variable_get(:@lookup).dup
10
+ end
11
+
12
+ def teardown
13
+ @registry.instance_variable_set(:@specs, @saved_specs)
14
+ @registry.instance_variable_set(:@lookup, @saved_lookup)
15
+ end
16
+
17
+ def test_register_and_resolve
18
+ @registry.clear!
19
+ spec = @registry.register("testcmd", call: ->(_ctx) {}, desc: "test")
20
+ assert_equal "testcmd", spec.name
21
+
22
+ resolved = @registry.resolve("testcmd")
23
+ assert_equal spec, resolved
24
+ end
25
+
26
+ def test_register_with_aliases
27
+ @registry.clear!
28
+ spec = @registry.register("testcmd", call: ->(_ctx) {}, aliases: ["tc", "tcmd"])
29
+
30
+ assert_equal @registry.resolve("tc"), spec
31
+ assert_equal @registry.resolve("tcmd"), spec
32
+ end
33
+
34
+ def test_register_duplicate_raises
35
+ @registry.clear!
36
+ @registry.register("testcmd", call: ->(_ctx) {})
37
+
38
+ assert_raises(RuVim::CommandError) do
39
+ @registry.register("testcmd", call: ->(_ctx) {})
40
+ end
41
+ end
42
+
43
+ def test_register_alias_collision_raises
44
+ @registry.clear!
45
+ @registry.register("cmd1", call: ->(_ctx) {}, aliases: ["shared"])
46
+
47
+ assert_raises(RuVim::CommandError) do
48
+ @registry.register("cmd2", call: ->(_ctx) {}, aliases: ["shared"])
49
+ end
50
+ end
51
+
52
+ def test_resolve_returns_nil_for_unknown
53
+ @registry.clear!
54
+ assert_nil @registry.resolve("nonexistent")
55
+ end
56
+
57
+ def test_fetch_raises_for_unknown
58
+ @registry.clear!
59
+ assert_raises(RuVim::CommandError) do
60
+ @registry.fetch("nonexistent")
61
+ end
62
+ end
63
+
64
+ def test_fetch_returns_spec
65
+ @registry.clear!
66
+ spec = @registry.register("testcmd", call: ->(_ctx) {})
67
+ assert_equal spec, @registry.fetch("testcmd")
68
+ end
69
+
70
+ def test_registered?
71
+ @registry.clear!
72
+ refute @registry.registered?("testcmd")
73
+ @registry.register("testcmd", call: ->(_ctx) {})
74
+ assert @registry.registered?("testcmd")
75
+ end
76
+
77
+ def test_all_returns_sorted
78
+ @registry.clear!
79
+ @registry.register("beta", call: ->(_ctx) {})
80
+ @registry.register("alpha", call: ->(_ctx) {})
81
+ names = @registry.all.map(&:name)
82
+ assert_equal ["alpha", "beta"], names
83
+ end
84
+
85
+ def test_unregister
86
+ @registry.clear!
87
+ @registry.register("testcmd", call: ->(_ctx) {}, aliases: ["tc"])
88
+
89
+ removed = @registry.unregister("testcmd")
90
+ assert_equal "testcmd", removed.name
91
+ assert_nil @registry.resolve("testcmd")
92
+ assert_nil @registry.resolve("tc")
93
+ end
94
+
95
+ def test_unregister_unknown_returns_nil
96
+ @registry.clear!
97
+ assert_nil @registry.unregister("nonexistent")
98
+ end
99
+
100
+ def test_clear!
101
+ @registry.clear!
102
+ @registry.register("testcmd", call: ->(_ctx) {})
103
+ @registry.clear!
104
+ assert_equal [], @registry.all
105
+ end
106
+ end