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
@@ -9,17 +9,20 @@ class GitBlameTest < Minitest::Test
9
9
  @app = RuVim::App.new(clean: true)
10
10
  @editor = @app.instance_variable_get(:@editor)
11
11
  @dispatcher = @app.instance_variable_get(:@dispatcher)
12
+ @key_handler = @app.instance_variable_get(:@key_handler)
13
+ @stream_mixer = @app.instance_variable_get(:@stream_mixer)
12
14
  @editor.materialize_intro_buffer!
13
15
  end
14
16
 
15
17
  def feed(*keys)
16
- keys.each { |k| @app.send(:handle_key, k) }
18
+ keys.each { |k| @key_handler.handle(k) }
17
19
  end
18
20
 
19
21
  def drain_git_stream!
20
- threads = @app.instance_variable_get(:@git_stream_threads)
21
- threads&.each_value(&:join)
22
- @app.send(:drain_stream_events!)
22
+ @editor.buffers.each_value do |buf|
23
+ buf.stream&.thread&.join
24
+ end
25
+ @stream_mixer.drain_events!
23
26
  end
24
27
 
25
28
  # --- Parsing ---
@@ -81,12 +84,66 @@ class GitBlameTest < Minitest::Test
81
84
  { short_hash: "abc12345", author: "Alice", date: "2023-11-14", text: "hello", orig_line: 1, hash: "abc12345" * 5 },
82
85
  { short_hash: "def67890", author: "Bob", date: "2023-11-15", text: "world", orig_line: 2, hash: "def67890" * 5 },
83
86
  ]
84
- lines = RuVim::Git::Blame.format_lines(entries)
87
+ lines, labels = RuVim::Git::Blame.format_lines(entries)
85
88
  assert_equal 2, lines.length
86
- assert_includes lines[0], "abc12345"
87
- assert_includes lines[0], "Alice"
88
- assert_includes lines[0], "hello"
89
- assert_includes lines[1], "Bob"
89
+ assert_equal "hello", lines[0]
90
+ assert_equal "world", lines[1]
91
+ assert_equal 2, labels.length
92
+ assert_includes labels[0], "abc12345"
93
+ assert_includes labels[0], "Alice"
94
+ assert_includes labels[1], "Bob"
95
+ end
96
+
97
+ def test_blame_buffer_has_source_filetype
98
+ Dir.mktmpdir do |dir|
99
+ setup_git_repo(dir, "test_file.rb", "puts 'hello'\n")
100
+
101
+ file_path = File.join(dir, "test_file.rb")
102
+ buf = @editor.add_buffer_from_file(file_path)
103
+ @editor.switch_to_buffer(buf.id)
104
+ assert_equal "ruby", buf.options["filetype"]
105
+
106
+ @dispatcher.dispatch_ex(@editor, "git blame")
107
+
108
+ blame_buf = @editor.current_buffer
109
+ assert_equal :blame, blame_buf.kind
110
+ assert_equal "ruby", blame_buf.options["filetype"]
111
+ end
112
+ end
113
+
114
+ def test_blame_buffer_has_gutter_labels
115
+ Dir.mktmpdir do |dir|
116
+ setup_git_repo(dir, "test_file.txt", "line1\nline2\n")
117
+
118
+ file_path = File.join(dir, "test_file.txt")
119
+ buf = @editor.add_buffer_from_file(file_path)
120
+ @editor.switch_to_buffer(buf.id)
121
+
122
+ @dispatcher.dispatch_ex(@editor, "git blame")
123
+
124
+ blame_buf = @editor.current_buffer
125
+ labels = blame_buf.options["gutter_labels"]
126
+ assert_kind_of Array, labels
127
+ assert_equal 2, labels.length
128
+ # Labels should contain hash and author info
129
+ assert_match(/\A[0-9a-f]{8} /, labels[0])
130
+ end
131
+ end
132
+
133
+ def test_blame_buffer_lines_are_code_only
134
+ Dir.mktmpdir do |dir|
135
+ setup_git_repo(dir, "test_file.txt", "line1\nline2\n")
136
+
137
+ file_path = File.join(dir, "test_file.txt")
138
+ buf = @editor.add_buffer_from_file(file_path)
139
+ @editor.switch_to_buffer(buf.id)
140
+
141
+ @dispatcher.dispatch_ex(@editor, "git blame")
142
+
143
+ blame_buf = @editor.current_buffer
144
+ assert_equal "line1", blame_buf.line_at(0)
145
+ assert_equal "line2", blame_buf.line_at(1)
146
+ end
90
147
  end
