ruvim 0.4.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 (113) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +53 -4
  3. data/README.md +15 -6
  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 +3 -2
  10. data/docs/command.md +81 -9
  11. data/docs/done.md +23 -0
  12. data/docs/spec.md +105 -19
  13. data/docs/todo.md +9 -0
  14. data/docs/tutorial.md +9 -1
  15. data/docs/vim_diff.md +13 -0
  16. data/ext/ruvim/extconf.rb +5 -0
  17. data/ext/ruvim/ruvim_ext.c +519 -0
  18. data/lib/ruvim/app.rb +217 -2778
  19. data/lib/ruvim/browser.rb +104 -0
  20. data/lib/ruvim/buffer.rb +39 -28
  21. data/lib/ruvim/command_invocation.rb +2 -2
  22. data/lib/ruvim/completion_manager.rb +708 -0
  23. data/lib/ruvim/dispatcher.rb +14 -8
  24. data/lib/ruvim/display_width.rb +91 -45
  25. data/lib/ruvim/editor.rb +64 -81
  26. data/lib/ruvim/ex_command_registry.rb +3 -1
  27. data/lib/ruvim/gh/link.rb +207 -0
  28. data/lib/ruvim/git/blame.rb +16 -6
  29. data/lib/ruvim/git/branch.rb +20 -5
  30. data/lib/ruvim/git/grep.rb +107 -0
  31. data/lib/ruvim/git/handler.rb +42 -1
  32. data/lib/ruvim/global_commands.rb +175 -35
  33. data/lib/ruvim/highlighter.rb +4 -13
  34. data/lib/ruvim/key_handler.rb +1510 -0
  35. data/lib/ruvim/keymap_manager.rb +7 -7
  36. data/lib/ruvim/lang/base.rb +5 -0
  37. data/lib/ruvim/lang/c.rb +116 -0
  38. data/lib/ruvim/lang/cpp.rb +107 -0
  39. data/lib/ruvim/lang/csv.rb +4 -1
  40. data/lib/ruvim/lang/diff.rb +2 -0
  41. data/lib/ruvim/lang/dockerfile.rb +36 -0
  42. data/lib/ruvim/lang/elixir.rb +85 -0
  43. data/lib/ruvim/lang/erb.rb +30 -0
  44. data/lib/ruvim/lang/go.rb +83 -0
  45. data/lib/ruvim/lang/html.rb +34 -0
  46. data/lib/ruvim/lang/javascript.rb +83 -0
  47. data/lib/ruvim/lang/json.rb +6 -0
  48. data/lib/ruvim/lang/lua.rb +76 -0
  49. data/lib/ruvim/lang/makefile.rb +36 -0
  50. data/lib/ruvim/lang/markdown.rb +3 -4
  51. data/lib/ruvim/lang/ocaml.rb +77 -0
  52. data/lib/ruvim/lang/perl.rb +91 -0
  53. data/lib/ruvim/lang/python.rb +85 -0
  54. data/lib/ruvim/lang/registry.rb +102 -0
  55. data/lib/ruvim/lang/ruby.rb +7 -0
  56. data/lib/ruvim/lang/rust.rb +95 -0
  57. data/lib/ruvim/lang/scheme.rb +5 -0
  58. data/lib/ruvim/lang/sh.rb +76 -0
  59. data/lib/ruvim/lang/sql.rb +52 -0
  60. data/lib/ruvim/lang/toml.rb +36 -0
  61. data/lib/ruvim/lang/tsv.rb +4 -1
  62. data/lib/ruvim/lang/typescript.rb +53 -0
  63. data/lib/ruvim/lang/yaml.rb +62 -0
  64. data/lib/ruvim/rich_view/table_renderer.rb +3 -3
  65. data/lib/ruvim/rich_view.rb +14 -7
  66. data/lib/ruvim/screen.rb +126 -72
  67. data/lib/ruvim/stream/file_load.rb +85 -0
  68. data/lib/ruvim/stream/follow.rb +40 -0
  69. data/lib/ruvim/stream/git.rb +43 -0
  70. data/lib/ruvim/stream/run.rb +74 -0
  71. data/lib/ruvim/stream/stdin.rb +55 -0
  72. data/lib/ruvim/stream.rb +35 -0
  73. data/lib/ruvim/stream_mixer.rb +394 -0
  74. data/lib/ruvim/terminal.rb +18 -4
  75. data/lib/ruvim/text_metrics.rb +84 -65
  76. data/lib/ruvim/version.rb +1 -1
  77. data/lib/ruvim/window.rb +5 -5
  78. data/lib/ruvim.rb +23 -6
  79. data/test/app_command_test.rb +382 -0
  80. data/test/app_completion_test.rb +43 -19
  81. data/test/app_dot_repeat_test.rb +27 -3
  82. data/test/app_ex_command_test.rb +154 -0
  83. data/test/app_motion_test.rb +13 -12
  84. data/test/app_register_test.rb +2 -1
  85. data/test/app_scenario_test.rb +15 -10
  86. data/test/app_startup_test.rb +70 -27
  87. data/test/app_text_object_test.rb +2 -1
  88. data/test/app_unicode_behavior_test.rb +3 -2
  89. data/test/browser_test.rb +88 -0
  90. data/test/buffer_test.rb +24 -0
  91. data/test/cli_test.rb +63 -0
  92. data/test/command_invocation_test.rb +33 -0
  93. data/test/config_dsl_test.rb +47 -0
  94. data/test/dispatcher_test.rb +74 -4
  95. data/test/ex_command_registry_test.rb +106 -0
  96. data/test/follow_test.rb +20 -21
  97. data/test/gh_link_test.rb +141 -0
  98. data/test/git_blame_test.rb +96 -17
  99. data/test/git_grep_test.rb +64 -0
  100. data/test/highlighter_test.rb +125 -0
  101. data/test/indent_test.rb +137 -0
  102. data/test/input_screen_integration_test.rb +1 -1
  103. data/test/keyword_chars_test.rb +85 -0
  104. data/test/lang_test.rb +634 -0
  105. data/test/markdown_renderer_test.rb +5 -5
  106. data/test/on_save_hook_test.rb +12 -8
  107. data/test/render_snapshot_test.rb +78 -0
  108. data/test/rich_view_test.rb +42 -42
  109. data/test/run_command_test.rb +307 -0
  110. data/test/screen_test.rb +68 -5
  111. data/test/stream_test.rb +165 -0
  112. data/test/window_test.rb +59 -0
  113. metadata +52 -2
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "test_helper"
4
+
5
+ class AppExCommandTest < Minitest::Test
6
+ def setup
7
+ @app = RuVim::App.new(clean: true)
8
+ @editor = @app.instance_variable_get(:@editor)
9
+ @key_handler = @app.instance_variable_get(:@key_handler)
10
+ @editor.materialize_intro_buffer!
11
+ end
12
+
13
+ def feed(*keys)
14
+ keys.each { |k| @key_handler.handle(k) }
15
+ end
16
+
17
+ def buf
18
+ @editor.current_buffer
19
+ end
20
+
21
+ def win
22
+ @editor.current_window
23
+ end
24
+
25
+ # --- :set ---
26
+
27
+ def test_set_number_on_off
28
+ feed(":", "s", "e", "t", " ", "n", "u", "m", "b", "e", "r", :enter)
29
+ assert @editor.get_option("number")
30
+
31
+ feed(":", "s", "e", "t", " ", "n", "o", "n", "u", "m", "b", "e", "r", :enter)
32
+ refute @editor.get_option("number")
33
+ end
34
+
35
+ def test_set_tabstop_value
36
+ feed(":", "s", "e", "t", " ", "t", "a", "b", "s", "t", "o", "p", "=", "8", :enter)
37
+ assert_equal 8, @editor.get_option("tabstop")
38
+ end
39
+
40
+ def test_set_option_query
41
+ feed(":", "s", "e", "t", " ", "n", "u", "m", "b", "e", "r", "?", :enter)
42
+ assert_equal :normal, @editor.mode
43
+ end
44
+
45
+ # --- :bindings ---
46
+
47
+ def test_bindings_command
48
+ feed(":", "b", "i", "n", "d", "i", "n", "g", "s", :enter)
49
+ assert_equal :normal, @editor.mode
50
+ end
51
+
52
+ def test_bindings_with_mode_filter
53
+ feed(":", "b", "i", "n", "d", "i", "n", "g", "s", " ", "n", :enter)
54
+ assert_equal :normal, @editor.mode
55
+ end
56
+
57
+ # --- :tabnew / :tabnext / :tabprev ---
58
+
59
+ def test_tabnext_tabprev_via_ex
60
+ feed(":", "t", "a", "b", "n", "e", "w", :enter)
61
+ feed(":", "t", "a", "b", "p", "r", "e", "v", :enter)
62
+ feed(":", "t", "a", "b", "n", "e", "x", "t", :enter)
63
+ assert_equal :normal, @editor.mode
64
+ end
65
+
66
+ # --- :bnext / :bprev ---
67
+
68
+ def test_bnext_bprev_via_ex
69
+ feed(":", "b", "n", "e", "x", "t", :enter)
70
+ feed(":", "b", "p", "r", "e", "v", :enter)
71
+ assert_equal :normal, @editor.mode
72
+ end
73
+
74
+ # --- :copen / :cclose / :cprev / :lclose / :lprev ---
75
+
76
+ def test_copen_and_cclose
77
+ feed(":", "c", "o", "p", "e", "n", :enter)
78
+ feed(":", "c", "c", "l", "o", "s", "e", :enter)
79
+ assert_equal :normal, @editor.mode
80
+ end
81
+
82
+ def test_cprev_with_empty_list
83
+ feed(":", "c", "p", "r", "e", "v", :enter)
84
+ assert_equal :normal, @editor.mode
85
+ end
86
+
87
+ def test_lclose_without_open
88
+ feed(":", "l", "c", "l", "o", "s", "e", :enter)
89
+ assert_equal :normal, @editor.mode
90
+ end
91
+
92
+ def test_lprev_with_empty_list
93
+ feed(":", "l", "p", "r", "e", "v", :enter)
94
+ assert_equal :normal, @editor.mode
95
+ end
96
+
97
+ # --- :args ---
98
+
99
+ def test_arglist_operations
100
+ feed(":", "a", "r", "g", "s", :enter)
101
+ assert_equal :normal, @editor.mode
102
+ end
103
+
104
+ # --- :wq ---
105
+
106
+ def test_wq_with_no_filename_shows_error
107
+ feed(":", "w", "q", :enter)
108
+ assert_equal :normal, @editor.mode
109
+ end
110
+
111
+ # --- :edit ---
112
+
113
+ def test_edit_no_file_reloads_or_errors
114
+ feed(":", "e", "d", "i", "t", :enter)
115
+ assert_equal :normal, @editor.mode
116
+ end
117
+
118
+ # --- :split / :vsplit + window focus ---
119
+
120
+ def test_window_focus_after_split
121
+ feed(":", "s", "p", "l", "i", "t", :enter)
122
+ original_win_id = @editor.current_window.id
123
+
124
+ feed(:ctrl_w, "w")
125
+ refute_equal original_win_id, @editor.current_window.id
126
+
127
+ feed(:ctrl_w, "j")
128
+ feed(:ctrl_w, "k")
129
+ assert_equal :normal, @editor.mode
130
+ end
131
+
132
+ def test_window_focus_left_right
133
+ feed(":", "v", "s", "p", "l", "i", "t", :enter)
134
+ feed(:ctrl_w, "h")
135
+ feed(:ctrl_w, "l")
136
+ assert_equal :normal, @editor.mode
137
+ end
138
+
139
+ # --- :b# (alternate buffer) ---
140
+
141
+ def test_buffer_alternate_hash
142
+ feed(":", "s", "p", "l", "i", "t", :enter)
143
+ feed(":", "b", "#", :enter)
144
+ assert_equal :normal, @editor.mode
145
+ end
146
+
147
+ # --- rich view toggle ---
148
+
149
+ def test_rich_toggle
150
+ buf.replace_all_lines!(["hello world"])
151
+ feed("g", "r")
152
+ assert_equal :normal, @editor.mode
153
+ end
154
+ end
@@ -4,11 +4,12 @@ class AppMotionTest < 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
  end
