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.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +4 -0
- data/AGENTS.md +84 -0
- data/CLAUDE.md +1 -0
- data/docs/binding.md +29 -0
- data/docs/command.md +101 -0
- data/docs/config.md +203 -84
- data/docs/done.md +21 -0
- data/docs/lib_cleanup_report.md +79 -0
- data/docs/plugin.md +13 -15
- data/docs/spec.md +195 -33
- data/docs/todo.md +183 -10
- data/docs/tutorial.md +1 -1
- data/docs/vim_diff.md +94 -171
- data/lib/ruvim/app.rb +1543 -172
- data/lib/ruvim/buffer.rb +35 -1
- data/lib/ruvim/cli.rb +12 -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 +21 -5
- data/lib/ruvim/context.rb +2 -7
- data/lib/ruvim/dispatcher.rb +153 -13
- data/lib/ruvim/display_width.rb +28 -2
- data/lib/ruvim/editor.rb +622 -69
- data/lib/ruvim/ex_command_registry.rb +2 -0
- data/lib/ruvim/global_commands.rb +1386 -114
- data/lib/ruvim/highlighter.rb +16 -21
- data/lib/ruvim/input.rb +52 -29
- data/lib/ruvim/keymap_manager.rb +83 -0
- data/lib/ruvim/keyword_chars.rb +48 -0
- data/lib/ruvim/lang/base.rb +25 -0
- data/lib/ruvim/lang/csv.rb +18 -0
- data/lib/ruvim/lang/json.rb +18 -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/markdown_renderer.rb +248 -0
- data/lib/ruvim/rich_view/table_renderer.rb +176 -0
- data/lib/ruvim/rich_view.rb +93 -0
- data/lib/ruvim/screen.rb +851 -119
- data/lib/ruvim/terminal.rb +18 -1
- data/lib/ruvim/text_metrics.rb +28 -0
- data/lib/ruvim/version.rb +2 -2
- data/lib/ruvim/window.rb +37 -10
- data/lib/ruvim.rb +15 -0
- data/test/app_completion_test.rb +174 -0
- data/test/app_dot_repeat_test.rb +13 -0
- data/test/app_motion_test.rb +110 -2
- data/test/app_scenario_test.rb +998 -0
- data/test/app_startup_test.rb +197 -0
- data/test/arglist_test.rb +113 -0
- data/test/buffer_test.rb +49 -30
- data/test/config_loader_test.rb +37 -0
- data/test/dispatcher_test.rb +438 -0
- data/test/display_width_test.rb +18 -0
- data/test/editor_register_test.rb +23 -0
- data/test/fixtures/render_basic_snapshot.txt +7 -8
- data/test/fixtures/render_basic_snapshot_nonumber.txt +1 -2
- data/test/fixtures/render_unicode_scrolled_snapshot.txt +6 -7
- data/test/highlighter_test.rb +121 -0
- data/test/indent_test.rb +201 -0
- data/test/input_screen_integration_test.rb +65 -14
- data/test/markdown_renderer_test.rb +279 -0
- data/test/on_save_hook_test.rb +150 -0
- data/test/rich_view_test.rb +478 -0
- data/test/screen_test.rb +470 -0
- data/test/window_test.rb +26 -0
- metadata +37 -2
data/test/dispatcher_test.rb
CHANGED
|
@@ -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
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
1 # title
|
|
2
|
-
2
|
|
3
|
-
3 foo
|
|
4
|
-
4 bar 日本語 編集
|
|
5
|
-
5 baz
|
|
6
|
-
|
|
7
|
-
-- NORMAL -- t1 4:5
|
|
1
|
+
1 # title
|
|
2
|
+
2
|
|
3
|
+
3 foo
|
|
4
|
+
4 bar 日本語 編集
|
|
5
|
+
5 baz
|
|
6
|
+
-- NORMAL -- [N 4:5
|
data/test/highlighter_test.rb
CHANGED
|
@@ -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
|