91
148
 
92
149
  # --- Status filename parsing ---
@@ -221,9 +278,14 @@ class GitBlameTest < Minitest::Test
221
278
  end
222
279
  end
223
280
 
224
- def test_git_unknown_subcommand_shows_error
225
- @dispatcher.dispatch_ex(@editor, "git unknown")
226
- assert_match(/Unknown Git subcommand/, @editor.message)
281
+ def test_git_unknown_subcommand_runs_shell
282
+ executed = nil
283
+ fake_status = Struct.new(:exitstatus).new(0)
284
+ @editor.shell_executor = ->(cmd) { executed = cmd; fake_status }
285
+
286
+ @dispatcher.dispatch_ex(@editor, "git stash")
287
+
288
+ assert_equal "git stash", executed
227
289
  end
228
290
 
229
291
  def test_git_no_subcommand_shows_list
@@ -485,10 +547,9 @@ class GitBlameTest < Minitest::Test
485
547
  assert_nil RuVim::Git::Branch.parse_branch_name("")
486
548
  end
487
549
 
488
- def test_git_branch_enter_checks_out
550
+ def test_git_branch_enter_populates_checkout_command
489
551
  Dir.mktmpdir do |dir|
490
552
  setup_git_repo(dir, "test_file.txt", "line1\n")
491
- # Create a second branch
492
553
  Dir.chdir(dir) do
493
554
  system("git branch test-branch", exception: true)
494
555
  end
@@ -507,9 +568,27 @@ class GitBlameTest < Minitest::Test
507
568
 
508
569
  feed(:enter)
509
570
 
510
- # Branch buffer should be closed, and we should have switched
511
- refute_equal :git_branch, @editor.current_buffer.kind
512
- # Verify checkout happened
571
+ # Should enter command-line mode with checkout command pre-filled
572
+ assert_equal :command_line, @editor.mode
573
+ assert_equal "git checkout test-branch", @editor.command_line.text
574
+ end
575
+ end
576
+
577
+ def test_git_checkout_subcommand_executes
578
+ Dir.mktmpdir do |dir|
579
+ setup_git_repo(dir, "test_file.txt", "line1\n")
580
+ Dir.chdir(dir) do
581
+ system("git branch test-branch", exception: true)
582
+ end
583
+
584
+ file_path = File.join(dir, "test_file.txt")
585
+ buf = @editor.add_buffer_from_file(file_path)
586
+ @editor.switch_to_buffer(buf.id)
587
+
588
+ @dispatcher.dispatch_ex(@editor, "git checkout test-branch")
589
+
590
+ refute @editor.message_error?, "Unexpected error: #{@editor.message}"
591
+ assert_includes @editor.message.to_s, "Switched to branch"
513
592
  Dir.chdir(dir) do
514
593
  current = `git rev-parse --abbrev-ref HEAD`.strip