9
10
 
10
11
  def press(*keys)
11
- keys.each { |k| @app.send(:handle_normal_key, k) }
12
+ keys.each { |k| @key_handler.send(:handle_normal_key, k) }
12
13
  end
13
14
 
14
15
  def test_find_char_and_repeat
@@ -59,14 +60,14 @@ class AppMotionTest < Minitest::Test
59
60
 
60
61
  @editor.current_window_view_height_hint = 5
61
62
 
62
- @app.send(:handle_normal_key, :pagedown)
63
+ @key_handler.send(:handle_normal_key, :pagedown)
63
64
  assert_equal 4, @editor.current_window.cursor_y
64
65
 
65
66
  @editor.pending_count = 2
66
- @app.send(:handle_normal_key, :pagedown)
67
+ @key_handler.send(:handle_normal_key, :pagedown)
67
68
  assert_equal 12, @editor.current_window.cursor_y
68
69
 
69
- @app.send(:handle_normal_key, :pageup)
70
+ @key_handler.send(:handle_normal_key, :pageup)
70
71
  assert_equal 8, @editor.current_window.cursor_y
71
72
  end
72
73
 
@@ -78,16 +79,16 @@ class AppMotionTest < Minitest::Test
78
79
 
79
80
  @editor.current_window_view_height_hint = 10
