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
data/lib/ruvim/window.rb CHANGED
@@ -19,17 +19,17 @@ module RuVim
19
19
  end
20
20
 
21
21
  def cursor_x=(value)
22
- @cursor_x = value.to_i
22
+ @cursor_x = value
23
23
  @preferred_x = nil
24
24
  end
25
25
 
26
26
  def cursor_y=(value)
27
- @cursor_y = value.to_i
27
+ @cursor_y = value
28
28
  end
29
29
 
30
30
  def clamp_to_buffer(buffer, max_extra_col: 0)
31
31
  @cursor_y = [[@cursor_y, 0].max, buffer.line_count - 1].min
32
- max_col = buffer.line_length(@cursor_y) + [max_extra_col.to_i, 0].max
32
+ max_col = buffer.line_length(@cursor_y) + [max_extra_col, 0].max
33
33
  @cursor_x = [[@cursor_x, 0].max, max_col].min
34
34
  self
35
35
  end
@@ -71,7 +71,7 @@ module RuVim
71
71
 
72
72
  def ensure_visible(buffer, height:, width:, tabstop: 2, scrolloff: 0, sidescrolloff: 0)
73
73
  clamp_to_buffer(buffer)
74
- so = [[scrolloff.to_i, 0].max, [height.to_i - 1, 0].max].min
74
+ so = [[scrolloff, 0].max, [height - 1, 0].max].min
75
75
 
76
76
  top_target = @cursor_y - so
77
77
  bottom_target = @cursor_y + so
@@ -82,7 +82,7 @@ module RuVim
82
82
  line = buffer.line_at(@cursor_y)
83
83
  cursor_screen_col = RuVim::TextMetrics.screen_col_for_char_index(line, @cursor_x, tabstop:)
84
84
  offset_screen_col = RuVim::TextMetrics.screen_col_for_char_index(line, @col_offset, tabstop:)
85
- sso = [[sidescrolloff.to_i, 0].max, [width.to_i - 1, 0].max].min
85
+ sso = [[sidescrolloff, 0].max, [width - 1, 0].max].min
86
86
 
87
87
  if cursor_screen_col < offset_screen_col + sso
88
88
  target_left = [cursor_screen_col - sso, 0].max
data/lib/ruvim.rb CHANGED
@@ -13,12 +13,33 @@ require_relative "ruvim/display_width"
13
13
  require_relative "ruvim/keyword_chars"
14
14
  require_relative "ruvim/text_metrics"
15
15
  require_relative "ruvim/clipboard"
16
+ require_relative "ruvim/browser"
17
+ require_relative "ruvim/lang/registry"
16
18
  require_relative "ruvim/lang/base"
17
19
  require_relative "ruvim/lang/markdown"
18
20
  require_relative "ruvim/lang/ruby"
19
21
  require_relative "ruvim/lang/json"
20
22
  require_relative "ruvim/lang/scheme"
23
+ require_relative "ruvim/lang/c"
24
+ require_relative "ruvim/lang/cpp"
21
25
  require_relative "ruvim/lang/diff"
26
+ require_relative "ruvim/lang/yaml"
27
+ require_relative "ruvim/lang/sh"
28
+ require_relative "ruvim/lang/python"
29
+ require_relative "ruvim/lang/javascript"
30
+ require_relative "ruvim/lang/typescript"
31
+ require_relative "ruvim/lang/html"
32
+ require_relative "ruvim/lang/toml"
33
+ require_relative "ruvim/lang/go"
34
+ require_relative "ruvim/lang/rust"
35
+ require_relative "ruvim/lang/makefile"
36
+ require_relative "ruvim/lang/dockerfile"
37
+ require_relative "ruvim/lang/sql"
38
+ require_relative "ruvim/lang/elixir"
39
+ require_relative "ruvim/lang/perl"
40
+ require_relative "ruvim/lang/lua"
41
+ require_relative "ruvim/lang/ocaml"
42
+ require_relative "ruvim/lang/erb"
22
43
  require_relative "ruvim/highlighter"