515
594
  assert_equal "test-branch", current
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "test_helper"
4
+
5
+ class GitGrepTest < Minitest::Test
6
+ def setup
7
+ @app = RuVim::App.new(clean: true)
8
+ @editor = @app.instance_variable_get(:@editor)
9
+ @dispatcher = @app.instance_variable_get(:@dispatcher)
10
+ @key_handler = @app.instance_variable_get(:@key_handler)
11
+ @editor.materialize_intro_buffer!
12
+ end
13
+
14
+ def feed(*keys)
15
+ keys.each { |k| @key_handler.handle(k) }
16
+ end
17
+
18
+ # --- Parsing ---
19
+
20
+ def test_parse_location_basic
21
+ line = "lib/ruvim/app.rb:42: def run"
22
+ result = RuVim::Git::Grep.parse_location(line)
23
+ assert_equal ["lib/ruvim/app.rb", 42], result
24
+ end
25
+
26
+ def test_parse_location_no_line_number
27
+ line = "lib/ruvim/app.rb: def run"
28
+ result = RuVim::Git::Grep.parse_location(line)
29
+ assert_nil result
30
+ end
31
+
32
+ def test_parse_location_empty
33
+ result = RuVim::Git::Grep.parse_location("")
34
+ assert_nil result
35
+ end
36
+
37
+ def test_parse_location_separator_line
38
+ result = RuVim::Git::Grep.parse_location("--")
39
+ assert_nil result
40
+ end
41
+
42
+ def test_parse_location_with_colon_in_content
43
+ line = "config.rb:10: url = \"http://example.com\""
44
+ result = RuVim::Git::Grep.parse_location(line)
45
+ assert_equal ["config.rb", 10], result
46
+ end
47
+
48
+ def test_parse_location_windows_style_path
49
+ line = "src/main.rb:5:hello"
50
+ result = RuVim::Git::Grep.parse_location(line)
51
+ assert_equal ["src/main.rb", 5], result
52
+ end
53
+
54
+ # --- Command registration ---
55
+
56
+ def test_git_grep_subcommand_registered
57
+ assert RuVim::Git::Handler::GIT_SUBCOMMANDS.key?("grep")
58
+ end
59
+
60
+ def test_git_grep_open_file_command_registered
61
+ cmd = RuVim::CommandRegistry.instance
62
+ assert cmd.registered?("git.grep.open_file")
63
+ end
64
+ end
@@ -178,4 +178,129 @@ class HighlighterTest < Minitest::Test
178
178
  refute_empty cols
179
179
  assert_equal "\e[33m", cols[0]
180
180
  end
