ruvim 0.1.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 +7 -0
- data/.github/workflows/test.yml +15 -0
- data/README.md +135 -0
- data/Rakefile +36 -0
- data/docs/binding.md +125 -0
- data/docs/command.md +306 -0
- data/docs/config.md +155 -0
- data/docs/done.md +112 -0
- data/docs/plugin.md +559 -0
- data/docs/spec.md +655 -0
- data/docs/todo.md +63 -0
- data/docs/tutorial.md +490 -0
- data/docs/vim_diff.md +179 -0
- data/exe/ruvim +6 -0
- data/lib/ruvim/app.rb +1600 -0
- data/lib/ruvim/buffer.rb +421 -0
- data/lib/ruvim/cli.rb +264 -0
- data/lib/ruvim/clipboard.rb +73 -0
- data/lib/ruvim/command_invocation.rb +14 -0
- data/lib/ruvim/command_line.rb +63 -0
- data/lib/ruvim/command_registry.rb +38 -0
- data/lib/ruvim/config_dsl.rb +134 -0
- data/lib/ruvim/config_loader.rb +68 -0
- data/lib/ruvim/context.rb +26 -0
- data/lib/ruvim/dispatcher.rb +120 -0
- data/lib/ruvim/display_width.rb +110 -0
- data/lib/ruvim/editor.rb +1025 -0
- data/lib/ruvim/ex_command_registry.rb +80 -0
- data/lib/ruvim/global_commands.rb +1889 -0
- data/lib/ruvim/highlighter.rb +52 -0
- data/lib/ruvim/input.rb +66 -0
- data/lib/ruvim/keymap_manager.rb +96 -0
- data/lib/ruvim/screen.rb +452 -0
- data/lib/ruvim/terminal.rb +30 -0
- data/lib/ruvim/text_metrics.rb +96 -0
- data/lib/ruvim/version.rb +5 -0
- data/lib/ruvim/window.rb +71 -0
- data/lib/ruvim.rb +30 -0
- data/sig/ruvim.rbs +4 -0
- data/test/app_completion_test.rb +39 -0
- data/test/app_dot_repeat_test.rb +54 -0
- data/test/app_motion_test.rb +73 -0
- data/test/app_register_test.rb +47 -0
- data/test/app_scenario_test.rb +77 -0
- data/test/app_startup_test.rb +199 -0
- data/test/app_text_object_test.rb +54 -0
- data/test/app_unicode_behavior_test.rb +66 -0
- data/test/buffer_test.rb +72 -0
- data/test/cli_test.rb +165 -0
- data/test/config_dsl_test.rb +78 -0
- data/test/dispatcher_test.rb +124 -0
- data/test/editor_mark_test.rb +69 -0
- data/test/editor_register_test.rb +64 -0
- data/test/fixtures/render_basic_snapshot.txt +8 -0
- data/test/fixtures/render_basic_snapshot_nonumber.txt +8 -0
- data/test/fixtures/render_unicode_scrolled_snapshot.txt +7 -0
- data/test/highlighter_test.rb +16 -0
- data/test/input_screen_integration_test.rb +69 -0
- data/test/keymap_manager_test.rb +48 -0
- data/test/render_snapshot_test.rb +70 -0
- data/test/screen_test.rb +123 -0
- data/test/search_option_test.rb +39 -0
- data/test/test_helper.rb +15 -0
- data/test/text_metrics_test.rb +42 -0
- data/test/window_test.rb +21 -0
- metadata +106 -0
data/lib/ruvim/buffer.rb
ADDED
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
module RuVim
|
|
2
|
+
class Buffer
|
|
3
|
+
attr_reader :id, :kind, :name
|
|
4
|
+
attr_accessor :path
|
|
5
|
+
attr_reader :options
|
|
6
|
+
attr_writer :modified
|
|
7
|
+
|
|
8
|
+
def self.from_file(id:, path:)
|
|
9
|
+
lines =
|
|
10
|
+
if File.exist?(path)
|
|
11
|
+
data = decode_text(File.binread(path))
|
|
12
|
+
split_lines(data)
|
|
13
|
+
else
|
|
14
|
+
[""]
|
|
15
|
+
end
|
|
16
|
+
new(id:, path:, lines:)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.split_lines(data)
|
|
20
|
+
return [""] if data.empty?
|
|
21
|
+
|
|
22
|
+
lines = data.split("\n", -1)
|
|
23
|
+
if data.end_with?("\n")
|
|
24
|
+
lines.pop
|
|
25
|
+
end
|
|
26
|
+
lines = [""] if lines.empty?
|
|
27
|
+
lines
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.decode_text(bytes)
|
|
31
|
+
s = bytes.to_s.dup
|
|
32
|
+
return s if s.encoding == Encoding::UTF_8 && s.valid_encoding?
|
|
33
|
+
|
|
34
|
+
utf8 = s.dup.force_encoding(Encoding::UTF_8)
|
|
35
|
+
return utf8 if utf8.valid_encoding?
|
|
36
|
+
|
|
37
|
+
ext = Encoding.default_external
|
|
38
|
+
if ext && ext != Encoding::UTF_8
|
|
39
|
+
return s.dup.force_encoding(ext).encode(Encoding::UTF_8, invalid: :replace, undef: :replace)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
utf8.scrub
|
|
43
|
+
rescue StandardError
|
|
44
|
+
s.dup.force_encoding(Encoding::UTF_8).scrub
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def initialize(id:, path: nil, lines: [""], kind: :file, name: nil, readonly: false, modifiable: true)
|
|
48
|
+
@id = id
|
|
49
|
+
@path = path
|
|
50
|
+
@kind = kind.to_sym
|
|
51
|
+
@name = name
|
|
52
|
+
@lines = lines.dup
|
|
53
|
+
@lines = [""] if @lines.empty?
|
|
54
|
+
@modified = false
|
|
55
|
+
@readonly = !!readonly
|
|
56
|
+
@modifiable = !!modifiable
|
|
57
|
+
@undo_stack = []
|
|
58
|
+
@redo_stack = []
|
|
59
|
+
@change_group_depth = 0
|
|
60
|
+
@group_before_snapshot = nil
|
|
61
|
+
@group_changed = false
|
|
62
|
+
@recording_suspended = false
|
|
63
|
+
@options = {}
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def lines
|
|
67
|
+
@lines
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def modified?
|
|
71
|
+
!!@modified
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def readonly?
|
|
75
|
+
@readonly
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def readonly=(value)
|
|
79
|
+
@readonly = !!value
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def modifiable?
|
|
83
|
+
@modifiable
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def modifiable=(value)
|
|
87
|
+
@modifiable = !!value
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def file_buffer?
|
|
91
|
+
@kind == :file
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def intro_buffer?
|
|
95
|
+
@kind == :intro
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def virtual_buffer?
|
|
99
|
+
!file_buffer?
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def display_name
|
|
103
|
+
@name || @path || "[No Name]"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def configure_special!(kind:, name: nil, readonly: true, modifiable: false)
|
|
107
|
+
@kind = kind.to_sym
|
|
108
|
+
@name = name
|
|
109
|
+
@readonly = !!readonly
|
|
110
|
+
@modifiable = !!modifiable
|
|
111
|
+
self
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def become_normal_empty_buffer!
|
|
115
|
+
@kind = :file
|
|
116
|
+
@name = nil
|
|
117
|
+
@path = nil
|
|
118
|
+
@readonly = false
|
|
119
|
+
@modifiable = true
|
|
120
|
+
@lines = [""]
|
|
121
|
+
@modified = false
|
|
122
|
+
@undo_stack.clear
|
|
123
|
+
@redo_stack.clear
|
|
124
|
+
@change_group_depth = 0
|
|
125
|
+
@group_before_snapshot = nil
|
|
126
|
+
@group_changed = false
|
|
127
|
+
self
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def can_undo?
|
|
131
|
+
!@undo_stack.empty?
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def can_redo?
|
|
135
|
+
!@redo_stack.empty?
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def begin_change_group
|
|
139
|
+
@change_group_depth += 1
|
|
140
|
+
nil
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def end_change_group
|
|
144
|
+
return nil if @change_group_depth.zero?
|
|
145
|
+
|
|
146
|
+
@change_group_depth -= 1
|
|
147
|
+
return nil unless @change_group_depth.zero?
|
|
148
|
+
|
|
149
|
+
if @group_changed && @group_before_snapshot
|
|
150
|
+
@undo_stack << @group_before_snapshot
|
|
151
|
+
@redo_stack.clear
|
|
152
|
+
end
|
|
153
|
+
@group_before_snapshot = nil
|
|
154
|
+
@group_changed = false
|
|
155
|
+
nil
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def undo!
|
|
159
|
+
close_change_group_if_open
|
|
160
|
+
return false unless can_undo?
|
|
161
|
+
|
|
162
|
+
current = snapshot
|
|
163
|
+
prev = @undo_stack.pop
|
|
164
|
+
with_recording_suspended do
|
|
165
|
+
restore_snapshot(prev)
|
|
166
|
+
end
|
|
167
|
+
@redo_stack << current
|
|
168
|
+
true
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def redo!
|
|
172
|
+
close_change_group_if_open
|
|
173
|
+
return false unless can_redo?
|
|
174
|
+
|
|
175
|
+
current = snapshot
|
|
176
|
+
nxt = @redo_stack.pop
|
|
177
|
+
with_recording_suspended do
|
|
178
|
+
restore_snapshot(nxt)
|
|
179
|
+
end
|
|
180
|
+
@undo_stack << current
|
|
181
|
+
true
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def line_count
|
|
185
|
+
@lines.length
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def line_at(row)
|
|
189
|
+
@lines.fetch(row)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def line_length(row)
|
|
193
|
+
@lines.fetch(row).length
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def insert_char(row, col, char)
|
|
197
|
+
record_change_before_mutation!
|
|
198
|
+
line = @lines.fetch(row)
|
|
199
|
+
@lines[row] = line.dup.insert(col, char)
|
|
200
|
+
@modified = true
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def insert_text(row, col, text)
|
|
204
|
+
text.each_char do |ch|
|
|
205
|
+
if ch == "\n"
|
|
206
|
+
row, col = insert_newline(row, col)
|
|
207
|
+
else
|
|
208
|
+
insert_char(row, col, ch)
|
|
209
|
+
col += 1
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
[row, col]
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def insert_newline(row, col)
|
|
216
|
+
record_change_before_mutation!
|
|
217
|
+
line = @lines.fetch(row)
|
|
218
|
+
head = line[0...col]
|
|
219
|
+
tail = line[col..] || ""
|
|
220
|
+
@lines[row] = head
|
|
221
|
+
@lines.insert(row + 1, tail)
|
|
222
|
+
@modified = true
|
|
223
|
+
[row + 1, 0]
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def backspace(row, col)
|
|
227
|
+
if col > 0
|
|
228
|
+
record_change_before_mutation!
|
|
229
|
+
line = @lines.fetch(row)
|
|
230
|
+
@lines[row] = line[0...(col - 1)] + line[col..].to_s
|
|
231
|
+
@modified = true
|
|
232
|
+
return [row, col - 1]
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
return [row, col] if row.zero?
|
|
236
|
+
|
|
237
|
+
record_change_before_mutation!
|
|
238
|
+
prev = @lines.fetch(row - 1)
|
|
239
|
+
cur = @lines.fetch(row)
|
|
240
|
+
new_col = prev.length
|
|
241
|
+
@lines[row - 1] = prev + cur
|
|
242
|
+
@lines.delete_at(row)
|
|
243
|
+
@modified = true
|
|
244
|
+
[row - 1, new_col]
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def delete_char(row, col)
|
|
248
|
+
line = @lines.fetch(row)
|
|
249
|
+
if col < line.length
|
|
250
|
+
record_change_before_mutation!
|
|
251
|
+
@lines[row] = line[0...col] + line[(col + 1)..].to_s
|
|
252
|
+
@modified = true
|
|
253
|
+
return true
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
return false if row >= @lines.length - 1
|
|
257
|
+
|
|
258
|
+
record_change_before_mutation!
|
|
259
|
+
@lines[row] = line + @lines[row + 1]
|
|
260
|
+
@lines.delete_at(row + 1)
|
|
261
|
+
@modified = true
|
|
262
|
+
true
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def delete_line(row)
|
|
266
|
+
row = [[row, 0].max, @lines.length - 1].min
|
|
267
|
+
record_change_before_mutation!
|
|
268
|
+
deleted = @lines.delete_at(row)
|
|
269
|
+
@lines << "" if @lines.empty?
|
|
270
|
+
@modified = true
|
|
271
|
+
deleted
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def delete_span(start_row, start_col, end_row, end_col)
|
|
275
|
+
s_row, s_col, e_row, e_col = normalize_span(start_row, start_col, end_row, end_col)
|
|
276
|
+
return false if s_row == e_row && s_col == e_col
|
|
277
|
+
|
|
278
|
+
record_change_before_mutation!
|
|
279
|
+
|
|
280
|
+
if s_row == e_row
|
|
281
|
+
line = @lines.fetch(s_row)
|
|
282
|
+
@lines[s_row] = line[0...s_col] + line[e_col..].to_s
|
|
283
|
+
else
|
|
284
|
+
head = @lines.fetch(s_row)[0...s_col]
|
|
285
|
+
tail = @lines.fetch(e_row)[e_col..].to_s
|
|
286
|
+
@lines[s_row] = head + tail
|
|
287
|
+
(e_row - s_row).times { @lines.delete_at(s_row + 1) }
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
@lines << "" if @lines.empty?
|
|
291
|
+
@modified = true
|
|
292
|
+
true
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def span_text(start_row, start_col, end_row, end_col)
|
|
296
|
+
s_row, s_col, e_row, e_col = normalize_span(start_row, start_col, end_row, end_col)
|
|
297
|
+
return "" if s_row == e_row && s_col == e_col
|
|
298
|
+
|
|
299
|
+
if s_row == e_row
|
|
300
|
+
return @lines.fetch(s_row)[s_col...e_col].to_s
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
parts = []
|
|
304
|
+
parts << @lines.fetch(s_row)[s_col..].to_s
|
|
305
|
+
((s_row + 1)...e_row).each { |row| parts << @lines.fetch(row) }
|
|
306
|
+
parts << @lines.fetch(e_row)[0...e_col].to_s
|
|
307
|
+
parts.join("\n")
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def line_block_text(start_row, count = 1)
|
|
311
|
+
rows = @lines[start_row, count] || []
|
|
312
|
+
rows.join("\n") + "\n"
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def insert_lines_at(index, new_lines)
|
|
316
|
+
lines = Array(new_lines).map(&:to_s)
|
|
317
|
+
return if lines.empty?
|
|
318
|
+
|
|
319
|
+
record_change_before_mutation!
|
|
320
|
+
idx = [[index, 0].max, @lines.length].min
|
|
321
|
+
@lines.insert(idx, *lines)
|
|
322
|
+
@modified = true
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def replace_all_lines!(new_lines)
|
|
326
|
+
record_change_before_mutation!
|
|
327
|
+
@lines = Array(new_lines).map(&:dup)
|
|
328
|
+
@lines = [""] if @lines.empty?
|
|
329
|
+
@modified = true
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def write_to(path = nil)
|
|
333
|
+
raise RuVim::CommandError, "Buffer is readonly" if readonly?
|
|
334
|
+
|
|
335
|
+
target = path || @path
|
|
336
|
+
raise RuVim::CommandError, "No file name" if target.nil? || target.empty?
|
|
337
|
+
|
|
338
|
+
File.binwrite(target, @lines.join("\n"))
|
|
339
|
+
@path = target
|
|
340
|
+
@modified = false
|
|
341
|
+
target
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def reload_from_file!(path = nil)
|
|
345
|
+
target = path || @path
|
|
346
|
+
raise RuVim::CommandError, "No file name" if target.nil? || target.empty?
|
|
347
|
+
|
|
348
|
+
data = File.exist?(target) ? self.class.decode_text(File.binread(target)) : ""
|
|
349
|
+
@lines = self.class.split_lines(data)
|
|
350
|
+
@path = target
|
|
351
|
+
@modified = false
|
|
352
|
+
@undo_stack.clear
|
|
353
|
+
@redo_stack.clear
|
|
354
|
+
@change_group_depth = 0
|
|
355
|
+
@group_before_snapshot = nil
|
|
356
|
+
@group_changed = false
|
|
357
|
+
target
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
private
|
|
361
|
+
|
|
362
|
+
def normalize_span(start_row, start_col, end_row, end_col)
|
|
363
|
+
a_before_b = (start_row < end_row) || (start_row == end_row && start_col <= end_col)
|
|
364
|
+
s_row, s_col, e_row, e_col =
|
|
365
|
+
if a_before_b
|
|
366
|
+
[start_row, start_col, end_row, end_col]
|
|
367
|
+
else
|
|
368
|
+
[end_row, end_col, start_row, start_col]
|
|
369
|
+
end
|
|
370
|
+
[s_row, s_col, e_row, e_col]
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def close_change_group_if_open
|
|
374
|
+
return if @change_group_depth.zero?
|
|
375
|
+
|
|
376
|
+
@change_group_depth = 1
|
|
377
|
+
end_change_group
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
def record_change_before_mutation!
|
|
381
|
+
ensure_modifiable!
|
|
382
|
+
return if @recording_suspended
|
|
383
|
+
|
|
384
|
+
if @change_group_depth.positive?
|
|
385
|
+
unless @group_changed
|
|
386
|
+
@group_before_snapshot = snapshot
|
|
387
|
+
@group_changed = true
|
|
388
|
+
end
|
|
389
|
+
return
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
@undo_stack << snapshot
|
|
393
|
+
@redo_stack.clear
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
def ensure_modifiable!
|
|
397
|
+
raise RuVim::CommandError, "Buffer is not modifiable" unless modifiable?
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def snapshot
|
|
401
|
+
{
|
|
402
|
+
lines: @lines.map(&:dup),
|
|
403
|
+
modified: @modified
|
|
404
|
+
}
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def restore_snapshot(snap)
|
|
408
|
+
@lines = snap.fetch(:lines).map(&:dup)
|
|
409
|
+
@lines = [""] if @lines.empty?
|
|
410
|
+
@modified = snap.fetch(:modified)
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def with_recording_suspended
|
|
414
|
+
prev = @recording_suspended
|
|
415
|
+
@recording_suspended = true
|
|
416
|
+
yield
|
|
417
|
+
ensure
|
|
418
|
+
@recording_suspended = prev
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
end
|
data/lib/ruvim/cli.rb
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
module RuVim
|
|
2
|
+
class CLI
|
|
3
|
+
Options = Struct.new(
|
|
4
|
+
:files,
|
|
5
|
+
:pre_config_actions,
|
|
6
|
+
:startup_actions,
|
|
7
|
+
:clean,
|
|
8
|
+
:skip_user_config,
|
|
9
|
+
:config_path,
|
|
10
|
+
:readonly,
|
|
11
|
+
:diff_mode,
|
|
12
|
+
:quickfix_errorfile,
|
|
13
|
+
:session_file,
|
|
14
|
+
:no_swap,
|
|
15
|
+
:nomodifiable,
|
|
16
|
+
:restricted_mode,
|
|
17
|
+
:verbose_level,
|
|
18
|
+
:startup_time_path,
|
|
19
|
+
:startup_open_layout,
|
|
20
|
+
:startup_open_count,
|
|
21
|
+
:show_help,
|
|
22
|
+
:show_version,
|
|
23
|
+
keyword_init: true
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
class ParseError < RuVim::Error; end
|
|
27
|
+
|
|
28
|
+
def self.run(argv = ARGV, stdin: STDIN, stdout: STDOUT, stderr: STDERR)
|
|
29
|
+
opts = parse(argv)
|
|
30
|
+
|
|
31
|
+
if opts.show_help
|
|
32
|
+
stdout.write(help_text)
|
|
33
|
+
return 0
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
if opts.show_version
|
|
37
|
+
stdout.puts("RuVim #{RuVim::VERSION}")
|
|
38
|
+
return 0
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
if opts.files.length > 1 && opts.startup_open_layout.nil?
|
|
42
|
+
raise ParseError, "multiple files are not supported yet"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
if opts.config_path && !File.file?(opts.config_path)
|
|
46
|
+
raise ParseError, "config file not found: #{opts.config_path}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
app = RuVim::App.new(
|
|
50
|
+
path: opts.files.first,
|
|
51
|
+
paths: opts.files,
|
|
52
|
+
stdin: stdin,
|
|
53
|
+
stdout: stdout,
|
|
54
|
+
pre_config_actions: opts.pre_config_actions,
|
|
55
|
+
startup_actions: opts.startup_actions,
|
|
56
|
+
clean: opts.clean,
|
|
57
|
+
skip_user_config: opts.skip_user_config,
|
|
58
|
+
config_path: opts.config_path,
|
|
59
|
+
readonly: opts.readonly,
|
|
60
|
+
diff_mode: opts.diff_mode,
|
|
61
|
+
quickfix_errorfile: opts.quickfix_errorfile,
|
|
62
|
+
session_file: opts.session_file,
|
|
63
|
+
nomodifiable: opts.nomodifiable,
|
|
64
|
+
restricted: opts.restricted_mode,
|
|
65
|
+
verbose_level: opts.verbose_level,
|
|
66
|
+
verbose_io: stderr,
|
|
67
|
+
startup_time_path: opts.startup_time_path,
|
|
68
|
+
startup_open_layout: opts.startup_open_layout,
|
|
69
|
+
startup_open_count: opts.startup_open_count
|
|
70
|
+
)
|
|
71
|
+
app.run
|
|
72
|
+
0
|
|
73
|
+
rescue ParseError => e
|
|
74
|
+
stderr.puts("ruvim: #{e.message}")
|
|
75
|
+
stderr.puts("Try 'ruvim --help'.")
|
|
76
|
+
2
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def self.parse(argv)
|
|
80
|
+
args = Array(argv).dup
|
|
81
|
+
opts = Options.new(
|
|
82
|
+
files: [],
|
|
83
|
+
pre_config_actions: [],
|
|
84
|
+
startup_actions: [],
|
|
85
|
+
clean: false,
|
|
86
|
+
skip_user_config: false,
|
|
87
|
+
config_path: nil,
|
|
88
|
+
readonly: false,
|
|
89
|
+
diff_mode: false,
|
|
90
|
+
quickfix_errorfile: nil,
|
|
91
|
+
session_file: nil,
|
|
92
|
+
no_swap: false,
|
|
93
|
+
nomodifiable: false,
|
|
94
|
+
restricted_mode: false,
|
|
95
|
+
verbose_level: 0,
|
|
96
|
+
startup_time_path: nil,
|
|
97
|
+
startup_open_layout: nil,
|
|
98
|
+
startup_open_count: nil,
|
|
99
|
+
show_help: false,
|
|
100
|
+
show_version: false
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
i = 0
|
|
104
|
+
stop_options = false
|
|
105
|
+
while i < args.length
|
|
106
|
+
arg = args[i]
|
|
107
|
+
if stop_options
|
|
108
|
+
opts.files << arg
|
|
109
|
+
i += 1
|
|
110
|
+
next
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
case arg
|
|
114
|
+
when "--"
|
|
115
|
+
stop_options = true
|
|
116
|
+
when "--help", "-h"
|
|
117
|
+
opts.show_help = true
|
|
118
|
+
when "--version", "-v"
|
|
119
|
+
opts.show_version = true
|
|
120
|
+
when "--verbose"
|
|
121
|
+
opts.verbose_level = [opts.verbose_level.to_i, 1].max
|
|
122
|
+
when /\A--verbose=(\d+)\z/
|
|
123
|
+
opts.verbose_level = Regexp.last_match(1).to_i
|
|
124
|
+
when "--startuptime"
|
|
125
|
+
i += 1
|
|
126
|
+
raise ParseError, "--startuptime requires a file path" if i >= args.length
|
|
127
|
+
opts.startup_time_path = args[i]
|
|
128
|
+
when "--cmd"
|
|
129
|
+
i += 1
|
|
130
|
+
raise ParseError, "--cmd requires an argument" if i >= args.length
|
|
131
|
+
opts.pre_config_actions << { type: :ex, value: args[i] }
|
|
132
|
+
when /\A--cmd=(.+)\z/
|
|
133
|
+
opts.pre_config_actions << { type: :ex, value: Regexp.last_match(1) }
|
|
134
|
+
when "--clean"
|
|
135
|
+
opts.clean = true
|
|
136
|
+
when "-R"
|
|
137
|
+
opts.readonly = true
|
|
138
|
+
when "-d"
|
|
139
|
+
opts.diff_mode = true
|
|
140
|
+
when "-q"
|
|
141
|
+
i += 1
|
|
142
|
+
raise ParseError, "-q requires an errorfile path" if i >= args.length
|
|
143
|
+
opts.quickfix_errorfile = args[i]
|
|
144
|
+
when "-S"
|
|
145
|
+
if i + 1 < args.length && !args[i + 1].start_with?("-")
|
|
146
|
+
i += 1
|
|
147
|
+
opts.session_file = args[i]
|
|
148
|
+
else
|
|
149
|
+
opts.session_file = "Session.vim"
|
|
150
|
+
end
|
|
151
|
+
when "-n"
|
|
152
|
+
opts.no_swap = true
|
|
153
|
+
when "-M"
|
|
154
|
+
opts.nomodifiable = true
|
|
155
|
+
when "-Z"
|
|
156
|
+
opts.restricted_mode = true
|
|
157
|
+
when "-V"
|
|
158
|
+
opts.verbose_level = [opts.verbose_level.to_i, 1].max
|
|
159
|
+
when /\A-V(\d+)\z/
|
|
160
|
+
opts.verbose_level = Regexp.last_match(1).to_i
|
|
161
|
+
when "-o", "-O", "-p"
|
|
162
|
+
apply_layout_option(opts, arg, nil)
|
|
163
|
+
when /\A-(o|O|p)(\d+)\z/
|
|
164
|
+
apply_layout_option(opts, Regexp.last_match(1), Regexp.last_match(2).to_i)
|
|
165
|
+
when "-u"
|
|
166
|
+
i += 1
|
|
167
|
+
raise ParseError, "-u requires an argument" if i >= args.length
|
|
168
|
+
apply_u_option(opts, args[i])
|
|
169
|
+
when /\A-u(.+)\z/
|
|
170
|
+
apply_u_option(opts, Regexp.last_match(1))
|
|
171
|
+
when "-c"
|
|
172
|
+
i += 1
|
|
173
|
+
raise ParseError, "-c requires an argument" if i >= args.length
|
|
174
|
+
opts.startup_actions << { type: :ex, value: args[i] }
|
|
175
|
+
when /\A\+/
|
|
176
|
+
apply_plus_option(opts, arg)
|
|
177
|
+
else
|
|
178
|
+
if arg.start_with?("-")
|
|
179
|
+
raise ParseError, "unknown option: #{arg}"
|
|
180
|
+
end
|
|
181
|
+
opts.files << arg
|
|
182
|
+
end
|
|
183
|
+
i += 1
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
opts
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def self.apply_u_option(opts, value)
|
|
190
|
+
if value == "NONE"
|
|
191
|
+
opts.skip_user_config = true
|
|
192
|
+
opts.config_path = nil
|
|
193
|
+
else
|
|
194
|
+
opts.skip_user_config = false
|
|
195
|
+
opts.config_path = value
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
private_class_method :apply_u_option
|
|
199
|
+
|
|
200
|
+
def self.apply_plus_option(opts, arg)
|
|
201
|
+
rest = arg[1..].to_s
|
|
202
|
+
if rest.empty?
|
|
203
|
+
opts.startup_actions << { type: :line_end }
|
|
204
|
+
elsif rest.match?(/\A\d+\z/)
|
|
205
|
+
opts.startup_actions << { type: :line, value: rest.to_i }
|
|
206
|
+
else
|
|
207
|
+
opts.startup_actions << { type: :ex, value: rest }
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
private_class_method :apply_plus_option
|
|
211
|
+
|
|
212
|
+
def self.apply_layout_option(opts, token, count)
|
|
213
|
+
layout =
|
|
214
|
+
case token
|
|
215
|
+
when "-o", "o" then :horizontal
|
|
216
|
+
when "-O", "O" then :vertical
|
|
217
|
+
when "-p", "p" then :tab
|
|
218
|
+
else
|
|
219
|
+
raise ParseError, "unknown layout option: #{token}"
|
|
220
|
+
end
|
|
221
|
+
opts.startup_open_layout = layout
|
|
222
|
+
opts.startup_open_count = count
|
|
223
|
+
end
|
|
224
|
+
private_class_method :apply_layout_option
|
|
225
|
+
|
|
226
|
+
def self.help_text
|
|
227
|
+
<<~TXT
|
|
228
|
+
Usage: ruvim [options] [file]
|
|
229
|
+
|
|
230
|
+
Options:
|
|
231
|
+
-h, --help Show this help
|
|
232
|
+
-v, --version Show version
|
|
233
|
+
--clean Start without user config and ftplugin
|
|
234
|
+
-R Open file readonly (disallow :w on current buffer)
|
|
235
|
+
-d Diff mode requested (compat placeholder; not implemented yet)
|
|
236
|
+
-q {errorfile} Quickfix startup placeholder (not implemented yet)
|
|
237
|
+
-S [session] Session startup placeholder (not implemented yet)
|
|
238
|
+
-M Open file unmodifiable (disallow editing; also readonly)
|
|
239
|
+
-Z Restricted mode (skip config/ftplugin, disable :ruby)
|
|
240
|
+
-V[N], --verbose[=N]
|
|
241
|
+
Verbose startup/config/command logs to stderr
|
|
242
|
+
--startuptime FILE
|
|
243
|
+
Write startup timing log
|
|
244
|
+
--cmd {cmd} Execute Ex command before loading user config
|
|
245
|
+
-n No-op (reserved for swap/persistent features compatibility)
|
|
246
|
+
-o[N] Open files in horizontal splits
|
|
247
|
+
-O[N] Open files in vertical splits
|
|
248
|
+
-p[N] Open files in tabs
|
|
249
|
+
-u {path|NONE} Use config file path, or disable user config with NONE
|
|
250
|
+
-c {cmd} Execute Ex command after startup
|
|
251
|
+
+{cmd} Execute Ex command after startup
|
|
252
|
+
+{line} Move cursor to line after startup
|
|
253
|
+
+ Move cursor to last line after startup
|
|
254
|
+
|
|
255
|
+
Examples:
|
|
256
|
+
ruvim file.txt
|
|
257
|
+
ruvim +10 file.txt
|
|
258
|
+
ruvim --cmd 'set number' file.txt
|
|
259
|
+
ruvim -c 'set number' file.txt
|
|
260
|
+
ruvim --clean -u NONE
|
|
261
|
+
TXT
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|