23
44
  require_relative "ruvim/context"
24
45
  require_relative "ruvim/buffer"
@@ -32,6 +53,8 @@ require_relative "ruvim/git/diff"
32
53
  require_relative "ruvim/git/log"
33
54
  require_relative "ruvim/git/branch"
34
55
  require_relative "ruvim/git/commit"
56
+ require_relative "ruvim/git/grep"
57
+ require_relative "ruvim/gh/link"
35
58
  require_relative "ruvim/git/handler"
36
59
  require_relative "ruvim/global_commands"
37
60
  require_relative "ruvim/dispatcher"
@@ -40,12 +63,6 @@ require_relative "ruvim/command_line"
40
63
  require_relative "ruvim/input"
41
64
  require_relative "ruvim/terminal"
42
65
  require_relative "ruvim/rich_view"
43
-
44
- # Register renderers after RichView is defined
45
- RuVim::RichView.register("markdown", RuVim::RichView::MarkdownRenderer)
46
- RuVim::RichView.register("json", RuVim::RichView::JsonRenderer)
47
- RuVim::RichView.register("jsonl", RuVim::RichView::JsonlRenderer)
48
-
49
66
  require_relative "ruvim/lang/tsv"
50
67
  require_relative "ruvim/lang/csv"
51
68
  require_relative "ruvim/screen"
