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/screen_test.rb
CHANGED
|
@@ -55,6 +55,38 @@ class ScreenTest < Minitest::Test
|
|
|
55
55
|
assert_equal " 3 ", screen.send(:line_number_prefix, editor, win, buf, 2, 3) # current line is absolute when both enabled
|
|
56
56
|
end
|
|
57
57
|
|
|
58
|
+
def test_signcolumn_yes_reserves_one_column_in_gutter
|
|
59
|
+
editor = RuVim::Editor.new
|
|
60
|
+
buf = editor.add_empty_buffer
|
|
61
|
+
win = editor.add_window(buffer_id: buf.id)
|
|
62
|
+
editor.set_option("number", true, scope: :window, window: win, buffer: buf)
|
|
63
|
+
editor.set_option("signcolumn", "yes", scope: :window, window: win, buffer: buf)
|
|
64
|
+
term = TerminalStub.new([8, 20])
|
|
65
|
+
screen = RuVim::Screen.new(terminal: term)
|
|
66
|
+
|
|
67
|
+
w = screen.send(:number_column_width, editor, win, buf)
|
|
68
|
+
prefix = screen.send(:line_number_prefix, editor, win, buf, 0, w)
|
|
69
|
+
|
|
70
|
+
assert_equal 6, w # sign(1) + default numberwidth(4) + trailing space
|
|
71
|
+
assert_equal " 1 ", prefix
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def test_signcolumn_yes_with_width_reserves_multiple_columns
|
|
75
|
+
editor = RuVim::Editor.new
|
|
76
|
+
buf = editor.add_empty_buffer
|
|
77
|
+
win = editor.add_window(buffer_id: buf.id)
|
|
78
|
+
editor.set_option("number", true, scope: :window, window: win, buffer: buf)
|
|
79
|
+
editor.set_option("signcolumn", "yes:2", scope: :window, window: win, buffer: buf)
|
|
80
|
+
term = TerminalStub.new([8, 20])
|
|
81
|
+
screen = RuVim::Screen.new(terminal: term)
|
|
82
|
+
|
|
83
|
+
w = screen.send(:number_column_width, editor, win, buf)
|
|
84
|
+
prefix = screen.send(:line_number_prefix, editor, win, buf, 0, w)
|
|
85
|
+
|
|
86
|
+
assert_equal 7, w # sign(2) + default numberwidth(4) + trailing space
|
|
87
|
+
assert_equal " 1 ", prefix
|
|
88
|
+
end
|
|
89
|
+
|
|
58
90
|
def test_render_shows_error_message_on_command_line_row_with_highlight
|
|
59
91
|
editor = RuVim::Editor.new
|
|
60
92
|
buf = editor.add_empty_buffer
|
|
@@ -69,6 +101,36 @@ class ScreenTest < Minitest::Test
|
|
|
69
101
|
assert_includes out, "boom"
|
|
70
102
|
end
|
|
71
103
|
|
|
104
|
+
def test_status_line_shows_stream_state_for_stdin_buffer
|
|
105
|
+
editor = RuVim::Editor.new
|
|
106
|
+
buf = editor.add_virtual_buffer(kind: :stream, name: "[stdin]", lines: [""], readonly: true, modifiable: false)
|
|
107
|
+
buf.stream_state = :closed
|
|
108
|
+
editor.add_window(buffer_id: buf.id)
|
|
109
|
+
|
|
110
|
+
term = TerminalStub.new([6, 60])
|
|
111
|
+
screen = RuVim::Screen.new(terminal: term)
|
|
112
|
+
line = screen.send(:status_line, editor, 60)
|
|
113
|
+
|
|
114
|
+
assert_includes line, "[stdin/closed]"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def test_render_uses_dim_and_brighter_line_number_gutter_colors
|
|
118
|
+
editor = RuVim::Editor.new
|
|
119
|
+
buf = editor.add_empty_buffer
|
|
120
|
+
win = editor.add_window(buffer_id: buf.id)
|
|
121
|
+
buf.replace_all_lines!(["foo", "bar"])
|
|
122
|
+
editor.set_option("number", true, scope: :window, window: win, buffer: buf)
|
|
123
|
+
win.cursor_y = 1
|
|
124
|
+
|
|
125
|
+
term = TerminalStub.new([6, 20])
|
|
126
|
+
screen = RuVim::Screen.new(terminal: term)
|
|
127
|
+
screen.render(editor)
|
|
128
|
+
out = term.writes.last
|
|
129
|
+
|
|
130
|
+
assert_includes out, "\e[90m"
|
|
131
|
+
assert_includes out, "\e[37m"
|
|
132
|
+
end
|
|
133
|
+
|
|
72
134
|
def test_render_reuses_syntax_highlight_cache_for_same_line
|
|
73
135
|
editor = RuVim::Editor.new
|
|
74
136
|
buf = editor.add_empty_buffer
|
|
@@ -100,6 +162,40 @@ class ScreenTest < Minitest::Test
|
|
|
100
162
|
assert_equal 1, calls
|
|
101
163
|
end
|
|
102
164
|
|
|
165
|
+
def test_render_reuses_wrap_segment_cache_for_same_line
|
|
166
|
+
editor = RuVim::Editor.new
|
|
167
|
+
buf = editor.add_empty_buffer
|
|
168
|
+
win = editor.add_window(buffer_id: buf.id)
|
|
169
|
+
buf.replace_all_lines!(["x" * 400])
|
|
170
|
+
editor.set_option("wrap", true, scope: :window, window: win, buffer: buf)
|
|
171
|
+
|
|
172
|
+
term = TerminalStub.new([8, 20])
|
|
173
|
+
screen = RuVim::Screen.new(terminal: term)
|
|
174
|
+
|
|
175
|
+
calls = [0, 0]
|
|
176
|
+
render_index = 0
|
|
177
|
+
mod = RuVim::TextMetrics.singleton_class
|
|
178
|
+
verbose, $VERBOSE = $VERBOSE, nil
|
|
179
|
+
mod.alias_method(:__orig_clip_cells_for_width_for_screen_test, :clip_cells_for_width)
|
|
180
|
+
mod.define_method(:clip_cells_for_width) do |*args, **kwargs|
|
|
181
|
+
calls[render_index] += 1
|
|
182
|
+
__orig_clip_cells_for_width_for_screen_test(*args, **kwargs)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
begin
|
|
186
|
+
render_index = 0
|
|
187
|
+
screen.render(editor)
|
|
188
|
+
render_index = 1
|
|
189
|
+
screen.render(editor)
|
|
190
|
+
ensure
|
|
191
|
+
mod.alias_method(:clip_cells_for_width, :__orig_clip_cells_for_width_for_screen_test)
|
|
192
|
+
mod.remove_method(:__orig_clip_cells_for_width_for_screen_test) rescue nil
|
|
193
|
+
$VERBOSE = verbose
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
assert_operator calls[1], :<, calls[0]
|
|
197
|
+
end
|
|
198
|
+
|
|
103
199
|
def test_render_text_line_with_cursor_search_and_syntax_highlights_fits_width
|
|
104
200
|
editor = RuVim::Editor.new
|
|
105
201
|
buf = editor.add_empty_buffer
|
|
@@ -120,4 +216,378 @@ class ScreenTest < Minitest::Test
|
|
|
120
216
|
assert_equal 10, RuVim::DisplayWidth.display_width(visible, tabstop: 2)
|
|
121
217
|
refute_includes out, "\n"
|
|
122
218
|
end
|
|
219
|
+
|
|
220
|
+
def test_termguicolors_uses_truecolor_sequences_for_search_highlight
|
|
221
|
+
editor = RuVim::Editor.new
|
|
222
|
+
buf = editor.add_empty_buffer
|
|
223
|
+
win = editor.add_window(buffer_id: buf.id)
|
|
224
|
+
buf.replace_all_lines!(["foo"])
|
|
225
|
+
editor.set_last_search(pattern: "foo", direction: :forward)
|
|
226
|
+
editor.set_option("termguicolors", true, scope: :global)
|
|
227
|
+
|
|
228
|
+
term = TerminalStub.new([6, 20])
|
|
229
|
+
screen = RuVim::Screen.new(terminal: term)
|
|
230
|
+
out = screen.send(:render_text_line, buf.line_at(0), editor, buffer_row: 0, window: win, buffer: buf, width: 6)
|
|
231
|
+
|
|
232
|
+
assert_includes out, "\e[48;2;255;215;0m"
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def test_render_text_line_respects_list_and_listchars_for_tab_trail_and_nbsp
|
|
236
|
+
editor = RuVim::Editor.new
|
|
237
|
+
buf = editor.add_empty_buffer
|
|
238
|
+
win = editor.add_window(buffer_id: buf.id)
|
|
239
|
+
buf.replace_all_lines!(["\tA \u00A0 "])
|
|
240
|
+
editor.set_option("list", true, scope: :window, window: win, buffer: buf)
|
|
241
|
+
editor.set_option("listchars", "tab:>.,trail:~,nbsp:*", scope: :window, window: win, buffer: buf)
|
|
242
|
+
|
|
243
|
+
term = TerminalStub.new([6, 20])
|
|
244
|
+
screen = RuVim::Screen.new(terminal: term)
|
|
245
|
+
out = screen.send(:render_text_line, buf.line_at(0), editor, buffer_row: 0, window: win, buffer: buf, width: 12)
|
|
246
|
+
visible = out.gsub(/\e\[[0-9;?]*[A-Za-z]/, "")
|
|
247
|
+
|
|
248
|
+
assert_includes visible, ">."
|
|
249
|
+
assert_includes visible, "*"
|
|
250
|
+
assert_includes visible, "~"
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def test_render_text_line_sanitizes_terminal_escape_controls
|
|
254
|
+
editor = RuVim::Editor.new
|
|
255
|
+
buf = editor.add_empty_buffer
|
|
256
|
+
win = editor.add_window(buffer_id: buf.id)
|
|
257
|
+
buf.replace_all_lines!(["A\e]52;c;owned\aB"])
|
|
258
|
+
win.cursor_y = 1 # avoid cursor highlight on the tested row
|
|
259
|
+
|
|
260
|
+
term = TerminalStub.new([6, 40])
|
|
261
|
+
screen = RuVim::Screen.new(terminal: term)
|
|
262
|
+
out = screen.send(:render_text_line, buf.line_at(0), editor, buffer_row: 0, window: win, buffer: buf, width: 20)
|
|
263
|
+
|
|
264
|
+
refute_includes out, "\e]52"
|
|
265
|
+
visible = out.gsub(/\e\[[0-9;?]*[A-Za-z]/, "")
|
|
266
|
+
assert_includes visible, "A"
|
|
267
|
+
assert_includes visible, "B"
|
|
268
|
+
assert_includes visible, "?"
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def test_wrap_and_showbreak_render_continuation_rows
|
|
272
|
+
editor = RuVim::Editor.new
|
|
273
|
+
buf = editor.add_empty_buffer
|
|
274
|
+
win = editor.add_window(buffer_id: buf.id)
|
|
275
|
+
buf.replace_all_lines!(["abcdef ghijkl"])
|
|
276
|
+
editor.set_option("wrap", true, scope: :window, window: win, buffer: buf)
|
|
277
|
+
editor.set_option("showbreak", ">>", scope: :window, window: win, buffer: buf)
|
|
278
|
+
|
|
279
|
+
term = TerminalStub.new([6, 8])
|
|
280
|
+
screen = RuVim::Screen.new(terminal: term)
|
|
281
|
+
rows, cols = term.winsize
|
|
282
|
+
text_rows, text_cols = editor.text_viewport_size(rows:, cols:)
|
|
283
|
+
rects = screen.send(:window_rects, editor, text_rows:, text_cols:)
|
|
284
|
+
frame = screen.send(:build_frame, editor, rows:, cols:, text_rows:, text_cols:, rects:)
|
|
285
|
+
|
|
286
|
+
row1 = frame[:lines][1].to_s.gsub(/\e\[[0-9;?]*[A-Za-z]/, "")
|
|
287
|
+
row2 = frame[:lines][2].to_s.gsub(/\e\[[0-9;?]*[A-Za-z]/, "")
|
|
288
|
+
assert_includes row1, "abcdef"
|
|
289
|
+
assert_includes row2, ">>"
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def test_cursor_screen_position_supports_virtualedit_past_eol
|
|
293
|
+
editor = RuVim::Editor.new
|
|
294
|
+
buf = editor.add_empty_buffer
|
|
295
|
+
win = editor.add_window(buffer_id: buf.id)
|
|
296
|
+
buf.replace_all_lines!(["abc"])
|
|
297
|
+
editor.set_option("virtualedit", "onemore", scope: :global)
|
|
298
|
+
win.cursor_y = 0
|
|
299
|
+
win.cursor_x = 4
|
|
300
|
+
|
|
301
|
+
term = TerminalStub.new([6, 20])
|
|
302
|
+
screen = RuVim::Screen.new(terminal: term)
|
|
303
|
+
rows, cols = term.winsize
|
|
304
|
+
text_rows, text_cols = editor.text_viewport_size(rows:, cols:)
|
|
305
|
+
rects = screen.send(:window_rects, editor, text_rows:, text_cols:)
|
|
306
|
+
pos = screen.send(:cursor_screen_position, editor, text_rows, rects)
|
|
307
|
+
|
|
308
|
+
# col 1-based; "abc" places cursor at 4, one-past-eol should be 5
|
|
309
|
+
assert_equal [1, 5], pos
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def test_cursor_screen_position_is_clamped_to_text_area_under_wrap
|
|
313
|
+
editor = RuVim::Editor.new
|
|
314
|
+
buf = editor.add_empty_buffer
|
|
315
|
+
win = editor.add_window(buffer_id: buf.id)
|
|
316
|
+
buf.replace_all_lines!(["x" * 40, "tail"])
|
|
317
|
+
editor.set_option("wrap", true, scope: :window, window: win, buffer: buf)
|
|
318
|
+
win.row_offset = 0
|
|
319
|
+
win.cursor_y = 1
|
|
320
|
+
win.cursor_x = 0
|
|
321
|
+
|
|
322
|
+
term = TerminalStub.new([6, 8]) # text_rows = 4 (footer 2 rows)
|
|
323
|
+
screen = RuVim::Screen.new(terminal: term)
|
|
324
|
+
rows, cols = term.winsize
|
|
325
|
+
text_rows, text_cols = editor.text_viewport_size(rows:, cols:)
|
|
326
|
+
rects = screen.send(:window_rects, editor, text_rows:, text_cols:)
|
|
327
|
+
row, _col = screen.send(:cursor_screen_position, editor, text_rows, rects)
|
|
328
|
+
|
|
329
|
+
assert_operator row, :<=, text_rows
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def test_linebreak_and_breakindent_prefer_space_wrap
|
|
333
|
+
editor = RuVim::Editor.new
|
|
334
|
+
buf = editor.add_empty_buffer
|
|
335
|
+
win = editor.add_window(buffer_id: buf.id)
|
|
336
|
+
buf.replace_all_lines!([" foo bar baz"])
|
|
337
|
+
editor.set_option("wrap", true, scope: :window, window: win, buffer: buf)
|
|
338
|
+
editor.set_option("linebreak", true, scope: :window, window: win, buffer: buf)
|
|
339
|
+
editor.set_option("breakindent", true, scope: :window, window: win, buffer: buf)
|
|
340
|
+
editor.set_option("showbreak", ">", scope: :window, window: win, buffer: buf)
|
|
341
|
+
|
|
342
|
+
term = TerminalStub.new([6, 10])
|
|
343
|
+
screen = RuVim::Screen.new(terminal: term)
|
|
344
|
+
rows, cols = term.winsize
|
|
345
|
+
text_rows, text_cols = editor.text_viewport_size(rows:, cols:)
|
|
346
|
+
rects = screen.send(:window_rects, editor, text_rows:, text_cols:)
|
|
347
|
+
frame = screen.send(:build_frame, editor, rows:, cols:, text_rows:, text_cols:, rects:)
|
|
348
|
+
|
|
349
|
+
row2 = frame[:lines][2].to_s.gsub(/\e\[[0-9;?]*[A-Za-z]/, "")
|
|
350
|
+
assert_includes row2, ">"
|
|
351
|
+
assert_match(/\s>|\>\s/, row2)
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def test_wrap_keeps_cursor_visible_after_very_long_previous_line
|
|
355
|
+
editor = RuVim::Editor.new
|
|
356
|
+
buf = editor.add_empty_buffer
|
|
357
|
+
win = editor.add_window(buffer_id: buf.id)
|
|
358
|
+
buf.replace_all_lines!(["x" * 200, "tail"])
|
|
359
|
+
editor.set_option("wrap", true, scope: :window, window: win, buffer: buf)
|
|
360
|
+
win.cursor_y = 1
|
|
361
|
+
win.cursor_x = 0
|
|
362
|
+
|
|
363
|
+
term = TerminalStub.new([6, 8]) # text_rows=4, content width ~8 (no gutter)
|
|
364
|
+
screen = RuVim::Screen.new(terminal: term)
|
|
365
|
+
screen.render(editor)
|
|
366
|
+
|
|
367
|
+
assert_equal 1, win.row_offset
|
|
368
|
+
|
|
369
|
+
rows, cols = term.winsize
|
|
370
|
+
text_rows, text_cols = editor.text_viewport_size(rows:, cols:)
|
|
371
|
+
rects = screen.send(:window_rects, editor, text_rows:, text_cols:)
|
|
372
|
+
row, _col = screen.send(:cursor_screen_position, editor, text_rows, rects)
|
|
373
|
+
assert_equal 1, row
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def test_rich_mode_horizontal_scroll_preserves_column_alignment
|
|
377
|
+
editor = RuVim::Editor.new
|
|
378
|
+
buf = editor.add_empty_buffer
|
|
379
|
+
win = editor.add_window(buffer_id: buf.id)
|
|
380
|
+
# Create rows where raw lines differ in length but formatted lines are aligned
|
|
381
|
+
buf.replace_all_lines!(["Short\tSecond\tThird", "LongerField\tB\tC"])
|
|
382
|
+
buf.options["filetype"] = "tsv"
|
|
383
|
+
RuVim::RichView.open!(editor, format: "tsv")
|
|
384
|
+
|
|
385
|
+
# Move cursor to end of line (like pressing $)
|
|
386
|
+
win.cursor_y = 0
|
|
387
|
+
win.cursor_x = buf.line_length(0)
|
|
388
|
+
|
|
389
|
+
# Use a narrow terminal so that the formatted line won't fit
|
|
390
|
+
term = TerminalStub.new([6, 20])
|
|
391
|
+
screen = RuVim::Screen.new(terminal: term)
|
|
392
|
+
screen.render(editor)
|
|
393
|
+
|
|
394
|
+
# After render, col_offset should be in formatted space.
|
|
395
|
+
# Both rendered lines should use the same col_offset, keeping separators aligned.
|
|
396
|
+
rows_key, cols_key = term.winsize
|
|
397
|
+
text_rows, text_cols = editor.text_viewport_size(rows: rows_key, cols: cols_key)
|
|
398
|
+
rects = screen.send(:window_rects, editor, text_rows:, text_cols:)
|
|
399
|
+
frame = screen.send(:build_frame, editor, rows: rows_key, cols: cols_key, text_rows:, text_cols:, rects:)
|
|
400
|
+
|
|
401
|
+
# Strip ANSI and check that "|" separators are at the same column in both rows
|
|
402
|
+
row1 = frame[:lines][1].to_s.gsub(/\e\[[0-9;?]*[A-Za-z]/, "")
|
|
403
|
+
row2 = frame[:lines][2].to_s.gsub(/\e\[[0-9;?]*[A-Za-z]/, "")
|
|
404
|
+
|
|
405
|
+
sep_positions_row1 = row1.enum_for(:scan, /\|/).map { Regexp.last_match.begin(0) }
|
|
406
|
+
sep_positions_row2 = row2.enum_for(:scan, /\|/).map { Regexp.last_match.begin(0) }
|
|
407
|
+
|
|
408
|
+
# They should have the same separator positions (column alignment preserved)
|
|
409
|
+
assert_equal sep_positions_row1, sep_positions_row2,
|
|
410
|
+
"Separators should align: row1=#{row1.inspect} row2=#{row2.inspect}"
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def test_rich_mode_cjk_horizontal_scroll_preserves_display_column_alignment
|
|
414
|
+
editor = RuVim::Editor.new
|
|
415
|
+
buf = editor.add_empty_buffer
|
|
416
|
+
win = editor.add_window(buffer_id: buf.id)
|
|
417
|
+
buf.replace_all_lines!([
|
|
418
|
+
"色\t果物名\t産地",
|
|
419
|
+
"赤\tりんご\t青森",
|
|
420
|
+
"緑\tキウイフルーツジャム\t静岡"
|
|
421
|
+
])
|
|
422
|
+
buf.options["filetype"] = "tsv"
|
|
423
|
+
RuVim::RichView.open!(editor, format: "tsv")
|
|
424
|
+
|
|
425
|
+
# Move to end of line to trigger horizontal scroll
|
|
426
|
+
win.cursor_y = 2
|
|
427
|
+
win.cursor_x = buf.line_length(2)
|
|
428
|
+
|
|
429
|
+
# Narrow terminal so formatted line doesn't fit
|
|
430
|
+
term = TerminalStub.new([6, 30])
|
|
431
|
+
screen = RuVim::Screen.new(terminal: term)
|
|
432
|
+
screen.render(editor)
|
|
433
|
+
|
|
434
|
+
rows_key, cols_key = term.winsize
|
|
435
|
+
text_rows, text_cols = editor.text_viewport_size(rows: rows_key, cols: cols_key)
|
|
436
|
+
rects = screen.send(:window_rects, editor, text_rows:, text_cols:)
|
|
437
|
+
frame = screen.send(:build_frame, editor, rows: rows_key, cols: cols_key, text_rows:, text_cols:, rects:)
|
|
438
|
+
|
|
439
|
+
# Strip ANSI, collect visible lines
|
|
440
|
+
visible_rows = (1..3).map do |r|
|
|
441
|
+
frame[:lines][r].to_s.gsub(/\e\[[0-9;?]*[A-Za-z]/, "")
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
# All rows should have separator "|" at the same display column
|
|
445
|
+
pipe_display_cols = visible_rows.map do |row|
|
|
446
|
+
col = 0
|
|
447
|
+
pipe_col = nil
|
|
448
|
+
row.each_char do |ch|
|
|
449
|
+
if ch == "|"
|
|
450
|
+
pipe_col = col
|
|
451
|
+
break
|
|
452
|
+
end
|
|
453
|
+
col += RuVim::DisplayWidth.cell_width(ch, col: col, tabstop: 2)
|
|
454
|
+
end
|
|
455
|
+
pipe_col
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
# Filter out rows where pipe might not be visible (scrolled away)
|
|
459
|
+
visible_pipes = pipe_display_cols.compact
|
|
460
|
+
assert(visible_pipes.length >= 2, "At least 2 rows should show a pipe separator")
|
|
461
|
+
assert_equal 1, visible_pipes.uniq.length,
|
|
462
|
+
"Pipe separators should align at same display column: #{pipe_display_cols.inspect} in #{visible_rows.inspect}"
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
def test_truncate_respects_display_width_for_wide_chars
|
|
466
|
+
editor = RuVim::Editor.new
|
|
467
|
+
buf = editor.add_empty_buffer
|
|
468
|
+
editor.add_window(buffer_id: buf.id)
|
|
469
|
+
term = TerminalStub.new([6, 20])
|
|
470
|
+
screen = RuVim::Screen.new(terminal: term)
|
|
471
|
+
|
|
472
|
+
# "Unknown key: あ" => 13 ASCII + 1 wide char (display width 2) = 15 display cols
|
|
473
|
+
result = screen.send(:truncate, "Unknown key: あ", 20)
|
|
474
|
+
dw = RuVim::DisplayWidth.display_width(result)
|
|
475
|
+
assert_equal 20, dw, "truncate must produce exactly 20 display columns, got #{dw}"
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
def test_error_message_with_wide_char_does_not_exceed_terminal_width
|
|
479
|
+
editor = RuVim::Editor.new
|
|
480
|
+
buf = editor.add_empty_buffer
|
|
481
|
+
editor.add_window(buffer_id: buf.id)
|
|
482
|
+
|
|
483
|
+
editor.echo_error("Unknown key: あ")
|
|
484
|
+
|
|
485
|
+
term = TerminalStub.new([6, 20])
|
|
486
|
+
screen = RuVim::Screen.new(terminal: term)
|
|
487
|
+
|
|
488
|
+
rows, cols = term.winsize
|
|
489
|
+
text_rows, text_cols = editor.text_viewport_size(rows:, cols:)
|
|
490
|
+
rects = screen.send(:window_rects, editor, text_rows:, text_cols:)
|
|
491
|
+
frame = screen.send(:build_frame, editor, rows:, cols:, text_rows:, text_cols:, rects:)
|
|
492
|
+
|
|
493
|
+
error_row = frame[:lines][text_rows + 2]
|
|
494
|
+
# Strip ANSI codes and measure display width
|
|
495
|
+
plain = error_row.to_s.gsub(/\e\[[0-9;?]*[A-Za-z]/, "")
|
|
496
|
+
dw = RuVim::DisplayWidth.display_width(plain)
|
|
497
|
+
assert_operator dw, :<=, cols, "error line display width #{dw} exceeds terminal cols #{cols}"
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
def test_rich_mode_cursor_visible_after_end_of_line
|
|
501
|
+
editor = RuVim::Editor.new
|
|
502
|
+
buf = editor.add_empty_buffer
|
|
503
|
+
win = editor.add_window(buffer_id: buf.id)
|
|
504
|
+
buf.replace_all_lines!(["A\tB\tC", "DD\tEE\tFF"])
|
|
505
|
+
buf.options["filetype"] = "tsv"
|
|
506
|
+
RuVim::RichView.open!(editor, format: "tsv")
|
|
507
|
+
|
|
508
|
+
# Move cursor to end of raw line
|
|
509
|
+
win.cursor_y = 0
|
|
510
|
+
win.cursor_x = buf.line_length(0) # last char
|
|
511
|
+
|
|
512
|
+
# Narrow terminal
|
|
513
|
+
term = TerminalStub.new([6, 15])
|
|
514
|
+
screen = RuVim::Screen.new(terminal: term)
|
|
515
|
+
screen.render(editor)
|
|
516
|
+
|
|
517
|
+
rows_key, cols_key = term.winsize
|
|
518
|
+
text_rows, text_cols = editor.text_viewport_size(rows: rows_key, cols: cols_key)
|
|
519
|
+
rects = screen.send(:window_rects, editor, text_rows:, text_cols:)
|
|
520
|
+
_cursor_row, cursor_col = screen.send(:cursor_screen_position, editor, text_rows, rects)
|
|
521
|
+
|
|
522
|
+
# Cursor should be within the visible area
|
|
523
|
+
assert_operator cursor_col, :>=, 1
|
|
524
|
+
assert_operator cursor_col, :<=, cols_key
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
def test_status_line_shows_tab_indicator_when_multiple_tabs
|
|
528
|
+
editor = RuVim::Editor.new
|
|
529
|
+
buf = editor.add_empty_buffer
|
|
530
|
+
editor.add_window(buffer_id: buf.id)
|
|
531
|
+
editor.tabnew
|
|
532
|
+
editor.tabnew
|
|
533
|
+
|
|
534
|
+
term = TerminalStub.new([6, 60])
|
|
535
|
+
screen = RuVim::Screen.new(terminal: term)
|
|
536
|
+
line = screen.send(:status_line, editor, 60)
|
|
537
|
+
|
|
538
|
+
assert_includes line, "tab:3/3"
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
def test_status_line_hides_tab_indicator_when_single_tab
|
|
542
|
+
editor = RuVim::Editor.new
|
|
543
|
+
buf = editor.add_empty_buffer
|
|
544
|
+
editor.add_window(buffer_id: buf.id)
|
|
545
|
+
|
|
546
|
+
term = TerminalStub.new([6, 60])
|
|
547
|
+
screen = RuVim::Screen.new(terminal: term)
|
|
548
|
+
line = screen.send(:status_line, editor, 60)
|
|
549
|
+
|
|
550
|
+
refute_includes line, "tab:"
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
def test_window_rects_nested_vsplit_hsplit
|
|
554
|
+
editor = RuVim::Editor.new
|
|
555
|
+
buf = editor.add_empty_buffer
|
|
556
|
+
# win1
|
|
557
|
+
editor.add_window(buffer_id: buf.id)
|
|
558
|
+
win1 = editor.current_window
|
|
559
|
+
# vsplit → win2
|
|
560
|
+
editor.split_current_window(layout: :vertical)
|
|
561
|
+
win2 = editor.current_window
|
|
562
|
+
# split win2 → win3
|
|
563
|
+
editor.split_current_window(layout: :horizontal)
|
|
564
|
+
win3 = editor.current_window
|
|
565
|
+
|
|
566
|
+
term = TerminalStub.new([22, 80])
|
|
567
|
+
screen = RuVim::Screen.new(terminal: term)
|
|
568
|
+
rows, cols = term.winsize
|
|
569
|
+
text_rows, text_cols = editor.text_viewport_size(rows:, cols:)
|
|
570
|
+
rects = screen.send(:window_rects, editor, text_rows:, text_cols:)
|
|
571
|
+
|
|
572
|
+
# All three windows should have rects
|
|
573
|
+
assert rects.key?(win1.id), "win1 should have a rect"
|
|
574
|
+
assert rects.key?(win2.id), "win2 should have a rect"
|
|
575
|
+
assert rects.key?(win3.id), "win3 should have a rect"
|
|
576
|
+
|
|
577
|
+
# win1 should be on the left, win2/win3 should be on the right
|
|
578
|
+
assert_operator rects[win1.id][:left], :<, rects[win2.id][:left]
|
|
579
|
+
assert_operator rects[win1.id][:left], :<, rects[win3.id][:left]
|
|
580
|
+
|
|
581
|
+
# win2 and win3 should share the same left position (same column)
|
|
582
|
+
assert_equal rects[win2.id][:left], rects[win3.id][:left]
|
|
583
|
+
|
|
584
|
+
# win2 should be above win3
|
|
585
|
+
assert_operator rects[win2.id][:top], :<, rects[win3.id][:top]
|
|
586
|
+
|
|
587
|
+
# win1 should span the full height
|
|
588
|
+
assert_equal rects[win1.id][:height], text_rows
|
|
589
|
+
|
|
590
|
+
# win2 + win3 heights + separator should equal text_rows
|
|
591
|
+
assert_equal text_rows, rects[win2.id][:height] + rects[win3.id][:height] + 1
|
|
592
|
+
end
|
|
123
593
|
end
|
data/test/window_test.rb
CHANGED
|
@@ -18,4 +18,30 @@ class WindowTest < Minitest::Test
|
|
|
18
18
|
assert_operator cursor_col, :<, offset_col + 4
|
|
19
19
|
assert_equal 3, win.col_offset
|
|
20
20
|
end
|
|
21
|
+
|
|
22
|
+
def test_ensure_visible_respects_scrolloff_and_sidescrolloff
|
|
23
|
+
buffer = RuVim::Buffer.new(id: 1, lines: ["0123456789", "aaaaa", "bbbbb", "ccccc", "0123456789", "eeeee"])
|
|
24
|
+
win = RuVim::Window.new(id: 1, buffer_id: 1)
|
|
25
|
+
win.cursor_y = 4
|
|
26
|
+
win.cursor_x = 8
|
|
27
|
+
|
|
28
|
+
win.ensure_visible(buffer, height: 3, width: 5, tabstop: 2, scrolloff: 1, sidescrolloff: 1)
|
|
29
|
+
|
|
30
|
+
assert_equal 3, win.row_offset
|
|
31
|
+
assert_operator win.col_offset, :>, 0
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def test_move_down_preserves_preferred_column_across_empty_line
|
|
35
|
+
long = "x" * 80
|
|
36
|
+
buffer = RuVim::Buffer.new(id: 1, lines: [long, "", long])
|
|
37
|
+
win = RuVim::Window.new(id: 1, buffer_id: 1)
|
|
38
|
+
win.cursor_y = 0
|
|
39
|
+
win.cursor_x = 50
|
|
40
|
+
|
|
41
|
+
win.move_down(buffer)
|
|
42
|
+
assert_equal [1, 0], [win.cursor_y, win.cursor_x]
|
|
43
|
+
|
|
44
|
+
win.move_down(buffer)
|
|
45
|
+
assert_equal [2, 50], [win.cursor_y, win.cursor_x]
|
|
46
|
+
end
|
|
21
47
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,28 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ruvim
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Koichi Sasada
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
-
dependencies:
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: minitest
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
12
26
|
description: vim clone on Ruby by codex
|
|
13
27
|
email:
|
|
14
28
|
- ko1@atdot.net
|
|
@@ -18,12 +32,15 @@ extensions: []
|
|
|
18
32
|
extra_rdoc_files: []
|
|
19
33
|
files:
|
|
20
34
|
- ".github/workflows/test.yml"
|
|
35
|
+
- AGENTS.md
|
|
36
|
+
- CLAUDE.md
|
|
21
37
|
- README.md
|
|
22
38
|
- Rakefile
|
|
23
39
|
- docs/binding.md
|
|
24
40
|
- docs/command.md
|
|
25
41
|
- docs/config.md
|
|
26
42
|
- docs/done.md
|
|
43
|
+
- docs/lib_cleanup_report.md
|
|
27
44
|
- docs/plugin.md
|
|
28
45
|
- docs/spec.md
|
|
29
46
|
- docs/todo.md
|
|
@@ -49,6 +66,17 @@ files:
|
|
|
49
66
|
- lib/ruvim/highlighter.rb
|
|
50
67
|
- lib/ruvim/input.rb
|
|
51
68
|
- lib/ruvim/keymap_manager.rb
|
|
69
|
+
- lib/ruvim/keyword_chars.rb
|
|
70
|
+
- lib/ruvim/lang/base.rb
|
|
71
|
+
- lib/ruvim/lang/csv.rb
|
|
72
|
+
- lib/ruvim/lang/json.rb
|
|
73
|
+
- lib/ruvim/lang/markdown.rb
|
|
74
|
+
- lib/ruvim/lang/ruby.rb
|
|
75
|
+
- lib/ruvim/lang/scheme.rb
|
|
76
|
+
- lib/ruvim/lang/tsv.rb
|
|
77
|
+
- lib/ruvim/rich_view.rb
|
|
78
|
+
- lib/ruvim/rich_view/markdown_renderer.rb
|
|
79
|
+
- lib/ruvim/rich_view/table_renderer.rb
|
|
52
80
|
- lib/ruvim/screen.rb
|
|
53
81
|
- lib/ruvim/terminal.rb
|
|
54
82
|
- lib/ruvim/text_metrics.rb
|
|
@@ -63,19 +91,26 @@ files:
|
|
|
63
91
|
- test/app_startup_test.rb
|
|
64
92
|
- test/app_text_object_test.rb
|
|
65
93
|
- test/app_unicode_behavior_test.rb
|
|
94
|
+
- test/arglist_test.rb
|
|
66
95
|
- test/buffer_test.rb
|
|
67
96
|
- test/cli_test.rb
|
|
68
97
|
- test/config_dsl_test.rb
|
|
98
|
+
- test/config_loader_test.rb
|
|
69
99
|
- test/dispatcher_test.rb
|
|
100
|
+
- test/display_width_test.rb
|
|
70
101
|
- test/editor_mark_test.rb
|
|
71
102
|
- test/editor_register_test.rb
|
|
72
103
|
- test/fixtures/render_basic_snapshot.txt
|
|
73
104
|
- test/fixtures/render_basic_snapshot_nonumber.txt
|
|
74
105
|
- test/fixtures/render_unicode_scrolled_snapshot.txt
|
|
75
106
|
- test/highlighter_test.rb
|
|
107
|
+
- test/indent_test.rb
|
|
76
108
|
- test/input_screen_integration_test.rb
|
|
77
109
|
- test/keymap_manager_test.rb
|
|
110
|
+
- test/markdown_renderer_test.rb
|
|
111
|
+
- test/on_save_hook_test.rb
|
|
78
112
|
- test/render_snapshot_test.rb
|
|
113
|
+
- test/rich_view_test.rb
|
|
79
114
|
- test/screen_test.rb
|
|
80
115
|
- test/search_option_test.rb
|
|
81
116
|
- test/test_helper.rb
|