ruvim 0.3.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/AGENTS.md +68 -7
- data/README.md +30 -7
- data/Rakefile +7 -0
- data/benchmark/cext_compare.rb +165 -0
- data/benchmark/chunked_load.rb +256 -0
- data/benchmark/file_load.rb +140 -0
- data/benchmark/hotspots.rb +178 -0
- data/docs/binding.md +18 -1
- data/docs/command.md +156 -10
- data/docs/config.md +10 -2
- data/docs/done.md +23 -0
- data/docs/spec.md +162 -25
- data/docs/todo.md +9 -0
- data/docs/tutorial.md +33 -1
- data/docs/vim_diff.md +31 -8
- data/ext/ruvim/extconf.rb +5 -0
- data/ext/ruvim/ruvim_ext.c +519 -0
- data/lib/ruvim/app.rb +246 -2525
- data/lib/ruvim/browser.rb +104 -0
- data/lib/ruvim/buffer.rb +43 -20
- data/lib/ruvim/cli.rb +6 -0
- data/lib/ruvim/command_invocation.rb +2 -2
- data/lib/ruvim/completion_manager.rb +708 -0
- data/lib/ruvim/dispatcher.rb +14 -8
- data/lib/ruvim/display_width.rb +91 -45
- data/lib/ruvim/editor.rb +74 -80
- data/lib/ruvim/ex_command_registry.rb +3 -1
- data/lib/ruvim/file_watcher.rb +243 -0
- data/lib/ruvim/gh/link.rb +207 -0
- data/lib/ruvim/git/blame.rb +255 -0
- data/lib/ruvim/git/branch.rb +112 -0
- data/lib/ruvim/git/commit.rb +102 -0
- data/lib/ruvim/git/diff.rb +129 -0
- data/lib/ruvim/git/grep.rb +107 -0
- data/lib/ruvim/git/handler.rb +125 -0
- data/lib/ruvim/git/log.rb +41 -0
- data/lib/ruvim/git/status.rb +103 -0
- data/lib/ruvim/global_commands.rb +351 -77
- data/lib/ruvim/highlighter.rb +4 -11
- data/lib/ruvim/input.rb +1 -0
- data/lib/ruvim/key_handler.rb +1510 -0
- data/lib/ruvim/keymap_manager.rb +7 -7
- data/lib/ruvim/lang/base.rb +5 -0
- data/lib/ruvim/lang/c.rb +116 -0
- data/lib/ruvim/lang/cpp.rb +107 -0
- data/lib/ruvim/lang/csv.rb +4 -1
- data/lib/ruvim/lang/diff.rb +43 -0
- data/lib/ruvim/lang/dockerfile.rb +36 -0
- data/lib/ruvim/lang/elixir.rb +85 -0
- data/lib/ruvim/lang/erb.rb +30 -0
- data/lib/ruvim/lang/go.rb +83 -0
- data/lib/ruvim/lang/html.rb +34 -0
- data/lib/ruvim/lang/javascript.rb +83 -0
- data/lib/ruvim/lang/json.rb +40 -0
- data/lib/ruvim/lang/lua.rb +76 -0
- data/lib/ruvim/lang/makefile.rb +36 -0
- data/lib/ruvim/lang/markdown.rb +3 -4
- data/lib/ruvim/lang/ocaml.rb +77 -0
- data/lib/ruvim/lang/perl.rb +91 -0
- data/lib/ruvim/lang/python.rb +85 -0
- data/lib/ruvim/lang/registry.rb +102 -0
- data/lib/ruvim/lang/ruby.rb +7 -0
- data/lib/ruvim/lang/rust.rb +95 -0
- data/lib/ruvim/lang/scheme.rb +5 -0
- data/lib/ruvim/lang/sh.rb +76 -0
- data/lib/ruvim/lang/sql.rb +52 -0
- data/lib/ruvim/lang/toml.rb +36 -0
- data/lib/ruvim/lang/tsv.rb +4 -1
- data/lib/ruvim/lang/typescript.rb +53 -0
- data/lib/ruvim/lang/yaml.rb +62 -0
- data/lib/ruvim/rich_view/json_renderer.rb +131 -0
- data/lib/ruvim/rich_view/jsonl_renderer.rb +57 -0
- data/lib/ruvim/rich_view/table_renderer.rb +3 -3
- data/lib/ruvim/rich_view.rb +30 -7
- data/lib/ruvim/screen.rb +135 -84
- data/lib/ruvim/stream/file_load.rb +85 -0
- data/lib/ruvim/stream/follow.rb +40 -0
- data/lib/ruvim/stream/git.rb +43 -0
- data/lib/ruvim/stream/run.rb +74 -0
- data/lib/ruvim/stream/stdin.rb +55 -0
- data/lib/ruvim/stream.rb +35 -0
- data/lib/ruvim/stream_mixer.rb +394 -0
- data/lib/ruvim/terminal.rb +18 -4
- data/lib/ruvim/text_metrics.rb +84 -65
- data/lib/ruvim/version.rb +1 -1
- data/lib/ruvim/window.rb +5 -5
- data/lib/ruvim.rb +31 -4
- data/test/app_command_test.rb +382 -0
- data/test/app_completion_test.rb +65 -16
- data/test/app_dot_repeat_test.rb +27 -3
- data/test/app_ex_command_test.rb +154 -0
- data/test/app_motion_test.rb +13 -12
- data/test/app_register_test.rb +2 -1
- data/test/app_scenario_test.rb +182 -8
- data/test/app_startup_test.rb +70 -27
- data/test/app_text_object_test.rb +2 -1
- data/test/app_unicode_behavior_test.rb +3 -2
- data/test/browser_test.rb +88 -0
- data/test/buffer_test.rb +24 -0
- data/test/cli_test.rb +77 -0
- data/test/clipboard_test.rb +67 -0
- data/test/command_invocation_test.rb +33 -0
- data/test/command_line_test.rb +118 -0
- data/test/config_dsl_test.rb +134 -0
- data/test/dispatcher_test.rb +74 -4
- data/test/display_width_test.rb +41 -0
- data/test/ex_command_registry_test.rb +106 -0
- data/test/file_watcher_test.rb +197 -0
- data/test/follow_test.rb +198 -0
- data/test/gh_link_test.rb +141 -0
- data/test/git_blame_test.rb +792 -0
- data/test/git_grep_test.rb +64 -0
- data/test/highlighter_test.rb +169 -0
- data/test/indent_test.rb +223 -0
- data/test/input_screen_integration_test.rb +1 -1
- data/test/keyword_chars_test.rb +85 -0
- data/test/lang_test.rb +634 -0
- data/test/markdown_renderer_test.rb +5 -5
- data/test/on_save_hook_test.rb +12 -8
- data/test/render_snapshot_test.rb +78 -0
- data/test/rich_view_test.rb +279 -23
- data/test/run_command_test.rb +307 -0
- data/test/screen_test.rb +68 -5
- data/test/search_option_test.rb +19 -0
- data/test/stream_test.rb +165 -0
- data/test/test_helper.rb +9 -0
- data/test/window_test.rb +59 -0
- metadata +68 -2
data/test/on_save_hook_test.rb
CHANGED
|
@@ -66,8 +66,9 @@ class OnSaveHookTest < Minitest::Test
|
|
|
66
66
|
editor.current_buffer.replace_all_lines!(["hello"])
|
|
67
67
|
|
|
68
68
|
# Execute :w command
|
|
69
|
+
kh = app.instance_variable_get(:@key_handler)
|
|
69
70
|
keys = ":w #{path}\n".chars
|
|
70
|
-
keys.each { |k|
|
|
71
|
+
keys.each { |k| kh.handle(k == "\n" ? :enter : k) }
|
|
71
72
|
|
|
72
73
|
assert called, "on_save hook should have been called"
|
|
73
74
|
end
|
|
@@ -84,15 +85,16 @@ class OnSaveHookTest < Minitest::Test
|
|
|
84
85
|
File.write(path, "x = 1\ndef foo(\n")
|
|
85
86
|
|
|
86
87
|
# Open and write the file
|
|
87
|
-
|
|
88
|
-
":
|
|
88
|
+
kh = app.instance_variable_get(:@key_handler)
|
|
89
|
+
":e #{path}\n".chars.each { |k| kh.handle(k == "\n" ? :enter : k) }
|
|
90
|
+
":w\n".chars.each { |k| kh.handle(k == "\n" ? :enter : k) }
|
|
89
91
|
|
|
90
92
|
refute_empty editor.quickfix_items, "quickfix should be populated after :w with syntax error"
|
|
91
93
|
assert_nil editor.quickfix_index, "quickfix index should be nil before navigation"
|
|
92
94
|
|
|
93
95
|
# Press ]q — should jump to first item (index 0)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
+
kh.handle("]")
|
|
97
|
+
kh.handle("q")
|
|
96
98
|
|
|
97
99
|
assert_equal 0, editor.quickfix_index
|
|
98
100
|
first_item = editor.quickfix_items.first
|
|
@@ -112,12 +114,13 @@ class OnSaveHookTest < Minitest::Test
|
|
|
112
114
|
path = File.join(dir, "ok.rb")
|
|
113
115
|
File.write(path, "puts 'hi'\n")
|
|
114
116
|
|
|
115
|
-
|
|
117
|
+
kh = app.instance_variable_get(:@key_handler)
|
|
118
|
+
":e #{path}\n".chars.each { |k| kh.handle(k == "\n" ? :enter : k) }
|
|
116
119
|
# Set some dummy quickfix items first
|
|
117
120
|
editor.set_quickfix_list([{ buffer_id: editor.current_buffer.id, row: 0, col: 0, text: "dummy" }])
|
|
118
121
|
refute_empty editor.quickfix_items
|
|
119
122
|
|
|
120
|
-
":w\n".chars.each { |k|
|
|
123
|
+
":w\n".chars.each { |k| kh.handle(k == "\n" ? :enter : k) }
|
|
121
124
|
assert_empty editor.quickfix_items, "quickfix should be cleared after :w with valid file"
|
|
122
125
|
end
|
|
123
126
|
end
|
|
@@ -141,8 +144,9 @@ class OnSaveHookTest < Minitest::Test
|
|
|
141
144
|
editor.current_buffer.replace_all_lines!(["hello"])
|
|
142
145
|
editor.set_option("onsavehook", false)
|
|
143
146
|
|
|
147
|
+
kh = app.instance_variable_get(:@key_handler)
|
|
144
148
|
keys = ":w #{path}\n".chars
|
|
145
|
-
keys.each { |k|
|
|
149
|
+
keys.each { |k| kh.handle(k == "\n" ? :enter : k) }
|
|
146
150
|
|
|
147
151
|
refute called, "on_save hook should NOT have been called when onsavehook is disabled"
|
|
148
152
|
end
|
|
@@ -64,7 +64,85 @@ class RenderSnapshotTest < Minitest::Test
|
|
|
64
64
|
(1..rows).map { |row| strip_ansi(frame[:lines][row].to_s) }.join("\n")
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
+
def build_raw_frame(lines:, winsize:, number: false, rich_format: nil, cursor_y: 0, cursor_x: 0)
|
|
68
|
+
editor = RuVim::Editor.new
|
|
69
|
+
buf = editor.add_empty_buffer
|
|
70
|
+
win = editor.add_window(buffer_id: buf.id)
|
|
71
|
+
buf.replace_all_lines!(lines)
|
|
72
|
+
editor.set_option("number", number, scope: :window, window: win, buffer: buf)
|
|
73
|
+
win.cursor_y = cursor_y
|
|
74
|
+
win.cursor_x = cursor_x
|
|
75
|
+
if rich_format
|
|
76
|
+
editor.instance_variable_set(:@rich_state, { format: rich_format })
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
term = TerminalStub.new(winsize)
|
|
80
|
+
screen = RuVim::Screen.new(terminal: term)
|
|
81
|
+
|
|
82
|
+
rows, cols = term.winsize
|
|
83
|
+
text_rows, text_cols = editor.text_viewport_size(rows:, cols:)
|
|
84
|
+
rects = screen.send(:window_rects, editor, text_rows:, text_cols:)
|
|
85
|
+
win.ensure_visible(buf, height: text_rows, width: text_cols, tabstop: 2)
|
|
86
|
+
frame = screen.send(:build_frame, editor, rows:, cols:, text_rows:, text_cols:, rects:)
|
|
87
|
+
|
|
88
|
+
(1..rows).map { |row| frame[:lines][row].to_s }.join("\n")
|
|
89
|
+
end
|
|
90
|
+
|
|
67
91
|
def strip_ansi(str)
|
|
68
92
|
str.gsub(/\e\[[0-9;?]*[A-Za-z]/, "")
|
|
69
93
|
end
|
|
70
94
|
end
|
|
95
|
+
|
|
96
|
+
class RenderSanitizeTest < Minitest::Test
|
|
97
|
+
TerminalStub = Struct.new(:winsize) do
|
|
98
|
+
def write(_data); end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def test_normal_buffer_sanitizes_escape_sequences
|
|
102
|
+
# ESC (0x1B) should be replaced with "?" in normal rendering
|
|
103
|
+
lines = ["hello\x1b]52;c;dGVzdA==\x07world"]
|
|
104
|
+
snapshot = build_raw_frame(lines: lines, winsize: [5, 40])
|
|
105
|
+
refute_includes snapshot, "\x1b]52"
|
|
106
|
+
refute_includes snapshot, "\x07"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def test_rich_view_sanitizes_escape_sequences_in_tsv
|
|
110
|
+
lines = ["col1\tcol2", "val1\t\x1b]52;c;dGVzdA==\x07evil"]
|
|
111
|
+
snapshot = build_raw_frame(lines: lines, winsize: [6, 40], rich_format: :tsv)
|
|
112
|
+
refute_includes snapshot, "\x1b]52"
|
|
113
|
+
refute_includes snapshot, "\x07"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def test_rich_view_sanitizes_escape_sequences_in_markdown
|
|
117
|
+
lines = ["# heading", "\x1b]52;c;dGVzdA==\x07evil text"]
|
|
118
|
+
snapshot = build_raw_frame(lines: lines, winsize: [6, 40], rich_format: :markdown)
|
|
119
|
+
refute_includes snapshot, "\x1b]52"
|
|
120
|
+
refute_includes snapshot, "\x07"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
def build_raw_frame(lines:, winsize:, number: false, rich_format: nil, cursor_y: 0, cursor_x: 0)
|
|
126
|
+
editor = RuVim::Editor.new
|
|
127
|
+
buf = editor.add_empty_buffer
|
|
128
|
+
win = editor.add_window(buffer_id: buf.id)
|
|
129
|
+
buf.replace_all_lines!(lines)
|
|
130
|
+
editor.set_option("number", number, scope: :window, window: win, buffer: buf)
|
|
131
|
+
win.cursor_y = cursor_y
|
|
132
|
+
win.cursor_x = cursor_x
|
|
133
|
+
if rich_format
|
|
134
|
+
editor.instance_variable_set(:@rich_state, { format: rich_format })
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
term = TerminalStub.new(winsize)
|
|
138
|
+
screen = RuVim::Screen.new(terminal: term)
|
|
139
|
+
|
|
140
|
+
rows, cols = term.winsize
|
|
141
|
+
text_rows, text_cols = editor.text_viewport_size(rows:, cols:)
|
|
142
|
+
rects = screen.send(:window_rects, editor, text_rows:, text_cols:)
|
|
143
|
+
win.ensure_visible(buf, height: text_rows, width: text_cols, tabstop: 2)
|
|
144
|
+
frame = screen.send(:build_frame, editor, rows:, cols:, text_rows:, text_cols:, rects:)
|
|
145
|
+
|
|
146
|
+
(1..rows).map { |row| frame[:lines][row].to_s }.join("\n")
|
|
147
|
+
end
|
|
148
|
+
end
|
data/test/rich_view_test.rb
CHANGED
|
@@ -4,36 +4,36 @@ class RichViewTest < Minitest::Test
|
|
|
4
4
|
# --- Framework tests ---
|
|
5
5
|
|
|
6
6
|
def test_register_and_renderer_for
|
|
7
|
-
assert RuVim::RichView.renderer_for(
|
|
8
|
-
assert RuVim::RichView.renderer_for(
|
|
9
|
-
assert_nil RuVim::RichView.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
10
|
end
|
|
11
11
|
|
|
12
12
|
def test_registered_filetypes
|
|
13
13
|
fts = RuVim::RichView.registered_filetypes
|
|
14
|
-
assert_includes fts,
|
|
15
|
-
assert_includes fts,
|
|
14
|
+
assert_includes fts, :tsv
|
|
15
|
+
assert_includes fts, :csv
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def test_detect_format_from_filetype
|
|
19
19
|
editor = fresh_editor
|
|
20
20
|
buf = editor.current_buffer
|
|
21
21
|
buf.options["filetype"] = "csv"
|
|
22
|
-
assert_equal
|
|
22
|
+
assert_equal :csv, RuVim::RichView.detect_format(buf)
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
def test_detect_format_auto_tsv
|
|
26
26
|
editor = fresh_editor
|
|
27
27
|
buf = editor.current_buffer
|
|
28
28
|
buf.replace_all_lines!(["a\tb\tc", "d\te\tf"])
|
|
29
|
-
assert_equal
|
|
29
|
+
assert_equal :tsv, RuVim::RichView.detect_format(buf)
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
def test_detect_format_auto_csv
|
|
33
33
|
editor = fresh_editor
|
|
34
34
|
buf = editor.current_buffer
|
|
35
35
|
buf.replace_all_lines!(["a,b,c", "d,e,f"])
|
|
36
|
-
assert_equal
|
|
36
|
+
assert_equal :csv, RuVim::RichView.detect_format(buf)
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
def test_detect_format_returns_nil_for_plain_text
|
|
@@ -63,7 +63,7 @@ class RichViewTest < Minitest::Test
|
|
|
63
63
|
buf.replace_all_lines!(["a\tb", "c\td"])
|
|
64
64
|
buf.options["filetype"] = "tsv"
|
|
65
65
|
|
|
66
|
-
RuVim::RichView.open!(editor, format:
|
|
66
|
+
RuVim::RichView.open!(editor, format: :tsv)
|
|
67
67
|
assert_equal :rich, editor.mode
|
|
68
68
|
assert RuVim::RichView.active?(editor)
|
|
69
69
|
end
|
|
@@ -75,7 +75,7 @@ class RichViewTest < Minitest::Test
|
|
|
75
75
|
buf.options["filetype"] = "tsv"
|
|
76
76
|
original_id = buf.id
|
|
77
77
|
|
|
78
|
-
RuVim::RichView.open!(editor, format:
|
|
78
|
+
RuVim::RichView.open!(editor, format: :tsv)
|
|
79
79
|
assert_equal original_id, editor.current_buffer.id
|
|
80
80
|
end
|
|
81
81
|
|
|
@@ -85,7 +85,7 @@ class RichViewTest < Minitest::Test
|
|
|
85
85
|
buf.replace_all_lines!(["x\ty"])
|
|
86
86
|
buf.options["filetype"] = "tsv"
|
|
87
87
|
|
|
88
|
-
RuVim::RichView.open!(editor, format:
|
|
88
|
+
RuVim::RichView.open!(editor, format: :tsv)
|
|
89
89
|
assert_equal :rich, editor.mode
|
|
90
90
|
|
|
91
91
|
RuVim::RichView.close!(editor)
|
|
@@ -100,7 +100,7 @@ class RichViewTest < Minitest::Test
|
|
|
100
100
|
buf.options["filetype"] = "tsv"
|
|
101
101
|
original_id = buf.id
|
|
102
102
|
|
|
103
|
-
RuVim::RichView.open!(editor, format:
|
|
103
|
+
RuVim::RichView.open!(editor, format: :tsv)
|
|
104
104
|
RuVim::RichView.close!(editor)
|
|
105
105
|
assert_equal original_id, editor.current_buffer.id
|
|
106
106
|
end
|
|
@@ -124,9 +124,9 @@ class RichViewTest < Minitest::Test
|
|
|
124
124
|
buf.replace_all_lines!(["a\tb"])
|
|
125
125
|
buf.options["filetype"] = "tsv"
|
|
126
126
|
|
|
127
|
-
RuVim::RichView.open!(editor, format:
|
|
127
|
+
RuVim::RichView.open!(editor, format: :tsv)
|
|
128
128
|
state = editor.rich_state
|
|
129
|
-
assert_equal
|
|
129
|
+
assert_equal :tsv, state[:format]
|
|
130
130
|
assert_equal "\t", state[:delimiter]
|
|
131
131
|
end
|
|
132
132
|
|
|
@@ -136,9 +136,9 @@ class RichViewTest < Minitest::Test
|
|
|
136
136
|
buf.replace_all_lines!(["a,b"])
|
|
137
137
|
buf.options["filetype"] = "csv"
|
|
138
138
|
|
|
139
|
-
RuVim::RichView.open!(editor, format:
|
|
139
|
+
RuVim::RichView.open!(editor, format: :csv)
|
|
140
140
|
state = editor.rich_state
|
|
141
|
-
assert_equal
|
|
141
|
+
assert_equal :csv, state[:format]
|
|
142
142
|
assert_equal ",", state[:delimiter]
|
|
143
143
|
end
|
|
144
144
|
|
|
@@ -148,7 +148,7 @@ class RichViewTest < Minitest::Test
|
|
|
148
148
|
buf.replace_all_lines!(["a\tb"])
|
|
149
149
|
buf.options["filetype"] = "tsv"
|
|
150
150
|
|
|
151
|
-
RuVim::RichView.open!(editor, format:
|
|
151
|
+
RuVim::RichView.open!(editor, format: :tsv)
|
|
152
152
|
RuVim::RichView.close!(editor)
|
|
153
153
|
assert_nil editor.rich_state
|
|
154
154
|
end
|
|
@@ -159,7 +159,7 @@ class RichViewTest < Minitest::Test
|
|
|
159
159
|
buf.replace_all_lines!(["a\tb"])
|
|
160
160
|
buf.options["filetype"] = "tsv"
|
|
161
161
|
|
|
162
|
-
RuVim::RichView.open!(editor, format:
|
|
162
|
+
RuVim::RichView.open!(editor, format: :tsv)
|
|
163
163
|
editor.enter_command_line_mode(":")
|
|
164
164
|
assert_equal :command_line, editor.mode
|
|
165
165
|
assert RuVim::RichView.active?(editor)
|
|
@@ -171,7 +171,7 @@ class RichViewTest < Minitest::Test
|
|
|
171
171
|
buf.replace_all_lines!(["a\tb"])
|
|
172
172
|
buf.options["filetype"] = "tsv"
|
|
173
173
|
|
|
174
|
-
RuVim::RichView.open!(editor, format:
|
|
174
|
+
RuVim::RichView.open!(editor, format: :tsv)
|
|
175
175
|
assert_equal :rich, editor.mode
|
|
176
176
|
|
|
177
177
|
editor.enter_command_line_mode(":")
|
|
@@ -189,7 +189,7 @@ class RichViewTest < Minitest::Test
|
|
|
189
189
|
buf.replace_all_lines!(["a\tb"])
|
|
190
190
|
buf.options["filetype"] = "tsv"
|
|
191
191
|
|
|
192
|
-
RuVim::RichView.open!(editor, format:
|
|
192
|
+
RuVim::RichView.open!(editor, format: :tsv)
|
|
193
193
|
editor.enter_command_line_mode(":")
|
|
194
194
|
editor.leave_command_line
|
|
195
195
|
assert_equal :rich, editor.mode
|
|
@@ -210,7 +210,7 @@ class RichViewTest < Minitest::Test
|
|
|
210
210
|
buf.replace_all_lines!(["a\tb"])
|
|
211
211
|
buf.options["filetype"] = "tsv"
|
|
212
212
|
|
|
213
|
-
RuVim::RichView.open!(editor, format:
|
|
213
|
+
RuVim::RichView.open!(editor, format: :tsv)
|
|
214
214
|
editor.enter_normal_mode
|
|
215
215
|
assert_nil editor.rich_state
|
|
216
216
|
assert_equal :normal, editor.mode
|
|
@@ -222,7 +222,7 @@ class RichViewTest < Minitest::Test
|
|
|
222
222
|
buf.replace_all_lines!(["a\tbb", "ccc\td"])
|
|
223
223
|
buf.options["filetype"] = "tsv"
|
|
224
224
|
|
|
225
|
-
RuVim::RichView.open!(editor, format:
|
|
225
|
+
RuVim::RichView.open!(editor, format: :tsv)
|
|
226
226
|
lines = [buf.line_at(0), buf.line_at(1)]
|
|
227
227
|
rendered = RuVim::RichView.render_visible_lines(editor, lines)
|
|
228
228
|
assert_equal 2, rendered.length
|
|
@@ -236,7 +236,7 @@ class RichViewTest < Minitest::Test
|
|
|
236
236
|
buf.options["filetype"] = "tsv"
|
|
237
237
|
count_before = editor.buffers.length
|
|
238
238
|
|
|
239
|
-
RuVim::RichView.open!(editor, format:
|
|
239
|
+
RuVim::RichView.open!(editor, format: :tsv)
|
|
240
240
|
assert_equal count_before, editor.buffers.length
|
|
241
241
|
end
|
|
242
242
|
|
|
@@ -459,6 +459,161 @@ class RichViewTest < Minitest::Test
|
|
|
459
459
|
assert_equal dc0, dc1, "Second field start should align across CJK and ASCII rows"
|
|
460
460
|
end
|
|
461
461
|
|
|
462
|
+
# --- JSON Rich View tests ---
|
|
463
|
+
|
|
464
|
+
def test_json_registered
|
|
465
|
+
assert RuVim::RichView.renderer_for(:json)
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
def test_json_open_creates_virtual_buffer
|
|
469
|
+
editor = fresh_editor
|
|
470
|
+
buf = editor.current_buffer
|
|
471
|
+
buf.replace_all_lines!(['{"a":1,"b":[2,3]}'])
|
|
472
|
+
buf.options["filetype"] = "json"
|
|
473
|
+
count_before = editor.buffers.length
|
|
474
|
+
|
|
475
|
+
RuVim::RichView.open!(editor, format: :json)
|
|
476
|
+
assert_equal count_before + 1, editor.buffers.length
|
|
477
|
+
new_buf = editor.current_buffer
|
|
478
|
+
refute_equal buf.id, new_buf.id
|
|
479
|
+
assert_equal :json_formatted, new_buf.kind
|
|
480
|
+
assert new_buf.readonly?
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
def test_json_open_binds_close_keys
|
|
484
|
+
editor = fresh_editor
|
|
485
|
+
editor.keymap_manager = RuVim::KeymapManager.new
|
|
486
|
+
buf = editor.current_buffer
|
|
487
|
+
buf.replace_all_lines!(['{"a":1}'])
|
|
488
|
+
buf.options["filetype"] = "json"
|
|
489
|
+
|
|
490
|
+
RuVim::RichView.open!(editor, format: :json)
|
|
491
|
+
result = editor.keymap_manager.resolve_with_context(:normal, ["\e"], editor: editor)
|
|
492
|
+
assert_equal "rich.close_buffer", result.invocation.id
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
def test_json_open_pretty_prints
|
|
496
|
+
editor = fresh_editor
|
|
497
|
+
buf = editor.current_buffer
|
|
498
|
+
buf.replace_all_lines!(['{"a":1,"b":[2,3]}'])
|
|
499
|
+
buf.options["filetype"] = "json"
|
|
500
|
+
|
|
501
|
+
RuVim::RichView.open!(editor, format: :json)
|
|
502
|
+
new_buf = editor.current_buffer
|
|
503
|
+
lines = new_buf.lines
|
|
504
|
+
assert lines.length > 1, "Minified JSON should be expanded to multiple lines"
|
|
505
|
+
assert_equal "{", lines.first.strip
|
|
506
|
+
assert_equal "}", lines.last.strip
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
def test_json_open_multiline_buffer
|
|
510
|
+
editor = fresh_editor
|
|
511
|
+
buf = editor.current_buffer
|
|
512
|
+
buf.replace_all_lines!(['{', '"key": "value"', '}'])
|
|
513
|
+
buf.options["filetype"] = "json"
|
|
514
|
+
|
|
515
|
+
RuVim::RichView.open!(editor, format: :json)
|
|
516
|
+
new_buf = editor.current_buffer
|
|
517
|
+
lines = new_buf.lines
|
|
518
|
+
assert lines.length >= 3
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
def test_json_open_invalid_json_shows_error
|
|
522
|
+
editor = fresh_editor
|
|
523
|
+
buf = editor.current_buffer
|
|
524
|
+
buf.replace_all_lines!(['{"invalid json'])
|
|
525
|
+
buf.options["filetype"] = "json"
|
|
526
|
+
|
|
527
|
+
RuVim::RichView.open!(editor, format: :json)
|
|
528
|
+
# Should stay on original buffer
|
|
529
|
+
assert_equal buf.id, editor.current_buffer.id
|
|
530
|
+
assert_match(/JSON/, editor.message.to_s)
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
def test_json_open_does_not_enter_rich_mode
|
|
534
|
+
editor = fresh_editor
|
|
535
|
+
buf = editor.current_buffer
|
|
536
|
+
buf.replace_all_lines!(['{"a":1}'])
|
|
537
|
+
buf.options["filetype"] = "json"
|
|
538
|
+
|
|
539
|
+
RuVim::RichView.open!(editor, format: :json)
|
|
540
|
+
# Virtual buffer approach — no rich mode
|
|
541
|
+
assert_equal :normal, editor.mode
|
|
542
|
+
assert_nil editor.rich_state
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
def test_json_cursor_maps_to_formatted_line
|
|
546
|
+
editor = fresh_editor
|
|
547
|
+
buf = editor.current_buffer
|
|
548
|
+
# {"a":1,"b":{"c":2}}
|
|
549
|
+
buf.replace_all_lines!(['{"a":1,"b":{"c":2}}'])
|
|
550
|
+
buf.options["filetype"] = "json"
|
|
551
|
+
|
|
552
|
+
# Place cursor at "c" key — find its offset
|
|
553
|
+
line = buf.line_at(0)
|
|
554
|
+
idx = line.index('"c"')
|
|
555
|
+
editor.current_window.cursor_x = idx
|
|
556
|
+
|
|
557
|
+
RuVim::RichView.open!(editor, format: :json)
|
|
558
|
+
new_buf = editor.current_buffer
|
|
559
|
+
# Cursor should be on the line containing "c"
|
|
560
|
+
cursor_line = new_buf.line_at(editor.current_window.cursor_y)
|
|
561
|
+
assert_match(/"c"/, cursor_line, "Cursor should be on the line with \"c\" key")
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
def test_json_cursor_maps_multiline_source
|
|
565
|
+
editor = fresh_editor
|
|
566
|
+
buf = editor.current_buffer
|
|
567
|
+
buf.replace_all_lines!(['{', ' "x": [1, 2, 3]', '}'])
|
|
568
|
+
buf.options["filetype"] = "json"
|
|
569
|
+
|
|
570
|
+
# Place cursor on line 1 at the "x" key (col 2 = opening quote)
|
|
571
|
+
editor.current_window.cursor_y = 1
|
|
572
|
+
editor.current_window.cursor_x = 2
|
|
573
|
+
|
|
574
|
+
RuVim::RichView.open!(editor, format: :json)
|
|
575
|
+
new_buf = editor.current_buffer
|
|
576
|
+
cursor_line = new_buf.line_at(editor.current_window.cursor_y)
|
|
577
|
+
assert_match(/"x"/, cursor_line, "Cursor should be on the line with \"x\" key")
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
def test_json_cursor_at_start_stays_at_start
|
|
581
|
+
editor = fresh_editor
|
|
582
|
+
buf = editor.current_buffer
|
|
583
|
+
buf.replace_all_lines!(['{"a":1}'])
|
|
584
|
+
buf.options["filetype"] = "json"
|
|
585
|
+
editor.current_window.cursor_x = 0
|
|
586
|
+
|
|
587
|
+
RuVim::RichView.open!(editor, format: :json)
|
|
588
|
+
assert_equal 0, editor.current_window.cursor_y
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
def test_json_significant_offset
|
|
592
|
+
r = RuVim::RichView::JsonRenderer
|
|
593
|
+
# {"a" — 4 significant chars: { " a "
|
|
594
|
+
assert_equal 4, r.significant_char_count('{"a"', 4)
|
|
595
|
+
# { "a" — space outside string skipped, still 4 significant
|
|
596
|
+
assert_equal 4, r.significant_char_count('{ "a"', 5)
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
def test_json_line_for_significant_offset
|
|
600
|
+
formatted = "{\n \"a\": 1\n}"
|
|
601
|
+
r = RuVim::RichView::JsonRenderer
|
|
602
|
+
# count 0 → line 0 (before any char)
|
|
603
|
+
assert_equal 0, r.line_for_significant_count(formatted, 0)
|
|
604
|
+
# count 1 → line 0 ({ is the 1st significant char, on line 0)
|
|
605
|
+
assert_equal 0, r.line_for_significant_count(formatted, 1)
|
|
606
|
+
# count 2 → line 1 (" opening quote of "a" is on line 1)
|
|
607
|
+
assert_equal 1, r.line_for_significant_count(formatted, 2)
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
def test_json_filetype_detected
|
|
611
|
+
editor = fresh_editor
|
|
612
|
+
buf = editor.current_buffer
|
|
613
|
+
buf.options["filetype"] = "json"
|
|
614
|
+
assert_equal :json, RuVim::RichView.detect_format(buf)
|
|
615
|
+
end
|
|
616
|
+
|
|
462
617
|
# --- Filetype detection tests ---
|
|
463
618
|
|
|
464
619
|
def test_detect_filetype_tsv
|
|
@@ -475,4 +630,105 @@ class RichViewTest < Minitest::Test
|
|
|
475
630
|
editor = RuVim::Editor.new
|
|
476
631
|
assert_equal "tsv", editor.detect_filetype("DATA.TSV")
|
|
477
632
|
end
|
|
633
|
+
|
|
634
|
+
def test_detect_filetype_jsonl
|
|
635
|
+
editor = RuVim::Editor.new
|
|
636
|
+
assert_equal "jsonl", editor.detect_filetype("data.jsonl")
|
|
637
|
+
end
|
|
638
|
+
|
|
639
|
+
# --- JSONL Rich View tests ---
|
|
640
|
+
|
|
641
|
+
def test_jsonl_registered
|
|
642
|
+
assert RuVim::RichView.renderer_for(:jsonl)
|
|
643
|
+
end
|
|
644
|
+
|
|
645
|
+
def test_jsonl_open_creates_virtual_buffer
|
|
646
|
+
editor = fresh_editor
|
|
647
|
+
buf = editor.current_buffer
|
|
648
|
+
buf.replace_all_lines!(['{"a":1}', '{"b":2}'])
|
|
649
|
+
buf.options["filetype"] = "jsonl"
|
|
650
|
+
count_before = editor.buffers.length
|
|
651
|
+
|
|
652
|
+
RuVim::RichView.open!(editor, format: :jsonl)
|
|
653
|
+
assert_equal count_before + 1, editor.buffers.length
|
|
654
|
+
new_buf = editor.current_buffer
|
|
655
|
+
refute_equal buf.id, new_buf.id
|
|
656
|
+
assert_equal :jsonl_formatted, new_buf.kind
|
|
657
|
+
assert new_buf.readonly?
|
|
658
|
+
end
|
|
659
|
+
|
|
660
|
+
def test_jsonl_open_binds_close_keys
|
|
661
|
+
editor = fresh_editor
|
|
662
|
+
editor.keymap_manager = RuVim::KeymapManager.new
|
|
663
|
+
buf = editor.current_buffer
|
|
664
|
+
buf.replace_all_lines!(['{"a":1}', '{"b":2}'])
|
|
665
|
+
buf.options["filetype"] = "jsonl"
|
|
666
|
+
|
|
667
|
+
RuVim::RichView.open!(editor, format: :jsonl)
|
|
668
|
+
result = editor.keymap_manager.resolve_with_context(:normal, ["\e"], editor: editor)
|
|
669
|
+
assert_equal "rich.close_buffer", result.invocation.id
|
|
670
|
+
end
|
|
671
|
+
|
|
672
|
+
def test_jsonl_open_pretty_prints_each_line
|
|
673
|
+
editor = fresh_editor
|
|
674
|
+
buf = editor.current_buffer
|
|
675
|
+
buf.replace_all_lines!(['{"a":1,"b":[2,3]}', '{"c":4}'])
|
|
676
|
+
buf.options["filetype"] = "jsonl"
|
|
677
|
+
|
|
678
|
+
RuVim::RichView.open!(editor, format: :jsonl)
|
|
679
|
+
new_buf = editor.current_buffer
|
|
680
|
+
lines = new_buf.lines
|
|
681
|
+
# Each JSON object should be expanded; separated by "---"
|
|
682
|
+
assert lines.length > 2, "JSONL should be expanded to multiple lines"
|
|
683
|
+
assert lines.any? { |l| l.include?("---") }, "Entries should be separated"
|
|
684
|
+
end
|
|
685
|
+
|
|
686
|
+
def test_jsonl_open_maps_cursor_to_correct_entry
|
|
687
|
+
editor = fresh_editor
|
|
688
|
+
buf = editor.current_buffer
|
|
689
|
+
buf.replace_all_lines!(['{"a":1}', '{"b":2}', '{"c":3}'])
|
|
690
|
+
buf.options["filetype"] = "jsonl"
|
|
691
|
+
editor.current_window.cursor_y = 1 # on second entry
|
|
692
|
+
|
|
693
|
+
RuVim::RichView.open!(editor, format: :jsonl)
|
|
694
|
+
new_buf = editor.current_buffer
|
|
695
|
+
cy = editor.current_window.cursor_y
|
|
696
|
+
# Cursor should be within the second entry's formatted block
|
|
697
|
+
nearby = (cy..[cy + 2, new_buf.lines.length - 1].min).map { |r| new_buf.line_at(r) }.join("\n")
|
|
698
|
+
assert_match(/"b"/, nearby, "Cursor should be near the entry with \"b\"")
|
|
699
|
+
end
|
|
700
|
+
|
|
701
|
+
def test_jsonl_open_skips_blank_lines
|
|
702
|
+
editor = fresh_editor
|
|
703
|
+
buf = editor.current_buffer
|
|
704
|
+
buf.replace_all_lines!(['{"a":1}', '', '{"b":2}'])
|
|
705
|
+
buf.options["filetype"] = "jsonl"
|
|
706
|
+
|
|
707
|
+
RuVim::RichView.open!(editor, format: :jsonl)
|
|
708
|
+
new_buf = editor.current_buffer
|
|
709
|
+
lines = new_buf.lines
|
|
710
|
+
# Should contain both entries
|
|
711
|
+
assert lines.any? { |l| l.include?('"a"') }
|
|
712
|
+
assert lines.any? { |l| l.include?('"b"') }
|
|
713
|
+
end
|
|
714
|
+
|
|
715
|
+
def test_jsonl_open_shows_parse_error_inline
|
|
716
|
+
editor = fresh_editor
|
|
717
|
+
buf = editor.current_buffer
|
|
718
|
+
buf.replace_all_lines!(['{"a":1}', 'bad json', '{"b":2}'])
|
|
719
|
+
buf.options["filetype"] = "jsonl"
|
|
720
|
+
|
|
721
|
+
RuVim::RichView.open!(editor, format: :jsonl)
|
|
722
|
+
new_buf = editor.current_buffer
|
|
723
|
+
lines = new_buf.lines
|
|
724
|
+
# Invalid line should show an error marker
|
|
725
|
+
assert lines.any? { |l| l.include?("PARSE ERROR") }, "Invalid JSON line should show error"
|
|
726
|
+
end
|
|
727
|
+
|
|
728
|
+
def test_jsonl_filetype_detected
|
|
729
|
+
editor = fresh_editor
|
|
730
|
+
buf = editor.current_buffer
|
|
731
|
+
buf.options["filetype"] = "jsonl"
|
|
732
|
+
assert_equal :jsonl, RuVim::RichView.detect_format(buf)
|
|
733
|
+
end
|
|
478
734
|
end
|