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
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
require_relative "test_helper"
|
|
2
|
+
require "tmpdir"
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
class OnSaveHookTest < Minitest::Test
|
|
6
|
+
def test_base_on_save_is_noop
|
|
7
|
+
editor = fresh_editor
|
|
8
|
+
ctx = RuVim::Context.new(editor: editor)
|
|
9
|
+
# Should not raise
|
|
10
|
+
RuVim::Lang::Base.on_save(ctx, "/tmp/nonexistent.txt")
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def test_ruby_on_save_with_valid_file
|
|
14
|
+
editor = fresh_editor
|
|
15
|
+
ctx = RuVim::Context.new(editor: editor)
|
|
16
|
+
Dir.mktmpdir do |dir|
|
|
17
|
+
path = File.join(dir, "valid.rb")
|
|
18
|
+
File.write(path, "puts 'hello'\n")
|
|
19
|
+
RuVim::Lang::Ruby.on_save(ctx, path)
|
|
20
|
+
# No error message should be set (echo from before should remain)
|
|
21
|
+
refute editor.message_error?
|
|
22
|
+
assert_empty editor.quickfix_items, "quickfix list should be empty for valid file"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def test_ruby_on_save_with_syntax_error
|
|
27
|
+
editor = fresh_editor
|
|
28
|
+
ctx = RuVim::Context.new(editor: editor)
|
|
29
|
+
Dir.mktmpdir do |dir|
|
|
30
|
+
path = File.join(dir, "bad.rb")
|
|
31
|
+
File.write(path, "def foo(\n")
|
|
32
|
+
RuVim::Lang::Ruby.on_save(ctx, path)
|
|
33
|
+
assert editor.message_error?
|
|
34
|
+
assert_match(/syntax error/i, editor.message)
|
|
35
|
+
refute_empty editor.quickfix_items, "quickfix list should be populated on syntax error"
|
|
36
|
+
item = editor.quickfix_items.first
|
|
37
|
+
assert_kind_of Integer, item[:row]
|
|
38
|
+
assert_match(/syntax error/i, item[:text])
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def test_ruby_on_save_with_nil_path
|
|
43
|
+
editor = fresh_editor
|
|
44
|
+
ctx = RuVim::Context.new(editor: editor)
|
|
45
|
+
# Should not raise
|
|
46
|
+
RuVim::Lang::Ruby.on_save(ctx, nil)
|
|
47
|
+
refute editor.message_error?
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def test_file_write_calls_on_save
|
|
51
|
+
app = RuVim::App.new(clean: true)
|
|
52
|
+
editor = app.instance_variable_get(:@editor)
|
|
53
|
+
editor.materialize_intro_buffer!
|
|
54
|
+
|
|
55
|
+
called = false
|
|
56
|
+
hook_module = Module.new do
|
|
57
|
+
define_method(:on_save) do |_ctx, _path|
|
|
58
|
+
called = true
|
|
59
|
+
end
|
|
60
|
+
module_function :on_save
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
Dir.mktmpdir do |dir|
|
|
64
|
+
path = File.join(dir, "test.rb")
|
|
65
|
+
editor.current_buffer.instance_variable_set(:@lang_module, hook_module)
|
|
66
|
+
editor.current_buffer.replace_all_lines!(["hello"])
|
|
67
|
+
|
|
68
|
+
# Execute :w command
|
|
69
|
+
keys = ":w #{path}\n".chars
|
|
70
|
+
keys.each { |k| app.send(:handle_key, k == "\n" ? :enter : k) }
|
|
71
|
+
|
|
72
|
+
assert called, "on_save hook should have been called"
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def test_write_then_bracket_q_navigates_quickfix
|
|
77
|
+
app = RuVim::App.new(clean: true)
|
|
78
|
+
editor = app.instance_variable_get(:@editor)
|
|
79
|
+
editor.materialize_intro_buffer!
|
|
80
|
+
|
|
81
|
+
Dir.mktmpdir do |dir|
|
|
82
|
+
path = File.join(dir, "bad.rb")
|
|
83
|
+
# Two lines so error is on line 2 — verifiable jump target
|
|
84
|
+
File.write(path, "x = 1\ndef foo(\n")
|
|
85
|
+
|
|
86
|
+
# Open and write the file
|
|
87
|
+
":e #{path}\n".chars.each { |k| app.send(:handle_key, k == "\n" ? :enter : k) }
|
|
88
|
+
":w\n".chars.each { |k| app.send(:handle_key, k == "\n" ? :enter : k) }
|
|
89
|
+
|
|
90
|
+
refute_empty editor.quickfix_items, "quickfix should be populated after :w with syntax error"
|
|
91
|
+
assert_nil editor.quickfix_index, "quickfix index should be nil before navigation"
|
|
92
|
+
|
|
93
|
+
# Press ]q — should jump to first item (index 0)
|
|
94
|
+
app.send(:handle_key, "]")
|
|
95
|
+
app.send(:handle_key, "q")
|
|
96
|
+
|
|
97
|
+
assert_equal 0, editor.quickfix_index
|
|
98
|
+
first_item = editor.quickfix_items.first
|
|
99
|
+
assert_equal first_item[:row], editor.current_window.cursor_y,
|
|
100
|
+
"cursor should jump to first quickfix item on ]q"
|
|
101
|
+
|
|
102
|
+
assert_match(/qf/, editor.message)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def test_write_valid_clears_quickfix
|
|
107
|
+
app = RuVim::App.new(clean: true)
|
|
108
|
+
editor = app.instance_variable_get(:@editor)
|
|
109
|
+
editor.materialize_intro_buffer!
|
|
110
|
+
|
|
111
|
+
Dir.mktmpdir do |dir|
|
|
112
|
+
path = File.join(dir, "ok.rb")
|
|
113
|
+
File.write(path, "puts 'hi'\n")
|
|
114
|
+
|
|
115
|
+
":e #{path}\n".chars.each { |k| app.send(:handle_key, k == "\n" ? :enter : k) }
|
|
116
|
+
# Set some dummy quickfix items first
|
|
117
|
+
editor.set_quickfix_list([{ buffer_id: editor.current_buffer.id, row: 0, col: 0, text: "dummy" }])
|
|
118
|
+
refute_empty editor.quickfix_items
|
|
119
|
+
|
|
120
|
+
":w\n".chars.each { |k| app.send(:handle_key, k == "\n" ? :enter : k) }
|
|
121
|
+
assert_empty editor.quickfix_items, "quickfix should be cleared after :w with valid file"
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def test_file_write_skips_on_save_when_onsavehook_disabled
|
|
126
|
+
app = RuVim::App.new(clean: true)
|
|
127
|
+
editor = app.instance_variable_get(:@editor)
|
|
128
|
+
editor.materialize_intro_buffer!
|
|
129
|
+
|
|
130
|
+
called = false
|
|
131
|
+
hook_module = Module.new do
|
|
132
|
+
define_method(:on_save) do |_ctx, _path|
|
|
133
|
+
called = true
|
|
134
|
+
end
|
|
135
|
+
module_function :on_save
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
Dir.mktmpdir do |dir|
|
|
139
|
+
path = File.join(dir, "test.rb")
|
|
140
|
+
editor.current_buffer.instance_variable_set(:@lang_module, hook_module)
|
|
141
|
+
editor.current_buffer.replace_all_lines!(["hello"])
|
|
142
|
+
editor.set_option("onsavehook", false)
|
|
143
|
+
|
|
144
|
+
keys = ":w #{path}\n".chars
|
|
145
|
+
keys.each { |k| app.send(:handle_key, k == "\n" ? :enter : k) }
|
|
146
|
+
|
|
147
|
+
refute called, "on_save hook should NOT have been called when onsavehook is disabled"
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
require_relative "test_helper"
|
|
2
|
+
|
|
3
|
+
class RichViewTest < Minitest::Test
|
|
4
|
+
# --- Framework tests ---
|
|
5
|
+
|
|
6
|
+
def test_register_and_renderer_for
|
|
7
|
+
assert RuVim::RichView.renderer_for("tsv")
|
|
8
|
+
assert RuVim::RichView.renderer_for("csv")
|
|
9
|
+
assert_nil RuVim::RichView.renderer_for("unknown")
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def test_registered_filetypes
|
|
13
|
+
fts = RuVim::RichView.registered_filetypes
|
|
14
|
+
assert_includes fts, "tsv"
|
|
15
|
+
assert_includes fts, "csv"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def test_detect_format_from_filetype
|
|
19
|
+
editor = fresh_editor
|
|
20
|
+
buf = editor.current_buffer
|
|
21
|
+
buf.options["filetype"] = "csv"
|
|
22
|
+
assert_equal "csv", RuVim::RichView.detect_format(buf)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def test_detect_format_auto_tsv
|
|
26
|
+
editor = fresh_editor
|
|
27
|
+
buf = editor.current_buffer
|
|
28
|
+
buf.replace_all_lines!(["a\tb\tc", "d\te\tf"])
|
|
29
|
+
assert_equal "tsv", RuVim::RichView.detect_format(buf)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def test_detect_format_auto_csv
|
|
33
|
+
editor = fresh_editor
|
|
34
|
+
buf = editor.current_buffer
|
|
35
|
+
buf.replace_all_lines!(["a,b,c", "d,e,f"])
|
|
36
|
+
assert_equal "csv", RuVim::RichView.detect_format(buf)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def test_detect_format_returns_nil_for_plain_text
|
|
40
|
+
editor = fresh_editor
|
|
41
|
+
buf = editor.current_buffer
|
|
42
|
+
buf.replace_all_lines!(["hello world", "foo bar"])
|
|
43
|
+
assert_nil RuVim::RichView.detect_format(buf)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def test_open_raises_when_format_unknown
|
|
47
|
+
editor = fresh_editor
|
|
48
|
+
buf = editor.current_buffer
|
|
49
|
+
buf.replace_all_lines!(["hello world"])
|
|
50
|
+
assert_raises(RuVim::CommandError) { RuVim::RichView.open!(editor) }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# --- Mode transition tests ---
|
|
54
|
+
|
|
55
|
+
def test_active_returns_false_for_normal_mode
|
|
56
|
+
editor = fresh_editor
|
|
57
|
+
refute RuVim::RichView.active?(editor)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def test_open_enters_rich_mode
|
|
61
|
+
editor = fresh_editor
|
|
62
|
+
buf = editor.current_buffer
|
|
63
|
+
buf.replace_all_lines!(["a\tb", "c\td"])
|
|
64
|
+
buf.options["filetype"] = "tsv"
|
|
65
|
+
|
|
66
|
+
RuVim::RichView.open!(editor, format: "tsv")
|
|
67
|
+
assert_equal :rich, editor.mode
|
|
68
|
+
assert RuVim::RichView.active?(editor)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def test_open_stays_on_same_buffer
|
|
72
|
+
editor = fresh_editor
|
|
73
|
+
buf = editor.current_buffer
|
|
74
|
+
buf.replace_all_lines!(["a\tb", "c\td"])
|
|
75
|
+
buf.options["filetype"] = "tsv"
|
|
76
|
+
original_id = buf.id
|
|
77
|
+
|
|
78
|
+
RuVim::RichView.open!(editor, format: "tsv")
|
|
79
|
+
assert_equal original_id, editor.current_buffer.id
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def test_close_returns_to_normal_mode
|
|
83
|
+
editor = fresh_editor
|
|
84
|
+
buf = editor.current_buffer
|
|
85
|
+
buf.replace_all_lines!(["x\ty"])
|
|
86
|
+
buf.options["filetype"] = "tsv"
|
|
87
|
+
|
|
88
|
+
RuVim::RichView.open!(editor, format: "tsv")
|
|
89
|
+
assert_equal :rich, editor.mode
|
|
90
|
+
|
|
91
|
+
RuVim::RichView.close!(editor)
|
|
92
|
+
assert_equal :normal, editor.mode
|
|
93
|
+
refute RuVim::RichView.active?(editor)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def test_close_keeps_same_buffer
|
|
97
|
+
editor = fresh_editor
|
|
98
|
+
buf = editor.current_buffer
|
|
99
|
+
buf.replace_all_lines!(["x\ty"])
|
|
100
|
+
buf.options["filetype"] = "tsv"
|
|
101
|
+
original_id = buf.id
|
|
102
|
+
|
|
103
|
+
RuVim::RichView.open!(editor, format: "tsv")
|
|
104
|
+
RuVim::RichView.close!(editor)
|
|
105
|
+
assert_equal original_id, editor.current_buffer.id
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def test_toggle_opens_and_closes
|
|
109
|
+
editor = fresh_editor
|
|
110
|
+
buf = editor.current_buffer
|
|
111
|
+
buf.replace_all_lines!(["a\tb"])
|
|
112
|
+
buf.options["filetype"] = "tsv"
|
|
113
|
+
|
|
114
|
+
RuVim::RichView.toggle!(editor)
|
|
115
|
+
assert RuVim::RichView.active?(editor)
|
|
116
|
+
|
|
117
|
+
RuVim::RichView.toggle!(editor)
|
|
118
|
+
refute RuVim::RichView.active?(editor)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def test_rich_state_stores_format_and_delimiter
|
|
122
|
+
editor = fresh_editor
|
|
123
|
+
buf = editor.current_buffer
|
|
124
|
+
buf.replace_all_lines!(["a\tb"])
|
|
125
|
+
buf.options["filetype"] = "tsv"
|
|
126
|
+
|
|
127
|
+
RuVim::RichView.open!(editor, format: "tsv")
|
|
128
|
+
state = editor.rich_state
|
|
129
|
+
assert_equal "tsv", state[:format]
|
|
130
|
+
assert_equal "\t", state[:delimiter]
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def test_rich_state_csv
|
|
134
|
+
editor = fresh_editor
|
|
135
|
+
buf = editor.current_buffer
|
|
136
|
+
buf.replace_all_lines!(["a,b"])
|
|
137
|
+
buf.options["filetype"] = "csv"
|
|
138
|
+
|
|
139
|
+
RuVim::RichView.open!(editor, format: "csv")
|
|
140
|
+
state = editor.rich_state
|
|
141
|
+
assert_equal "csv", state[:format]
|
|
142
|
+
assert_equal ",", state[:delimiter]
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def test_rich_state_nil_after_close
|
|
146
|
+
editor = fresh_editor
|
|
147
|
+
buf = editor.current_buffer
|
|
148
|
+
buf.replace_all_lines!(["a\tb"])
|
|
149
|
+
buf.options["filetype"] = "tsv"
|
|
150
|
+
|
|
151
|
+
RuVim::RichView.open!(editor, format: "tsv")
|
|
152
|
+
RuVim::RichView.close!(editor)
|
|
153
|
+
assert_nil editor.rich_state
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def test_active_during_command_line_from_rich_mode
|
|
157
|
+
editor = fresh_editor
|
|
158
|
+
buf = editor.current_buffer
|
|
159
|
+
buf.replace_all_lines!(["a\tb"])
|
|
160
|
+
buf.options["filetype"] = "tsv"
|
|
161
|
+
|
|
162
|
+
RuVim::RichView.open!(editor, format: "tsv")
|
|
163
|
+
editor.enter_command_line_mode(":")
|
|
164
|
+
assert_equal :command_line, editor.mode
|
|
165
|
+
assert RuVim::RichView.active?(editor)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def test_cancel_command_line_returns_to_rich_mode
|
|
169
|
+
editor = fresh_editor
|
|
170
|
+
buf = editor.current_buffer
|
|
171
|
+
buf.replace_all_lines!(["a\tb"])
|
|
172
|
+
buf.options["filetype"] = "tsv"
|
|
173
|
+
|
|
174
|
+
RuVim::RichView.open!(editor, format: "tsv")
|
|
175
|
+
assert_equal :rich, editor.mode
|
|
176
|
+
|
|
177
|
+
editor.enter_command_line_mode(":")
|
|
178
|
+
assert_equal :command_line, editor.mode
|
|
179
|
+
assert editor.rich_state
|
|
180
|
+
|
|
181
|
+
editor.cancel_command_line
|
|
182
|
+
assert_equal :rich, editor.mode
|
|
183
|
+
assert editor.rich_state
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def test_leave_command_line_returns_to_rich_mode
|
|
187
|
+
editor = fresh_editor
|
|
188
|
+
buf = editor.current_buffer
|
|
189
|
+
buf.replace_all_lines!(["a\tb"])
|
|
190
|
+
buf.options["filetype"] = "tsv"
|
|
191
|
+
|
|
192
|
+
RuVim::RichView.open!(editor, format: "tsv")
|
|
193
|
+
editor.enter_command_line_mode(":")
|
|
194
|
+
editor.leave_command_line
|
|
195
|
+
assert_equal :rich, editor.mode
|
|
196
|
+
assert editor.rich_state
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def test_leave_command_line_returns_to_normal_without_rich_state
|
|
200
|
+
editor = fresh_editor
|
|
201
|
+
editor.enter_command_line_mode(":")
|
|
202
|
+
editor.leave_command_line
|
|
203
|
+
assert_equal :normal, editor.mode
|
|
204
|
+
assert_nil editor.rich_state
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def test_enter_normal_mode_clears_rich_state
|
|
208
|
+
editor = fresh_editor
|
|
209
|
+
buf = editor.current_buffer
|
|
210
|
+
buf.replace_all_lines!(["a\tb"])
|
|
211
|
+
buf.options["filetype"] = "tsv"
|
|
212
|
+
|
|
213
|
+
RuVim::RichView.open!(editor, format: "tsv")
|
|
214
|
+
editor.enter_normal_mode
|
|
215
|
+
assert_nil editor.rich_state
|
|
216
|
+
assert_equal :normal, editor.mode
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def test_render_visible_lines_integration
|
|
220
|
+
editor = fresh_editor
|
|
221
|
+
buf = editor.current_buffer
|
|
222
|
+
buf.replace_all_lines!(["a\tbb", "ccc\td"])
|
|
223
|
+
buf.options["filetype"] = "tsv"
|
|
224
|
+
|
|
225
|
+
RuVim::RichView.open!(editor, format: "tsv")
|
|
226
|
+
lines = [buf.line_at(0), buf.line_at(1)]
|
|
227
|
+
rendered = RuVim::RichView.render_visible_lines(editor, lines)
|
|
228
|
+
assert_equal 2, rendered.length
|
|
229
|
+
assert_includes rendered[0], " | "
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def test_buffer_count_unchanged_after_open
|
|
233
|
+
editor = fresh_editor
|
|
234
|
+
buf = editor.current_buffer
|
|
235
|
+
buf.replace_all_lines!(["a\tb"])
|
|
236
|
+
buf.options["filetype"] = "tsv"
|
|
237
|
+
count_before = editor.buffers.length
|
|
238
|
+
|
|
239
|
+
RuVim::RichView.open!(editor, format: "tsv")
|
|
240
|
+
assert_equal count_before, editor.buffers.length
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# --- TableRenderer tests ---
|
|
244
|
+
|
|
245
|
+
def test_basic_alignment
|
|
246
|
+
lines = ["a\tbb\tccc", "dddd\te\tf"]
|
|
247
|
+
result = RuVim::RichView::TableRenderer.render_visible(lines, delimiter: "\t")
|
|
248
|
+
assert_equal 2, result.length
|
|
249
|
+
# Both lines should have same total display width
|
|
250
|
+
w0 = RuVim::DisplayWidth.display_width(result[0])
|
|
251
|
+
w1 = RuVim::DisplayWidth.display_width(result[1])
|
|
252
|
+
assert_equal w0, w1
|
|
253
|
+
# Check separator presence
|
|
254
|
+
assert_includes result[0], " | "
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def test_uneven_column_count
|
|
258
|
+
lines = ["a\tb", "c\td\te"]
|
|
259
|
+
result = RuVim::RichView::TableRenderer.render_visible(lines, delimiter: "\t")
|
|
260
|
+
assert_equal 2, result.length
|
|
261
|
+
# First row should have 3 columns padded (missing column filled)
|
|
262
|
+
parts0 = result[0].split(" | ")
|
|
263
|
+
parts1 = result[1].split(" | ")
|
|
264
|
+
assert_equal 3, parts0.length
|
|
265
|
+
assert_equal 3, parts1.length
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def test_empty_cells
|
|
269
|
+
lines = ["a\t\tc", "\tb\t"]
|
|
270
|
+
result = RuVim::RichView::TableRenderer.render_visible(lines, delimiter: "\t")
|
|
271
|
+
assert_equal 2, result.length
|
|
272
|
+
assert_includes result[0], " | "
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def test_single_column_passthrough
|
|
276
|
+
lines = ["abc", "def", "ghi"]
|
|
277
|
+
result = RuVim::RichView::TableRenderer.render_visible(lines, delimiter: "\t")
|
|
278
|
+
assert_equal lines, result
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def test_cjk_characters_alignment
|
|
282
|
+
lines = ["名前\t年齢", "太郎\t25", "Alice\t30"]
|
|
283
|
+
result = RuVim::RichView::TableRenderer.render_visible(lines, delimiter: "\t")
|
|
284
|
+
assert_equal 3, result.length
|
|
285
|
+
# All rows should have the same display width
|
|
286
|
+
widths = result.map { |r| RuVim::DisplayWidth.display_width(r) }
|
|
287
|
+
assert_equal 1, widths.uniq.length, "All rows should have same display width: #{widths.inspect}"
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def test_csv_basic
|
|
291
|
+
lines = ["a,b,c", "dd,e,ff"]
|
|
292
|
+
result = RuVim::RichView::TableRenderer.render_visible(lines, delimiter: ",")
|
|
293
|
+
assert_equal 2, result.length
|
|
294
|
+
assert_includes result[0], " | "
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def test_csv_quoted_fields
|
|
298
|
+
lines = ['"hello, world",b,c', 'a,"say ""hi""",d']
|
|
299
|
+
result = RuVim::RichView::TableRenderer.render_visible(lines, delimiter: ",")
|
|
300
|
+
assert_equal 2, result.length
|
|
301
|
+
# First row first field should be unquoted
|
|
302
|
+
first_field = result[0].split(" | ").first.strip
|
|
303
|
+
assert_equal "hello, world", first_field
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def test_csv_quoted_field_with_escaped_quotes
|
|
307
|
+
fields = RuVim::RichView::TableRenderer.parse_csv_fields('"say ""hi""",b')
|
|
308
|
+
assert_equal ['say "hi"', "b"], fields
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def test_empty_lines
|
|
312
|
+
result = RuVim::RichView::TableRenderer.render_visible([], delimiter: "\t")
|
|
313
|
+
assert_equal [], result
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# --- TableRenderer helper method tests ---
|
|
317
|
+
|
|
318
|
+
def test_compute_col_widths_basic
|
|
319
|
+
lines = ["a\tbb\tccc", "dddd\te\tf"]
|
|
320
|
+
widths = RuVim::RichView::TableRenderer.compute_col_widths(lines, delimiter: "\t")
|
|
321
|
+
assert_equal [4, 2, 3], widths
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def test_compute_col_widths_single_column
|
|
325
|
+
lines = ["abc", "def"]
|
|
326
|
+
assert_nil RuVim::RichView::TableRenderer.compute_col_widths(lines, delimiter: "\t")
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def test_compute_col_widths_empty
|
|
330
|
+
assert_nil RuVim::RichView::TableRenderer.compute_col_widths([], delimiter: "\t")
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def test_compute_col_widths_cjk
|
|
334
|
+
lines = ["名前\t年齢", "Alice\t30"]
|
|
335
|
+
widths = RuVim::RichView::TableRenderer.compute_col_widths(lines, delimiter: "\t")
|
|
336
|
+
# 名前 = 4 display cols, Alice = 5 → max = 5
|
|
337
|
+
# 年齢 = 4 display cols, 30 = 2 → max = 4
|
|
338
|
+
assert_equal [5, 4], widths
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def test_format_line_basic
|
|
342
|
+
col_widths = [4, 2, 3]
|
|
343
|
+
result = RuVim::RichView::TableRenderer.format_line("a\tbb\tccc", delimiter: "\t", col_widths: col_widths)
|
|
344
|
+
assert_equal "a | bb | ccc", result
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def test_format_line_consistency_with_render_visible
|
|
348
|
+
lines = ["a\tbb\tccc", "dddd\te\tf"]
|
|
349
|
+
rendered = RuVim::RichView::TableRenderer.render_visible(lines, delimiter: "\t")
|
|
350
|
+
col_widths = RuVim::RichView::TableRenderer.compute_col_widths(lines, delimiter: "\t")
|
|
351
|
+
lines.each_with_index do |line, i|
|
|
352
|
+
formatted = RuVim::RichView::TableRenderer.format_line(line, delimiter: "\t", col_widths: col_widths)
|
|
353
|
+
assert_equal rendered[i], formatted, "format_line should match render_visible for line #{i}"
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def test_raw_to_formatted_char_index_first_field
|
|
358
|
+
# Raw: "Hello\tWorld\tFoo"
|
|
359
|
+
col_widths = [10, 10, 5]
|
|
360
|
+
r = RuVim::RichView::TableRenderer
|
|
361
|
+
# 'H' at raw 0 → formatted 0
|
|
362
|
+
assert_equal 0, r.raw_to_formatted_char_index("Hello\tWorld\tFoo", 0, delimiter: "\t", col_widths: col_widths)
|
|
363
|
+
# 'o' at raw 4 → formatted 4
|
|
364
|
+
assert_equal 4, r.raw_to_formatted_char_index("Hello\tWorld\tFoo", 4, delimiter: "\t", col_widths: col_widths)
|
|
365
|
+
# End of first field at raw 5 → formatted 5 (just past 'o', still in padded area)
|
|
366
|
+
assert_equal 5, r.raw_to_formatted_char_index("Hello\tWorld\tFoo", 5, delimiter: "\t", col_widths: col_widths)
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def test_raw_to_formatted_char_index_second_field
|
|
370
|
+
col_widths = [10, 10, 5]
|
|
371
|
+
r = RuVim::RichView::TableRenderer
|
|
372
|
+
# 'W' at raw 6 → formatted 10 (col_widths[0]) + 3 (separator) = 13
|
|
373
|
+
assert_equal 13, r.raw_to_formatted_char_index("Hello\tWorld\tFoo", 6, delimiter: "\t", col_widths: col_widths)
|
|
374
|
+
# 'd' at raw 10 → formatted 13 + 4 = 17
|
|
375
|
+
assert_equal 17, r.raw_to_formatted_char_index("Hello\tWorld\tFoo", 10, delimiter: "\t", col_widths: col_widths)
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def test_raw_to_formatted_char_index_third_field
|
|
379
|
+
col_widths = [10, 10, 5]
|
|
380
|
+
r = RuVim::RichView::TableRenderer
|
|
381
|
+
# 'F' at raw 12 → formatted 10 + 3 + 10 + 3 = 26
|
|
382
|
+
assert_equal 26, r.raw_to_formatted_char_index("Hello\tWorld\tFoo", 12, delimiter: "\t", col_widths: col_widths)
|
|
383
|
+
# Last 'o' at raw 14 → formatted 26 + 2 = 28
|
|
384
|
+
assert_equal 28, r.raw_to_formatted_char_index("Hello\tWorld\tFoo", 14, delimiter: "\t", col_widths: col_widths)
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def test_raw_to_formatted_alignment_across_rows
|
|
388
|
+
# When different rows map col_offset to different fields, the formatted
|
|
389
|
+
# positions should still be aligned (same column structure).
|
|
390
|
+
lines = ["Short\tField\tEnd", "LongerField\tF\tEnd"]
|
|
391
|
+
col_widths = RuVim::RichView::TableRenderer.compute_col_widths(lines, delimiter: "\t")
|
|
392
|
+
r = RuVim::RichView::TableRenderer
|
|
393
|
+
|
|
394
|
+
# Format both lines and verify separator positions match
|
|
395
|
+
f0 = r.format_line(lines[0], delimiter: "\t", col_widths: col_widths)
|
|
396
|
+
f1 = r.format_line(lines[1], delimiter: "\t", col_widths: col_widths)
|
|
397
|
+
assert_equal RuVim::DisplayWidth.display_width(f0), RuVim::DisplayWidth.display_width(f1)
|
|
398
|
+
|
|
399
|
+
# Map cursor at "End" field start for both lines — should give same formatted position
|
|
400
|
+
# Line 0: "Short\tField\tEnd" → raw 12 is 'E' in End
|
|
401
|
+
# Line 1: "LongerField\tF\tEnd" → raw 14 is 'E' in End
|
|
402
|
+
fi0 = r.raw_to_formatted_char_index(lines[0], 12, delimiter: "\t", col_widths: col_widths)
|
|
403
|
+
fi1 = r.raw_to_formatted_char_index(lines[1], 14, delimiter: "\t", col_widths: col_widths)
|
|
404
|
+
assert_equal fi0, fi1, "Same column start should map to same formatted position"
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def test_raw_to_formatted_char_index_cjk
|
|
408
|
+
# CJK fields: "太郎" is 2 chars but 4 display cols
|
|
409
|
+
col_widths = [5, 4]
|
|
410
|
+
r = RuVim::RichView::TableRenderer
|
|
411
|
+
# "太郎\t30" → formatted: "太郎 " (2+1 pad) + " | " (3) + "30 " (2+2 pad)
|
|
412
|
+
# Character counts: field "太郎"(2) + pad(1) + separator(3) = 6
|
|
413
|
+
# So "3" at raw 3 → formatted 6
|
|
414
|
+
assert_equal 6, r.raw_to_formatted_char_index("太郎\t30", 3, delimiter: "\t", col_widths: col_widths)
|
|
415
|
+
# "0" at raw 4 → formatted 7
|
|
416
|
+
assert_equal 7, r.raw_to_formatted_char_index("太郎\t30", 4, delimiter: "\t", col_widths: col_widths)
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
def test_raw_to_formatted_display_col_basic
|
|
420
|
+
col_widths = [10, 10, 5]
|
|
421
|
+
r = RuVim::RichView::TableRenderer
|
|
422
|
+
# 'H' at raw 0 → display col 0
|
|
423
|
+
assert_equal 0, r.raw_to_formatted_display_col("Hello\tWorld\tFoo", 0, delimiter: "\t", col_widths: col_widths)
|
|
424
|
+
# 'o' at raw 4 → display col 4
|
|
425
|
+
assert_equal 4, r.raw_to_formatted_display_col("Hello\tWorld\tFoo", 4, delimiter: "\t", col_widths: col_widths)
|
|
426
|
+
# 'W' at raw 6 → display col 10 + 3 = 13
|
|
427
|
+
assert_equal 13, r.raw_to_formatted_display_col("Hello\tWorld\tFoo", 6, delimiter: "\t", col_widths: col_widths)
|
|
428
|
+
# 'F' at raw 12 → display col 10 + 3 + 10 + 3 = 26
|
|
429
|
+
assert_equal 26, r.raw_to_formatted_display_col("Hello\tWorld\tFoo", 12, delimiter: "\t", col_widths: col_widths)
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
def test_raw_to_formatted_display_col_cjk
|
|
433
|
+
# "太郎" = 2 chars, 4 display cols; "Alice" = 5 chars, 5 display cols → max = 5
|
|
434
|
+
# "30" = 2 chars, 2 display cols; "年齢" = 2 chars, 4 display cols → max = 4
|
|
435
|
+
col_widths = [5, 4]
|
|
436
|
+
r = RuVim::RichView::TableRenderer
|
|
437
|
+
# "太郎\t30"
|
|
438
|
+
# "太" at raw 0 → display col 0
|
|
439
|
+
assert_equal 0, r.raw_to_formatted_display_col("太郎\t30", 0, delimiter: "\t", col_widths: col_widths)
|
|
440
|
+
# "郎" at raw 1 → display col = dw("太") = 2
|
|
441
|
+
assert_equal 2, r.raw_to_formatted_display_col("太郎\t30", 1, delimiter: "\t", col_widths: col_widths)
|
|
442
|
+
# end of first field at raw 2 → display col = dw("太郎") = 4
|
|
443
|
+
assert_equal 4, r.raw_to_formatted_display_col("太郎\t30", 2, delimiter: "\t", col_widths: col_widths)
|
|
444
|
+
# "3" at raw 3 → display col = 5 (col_widths[0]) + 3 (separator) = 8
|
|
445
|
+
assert_equal 8, r.raw_to_formatted_display_col("太郎\t30", 3, delimiter: "\t", col_widths: col_widths)
|
|
446
|
+
# "0" at raw 4 → display col = 8 + dw("3") = 9
|
|
447
|
+
assert_equal 9, r.raw_to_formatted_display_col("太郎\t30", 4, delimiter: "\t", col_widths: col_widths)
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
def test_raw_to_formatted_display_col_alignment_across_cjk_rows
|
|
451
|
+
lines = ["太郎\t年齢", "Alice\t30"]
|
|
452
|
+
col_widths = RuVim::RichView::TableRenderer.compute_col_widths(lines, delimiter: "\t")
|
|
453
|
+
r = RuVim::RichView::TableRenderer
|
|
454
|
+
# Second field starts at same display col for both lines
|
|
455
|
+
# Line 0: "太郎\t年齢" → raw 3 is "年" → display col = col_widths[0]+3
|
|
456
|
+
# Line 1: "Alice\t30" → raw 6 is "3" → display col = col_widths[0]+3
|
|
457
|
+
dc0 = r.raw_to_formatted_display_col(lines[0], 3, delimiter: "\t", col_widths: col_widths)
|
|
458
|
+
dc1 = r.raw_to_formatted_display_col(lines[1], 6, delimiter: "\t", col_widths: col_widths)
|
|
459
|
+
assert_equal dc0, dc1, "Second field start should align across CJK and ASCII rows"
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
# --- Filetype detection tests ---
|
|
463
|
+
|
|
464
|
+
def test_detect_filetype_tsv
|
|
465
|
+
editor = RuVim::Editor.new
|
|
466
|
+
assert_equal "tsv", editor.detect_filetype("data.tsv")
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
def test_detect_filetype_csv
|
|
470
|
+
editor = RuVim::Editor.new
|
|
471
|
+
assert_equal "csv", editor.detect_filetype("data.csv")
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
def test_detect_filetype_tsv_uppercase
|
|
475
|
+
editor = RuVim::Editor.new
|
|
476
|
+
assert_equal "tsv", editor.detect_filetype("DATA.TSV")
|
|
477
|
+
end
|
|
478
|
+
end
|