@@ -0,0 +1,382 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "test_helper"
4
+
5
+ class AppCommandTest < 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
+ # --- cursor commands ---
26
+
27
+ def test_cursor_line_end
28
+ buf.replace_all_lines!(["hello world"])
29
+ win.cursor_x = 0
30
+ feed("$")
31
+ assert_equal buf.line_length(0), win.cursor_x
32
+ end
33
+
34
+ def test_cursor_buffer_start_gg
35
+ buf.replace_all_lines!(["line1", "line2", "line3"])
36
+ win.cursor_y = 2
37
+ feed("g", "g")
38
+ assert_equal 0, win.cursor_y
39
+ end
40
+
41
+ # --- insert mode variants ---
42
+
43
+ def test_append_mode_a
44
+ buf.replace_all_lines!(["abc"])
45
+ win.cursor_x = 1
46
+ feed("a", "X", :escape)
47
+ assert_equal ["abXc"], buf.lines
48
+ end
49
+
50
+ def test_insert_line_start_nonblank_I
51
+ buf.replace_all_lines!([" hello"])
52
+ win.cursor_x = 4
53
+ feed("I", "X", :escape)
54
+ assert_equal [" Xhello"], buf.lines
55
+ end
56
+
57
+ def test_open_line_below_o
58
+ buf.replace_all_lines!(["hello", "world"])
59
+ win.cursor_y = 0
60
+ feed("o", "new", :escape)
61
+ assert_equal ["hello", "new", "world"], buf.lines
62
+ assert_equal :normal, @editor.mode
63
+ end
64
+
65
+ def test_open_line_above_O
66
+ buf.replace_all_lines!(["hello", "world"])
67
+ win.cursor_y = 1
68
+ feed("O", "new", :escape)
69
+ assert_equal ["hello", "new", "world"], buf.lines
70
+ assert_equal :normal, @editor.mode
71
+ end
72
+
73
+ # --- undo / redo ---
74
+
75
+ def test_undo_and_redo
76
+ buf.replace_all_lines!(["hello"])
77
+ feed("x")
78
+ assert_equal ["ello"], buf.lines
79
+
80
+ feed("u")
81
+ assert_equal ["hello"], buf.lines
82
+ assert_match(/Undo/, @editor.message)
83
+
84
+ feed(:ctrl_r)
85
+ assert_equal ["ello"], buf.lines
86
+ assert_match(/Redo/, @editor.message)
87
+ end
88
+
89
+ def test_undo_at_oldest
90
+ buf.replace_all_lines!(["hello"])
91
+ buf.instance_variable_get(:@undo_stack)&.clear
92
+ feed("u")
93
+ assert_match(/oldest/, @editor.message)
94
+ end
95
+
96
+ # --- search ---
97
+
98
+ def test_search_backward_mode
99
+ buf.replace_all_lines!(["hello world"])
100
+ feed("?")
101
+ assert_equal :command_line, @editor.mode
102
+ end
103
+
104
+ def test_search_prev_N
105
+ buf.replace_all_lines!(["aaa", "bbb", "aaa"])
106
+ win.cursor_y = 0
107
+ feed("/", "b", "b", "b", :enter)
108
+ assert_equal 1, win.cursor_y
109
+
110
+ feed("N")
111
+ assert_operator win.cursor_y, :>=, 0
112
+ end
113
+
114
+ def test_search_word_backward
115
+ buf.replace_all_lines!(["foo bar foo"])
116
+ win.cursor_x = 8
117
+ feed("#")
118
+ assert_equal 0, win.cursor_x
119
+ end
120
+
121
+ def test_search_word_forward_partial
122
+ buf.replace_all_lines!(["foobar foo foobar"])
123
+ win.cursor_x = 0
124
+ feed("g", "*")
125
+ assert_operator win.cursor_x, :>, 0
126
+ end
127
+
128
+ def test_search_word_backward_partial
129
+ buf.replace_all_lines!(["foobar foo foobar"])
130
+ win.cursor_x = 11
131
+ feed("g", "#")
132
+ assert_operator win.cursor_x, :<, 11
133
+ end
134
+
135
+ # --- marks ---
136
+
137
+ def test_mark_set_and_jump
138
+ buf.replace_all_lines!(["line1", "line2", "line3"])
139
+ win.cursor_y = 1
140
+ feed("m", "a")
141
+
142
+ win.cursor_y = 0
143
+ feed("'", "a")
144
+ assert_equal 1, win.cursor_y
145
+ end
146
+
147
+ def test_mark_jump_unset
148
+ buf.replace_all_lines!(["line1"])
149
+ feed("`", "z")
150
+ assert_match(/Mark not set/, @editor.message)
151
+ end
152
+
153
+ def test_mark_pending_escape_cancels
154
+ buf.replace_all_lines!(["hello"])
155
+ feed("m", "\e")
156
+ assert_equal :normal, @editor.mode
157
+ end
158
+
159
+ def test_mark_pending_invalid_char
160
+ buf.replace_all_lines!(["hello"])
161
+ feed("m", " ")
162
+ assert_equal :normal, @editor.mode
163
+ end
164
+
165
+ # --- jump list ---
166
+
167
+ def test_jump_older_and_newer
168
+ buf.replace_all_lines!((1..20).map { |i| "line#{i}" })
169
+ win.cursor_y = 0
170
+
171
+ feed("G")
172
+ last_line = buf.line_count - 1
173
+ assert_equal last_line, win.cursor_y
174
+
175
+ feed(:ctrl_o)
176
+ old_y = win.cursor_y
177
+
178
+ feed(:ctrl_i)
179
+ new_y = win.cursor_y
180
+ assert_operator new_y, :>=, old_y
181
+ end
182
+
183
+ def test_jump_older_empty
184
+ buf.replace_all_lines!(["line1"])
185
+ @editor.instance_variable_get(:@jump_list)&.clear rescue nil
186
+ feed(:ctrl_o)
187
+ assert_match(/Jump list/, @editor.message.to_s) if @editor.message
188
+ end
189
+
190
+ def test_backtick_backtick_jumps_older
191
+ buf.replace_all_lines!((1..10).map { |i| "line#{i}" })
192
+ win.cursor_y = 0
193
+ feed("G")
194
+ feed("`", "`")
195
+ assert_equal 0, win.cursor_y
196
+ end
197
+
198
+ def test_jump_pending_escape_cancels
199
+ buf.replace_all_lines!(["hello"])
200
+ feed("'", "\e")
201
+ assert_equal :normal, @editor.mode
202
+ end
203
+
204
+ def test_jump_pending_invalid_mark
205
+ buf.replace_all_lines!(["hello"])
206
+ feed("'", " ")
207
+ assert_equal :normal, @editor.mode
208
+ end
209
+
210
+ # --- visual mode ---
211
+
212
+ def test_visual_line_yank
213
+ buf.replace_all_lines!(["aaa", "bbb", "ccc"])
214
+ win.cursor_y = 0
215
+ feed("V", "j", "y")
216
+
217
+ reg = @editor.get_register("\"")
218
+ assert_equal :normal, @editor.mode
219
+ assert_includes reg[:text], "aaa"
220
+ assert_includes reg[:text], "bbb"
221
+ end
222
+
223
+ def test_visual_line_delete
224
+ buf.replace_all_lines!(["aaa", "bbb", "ccc"])
225
+ win.cursor_y = 0
226
+ feed("V", "j", "d")
227
+
228
+ assert_equal ["ccc"], buf.lines
229
+ assert_equal :normal, @editor.mode
230
+ end
231
+
232
+ def test_visual_char_delete
233
+ buf.replace_all_lines!(["abcdef"])
234
+ win.cursor_x = 1
235
+ feed("v", "l", "l", "d")
236
+
237
+ assert_equal ["aef"], buf.lines
238
+ assert_equal :normal, @editor.mode
239
+ end
240
+
241
+ def test_visual_select_text_object_iw
242
+ buf.replace_all_lines!(["hello world"])
243
+ win.cursor_x = 0
244
+ feed("v", "i", "w")
245
+ assert_equal :visual_char, @editor.mode
246
+ end
247
+
248
+ # --- delete operator ---
249
+
250
+ def test_delete_gg_motion
251
+ buf.replace_all_lines!(["aaa", "bbb", "ccc"])
252
+ win.cursor_y = 2
253
+ feed("d", "g", "g")
254
+ assert_equal [""], buf.lines
255
+ end
256
+
257
+ def test_delete_j_motion
258
+ buf.replace_all_lines!(["aaa", "bbb", "ccc"])
259
+ win.cursor_y = 0
260
+ feed("d", "j")
261
+ assert_equal ["ccc"], buf.lines
262
+ end
263
+
264
+ def test_delete_k_motion
265
+ buf.replace_all_lines!(["aaa", "bbb", "ccc"])
266
+ win.cursor_y = 1
267
+ feed("d", "k")
268
+ assert_equal ["ccc"], buf.lines
269
+ end
270
+
271
+ def test_delete_word_dw
272
+ buf.replace_all_lines!(["hello world"])
273
+ win.cursor_x = 0
274
+ feed("d", "w")
275
+ assert_equal ["world"], buf.lines
276
+ end
277
+
278
+ def test_delete_aw
279
+ buf.replace_all_lines!(["hello world"])
280
+ win.cursor_x = 0
281
+ feed("d", "a", "w")
282
+ assert_equal ["world"], buf.lines
283
+ end
284
+
285
+ # --- yank operator ---
286
+
287
+ def test_yank_word_yw
288
+ buf.replace_all_lines!(["hello world"])
289
+ win.cursor_x = 0
290
+ feed("y", "w")
291
+ reg = @editor.get_register("\"")
292
+ assert_equal "hello ", reg[:text]
293
+ end
294
+
295
+ def test_yank_gg_motion
296
+ buf.replace_all_lines!(["aaa", "bbb", "ccc"])
297
+ win.cursor_y = 2
298
+ feed("y", "g", "g")
299
+ reg = @editor.get_register("\"")
300
+ assert_includes reg[:text], "aaa"
301
+ assert_includes reg[:text], "ccc"
302
+ end
303
+
304
+ def test_yank_iw
305
+ buf.replace_all_lines!(["hello world"])
306
+ win.cursor_x = 0
307
+ feed("y", "i", "w")
308
+ reg = @editor.get_register("\"")
309
+ assert_equal "hello", reg[:text]
310
+ end
311
+
312
+ def test_yank_aw
313
+ buf.replace_all_lines!(["hello world"])
314
+ win.cursor_x = 0
315
+ feed("y", "a", "w")
316
+ reg = @editor.get_register("\"")
317
+ assert_equal "hello ", reg[:text]
318
+ end
319
+
320
+ # --- indent operator ---
321
+
322
+ def test_indent_j_motion
323
+ buf.replace_all_lines!([" aaa", " bbb", "ccc"])
324
+ win.cursor_y = 0
325
+ feed("=", "j")
326
+ assert_equal :normal, @editor.mode
327
+ end
328
+
329
+ def test_indent_gg_motion
330
+ buf.replace_all_lines!([" aaa", " bbb"])
331
+ win.cursor_y = 1
332
+ feed("=", "g", "g")
333
+ assert_equal :normal, @editor.mode
334
+ end
335
+
336
+ def test_indent_k_motion
337
+ buf.replace_all_lines!(["aaa", " bbb", "ccc"])
338
+ win.cursor_y = 1
339
+ feed("=", "k")
340
+ assert_equal :normal, @editor.mode
341
+ end
342
+
343
+ def test_indent_G_motion
344
+ buf.replace_all_lines!([" aaa", " bbb"])
345
+ win.cursor_y = 0
346
+ feed("=", "G")
347
+ assert_equal :normal, @editor.mode
348
+ end
349
+
350
+ # --- arrow and page keys ---
351
+
352
+ def test_arrow_keys_in_normal_mode
353
+ buf.replace_all_lines!(["abc", "def"])
354
+ win.cursor_y = 0
355
+ win.cursor_x = 0
356
+ feed(:right)
357
+ assert_equal 1, win.cursor_x
358
+ feed(:down)
359
+ assert_equal 1, win.cursor_y
360
+ feed(:left)
361
+ assert_equal 0, win.cursor_x
362
+ feed(:up)
363
+ assert_equal 0, win.cursor_y
364
+ end
365
+
366
+ def test_page_keys_in_normal_mode
367
+ buf.replace_all_lines!((1..30).map { |i| "line#{i}" })
368
+ @editor.current_window_view_height_hint = 10
369
+ win.cursor_y = 0
370
+ feed(:pagedown)
371
+ assert_operator win.cursor_y, :>, 0
372
+ feed(:pageup)
373
+ assert_equal 0, win.cursor_y
374
+ end
375
+
376
+ # --- escape clears message ---
377
+
378
+ def test_escape_clears_message
379
+ @editor.echo("test message")
380
+ feed("\e")
381
+ end
382
+ end
@@ -6,6 +6,8 @@ class AppCompletionTest < Minitest::Test
6
6
  def setup