181
+
182
+ # --- C ---
183
+
184
+ def test_c_keyword_if
185
+ cols = RuVim::Highlighter.color_columns("c", "if (x > 0) {")
186
+ assert_equal "\e[36m", cols[0] # "if"
187
+ assert_equal "\e[36m", cols[1]
188
+ end
189
+
190
+ def test_c_keyword_return
191
+ cols = RuVim::Highlighter.color_columns("c", " return 0;")
192
+ assert_equal "\e[36m", cols[2] # "return"
193
+ end
194
+
195
+ def test_c_type_keyword
196
+ cols = RuVim::Highlighter.color_columns("c", "int main(void) {")
197
+ assert_equal "\e[36m", cols[0] # "int"
198
+ assert_equal "\e[36m", cols[9] # "void"
199
+ end
200
+
201
+ def test_c_string
202
+ cols = RuVim::Highlighter.color_columns("c", 'printf("hello");')
203
+ assert_equal "\e[32m", cols[7] # opening quote
204
+ assert_equal "\e[32m", cols[13] # closing quote
205
+ end
206
+
207
+ def test_c_char_literal
208
+ cols = RuVim::Highlighter.color_columns("c", "char c = 'a';")
209
+ assert_equal "\e[32m", cols[9] # opening quote
210
+ assert_equal "\e[32m", cols[11] # closing quote
211
+ end
212
+
213
+ def test_c_number_decimal
214
+ cols = RuVim::Highlighter.color_columns("c", "int x = 42;")
215
+ assert_equal "\e[33m", cols[8] # "4"
216
+ assert_equal "\e[33m", cols[9] # "2"
217
+ end
218
+
219
+ def test_c_number_hex
220
+ cols = RuVim::Highlighter.color_columns("c", "int x = 0xFF;")
221
+ assert_equal "\e[33m", cols[8] # "0"
222
+ assert_equal "\e[33m", cols[11] # "F"
223
+ end
224
+
225
+ def test_c_line_comment
226
+ cols = RuVim::Highlighter.color_columns("c", "x = 1; // comment")
227
+ assert_equal "\e[90m", cols[7] # "//"
228
+ assert_equal "\e[90m", cols[16] # end of comment
229
+ end
230
+
231
+ def test_c_block_comment_single_line
232
+ cols = RuVim::Highlighter.color_columns("c", "x = 1; /* comment */")
233
+ assert_equal "\e[90m", cols[7] # "/*"
234
+ assert_equal "\e[90m", cols[19] # "*/"
235
+ end
236
+
237
+ def test_c_preprocessor
238
+ cols = RuVim::Highlighter.color_columns("c", "#include <stdio.h>")
239
+ assert_equal "\e[35m", cols[0] # "#"
240
+ assert_equal "\e[35m", cols[7] # "e" of include
241
+ end
242
+
243
+ def test_c_define_preprocessor
244
+ cols = RuVim::Highlighter.color_columns("c", "#define MAX 100")
245
+ assert_equal "\e[35m", cols[0] # "#"
246
+ end
247
+
248
+ def test_c_constant_macro
249
+ cols = RuVim::Highlighter.color_columns("c", "if (ptr == NULL) {")
250
+ assert_equal "\e[96m", cols[11] # "N" of NULL
251
+ end
252
+
253
+ def test_c_all_caps_identifier
254
+ cols = RuVim::Highlighter.color_columns("c", "x = MAX_SIZE;")
255
+ assert_equal "\e[96m", cols[4] # "M"
256
+ end
257
+
258
+ def test_c_empty_line
259
+ cols = RuVim::Highlighter.color_columns("c", "")
260
+ assert_empty cols
261
+ end
262
+
263
+ # --- C++ ---
264
+
265
+ def test_cpp_keyword_class
266
+ cols = RuVim::Highlighter.color_columns("cpp", "class Foo {")
267
+ assert_equal "\e[36m", cols[0] # "class"
268
+ assert_equal "\e[36m", cols[4]
269
+ end
270
+
271
+ def test_cpp_keyword_namespace
272
+ cols = RuVim::Highlighter.color_columns("cpp", "namespace std {")
273
+ assert_equal "\e[36m", cols[0] # "namespace"
274
+ end
275
+
276
+ def test_cpp_keyword_template
277
+ cols = RuVim::Highlighter.color_columns("cpp", "template <typename T>")
278
+ assert_equal "\e[36m", cols[0] # "template"
279
+ assert_equal "\e[36m", cols[10] # "typename"
280
+ end
281
+
282
+ def test_cpp_keyword_nullptr
283
+ cols = RuVim::Highlighter.color_columns("cpp", "int* p = nullptr;")
284
+ assert_equal "\e[36m", cols[9] # "nullptr"
285
+ end
286
+
287
+ def test_cpp_keyword_auto
288
+ cols = RuVim::Highlighter.color_columns("cpp", "auto x = 42;")
289
+ assert_equal "\e[36m", cols[0] # "auto"
290
+ end
291
+
292
+ def test_cpp_inherits_c_string
293
+ cols = RuVim::Highlighter.color_columns("cpp", 'std::cout << "hello";')
294
+ assert_equal "\e[32m", cols[13] # opening quote
295
+ end
296
+
297
+ def test_cpp_inherits_c_comment
298
+ cols = RuVim::Highlighter.color_columns("cpp", "x = 1; // comment")
299
+ assert_equal "\e[90m", cols[7] # "//"
300
+ end
301
+
302
+ def test_cpp_empty_line
303
+ cols = RuVim::Highlighter.color_columns("cpp", "")
304
+ assert_empty cols
305
+ end
181
306
  end
data/test/indent_test.rb CHANGED
@@ -285,3 +285,140 @@ class RubyIndentTest < Minitest::Test
285
285
  assert_equal 0, calc(lines, 3) # end
286
286
  end
287
287
  end