80
81
 
81
- @app.send(:handle_normal_key, :ctrl_d)
82
+ @key_handler.send(:handle_normal_key, :ctrl_d)
82
83
  assert_equal 5, @editor.current_window.cursor_y
83
84
 
84
- @app.send(:handle_normal_key, :ctrl_u)
85
+ @key_handler.send(:handle_normal_key, :ctrl_u)
85
86
  assert_equal 0, @editor.current_window.cursor_y
86
87
 
87
- @app.send(:handle_normal_key, :ctrl_f)
88
+ @key_handler.send(:handle_normal_key, :ctrl_f)
88
89
  assert_equal 9, @editor.current_window.cursor_y
89
90
 
90
- @app.send(:handle_normal_key, :ctrl_b)
91
+ @key_handler.send(:handle_normal_key, :ctrl_b)
91
92
  assert_equal 0, @editor.current_window.cursor_y
92
93
  end
93
94
 
@@ -100,11 +101,11 @@ class AppMotionTest < Minitest::Test
100
101
 
101
102
  @editor.current_window_view_height_hint = 10
102
103
 
103
- @app.send(:handle_normal_key, :ctrl_e)
104
+ @key_handler.send(:handle_normal_key, :ctrl_e)
104
105
  assert_equal 6, @editor.current_window.row_offset