7
7
  @app = RuVim::App.new
8
8
  @editor = @app.instance_variable_get(:@editor)
9
+ @key_handler = @app.instance_variable_get(:@key_handler)
10
+ @completion = @app.instance_variable_get(:@completion)
9
11
  end
10
12
 
11
13
  def test_app_starts_with_intro_buffer_without_path
@@ -20,7 +22,7 @@ class AppCompletionTest < Minitest::Test
20
22
  cmd = @editor.command_line
21
23
  cmd.replace_text("set nu")
22
24
 
23
- @app.send(:command_line_complete)
25
+ @completion.command_line_complete
24
26
 
25
27
  assert_equal "set number", cmd.text
26
28
  end
@@ -33,7 +35,7 @@ class AppCompletionTest < Minitest::Test
33
35
  @editor.current_window.cursor_x = 2
34
36
  @editor.enter_insert_mode
35
37
 
36
- @app.send(:handle_insert_key, :ctrl_n)
38
+ @key_handler.send(:handle_insert_key, :ctrl_n)
37
39
 
38
40
  assert_equal "foobar", b.line_at(0)
39
41
  assert_equal 6, @editor.current_window.cursor_x
@@ -45,7 +47,7 @@ class AppCompletionTest < Minitest::Test
45
47
  File.write(File.join(dir, "a.o"), "")