288
+
289
+ class CIndentTest < Minitest::Test
290
+ def calc(lines, target_row, sw = 2)
291
+ RuVim::Lang::C.calculate_indent(lines, target_row, sw)
292
+ end
293
+
294
+ def test_first_line_is_zero
295
+ assert_equal 0, calc(["int main() {"], 0)
296
+ end
297
+
298
+ def test_after_open_brace
299
+ lines = ["int main() {", " return 0;"]
300
+ assert_equal 2, calc(lines, 1)
301
+ end
302
+
303
+ def test_close_brace
304
+ lines = ["int main() {", " return 0;", "}"]
305
+ assert_equal 0, calc(lines, 2)
306
+ end
307
+
308
+ def test_nested_braces
309
+ lines = [
310
+ "void foo() {",
311
+ " if (x) {",
312
+ " bar();",
313
+ " }",
314
+ "}"
315
+ ]
316
+ assert_equal 2, calc(lines, 1)
317
+ assert_equal 4, calc(lines, 2)
318
+ assert_equal 2, calc(lines, 3)
319
+ assert_equal 0, calc(lines, 4)
320
+ end
321
+
322
+ def test_switch_case
323
+ lines = [
324
+ "switch (x) {",
325
+ "case 1:",
326
+ " foo();",
327
+ " break;",
328
+ "case 2:",
329
+ " bar();",
330
+ " break;",
331
+ "default:",
332
+ " baz();",
333
+ "}"
334
+ ]
335
+ assert_equal 0, calc(lines, 1) # case 1:
336
+ assert_equal 2, calc(lines, 2) # foo()
337
+ assert_equal 2, calc(lines, 3) # break
338
+ assert_equal 0, calc(lines, 4) # case 2:
339
+ assert_equal 2, calc(lines, 5) # bar()
340
+ assert_equal 0, calc(lines, 7) # default:
341
+ assert_equal 2, calc(lines, 8) # baz()
342
+ assert_equal 0, calc(lines, 9) # }
343
+ end
344
+
345
+ def test_shiftwidth_4
346
+ lines = ["void foo() {", " bar();", "}"]
347
+ assert_equal 4, calc(lines, 1, 4)
348
+ assert_equal 0, calc(lines, 2, 4)
349
+ end
350
+
351
+ def test_indent_trigger_open_brace
352
+ assert RuVim::Lang::C.indent_trigger?("int main() {")
353
+ assert RuVim::Lang::C.indent_trigger?("if (x) {")
354
+ end
355
+
356
+ def test_indent_trigger_no_trigger
357
+ refute RuVim::Lang::C.indent_trigger?("return 0;")
358
+ refute RuVim::Lang::C.indent_trigger?("}")
359
+ end
360
+
361
+ def test_dedent_trigger_close_brace
362
+ assert_kind_of Regexp, RuVim::Lang::C.dedent_trigger("}")
363
+ end
364
+
365
+ def test_dedent_trigger_no_trigger
366
+ assert_nil RuVim::Lang::C.dedent_trigger("a")
367
+ end
368
+ end
369
+
370
+ class CppIndentTest < Minitest::Test
371
+ def calc(lines, target_row, sw = 2)
372
+ RuVim::Lang::Cpp.calculate_indent(lines, target_row, sw)
373
+ end
374
+
375
+ def test_class_body
376
+ lines = [
377
+ "class Foo {",
378
+ " int x;",
379
+ "};"
380
+ ]
381
+ assert_equal 2, calc(lines, 1)
382
+ assert_equal 0, calc(lines, 2)
383
+ end
384
+
385
+ def test_namespace_body
386
+ lines = [
387
+ "namespace ns {",
388
+ " class Bar {",
389
+ " void f();",
390
+ " };",
391
+ "}"
392
+ ]
393
+ assert_equal 2, calc(lines, 1)
394
+ assert_equal 4, calc(lines, 2)
395
+ assert_equal 2, calc(lines, 3)
396
+ assert_equal 0, calc(lines, 4)
397
+ end
398
+
399
+ def test_access_specifier_dedent
400
+ lines = [
401
+ "class Foo {",
402
+ "public:",
403
+ " int x;",
404
+ "private:",
405
+ " int y;",
406
+ "};"
407
+ ]
408
+ assert_equal 0, calc(lines, 1) # public:
409
+ assert_equal 2, calc(lines, 2) # int x
410
+ assert_equal 0, calc(lines, 3) # private:
411
+ assert_equal 2, calc(lines, 4) # int y
412
+ assert_equal 0, calc(lines, 5) # };
413
+ end
414
+
415
+ def test_indent_trigger
416
+ assert RuVim::Lang::Cpp.indent_trigger?("class Foo {")
417
+ assert RuVim::Lang::Cpp.indent_trigger?("namespace ns {")
418
+ end
419
+
420
+ def test_dedent_trigger
421
+ assert_kind_of Regexp, RuVim::Lang::Cpp.dedent_trigger("}")
422
+ assert_nil RuVim::Lang::Cpp.dedent_trigger("x")
423
+ end
424
+ end
@@ -62,7 +62,7 @@ class InputScreenIntegrationTest < Minitest::Test
62
62
  key = input.read_key(timeout: 0.2)