105
106
  assert_equal 6, @editor.current_window.cursor_y
106
107
 
107
- @app.send(:handle_normal_key, :ctrl_y)
108
+ @key_handler.send(:handle_normal_key, :ctrl_y)
108
109
  assert_equal 5, @editor.current_window.row_offset
109
110
  assert_equal 6, @editor.current_window.cursor_y
110
111
  end
@@ -118,7 +119,7 @@ class AppMotionTest < Minitest::Test
118
119
  keymaps = @app.instance_variable_get(:@keymaps)
119
120
  keymaps.bind(:normal, ["<C-d>"], "cursor.down")
120
121
 
121
- @app.send(:handle_normal_key, :ctrl_d)
122
+ @key_handler.send(:handle_normal_key, :ctrl_d)
122
123
  assert_equal 1, @editor.current_window.cursor_y
123
124
  end
124
125
 
@@ -131,7 +132,7 @@ class AppMotionTest < Minitest::Test
131
132
  keymaps = @app.instance_variable_get(:@keymaps)
132
133
  keymaps.bind(:normal, ["<PageDown>"], "cursor.down")
133
134
 
134
- @app.send(:handle_normal_key, :pagedown)
135
+ @key_handler.send(:handle_normal_key, :pagedown)
135
136
  assert_equal 1, @editor.current_window.cursor_y
