ruvim 0.2.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 +23 -0
- data/docs/command.md +85 -0
- data/docs/config.md +2 -2
- data/docs/done.md +21 -0
- data/docs/spec.md +157 -12
- data/docs/todo.md +1 -5
- data/docs/vim_diff.md +94 -172
- data/lib/ruvim/app.rb +882 -69
- 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 +2 -0
- data/lib/ruvim/context.rb +2 -0
- data/lib/ruvim/dispatcher.rb +143 -13
- data/lib/ruvim/display_width.rb +3 -0
- data/lib/ruvim/editor.rb +455 -71
- data/lib/ruvim/ex_command_registry.rb +2 -0
- data/lib/ruvim/global_commands.rb +890 -63
- data/lib/ruvim/highlighter.rb +16 -21
- data/lib/ruvim/input.rb +39 -28
- data/lib/ruvim/keymap_manager.rb +83 -0
- data/lib/ruvim/keyword_chars.rb +2 -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 +503 -106
- data/lib/ruvim/terminal.rb +18 -1
- data/lib/ruvim/text_metrics.rb +2 -0
- data/lib/ruvim/version.rb +1 -1
- data/lib/ruvim/window.rb +2 -0
- data/lib/ruvim.rb +14 -0
- data/test/app_completion_test.rb +73 -0
- data/test/app_dot_repeat_test.rb +13 -0
- data/test/app_motion_test.rb +13 -0
- data/test/app_scenario_test.rb +729 -1
- data/test/app_startup_test.rb +187 -0
- data/test/arglist_test.rb +113 -0
- data/test/buffer_test.rb +49 -30
- data/test/dispatcher_test.rb +322 -0
- data/test/editor_register_test.rb +23 -0
- data/test/highlighter_test.rb +121 -0
- data/test/indent_test.rb +201 -0
- data/test/input_screen_integration_test.rb +40 -2
- 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 +304 -0
- metadata +33 -2
|
@@ -56,7 +56,7 @@ class InputScreenIntegrationTest < Minitest::Test
|
|
|
56
56
|
app.instance_variable_set(:@screen, screen)
|
|
57
57
|
|
|
58
58
|
stdin = FakeTTY.new("\e[6~")
|
|
59
|
-
input = RuVim::Input.new(stdin
|
|
59
|
+
input = RuVim::Input.new(stdin)
|
|
60
60
|
|
|
61
61
|
with_fake_select do
|
|
62
62
|
key = input.read_key(timeout: 0.2)
|
|
@@ -72,11 +72,49 @@ class InputScreenIntegrationTest < Minitest::Test
|
|
|
72
72
|
|
|
73
73
|
def test_input_keeps_repeated_arrow_sequences_separate
|
|
74
74
|
stdin = FakeTTY.new("\e[A\e[A")
|
|
75
|
-
input = RuVim::Input.new(stdin
|
|
75
|
+
input = RuVim::Input.new(stdin)
|
|
76
76
|
|
|
77
77
|
with_fake_select do
|
|
78
78
|
assert_equal :up, input.read_key(timeout: 0.2)
|
|
79
79
|
assert_equal :up, input.read_key(timeout: 0.2)
|
|
80
80
|
end
|
|
81
81
|
end
|
|
82
|
+
|
|
83
|
+
def test_input_reads_ctrl_z
|
|
84
|
+
stdin = FakeTTY.new("\u001a")
|
|
85
|
+
input = RuVim::Input.new(stdin)
|
|
86
|
+
|
|
87
|
+
with_fake_select do
|
|
88
|
+
assert_equal :ctrl_z, input.read_key(timeout: 0.2)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def test_input_shift_arrow_sequences
|
|
93
|
+
{
|
|
94
|
+
"\e[1;2A" => :shift_up,
|
|
95
|
+
"\e[1;2B" => :shift_down,
|
|
96
|
+
"\e[1;2C" => :shift_right,
|
|
97
|
+
"\e[1;2D" => :shift_left
|
|
98
|
+
}.each do |seq, expected|
|
|
99
|
+
stdin = FakeTTY.new(seq)
|
|
100
|
+
input = RuVim::Input.new(stdin)
|
|
101
|
+
|
|
102
|
+
with_fake_select do
|
|
103
|
+
assert_equal expected, input.read_key(timeout: 0.2), "Expected #{expected} for sequence #{seq.inspect}"
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def test_has_pending_input_returns_true_when_data_available
|
|
109
|
+
stdin = FakeTTY.new("abc")
|
|
110
|
+
input = RuVim::Input.new(stdin)
|
|
111
|
+
|
|
112
|
+
with_fake_select do
|
|
113
|
+
assert input.has_pending_input?
|
|
114
|
+
input.read_key(timeout: 0)
|
|
115
|
+
input.read_key(timeout: 0)
|
|
116
|
+
input.read_key(timeout: 0)
|
|
117
|
+
refute input.has_pending_input?
|
|
118
|
+
end
|
|
119
|
+
end
|
|
82
120
|
end
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
require_relative "test_helper"
|
|
2
|
+
|
|
3
|
+
class MarkdownRendererTest < Minitest::Test
|
|
4
|
+
def renderer
|
|
5
|
+
RuVim::RichView::MarkdownRenderer
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
# --- Registration ---
|
|
9
|
+
|
|
10
|
+
def test_renderer_registered_for_markdown
|
|
11
|
+
assert_equal renderer, RuVim::RichView.renderer_for("markdown")
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def test_delimiter_for_markdown
|
|
15
|
+
assert_nil renderer.delimiter_for("markdown")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# --- Headings ---
|
|
19
|
+
|
|
20
|
+
def test_heading_h1_bold
|
|
21
|
+
lines = ["# Hello"]
|
|
22
|
+
result = renderer.render_visible(lines, delimiter: nil)
|
|
23
|
+
assert_equal 1, result.length
|
|
24
|
+
assert_match(/\e\[1[;m]/, result[0]) # bold (standalone or combined)
|
|
25
|
+
assert_includes result[0], "# Hello"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def test_heading_h2
|
|
29
|
+
lines = ["## Section"]
|
|
30
|
+
result = renderer.render_visible(lines, delimiter: nil)
|
|
31
|
+
assert_includes result[0], "## Section"
|
|
32
|
+
assert_match(/\e\[1[;m]/, result[0]) # bold
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def test_heading_h3_to_h6
|
|
36
|
+
(3..6).each do |level|
|
|
37
|
+
hashes = "#" * level
|
|
38
|
+
lines = ["#{hashes} Title"]
|
|
39
|
+
result = renderer.render_visible(lines, delimiter: nil)
|
|
40
|
+
assert_includes result[0], "#{hashes} Title", "H#{level} text should be preserved"
|
|
41
|
+
assert_includes result[0], "\e[", "H#{level} should have ANSI styling"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# --- Inline elements ---
|
|
46
|
+
|
|
47
|
+
def test_inline_bold
|
|
48
|
+
lines = ["hello **bold** world"]
|
|
49
|
+
result = renderer.render_visible(lines, delimiter: nil)
|
|
50
|
+
assert_includes result[0], "\e[1m" # bold on
|
|
51
|
+
assert_includes result[0], "\e[22m" # bold off
|
|
52
|
+
assert_includes result[0], "**bold**"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def test_inline_italic
|
|
56
|
+
lines = ["hello *italic* world"]
|
|
57
|
+
result = renderer.render_visible(lines, delimiter: nil)
|
|
58
|
+
assert_includes result[0], "\e[3m" # italic on
|
|
59
|
+
assert_includes result[0], "\e[23m" # italic off
|
|
60
|
+
assert_includes result[0], "*italic*"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def test_inline_code
|
|
64
|
+
lines = ["use `foo()` here"]
|
|
65
|
+
result = renderer.render_visible(lines, delimiter: nil)
|
|
66
|
+
assert_includes result[0], "\e[33m" # yellow
|
|
67
|
+
assert_includes result[0], "`foo()`"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def test_inline_link
|
|
71
|
+
lines = ["click [here](http://example.com) now"]
|
|
72
|
+
result = renderer.render_visible(lines, delimiter: nil)
|
|
73
|
+
assert_includes result[0], "\e[4m" # underline for text
|
|
74
|
+
assert_includes result[0], "here"
|
|
75
|
+
assert_includes result[0], "http://example.com"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def test_checkbox_unchecked
|
|
79
|
+
lines = ["- [ ] todo item"]
|
|
80
|
+
result = renderer.render_visible(lines, delimiter: nil)
|
|
81
|
+
assert_includes result[0], "\e[90m" # dim
|
|
82
|
+
assert_includes result[0], "[ ]"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def test_checkbox_checked
|
|
86
|
+
lines = ["- [x] done item"]
|
|
87
|
+
result = renderer.render_visible(lines, delimiter: nil)
|
|
88
|
+
assert_includes result[0], "\e[32m" # green
|
|
89
|
+
assert_includes result[0], "[x]"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# --- Code blocks ---
|
|
93
|
+
|
|
94
|
+
def test_code_fence_styling
|
|
95
|
+
lines = ["```ruby", "puts 'hi'", "```"]
|
|
96
|
+
result = renderer.render_visible(lines, delimiter: nil)
|
|
97
|
+
# Fence lines should be dim
|
|
98
|
+
assert_includes result[0], "\e[90m"
|
|
99
|
+
# Content should be warm-colored
|
|
100
|
+
assert_includes result[1], "\e[38;5;223m"
|
|
101
|
+
# Closing fence should be dim
|
|
102
|
+
assert_includes result[2], "\e[90m"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def test_code_fence_tilde
|
|
106
|
+
lines = ["~~~", "code line", "~~~"]
|
|
107
|
+
result = renderer.render_visible(lines, delimiter: nil)
|
|
108
|
+
assert_includes result[0], "\e[90m"
|
|
109
|
+
assert_includes result[1], "\e[38;5;223m"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def test_code_block_context_from_pre_context
|
|
113
|
+
# Simulate rendering in the middle of a code block
|
|
114
|
+
# pre_context_lines should carry the open fence state
|
|
115
|
+
pre_context = ["```ruby", "line1"]
|
|
116
|
+
lines = ["line2", "```"]
|
|
117
|
+
result = renderer.render_visible(lines, delimiter: nil, context: { pre_context_lines: pre_context })
|
|
118
|
+
# line2 should be inside code block (warm color)
|
|
119
|
+
assert_includes result[0], "\e[38;5;223m"
|
|
120
|
+
# closing fence
|
|
121
|
+
assert_includes result[1], "\e[90m"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def test_needs_pre_context
|
|
125
|
+
assert renderer.needs_pre_context?
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# --- HR ---
|
|
129
|
+
|
|
130
|
+
def test_hr_dashes
|
|
131
|
+
lines = ["---"]
|
|
132
|
+
result = renderer.render_visible(lines, delimiter: nil)
|
|
133
|
+
assert_includes result[0], "\u2500" # box drawing horizontal
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def test_hr_asterisks
|
|
137
|
+
lines = ["***"]
|
|
138
|
+
result = renderer.render_visible(lines, delimiter: nil)
|
|
139
|
+
assert_includes result[0], "\u2500"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def test_hr_underscores
|
|
143
|
+
lines = ["___"]
|
|
144
|
+
result = renderer.render_visible(lines, delimiter: nil)
|
|
145
|
+
assert_includes result[0], "\u2500"
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# --- Block quotes ---
|
|
149
|
+
|
|
150
|
+
def test_block_quote
|
|
151
|
+
lines = ["> quoted text"]
|
|
152
|
+
result = renderer.render_visible(lines, delimiter: nil)
|
|
153
|
+
assert_includes result[0], "\e[36m" # cyan
|
|
154
|
+
assert_includes result[0], "> quoted text"
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# --- Tables ---
|
|
158
|
+
|
|
159
|
+
def test_table_basic
|
|
160
|
+
lines = [
|
|
161
|
+
"| Name | Age |",
|
|
162
|
+
"| ----- | --- |",
|
|
163
|
+
"| Alice | 30 |"
|
|
164
|
+
]
|
|
165
|
+
result = renderer.render_visible(lines, delimiter: nil)
|
|
166
|
+
assert_equal 3, result.length
|
|
167
|
+
# Data rows should use box-drawing vertical bar
|
|
168
|
+
assert_includes result[0], "\u2502" # │
|
|
169
|
+
assert_includes result[2], "\u2502"
|
|
170
|
+
# Separator row should use box-drawing
|
|
171
|
+
assert_includes result[1], "\u2500" # ─
|
|
172
|
+
assert_includes result[1], "\u253c" # ┼
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def test_table_column_alignment
|
|
176
|
+
lines = [
|
|
177
|
+
"| Short | Long column |",
|
|
178
|
+
"| ----- | ----------- |",
|
|
179
|
+
"| A | B |"
|
|
180
|
+
]
|
|
181
|
+
result = renderer.render_visible(lines, delimiter: nil)
|
|
182
|
+
# Both data rows should have same display width
|
|
183
|
+
w0 = display_width_without_ansi(result[0])
|
|
184
|
+
w2 = display_width_without_ansi(result[2])
|
|
185
|
+
assert_equal w0, w2
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# --- cursor_display_col ---
|
|
189
|
+
|
|
190
|
+
def test_cursor_display_col_non_table_line
|
|
191
|
+
# For non-table lines, should return screen_col_for_char_index
|
|
192
|
+
line = "hello world"
|
|
193
|
+
col = renderer.cursor_display_col(line, 5, visible_lines: [line], delimiter: nil)
|
|
194
|
+
expected = RuVim::TextMetrics.screen_col_for_char_index(line, 5)
|
|
195
|
+
assert_equal expected, col
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def test_cursor_display_col_table_line
|
|
199
|
+
lines = [
|
|
200
|
+
"| Name | Age |",
|
|
201
|
+
"| ----- | --- |",
|
|
202
|
+
"| Alice | 30 |"
|
|
203
|
+
]
|
|
204
|
+
# Cursor at start of "Alice" (index within raw line)
|
|
205
|
+
raw_line = lines[2]
|
|
206
|
+
col = renderer.cursor_display_col(raw_line, 2, visible_lines: lines, delimiter: nil)
|
|
207
|
+
# Should be > 0 (after the leading │ and padding)
|
|
208
|
+
assert col >= 0
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# --- ANSI support in render_rich_view_line_sc ---
|
|
212
|
+
|
|
213
|
+
def test_render_rich_view_line_sc_with_ansi
|
|
214
|
+
screen = create_test_screen
|
|
215
|
+
# Line with ANSI bold: "\e[1m" is 4 chars but 0 display width
|
|
216
|
+
text = "\e[1mhello\e[m world"
|
|
217
|
+
result = screen.send(:render_rich_view_line_sc, text, width: 20, skip_sc: 0)
|
|
218
|
+
# Should contain the ANSI sequences and the text
|
|
219
|
+
assert_includes result, "\e[1m"
|
|
220
|
+
assert_includes result, "hello"
|
|
221
|
+
assert_includes result, "world"
|
|
222
|
+
# Should end with reset
|
|
223
|
+
assert result.end_with?("\e[m") || result.include?("\e[m")
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def test_render_rich_view_line_sc_ansi_skip
|
|
227
|
+
screen = create_test_screen
|
|
228
|
+
# ANSI at start, skip some display columns
|
|
229
|
+
text = "\e[1mhello\e[m world"
|
|
230
|
+
result = screen.send(:render_rich_view_line_sc, text, width: 5, skip_sc: 3)
|
|
231
|
+
# Should show "lo wo" or similar (skipping 3 display cols of "hello")
|
|
232
|
+
assert_includes result, "lo"
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# --- Integration test ---
|
|
236
|
+
|
|
237
|
+
def test_markdown_rich_mode_integration
|
|
238
|
+
editor = fresh_editor
|
|
239
|
+
buf = editor.current_buffer
|
|
240
|
+
buf.replace_all_lines!(["# Title", "", "Some **bold** text"])
|
|
241
|
+
buf.options["filetype"] = "markdown"
|
|
242
|
+
|
|
243
|
+
RuVim::RichView.open!(editor, format: "markdown")
|
|
244
|
+
assert_equal :rich, editor.mode
|
|
245
|
+
state = editor.rich_state
|
|
246
|
+
assert_equal "markdown", state[:format]
|
|
247
|
+
|
|
248
|
+
# Render visible lines
|
|
249
|
+
lines = (0...buf.line_count).map { |i| buf.line_at(i) }
|
|
250
|
+
rendered = RuVim::RichView.render_visible_lines(editor, lines)
|
|
251
|
+
assert_equal 3, rendered.length
|
|
252
|
+
# Heading should have styling
|
|
253
|
+
assert_includes rendered[0], "\e["
|
|
254
|
+
|
|
255
|
+
RuVim::RichView.close!(editor)
|
|
256
|
+
assert_equal :normal, editor.mode
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def test_detect_format_markdown
|
|
260
|
+
editor = fresh_editor
|
|
261
|
+
buf = editor.current_buffer
|
|
262
|
+
buf.options["filetype"] = "markdown"
|
|
263
|
+
assert_equal "markdown", RuVim::RichView.detect_format(buf)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
private
|
|
267
|
+
|
|
268
|
+
def display_width_without_ansi(str)
|
|
269
|
+
# Strip ANSI escape sequences for width calculation
|
|
270
|
+
clean = str.gsub(/\e\[[0-9;]*m/, "")
|
|
271
|
+
RuVim::DisplayWidth.display_width(clean)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def create_test_screen
|
|
275
|
+
terminal = Object.new
|
|
276
|
+
def terminal.winsize; [24, 80]; end
|
|
277
|
+
RuVim::Screen.new(terminal: terminal)
|
|
278
|
+
end
|
|
279
|
+
end
|
|
@@ -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
|