63
63
  assert_equal :pagedown, key
64
64
 
65
- app.send(:handle_normal_key, key)
65
+ app.instance_variable_get(:@key_handler).send(:handle_normal_key, key)
66
66
  screen.render(editor)
67
67
 
68
68
  assert_operator editor.current_window.cursor_y, :>, 0
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "test_helper"
4
+
5
+ class KeywordCharsTest < Minitest::Test
6
+ def setup
7
+ # Clear caches between tests
8
+ RuVim::KeywordChars.instance_variable_set(:@char_class_cache, nil)
9
+ RuVim::KeywordChars.instance_variable_set(:@regex_cache, nil)
10
+ end
11
+
12
+ # --- char_class ---
13
+
14
+ def test_char_class_default_for_empty
15
+ assert_equal "[:alnum:]_", RuVim::KeywordChars.char_class("")
16
+ end
17
+
18
+ def test_char_class_default_for_nil
19
+ assert_equal "[:alnum:]_", RuVim::KeywordChars.char_class(nil)
20
+ end
21
+
22
+ def test_char_class_single_char
23
+ result = RuVim::KeywordChars.char_class("-")
24
+ assert_includes result, "[:alnum:]_"
25
+ assert_includes result, "\\-"
26
+ end
27
+
28
+ def test_char_class_range
29
+ result = RuVim::KeywordChars.char_class("65-90")
30
+ assert_includes result, "[:alnum:]_"
31
+ # Should include A-Z range
32
+ assert_includes result, "A-Z"
33
+ end
34
+
35
+ def test_char_class_reversed_range
36
+ # minmax should handle reversed ranges
37
+ result = RuVim::KeywordChars.char_class("90-65")
38
+ assert_includes result, "A-Z"
39
+ end
40
+
41
+ def test_char_class_skips_at_sign
42
+ result = RuVim::KeywordChars.char_class("@")
43
+ assert_equal "[:alnum:]_", result
44
+ end
45
+
46
+ def test_char_class_skips_out_of_range
47
+ result = RuVim::KeywordChars.char_class("256-300")
48
+ assert_equal "[:alnum:]_", result
49
+ end
50
+
51
+ def test_char_class_mixed_spec
52
+ result = RuVim::KeywordChars.char_class("-,65-90")
53
+ assert_includes result, "\\-"
54
+ assert_includes result, "A-Z"
55
+ end
56
+
57
+ def test_char_class_caches_result
58
+ result1 = RuVim::KeywordChars.char_class("-")
59
+ result2 = RuVim::KeywordChars.char_class("-")
60
+ assert_same result1, result2
61
+ end
62
+
63
+ # --- regex ---
64
+
65
+ def test_regex_default_for_empty
66
+ assert_equal RuVim::KeywordChars::DEFAULT_REGEX, RuVim::KeywordChars.regex("")
67
+ end
68
+
69
+ def test_regex_default_for_nil
70
+ assert_equal RuVim::KeywordChars::DEFAULT_REGEX, RuVim::KeywordChars.regex(nil)
71
+ end
72
+
73
+ def test_regex_matches_with_spec
74
+ re = RuVim::KeywordChars.regex("-")
75
+ assert_match re, "a"
76
+ assert_match re, "-"
77
+ refute_match re, " "
78
+ end
79
+
80
+ def test_regex_caches_result
81
+ result1 = RuVim::KeywordChars.regex("-")
82
+ result2 = RuVim::KeywordChars.regex("-")
83
+ assert_same result1, result2
84
+ end
85
+ end