136
137
  end
137
138
 
@@ -4,12 +4,13 @@ class AppRegisterTest < 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
  end
10
11
 
11
12
  def press(*keys)
12
- keys.each { |k| @app.send(:handle_normal_key, k) }
13
+ keys.each { |k| @key_handler.send(:handle_normal_key, k) }
13
14
  end
14
15
 
15
16
  def test_yy_updates_register_zero
@@ -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| @app.send(:handle_key, 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
- @app.send(:handle_key, "x")
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
- @app.send(:handle_key, "i")
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.instance_variable_set(:@stdin_stream_source, stream)
377
- @app.send(:prepare_stdin_stream_buffer!)
377
+ sh = @app.instance_variable_get(:@stream_mixer)
378
+ sh.prepare_stdin_stream_buffer!(stream)
379
+ sh.start_pending_stdin!
378
380
 
379
- @app.send(:handle_key, :ctrl_c)
381
+ @key_handler.handle(:ctrl_c)
380
382
 
381
- assert_equal :closed, @editor.current_buffer.stream_state
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(/\[stdin\] closed/, @editor.message)
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
 
@@ -709,9 +714,9 @@ class AppScenarioTest < Minitest::Test
709
714
  @editor.current_window.cursor_y = 0
710
715
  @editor.current_window.cursor_x = 0
711
716
  feed("A")
712
- @app.instance_variable_set(:@paste_batch, true)
717
+ @app.instance_variable_get(:@key_handler).paste_batch = true
713
718
  feed(:enter, *"world".chars)
714
- @app.instance_variable_set(:@paste_batch, false)
719
+ @app.instance_variable_get(:@key_handler).paste_batch = false
715
720
  feed(:escape)
716
721
 
717
722
  assert_equal [" hello", "world"], @editor.current_buffer.lines
@@ -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
- app.send(:drain_stream_events!)
265
- thread = app.instance_variable_get(:@stream_reader_thread)
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.stream_state
276
- assert_equal ["line1", "line2", ""], buf.lines
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&.send(:shutdown_stream_reader!)
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.loading_state
338
+ assert_includes [:live, :closed], buf.stream&.state
297
339
  assert_equal false, buf.modifiable?
298
340
 
299
341
  100.times do
300
- app.send(:drain_stream_events!)
301
- break if buf.loading_state != :live
302
- state = app.instance_variable_get(:@async_file_loads)[buf.id]
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
- app.send(:drain_stream_events!)
347
+ sh.drain_events!
307
348
 
308
- assert_equal :closed, buf.loading_state
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&.send(:shutdown_background_readers!)
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.loading_state
335
- assert_equal ["ab", "c"], buf.lines
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
- app.send(:drain_stream_events!)
340
- break if buf.loading_state != :live
381
+ sh.drain_events!
382
+ break if buf.stream&.state != :live
341
383
  sleep 0.005
342
384
  end
343
- app.send(:drain_stream_events!)
385
+ sh.drain_events!
344
386
 
345
- assert_equal :closed, buf.loading_state
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&.send(:shutdown_background_readers!)
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.send(:push_command_line_history, ":", "set number")
364
- app1.send(:push_command_line_history, "/", "foo")
365
- app1.send(:push_command_line_history, "?", "bar")
366
- app1.send(:save_command_line_history!)
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(:command_line_history_file_path)
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| @app.send(:handle_normal_key, 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| @app.send(:handle_normal_key, 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| @app.send(:handle_key, 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")