ruvim 0.1.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 (66) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/test.yml +15 -0
  3. data/README.md +135 -0
  4. data/Rakefile +36 -0
  5. data/docs/binding.md +125 -0
  6. data/docs/command.md +306 -0
  7. data/docs/config.md +155 -0
  8. data/docs/done.md +112 -0
  9. data/docs/plugin.md +559 -0
  10. data/docs/spec.md +655 -0
  11. data/docs/todo.md +63 -0
  12. data/docs/tutorial.md +490 -0
  13. data/docs/vim_diff.md +179 -0
  14. data/exe/ruvim +6 -0
  15. data/lib/ruvim/app.rb +1600 -0
  16. data/lib/ruvim/buffer.rb +421 -0
  17. data/lib/ruvim/cli.rb +264 -0
  18. data/lib/ruvim/clipboard.rb +73 -0
  19. data/lib/ruvim/command_invocation.rb +14 -0
  20. data/lib/ruvim/command_line.rb +63 -0
  21. data/lib/ruvim/command_registry.rb +38 -0
  22. data/lib/ruvim/config_dsl.rb +134 -0
  23. data/lib/ruvim/config_loader.rb +68 -0
  24. data/lib/ruvim/context.rb +26 -0
  25. data/lib/ruvim/dispatcher.rb +120 -0
  26. data/lib/ruvim/display_width.rb +110 -0
  27. data/lib/ruvim/editor.rb +1025 -0
  28. data/lib/ruvim/ex_command_registry.rb +80 -0
  29. data/lib/ruvim/global_commands.rb +1889 -0
  30. data/lib/ruvim/highlighter.rb +52 -0
  31. data/lib/ruvim/input.rb +66 -0
  32. data/lib/ruvim/keymap_manager.rb +96 -0
  33. data/lib/ruvim/screen.rb +452 -0
  34. data/lib/ruvim/terminal.rb +30 -0
  35. data/lib/ruvim/text_metrics.rb +96 -0
  36. data/lib/ruvim/version.rb +5 -0
  37. data/lib/ruvim/window.rb +71 -0
  38. data/lib/ruvim.rb +30 -0
  39. data/sig/ruvim.rbs +4 -0
  40. data/test/app_completion_test.rb +39 -0
  41. data/test/app_dot_repeat_test.rb +54 -0
  42. data/test/app_motion_test.rb +73 -0
  43. data/test/app_register_test.rb +47 -0
  44. data/test/app_scenario_test.rb +77 -0
  45. data/test/app_startup_test.rb +199 -0
  46. data/test/app_text_object_test.rb +54 -0
  47. data/test/app_unicode_behavior_test.rb +66 -0
  48. data/test/buffer_test.rb +72 -0
  49. data/test/cli_test.rb +165 -0
  50. data/test/config_dsl_test.rb +78 -0
  51. data/test/dispatcher_test.rb +124 -0
  52. data/test/editor_mark_test.rb +69 -0
  53. data/test/editor_register_test.rb +64 -0
  54. data/test/fixtures/render_basic_snapshot.txt +8 -0
  55. data/test/fixtures/render_basic_snapshot_nonumber.txt +8 -0
  56. data/test/fixtures/render_unicode_scrolled_snapshot.txt +7 -0
  57. data/test/highlighter_test.rb +16 -0
  58. data/test/input_screen_integration_test.rb +69 -0
  59. data/test/keymap_manager_test.rb +48 -0
  60. data/test/render_snapshot_test.rb +70 -0
  61. data/test/screen_test.rb +123 -0
  62. data/test/search_option_test.rb +39 -0
  63. data/test/test_helper.rb +15 -0
  64. data/test/text_metrics_test.rb +42 -0
  65. data/test/window_test.rb +21 -0
  66. metadata +106 -0