46
48
  @editor.set_option("wildignore", "*.o", scope: :global)
47
49
 
48
- matches = @app.send(:path_completion_candidates, File.join(dir, "a"))
50
+ matches = @completion.send(:path_completion_candidates, File.join(dir, "a"))
49
51
 
50
52
  assert_includes matches, File.join(dir, "a.rb")
51
53
  refute_includes matches, File.join(dir, "a.o")
@@ -57,7 +59,7 @@ class AppCompletionTest < Minitest::Test
57
59
  Dir.chdir(dir) do
58
60
  FileUtils.mkdir_p("lib")
59
61
 
60
- matches = @app.send(:path_completion_candidates, "li")
62
+ matches = @completion.send(:path_completion_candidates, "li")
61
63
 
62
64
  assert_includes matches, "lib/"
63
65
  end
@@ -70,7 +72,7 @@ class AppCompletionTest < Minitest::Test
70
72
  File.write(File.join(dir, "aaa.txt"), "")
71
73
  File.write(File.join(dir, "bbb.txt"), "")
72
74
 
73
- matches = @app.send(:path_completion_candidates, File.join(dir, ""))
75
+ matches = @completion.send(:path_completion_candidates, File.join(dir, ""))
74
76
 
75
77
  assert_includes matches, File.join(dir, ".git/")
