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
data/lib/ruvim/buffer.rb
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module RuVim
|
|
2
4
|
class Buffer
|
|
3
5
|
attr_reader :id, :kind, :name
|
|
4
6
|
attr_accessor :path
|
|
5
7
|
attr_reader :options
|
|
6
8
|
attr_writer :modified
|
|
9
|
+
attr_accessor :stream_state, :loading_state
|
|
10
|
+
attr_accessor :lang_module
|
|
7
11
|
|
|
8
12
|
def self.from_file(id:, path:)
|
|
9
13
|
lines =
|
|
@@ -54,12 +58,15 @@ module RuVim
|
|
|
54
58
|
@modified = false
|
|
55
59
|
@readonly = !!readonly
|
|
56
60
|
@modifiable = !!modifiable
|
|
61
|
+
@stream_state = nil
|
|
62
|
+
@loading_state = nil
|
|
57
63
|
@undo_stack = []
|
|
58
64
|
@redo_stack = []
|
|
59
65
|
@change_group_depth = 0
|
|
60
66
|
@group_before_snapshot = nil
|
|
61
67
|
@group_changed = false
|
|
62
68
|
@recording_suspended = false
|
|
69
|
+
@lang_module = Lang::Base
|
|
63
70
|
@options = {}
|
|
64
71
|
end
|
|
65
72
|
|
|
@@ -80,7 +87,7 @@ module RuVim
|
|
|
80
87
|
end
|
|
81
88
|
|
|
82
89
|
def modifiable?
|
|
83
|
-
@modifiable
|
|
90
|
+
@modifiable && @loading_state != :live
|
|
84
91
|
end
|
|
85
92
|
|
|
86
93
|
def modifiable=(value)
|
|
@@ -108,6 +115,8 @@ module RuVim
|
|
|
108
115
|
@name = name
|
|
109
116
|
@readonly = !!readonly
|
|
110
117
|
@modifiable = !!modifiable
|
|
118
|
+
@stream_state = nil unless @kind == :stream
|
|
119
|
+
@loading_state = nil
|
|
111
120
|
self
|
|
112
121
|
end
|
|
113
122
|
|
|
@@ -117,6 +126,8 @@ module RuVim
|
|
|
117
126
|
@path = nil
|
|
118
127
|
@readonly = false
|
|
119
128
|
@modifiable = true
|
|
129
|
+
@stream_state = nil
|
|
130
|
+
@loading_state = nil
|
|
120
131
|
@lines = [""]
|
|
121
132
|
@modified = false
|
|
122
133
|
@undo_stack.clear
|
|
@@ -329,6 +340,29 @@ module RuVim
|
|
|
329
340
|
@modified = true
|
|
330
341
|
end
|
|
331
342
|
|
|
343
|
+
# Append externally-streamed text without touching undo history or modifiable state.
|
|
344
|
+
def append_stream_text!(text)
|
|
345
|
+
chunk = text.to_s
|
|
346
|
+
return [@lines.length - 1, @lines[-1].length] if chunk.empty?
|
|
347
|
+
|
|
348
|
+
parts = chunk.split("\n", -1)
|
|
349
|
+
head = parts.shift || ""
|
|
350
|
+
@lines[-1] = @lines[-1].to_s + head
|
|
351
|
+
@lines.concat(parts)
|
|
352
|
+
@lines = [""] if @lines.empty?
|
|
353
|
+
@modified = false
|
|
354
|
+
[@lines.length - 1, @lines[-1].length]
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def finalize_async_file_load!(ended_with_newline:)
|
|
358
|
+
if ended_with_newline && @lines.length > 1 && @lines[-1] == ""
|
|
359
|
+
@lines.pop
|
|
360
|
+
end
|
|
361
|
+
@lines = [""] if @lines.empty?
|
|
362
|
+
@modified = false
|
|
363
|
+
self
|
|
364
|
+
end
|
|
365
|
+
|
|
332
366
|
def write_to(path = nil)
|
|
333
367
|
raise RuVim::CommandError, "Buffer is readonly" if readonly?
|
|
334
368
|
|
data/lib/ruvim/cli.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module RuVim
|
|
2
4
|
class CLI
|
|
3
5
|
Options = Struct.new(
|
|
@@ -38,18 +40,25 @@ module RuVim
|
|
|
38
40
|
return 0
|
|
39
41
|
end
|
|
40
42
|
|
|
41
|
-
if opts.files.length > 1 && opts.startup_open_layout.nil?
|
|
42
|
-
raise ParseError, "multiple files are not supported yet"
|
|
43
|
-
end
|
|
44
43
|
|
|
45
44
|
if opts.config_path && !File.file?(opts.config_path)
|
|
46
45
|
raise ParseError, "config file not found: #{opts.config_path}"
|
|
47
46
|
end
|
|
48
47
|
|
|
48
|
+
ui_stdin = stdin
|
|
49
|
+
stdin_stream_mode = false
|
|
50
|
+
if stdin.respond_to?(:tty?) && !stdin.tty?
|
|
51
|
+
ui_stdin = IO.console
|
|
52
|
+
raise ParseError, "no controlling terminal available for interactive UI" unless ui_stdin
|
|
53
|
+
stdin_stream_mode = opts.files.empty?
|
|
54
|
+
end
|
|
55
|
+
|
|
49
56
|
app = RuVim::App.new(
|
|
50
57
|
path: opts.files.first,
|
|
51
58
|
paths: opts.files,
|
|
52
59
|
stdin: stdin,
|
|
60
|
+
ui_stdin: ui_stdin,
|
|
61
|
+
stdin_stream_mode: stdin_stream_mode,
|
|
53
62
|
stdout: stdout,
|
|
54
63
|
pre_config_actions: opts.pre_config_actions,
|
|
55
64
|
startup_actions: opts.startup_actions,
|
data/lib/ruvim/clipboard.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module RuVim
|
|
2
4
|
class CommandInvocation
|
|
3
5
|
attr_accessor :id, :argv, :kwargs, :count, :bang, :raw_keys
|
|
@@ -6,7 +8,7 @@ module RuVim
|
|
|
6
8
|
@id = id
|
|
7
9
|
@argv = argv || []
|
|
8
10
|
@kwargs = kwargs || {}
|
|
9
|
-
@count = count
|
|
11
|
+
@count = count
|
|
10
12
|
@bang = !!bang
|
|
11
13
|
@raw_keys = raw_keys
|
|
12
14
|
end
|
data/lib/ruvim/command_line.rb
CHANGED
data/lib/ruvim/config_dsl.rb
CHANGED
data/lib/ruvim/config_loader.rb
CHANGED
data/lib/ruvim/context.rb
CHANGED
data/lib/ruvim/dispatcher.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require "shellwords"
|
|
2
4
|
|
|
3
5
|
module RuVim
|
|
@@ -29,26 +31,40 @@ module RuVim
|
|
|
29
31
|
return
|
|
30
32
|
end
|
|
31
33
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
# Parse range prefix
|
|
35
|
+
range_result = parse_range(raw, editor)
|
|
36
|
+
rest = range_result ? range_result[:rest] : raw
|
|
37
|
+
|
|
38
|
+
# Try substitute on rest
|
|
39
|
+
if (sub = parse_substitute(rest))
|
|
40
|
+
kwargs = sub.merge(
|
|
41
|
+
range_start: range_result&.dig(:range_start),
|
|
42
|
+
range_end: range_result&.dig(:range_end)
|
|
43
|
+
)
|
|
44
|
+
invocation = CommandInvocation.new(id: "__substitute__", kwargs:)
|
|
34
45
|
ctx = Context.new(editor:, invocation:)
|
|
35
|
-
@command_host.ex_substitute(ctx, **
|
|
46
|
+
@command_host.ex_substitute(ctx, **kwargs)
|
|
36
47
|
editor.enter_normal_mode
|
|
37
48
|
return
|
|
38
49
|
end
|
|
39
50
|
|
|
40
|
-
parsed = parse_ex(
|
|
51
|
+
parsed = parse_ex(rest)
|
|
41
52
|
return if parsed.nil?
|
|
42
53
|
|
|
43
54
|
spec = @ex_registry.fetch(parsed.name)
|
|
44
55
|
validate_ex_args!(spec, parsed.argv, parsed.bang)
|
|
45
56
|
invocation = CommandInvocation.new(id: spec.name, argv: parsed.argv, bang: parsed.bang)
|
|
46
57
|
ctx = Context.new(editor:, invocation:)
|
|
47
|
-
|
|
58
|
+
range_kwargs = {}
|
|
59
|
+
if range_result
|
|
60
|
+
range_kwargs[:range_start] = range_result[:range_start]
|
|
61
|
+
range_kwargs[:range_end] = range_result[:range_end]
|
|
62
|
+
end
|
|
63
|
+
@command_host.call(spec.call, ctx, argv: parsed.argv, bang: parsed.bang, count: 1, kwargs: range_kwargs)
|
|
48
64
|
rescue StandardError => e
|
|
49
65
|
editor.echo_error("Error: #{e.message}")
|
|
50
66
|
ensure
|
|
51
|
-
editor.
|
|
67
|
+
editor.leave_command_line if editor.mode == :command_line
|
|
52
68
|
end
|
|
53
69
|
|
|
54
70
|
def parse_ex(line)
|
|
@@ -66,28 +82,142 @@ module RuVim
|
|
|
66
82
|
raise RuVim::CommandError, "Parse error: #{e.message}"
|
|
67
83
|
end
|
|
68
84
|
|
|
69
|
-
|
|
85
|
+
# Parse a substitute command: s/pat/repl/flags
|
|
86
|
+
# Returns {pattern:, replacement:, flags_str:} or nil
|
|
87
|
+
def parse_substitute(line)
|
|
70
88
|
raw = line.to_s.strip
|
|
71
|
-
return nil unless raw.
|
|
72
|
-
return nil if raw.length <
|
|
89
|
+
return nil unless raw.match?(/\As[^a-zA-Z]/)
|
|
90
|
+
return nil if raw.length < 2
|
|
73
91
|
|
|
74
|
-
delim = raw[
|
|
92
|
+
delim = raw[1]
|
|
75
93
|
return nil if delim.nil? || delim =~ /\s/
|
|
76
|
-
i =
|
|
94
|
+
i = 2
|
|
77
95
|
pat, i = parse_delimited_segment(raw, i, delim)
|
|
78
96
|
return nil unless pat
|
|
79
97
|
rep, i = parse_delimited_segment(raw, i, delim)
|
|
80
98
|
return nil unless rep
|
|
81
|
-
|
|
99
|
+
flags_str = raw[i..].to_s
|
|
82
100
|
{
|
|
83
101
|
pattern: pat,
|
|
84
102
|
replacement: rep,
|
|
85
|
-
|
|
103
|
+
flags_str: flags_str
|
|
86
104
|
}
|
|
87
105
|
rescue StandardError
|
|
88
106
|
nil
|
|
89
107
|
end
|
|
90
108
|
|
|
109
|
+
# Parse an address at position pos in str.
|
|
110
|
+
# Returns [resolved_line_number, new_pos] or nil.
|
|
111
|
+
def parse_address(str, pos, editor)
|
|
112
|
+
return nil if pos >= str.length
|
|
113
|
+
|
|
114
|
+
ch = str[pos]
|
|
115
|
+
base = nil
|
|
116
|
+
new_pos = pos
|
|
117
|
+
|
|
118
|
+
case ch
|
|
119
|
+
when /\d/
|
|
120
|
+
# Numeric address
|
|
121
|
+
m = str[pos..].match(/\A(\d+)/)
|
|
122
|
+
return nil unless m
|
|
123
|
+
base = m[1].to_i - 1 # convert 1-based to 0-based
|
|
124
|
+
new_pos = pos + m[0].length
|
|
125
|
+
when "."
|
|
126
|
+
base = editor.current_window.cursor_y
|
|
127
|
+
new_pos = pos + 1
|
|
128
|
+
when "$"
|
|
129
|
+
base = editor.current_buffer.line_count - 1
|
|
130
|
+
new_pos = pos + 1
|
|
131
|
+
when "'"
|
|
132
|
+
# Mark address
|
|
133
|
+
mark_ch = str[pos + 1]
|
|
134
|
+
return nil unless mark_ch
|
|
135
|
+
if mark_ch == "<" || mark_ch == ">"
|
|
136
|
+
sel = editor.visual_selection
|
|
137
|
+
if sel
|
|
138
|
+
base = mark_ch == "<" ? sel[:start_row] : sel[:end_row]
|
|
139
|
+
else
|
|
140
|
+
return nil
|
|
141
|
+
end
|
|
142
|
+
else
|
|
143
|
+
loc = editor.mark_location(mark_ch)
|
|
144
|
+
return nil unless loc
|
|
145
|
+
base = loc[:row]
|
|
146
|
+
end
|
|
147
|
+
new_pos = pos + 2
|
|
148
|
+
when "+", "-"
|
|
149
|
+
# Relative offset with implicit current line
|
|
150
|
+
base = editor.current_window.cursor_y
|
|
151
|
+
# Don't advance new_pos — the offset parsing below will handle +/-
|
|
152
|
+
else
|
|
153
|
+
return nil
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Parse trailing +N / -N offsets
|
|
157
|
+
while new_pos < str.length
|
|
158
|
+
offset_ch = str[new_pos]
|
|
159
|
+
if offset_ch == "+"
|
|
160
|
+
m = str[new_pos + 1..].to_s.match(/\A(\d+)/)
|
|
161
|
+
if m
|
|
162
|
+
base += m[1].to_i
|
|
163
|
+
new_pos += 1 + m[0].length
|
|
164
|
+
else
|
|
165
|
+
base += 1
|
|
166
|
+
new_pos += 1
|
|
167
|
+
end
|
|
168
|
+
elsif offset_ch == "-"
|
|
169
|
+
m = str[new_pos + 1..].to_s.match(/\A(\d+)/)
|
|
170
|
+
if m
|
|
171
|
+
base -= m[1].to_i
|
|
172
|
+
new_pos += 1 + m[0].length
|
|
173
|
+
else
|
|
174
|
+
base -= 1
|
|
175
|
+
new_pos += 1
|
|
176
|
+
end
|
|
177
|
+
else
|
|
178
|
+
break
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Clamp to valid range
|
|
183
|
+
max_line = editor.current_buffer.line_count - 1
|
|
184
|
+
base = [[base, 0].max, max_line].min
|
|
185
|
+
|
|
186
|
+
[base, new_pos]
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Parse a range from the beginning of raw.
|
|
190
|
+
# Returns {range_start:, range_end:, rest:} or nil.
|
|
191
|
+
def parse_range(raw, editor)
|
|
192
|
+
str = raw.to_s
|
|
193
|
+
return nil if str.empty?
|
|
194
|
+
|
|
195
|
+
# % = whole file
|
|
196
|
+
if str[0] == "%"
|
|
197
|
+
max_line = editor.current_buffer.line_count - 1
|
|
198
|
+
rest = str[1..].to_s
|
|
199
|
+
return { range_start: 0, range_end: max_line, rest: rest }
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Try first address
|
|
203
|
+
addr1 = parse_address(str, 0, editor)
|
|
204
|
+
return nil unless addr1
|
|
205
|
+
|
|
206
|
+
line1, pos = addr1
|
|
207
|
+
|
|
208
|
+
if pos < str.length && str[pos] == ","
|
|
209
|
+
# addr,addr range
|
|
210
|
+
addr2 = parse_address(str, pos + 1, editor)
|
|
211
|
+
if addr2
|
|
212
|
+
line2, pos2 = addr2
|
|
213
|
+
return { range_start: line1, range_end: line2, rest: str[pos2..].to_s }
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Single address
|
|
218
|
+
{ range_start: line1, range_end: line1, rest: str[pos..].to_s }
|
|
219
|
+
end
|
|
220
|
+
|
|
91
221
|
private
|
|
92
222
|
|
|
93
223
|
def parse_delimited_segment(str, idx, delim)
|
data/lib/ruvim/display_width.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module RuVim
|
|
2
4
|
module DisplayWidth
|
|
3
5
|
module_function
|
|
@@ -11,6 +13,7 @@ module RuVim
|
|
|
11
13
|
end
|
|
12
14
|
|
|
13
15
|
code = ch.ord
|
|
16
|
+
return 1 if code <= 0xA0 && !code.zero?
|
|
14
17
|
return cached_codepoint_width(code) if codepoint_cacheable?(code)
|
|
15
18
|
|
|
16
19
|
uncached_codepoint_width(code)
|