ruvim 0.2.0 → 0.4.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.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +4 -0
- data/AGENTS.md +96 -0
- data/CLAUDE.md +1 -0
- data/README.md +15 -1
- data/docs/binding.md +39 -0
- data/docs/command.md +163 -4
- data/docs/config.md +12 -4
- data/docs/done.md +21 -0
- data/docs/spec.md +214 -18
- data/docs/todo.md +1 -5
- data/docs/tutorial.md +24 -0
- data/docs/vim_diff.md +105 -173
- data/lib/ruvim/app.rb +1165 -70
- data/lib/ruvim/buffer.rb +47 -1
- data/lib/ruvim/cli.rb +18 -3
- data/lib/ruvim/clipboard.rb +2 -0
- data/lib/ruvim/command_invocation.rb +3 -1
- data/lib/ruvim/command_line.rb +2 -0
- data/lib/ruvim/command_registry.rb +2 -0
- data/lib/ruvim/config_dsl.rb +2 -0
- data/lib/ruvim/config_loader.rb +2 -0
- data/lib/ruvim/context.rb +2 -0
- data/lib/ruvim/dispatcher.rb +143 -13
- data/lib/ruvim/display_width.rb +3 -0
- data/lib/ruvim/editor.rb +466 -71
- data/lib/ruvim/ex_command_registry.rb +2 -0
- data/lib/ruvim/file_watcher.rb +243 -0
- data/lib/ruvim/git/blame.rb +245 -0
- data/lib/ruvim/git/branch.rb +97 -0
- data/lib/ruvim/git/commit.rb +102 -0
- data/lib/ruvim/git/diff.rb +129 -0
- data/lib/ruvim/git/handler.rb +84 -0
- data/lib/ruvim/git/log.rb +41 -0
- data/lib/ruvim/git/status.rb +103 -0
- data/lib/ruvim/global_commands.rb +1066 -105
- data/lib/ruvim/highlighter.rb +19 -22
- data/lib/ruvim/input.rb +40 -28
- data/lib/ruvim/keymap_manager.rb +83 -0
- data/lib/ruvim/keyword_chars.rb +2 -0
- data/lib/ruvim/lang/base.rb +25 -0
- data/lib/ruvim/lang/csv.rb +18 -0
- data/lib/ruvim/lang/diff.rb +41 -0
- data/lib/ruvim/lang/json.rb +52 -0
- data/lib/ruvim/lang/markdown.rb +170 -0
- data/lib/ruvim/lang/ruby.rb +236 -0
- data/lib/ruvim/lang/scheme.rb +44 -0
- data/lib/ruvim/lang/tsv.rb +19 -0
- data/lib/ruvim/rich_view/json_renderer.rb +131 -0
- data/lib/ruvim/rich_view/jsonl_renderer.rb +57 -0
- data/lib/ruvim/rich_view/markdown_renderer.rb +248 -0
- data/lib/ruvim/rich_view/table_renderer.rb +176 -0
- data/lib/ruvim/rich_view.rb +109 -0
- data/lib/ruvim/screen.rb +503 -109
- data/lib/ruvim/terminal.rb +18 -1
- data/lib/ruvim/text_metrics.rb +2 -0
- data/lib/ruvim/version.rb +1 -1
- data/lib/ruvim/window.rb +2 -0
- data/lib/ruvim.rb +24 -0
- data/test/app_completion_test.rb +98 -0
- data/test/app_dot_repeat_test.rb +13 -0
- data/test/app_motion_test.rb +13 -0
- data/test/app_scenario_test.rb +898 -1
- data/test/app_startup_test.rb +187 -0
- data/test/arglist_test.rb +113 -0
- data/test/buffer_test.rb +49 -30
- data/test/cli_test.rb +14 -0
- data/test/clipboard_test.rb +67 -0
- data/test/command_line_test.rb +118 -0
- data/test/config_dsl_test.rb +87 -0
- data/test/dispatcher_test.rb +322 -0
- data/test/display_width_test.rb +41 -0
- data/test/editor_register_test.rb +23 -0
- data/test/file_watcher_test.rb +197 -0
- data/test/follow_test.rb +199 -0
- data/test/git_blame_test.rb +713 -0
- data/test/highlighter_test.rb +165 -0
- data/test/indent_test.rb +287 -0
- data/test/input_screen_integration_test.rb +40 -2
- data/test/markdown_renderer_test.rb +279 -0
- data/test/on_save_hook_test.rb +150 -0
- data/test/rich_view_test.rb +734 -0
- data/test/screen_test.rb +304 -0
- data/test/search_option_test.rb +19 -0
- data/test/test_helper.rb +9 -0
- metadata +49 -2
data/test/config_dsl_test.rb
CHANGED
|
@@ -75,4 +75,91 @@ 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
|
|
78
165
|
end
|
data/test/dispatcher_test.rb
CHANGED
|
@@ -33,6 +33,53 @@ class DispatcherTest < Minitest::Test
|
|
|
33
33
|
assert_includes body, "Write current buffer"
|
|
34
34
|
end
|
|
35
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
|
+
|
|
36
83
|
def test_dispatch_ex_command_and_ruby
|
|
37
84
|
@dispatcher.dispatch_ex(@editor, "command Hi help")
|
|
38
85
|
assert_equal "Defined :Hi", @editor.message
|
|
@@ -44,6 +91,20 @@ class DispatcherTest < Minitest::Test
|
|
|
44
91
|
assert_equal "ruby: 3", @editor.message
|
|
45
92
|
end
|
|
46
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
|
+
|
|
47
108
|
def test_dispatch_ex_ruby_captures_stdout_and_stderr_into_virtual_buffer
|
|
48
109
|
@dispatcher.dispatch_ex(@editor, "ruby STDOUT.puts(%q[out]); STDERR.puts(%q[err]); 42")
|
|
49
110
|
|
|
@@ -224,6 +285,21 @@ class DispatcherTest < Minitest::Test
|
|
|
224
285
|
refute @editor.message_error?
|
|
225
286
|
end
|
|
226
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
|
+
|
|
227
303
|
def test_splitbelow_and_splitright_change_insertion_side
|
|
228
304
|
@editor.set_option("splitbelow", false, scope: :global)
|
|
229
305
|
first = @editor.current_window_id
|
|
@@ -237,4 +313,250 @@ class DispatcherTest < Minitest::Test
|
|
|
237
313
|
idx = @editor.window_order.index(@editor.current_window_id)
|
|
238
314
|
assert_equal 1, idx
|
|
239
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
|
|
240
562
|
end
|
data/test/display_width_test.rb
CHANGED
|
@@ -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
|
|
@@ -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
|