@@ -0,0 +1,199 @@
1
+ require_relative "test_helper"
2
+ require "tempfile"
3
+ require "stringio"
4
+
5
+ class AppStartupTest < Minitest::Test
6
+ def test_startup_ex_command_runs_after_boot
7
+ app = RuVim::App.new(clean: true, startup_actions: [{ type: :ex, value: "set number" }])
8
+ editor = app.instance_variable_get(:@editor)
9
+
10
+ assert_equal true, editor.effective_option("number")
11
+ end
12
+
13
+ def test_startup_line_moves_cursor
14
+ Tempfile.create(["ruvim-startup", ".txt"]) do |f|
15
+ f.write("a\nb\nc\nd\n")
16
+ f.flush
17
+
18
+ app = RuVim::App.new(path: f.path, clean: true, startup_actions: [{ type: :line, value: 3 }])
19
+ editor = app.instance_variable_get(:@editor)
20
+
21
+ assert_equal 2, editor.current_window.cursor_y
22
+ assert_equal "c", editor.current_buffer.line_at(editor.current_window.cursor_y)
23
+ end
24
+ end
25
+
26
+ def test_startup_line_end_moves_to_last_line
27
+ Tempfile.create(["ruvim-startup", ".txt"]) do |f|
28
+ f.write("a\nb\nc\n")
29
+ f.flush
30
+
31
+ app = RuVim::App.new(path: f.path, clean: true, startup_actions: [{ type: :line_end }])
32
+ editor = app.instance_variable_get(:@editor)
33
+
34
+ assert_equal 2, editor.current_window.cursor_y
35
+ assert_equal "c", editor.current_buffer.line_at(editor.current_window.cursor_y)
36
+ end
37
+ end
38
+
39
+ def test_startup_readonly_marks_opened_buffer_readonly
40
+ Tempfile.create(["ruvim-startup", ".txt"]) do |f|
41
+ f.write("hello\n")
42
+ f.flush
43
+
44
+ app = RuVim::App.new(path: f.path, clean: true, readonly: true)
45
+ editor = app.instance_variable_get(:@editor)
46
+
47
+ assert_equal true, editor.current_buffer.readonly?
48
+ end
49
+ end
50
+
51
+ def test_startup_diff_mode_placeholder_emits_message
52
+ app = RuVim::App.new(clean: true, diff_mode: true)
53
+ editor = app.instance_variable_get(:@editor)
54
+
55
+ assert_match(/diff mode .*not implemented/i, editor.message)
56
+ end
57
+
58
+ def test_startup_quickfix_placeholder_emits_message
59
+ app = RuVim::App.new(clean: true, quickfix_errorfile: "errors.log")
60
+ editor = app.instance_variable_get(:@editor)
61
+
62
+ assert_match(/quickfix startup .*errors\.log.*not implemented/i, editor.message)
63
+ end
64
+
65
+ def test_startup_session_placeholder_emits_message
66
+ app = RuVim::App.new(clean: true, session_file: "Session.ruvim")
67
+ editor = app.instance_variable_get(:@editor)
68
+
69
+ assert_match(/session startup .*Session\.ruvim.*not implemented/i, editor.message)
70
+ end
71
+
72
+ def test_startup_nomodifiable_marks_opened_buffer_unmodifiable_and_readonly
73
+ Tempfile.create(["ruvim-startup", ".txt"]) do |f|
74
+ f.write("hello\n")
75
+ f.flush
76
+
77
+ app = RuVim::App.new(path: f.path, clean: true, nomodifiable: true)
78
+ editor = app.instance_variable_get(:@editor)
79
+
80
+ assert_equal false, editor.current_buffer.modifiable?
81
+ assert_equal true, editor.current_buffer.readonly?
82
+ end
83
+ end
84
+
85
+ def test_startup_restricted_mode_sets_editor_flag
86
+ app = RuVim::App.new(clean: true, restricted: true)
87
+ editor = app.instance_variable_get(:@editor)
88
+
89
+ assert_equal true, editor.restricted_mode?
90
+ end
91
+
92
+ def test_startup_horizontal_split_opens_multiple_files
93
+ Tempfile.create(["ruvim-a", ".txt"]) do |a|
94
+ Tempfile.create(["ruvim-b", ".txt"]) do |b|
95
+ a.write("a\n"); a.flush
96
+ b.write("b\n"); b.flush
97
+
98
+ app = RuVim::App.new(paths: [a.path, b.path], clean: true, startup_open_layout: :horizontal)
99
+ editor = app.instance_variable_get(:@editor)
100
+
101
+ assert_equal :horizontal, editor.window_layout
102
+ assert_equal 2, editor.window_order.length
103
+ names = editor.window_order.map { |id| editor.buffers[editor.windows[id].buffer_id].path }
104
+ assert_includes names, a.path
105
+ assert_includes names, b.path
106
+ end
107
+ end
108
+ end
109
+
110
+ def test_startup_tab_layout_opens_multiple_tabs
111
+ Tempfile.create(["ruvim-a", ".txt"]) do |a|
112
+ Tempfile.create(["ruvim-b", ".txt"]) do |b|
113
+ a.write("a\n"); a.flush
114
+ b.write("b\n"); b.flush
115
+
116
+ app = RuVim::App.new(paths: [a.path, b.path], clean: true, startup_open_layout: :tab)
117
+ editor = app.instance_variable_get(:@editor)
118
+
119
+ assert_equal 2, editor.tabpage_count
120
+ end
121
+ end
122
+ end
123
+
124
+ def test_restricted_mode_disables_ex_ruby
125
+ app = RuVim::App.new(clean: true, restricted: true)
126
+ editor = app.instance_variable_get(:@editor)
127
+ dispatcher = app.instance_variable_get(:@dispatcher)
128
+
129
+ dispatcher.dispatch_ex(editor, "ruby 1+1")
130
+
131
+ assert_match(/Restricted mode/, editor.message)
132
+ end
133
+
134
+ def test_verbose_logs_startup_and_startup_ex_actions
135
+ log = StringIO.new
136
+ app = RuVim::App.new(
137
+ clean: true,
138
+ verbose_level: 2,
139
+ verbose_io: log,
140
+ startup_actions: [{ type: :ex, value: "set number" }]
141
+ )
142
+ editor = app.instance_variable_get(:@editor)
143
+ assert_equal true, editor.effective_option("number")
144
+
145
+ text = log.string
146
+ assert_match(/startup: load_user_config/, text)
147
+ assert_match(/startup: run_startup_actions/, text)
148
+ assert_match(/startup ex: set number/, text)
149
+ end
150
+
151
+ def test_pre_config_actions_run_before_user_config_and_startup_actions
152
+ Tempfile.create(["ruvim-config", ".rb"]) do |cfg|
153
+ cfg.write("::ENV['RUVIM_PRECONFIG_ORDER_TEST'] = 'config_loaded'\n")
154
+ cfg.flush
155
+
156
+ begin
157
+ ENV.delete("RUVIM_PRECONFIG_ORDER_TEST")
158
+ log = StringIO.new
159
+ app = RuVim::App.new(
160
+ config_path: cfg.path,
161
+ pre_config_actions: [{ type: :ex, value: "set number" }],
162
+ startup_actions: [{ type: :ex, value: "set relativenumber" }],
163
+ verbose_level: 2,
164
+ verbose_io: log
165
+ )
166
+ editor = app.instance_variable_get(:@editor)
167
+
168
+ assert_equal true, editor.effective_option("number")
169
+ assert_equal true, editor.effective_option("relativenumber")
170
+ assert_equal "config_loaded", ENV["RUVIM_PRECONFIG_ORDER_TEST"]
171
+
172
+ text = log.string
173
+ assert_match(/startup: run_pre_config_actions count=1/, text)
174
+ assert_match(/pre-config ex: set number/, text)
175
+ assert_match(/startup: load_user_config/, text)
176
+ assert_match(/startup ex: set relativenumber/, text)
177
+ assert_operator text.index("startup: run_pre_config_actions"), :<, text.index("startup: load_user_config")
178
+ assert_operator text.index("startup: load_user_config"), :<, text.index("startup: run_startup_actions")
179
+ ensure
180
+ ENV.delete("RUVIM_PRECONFIG_ORDER_TEST")
181
+ end
182
+ end
183
+ end
184
+
185
+ def test_startuptime_writes_startup_phase_log
186
+ Tempfile.create(["ruvim-startuptime", ".log"]) do |f|
187
+ path = f.path
188
+ f.close
189
+
190
+ RuVim::App.new(clean: true, startup_time_path: path)
191
+
192
+ text = File.read(path)
193
+ assert_match(/init\.start/, text)
194
+ assert_match(/pre_config_actions\.done/, text)
195
+ assert_match(/buffers\.opened/, text)
196
+ assert_match(/startup_actions\.done/, text)
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,54 @@
1
+ require_relative "test_helper"
2
+
3
+ class AppTextObjectTest < Minitest::Test
4
+ def setup
5
+ @app = RuVim::App.new(clean: true)
6
+ @editor = @app.instance_variable_get(:@editor)
7
+ @editor.materialize_intro_buffer!
8
+ @buffer = @editor.current_buffer
9
+ @win = @editor.current_window
10
+ end
11
+
12
+ def press(*keys)
13
+ keys.each { |k| @app.send(:handle_normal_key, k) }
14
+ end
15
+
16
+ def test_delete_inside_square_brackets
17
+ @buffer.replace_all_lines!(["x[abc]y"])
18
+ @win.cursor_x = 2
19
+
20
+ press("d", "i", "]")
21
+
22
+ assert_equal "x[]y", @buffer.line_at(0)
23
+ end
24
+
25
+ def test_delete_inside_backticks
26
+ @buffer.replace_all_lines!(["a`bc`d"])
27
+ @win.cursor_x = 2
28
+
29
+ press("d", "i", "`")
30
+
31
+ assert_equal "a``d", @buffer.line_at(0)
32
+ end
33
+
34
+ def test_yank_inner_paragraph
35
+ @buffer.replace_all_lines!(["foo", "bar", "", "baz"])
36
+ @win.cursor_y = 1
37
+ @win.cursor_x = 1
38
+
39
+ press("y", "i", "p")
40
+
41
+ assert_equal({ text: "foo\nbar", type: :charwise }, @editor.get_register("\""))
42
+ assert_equal({ text: "foo\nbar", type: :charwise }, @editor.get_register("0"))
43
+ end
44
+
45
+ def test_delete_around_paragraph_includes_separator_blank_line
46
+ @buffer.replace_all_lines!(["foo", "bar", "", "baz"])
47
+ @win.cursor_y = 0
48
+ @win.cursor_x = 0
49
+
50
+ press("d", "a", "p")
51
+
52
+ assert_equal ["baz"], @buffer.lines
53
+ end
54
+ end
@@ -0,0 +1,66 @@
1
+ require_relative "test_helper"
2
+
3
+ class AppUnicodeBehaviorTest < Minitest::Test
4
+ def setup
5
+ @app = RuVim::App.new(clean: true)
6
+ @editor = @app.instance_variable_get(:@editor)
7
+ @editor.materialize_intro_buffer!
8
+ @buffer = @editor.current_buffer
9
+ @win = @editor.current_window
10
+ end
11
+
12
+ def press_normal(*keys)
13
+ keys.each { |k| @app.send(:handle_normal_key, k) }
14
+ end
15
+
16
+ def press(*keys)
17
+ keys.each { |k| @app.send(:handle_key, k) }
18
+ end
19
+
20
+ def test_word_motions_on_japanese_text_do_not_break_character_boundaries
21
+ @buffer.replace_all_lines!(["foo 日本語 編集"])
22
+ @win.cursor_y = 0
23
+ @win.cursor_x = 0
24
+
25
+ press_normal("w")
26
+ assert_equal "日", @buffer.line_at(0)[@win.cursor_x]
27
+
28
+ press_normal("e")
29
+ assert_equal "語", @buffer.line_at(0)[@win.cursor_x]
30
+
31
+ press_normal("w")
32
+ assert_equal "編", @buffer.line_at(0)[@win.cursor_x]
33
+
34
+ press_normal("b")
35
+ assert_equal "日", @buffer.line_at(0)[@win.cursor_x]
36
+ end
37
+
38
+ def test_paste_charwise_japanese_text_keeps_valid_cursor_position
39
+ @buffer.replace_all_lines!(["abc"])
40
+ @editor.set_register("\"", text: "日本", type: :charwise)
41
+ @win.cursor_y = 0
42
+ @win.cursor_x = 0
43
+
44
+ press_normal("p")
45
+ assert_equal "a日本bc", @buffer.line_at(0)
46
+ assert_equal true, @buffer.line_at(0).valid_encoding?
47
+ assert_equal "本", @buffer.line_at(0)[@win.cursor_x]
48
+
49
+ press_normal("P")
50
+ assert_equal "a日日本本bc", @buffer.line_at(0)
51
+ assert_equal true, @buffer.line_at(0).valid_encoding?
52
+ assert_equal "本", @buffer.line_at(0)[@win.cursor_x]
53
+ end
54
+
55
+ def test_visual_yank_on_japanese_text_is_inclusive_and_valid_utf8
56
+ @buffer.replace_all_lines!(["A日本語B"])
57
+ @win.cursor_x = 1 # 日
58
+
59
+ press("v", "l", "y")
60
+
61
+ reg = @editor.get_register("\"")
62
+ assert_equal({ text: "日本", type: :charwise }, reg)
63
+ assert_equal true, reg[:text].valid_encoding?
64
+ assert_equal :normal, @editor.mode
65
+ end
66
+ end
@@ -0,0 +1,72 @@
1
+ require_relative "test_helper"
2
+
3
+ class BufferTest < Minitest::Test
4
+ def test_insert_and_undo_redo_group
5
+ b = RuVim::Buffer.new(id: 1)
6
+ b.begin_change_group
7
+ b.insert_char(0, 0, "a")
8
+ b.insert_char(0, 1, "b")
9
+ b.end_change_group
10
+
11
+ assert_equal ["ab"], b.lines
12
+ assert b.undo!
13
+ assert_equal [""], b.lines
14
+ assert b.redo!
15
+ assert_equal ["ab"], b.lines
16
+ end
17
+
18
+ def test_delete_span_across_lines
19
+ b = RuVim::Buffer.new(id: 1, lines: ["abc", "def", "ghi"])
20
+ deleted = b.span_text(0, 1, 2, 1)
21
+ assert_equal "bc\ndef\ng", deleted
22
+
23
+ b.delete_span(0, 1, 2, 1)
24
+ assert_equal ["ahi"], b.lines
25
+ end
26
+
27
+ def test_reload_from_file_clears_modified_and_history
28
+ path = "/tmp/ruvim_buffer_reload_test.txt"
29
+ File.write(path, "one\n")
30
+ b = RuVim::Buffer.from_file(id: 1, path: path)
31
+ b.insert_char(0, 0, "X")
32
+ assert b.modified?
33
+ assert b.can_undo?
34
+
35
+ b.reload_from_file!
36
+ assert_equal ["one"], b.lines
37
+ refute b.modified?
38
+ refute b.can_undo?
39
+ end
40
+
41
+ def test_utf8_file_is_loaded_as_utf8_text_not_binary_bytes
42
+ path = "/tmp/ruvim_utf8_test.txt"
43
+ File.binwrite(path, "bar 日本語 編集\n".encode("UTF-8"))
44
+
45
+ b = RuVim::Buffer.from_file(id: 1, path: path)
46
+ assert_equal Encoding::UTF_8, b.line_at(0).encoding
47
+ assert_equal "bar 日本語 編集", b.line_at(0)
48
+ assert_equal 10, b.line_length(0)
49
+ end
50
+
51
+ def test_invalid_bytes_are_decoded_to_valid_utf8_with_replacement
52
+ path = "/tmp/ruvim_invalid_bytes_test.txt"
53
+ File.binwrite(path, "A\xFFB\n".b)
54
+
55
+ b = RuVim::Buffer.from_file(id: 1, path: path)
56
+ line = b.line_at(0)
57
+ assert_equal Encoding::UTF_8, line.encoding
58
+ assert_equal true, line.valid_encoding?
59
+ assert_match(/A.*B/, line)
60
+ end
61
+
62
+ def test_write_to_saves_utf8_text
63
+ path = "/tmp/ruvim_write_utf8_test.txt"
64
+ b = RuVim::Buffer.new(id: 1, lines: ["日本語", "abc"])
65
+ b.write_to(path)
66
+
67
+ data = File.binread(path)
68
+ text = data.force_encoding(Encoding::UTF_8)
69
+ assert_equal true, text.valid_encoding?
70
+ assert_equal "日本語\nabc", text
71
+ end
72
+ end
data/test/cli_test.rb ADDED
@@ -0,0 +1,165 @@
1
+ require_relative "test_helper"
2
+ require "stringio"
3
+ require "tempfile"
4
+
5
+ class CLITest < Minitest::Test
6
+ def test_parse_clean_and_u_none
7
+ opts = RuVim::CLI.parse(["--clean", "-u", "NONE"])
8
+
9
+ assert_equal true, opts.clean
10
+ assert_equal true, opts.skip_user_config
11
+ assert_nil opts.config_path
12
+ end
13
+
14
+ def test_parse_startup_actions_preserves_order
15
+ opts = RuVim::CLI.parse(["+10", "-c", "set number", "+", "file.txt"])
16
+
17
+ assert_equal ["file.txt"], opts.files
18
+ assert_equal [
19
+ { type: :line, value: 10 },
20
+ { type: :ex, value: "set number" },
21
+ { type: :line_end }
22
+ ], opts.startup_actions
23
+ end
24
+
25
+ def test_parse_pre_config_cmd_option
26
+ opts = RuVim::CLI.parse(["--cmd", "set number", "--cmd=set relativenumber", "file.txt"])
27
+
28
+ assert_equal ["file.txt"], opts.files
29
+ assert_equal [
30
+ { type: :ex, value: "set number" },
31
+ { type: :ex, value: "set relativenumber" }
32
+ ], opts.pre_config_actions
33
+ assert_equal [], opts.startup_actions
34
+ end
35
+
36
+ def test_parse_custom_config_path
37
+ opts = RuVim::CLI.parse(["-u", "/tmp/ruvimrc.rb"])
38
+
39
+ assert_equal false, opts.skip_user_config
40
+ assert_equal "/tmp/ruvimrc.rb", opts.config_path
41
+ end
42
+
43
+ def test_parse_readonly_option
44
+ opts = RuVim::CLI.parse(["-R", "file.txt"])
45
+
46
+ assert_equal true, opts.readonly
47
+ assert_equal ["file.txt"], opts.files
48
+ end
49
+
50
+ def test_parse_diff_mode_option
51
+ opts = RuVim::CLI.parse(["-d", "file.txt"])
52
+
53
+ assert_equal true, opts.diff_mode
54
+ assert_equal ["file.txt"], opts.files
55
+ end
56
+
57
+ def test_parse_quickfix_option
58
+ opts = RuVim::CLI.parse(["-q", "errors.log", "file.txt"])
59
+
60
+ assert_equal "errors.log", opts.quickfix_errorfile
61
+ assert_equal ["file.txt"], opts.files
62
+ end
63
+
64
+ def test_parse_session_option_with_and_without_argument
65
+ opts1 = RuVim::CLI.parse(["-S", "Session.ruvim", "file.txt"])
66
+ assert_equal "Session.ruvim", opts1.session_file
67
+ assert_equal ["file.txt"], opts1.files
68
+
69
+ opts2 = RuVim::CLI.parse(["-S"])
70
+ assert_equal "Session.vim", opts2.session_file
71
+ end
72
+
73
+ def test_parse_nomodifiable_option
74
+ opts = RuVim::CLI.parse(["-M", "file.txt"])
75
+
76
+ assert_equal true, opts.nomodifiable
77
+ assert_equal ["file.txt"], opts.files
78
+ end
79
+
80
+ def test_parse_restricted_option
81
+ opts = RuVim::CLI.parse(["-Z", "file.txt"])
82
+
83
+ assert_equal true, opts.restricted_mode
84
+ assert_equal ["file.txt"], opts.files
85
+ end
86
+
87
+ def test_parse_verbose_options
88
+ v1 = RuVim::CLI.parse(["-V", "file.txt"])
89
+ assert_equal 1, v1.verbose_level
90
+
91
+ v2 = RuVim::CLI.parse(["-V2", "file.txt"])
92
+ assert_equal 2, v2.verbose_level
93
+
94
+ v3 = RuVim::CLI.parse(["--verbose=3", "file.txt"])
95
+ assert_equal 3, v3.verbose_level
96
+ end
97
+
98
+ def test_parse_startuptime_option
99
+ opts = RuVim::CLI.parse(["--startuptime", "/tmp/ruvim-startup.log", "file.txt"])
100
+
101
+ assert_equal "/tmp/ruvim-startup.log", opts.startup_time_path
102
+ assert_equal ["file.txt"], opts.files
103
+ end
104
+
105
+ def test_parse_n_option_as_accepted_no_op
106
+ opts = RuVim::CLI.parse(["-n", "file.txt"])
107
+
108
+ assert_equal true, opts.no_swap
109
+ assert_equal ["file.txt"], opts.files
110
+ end
111
+
112
+ def test_parse_split_and_tab_layout_options
113
+ o = RuVim::CLI.parse(["-o", "a", "b"])
114
+ assert_equal :horizontal, o.startup_open_layout
115
+ assert_nil o.startup_open_count
116
+
117
+ ov = RuVim::CLI.parse(["-O3", "a", "b"])
118
+ assert_equal :vertical, ov.startup_open_layout
119
+ assert_equal 3, ov.startup_open_count
120
+
121
+ p = RuVim::CLI.parse(["-p2", "a", "b"])
122
+ assert_equal :tab, p.startup_open_layout
123
+ assert_equal 2, p.startup_open_count
124
+ end
125
+
126
+ def test_help_and_version_return_without_starting_ui
127
+ out = StringIO.new
128
+ err = StringIO.new
129
+
130
+ code = RuVim::CLI.run(["--version"], stdout: out, stderr: err, stdin: StringIO.new)
131
+ assert_equal 0, code
132
+ assert_match(/RuVim /, out.string)
133
+ assert_equal "", err.string
134
+
135
+ out = StringIO.new
136
+ err = StringIO.new
137
+ code = RuVim::CLI.run(["--help"], stdout: out, stderr: err, stdin: StringIO.new)
138
+ assert_equal 0, code
139
+ assert_match(/Usage: ruvim/, out.string)
140
+ assert_match(/-R\s+Open file readonly/, out.string)
141
+ assert_match(/-d\s+Diff mode requested/, out.string)
142
+ assert_match(/-q \{errorfile\}\s+Quickfix startup placeholder/, out.string)
143
+ assert_match(/-S \[session\]\s+Session startup placeholder/, out.string)
144
+ assert_match(/-M\s+Open file unmodifiable/, out.string)
145
+ assert_match(/-Z\s+Restricted mode/, out.string)
146
+ assert_match(/-V\[N\], --verbose/, out.string)
147
+ assert_match(/--startuptime FILE/, out.string)
148
+ assert_match(/--cmd \{cmd\}/, out.string)
149
+ assert_match(/-n\s+No-op/, out.string)
150
+ assert_match(/-o\[N\]/, out.string)
151
+ assert_match(/-O\[N\]/, out.string)
152
+ assert_match(/-p\[N\]/, out.string)
153
+ assert_equal "", err.string
154
+ end
155
+
156
+ def test_run_returns_error_for_missing_config_file
157
+ out = StringIO.new
158
+ err = StringIO.new
159
+
160
+ code = RuVim::CLI.run(["-u", "/tmp/ruvim/no-such-config.rb"], stdout: out, stderr: err, stdin: StringIO.new)
161
+
162
+ assert_equal 2, code
163
+ assert_match(/config file not found/, err.string)
164
+ end
165
+ end
@@ -0,0 +1,78 @@
1
+ require_relative "test_helper"
2
+
3
+ class ConfigDSLTest < Minitest::Test
4
+ FakeCommandSpec = Struct.new(:id, :call, :desc, :source, keyword_init: true)
5
+
6
+ class FakeCommandRegistry
7
+ attr_reader :specs
8
+
9
+ def initialize
10
+ @specs = {}
11
+ end
12
+
13
+ def register(id, call:, desc:, source:)
14
+ @specs[id.to_s] = FakeCommandSpec.new(id: id.to_s, call:, desc:, source:)
15
+ end
16
+
17
+ def fetch(id)
18
+ @specs.fetch(id.to_s)
19
+ end
20
+ end
21
+
22
+ class FakeExRegistry
23
+ def registered?(_name)
24
+ false
25
+ end
26
+
27
+ def unregister(_name)
28
+ nil
29
+ end
30
+
31
+ def register(*)
32
+ nil
33
+ end
34
+ end
35
+
36
+ def setup
37
+ @command_registry = FakeCommandRegistry.new
38
+ @ex_registry = FakeExRegistry.new
39
+ @keymaps = RuVim::KeymapManager.new
40
+ @dsl = RuVim::ConfigDSL.new(
41
+ command_registry: @command_registry,
42
+ ex_registry: @ex_registry,
43
+ keymaps: @keymaps,
44
+ command_host: RuVim::GlobalCommands.instance
45
+ )
46
+ end
47
+
48
+ def test_nmap_block_registers_inline_command_and_binds_key
49
+ @dsl.nmap("K", desc: "Show name") { |ctx, **| ctx.editor.echo(ctx.buffer.display_name) }
50
+
51
+ match = @keymaps.resolve(:normal, ["K"])
52
+ assert_equal :match, match.status
53
+ refute_nil match.invocation
54
+ assert match.invocation.id.start_with?("user.keymap.normal.")
55
+
56
+ spec = @command_registry.fetch(match.invocation.id)
57
+ assert_equal :user, spec.source
58
+ assert_equal "Show name", spec.desc
59
+ assert_respond_to spec.call, :call
60
+ end
61
+
62
+ def test_imap_block_registers_insert_mode_binding
63
+ @dsl.imap("Q", desc: "Insert helper") { |_ctx, **| }
64
+
65
+ match = @keymaps.resolve(:insert, ["Q"])
66
+ assert_equal :match, match.status
67
+ assert match.invocation.id.start_with?("user.keymap.insert.")
68
+ end
69
+
70
+ def test_map_global_block_binds_fallback_layer_when_mode_nil
71
+ @dsl.map_global("Z", mode: nil, desc: "Global fallback") { |_ctx, **| }
72
+
73
+ editor = fresh_editor
74
+ match = @keymaps.resolve_with_context(:normal, ["Z"], editor: editor)
75
+ assert_equal :match, match.status
76
+ assert match.invocation.id.start_with?("user.keymap.global.")
77
+ end
78
+ end