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