76
78
  visible_idx = matches.index(File.join(dir, "aaa.txt"))
@@ -96,10 +98,10 @@ class AppCompletionTest < Minitest::Test
96
98
  cmd = @editor.command_line
97
99
  cmd.replace_text("e #{File.join(dir, "a")}")
98
100
 
99
- @app.send(:command_line_complete)
101
+ @completion.command_line_complete
100
102
  first = cmd.text.dup
101
103
  first_msg = @editor.message.dup
102
- @app.send(:command_line_complete)
104
+ @completion.command_line_complete
103
105
  second = cmd.text.dup
104
106
  second_msg = @editor.message.dup
105
107
 
@@ -125,10 +127,10 @@ class AppCompletionTest < Minitest::Test
125
127
  cmd = @editor.command_line
126
128
  cmd.replace_text("e #{File.join(dir, "a")}")
127
129
 
128
- @app.send(:command_line_complete)
130
+ @completion.command_line_complete
129
131
  assert_equal "e #{File.join(dir, "a")}", cmd.text
130
132
 
131
- @app.send(:command_line_complete)
133
+ @completion.command_line_complete
132
134
  assert([a, b].any? { |p| cmd.text.end_with?(p) })
133
135
  end
134
136
  end
@@ -148,9 +150,9 @@ class AppCompletionTest < Minitest::Test
148
150
  cmd = @editor.command_line
149
151
  cmd.replace_text("e #{File.join(dir, "a")}")
150
152
 
151
- @app.send(:command_line_complete)
153
+ @completion.command_line_complete
152
154
  first = cmd.text.dup
153
- @app.send(:command_line_complete)
155
+ @completion.command_line_complete
154
156
  second = cmd.text.dup
155
157
 
156
158
  refute_equal first, second
@@ -168,7 +170,7 @@ class AppCompletionTest < Minitest::Test
168
170
  cmd = @editor.command_line
169
171
  cmd.replace_text("se")
170
172
 
171
- @app.send(:command_line_complete)
173
+ @completion.command_line_complete
172
174
 
173
175
  assert_equal "se", cmd.text
174
176
  assert_includes @editor.message, "set"
@@ -186,11 +188,11 @@ class AppCompletionTest < Minitest::Test
186
188
  @editor.current_window.cursor_x = 2
187
189
  @editor.enter_insert_mode
188
190
 
189
- @app.send(:handle_insert_key, :ctrl_n)
191
+ @key_handler.send(:handle_insert_key, :ctrl_n)
190
192
  assert_equal "fo", b.line_at(0)
191
193
  assert_includes @editor.message, "..."
192
194
 
193
- @app.send(:handle_insert_key, :ctrl_n)
195
+ @key_handler.send(:handle_insert_key, :ctrl_n)
194
196
  assert_equal "foobar", b.line_at(0)
