ruvim 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +4 -0
- data/AGENTS.md +84 -0
- data/CLAUDE.md +1 -0
- data/docs/binding.md +29 -0
- data/docs/command.md +101 -0
- data/docs/config.md +203 -84
- data/docs/done.md +21 -0
- data/docs/lib_cleanup_report.md +79 -0
- data/docs/plugin.md +13 -15
- data/docs/spec.md +195 -33
- data/docs/todo.md +183 -10
- data/docs/tutorial.md +1 -1
- data/docs/vim_diff.md +94 -171
- data/lib/ruvim/app.rb +1543 -172
- data/lib/ruvim/buffer.rb +35 -1
- data/lib/ruvim/cli.rb +12 -3
- data/lib/ruvim/clipboard.rb +2 -0
- data/lib/ruvim/command_invocation.rb +3 -1
- data/lib/ruvim/command_line.rb +2 -0
- data/lib/ruvim/command_registry.rb +2 -0
- data/lib/ruvim/config_dsl.rb +2 -0
- data/lib/ruvim/config_loader.rb +21 -5
- data/lib/ruvim/context.rb +2 -7
- data/lib/ruvim/dispatcher.rb +153 -13
- data/lib/ruvim/display_width.rb +28 -2
- data/lib/ruvim/editor.rb +622 -69
- data/lib/ruvim/ex_command_registry.rb +2 -0
- data/lib/ruvim/global_commands.rb +1386 -114
- data/lib/ruvim/highlighter.rb +16 -21
- data/lib/ruvim/input.rb +52 -29
- data/lib/ruvim/keymap_manager.rb +83 -0
- data/lib/ruvim/keyword_chars.rb +48 -0
- data/lib/ruvim/lang/base.rb +25 -0
- data/lib/ruvim/lang/csv.rb +18 -0
- data/lib/ruvim/lang/json.rb +18 -0
- data/lib/ruvim/lang/markdown.rb +170 -0
- data/lib/ruvim/lang/ruby.rb +236 -0
- data/lib/ruvim/lang/scheme.rb +44 -0
- data/lib/ruvim/lang/tsv.rb +19 -0
- data/lib/ruvim/rich_view/markdown_renderer.rb +248 -0
- data/lib/ruvim/rich_view/table_renderer.rb +176 -0
- data/lib/ruvim/rich_view.rb +93 -0
- data/lib/ruvim/screen.rb +851 -119
- data/lib/ruvim/terminal.rb +18 -1
- data/lib/ruvim/text_metrics.rb +28 -0
- data/lib/ruvim/version.rb +2 -2
- data/lib/ruvim/window.rb +37 -10
- data/lib/ruvim.rb +15 -0
- data/test/app_completion_test.rb +174 -0
- data/test/app_dot_repeat_test.rb +13 -0
- data/test/app_motion_test.rb +110 -2
- data/test/app_scenario_test.rb +998 -0
- data/test/app_startup_test.rb +197 -0
- data/test/arglist_test.rb +113 -0
- data/test/buffer_test.rb +49 -30
- data/test/config_loader_test.rb +37 -0
- data/test/dispatcher_test.rb +438 -0
- data/test/display_width_test.rb +18 -0
- data/test/editor_register_test.rb +23 -0
- data/test/fixtures/render_basic_snapshot.txt +7 -8
- data/test/fixtures/render_basic_snapshot_nonumber.txt +1 -2
- data/test/fixtures/render_unicode_scrolled_snapshot.txt +6 -7
- data/test/highlighter_test.rb +121 -0
- data/test/indent_test.rb +201 -0
- data/test/input_screen_integration_test.rb +65 -14
- data/test/markdown_renderer_test.rb +279 -0
- data/test/on_save_hook_test.rb +150 -0
- data/test/rich_view_test.rb +478 -0
- data/test/screen_test.rb +470 -0
- data/test/window_test.rb +26 -0
- metadata +37 -2
data/test/app_scenario_test.rb
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
require_relative "test_helper"
|
|
2
|
+
require "fileutils"
|
|
3
|
+
require "tmpdir"
|
|
4
|
+
require "stringio"
|
|
2
5
|
|
|
3
6
|
class AppScenarioTest < Minitest::Test
|
|
4
7
|
def setup
|
|
5
8
|
@app = RuVim::App.new(clean: true)
|
|
6
9
|
@editor = @app.instance_variable_get(:@editor)
|
|
10
|
+
@dispatcher = @app.instance_variable_get(:@dispatcher)
|
|
7
11
|
@editor.materialize_intro_buffer!
|
|
8
12
|
end
|
|
9
13
|
|
|
@@ -74,4 +78,998 @@ class AppScenarioTest < Minitest::Test
|
|
|
74
78
|
|
|
75
79
|
assert_equal ["bc", "bc", "bc"], @editor.current_buffer.lines
|
|
76
80
|
end
|
|
81
|
+
|
|
82
|
+
def test_s_substitutes_char_and_enters_insert_mode
|
|
83
|
+
@editor.current_buffer.replace_all_lines!(["abcd"])
|
|
84
|
+
@editor.current_window.cursor_x = 1
|
|
85
|
+
|
|
86
|
+
feed("s", "X", :escape)
|
|
87
|
+
|
|
88
|
+
assert_equal ["aXcd"], @editor.current_buffer.lines
|
|
89
|
+
assert_equal :normal, @editor.mode
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def test_z_commands_reposition_current_line_in_window
|
|
93
|
+
@editor.current_buffer.replace_all_lines!((1..20).map { |i| "line#{i}" })
|
|
94
|
+
@editor.current_window_view_height_hint = 5
|
|
95
|
+
@editor.current_window.cursor_y = 10
|
|
96
|
+
|
|
97
|
+
feed("z", "t")
|
|
98
|
+
assert_equal 10, @editor.current_window.row_offset
|
|
99
|
+
|
|
100
|
+
feed("z", "z")
|
|
101
|
+
assert_equal 8, @editor.current_window.row_offset
|
|
102
|
+
|
|
103
|
+
feed("z", "b")
|
|
104
|
+
assert_equal 6, @editor.current_window.row_offset
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def test_j_joins_next_line_trimming_indent
|
|
108
|
+
@editor.current_buffer.replace_all_lines!(["foo", " bar", "baz"])
|
|
109
|
+
@editor.current_window.cursor_y = 0
|
|
110
|
+
@editor.current_window.cursor_x = 0
|
|
111
|
+
|
|
112
|
+
feed("J")
|
|
113
|
+
|
|
114
|
+
assert_equal ["foo bar", "baz"], @editor.current_buffer.lines
|
|
115
|
+
assert_equal 3, @editor.current_window.cursor_x
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def test_uppercase_aliases_d_c_s_x_y_and_tilde
|
|
119
|
+
@editor.current_buffer.replace_all_lines!(["abcd"])
|
|
120
|
+
@editor.current_window.cursor_x = 2
|
|
121
|
+
feed("X")
|
|
122
|
+
assert_equal ["acd"], @editor.current_buffer.lines
|
|
123
|
+
|
|
124
|
+
@editor.current_buffer.replace_all_lines!(["abcd"])
|
|
125
|
+
@editor.current_window.cursor_x = 1
|
|
126
|
+
feed("D")
|
|
127
|
+
assert_equal ["a"], @editor.current_buffer.lines
|
|
128
|
+
|
|
129
|
+
@editor.current_buffer.replace_all_lines!(["abcd"])
|
|
130
|
+
@editor.current_window.cursor_x = 1
|
|
131
|
+
feed("C", "X", :escape)
|
|
132
|
+
assert_equal ["aX"], @editor.current_buffer.lines
|
|
133
|
+
assert_equal :normal, @editor.mode
|
|
134
|
+
|
|
135
|
+
@editor.current_buffer.replace_all_lines!(["Abcd"])
|
|
136
|
+
@editor.current_window.cursor_x = 0
|
|
137
|
+
feed("~")
|
|
138
|
+
assert_equal ["abcd"], @editor.current_buffer.lines
|
|
139
|
+
assert_equal 1, @editor.current_window.cursor_x
|
|
140
|
+
|
|
141
|
+
@editor.current_buffer.replace_all_lines!(["hello"])
|
|
142
|
+
@editor.current_window.cursor_x = 0
|
|
143
|
+
feed("Y")
|
|
144
|
+
reg = @editor.get_register("\"")
|
|
145
|
+
assert_equal :linewise, reg[:type]
|
|
146
|
+
assert_equal "hello\n", reg[:text]
|
|
147
|
+
|
|
148
|
+
@editor.current_buffer.replace_all_lines!(["hello"])
|
|
149
|
+
@editor.current_window.cursor_x = 0
|
|
150
|
+
feed("S", "x", :escape)
|
|
151
|
+
assert_equal ["x"], @editor.current_buffer.lines
|
|
152
|
+
assert_equal :normal, @editor.mode
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def test_expandtab_and_autoindent_in_insert_mode
|
|
156
|
+
@editor.set_option("expandtab", true, scope: :buffer)
|
|
157
|
+
@editor.set_option("tabstop", 4, scope: :buffer)
|
|
158
|
+
@editor.set_option("softtabstop", 4, scope: :buffer)
|
|
159
|
+
@editor.set_option("autoindent", true, scope: :buffer)
|
|
160
|
+
@editor.current_buffer.replace_all_lines!([" foo"])
|
|
161
|
+
|
|
162
|
+
feed("A", :ctrl_i, :enter, :escape)
|
|
163
|
+
|
|
164
|
+
assert_equal [" foo ", " "], @editor.current_buffer.lines
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def test_smartindent_adds_shiftwidth_after_open_brace
|
|
168
|
+
@editor.set_option("autoindent", true, scope: :buffer)
|
|
169
|
+
@editor.set_option("smartindent", true, scope: :buffer)
|
|
170
|
+
@editor.set_option("shiftwidth", 2, scope: :buffer)
|
|
171
|
+
@editor.current_buffer.replace_all_lines!(["if x {"])
|
|
172
|
+
@editor.current_window.cursor_y = 0
|
|
173
|
+
@editor.current_window.cursor_x = @editor.current_buffer.line_length(0)
|
|
174
|
+
|
|
175
|
+
feed("A", :enter, :escape)
|
|
176
|
+
|
|
177
|
+
assert_equal ["if x {", " "], @editor.current_buffer.lines
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def test_smartindent_adds_shiftwidth_after_ruby_def
|
|
181
|
+
@editor.set_option("autoindent", true, scope: :buffer)
|
|
182
|
+
@editor.set_option("smartindent", true, scope: :buffer)
|
|
183
|
+
@editor.set_option("shiftwidth", 2, scope: :buffer)
|
|
184
|
+
@editor.assign_filetype(@editor.current_buffer, "ruby")
|
|
185
|
+
@editor.current_buffer.replace_all_lines!(["def foo"])
|
|
186
|
+
@editor.current_window.cursor_y = 0
|
|
187
|
+
@editor.current_window.cursor_x = @editor.current_buffer.line_length(0)
|
|
188
|
+
|
|
189
|
+
feed("A", :enter, :escape)
|
|
190
|
+
|
|
191
|
+
assert_equal ["def foo", " "], @editor.current_buffer.lines
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def test_smartindent_adds_shiftwidth_after_ruby_do_block
|
|
195
|
+
@editor.set_option("autoindent", true, scope: :buffer)
|
|
196
|
+
@editor.set_option("smartindent", true, scope: :buffer)
|
|
197
|
+
@editor.set_option("shiftwidth", 2, scope: :buffer)
|
|
198
|
+
@editor.assign_filetype(@editor.current_buffer, "ruby")
|
|
199
|
+
@editor.current_buffer.replace_all_lines!([" items.each do |x|"])
|
|
200
|
+
@editor.current_window.cursor_y = 0
|
|
201
|
+
@editor.current_window.cursor_x = @editor.current_buffer.line_length(0)
|
|
202
|
+
|
|
203
|
+
feed("A", :enter, :escape)
|
|
204
|
+
|
|
205
|
+
assert_equal [" items.each do |x|", " "], @editor.current_buffer.lines
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def test_smartindent_dedents_end_in_insert_mode
|
|
209
|
+
@editor.set_option("autoindent", true, scope: :buffer)
|
|
210
|
+
@editor.set_option("smartindent", true, scope: :buffer)
|
|
211
|
+
@editor.set_option("shiftwidth", 2, scope: :buffer)
|
|
212
|
+
@editor.assign_filetype(@editor.current_buffer, "ruby")
|
|
213
|
+
@editor.current_buffer.replace_all_lines!(["def foo"])
|
|
214
|
+
@editor.current_window.cursor_y = 0
|
|
215
|
+
@editor.current_window.cursor_x = @editor.current_buffer.line_length(0)
|
|
216
|
+
|
|
217
|
+
feed("A", :enter, "b", "a", "r", :enter, "e", "n", "d", :escape)
|
|
218
|
+
|
|
219
|
+
assert_equal ["def foo", " bar", "end"], @editor.current_buffer.lines
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def test_smartindent_dedents_else_in_insert_mode
|
|
223
|
+
@editor.set_option("autoindent", true, scope: :buffer)
|
|
224
|
+
@editor.set_option("smartindent", true, scope: :buffer)
|
|
225
|
+
@editor.set_option("shiftwidth", 2, scope: :buffer)
|
|
226
|
+
@editor.assign_filetype(@editor.current_buffer, "ruby")
|
|
227
|
+
@editor.current_buffer.replace_all_lines!(["if cond"])
|
|
228
|
+
@editor.current_window.cursor_y = 0
|
|
229
|
+
@editor.current_window.cursor_x = @editor.current_buffer.line_length(0)
|
|
230
|
+
|
|
231
|
+
feed("A", :enter, "a", :enter, "e", "l", "s", "e", :escape)
|
|
232
|
+
|
|
233
|
+
assert_equal ["if cond", " a", "else"], @editor.current_buffer.lines
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def test_incsearch_moves_cursor_while_typing_and_escape_restores
|
|
237
|
+
@editor.set_option("incsearch", true, scope: :global)
|
|
238
|
+
@editor.current_buffer.replace_all_lines!(["alpha", "beta", "gamma"])
|
|
239
|
+
@editor.current_window.cursor_y = 0
|
|
240
|
+
@editor.current_window.cursor_x = 0
|
|
241
|
+
|
|
242
|
+
feed("/", "b")
|
|
243
|
+
assert_equal :command_line, @editor.mode
|
|
244
|
+
assert_equal 1, @editor.current_window.cursor_y
|
|
245
|
+
assert_equal 0, @editor.current_window.cursor_x
|
|
246
|
+
|
|
247
|
+
feed(:escape)
|
|
248
|
+
assert_equal :normal, @editor.mode
|
|
249
|
+
assert_equal 0, @editor.current_window.cursor_y
|
|
250
|
+
assert_equal 0, @editor.current_window.cursor_x
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def test_search_command_line_backspace_on_empty_cancels
|
|
254
|
+
@editor.current_buffer.replace_all_lines!(["alpha", "beta"])
|
|
255
|
+
@editor.current_window.cursor_y = 0
|
|
256
|
+
@editor.current_window.cursor_x = 0
|
|
257
|
+
|
|
258
|
+
feed("/")
|
|
259
|
+
assert_equal :command_line, @editor.mode
|
|
260
|
+
assert_equal "/", @editor.command_line.prefix
|
|
261
|
+
assert_equal "", @editor.command_line.text
|
|
262
|
+
|
|
263
|
+
feed(:backspace)
|
|
264
|
+
|
|
265
|
+
assert_equal :normal, @editor.mode
|
|
266
|
+
assert_equal 0, @editor.current_window.cursor_y
|
|
267
|
+
assert_equal 0, @editor.current_window.cursor_x
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def test_lopen_enter_jumps_to_selected_location_and_returns_to_source_window
|
|
271
|
+
@editor.current_buffer.replace_all_lines!(["aa", "bb aa", "cc aa"])
|
|
272
|
+
source_window_id = @editor.current_window_id
|
|
273
|
+
|
|
274
|
+
@dispatcher.dispatch_ex(@editor, "lvimgrep /aa/")
|
|
275
|
+
@dispatcher.dispatch_ex(@editor, "lopen")
|
|
276
|
+
|
|
277
|
+
assert_equal :location_list, @editor.current_buffer.kind
|
|
278
|
+
assert_equal 2, @editor.window_count
|
|
279
|
+
|
|
280
|
+
# Header lines are: title, blank, then items...
|
|
281
|
+
@editor.current_window.cursor_y = 4 # 3rd item
|
|
282
|
+
feed(:enter)
|
|
283
|
+
|
|
284
|
+
refute_equal :location_list, @editor.current_buffer.kind
|
|
285
|
+
assert_equal source_window_id, @editor.current_window_id
|
|
286
|
+
assert_equal 2, @editor.current_window.cursor_y
|
|
287
|
+
assert_equal 3, @editor.current_window.cursor_x
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def test_incsearch_submit_stays_on_previewed_match
|
|
291
|
+
@editor.set_option("incsearch", true, scope: :global)
|
|
292
|
+
@editor.current_buffer.replace_all_lines!(["foo", "bar", "baz", "bar"])
|
|
293
|
+
@editor.current_window.cursor_y = 0
|
|
294
|
+
@editor.current_window.cursor_x = 0
|
|
295
|
+
|
|
296
|
+
feed("/", "b", "a", "r", :enter)
|
|
297
|
+
|
|
298
|
+
assert_equal :normal, @editor.mode
|
|
299
|
+
assert_equal 1, @editor.current_window.cursor_y
|
|
300
|
+
assert_equal 0, @editor.current_window.cursor_x
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def test_whichwrap_allows_h_and_l_to_cross_lines
|
|
304
|
+
@editor.set_option("whichwrap", "h,l", scope: :global)
|
|
305
|
+
@editor.current_buffer.replace_all_lines!(["ab", "cd"])
|
|
306
|
+
@editor.current_window.cursor_y = 1
|
|
307
|
+
@editor.current_window.cursor_x = 0
|
|
308
|
+
|
|
309
|
+
feed("h")
|
|
310
|
+
assert_equal [0, 2], [@editor.current_window.cursor_y, @editor.current_window.cursor_x]
|
|
311
|
+
|
|
312
|
+
feed("l")
|
|
313
|
+
assert_equal [1, 0], [@editor.current_window.cursor_y, @editor.current_window.cursor_x]
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def test_iskeyword_affects_word_motion
|
|
317
|
+
@editor.set_option("iskeyword", "@,-", scope: :buffer)
|
|
318
|
+
@editor.current_buffer.replace_all_lines!(["foo-bar baz"])
|
|
319
|
+
@editor.current_window.cursor_y = 0
|
|
320
|
+
@editor.current_window.cursor_x = 0
|
|
321
|
+
|
|
322
|
+
feed("w")
|
|
323
|
+
|
|
324
|
+
assert_equal 8, @editor.current_window.cursor_x
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def test_backspace_start_option_blocks_deleting_before_insert_start
|
|
328
|
+
@editor.set_option("backspace", "indent,eol", scope: :global)
|
|
329
|
+
@editor.current_buffer.replace_all_lines!(["ab"])
|
|
330
|
+
@editor.current_window.cursor_x = 1
|
|
331
|
+
|
|
332
|
+
feed("i", :backspace)
|
|
333
|
+
|
|
334
|
+
assert_equal ["ab"], @editor.current_buffer.lines
|
|
335
|
+
assert_equal 1, @editor.current_window.cursor_x
|
|
336
|
+
assert_equal :insert, @editor.mode
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def test_backspace_eol_option_blocks_joining_previous_line
|
|
340
|
+
@editor.set_option("backspace", "start", scope: :global)
|
|
341
|
+
@editor.current_buffer.replace_all_lines!(["a", "b"])
|
|
342
|
+
@editor.current_window.cursor_y = 1
|
|
343
|
+
@editor.current_window.cursor_x = 0
|
|
344
|
+
|
|
345
|
+
feed("i", :backspace)
|
|
346
|
+
|
|
347
|
+
assert_equal ["a", "b"], @editor.current_buffer.lines
|
|
348
|
+
assert_equal [1, 0], [@editor.current_window.cursor_y, @editor.current_window.cursor_x]
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def test_nomodifiable_buffer_edit_key_does_not_crash
|
|
352
|
+
@editor.current_buffer.replace_all_lines!(["hello"])
|
|
353
|
+
@editor.current_buffer.modifiable = false
|
|
354
|
+
@editor.current_buffer.readonly = true
|
|
355
|
+
|
|
356
|
+
@app.send(:handle_key, "x")
|
|
357
|
+
|
|
358
|
+
assert_equal ["hello"], @editor.current_buffer.lines
|
|
359
|
+
assert_match(/not modifiable/i, @editor.message)
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def test_nomodifiable_buffer_insert_mode_is_rejected
|
|
363
|
+
@editor.current_buffer.replace_all_lines!(["hello"])
|
|
364
|
+
@editor.current_buffer.modifiable = false
|
|
365
|
+
@editor.current_buffer.readonly = true
|
|
366
|
+
|
|
367
|
+
@app.send(:handle_key, "i")
|
|
368
|
+
|
|
369
|
+
assert_equal :normal, @editor.mode
|
|
370
|
+
assert_equal ["hello"], @editor.current_buffer.lines
|
|
371
|
+
assert_match(/not modifiable/i, @editor.message)
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def test_normal_ctrl_c_stops_stdin_stream_via_default_binding
|
|
375
|
+
stream = StringIO.new("hello\n")
|
|
376
|
+
@app.instance_variable_set(:@stdin_stream_source, stream)
|
|
377
|
+
@app.send(:prepare_stdin_stream_buffer!)
|
|
378
|
+
|
|
379
|
+
@app.send(:handle_key, :ctrl_c)
|
|
380
|
+
|
|
381
|
+
assert_equal :closed, @editor.current_buffer.stream_state
|
|
382
|
+
assert_equal :normal, @editor.mode
|
|
383
|
+
assert_equal true, stream.closed?
|
|
384
|
+
assert_match(/\[stdin\] closed/, @editor.message)
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def test_ctrl_z_calls_terminal_suspend
|
|
388
|
+
terminal_stub = Object.new
|
|
389
|
+
terminal_stub.instance_variable_set(:@suspend_calls, 0)
|
|
390
|
+
terminal_stub.define_singleton_method(:suspend_for_tstp) do
|
|
391
|
+
@suspend_calls += 1
|
|
392
|
+
end
|
|
393
|
+
terminal_stub.define_singleton_method(:suspend_calls) { @suspend_calls }
|
|
394
|
+
@app.instance_variable_set(:@terminal, terminal_stub)
|
|
395
|
+
|
|
396
|
+
feed("i", "a", :ctrl_z)
|
|
397
|
+
|
|
398
|
+
assert_equal 1, terminal_stub.suspend_calls
|
|
399
|
+
assert_equal :insert, @editor.mode
|
|
400
|
+
assert_equal ["a"], @editor.current_buffer.lines
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
def test_ctrl_z_invalidates_screen_cache_for_full_redraw_after_fg
|
|
404
|
+
terminal_stub = Object.new
|
|
405
|
+
terminal_stub.define_singleton_method(:suspend_for_tstp) {}
|
|
406
|
+
@app.instance_variable_set(:@terminal, terminal_stub)
|
|
407
|
+
|
|
408
|
+
screen_stub = Object.new
|
|
409
|
+
screen_stub.instance_variable_set(:@invalidated, false)
|
|
410
|
+
screen_stub.define_singleton_method(:invalidate_cache!) do
|
|
411
|
+
@invalidated = true
|
|
412
|
+
end
|
|
413
|
+
screen_stub.define_singleton_method(:invalidated?) { @invalidated }
|
|
414
|
+
@app.instance_variable_set(:@screen, screen_stub)
|
|
415
|
+
|
|
416
|
+
feed(:ctrl_z)
|
|
417
|
+
|
|
418
|
+
assert_equal true, screen_stub.invalidated?
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def test_g_and_1g_distinguish_implicit_and_explicit_count
|
|
422
|
+
@editor.current_buffer.replace_all_lines!(%w[a b c d])
|
|
423
|
+
@editor.current_window.cursor_y = 1
|
|
424
|
+
|
|
425
|
+
feed("G")
|
|
426
|
+
assert_equal 3, @editor.current_window.cursor_y
|
|
427
|
+
|
|
428
|
+
feed("1", "G")
|
|
429
|
+
assert_equal 0, @editor.current_window.cursor_y
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
def test_backspace_indent_allows_deleting_autoindent_before_insert_start
|
|
433
|
+
@editor.set_option("backspace", "indent,eol", scope: :global)
|
|
434
|
+
@editor.current_buffer.replace_all_lines!([" abc"])
|
|
435
|
+
@editor.current_window.cursor_x = 2
|
|
436
|
+
|
|
437
|
+
feed("i", :backspace)
|
|
438
|
+
|
|
439
|
+
assert_equal [" abc"], @editor.current_buffer.lines
|
|
440
|
+
assert_equal 1, @editor.current_window.cursor_x
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
def test_softtabstop_backspace_deletes_spaces_in_chunks_when_expandtab
|
|
444
|
+
@editor.set_option("expandtab", true, scope: :buffer)
|
|
445
|
+
@editor.set_option("tabstop", 4, scope: :buffer)
|
|
446
|
+
@editor.set_option("softtabstop", 4, scope: :buffer)
|
|
447
|
+
@editor.current_buffer.replace_all_lines!([" "])
|
|
448
|
+
@editor.current_window.cursor_x = 8
|
|
449
|
+
|
|
450
|
+
feed("i", :backspace)
|
|
451
|
+
|
|
452
|
+
assert_equal [" "], @editor.current_buffer.lines
|
|
453
|
+
assert_equal 4, @editor.current_window.cursor_x
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
def test_gf_uses_path_and_suffixesadd
|
|
457
|
+
Dir.mktmpdir("ruvim-gf") do |dir|
|
|
458
|
+
FileUtils.mkdir_p(File.join(dir, "lib"))
|
|
459
|
+
target = File.join(dir, "lib", "foo.rb")
|
|
460
|
+
File.write(target, "puts :ok\n")
|
|
461
|
+
@editor.current_buffer.path = File.join(dir, "main.txt")
|
|
462
|
+
@editor.current_buffer.replace_all_lines!(["foo"])
|
|
463
|
+
@editor.current_window.cursor_y = 0
|
|
464
|
+
@editor.current_window.cursor_x = 0
|
|
465
|
+
@editor.set_option("hidden", true, scope: :global)
|
|
466
|
+
@editor.set_option("path", "lib", scope: :buffer)
|
|
467
|
+
@editor.set_option("suffixesadd", ".rb", scope: :buffer)
|
|
468
|
+
|
|
469
|
+
feed("g", "f")
|
|
470
|
+
|
|
471
|
+
assert_equal File.expand_path(target), File.expand_path(@editor.current_buffer.path)
|
|
472
|
+
assert_equal "puts :ok", @editor.current_buffer.line_at(0)
|
|
473
|
+
end
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
def test_gf_supports_recursive_path_entry_with_double_star
|
|
477
|
+
Dir.mktmpdir("ruvim-gf-rec") do |dir|
|
|
478
|
+
FileUtils.mkdir_p(File.join(dir, "lib", "deep", "nest"))
|
|
479
|
+
target = File.join(dir, "lib", "deep", "nest", "foo.rb")
|
|
480
|
+
File.write(target, "puts :deep\n")
|
|
481
|
+
@editor.current_buffer.path = File.join(dir, "main.txt")
|
|
482
|
+
@editor.current_buffer.replace_all_lines!(["foo"])
|
|
483
|
+
@editor.current_window.cursor_y = 0
|
|
484
|
+
@editor.current_window.cursor_x = 0
|
|
485
|
+
@editor.set_option("hidden", true, scope: :global)
|
|
486
|
+
@editor.set_option("path", "lib/**", scope: :buffer)
|
|
487
|
+
@editor.set_option("suffixesadd", ".rb", scope: :buffer)
|
|
488
|
+
|
|
489
|
+
feed("g", "f")
|
|
490
|
+
|
|
491
|
+
assert_equal File.expand_path(target), File.expand_path(@editor.current_buffer.path)
|
|
492
|
+
end
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
def test_gf_supports_file_with_line_number_suffix
|
|
496
|
+
Dir.mktmpdir("ruvim-gf-line") do |dir|
|
|
497
|
+
target = File.join(dir, "foo.rb")
|
|
498
|
+
File.write(target, "line1\nline2\nline3\n")
|
|
499
|
+
@editor.current_buffer.path = File.join(dir, "main.txt")
|
|
500
|
+
@editor.current_buffer.replace_all_lines!(["foo.rb:3"])
|
|
501
|
+
@editor.current_window.cursor_y = 0
|
|
502
|
+
@editor.current_window.cursor_x = 3
|
|
503
|
+
@editor.set_option("hidden", true, scope: :global)
|
|
504
|
+
|
|
505
|
+
feed("g", "f")
|
|
506
|
+
|
|
507
|
+
assert_equal File.expand_path(target), File.expand_path(@editor.current_buffer.path)
|
|
508
|
+
assert_equal 2, @editor.current_window.cursor_y
|
|
509
|
+
end
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
def test_showmatch_message_respects_matchtime_and_clears
|
|
513
|
+
@editor.set_option("showmatch", true, scope: :global)
|
|
514
|
+
@editor.set_option("matchtime", 1, scope: :global) # 0.1 sec
|
|
515
|
+
@editor.current_buffer.replace_all_lines!([""])
|
|
516
|
+
|
|
517
|
+
feed("i", ")")
|
|
518
|
+
assert_equal "match", @editor.message
|
|
519
|
+
|
|
520
|
+
sleep 0.12
|
|
521
|
+
@app.send(:clear_expired_transient_message_if_any)
|
|
522
|
+
assert_equal "", @editor.message
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
def test_insert_arrow_left_respects_whichwrap
|
|
526
|
+
@editor.set_option("whichwrap", "left,right", scope: :global)
|
|
527
|
+
@editor.current_buffer.replace_all_lines!(["ab", "cd"])
|
|
528
|
+
@editor.current_window.cursor_y = 1
|
|
529
|
+
@editor.current_window.cursor_x = 0
|
|
530
|
+
|
|
531
|
+
feed("i", :left)
|
|
532
|
+
|
|
533
|
+
assert_equal :insert, @editor.mode
|
|
534
|
+
assert_equal [0, 2], [@editor.current_window.cursor_y, @editor.current_window.cursor_x]
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
def test_star_search_uses_iskeyword
|
|
538
|
+
@editor.set_option("iskeyword", "@,-", scope: :buffer)
|
|
539
|
+
@editor.current_buffer.replace_all_lines!(["foo-bar x", "foo y", "foo-bar z"])
|
|
540
|
+
@editor.current_window.cursor_y = 0
|
|
541
|
+
@editor.current_window.cursor_x = 1
|
|
542
|
+
|
|
543
|
+
feed("*")
|
|
544
|
+
|
|
545
|
+
assert_equal 2, @editor.current_window.cursor_y
|
|
546
|
+
assert_equal 0, @editor.current_window.cursor_x
|
|
547
|
+
assert_includes @editor.last_search[:pattern], "foo\\-bar"
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
def test_unknown_key_error_clears_on_next_successful_key
|
|
551
|
+
@editor.current_buffer.replace_all_lines!(["a", "b"])
|
|
552
|
+
@editor.current_window.cursor_y = 0
|
|
553
|
+
|
|
554
|
+
feed("_")
|
|
555
|
+
assert @editor.message_error?
|
|
556
|
+
assert_match(/Unknown key:/, @editor.message)
|
|
557
|
+
|
|
558
|
+
feed("j")
|
|
559
|
+
refute @editor.message_error?
|
|
560
|
+
assert_equal "", @editor.message
|
|
561
|
+
assert_equal 1, @editor.current_window.cursor_y
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
def test_normal_message_clears_on_next_key
|
|
565
|
+
@editor.current_buffer.replace_all_lines!(["a", "b"])
|
|
566
|
+
@editor.current_window.cursor_y = 0
|
|
567
|
+
@editor.echo("written")
|
|
568
|
+
|
|
569
|
+
feed("j")
|
|
570
|
+
|
|
571
|
+
refute @editor.message_error?
|
|
572
|
+
assert_equal "", @editor.message
|
|
573
|
+
assert_equal 1, @editor.current_window.cursor_y
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
# --- hit-enter prompt tests ---
|
|
577
|
+
|
|
578
|
+
def test_ls_with_multiple_buffers_enters_hit_enter_mode
|
|
579
|
+
@editor.add_empty_buffer(path: "second.rb")
|
|
580
|
+
@dispatcher.dispatch_ex(@editor, "ls")
|
|
581
|
+
|
|
582
|
+
assert_equal :hit_enter, @editor.mode
|
|
583
|
+
assert_instance_of Array, @editor.hit_enter_lines
|
|
584
|
+
assert_operator @editor.hit_enter_lines.length, :>=, 2
|
|
585
|
+
end
|
|
586
|
+
|
|
587
|
+
def test_ls_with_single_buffer_uses_normal_echo
|
|
588
|
+
@dispatcher.dispatch_ex(@editor, "ls")
|
|
589
|
+
|
|
590
|
+
refute_equal :hit_enter, @editor.mode
|
|
591
|
+
refute_nil @editor.message
|
|
592
|
+
refute @editor.message.to_s.empty?
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
def test_hit_enter_dismiss_with_enter
|
|
596
|
+
@editor.add_empty_buffer(path: "second.rb")
|
|
597
|
+
@dispatcher.dispatch_ex(@editor, "ls")
|
|
598
|
+
assert_equal :hit_enter, @editor.mode
|
|
599
|
+
|
|
600
|
+
feed(:enter)
|
|
601
|
+
|
|
602
|
+
assert_equal :normal, @editor.mode
|
|
603
|
+
assert_nil @editor.hit_enter_lines
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
def test_hit_enter_dismiss_with_escape
|
|
607
|
+
@editor.add_empty_buffer(path: "second.rb")
|
|
608
|
+
@dispatcher.dispatch_ex(@editor, "ls")
|
|
609
|
+
assert_equal :hit_enter, @editor.mode
|
|
610
|
+
|
|
611
|
+
feed(:escape)
|
|
612
|
+
|
|
613
|
+
assert_equal :normal, @editor.mode
|
|
614
|
+
assert_nil @editor.hit_enter_lines
|
|
615
|
+
end
|
|
616
|
+
|
|
617
|
+
def test_hit_enter_dismiss_with_ctrl_c
|
|
618
|
+
@editor.add_empty_buffer(path: "second.rb")
|
|
619
|
+
@dispatcher.dispatch_ex(@editor, "ls")
|
|
620
|
+
assert_equal :hit_enter, @editor.mode
|
|
621
|
+
|
|
622
|
+
feed(:ctrl_c)
|
|
623
|
+
|
|
624
|
+
assert_equal :normal, @editor.mode
|
|
625
|
+
assert_nil @editor.hit_enter_lines
|
|
626
|
+
end
|
|
627
|
+
|
|
628
|
+
def test_hit_enter_colon_enters_command_line
|
|
629
|
+
@editor.add_empty_buffer(path: "second.rb")
|
|
630
|
+
@dispatcher.dispatch_ex(@editor, "ls")
|
|
631
|
+
assert_equal :hit_enter, @editor.mode
|
|
632
|
+
|
|
633
|
+
feed(":")
|
|
634
|
+
|
|
635
|
+
assert_equal :command_line, @editor.mode
|
|
636
|
+
assert_nil @editor.hit_enter_lines
|
|
637
|
+
end
|
|
638
|
+
|
|
639
|
+
def test_hit_enter_slash_enters_search
|
|
640
|
+
@editor.add_empty_buffer(path: "second.rb")
|
|
641
|
+
@dispatcher.dispatch_ex(@editor, "ls")
|
|
642
|
+
assert_equal :hit_enter, @editor.mode
|
|
643
|
+
|
|
644
|
+
feed("/")
|
|
645
|
+
|
|
646
|
+
assert_equal :command_line, @editor.mode
|
|
647
|
+
assert_equal "/", @editor.command_line_prefix
|
|
648
|
+
assert_nil @editor.hit_enter_lines
|
|
649
|
+
end
|
|
650
|
+
|
|
651
|
+
def test_hit_enter_question_enters_reverse_search
|
|
652
|
+
@editor.add_empty_buffer(path: "second.rb")
|
|
653
|
+
@dispatcher.dispatch_ex(@editor, "ls")
|
|
654
|
+
assert_equal :hit_enter, @editor.mode
|
|
655
|
+
|
|
656
|
+
feed("?")
|
|
657
|
+
|
|
658
|
+
assert_equal :command_line, @editor.mode
|
|
659
|
+
assert_equal "?", @editor.command_line_prefix
|
|
660
|
+
assert_nil @editor.hit_enter_lines
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
def test_args_with_multiple_files_enters_hit_enter_mode
|
|
664
|
+
@editor.set_arglist(["a.rb", "b.rb", "c.rb"])
|
|
665
|
+
@dispatcher.dispatch_ex(@editor, "args")
|
|
666
|
+
|
|
667
|
+
assert_equal :hit_enter, @editor.mode
|
|
668
|
+
assert_instance_of Array, @editor.hit_enter_lines
|
|
669
|
+
assert_equal 3, @editor.hit_enter_lines.length
|
|
670
|
+
assert_match(/\[a\.rb\]/, @editor.hit_enter_lines[0])
|
|
671
|
+
end
|
|
672
|
+
|
|
673
|
+
def test_args_with_single_file_uses_normal_echo
|
|
674
|
+
@editor.set_arglist(["a.rb"])
|
|
675
|
+
@dispatcher.dispatch_ex(@editor, "args")
|
|
676
|
+
|
|
677
|
+
refute_equal :hit_enter, @editor.mode
|
|
678
|
+
end
|
|
679
|
+
|
|
680
|
+
def test_set_no_args_enters_hit_enter_mode
|
|
681
|
+
@dispatcher.dispatch_ex(@editor, "set")
|
|
682
|
+
|
|
683
|
+
# option_snapshot returns many options, so always > 1 line
|
|
684
|
+
assert_equal :hit_enter, @editor.mode
|
|
685
|
+
assert_instance_of Array, @editor.hit_enter_lines
|
|
686
|
+
assert_operator @editor.hit_enter_lines.length, :>, 1
|
|
687
|
+
end
|
|
688
|
+
|
|
689
|
+
def test_batch_insert_handles_pasted_text_correctly
|
|
690
|
+
@editor.current_buffer.replace_all_lines!([""])
|
|
691
|
+
# Simulate pasting "Hello World\n" in insert mode (batch of characters)
|
|
692
|
+
feed("i", *"Hello World".chars, :enter, *"Second line".chars, :escape)
|
|
693
|
+
|
|
694
|
+
assert_equal ["Hello World", "Second line"], @editor.current_buffer.lines
|
|
695
|
+
assert_equal :normal, @editor.mode
|
|
696
|
+
end
|
|
697
|
+
|
|
698
|
+
def test_batch_insert_stops_on_escape
|
|
699
|
+
@editor.current_buffer.replace_all_lines!([""])
|
|
700
|
+
# Escape exits insert mode; subsequent keys are normal-mode commands
|
|
701
|
+
feed("i", "a", "b", "c", :escape)
|
|
702
|
+
|
|
703
|
+
assert_equal ["abc"], @editor.current_buffer.lines
|
|
704
|
+
assert_equal :normal, @editor.mode
|
|
705
|
+
end
|
|
706
|
+
|
|
707
|
+
def test_ls_format_shows_vim_style_output
|
|
708
|
+
@editor.add_empty_buffer(path: "second.rb")
|
|
709
|
+
@dispatcher.dispatch_ex(@editor, "ls")
|
|
710
|
+
|
|
711
|
+
lines = @editor.hit_enter_lines
|
|
712
|
+
# Each line should contain the buffer id and name
|
|
713
|
+
assert_match(/1.*\[No Name\]/, lines[0])
|
|
714
|
+
assert_match(/2.*"second\.rb"/, lines[1])
|
|
715
|
+
end
|
|
716
|
+
|
|
717
|
+
def test_equal_equal_indents_current_line
|
|
718
|
+
@editor.current_buffer.replace_all_lines!(["def foo", "bar", "end"])
|
|
719
|
+
@editor.assign_filetype(@editor.current_buffer, "ruby")
|
|
720
|
+
@editor.current_window.cursor_y = 1
|
|
721
|
+
@editor.current_window.cursor_x = 0
|
|
722
|
+
|
|
723
|
+
feed("=", "=")
|
|
724
|
+
|
|
725
|
+
assert_equal ["def foo", " bar", "end"], @editor.current_buffer.lines
|
|
726
|
+
end
|
|
727
|
+
|
|
728
|
+
def test_equal_j_indents_two_lines
|
|
729
|
+
@editor.current_buffer.replace_all_lines!(["def foo", "bar", "baz", "end"])
|
|
730
|
+
@editor.assign_filetype(@editor.current_buffer, "ruby")
|
|
731
|
+
@editor.current_window.cursor_y = 1
|
|
732
|
+
@editor.current_window.cursor_x = 0
|
|
733
|
+
|
|
734
|
+
feed("=", "j")
|
|
735
|
+
|
|
736
|
+
assert_equal ["def foo", " bar", " baz", "end"], @editor.current_buffer.lines
|
|
737
|
+
end
|
|
738
|
+
|
|
739
|
+
def test_visual_equal_indents_selection
|
|
740
|
+
@editor.current_buffer.replace_all_lines!(["def foo", "bar", "end"])
|
|
741
|
+
@editor.assign_filetype(@editor.current_buffer, "ruby")
|
|
742
|
+
@editor.current_window.cursor_y = 0
|
|
743
|
+
@editor.current_window.cursor_x = 0
|
|
744
|
+
|
|
745
|
+
feed("V", "j", "j", "=")
|
|
746
|
+
|
|
747
|
+
assert_equal ["def foo", " bar", "end"], @editor.current_buffer.lines
|
|
748
|
+
assert_equal :normal, @editor.mode
|
|
749
|
+
end
|
|
750
|
+
|
|
751
|
+
# :qa / :qall tests
|
|
752
|
+
|
|
753
|
+
def test_qa_quits_with_multiple_windows
|
|
754
|
+
@dispatcher.dispatch_ex(@editor, "split")
|
|
755
|
+
assert_equal 2, @editor.window_count
|
|
756
|
+
|
|
757
|
+
@dispatcher.dispatch_ex(@editor, "qa")
|
|
758
|
+
assert_equal false, @editor.running?
|
|
759
|
+
end
|
|
760
|
+
|
|
761
|
+
def test_qa_refuses_with_unsaved_changes
|
|
762
|
+
@editor.current_buffer.replace_all_lines!(["modified"])
|
|
763
|
+
@editor.current_buffer.instance_variable_set(:@modified, true)
|
|
764
|
+
|
|
765
|
+
@dispatcher.dispatch_ex(@editor, "qa")
|
|
766
|
+
assert @editor.running?
|
|
767
|
+
assert_match(/unsaved changes/, @editor.message)
|
|
768
|
+
end
|
|
769
|
+
|
|
770
|
+
def test_qa_bang_forces_quit_with_unsaved_changes
|
|
771
|
+
@editor.current_buffer.replace_all_lines!(["modified"])
|
|
772
|
+
@editor.current_buffer.instance_variable_set(:@modified, true)
|
|
773
|
+
|
|
774
|
+
@dispatcher.dispatch_ex(@editor, "qa!")
|
|
775
|
+
assert_equal false, @editor.running?
|
|
776
|
+
end
|
|
777
|
+
|
|
778
|
+
def test_wqa_writes_all_and_quits
|
|
779
|
+
Dir.mktmpdir do |dir|
|
|
780
|
+
path1 = File.join(dir, "a.txt")
|
|
781
|
+
path2 = File.join(dir, "b.txt")
|
|
782
|
+
File.write(path1, "")
|
|
783
|
+
File.write(path2, "")
|
|
784
|
+
|
|
785
|
+
@editor.current_buffer.replace_all_lines!(["hello"])
|
|
786
|
+
@editor.current_buffer.instance_variable_set(:@path, path1)
|
|
787
|
+
@editor.current_buffer.instance_variable_set(:@modified, true)
|
|
788
|
+
|
|
789
|
+
buf2 = @editor.add_empty_buffer(path: path2)
|
|
790
|
+
buf2.replace_all_lines!(["world"])
|
|
791
|
+
buf2.instance_variable_set(:@modified, true)
|
|
792
|
+
|
|
793
|
+
@dispatcher.dispatch_ex(@editor, "wqa")
|
|
794
|
+
assert_equal false, @editor.running?
|
|
795
|
+
assert_equal "hello", File.read(path1)
|
|
796
|
+
assert_equal "world", File.read(path2)
|
|
797
|
+
end
|
|
798
|
+
end
|
|
799
|
+
|
|
800
|
+
def test_shift_right_splits_when_single_window
|
|
801
|
+
assert_equal 1, @editor.window_count
|
|
802
|
+
first_win = @editor.current_window
|
|
803
|
+
feed(:shift_right)
|
|
804
|
+
assert_equal 2, @editor.window_count
|
|
805
|
+
# Focus should be on the new (right) window
|
|
806
|
+
refute_equal first_win.id, @editor.current_window.id
|
|
807
|
+
assert_equal :vertical, @editor.window_layout
|
|
808
|
+
end
|
|
809
|
+
|
|
810
|
+
def test_shift_left_splits_when_single_window
|
|
811
|
+
assert_equal 1, @editor.window_count
|
|
812
|
+
first_win = @editor.current_window
|
|
813
|
+
feed(:shift_left)
|
|
814
|
+
assert_equal 2, @editor.window_count
|
|
815
|
+
# Focus should be on the new (left) window
|
|
816
|
+
refute_equal first_win.id, @editor.current_window.id
|
|
817
|
+
assert_equal :vertical, @editor.window_layout
|
|
818
|
+
end
|
|
819
|
+
|
|
820
|
+
def test_shift_down_splits_when_single_window
|
|
821
|
+
assert_equal 1, @editor.window_count
|
|
822
|
+
first_win = @editor.current_window
|
|
823
|
+
feed(:shift_down)
|
|
824
|
+
assert_equal 2, @editor.window_count
|
|
825
|
+
refute_equal first_win.id, @editor.current_window.id
|
|
826
|
+
assert_equal :horizontal, @editor.window_layout
|
|
827
|
+
end
|
|
828
|
+
|
|
829
|
+
def test_shift_up_splits_when_single_window
|
|
830
|
+
assert_equal 1, @editor.window_count
|
|
831
|
+
first_win = @editor.current_window
|
|
832
|
+
feed(:shift_up)
|
|
833
|
+
assert_equal 2, @editor.window_count
|
|
834
|
+
refute_equal first_win.id, @editor.current_window.id
|
|
835
|
+
assert_equal :horizontal, @editor.window_layout
|
|
836
|
+
end
|
|
837
|
+
|
|
838
|
+
def test_shift_right_splits_even_with_horizontal_split
|
|
839
|
+
# Horizontal split exists, but no window to the right → vsplit
|
|
840
|
+
@editor.split_current_window(layout: :horizontal)
|
|
841
|
+
assert_equal 2, @editor.window_count
|
|
842
|
+
feed(:shift_right)
|
|
843
|
+
assert_equal 3, @editor.window_count
|
|
844
|
+
end
|
|
845
|
+
|
|
846
|
+
def test_shift_left_splits_even_with_horizontal_split
|
|
847
|
+
@editor.split_current_window(layout: :horizontal)
|
|
848
|
+
assert_equal 2, @editor.window_count
|
|
849
|
+
feed(:shift_left)
|
|
850
|
+
assert_equal 3, @editor.window_count
|
|
851
|
+
end
|
|
852
|
+
|
|
853
|
+
def test_shift_down_splits_even_with_vertical_split
|
|
854
|
+
@editor.split_current_window(layout: :vertical)
|
|
855
|
+
assert_equal 2, @editor.window_count
|
|
856
|
+
feed(:shift_down)
|
|
857
|
+
assert_equal 3, @editor.window_count
|
|
858
|
+
end
|
|
859
|
+
|
|
860
|
+
def test_shift_up_splits_even_with_vertical_split
|
|
861
|
+
@editor.split_current_window(layout: :vertical)
|
|
862
|
+
assert_equal 2, @editor.window_count
|
|
863
|
+
feed(:shift_up)
|
|
864
|
+
assert_equal 3, @editor.window_count
|
|
865
|
+
end
|
|
866
|
+
|
|
867
|
+
def test_shift_arrow_moves_window_focus_when_multiple_windows
|
|
868
|
+
# Create a vertical split so we have two windows
|
|
869
|
+
first_win = @editor.current_window
|
|
870
|
+
@editor.split_current_window(layout: :vertical)
|
|
871
|
+
second_win = @editor.current_window
|
|
872
|
+
|
|
873
|
+
# After split, focus is on the new (second) window
|
|
874
|
+
assert_equal second_win.id, @editor.current_window.id
|
|
875
|
+
|
|
876
|
+
# Shift+Left should move focus to the left window (no new split)
|
|
877
|
+
feed(:shift_left)
|
|
878
|
+
assert_equal first_win.id, @editor.current_window.id
|
|
879
|
+
assert_equal 2, @editor.window_count
|
|
880
|
+
|
|
881
|
+
# Shift+Right should move focus back to the right window
|
|
882
|
+
feed(:shift_right)
|
|
883
|
+
assert_equal second_win.id, @editor.current_window.id
|
|
884
|
+
assert_equal 2, @editor.window_count
|
|
885
|
+
end
|
|
886
|
+
|
|
887
|
+
def test_shift_arrow_up_down_moves_window_focus_horizontal_split
|
|
888
|
+
# Create a horizontal split so we have two windows
|
|
889
|
+
first_win = @editor.current_window
|
|
890
|
+
@editor.split_current_window(layout: :horizontal)
|
|
891
|
+
second_win = @editor.current_window
|
|
892
|
+
|
|
893
|
+
assert_equal second_win.id, @editor.current_window.id
|
|
894
|
+
|
|
895
|
+
# Shift+Up should move focus to the upper window (no new split)
|
|
896
|
+
feed(:shift_up)
|
|
897
|
+
assert_equal first_win.id, @editor.current_window.id
|
|
898
|
+
assert_equal 2, @editor.window_count
|
|
899
|
+
|
|
900
|
+
# Shift+Down should move focus back to the lower window
|
|
901
|
+
feed(:shift_down)
|
|
902
|
+
assert_equal second_win.id, @editor.current_window.id
|
|
903
|
+
assert_equal 2, @editor.window_count
|
|
904
|
+
end
|
|
905
|
+
|
|
906
|
+
# --- Nested layout tree tests ---
|
|
907
|
+
|
|
908
|
+
def test_vsplit_then_split_creates_nested_layout
|
|
909
|
+
# Start with 1 window (win1)
|
|
910
|
+
win1 = @editor.current_window
|
|
911
|
+
# vsplit → creates win2 to the right
|
|
912
|
+
@editor.split_current_window(layout: :vertical)
|
|
913
|
+
win2 = @editor.current_window
|
|
914
|
+
assert_equal 2, @editor.window_count
|
|
915
|
+
|
|
916
|
+
# split the right window (win2) horizontally → creates win3 below win2
|
|
917
|
+
@editor.split_current_window(layout: :horizontal)
|
|
918
|
+
win3 = @editor.current_window
|
|
919
|
+
assert_equal 3, @editor.window_count
|
|
920
|
+
|
|
921
|
+
# Layout tree should be: vsplit[ win1, hsplit[ win2, win3 ] ]
|
|
922
|
+
tree = @editor.layout_tree
|
|
923
|
+
assert_equal :vsplit, tree[:type]
|
|
924
|
+
assert_equal 2, tree[:children].length
|
|
925
|
+
assert_equal :window, tree[:children][0][:type]
|
|
926
|
+
assert_equal win1.id, tree[:children][0][:id]
|
|
927
|
+
assert_equal :hsplit, tree[:children][1][:type]
|
|
928
|
+
assert_equal 2, tree[:children][1][:children].length
|
|
929
|
+
|
|
930
|
+
# window_order should traverse leaves left-to-right, top-to-bottom
|
|
931
|
+
assert_equal [win1.id, win2.id, win3.id], @editor.window_order
|
|
932
|
+
end
|
|
933
|
+
|
|
934
|
+
def test_split_then_vsplit_creates_nested_layout
|
|
935
|
+
# split → creates win2 below
|
|
936
|
+
@editor.split_current_window(layout: :horizontal)
|
|
937
|
+
assert_equal 2, @editor.window_count
|
|
938
|
+
|
|
939
|
+
# vsplit the lower window → creates win3 to the right of win2
|
|
940
|
+
@editor.split_current_window(layout: :vertical)
|
|
941
|
+
assert_equal 3, @editor.window_count
|
|
942
|
+
|
|
943
|
+
# Layout tree should be: hsplit[ win1, vsplit[ win2, win3 ] ]
|
|
944
|
+
tree = @editor.layout_tree
|
|
945
|
+
assert_equal :hsplit, tree[:type]
|
|
946
|
+
assert_equal 2, tree[:children].length
|
|
947
|
+
assert_equal :window, tree[:children][0][:type]
|
|
948
|
+
assert_equal :vsplit, tree[:children][1][:type]
|
|
949
|
+
end
|
|
950
|
+
|
|
951
|
+
def test_close_window_simplifies_nested_tree
|
|
952
|
+
@editor.split_current_window(layout: :vertical)
|
|
953
|
+
@editor.split_current_window(layout: :horizontal)
|
|
954
|
+
win3 = @editor.current_window
|
|
955
|
+
assert_equal 3, @editor.window_count
|
|
956
|
+
|
|
957
|
+
# Close win3 → hsplit node should collapse, leaving vsplit[ win1, win2 ]
|
|
958
|
+
@editor.close_window(win3.id)
|
|
959
|
+
assert_equal 2, @editor.window_count
|
|
960
|
+
|
|
961
|
+
tree = @editor.layout_tree
|
|
962
|
+
assert_equal :vsplit, tree[:type]
|
|
963
|
+
assert_equal 2, tree[:children].length
|
|
964
|
+
assert_equal :window, tree[:children][0][:type]
|
|
965
|
+
assert_equal :window, tree[:children][1][:type]
|
|
966
|
+
end
|
|
967
|
+
|
|
968
|
+
def test_close_window_to_single_produces_single_layout
|
|
969
|
+
@editor.split_current_window(layout: :vertical)
|
|
970
|
+
win2 = @editor.current_window
|
|
971
|
+
assert_equal 2, @editor.window_count
|
|
972
|
+
|
|
973
|
+
@editor.close_window(win2.id)
|
|
974
|
+
assert_equal 1, @editor.window_count
|
|
975
|
+
assert_equal :single, @editor.window_layout
|
|
976
|
+
end
|
|
977
|
+
|
|
978
|
+
def test_focus_window_direction_in_nested_layout
|
|
979
|
+
# Create vsplit[ win1, hsplit[ win2, win3 ] ]
|
|
980
|
+
win1 = @editor.current_window
|
|
981
|
+
@editor.split_current_window(layout: :vertical)
|
|
982
|
+
win2 = @editor.current_window
|
|
983
|
+
@editor.split_current_window(layout: :horizontal)
|
|
984
|
+
win3 = @editor.current_window
|
|
985
|
+
|
|
986
|
+
# From win3 (bottom-right), going left should reach win1
|
|
987
|
+
@editor.focus_window(win3.id)
|
|
988
|
+
@editor.focus_window_direction(:left)
|
|
989
|
+
assert_equal win1.id, @editor.current_window_id
|
|
990
|
+
|
|
991
|
+
# From win1 (left), going right should reach win2 or win3
|
|
992
|
+
@editor.focus_window_direction(:right)
|
|
993
|
+
assert_includes [win2.id, win3.id], @editor.current_window_id
|
|
994
|
+
|
|
995
|
+
# From win2 (top-right), going down should reach win3
|
|
996
|
+
@editor.focus_window(win2.id)
|
|
997
|
+
@editor.focus_window_direction(:down)
|
|
998
|
+
assert_equal win3.id, @editor.current_window_id
|
|
999
|
+
|
|
1000
|
+
# From win3 (bottom-right), going up should reach win2
|
|
1001
|
+
@editor.focus_window_direction(:up)
|
|
1002
|
+
assert_equal win2.id, @editor.current_window_id
|
|
1003
|
+
end
|
|
1004
|
+
|
|
1005
|
+
def test_shift_left_does_not_split_at_edge_of_existing_vsplit
|
|
1006
|
+
# vsplit creates [win1, win2], focus on win2
|
|
1007
|
+
@editor.split_current_window(layout: :vertical)
|
|
1008
|
+
# Move focus to left (win1)
|
|
1009
|
+
feed(:shift_left)
|
|
1010
|
+
assert_equal 2, @editor.window_count
|
|
1011
|
+
|
|
1012
|
+
# Now we're on win1 (leftmost). Shift+Left should NOT split because
|
|
1013
|
+
# there are already windows on the same axis (horizontal neighbors exist).
|
|
1014
|
+
feed(:shift_left)
|
|
1015
|
+
assert_equal 2, @editor.window_count, "Should not split at edge of existing vsplit"
|
|
1016
|
+
|
|
1017
|
+
# Pressing again should still not split
|
|
1018
|
+
feed(:shift_left)
|
|
1019
|
+
assert_equal 2, @editor.window_count
|
|
1020
|
+
end
|
|
1021
|
+
|
|
1022
|
+
def test_shift_left_splits_bottom_window_in_nested_layout
|
|
1023
|
+
# Create layout: hsplit[ vsplit[win1, win2], win3 ]
|
|
1024
|
+
# Start with win1
|
|
1025
|
+
win1 = @editor.current_window
|
|
1026
|
+
# vsplit → vsplit[win1, win2]
|
|
1027
|
+
@editor.split_current_window(layout: :vertical)
|
|
1028
|
+
win2 = @editor.current_window
|
|
1029
|
+
# Focus back to win1, then split horizontally from win1
|
|
1030
|
+
# Actually, easier: split from win2 horizontally to get the right structure
|
|
1031
|
+
# Let me build it differently: start fresh
|
|
1032
|
+
@editor.focus_window(win1.id)
|
|
1033
|
+
|
|
1034
|
+
# From win1, hsplit → hsplit[win1, win3], but we want vsplit on top.
|
|
1035
|
+
# Let me just build the tree directly.
|
|
1036
|
+
# Better approach: vsplit first, then move up and hsplit from the vsplit pair
|
|
1037
|
+
# Actually: vsplit[win1, win2], then from win1 do hsplit → hsplit[vsplit[...], win3]
|
|
1038
|
+
# No, that's wrong. Let me think:
|
|
1039
|
+
# We want hsplit[ vsplit[A, B], C ]
|
|
1040
|
+
# Step 1: split (horizontal) → hsplit[win1, win3], focus on win3
|
|
1041
|
+
@editor.close_window(win2.id)
|
|
1042
|
+
assert_equal 1, @editor.window_count
|
|
1043
|
+
@editor.split_current_window(layout: :horizontal)
|
|
1044
|
+
win3 = @editor.current_window
|
|
1045
|
+
# Step 2: focus win1 (top), then vsplit → vsplit[win1, win2] inside hsplit
|
|
1046
|
+
@editor.focus_window(win1.id)
|
|
1047
|
+
@editor.split_current_window(layout: :vertical)
|
|
1048
|
+
win2 = @editor.current_window
|
|
1049
|
+
assert_equal 3, @editor.window_count
|
|
1050
|
+
|
|
1051
|
+
# Layout should be: hsplit[ vsplit[win1, win2], win3 ]
|
|
1052
|
+
tree = @editor.layout_tree
|
|
1053
|
+
assert_equal :hsplit, tree[:type]
|
|
1054
|
+
assert_equal :vsplit, tree[:children][0][:type]
|
|
1055
|
+
assert_equal :window, tree[:children][1][:type]
|
|
1056
|
+
assert_equal win3.id, tree[:children][1][:id]
|
|
1057
|
+
|
|
1058
|
+
# From win3 (full-width bottom), Shift+Left should SPLIT (no vsplit ancestor)
|
|
1059
|
+
@editor.focus_window(win3.id)
|
|
1060
|
+
feed(:shift_left)
|
|
1061
|
+
assert_equal 4, @editor.window_count, "Shift+Left from full-width bottom should vsplit it"
|
|
1062
|
+
end
|
|
1063
|
+
|
|
1064
|
+
def test_same_direction_split_merges_into_parent
|
|
1065
|
+
# hsplit[ win1, win2 ], then split win2 again horizontally
|
|
1066
|
+
@editor.split_current_window(layout: :horizontal)
|
|
1067
|
+
@editor.split_current_window(layout: :horizontal)
|
|
1068
|
+
assert_equal 3, @editor.window_count
|
|
1069
|
+
|
|
1070
|
+
# All three should be in a single hsplit (no nested hsplit inside hsplit)
|
|
1071
|
+
tree = @editor.layout_tree
|
|
1072
|
+
assert_equal :hsplit, tree[:type]
|
|
1073
|
+
assert_equal 3, tree[:children].length
|
|
1074
|
+
end
|
|
77
1075
|
end
|