195
197
  end
196
198
 
@@ -203,11 +205,11 @@ class AppCompletionTest < Minitest::Test
203
205
  @editor.current_window.cursor_x = 2
204
206
  @editor.enter_insert_mode
205
207
 
206
- @app.send(:handle_insert_key, :ctrl_n)
208
+ @key_handler.send(:handle_insert_key, :ctrl_n)
207
209
  assert_equal "fo", b.line_at(0)
208
210
  assert_includes @editor.message, "["
209
211
 
210
- @app.send(:handle_insert_key, :ctrl_n)
212
+ @key_handler.send(:handle_insert_key, :ctrl_n)
211
213
  assert_equal "foobar", b.line_at(0)
212
214
  end
213
215
 
@@ -217,7 +219,7 @@ class AppCompletionTest < Minitest::Test
217
219
  cmd = @editor.command_line
218
220
  cmd.replace_text("git bl")
219
221
 
220
- @app.send(:command_line_complete)
222
+ @completion.command_line_complete
221
223
 
222
224
  # "bl" matches "blame", "blameback", "blamecommit", "blameprev", "branch"
223
225
  # With multiple matches, wildmode applies (longest common prefix or cycle)
@@ -229,10 +231,32 @@ class AppCompletionTest < Minitest::Test
229
231
  @editor.materialize_intro_buffer!
230
232
  @editor.enter_command_line_mode(":")
231
233
  cmd = @editor.command_line
232
- cmd.replace_text("git co")
234
+ cmd.replace_text("git commi")
233
235
 
234
- @app.send(:command_line_complete)
236
+ @completion.command_line_complete
235
237
 
236
238
  assert_equal "git commit", cmd.text
237
239
  end
240
+
241
+ def test_git_subcommand_completion_includes_external
242
+ @editor.materialize_intro_buffer!
243
+ @editor.enter_command_line_mode(":")
244
+ cmd = @editor.command_line
245
+ cmd.replace_text("git stas")
246
+
247
+ @completion.command_line_complete
248
+
249
+ assert_equal "git stash", cmd.text
250
+ end
251
+
252
+ def test_gh_subcommand_completion_includes_external
253
+ @editor.materialize_intro_buffer!
254
+ @editor.enter_command_line_mode(":")
255
+ cmd = @editor.command_line
256
+ cmd.replace_text("gh issu")
257
+
258
+ @completion.command_line_complete
259
+
260
+ assert_equal "gh issue", cmd.text
261
+ end
238
262
  end
@@ -4,13 +4,14 @@ class AppDotRepeatTest < 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_dot_repeats_x
@@ -57,11 +58,34 @@ class AppDotRepeatTest < Minitest::Test
57
58
  @win.cursor_x = 0
58
59
 
59
60
  press("s")
60
- @app.send(:handle_key, "X")
61
- @app.send(:handle_key, :escape)
61
+ @key_handler.handle("X")
62
+ @key_handler.handle(:escape)
62
63
  press("l")
63
64
  press(".")
64
65
 
65
66
  assert_equal "XbXd", @buffer.line_at(0)
66
67
  end
68
+
69
+ def test_dot_repeats_counted_x
70
+ @buffer.replace_all_lines!(["abcdefgh"])
71
+ @win.cursor_x = 0
72
+
73
+ @key_handler.handle("3")
74
+ press("x")
75
+ assert_equal "defgh", @buffer.line_at(0)
76
+
77
+ press(".")
78
+ assert_equal "gh", @buffer.line_at(0)
79
+ end
80
+
81
+ def test_dot_repeats_counted_dd
82
+ @buffer.replace_all_lines!(["a", "b", "c", "d", "e", "f"])
83
+
84
+ @key_handler.handle("2")
85
+ press("d", "d")
86
+ assert_equal ["c", "d", "e", "f"], @buffer.lines
87
+
88
+ press(".")
89
+ assert_equal ["e", "f"], @buffer.lines
90
+ end
67
91
  end