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
|
@@ -0,0 +1,1889 @@
|
|
|
1
|
+
module RuVim
|
|
2
|
+
class GlobalCommands
|
|
3
|
+
include Singleton
|
|
4
|
+
|
|
5
|
+
def call(spec_call, ctx, argv: [], kwargs: {}, bang: false, count: 1)
|
|
6
|
+
case spec_call
|
|
7
|
+
when Symbol, String
|
|
8
|
+
public_send(spec_call.to_sym, ctx, argv: argv, kwargs: kwargs, bang: bang, count: count)
|
|
9
|
+
else
|
|
10
|
+
spec_call.call(ctx, argv: argv, kwargs: kwargs, bang: bang, count: count)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def cursor_left(ctx, count:, **)
|
|
15
|
+
ctx.window.move_left(ctx.buffer, count)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def cursor_right(ctx, count:, **)
|
|
19
|
+
ctx.window.move_right(ctx.buffer, count)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def cursor_up(ctx, count:, **)
|
|
23
|
+
ctx.window.move_up(ctx.buffer, count)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def cursor_down(ctx, count:, **)
|
|
27
|
+
ctx.window.move_down(ctx.buffer, count)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def cursor_page_up(ctx, kwargs:, count:, **)
|
|
31
|
+
page_lines = [(kwargs[:page_lines] || kwargs["page_lines"] || 1).to_i, 1].max
|
|
32
|
+
ctx.window.move_up(ctx.buffer, page_lines * [count.to_i, 1].max)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def cursor_page_down(ctx, kwargs:, count:, **)
|
|
36
|
+
page_lines = [(kwargs[:page_lines] || kwargs["page_lines"] || 1).to_i, 1].max
|
|
37
|
+
ctx.window.move_down(ctx.buffer, page_lines * [count.to_i, 1].max)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def cursor_line_start(ctx, **)
|
|
41
|
+
ctx.window.cursor_x = 0
|
|
42
|
+
ctx.window.clamp_to_buffer(ctx.buffer)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def cursor_line_end(ctx, **)
|
|
46
|
+
ctx.window.cursor_x = ctx.buffer.line_length(ctx.window.cursor_y)
|
|
47
|
+
ctx.window.clamp_to_buffer(ctx.buffer)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def cursor_first_nonblank(ctx, **)
|
|
51
|
+
line = ctx.buffer.line_at(ctx.window.cursor_y)
|
|
52
|
+
idx = line.index(/\S/) || 0
|
|
53
|
+
ctx.window.cursor_x = idx
|
|
54
|
+
ctx.window.clamp_to_buffer(ctx.buffer)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def cursor_buffer_start(ctx, count:, **)
|
|
58
|
+
record_jump(ctx)
|
|
59
|
+
target_row = [count.to_i - 1, 0].max
|
|
60
|
+
target_row = [target_row, ctx.buffer.line_count - 1].min
|
|
61
|
+
ctx.window.cursor_y = target_row
|
|
62
|
+
cursor_first_nonblank(ctx)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def cursor_buffer_end(ctx, count:, **)
|
|
66
|
+
record_jump(ctx)
|
|
67
|
+
if count && count > 1
|
|
68
|
+
target_row = [count - 1, ctx.buffer.line_count - 1].min
|
|
69
|
+
else
|
|
70
|
+
target_row = ctx.buffer.line_count - 1
|
|
71
|
+
end
|
|
72
|
+
ctx.window.cursor_y = target_row
|
|
73
|
+
cursor_first_nonblank(ctx)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def cursor_word_forward(ctx, count:, **)
|
|
77
|
+
move_cursor_word(ctx, count:, kind: :forward_start)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def cursor_word_backward(ctx, count:, **)
|
|
81
|
+
move_cursor_word(ctx, count:, kind: :backward_start)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def cursor_word_end(ctx, count:, **)
|
|
85
|
+
move_cursor_word(ctx, count:, kind: :forward_end)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def cursor_match_bracket(ctx, **)
|
|
89
|
+
line = ctx.buffer.line_at(ctx.window.cursor_y)
|
|
90
|
+
ch = line[ctx.window.cursor_x]
|
|
91
|
+
unless ch
|
|
92
|
+
ctx.editor.echo_error("No bracket under cursor")
|
|
93
|
+
return
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
pair_map = {
|
|
97
|
+
"(" => [")", :forward],
|
|
98
|
+
"[" => ["]", :forward],
|
|
99
|
+
"{" => ["}", :forward],
|
|
100
|
+
")" => ["(", :backward],
|
|
101
|
+
"]" => ["[", :backward],
|
|
102
|
+
"}" => ["{", :backward]
|
|
103
|
+
}
|
|
104
|
+
pair = pair_map[ch]
|
|
105
|
+
unless pair
|
|
106
|
+
ctx.editor.echo_error("No bracket under cursor")
|
|
107
|
+
return
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
target_char, direction = pair
|
|
111
|
+
record_jump(ctx)
|
|
112
|
+
loc = find_matching_bracket(ctx.buffer, ctx.window.cursor_y, ctx.window.cursor_x, ch, target_char, direction)
|
|
113
|
+
if loc
|
|
114
|
+
ctx.window.cursor_y = loc[:row]
|
|
115
|
+
ctx.window.cursor_x = loc[:col]
|
|
116
|
+
ctx.window.clamp_to_buffer(ctx.buffer)
|
|
117
|
+
else
|
|
118
|
+
ctx.editor.echo_error("Match not found")
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def enter_insert_mode(ctx, **)
|
|
123
|
+
materialize_intro_buffer_if_needed(ctx)
|
|
124
|
+
ctx.buffer.begin_change_group
|
|
125
|
+
ctx.editor.enter_insert_mode
|
|
126
|
+
ctx.editor.echo("-- INSERT --")
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def append_mode(ctx, **)
|
|
130
|
+
x = ctx.window.cursor_x
|
|
131
|
+
len = ctx.buffer.line_length(ctx.window.cursor_y)
|
|
132
|
+
ctx.window.cursor_x = [x + 1, len].min
|
|
133
|
+
enter_insert_mode(ctx)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def append_line_end_mode(ctx, **)
|
|
137
|
+
ctx.window.cursor_x = ctx.buffer.line_length(ctx.window.cursor_y)
|
|
138
|
+
enter_insert_mode(ctx)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def insert_line_start_nonblank_mode(ctx, **)
|
|
142
|
+
cursor_first_nonblank(ctx)
|
|
143
|
+
enter_insert_mode(ctx)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def open_line_below(ctx, **)
|
|
147
|
+
materialize_intro_buffer_if_needed(ctx)
|
|
148
|
+
y = ctx.window.cursor_y
|
|
149
|
+
x = ctx.buffer.line_length(y)
|
|
150
|
+
ctx.buffer.begin_change_group
|
|
151
|
+
new_y, new_x = ctx.buffer.insert_newline(y, x)
|
|
152
|
+
ctx.window.cursor_y = new_y
|
|
153
|
+
ctx.window.cursor_x = new_x
|
|
154
|
+
ctx.editor.enter_insert_mode
|
|
155
|
+
ctx.editor.echo("-- INSERT --")
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def open_line_above(ctx, **)
|
|
159
|
+
materialize_intro_buffer_if_needed(ctx)
|
|
160
|
+
y = ctx.window.cursor_y
|
|
161
|
+
ctx.buffer.begin_change_group
|
|
162
|
+
ctx.buffer.insert_newline(y, 0)
|
|
163
|
+
ctx.window.cursor_x = 0
|
|
164
|
+
ctx.editor.enter_insert_mode
|
|
165
|
+
ctx.editor.echo("-- INSERT --")
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def enter_visual_char_mode(ctx, **)
|
|
169
|
+
ctx.editor.enter_visual(:visual_char)
|
|
170
|
+
ctx.editor.echo("-- VISUAL --")
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def enter_visual_line_mode(ctx, **)
|
|
174
|
+
ctx.editor.enter_visual(:visual_line)
|
|
175
|
+
ctx.editor.echo("-- VISUAL LINE --")
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def enter_visual_block_mode(ctx, **)
|
|
179
|
+
ctx.editor.enter_visual(:visual_block)
|
|
180
|
+
ctx.editor.echo("-- VISUAL BLOCK --")
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def window_split(ctx, **)
|
|
184
|
+
ctx.editor.split_current_window(layout: :horizontal)
|
|
185
|
+
ctx.editor.echo("split")
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def window_vsplit(ctx, **)
|
|
189
|
+
ctx.editor.split_current_window(layout: :vertical)
|
|
190
|
+
ctx.editor.echo("vsplit")
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def window_focus_next(ctx, **)
|
|
194
|
+
ctx.editor.focus_next_window
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def window_focus_left(ctx, **)
|
|
198
|
+
ctx.editor.focus_window_direction(:left)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def window_focus_right(ctx, **)
|
|
202
|
+
ctx.editor.focus_window_direction(:right)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def window_focus_up(ctx, **)
|
|
206
|
+
ctx.editor.focus_window_direction(:up)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def window_focus_down(ctx, **)
|
|
210
|
+
ctx.editor.focus_window_direction(:down)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def tab_new(ctx, argv:, **)
|
|
214
|
+
path = argv[0]
|
|
215
|
+
if ctx.buffer.modified?
|
|
216
|
+
ctx.editor.echo_error("Unsaved changes (use :w or :q!)")
|
|
217
|
+
return
|
|
218
|
+
end
|
|
219
|
+
tab = ctx.editor.tabnew(path: path)
|
|
220
|
+
if path && !path.empty?
|
|
221
|
+
b = ctx.editor.current_buffer
|
|
222
|
+
ctx.editor.echo("tab #{ctx.editor.current_tabpage_number}/#{ctx.editor.tabpage_count}: #{b.path || '[No Name]'}")
|
|
223
|
+
else
|
|
224
|
+
ctx.editor.echo("tab #{ctx.editor.current_tabpage_number}/#{ctx.editor.tabpage_count}")
|
|
225
|
+
end
|
|
226
|
+
tab
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def tab_next(ctx, count:, **)
|
|
230
|
+
ctx.editor.tabnext(count)
|
|
231
|
+
ctx.editor.echo("tab #{ctx.editor.current_tabpage_number}/#{ctx.editor.tabpage_count}")
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def tab_prev(ctx, count:, **)
|
|
235
|
+
ctx.editor.tabprev(count)
|
|
236
|
+
ctx.editor.echo("tab #{ctx.editor.current_tabpage_number}/#{ctx.editor.tabpage_count}")
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def enter_command_line_mode(ctx, **)
|
|
240
|
+
ctx.editor.enter_command_line_mode(":")
|
|
241
|
+
ctx.editor.clear_message
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def enter_search_forward_mode(ctx, **)
|
|
245
|
+
ctx.editor.enter_command_line_mode("/")
|
|
246
|
+
ctx.editor.clear_message
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def enter_search_backward_mode(ctx, **)
|
|
250
|
+
ctx.editor.enter_command_line_mode("?")
|
|
251
|
+
ctx.editor.clear_message
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def delete_char(ctx, count:, **)
|
|
255
|
+
materialize_intro_buffer_if_needed(ctx)
|
|
256
|
+
ctx.buffer.begin_change_group
|
|
257
|
+
deleted = +""
|
|
258
|
+
count.times do
|
|
259
|
+
chunk = char_at_cursor_for_delete(ctx.buffer, ctx.window.cursor_y, ctx.window.cursor_x)
|
|
260
|
+
ok = ctx.buffer.delete_char(ctx.window.cursor_y, ctx.window.cursor_x)
|
|
261
|
+
break unless ok
|
|
262
|
+
deleted << chunk.to_s
|
|
263
|
+
end
|
|
264
|
+
ctx.buffer.end_change_group
|
|
265
|
+
store_delete_register(ctx, text: deleted, type: :charwise) unless deleted.empty?
|
|
266
|
+
ctx.window.clamp_to_buffer(ctx.buffer)
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def delete_line(ctx, count:, **)
|
|
270
|
+
materialize_intro_buffer_if_needed(ctx)
|
|
271
|
+
ctx.buffer.begin_change_group
|
|
272
|
+
deleted_lines = []
|
|
273
|
+
count.times { deleted_lines << ctx.buffer.delete_line(ctx.window.cursor_y) }
|
|
274
|
+
ctx.buffer.end_change_group
|
|
275
|
+
store_delete_register(ctx, text: deleted_lines.join("\n") + "\n", type: :linewise)
|
|
276
|
+
ctx.window.clamp_to_buffer(ctx.buffer)
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def delete_motion(ctx, count:, kwargs:, **)
|
|
280
|
+
materialize_intro_buffer_if_needed(ctx)
|
|
281
|
+
motion = (kwargs[:motion] || kwargs["motion"]).to_s
|
|
282
|
+
handled =
|
|
283
|
+
case motion
|
|
284
|
+
when "h" then delete_chars_left(ctx, count)
|
|
285
|
+
when "l" then delete_chars_right(ctx, count)
|
|
286
|
+
when "j" then delete_lines_down(ctx, count)
|
|
287
|
+
when "k" then delete_lines_up(ctx, count)
|
|
288
|
+
when "$" then delete_to_end_of_line(ctx)
|
|
289
|
+
when "w" then delete_word_forward(ctx, count)
|
|
290
|
+
when "iw" then delete_text_object_word(ctx, around: false)
|
|
291
|
+
when "aw" then delete_text_object_word(ctx, around: true)
|
|
292
|
+
else
|
|
293
|
+
text_object_motion?(motion) ? delete_text_object(ctx, motion) : false
|
|
294
|
+
end
|
|
295
|
+
ctx.editor.echo("Unsupported motion for d: #{motion}") unless handled
|
|
296
|
+
handled
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def change_motion(ctx, count:, kwargs:, **)
|
|
300
|
+
materialize_intro_buffer_if_needed(ctx)
|
|
301
|
+
handled = delete_motion(ctx, count:, kwargs:)
|
|
302
|
+
return unless handled
|
|
303
|
+
|
|
304
|
+
enter_insert_mode(ctx)
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def change_line(ctx, count:, **)
|
|
308
|
+
materialize_intro_buffer_if_needed(ctx)
|
|
309
|
+
delete_line(ctx, count:)
|
|
310
|
+
enter_insert_mode(ctx)
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def buffer_undo(ctx, **)
|
|
314
|
+
if ctx.buffer.undo!
|
|
315
|
+
ctx.window.clamp_to_buffer(ctx.buffer)
|
|
316
|
+
ctx.editor.echo("Undo")
|
|
317
|
+
else
|
|
318
|
+
ctx.editor.echo("Already at oldest change")
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def buffer_redo(ctx, **)
|
|
323
|
+
if ctx.buffer.redo!
|
|
324
|
+
ctx.window.clamp_to_buffer(ctx.buffer)
|
|
325
|
+
ctx.editor.echo("Redo")
|
|
326
|
+
else
|
|
327
|
+
ctx.editor.echo("Already at newest change")
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def search_next(ctx, count:, **)
|
|
332
|
+
record_jump(ctx)
|
|
333
|
+
repeat_search(ctx, forward: true, count:)
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def search_prev(ctx, count:, **)
|
|
337
|
+
record_jump(ctx)
|
|
338
|
+
repeat_search(ctx, forward: false, count:)
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def search_word_forward(ctx, **)
|
|
342
|
+
record_jump(ctx)
|
|
343
|
+
search_current_word(ctx, exact: true, direction: :forward)
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def search_word_backward(ctx, **)
|
|
347
|
+
record_jump(ctx)
|
|
348
|
+
search_current_word(ctx, exact: true, direction: :backward)
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def search_word_forward_partial(ctx, **)
|
|
352
|
+
record_jump(ctx)
|
|
353
|
+
search_current_word(ctx, exact: false, direction: :forward)
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def search_word_backward_partial(ctx, **)
|
|
357
|
+
record_jump(ctx)
|
|
358
|
+
search_current_word(ctx, exact: false, direction: :backward)
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def mark_set(ctx, kwargs:, **)
|
|
362
|
+
mark = (kwargs[:mark] || kwargs["mark"]).to_s
|
|
363
|
+
raise RuVim::CommandError, "Invalid mark" unless ctx.editor.set_mark(mark)
|
|
364
|
+
|
|
365
|
+
ctx.editor.echo("mark #{mark}")
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def mark_jump(ctx, kwargs:, **)
|
|
369
|
+
mark = (kwargs[:mark] || kwargs["mark"]).to_s
|
|
370
|
+
linewise = !!(kwargs[:linewise] || kwargs["linewise"])
|
|
371
|
+
record_jump(ctx)
|
|
372
|
+
loc = ctx.editor.jump_to_mark(mark, linewise:)
|
|
373
|
+
if loc
|
|
374
|
+
ctx.editor.echo("#{linewise ? "'" : '`'}#{mark}")
|
|
375
|
+
else
|
|
376
|
+
ctx.editor.echo("Mark not set: #{mark}")
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
def jump_older(ctx, kwargs: {}, **)
|
|
381
|
+
linewise = !!(kwargs[:linewise] || kwargs["linewise"])
|
|
382
|
+
loc = ctx.editor.jump_older(linewise:)
|
|
383
|
+
ctx.editor.echo(loc ? (linewise ? "''" : "``") : "Jump list empty")
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def jump_newer(ctx, kwargs: {}, **)
|
|
387
|
+
linewise = !!(kwargs[:linewise] || kwargs["linewise"])
|
|
388
|
+
loc = ctx.editor.jump_newer(linewise:)
|
|
389
|
+
ctx.editor.echo(loc ? "<C-i>" : "At newest jump")
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
def replace_char(ctx, argv:, count:, **)
|
|
393
|
+
materialize_intro_buffer_if_needed(ctx)
|
|
394
|
+
ch = argv[0].to_s
|
|
395
|
+
raise RuVim::CommandError, "replace requires a character" if ch.empty?
|
|
396
|
+
|
|
397
|
+
y = ctx.window.cursor_y
|
|
398
|
+
x = ctx.window.cursor_x
|
|
399
|
+
line = ctx.buffer.line_at(y)
|
|
400
|
+
return if x >= line.length
|
|
401
|
+
|
|
402
|
+
ctx.buffer.begin_change_group
|
|
403
|
+
count.times do |i|
|
|
404
|
+
cx = x + i
|
|
405
|
+
break if cx >= ctx.buffer.line_length(y)
|
|
406
|
+
ctx.buffer.delete_span(y, cx, y, cx + 1)
|
|
407
|
+
ctx.buffer.insert_char(y, cx, ch[0])
|
|
408
|
+
end
|
|
409
|
+
ctx.buffer.end_change_group
|
|
410
|
+
ctx.window.clamp_to_buffer(ctx.buffer)
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def yank_line(ctx, count:, **)
|
|
414
|
+
start = ctx.window.cursor_y
|
|
415
|
+
text = ctx.buffer.line_block_text(start, count)
|
|
416
|
+
store_yank_register(ctx, text:, type: :linewise)
|
|
417
|
+
ctx.editor.echo("#{count} line(s) yanked")
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
def yank_motion(ctx, count:, kwargs:, **)
|
|
421
|
+
motion = (kwargs[:motion] || kwargs["motion"]).to_s
|
|
422
|
+
case motion
|
|
423
|
+
when "w"
|
|
424
|
+
y = ctx.window.cursor_y
|
|
425
|
+
x = ctx.window.cursor_x
|
|
426
|
+
target = advance_word_forward(ctx.buffer, y, x, count)
|
|
427
|
+
target ||= { row: y, col: x }
|
|
428
|
+
text = ctx.buffer.span_text(y, x, target[:row], target[:col])
|
|
429
|
+
store_yank_register(ctx, text:, type: :charwise)
|
|
430
|
+
ctx.editor.echo("yanked")
|
|
431
|
+
when "iw"
|
|
432
|
+
yank_text_object_word(ctx, around: false)
|
|
433
|
+
when "aw"
|
|
434
|
+
yank_text_object_word(ctx, around: true)
|
|
435
|
+
when "y"
|
|
436
|
+
yank_line(ctx, count:)
|
|
437
|
+
else
|
|
438
|
+
if text_object_motion?(motion)
|
|
439
|
+
yank_text_object(ctx, motion)
|
|
440
|
+
else
|
|
441
|
+
ctx.editor.echo("Unsupported motion for y: #{motion}")
|
|
442
|
+
end
|
|
443
|
+
end
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
def paste_after(ctx, count:, **)
|
|
447
|
+
materialize_intro_buffer_if_needed(ctx)
|
|
448
|
+
paste_register(ctx, before: false, count:)
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
def paste_before(ctx, count:, **)
|
|
452
|
+
materialize_intro_buffer_if_needed(ctx)
|
|
453
|
+
paste_register(ctx, before: true, count:)
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
def visual_yank(ctx, **)
|
|
457
|
+
sel = ctx.editor.visual_selection
|
|
458
|
+
return unless sel
|
|
459
|
+
|
|
460
|
+
if sel[:mode] == :linewise
|
|
461
|
+
count = sel[:end_row] - sel[:start_row] + 1
|
|
462
|
+
text = ctx.buffer.line_block_text(sel[:start_row], count)
|
|
463
|
+
store_yank_register(ctx, text:, type: :linewise)
|
|
464
|
+
elsif sel[:mode] == :blockwise
|
|
465
|
+
text = blockwise_selection_text(ctx.buffer, sel)
|
|
466
|
+
# Blockwise register/paste semantics are not implemented yet; store text payload.
|
|
467
|
+
store_yank_register(ctx, text:, type: :charwise)
|
|
468
|
+
else
|
|
469
|
+
text = ctx.buffer.span_text(sel[:start_row], sel[:start_col], sel[:end_row], sel[:end_col])
|
|
470
|
+
store_yank_register(ctx, text:, type: :charwise)
|
|
471
|
+
end
|
|
472
|
+
ctx.editor.enter_normal_mode
|
|
473
|
+
ctx.editor.echo("yanked")
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
def visual_delete(ctx, **)
|
|
477
|
+
materialize_intro_buffer_if_needed(ctx)
|
|
478
|
+
sel = ctx.editor.visual_selection
|
|
479
|
+
return unless sel
|
|
480
|
+
|
|
481
|
+
if sel[:mode] == :linewise
|
|
482
|
+
count = sel[:end_row] - sel[:start_row] + 1
|
|
483
|
+
text = ctx.buffer.line_block_text(sel[:start_row], count)
|
|
484
|
+
ctx.buffer.begin_change_group
|
|
485
|
+
count.times { ctx.buffer.delete_line(sel[:start_row]) }
|
|
486
|
+
ctx.buffer.end_change_group
|
|
487
|
+
store_delete_register(ctx, text:, type: :linewise)
|
|
488
|
+
ctx.window.cursor_y = [sel[:start_row], ctx.buffer.line_count - 1].min
|
|
489
|
+
ctx.window.cursor_x = 0
|
|
490
|
+
elsif sel[:mode] == :blockwise
|
|
491
|
+
text = blockwise_selection_text(ctx.buffer, sel)
|
|
492
|
+
ctx.buffer.begin_change_group
|
|
493
|
+
(sel[:start_row]..sel[:end_row]).each do |row|
|
|
494
|
+
line = ctx.buffer.line_at(row)
|
|
495
|
+
s_col = [sel[:start_col], line.length].min
|
|
496
|
+
e_col = [sel[:end_col], line.length].min
|
|
497
|
+
next if e_col <= s_col
|
|
498
|
+
|
|
499
|
+
ctx.buffer.delete_span(row, s_col, row, e_col)
|
|
500
|
+
end
|
|
501
|
+
ctx.buffer.end_change_group
|
|
502
|
+
store_delete_register(ctx, text:, type: :charwise)
|
|
503
|
+
ctx.window.cursor_y = sel[:start_row]
|
|
504
|
+
ctx.window.cursor_x = sel[:start_col]
|
|
505
|
+
else
|
|
506
|
+
text = ctx.buffer.span_text(sel[:start_row], sel[:start_col], sel[:end_row], sel[:end_col])
|
|
507
|
+
ctx.buffer.begin_change_group
|
|
508
|
+
ctx.buffer.delete_span(sel[:start_row], sel[:start_col], sel[:end_row], sel[:end_col])
|
|
509
|
+
ctx.buffer.end_change_group
|
|
510
|
+
store_delete_register(ctx, text:, type: :charwise)
|
|
511
|
+
ctx.window.cursor_y = sel[:start_row]
|
|
512
|
+
ctx.window.cursor_x = sel[:start_col]
|
|
513
|
+
end
|
|
514
|
+
ctx.window.clamp_to_buffer(ctx.buffer)
|
|
515
|
+
ctx.editor.enter_normal_mode
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
def visual_select_text_object(ctx, kwargs:, **)
|
|
519
|
+
motion = (kwargs[:motion] || kwargs["motion"]).to_s
|
|
520
|
+
span = text_object_span(ctx.buffer, ctx.window, motion)
|
|
521
|
+
return false unless span
|
|
522
|
+
|
|
523
|
+
ctx.editor.enter_visual(:visual_char) unless ctx.editor.mode == :visual_char
|
|
524
|
+
v = ctx.editor.visual_state
|
|
525
|
+
v[:anchor_y] = span[:start_row]
|
|
526
|
+
v[:anchor_x] = span[:start_col]
|
|
527
|
+
ctx.window.cursor_y = span[:end_row]
|
|
528
|
+
ctx.window.cursor_x = [span[:end_col] - 1, 0].max
|
|
529
|
+
ctx.window.clamp_to_buffer(ctx.buffer)
|
|
530
|
+
true
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
def clear_message(ctx, **)
|
|
534
|
+
ctx.editor.clear_message
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
def file_write(ctx, argv:, bang:, **)
|
|
538
|
+
path = argv[0]
|
|
539
|
+
target = ctx.buffer.write_to(path)
|
|
540
|
+
size = File.exist?(target) ? File.size(target) : 0
|
|
541
|
+
suffix = bang ? " (force accepted)" : ""
|
|
542
|
+
ctx.editor.echo("\"#{target}\" #{ctx.buffer.line_count}L, #{size}B written#{suffix}")
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
def app_quit(ctx, bang:, **)
|
|
546
|
+
if ctx.editor.window_count > 1
|
|
547
|
+
ctx.editor.close_current_window
|
|
548
|
+
ctx.editor.echo("closed window")
|
|
549
|
+
return
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
if ctx.editor.tabpage_count > 1
|
|
553
|
+
ctx.editor.close_current_tabpage
|
|
554
|
+
ctx.editor.echo("closed tab")
|
|
555
|
+
return
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
if ctx.buffer.modified? && !bang
|
|
559
|
+
ctx.editor.echo_error("No write since last change (add ! to override)")
|
|
560
|
+
return
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
ctx.editor.request_quit!
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
def file_write_quit(ctx, argv:, bang:, **)
|
|
567
|
+
file_write(ctx, argv:, bang:)
|
|
568
|
+
return unless ctx.editor.running?
|
|
569
|
+
app_quit(ctx, bang: true)
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
def file_edit(ctx, argv:, bang:, **)
|
|
573
|
+
path = argv[0]
|
|
574
|
+
if path.nil? || path.empty?
|
|
575
|
+
current_path = ctx.buffer.path
|
|
576
|
+
raise RuVim::CommandError, "Argument required" if current_path.nil? || current_path.empty?
|
|
577
|
+
|
|
578
|
+
if ctx.buffer.modified? && !bang
|
|
579
|
+
ctx.editor.echo_error("Unsaved changes (use :e! to discard)")
|
|
580
|
+
return
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
target = ctx.buffer.reload_from_file!(current_path)
|
|
584
|
+
ctx.window.clamp_to_buffer(ctx.buffer)
|
|
585
|
+
ctx.editor.echo("\"#{target}\" reloaded")
|
|
586
|
+
return
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
if ctx.buffer.modified? && !bang
|
|
590
|
+
ctx.editor.echo_error("Unsaved changes (use :e! to discard and open)")
|
|
591
|
+
return
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
new_buffer = ctx.editor.add_buffer_from_file(path)
|
|
595
|
+
ctx.editor.switch_to_buffer(new_buffer.id)
|
|
596
|
+
ctx.editor.echo(File.exist?(path) ? "\"#{path}\" #{new_buffer.line_count}L" : "\"#{path}\" [New File]")
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
def buffer_list(ctx, **)
|
|
600
|
+
current_id = ctx.buffer.id
|
|
601
|
+
alt_id = ctx.editor.alternate_buffer_id
|
|
602
|
+
items = ctx.editor.buffer_ids.map do |id|
|
|
603
|
+
b = ctx.editor.buffers.fetch(id)
|
|
604
|
+
flags = ""
|
|
605
|
+
flags << "%" if id == current_id
|
|
606
|
+
flags << "#" if id == alt_id
|
|
607
|
+
flags << "+" if b.modified?
|
|
608
|
+
path = b.path || "[No Name]"
|
|
609
|
+
"#{id}#{flags} #{path}"
|
|
610
|
+
end
|
|
611
|
+
ctx.editor.echo(items.join(" | "))
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
def buffer_next(ctx, count:, bang:, **)
|
|
615
|
+
target = ctx.editor.current_buffer.id
|
|
616
|
+
count.times { target = ctx.editor.next_buffer_id_from(target, 1) }
|
|
617
|
+
switch_buffer_id(ctx, target, bang:)
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
def buffer_prev(ctx, count:, bang:, **)
|
|
621
|
+
target = ctx.editor.current_buffer.id
|
|
622
|
+
count.times { target = ctx.editor.next_buffer_id_from(target, -1) }
|
|
623
|
+
switch_buffer_id(ctx, target, bang:)
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
def buffer_switch(ctx, argv:, bang:, **)
|
|
627
|
+
arg = argv[0]
|
|
628
|
+
raise RuVim::CommandError, "Usage: :buffer <id|#>" if arg.nil? || arg.empty?
|
|
629
|
+
|
|
630
|
+
target_id =
|
|
631
|
+
if arg == "#"
|
|
632
|
+
ctx.editor.alternate_buffer_id || raise(RuVim::CommandError, "No alternate buffer")
|
|
633
|
+
elsif arg.match?(/\A\d+\z/)
|
|
634
|
+
arg.to_i
|
|
635
|
+
else
|
|
636
|
+
find_buffer_by_name(ctx.editor, arg)&.id || raise(RuVim::CommandError, "No such buffer: #{arg}")
|
|
637
|
+
end
|
|
638
|
+
|
|
639
|
+
switch_buffer_id(ctx, target_id, bang:)
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
def ex_help(ctx, argv: [], **)
|
|
643
|
+
topic = argv.first.to_s
|
|
644
|
+
registry = RuVim::ExCommandRegistry.instance
|
|
645
|
+
|
|
646
|
+
if topic.empty?
|
|
647
|
+
lines = [
|
|
648
|
+
"RuVim help",
|
|
649
|
+
"",
|
|
650
|
+
"Topics:",
|
|
651
|
+
" :help commands",
|
|
652
|
+
" :help regex",
|
|
653
|
+
" :help options",
|
|
654
|
+
" :help config",
|
|
655
|
+
" :help bindings",
|
|
656
|
+
"",
|
|
657
|
+
"Ex command help:",
|
|
658
|
+
" :help w",
|
|
659
|
+
" :help set",
|
|
660
|
+
" :help buffer"
|
|
661
|
+
]
|
|
662
|
+
ctx.editor.show_help_buffer!(title: "[Help] help", lines:)
|
|
663
|
+
return
|
|
664
|
+
end
|
|
665
|
+
|
|
666
|
+
key = topic.downcase
|
|
667
|
+
text =
|
|
668
|
+
case key
|
|
669
|
+
when "commands", "command"
|
|
670
|
+
"Ex commands: use :commands (list), :help <name> (detail)"
|
|
671
|
+
when "regex", "search"
|
|
672
|
+
"Regex uses Ruby Regexp (not Vim regex). :%s/pat/rep/g is minimal parser + Ruby regex."
|
|
673
|
+
when "options", "set"
|
|
674
|
+
"Options: use :set/:setlocal/:setglobal. See :help number, :help relativenumber, :help ignorecase, :help smartcase, :help hlsearch"
|
|
675
|
+
when "config"
|
|
676
|
+
"Config: XDG Ruby DSL at ~/.config/ruvim/init.rb and ftplugin/<filetype>.rb"
|
|
677
|
+
when "bindings", "keys", "keymap"
|
|
678
|
+
"Bindings: see docs/binding.md. Ex complement: Tab, insert completion: Ctrl-n/Ctrl-p"
|
|
679
|
+
when "number", "relativenumber", "ignorecase", "smartcase", "hlsearch", "tabstop", "filetype"
|
|
680
|
+
option_help_line(key)
|
|
681
|
+
else
|
|
682
|
+
if (spec = registry.resolve(topic))
|
|
683
|
+
ex_command_help_line(spec)
|
|
684
|
+
else
|
|
685
|
+
"No help for #{topic}. Try :help or :help commands"
|
|
686
|
+
end
|
|
687
|
+
end
|
|
688
|
+
ctx.editor.show_help_buffer!(title: "[Help] #{topic}", lines: help_text_to_lines(topic, text))
|
|
689
|
+
end
|
|
690
|
+
|
|
691
|
+
def ex_define_command(ctx, argv:, bang:, **)
|
|
692
|
+
registry = RuVim::ExCommandRegistry.instance
|
|
693
|
+
if argv.empty?
|
|
694
|
+
items = registry.all.select { |spec| spec.source == :user }.map(&:name)
|
|
695
|
+
ctx.editor.echo(items.empty? ? "No user commands" : "User commands: #{items.join(', ')}")
|
|
696
|
+
return
|
|
697
|
+
end
|
|
698
|
+
|
|
699
|
+
name = argv[0].to_s
|
|
700
|
+
body_tokens = argv[1..] || []
|
|
701
|
+
raise RuVim::CommandError, "Usage: :command Name ex_body" if body_tokens.empty?
|
|
702
|
+
|
|
703
|
+
if registry.registered?(name)
|
|
704
|
+
unless bang
|
|
705
|
+
raise RuVim::CommandError, "Command exists: #{name} (use :command! to replace)"
|
|
706
|
+
end
|
|
707
|
+
registry.unregister(name)
|
|
708
|
+
end
|
|
709
|
+
|
|
710
|
+
body = body_tokens.join(" ")
|
|
711
|
+
handler = lambda do |inner_ctx, argv:, **_k|
|
|
712
|
+
expanded = [body, *argv].join(" ").strip
|
|
713
|
+
Dispatcher.new.dispatch_ex(inner_ctx.editor, expanded)
|
|
714
|
+
end
|
|
715
|
+
|
|
716
|
+
registry.register(name, call: handler, desc: "user-defined", nargs: :any, bang: true, source: :user)
|
|
717
|
+
ctx.editor.echo("Defined :#{name}")
|
|
718
|
+
end
|
|
719
|
+
|
|
720
|
+
def ex_ruby(ctx, argv:, **)
|
|
721
|
+
raise RuVim::CommandError, "Restricted mode: :ruby is disabled" if ctx.editor.respond_to?(:restricted_mode?) && ctx.editor.restricted_mode?
|
|
722
|
+
|
|
723
|
+
code = argv.join(" ")
|
|
724
|
+
raise RuVim::CommandError, "Usage: :ruby <code>" if code.strip.empty?
|
|
725
|
+
|
|
726
|
+
b = binding
|
|
727
|
+
b.local_variable_set(:editor, ctx.editor)
|
|
728
|
+
b.local_variable_set(:buffer, ctx.buffer)
|
|
729
|
+
b.local_variable_set(:window, ctx.window)
|
|
730
|
+
result = eval(code, b) # rubocop:disable Security/Eval
|
|
731
|
+
ctx.editor.echo(result.nil? ? "ruby: nil" : "ruby: #{result.inspect}")
|
|
732
|
+
rescue StandardError => e
|
|
733
|
+
raise RuVim::CommandError, "Ruby error: #{e.class}: #{e.message}"
|
|
734
|
+
end
|
|
735
|
+
|
|
736
|
+
def ex_commands(ctx, **)
|
|
737
|
+
items = RuVim::ExCommandRegistry.instance.all.map do |spec|
|
|
738
|
+
alias_text = spec.aliases.empty? ? "" : " (#{spec.aliases.join(', ')})"
|
|
739
|
+
source = spec.source == :user ? " [user]" : ""
|
|
740
|
+
"#{spec.name}#{alias_text}#{source}"
|
|
741
|
+
end
|
|
742
|
+
ctx.editor.show_help_buffer!(title: "[Commands]", lines: ["Ex commands", "", *items])
|
|
743
|
+
end
|
|
744
|
+
|
|
745
|
+
def ex_set(ctx, argv:, **)
|
|
746
|
+
ex_set_common(ctx, argv, scope: :auto)
|
|
747
|
+
end
|
|
748
|
+
|
|
749
|
+
def ex_setlocal(ctx, argv:, **)
|
|
750
|
+
ex_set_common(ctx, argv, scope: :local)
|
|
751
|
+
end
|
|
752
|
+
|
|
753
|
+
def ex_setglobal(ctx, argv:, **)
|
|
754
|
+
ex_set_common(ctx, argv, scope: :global)
|
|
755
|
+
end
|
|
756
|
+
|
|
757
|
+
def ex_vimgrep(ctx, argv:, **)
|
|
758
|
+
pattern = parse_vimgrep_pattern(argv)
|
|
759
|
+
regex = compile_search_regex(pattern, editor: ctx.editor, window: ctx.window, buffer: ctx.buffer)
|
|
760
|
+
items = grep_items_for_buffers(ctx.editor.buffers.values.select(&:file_buffer?), regex)
|
|
761
|
+
if items.empty?
|
|
762
|
+
ctx.editor.echo_error("Pattern not found: #{pattern}")
|
|
763
|
+
return
|
|
764
|
+
end
|
|
765
|
+
|
|
766
|
+
ctx.editor.set_quickfix_list(items)
|
|
767
|
+
ctx.editor.jump_to_location(ctx.editor.current_quickfix_item)
|
|
768
|
+
ctx.editor.echo("quickfix: #{items.length} item(s)")
|
|
769
|
+
end
|
|
770
|
+
|
|
771
|
+
def ex_lvimgrep(ctx, argv:, **)
|
|
772
|
+
pattern = parse_vimgrep_pattern(argv)
|
|
773
|
+
regex = compile_search_regex(pattern, editor: ctx.editor, window: ctx.window, buffer: ctx.buffer)
|
|
774
|
+
items = grep_items_for_buffers([ctx.buffer], regex)
|
|
775
|
+
if items.empty?
|
|
776
|
+
ctx.editor.echo_error("Pattern not found: #{pattern}")
|
|
777
|
+
return
|
|
778
|
+
end
|
|
779
|
+
|
|
780
|
+
ctx.editor.set_location_list(items, window_id: ctx.window.id)
|
|
781
|
+
ctx.editor.jump_to_location(ctx.editor.current_location_list_item(ctx.window.id))
|
|
782
|
+
ctx.editor.echo("location list: #{items.length} item(s)")
|
|
783
|
+
end
|
|
784
|
+
|
|
785
|
+
def ex_copen(ctx, **)
|
|
786
|
+
open_list_window(ctx, kind: :quickfix, title: "[Quickfix]", lines: quickfix_buffer_lines(ctx.editor))
|
|
787
|
+
end
|
|
788
|
+
|
|
789
|
+
def ex_cclose(ctx, **)
|
|
790
|
+
close_list_windows(ctx.editor, :quickfix)
|
|
791
|
+
end
|
|
792
|
+
|
|
793
|
+
def ex_cnext(ctx, **)
|
|
794
|
+
item = ctx.editor.move_quickfix(+1)
|
|
795
|
+
unless item
|
|
796
|
+
ctx.editor.echo_error("quickfix list is empty")
|
|
797
|
+
return
|
|
798
|
+
end
|
|
799
|
+
ctx.editor.jump_to_location(item)
|
|
800
|
+
ctx.editor.echo(quickfix_item_echo(ctx.editor))
|
|
801
|
+
end
|
|
802
|
+
|
|
803
|
+
def ex_cprev(ctx, **)
|
|
804
|
+
item = ctx.editor.move_quickfix(-1)
|
|
805
|
+
unless item
|
|
806
|
+
ctx.editor.echo_error("quickfix list is empty")
|
|
807
|
+
return
|
|
808
|
+
end
|
|
809
|
+
ctx.editor.jump_to_location(item)
|
|
810
|
+
ctx.editor.echo(quickfix_item_echo(ctx.editor))
|
|
811
|
+
end
|
|
812
|
+
|
|
813
|
+
def ex_lopen(ctx, **)
|
|
814
|
+
open_list_window(ctx, kind: :location_list, title: "[Location List]", lines: location_list_buffer_lines(ctx.editor, ctx.window.id))
|
|
815
|
+
end
|
|
816
|
+
|
|
817
|
+
def ex_lclose(ctx, **)
|
|
818
|
+
close_list_windows(ctx.editor, :location_list)
|
|
819
|
+
end
|
|
820
|
+
|
|
821
|
+
def ex_lnext(ctx, **)
|
|
822
|
+
item = ctx.editor.move_location_list(+1, window_id: ctx.window.id)
|
|
823
|
+
unless item
|
|
824
|
+
ctx.editor.echo_error("location list is empty")
|
|
825
|
+
return
|
|
826
|
+
end
|
|
827
|
+
ctx.editor.jump_to_location(item)
|
|
828
|
+
ctx.editor.echo(location_item_echo(ctx.editor, ctx.window.id))
|
|
829
|
+
end
|
|
830
|
+
|
|
831
|
+
def ex_lprev(ctx, **)
|
|
832
|
+
item = ctx.editor.move_location_list(-1, window_id: ctx.window.id)
|
|
833
|
+
unless item
|
|
834
|
+
ctx.editor.echo_error("location list is empty")
|
|
835
|
+
return
|
|
836
|
+
end
|
|
837
|
+
ctx.editor.jump_to_location(item)
|
|
838
|
+
ctx.editor.echo(location_item_echo(ctx.editor, ctx.window.id))
|
|
839
|
+
end
|
|
840
|
+
|
|
841
|
+
def ex_substitute(ctx, pattern:, replacement:, global: false, **)
|
|
842
|
+
materialize_intro_buffer_if_needed(ctx)
|
|
843
|
+
regex = compile_search_regex(pattern, editor: ctx.editor, window: ctx.window, buffer: ctx.buffer)
|
|
844
|
+
changed = 0
|
|
845
|
+
new_lines = ctx.buffer.lines.map do |line|
|
|
846
|
+
if global
|
|
847
|
+
line.scan(regex) { changed += 1 }
|
|
848
|
+
line.gsub(regex, replacement)
|
|
849
|
+
else
|
|
850
|
+
if line.match?(regex)
|
|
851
|
+
changed += 1
|
|
852
|
+
line.sub(regex, replacement)
|
|
853
|
+
else
|
|
854
|
+
line
|
|
855
|
+
end
|
|
856
|
+
end
|
|
857
|
+
end
|
|
858
|
+
|
|
859
|
+
if changed.positive?
|
|
860
|
+
ctx.buffer.begin_change_group
|
|
861
|
+
ctx.buffer.replace_all_lines!(new_lines)
|
|
862
|
+
ctx.buffer.end_change_group
|
|
863
|
+
ctx.editor.echo("#{changed} substitution(s)")
|
|
864
|
+
else
|
|
865
|
+
ctx.editor.echo("Pattern not found: #{pattern}")
|
|
866
|
+
end
|
|
867
|
+
end
|
|
868
|
+
|
|
869
|
+
def submit_search(ctx, pattern:, direction:)
|
|
870
|
+
text = pattern.to_s
|
|
871
|
+
if text.empty?
|
|
872
|
+
prev = ctx.editor.last_search
|
|
873
|
+
raise RuVim::CommandError, "No previous search" unless prev
|
|
874
|
+
text = prev[:pattern]
|
|
875
|
+
end
|
|
876
|
+
compile_search_regex(text, editor: ctx.editor, window: ctx.window, buffer: ctx.buffer)
|
|
877
|
+
ctx.editor.set_last_search(pattern: text, direction:)
|
|
878
|
+
record_jump(ctx)
|
|
879
|
+
move_to_search(ctx, pattern: text, direction:, count: 1)
|
|
880
|
+
end
|
|
881
|
+
|
|
882
|
+
private
|
|
883
|
+
|
|
884
|
+
def parse_vimgrep_pattern(argv)
|
|
885
|
+
raw = Array(argv).join(" ").strip
|
|
886
|
+
raise RuVim::CommandError, "Usage: :vimgrep /pattern/" if raw.empty?
|
|
887
|
+
|
|
888
|
+
if raw.length >= 2 && raw[0] == raw[-1] && raw[0] !~ /[[:alnum:]\s]/
|
|
889
|
+
raw[1...-1]
|
|
890
|
+
else
|
|
891
|
+
raw
|
|
892
|
+
end
|
|
893
|
+
end
|
|
894
|
+
|
|
895
|
+
def grep_items_for_buffers(buffers, regex)
|
|
896
|
+
Array(buffers).flat_map do |buffer|
|
|
897
|
+
buffer.lines.each_with_index.flat_map do |line, row|
|
|
898
|
+
line.to_enum(:scan, regex).map do
|
|
899
|
+
m = Regexp.last_match
|
|
900
|
+
next unless m
|
|
901
|
+
{ buffer_id: buffer.id, row: row, col: m.begin(0), text: line }
|
|
902
|
+
end.compact
|
|
903
|
+
end
|
|
904
|
+
end
|
|
905
|
+
end
|
|
906
|
+
|
|
907
|
+
def quickfix_buffer_lines(editor)
|
|
908
|
+
items = editor.quickfix_items
|
|
909
|
+
return ["Quickfix", "", "(empty)"] if items.empty?
|
|
910
|
+
|
|
911
|
+
idx = editor.quickfix_index || 0
|
|
912
|
+
build_list_buffer_lines(editor, items, idx, title: "Quickfix")
|
|
913
|
+
end
|
|
914
|
+
|
|
915
|
+
def location_list_buffer_lines(editor, window_id)
|
|
916
|
+
items = editor.location_items(window_id)
|
|
917
|
+
idx = editor.location_list(window_id)[:index] || 0
|
|
918
|
+
return ["Location List", "", "(empty)"] if items.empty?
|
|
919
|
+
|
|
920
|
+
build_list_buffer_lines(editor, items, idx, title: "Location List")
|
|
921
|
+
end
|
|
922
|
+
|
|
923
|
+
def build_list_buffer_lines(editor, items, current_index, title:)
|
|
924
|
+
[
|
|
925
|
+
title,
|
|
926
|
+
"",
|
|
927
|
+
*items.each_with_index.map do |it, i|
|
|
928
|
+
b = editor.buffers[it[:buffer_id]]
|
|
929
|
+
path = b&.display_name || "(missing)"
|
|
930
|
+
mark = i == current_index ? ">" : " "
|
|
931
|
+
"#{mark} #{i + 1}: #{path}:#{it[:row] + 1}:#{it[:col] + 1}: #{it[:text]}"
|
|
932
|
+
end
|
|
933
|
+
]
|
|
934
|
+
end
|
|
935
|
+
|
|
936
|
+
def open_list_window(ctx, kind:, title:, lines:)
|
|
937
|
+
editor = ctx.editor
|
|
938
|
+
editor.split_current_window(layout: :horizontal)
|
|
939
|
+
buffer = editor.add_virtual_buffer(kind:, name: title, lines:, filetype: "qf", readonly: true, modifiable: false)
|
|
940
|
+
editor.switch_to_buffer(buffer.id)
|
|
941
|
+
editor.echo(title)
|
|
942
|
+
buffer
|
|
943
|
+
end
|
|
944
|
+
|
|
945
|
+
def close_list_windows(editor, kind)
|
|
946
|
+
ids = editor.find_window_ids_by_buffer_kind(kind)
|
|
947
|
+
if ids.empty?
|
|
948
|
+
editor.echo_error("#{kind} window is not open")
|
|
949
|
+
return
|
|
950
|
+
end
|
|
951
|
+
|
|
952
|
+
ids.each do |wid|
|
|
953
|
+
break if editor.window_count <= 1
|
|
954
|
+
editor.close_window(wid)
|
|
955
|
+
end
|
|
956
|
+
editor.echo("#{kind} closed")
|
|
957
|
+
end
|
|
958
|
+
|
|
959
|
+
def quickfix_item_echo(editor)
|
|
960
|
+
item = editor.current_quickfix_item
|
|
961
|
+
list_item_echo(editor, item, editor.quickfix_index, editor.quickfix_items.length, label: "qf")
|
|
962
|
+
end
|
|
963
|
+
|
|
964
|
+
def location_item_echo(editor, window_id)
|
|
965
|
+
item = editor.current_location_list_item(window_id)
|
|
966
|
+
list = editor.location_list(window_id)
|
|
967
|
+
list_item_echo(editor, item, list[:index], list[:items].length, label: "ll")
|
|
968
|
+
end
|
|
969
|
+
|
|
970
|
+
def list_item_echo(editor, item, index, total, label:)
|
|
971
|
+
return "#{label}: empty" unless item
|
|
972
|
+
|
|
973
|
+
b = editor.buffers[item[:buffer_id]]
|
|
974
|
+
"#{label} #{index.to_i + 1}/#{total}: #{b&.display_name || '(missing)'}:#{item[:row] + 1}:#{item[:col] + 1}"
|
|
975
|
+
end
|
|
976
|
+
|
|
977
|
+
def switch_buffer_id(ctx, buffer_id, bang: false)
|
|
978
|
+
unless ctx.editor.buffers.key?(buffer_id)
|
|
979
|
+
raise RuVim::CommandError, "No such buffer: #{buffer_id}"
|
|
980
|
+
end
|
|
981
|
+
|
|
982
|
+
if ctx.buffer.modified? && ctx.buffer.id != buffer_id && !bang
|
|
983
|
+
ctx.editor.echo_error("Unsaved changes (use :w or :buffer! / :bnext! / :bprev!)")
|
|
984
|
+
return
|
|
985
|
+
end
|
|
986
|
+
|
|
987
|
+
record_jump(ctx)
|
|
988
|
+
ctx.editor.switch_to_buffer(buffer_id)
|
|
989
|
+
b = ctx.editor.current_buffer
|
|
990
|
+
ctx.editor.echo("#{b.id} #{b.path || '[No Name]'}")
|
|
991
|
+
end
|
|
992
|
+
|
|
993
|
+
def char_at_cursor_for_delete(buffer, row, col)
|
|
994
|
+
line = buffer.line_at(row)
|
|
995
|
+
if col < line.length
|
|
996
|
+
line[col]
|
|
997
|
+
elsif row < buffer.line_count - 1
|
|
998
|
+
"\n"
|
|
999
|
+
else
|
|
1000
|
+
""
|
|
1001
|
+
end
|
|
1002
|
+
end
|
|
1003
|
+
|
|
1004
|
+
def find_buffer_by_name(editor, token)
|
|
1005
|
+
editor.buffers.values.find do |b|
|
|
1006
|
+
path = b.path.to_s
|
|
1007
|
+
path == token || File.basename(path) == token
|
|
1008
|
+
end
|
|
1009
|
+
end
|
|
1010
|
+
|
|
1011
|
+
def blockwise_selection_text(buffer, sel)
|
|
1012
|
+
(sel[:start_row]..sel[:end_row]).map do |row|
|
|
1013
|
+
line = buffer.line_at(row)
|
|
1014
|
+
s_col = [sel[:start_col], line.length].min
|
|
1015
|
+
e_col = [sel[:end_col], line.length].min
|
|
1016
|
+
line[s_col...e_col].to_s
|
|
1017
|
+
end.join("\n")
|
|
1018
|
+
end
|
|
1019
|
+
|
|
1020
|
+
def search_current_word(ctx, exact:, direction:)
|
|
1021
|
+
word = current_word_under_cursor(ctx.buffer, ctx.window)
|
|
1022
|
+
if word.nil? || word.empty?
|
|
1023
|
+
ctx.editor.echo("No word under cursor")
|
|
1024
|
+
return
|
|
1025
|
+
end
|
|
1026
|
+
|
|
1027
|
+
pattern = exact ? "\\b#{Regexp.escape(word)}\\b" : Regexp.escape(word)
|
|
1028
|
+
ctx.editor.set_last_search(pattern:, direction:)
|
|
1029
|
+
move_to_search(ctx, pattern:, direction:, count: 1)
|
|
1030
|
+
end
|
|
1031
|
+
|
|
1032
|
+
def current_word_under_cursor(buffer, window)
|
|
1033
|
+
line = buffer.line_at(window.cursor_y)
|
|
1034
|
+
return nil if line.empty?
|
|
1035
|
+
|
|
1036
|
+
x = [window.cursor_x, line.length - 1].min
|
|
1037
|
+
return nil if x.negative?
|
|
1038
|
+
|
|
1039
|
+
if line[x] !~ /[[:alnum:]_]/
|
|
1040
|
+
left = x - 1
|
|
1041
|
+
if left >= 0 && line[left] =~ /[[:alnum:]_]/
|
|
1042
|
+
x = left
|
|
1043
|
+
else
|
|
1044
|
+
return nil
|
|
1045
|
+
end
|
|
1046
|
+
end
|
|
1047
|
+
|
|
1048
|
+
s = x
|
|
1049
|
+
s -= 1 while s.positive? && line[s - 1] =~ /[[:alnum:]_]/
|
|
1050
|
+
e = x + 1
|
|
1051
|
+
e += 1 while e < line.length && line[e] =~ /[[:alnum:]_]/
|
|
1052
|
+
line[s...e]
|
|
1053
|
+
end
|
|
1054
|
+
|
|
1055
|
+
def delete_chars_left(ctx, count)
|
|
1056
|
+
return true if count <= 0
|
|
1057
|
+
|
|
1058
|
+
y = ctx.window.cursor_y
|
|
1059
|
+
x = ctx.window.cursor_x
|
|
1060
|
+
start_x = [x - count, 0].max
|
|
1061
|
+
return true if start_x == x
|
|
1062
|
+
|
|
1063
|
+
deleted = ctx.buffer.span_text(y, start_x, y, x)
|
|
1064
|
+
ctx.buffer.begin_change_group
|
|
1065
|
+
ctx.buffer.delete_span(y, start_x, y, x)
|
|
1066
|
+
ctx.buffer.end_change_group
|
|
1067
|
+
store_delete_register(ctx, text: deleted, type: :charwise) unless deleted.empty?
|
|
1068
|
+
ctx.window.cursor_x = start_x
|
|
1069
|
+
ctx.window.clamp_to_buffer(ctx.buffer)
|
|
1070
|
+
true
|
|
1071
|
+
end
|
|
1072
|
+
|
|
1073
|
+
def delete_chars_right(ctx, count)
|
|
1074
|
+
return true if count <= 0
|
|
1075
|
+
|
|
1076
|
+
y = ctx.window.cursor_y
|
|
1077
|
+
x = ctx.window.cursor_x
|
|
1078
|
+
line_len = ctx.buffer.line_length(y)
|
|
1079
|
+
end_x = [x + count, line_len].min
|
|
1080
|
+
return true if end_x == x
|
|
1081
|
+
|
|
1082
|
+
deleted = ctx.buffer.span_text(y, x, y, end_x)
|
|
1083
|
+
ctx.buffer.begin_change_group
|
|
1084
|
+
ctx.buffer.delete_span(y, x, y, end_x)
|
|
1085
|
+
ctx.buffer.end_change_group
|
|
1086
|
+
store_delete_register(ctx, text: deleted, type: :charwise) unless deleted.empty?
|
|
1087
|
+
ctx.window.clamp_to_buffer(ctx.buffer)
|
|
1088
|
+
true
|
|
1089
|
+
end
|
|
1090
|
+
|
|
1091
|
+
def delete_lines_down(ctx, count)
|
|
1092
|
+
total = count + 1
|
|
1093
|
+
deleted = ctx.buffer.line_block_text(ctx.window.cursor_y, total)
|
|
1094
|
+
ctx.buffer.begin_change_group
|
|
1095
|
+
total.times { ctx.buffer.delete_line(ctx.window.cursor_y) }
|
|
1096
|
+
ctx.buffer.end_change_group
|
|
1097
|
+
store_delete_register(ctx, text: deleted, type: :linewise)
|
|
1098
|
+
ctx.window.clamp_to_buffer(ctx.buffer)
|
|
1099
|
+
true
|
|
1100
|
+
end
|
|
1101
|
+
|
|
1102
|
+
def delete_lines_up(ctx, count)
|
|
1103
|
+
y = ctx.window.cursor_y
|
|
1104
|
+
start = [y - count, 0].max
|
|
1105
|
+
total = y - start + 1
|
|
1106
|
+
deleted = ctx.buffer.line_block_text(start, total)
|
|
1107
|
+
ctx.buffer.begin_change_group
|
|
1108
|
+
total.times { ctx.buffer.delete_line(start) }
|
|
1109
|
+
ctx.buffer.end_change_group
|
|
1110
|
+
store_delete_register(ctx, text: deleted, type: :linewise)
|
|
1111
|
+
ctx.window.cursor_y = start
|
|
1112
|
+
ctx.window.cursor_x = 0 if ctx.window.cursor_x > ctx.buffer.line_length(ctx.window.cursor_y)
|
|
1113
|
+
ctx.window.clamp_to_buffer(ctx.buffer)
|
|
1114
|
+
true
|
|
1115
|
+
end
|
|
1116
|
+
|
|
1117
|
+
def delete_to_end_of_line(ctx)
|
|
1118
|
+
y = ctx.window.cursor_y
|
|
1119
|
+
x = ctx.window.cursor_x
|
|
1120
|
+
line_len = ctx.buffer.line_length(y)
|
|
1121
|
+
return true if x >= line_len
|
|
1122
|
+
|
|
1123
|
+
deleted = ctx.buffer.span_text(y, x, y, line_len)
|
|
1124
|
+
ctx.buffer.begin_change_group
|
|
1125
|
+
ctx.buffer.delete_span(y, x, y, line_len)
|
|
1126
|
+
ctx.buffer.end_change_group
|
|
1127
|
+
store_delete_register(ctx, text: deleted, type: :charwise) unless deleted.empty?
|
|
1128
|
+
ctx.window.clamp_to_buffer(ctx.buffer)
|
|
1129
|
+
true
|
|
1130
|
+
end
|
|
1131
|
+
|
|
1132
|
+
def delete_word_forward(ctx, count)
|
|
1133
|
+
y = ctx.window.cursor_y
|
|
1134
|
+
x = ctx.window.cursor_x
|
|
1135
|
+
target = advance_word_forward(ctx.buffer, y, x, count)
|
|
1136
|
+
return true unless target
|
|
1137
|
+
|
|
1138
|
+
deleted = ctx.buffer.span_text(y, x, target[:row], target[:col])
|
|
1139
|
+
ctx.buffer.begin_change_group
|
|
1140
|
+
ctx.buffer.delete_span(y, x, target[:row], target[:col])
|
|
1141
|
+
ctx.buffer.end_change_group
|
|
1142
|
+
store_delete_register(ctx, text: deleted, type: :charwise) unless deleted.empty?
|
|
1143
|
+
ctx.window.clamp_to_buffer(ctx.buffer)
|
|
1144
|
+
true
|
|
1145
|
+
end
|
|
1146
|
+
|
|
1147
|
+
def delete_text_object_word(ctx, around:)
|
|
1148
|
+
span = word_object_span(ctx.buffer, ctx.window, around:)
|
|
1149
|
+
return false unless span
|
|
1150
|
+
|
|
1151
|
+
text = ctx.buffer.span_text(span[:start_row], span[:start_col], span[:end_row], span[:end_col])
|
|
1152
|
+
ctx.buffer.begin_change_group
|
|
1153
|
+
ctx.buffer.delete_span(span[:start_row], span[:start_col], span[:end_row], span[:end_col])
|
|
1154
|
+
ctx.buffer.end_change_group
|
|
1155
|
+
store_delete_register(ctx, text:, type: :charwise) unless text.empty?
|
|
1156
|
+
ctx.window.cursor_y = span[:start_row]
|
|
1157
|
+
ctx.window.cursor_x = span[:start_col]
|
|
1158
|
+
ctx.window.clamp_to_buffer(ctx.buffer)
|
|
1159
|
+
true
|
|
1160
|
+
end
|
|
1161
|
+
|
|
1162
|
+
def delete_text_object(ctx, motion)
|
|
1163
|
+
span = text_object_span(ctx.buffer, ctx.window, motion)
|
|
1164
|
+
return false unless span
|
|
1165
|
+
|
|
1166
|
+
text = ctx.buffer.span_text(span[:start_row], span[:start_col], span[:end_row], span[:end_col])
|
|
1167
|
+
ctx.buffer.begin_change_group
|
|
1168
|
+
ctx.buffer.delete_span(span[:start_row], span[:start_col], span[:end_row], span[:end_col])
|
|
1169
|
+
ctx.buffer.end_change_group
|
|
1170
|
+
store_delete_register(ctx, text:, type: :charwise) unless text.empty?
|
|
1171
|
+
ctx.window.cursor_y = span[:start_row]
|
|
1172
|
+
ctx.window.cursor_x = span[:start_col]
|
|
1173
|
+
ctx.window.clamp_to_buffer(ctx.buffer)
|
|
1174
|
+
true
|
|
1175
|
+
end
|
|
1176
|
+
|
|
1177
|
+
def yank_text_object_word(ctx, around:)
|
|
1178
|
+
span = word_object_span(ctx.buffer, ctx.window, around:)
|
|
1179
|
+
return false unless span
|
|
1180
|
+
|
|
1181
|
+
text = ctx.buffer.span_text(span[:start_row], span[:start_col], span[:end_row], span[:end_col])
|
|
1182
|
+
store_yank_register(ctx, text:, type: :charwise) unless text.empty?
|
|
1183
|
+
ctx.editor.echo("yanked")
|
|
1184
|
+
true
|
|
1185
|
+
end
|
|
1186
|
+
|
|
1187
|
+
def yank_text_object(ctx, motion)
|
|
1188
|
+
span = text_object_span(ctx.buffer, ctx.window, motion)
|
|
1189
|
+
return false unless span
|
|
1190
|
+
|
|
1191
|
+
text = ctx.buffer.span_text(span[:start_row], span[:start_col], span[:end_row], span[:end_col])
|
|
1192
|
+
store_yank_register(ctx, text:, type: :charwise) unless text.empty?
|
|
1193
|
+
ctx.editor.echo("yanked")
|
|
1194
|
+
true
|
|
1195
|
+
end
|
|
1196
|
+
|
|
1197
|
+
def advance_word_forward(buffer, row, col, count)
|
|
1198
|
+
text = buffer.lines.join("\n")
|
|
1199
|
+
flat = cursor_to_offset(buffer, row, col)
|
|
1200
|
+
idx = flat
|
|
1201
|
+
count.times do
|
|
1202
|
+
idx = next_word_start_offset(text, idx)
|
|
1203
|
+
return nil unless idx
|
|
1204
|
+
end
|
|
1205
|
+
offset_to_cursor(buffer, idx)
|
|
1206
|
+
end
|
|
1207
|
+
|
|
1208
|
+
def move_cursor_word(ctx, count:, kind:)
|
|
1209
|
+
buffer = ctx.buffer
|
|
1210
|
+
row = ctx.window.cursor_y
|
|
1211
|
+
col = ctx.window.cursor_x
|
|
1212
|
+
count = 1 if count.to_i <= 0
|
|
1213
|
+
target = { row:, col: }
|
|
1214
|
+
count.times do
|
|
1215
|
+
target =
|
|
1216
|
+
case kind
|
|
1217
|
+
when :forward_start then advance_word_forward(buffer, target[:row], target[:col], 1)
|
|
1218
|
+
when :backward_start then advance_word_backward(buffer, target[:row], target[:col], 1)
|
|
1219
|
+
when :forward_end then advance_word_end(buffer, target[:row], target[:col], 1)
|
|
1220
|
+
end
|
|
1221
|
+
break unless target
|
|
1222
|
+
end
|
|
1223
|
+
return unless target
|
|
1224
|
+
|
|
1225
|
+
ctx.window.cursor_y = target[:row]
|
|
1226
|
+
ctx.window.cursor_x = target[:col]
|
|
1227
|
+
ctx.window.clamp_to_buffer(buffer)
|
|
1228
|
+
end
|
|
1229
|
+
|
|
1230
|
+
def advance_word_backward(buffer, row, col, _count)
|
|
1231
|
+
text = buffer.lines.join("\n")
|
|
1232
|
+
idx = cursor_to_offset(buffer, row, col)
|
|
1233
|
+
idx = [idx - 1, 0].max
|
|
1234
|
+
while idx > 0 && char_class(text[idx]) == :space
|
|
1235
|
+
idx -= 1
|
|
1236
|
+
end
|
|
1237
|
+
cls = char_class(text[idx])
|
|
1238
|
+
while idx > 0 && char_class(text[idx - 1]) == cls && cls != :space
|
|
1239
|
+
idx -= 1
|
|
1240
|
+
end
|
|
1241
|
+
while idx > 0 && char_class(text[idx]) == :space
|
|
1242
|
+
idx += 1
|
|
1243
|
+
end
|
|
1244
|
+
offset_to_cursor(buffer, idx)
|
|
1245
|
+
end
|
|
1246
|
+
|
|
1247
|
+
def advance_word_end(buffer, row, col, _count)
|
|
1248
|
+
text = buffer.lines.join("\n")
|
|
1249
|
+
idx = cursor_to_offset(buffer, row, col)
|
|
1250
|
+
n = text.length
|
|
1251
|
+
while idx < n && char_class(text[idx]) == :space
|
|
1252
|
+
idx += 1
|
|
1253
|
+
end
|
|
1254
|
+
return nil if idx >= n
|
|
1255
|
+
|
|
1256
|
+
cls = char_class(text[idx])
|
|
1257
|
+
idx += 1 while idx + 1 < n && char_class(text[idx + 1]) == cls && cls != :space
|
|
1258
|
+
offset_to_cursor(buffer, idx)
|
|
1259
|
+
end
|
|
1260
|
+
|
|
1261
|
+
def next_word_start_offset(text, from_offset)
|
|
1262
|
+
i = [from_offset, 0].max
|
|
1263
|
+
n = text.length
|
|
1264
|
+
return nil if i >= n
|
|
1265
|
+
|
|
1266
|
+
cls = char_class(text[i])
|
|
1267
|
+
if cls == :word
|
|
1268
|
+
i += 1 while i < n && char_class(text[i]) == :word
|
|
1269
|
+
elsif cls == :space
|
|
1270
|
+
i += 1 while i < n && char_class(text[i]) == :space
|
|
1271
|
+
else
|
|
1272
|
+
i += 1
|
|
1273
|
+
end
|
|
1274
|
+
i += 1 while i < n && char_class(text[i]) == :space
|
|
1275
|
+
return n if i > n
|
|
1276
|
+
|
|
1277
|
+
i <= n ? i : nil
|
|
1278
|
+
end
|
|
1279
|
+
|
|
1280
|
+
def char_class(ch)
|
|
1281
|
+
return :space if ch == "\n"
|
|
1282
|
+
return :space if ch =~ /\s/
|
|
1283
|
+
return :word if ch =~ /[[:alnum:]_]/
|
|
1284
|
+
:punct
|
|
1285
|
+
end
|
|
1286
|
+
|
|
1287
|
+
def word_object_span(buffer, window, around:)
|
|
1288
|
+
row = window.cursor_y
|
|
1289
|
+
line = buffer.line_at(row)
|
|
1290
|
+
return nil if line.empty?
|
|
1291
|
+
|
|
1292
|
+
x = [window.cursor_x, line.length - 1].min
|
|
1293
|
+
x = 0 if x.negative?
|
|
1294
|
+
|
|
1295
|
+
if x < line.length && line[x] =~ /\s/
|
|
1296
|
+
if around
|
|
1297
|
+
left = x
|
|
1298
|
+
left -= 1 while left.positive? && line[left - 1] =~ /\s/
|
|
1299
|
+
right = x
|
|
1300
|
+
right += 1 while right < line.length && line[right] =~ /\s/
|
|
1301
|
+
return { start_row: row, start_col: left, end_row: row, end_col: right }
|
|
1302
|
+
end
|
|
1303
|
+
|
|
1304
|
+
nxt = line.index(/\S/, x)
|
|
1305
|
+
return nil unless nxt
|
|
1306
|
+
x = nxt
|
|
1307
|
+
end
|
|
1308
|
+
|
|
1309
|
+
cls = line[x] =~ /[[:alnum:]_]/ ? :word : :punct
|
|
1310
|
+
start_col = x
|
|
1311
|
+
start_col -= 1 while start_col.positive? && same_word_class?(line[start_col - 1], cls)
|
|
1312
|
+
end_col = x + 1
|
|
1313
|
+
end_col += 1 while end_col < line.length && same_word_class?(line[end_col], cls)
|
|
1314
|
+
|
|
1315
|
+
if around
|
|
1316
|
+
while end_col < line.length && line[end_col] =~ /\s/
|
|
1317
|
+
end_col += 1
|
|
1318
|
+
end
|
|
1319
|
+
end
|
|
1320
|
+
|
|
1321
|
+
{ start_row: row, start_col:, end_row: row, end_col: }
|
|
1322
|
+
end
|
|
1323
|
+
|
|
1324
|
+
def text_object_span(buffer, window, motion)
|
|
1325
|
+
around = motion.start_with?("a")
|
|
1326
|
+
kind = motion[1..]
|
|
1327
|
+
case kind
|
|
1328
|
+
when "w"
|
|
1329
|
+
word_object_span(buffer, window, around:)
|
|
1330
|
+
when "p"
|
|
1331
|
+
paragraph_object_span(buffer, window, around:)
|
|
1332
|
+
when '"'
|
|
1333
|
+
quote_object_span(buffer, window, quote: '"', around:)
|
|
1334
|
+
when "'"
|
|
1335
|
+
quote_object_span(buffer, window, quote: "'", around:)
|
|
1336
|
+
when "`"
|
|
1337
|
+
quote_object_span(buffer, window, quote: "`", around:)
|
|
1338
|
+
when ")"
|
|
1339
|
+
paren_object_span(buffer, window, open: "(", close: ")", around:)
|
|
1340
|
+
when "]"
|
|
1341
|
+
paren_object_span(buffer, window, open: "[", close: "]", around:)
|
|
1342
|
+
when "}"
|
|
1343
|
+
paren_object_span(buffer, window, open: "{", close: "}", around:)
|
|
1344
|
+
else
|
|
1345
|
+
nil
|
|
1346
|
+
end
|
|
1347
|
+
end
|
|
1348
|
+
|
|
1349
|
+
def text_object_motion?(motion)
|
|
1350
|
+
motion.is_a?(String) && motion.match?(/\A[ia](w|p|["'`\)\]\}])\z/)
|
|
1351
|
+
end
|
|
1352
|
+
|
|
1353
|
+
def quote_object_span(buffer, window, quote:, around:)
|
|
1354
|
+
row = window.cursor_y
|
|
1355
|
+
line = buffer.line_at(row)
|
|
1356
|
+
return nil if line.empty?
|
|
1357
|
+
x = [window.cursor_x, line.length - 1].min
|
|
1358
|
+
return nil if x.negative?
|
|
1359
|
+
|
|
1360
|
+
left = find_left_quote(line, x, quote)
|
|
1361
|
+
right_from = [x, (left ? left + 1 : 0)].max
|
|
1362
|
+
right = find_right_quote(line, right_from, quote)
|
|
1363
|
+
return nil unless left && right && left < right
|
|
1364
|
+
|
|
1365
|
+
if around
|
|
1366
|
+
{ start_row: row, start_col: left, end_row: row, end_col: right + 1 }
|
|
1367
|
+
else
|
|
1368
|
+
{ start_row: row, start_col: left + 1, end_row: row, end_col: right }
|
|
1369
|
+
end
|
|
1370
|
+
end
|
|
1371
|
+
|
|
1372
|
+
def paren_object_span(buffer, window, open:, close:, around:)
|
|
1373
|
+
row = window.cursor_y
|
|
1374
|
+
line = buffer.line_at(row)
|
|
1375
|
+
return nil if line.empty?
|
|
1376
|
+
x = [window.cursor_x, line.length - 1].min
|
|
1377
|
+
return nil if x.negative?
|
|
1378
|
+
|
|
1379
|
+
left = find_matching_left_delim(line, x, open:, close:)
|
|
1380
|
+
right = find_matching_right_delim(line, [x, left || 0].max, open:, close:)
|
|
1381
|
+
return nil unless left && right && left < right
|
|
1382
|
+
|
|
1383
|
+
if around
|
|
1384
|
+
{ start_row: row, start_col: left, end_row: row, end_col: right + 1 }
|
|
1385
|
+
else
|
|
1386
|
+
{ start_row: row, start_col: left + 1, end_row: row, end_col: right }
|
|
1387
|
+
end
|
|
1388
|
+
end
|
|
1389
|
+
|
|
1390
|
+
def paragraph_object_span(buffer, window, around:)
|
|
1391
|
+
row = [[window.cursor_y, 0].max, buffer.line_count - 1].min
|
|
1392
|
+
return nil if row.negative?
|
|
1393
|
+
|
|
1394
|
+
blank = buffer.line_at(row).strip.empty?
|
|
1395
|
+
start_row = row
|
|
1396
|
+
end_row = row
|
|
1397
|
+
|
|
1398
|
+
while start_row.positive? && (buffer.line_at(start_row - 1).strip.empty? == blank)
|
|
1399
|
+
start_row -= 1
|
|
1400
|
+
end
|
|
1401
|
+
while end_row + 1 < buffer.line_count && (buffer.line_at(end_row + 1).strip.empty? == blank)
|
|
1402
|
+
end_row += 1
|
|
1403
|
+
end
|
|
1404
|
+
|
|
1405
|
+
if around && !blank
|
|
1406
|
+
if end_row + 1 < buffer.line_count && buffer.line_at(end_row + 1).strip.empty?
|
|
1407
|
+
while end_row + 1 < buffer.line_count && buffer.line_at(end_row + 1).strip.empty?
|
|
1408
|
+
end_row += 1
|
|
1409
|
+
end
|
|
1410
|
+
elsif start_row.positive? && buffer.line_at(start_row - 1).strip.empty?
|
|
1411
|
+
while start_row.positive? && buffer.line_at(start_row - 1).strip.empty?
|
|
1412
|
+
start_row -= 1
|
|
1413
|
+
end
|
|
1414
|
+
end
|
|
1415
|
+
end
|
|
1416
|
+
|
|
1417
|
+
if around && end_row + 1 < buffer.line_count
|
|
1418
|
+
{
|
|
1419
|
+
start_row: start_row,
|
|
1420
|
+
start_col: 0,
|
|
1421
|
+
end_row: end_row + 1,
|
|
1422
|
+
end_col: 0
|
|
1423
|
+
}
|
|
1424
|
+
else
|
|
1425
|
+
{
|
|
1426
|
+
start_row: start_row,
|
|
1427
|
+
start_col: 0,
|
|
1428
|
+
end_row: end_row,
|
|
1429
|
+
end_col: buffer.line_length(end_row)
|
|
1430
|
+
}
|
|
1431
|
+
end
|
|
1432
|
+
end
|
|
1433
|
+
|
|
1434
|
+
def find_left_quote(line, x, quote)
|
|
1435
|
+
i = x
|
|
1436
|
+
while i >= 0
|
|
1437
|
+
return i if line[i] == quote && !escaped?(line, i)
|
|
1438
|
+
i -= 1
|
|
1439
|
+
end
|
|
1440
|
+
nil
|
|
1441
|
+
end
|
|
1442
|
+
|
|
1443
|
+
def find_right_quote(line, x, quote)
|
|
1444
|
+
i = x
|
|
1445
|
+
while i < line.length
|
|
1446
|
+
return i if line[i] == quote && !escaped?(line, i)
|
|
1447
|
+
i += 1
|
|
1448
|
+
end
|
|
1449
|
+
nil
|
|
1450
|
+
end
|
|
1451
|
+
|
|
1452
|
+
def escaped?(line, idx)
|
|
1453
|
+
backslashes = 0
|
|
1454
|
+
i = idx - 1
|
|
1455
|
+
while i >= 0 && line[i] == "\\"
|
|
1456
|
+
backslashes += 1
|
|
1457
|
+
i -= 1
|
|
1458
|
+
end
|
|
1459
|
+
backslashes.odd?
|
|
1460
|
+
end
|
|
1461
|
+
|
|
1462
|
+
def find_matching_left_delim(line, x, open:, close:)
|
|
1463
|
+
depth = 0
|
|
1464
|
+
i = x
|
|
1465
|
+
while i >= 0
|
|
1466
|
+
ch = line[i]
|
|
1467
|
+
if ch == close
|
|
1468
|
+
depth += 1
|
|
1469
|
+
elsif ch == open
|
|
1470
|
+
return i if depth.zero?
|
|
1471
|
+
depth -= 1
|
|
1472
|
+
end
|
|
1473
|
+
i -= 1
|
|
1474
|
+
end
|
|
1475
|
+
nil
|
|
1476
|
+
end
|
|
1477
|
+
|
|
1478
|
+
def find_matching_right_delim(line, x, open:, close:)
|
|
1479
|
+
depth = 0
|
|
1480
|
+
i = x
|
|
1481
|
+
while i < line.length
|
|
1482
|
+
ch = line[i]
|
|
1483
|
+
if ch == open
|
|
1484
|
+
depth += 1
|
|
1485
|
+
elsif ch == close
|
|
1486
|
+
if depth <= 1
|
|
1487
|
+
return i
|
|
1488
|
+
end
|
|
1489
|
+
depth -= 1
|
|
1490
|
+
end
|
|
1491
|
+
i += 1
|
|
1492
|
+
end
|
|
1493
|
+
nil
|
|
1494
|
+
end
|
|
1495
|
+
|
|
1496
|
+
def same_word_class?(ch, cls)
|
|
1497
|
+
return false if ch.nil?
|
|
1498
|
+
case cls
|
|
1499
|
+
when :word then ch =~ /[[:alnum:]_]/
|
|
1500
|
+
when :punct then !(ch =~ /[[:alnum:]_\s]/)
|
|
1501
|
+
else false
|
|
1502
|
+
end
|
|
1503
|
+
end
|
|
1504
|
+
|
|
1505
|
+
def cursor_to_offset(buffer, row, col)
|
|
1506
|
+
offset = 0
|
|
1507
|
+
row.times { |r| offset += buffer.line_length(r) + 1 }
|
|
1508
|
+
offset + col
|
|
1509
|
+
end
|
|
1510
|
+
|
|
1511
|
+
def offset_to_cursor(buffer, offset)
|
|
1512
|
+
remaining = offset
|
|
1513
|
+
(0...buffer.line_count).each do |row|
|
|
1514
|
+
len = buffer.line_length(row)
|
|
1515
|
+
return { row:, col: [remaining, len].min } if remaining <= len
|
|
1516
|
+
remaining -= (len + 1)
|
|
1517
|
+
end
|
|
1518
|
+
{ row: buffer.line_count - 1, col: buffer.line_length(buffer.line_count - 1) }
|
|
1519
|
+
end
|
|
1520
|
+
|
|
1521
|
+
def paste_register(ctx, before:, count:)
|
|
1522
|
+
reg_name = ctx.editor.consume_active_register("\"")
|
|
1523
|
+
reg = ctx.editor.get_register(reg_name)
|
|
1524
|
+
unless reg
|
|
1525
|
+
ctx.editor.echo("Register is empty")
|
|
1526
|
+
return
|
|
1527
|
+
end
|
|
1528
|
+
|
|
1529
|
+
if reg[:type] == :linewise
|
|
1530
|
+
paste_linewise(ctx, reg[:text], before:, count:)
|
|
1531
|
+
else
|
|
1532
|
+
paste_charwise(ctx, reg[:text], before:, count:)
|
|
1533
|
+
end
|
|
1534
|
+
end
|
|
1535
|
+
|
|
1536
|
+
def paste_linewise(ctx, text, before:, count:)
|
|
1537
|
+
lines = text.sub(/\n\z/, "").split("\n", -1)
|
|
1538
|
+
return if lines.empty?
|
|
1539
|
+
|
|
1540
|
+
insert_at = before ? ctx.window.cursor_y : (ctx.window.cursor_y + 1)
|
|
1541
|
+
ctx.buffer.begin_change_group
|
|
1542
|
+
count.times { ctx.buffer.insert_lines_at(insert_at, lines) }
|
|
1543
|
+
ctx.buffer.end_change_group
|
|
1544
|
+
ctx.window.cursor_y = insert_at
|
|
1545
|
+
ctx.window.cursor_x = 0
|
|
1546
|
+
ctx.window.clamp_to_buffer(ctx.buffer)
|
|
1547
|
+
end
|
|
1548
|
+
|
|
1549
|
+
def store_register(ctx, text:, type:, kind: :generic)
|
|
1550
|
+
name = ctx.editor.consume_active_register("\"")
|
|
1551
|
+
if kind == :generic
|
|
1552
|
+
ctx.editor.set_register(name, text:, type:)
|
|
1553
|
+
else
|
|
1554
|
+
ctx.editor.store_operator_register(name, text:, type:, kind:)
|
|
1555
|
+
end
|
|
1556
|
+
end
|
|
1557
|
+
|
|
1558
|
+
def store_delete_register(ctx, text:, type:)
|
|
1559
|
+
store_register(ctx, text:, type:, kind: :delete)
|
|
1560
|
+
end
|
|
1561
|
+
|
|
1562
|
+
def store_yank_register(ctx, text:, type:)
|
|
1563
|
+
store_register(ctx, text:, type:, kind: :yank)
|
|
1564
|
+
end
|
|
1565
|
+
|
|
1566
|
+
def record_jump(ctx)
|
|
1567
|
+
ctx.editor.push_jump_location(ctx.editor.current_location)
|
|
1568
|
+
end
|
|
1569
|
+
|
|
1570
|
+
def find_matching_bracket(buffer, row, col, open_ch, close_ch, direction)
|
|
1571
|
+
depth = 1
|
|
1572
|
+
pos = { row: row, col: col }
|
|
1573
|
+
loop do
|
|
1574
|
+
pos = direction == :forward ? next_buffer_pos(buffer, pos[:row], pos[:col]) : prev_buffer_pos(buffer, pos[:row], pos[:col])
|
|
1575
|
+
return nil unless pos
|
|
1576
|
+
|
|
1577
|
+
ch = buffer.line_at(pos[:row])[pos[:col]]
|
|
1578
|
+
next unless ch
|
|
1579
|
+
|
|
1580
|
+
if ch == open_ch
|
|
1581
|
+
depth += 1
|
|
1582
|
+
elsif ch == close_ch
|
|
1583
|
+
depth -= 1
|
|
1584
|
+
return pos if depth.zero?
|
|
1585
|
+
end
|
|
1586
|
+
end
|
|
1587
|
+
end
|
|
1588
|
+
|
|
1589
|
+
def next_buffer_pos(buffer, row, col)
|
|
1590
|
+
line = buffer.line_at(row)
|
|
1591
|
+
if col + 1 < line.length
|
|
1592
|
+
{ row: row, col: col + 1 }
|
|
1593
|
+
elsif row + 1 < buffer.line_count
|
|
1594
|
+
{ row: row + 1, col: 0 }
|
|
1595
|
+
end
|
|
1596
|
+
end
|
|
1597
|
+
|
|
1598
|
+
def prev_buffer_pos(buffer, row, col)
|
|
1599
|
+
if col.positive?
|
|
1600
|
+
{ row: row, col: col - 1 }
|
|
1601
|
+
elsif row.positive?
|
|
1602
|
+
prev_row = row - 1
|
|
1603
|
+
prev_len = buffer.line_length(prev_row)
|
|
1604
|
+
return { row: prev_row, col: prev_len - 1 } if prev_len.positive?
|
|
1605
|
+
|
|
1606
|
+
{ row: prev_row, col: 0 }
|
|
1607
|
+
end
|
|
1608
|
+
end
|
|
1609
|
+
|
|
1610
|
+
def materialize_intro_buffer_if_needed(ctx)
|
|
1611
|
+
ctx.editor.materialize_intro_buffer!
|
|
1612
|
+
nil
|
|
1613
|
+
end
|
|
1614
|
+
|
|
1615
|
+
def ex_set_common(ctx, argv, scope:)
|
|
1616
|
+
editor = ctx.editor
|
|
1617
|
+
if argv.empty?
|
|
1618
|
+
items = editor.option_snapshot(window: ctx.window, buffer: ctx.buffer).map do |opt|
|
|
1619
|
+
format_option_value(opt[:name], opt[:effective])
|
|
1620
|
+
end
|
|
1621
|
+
ctx.editor.echo(items.join(" "))
|
|
1622
|
+
return
|
|
1623
|
+
end
|
|
1624
|
+
|
|
1625
|
+
output = []
|
|
1626
|
+
argv.each do |token|
|
|
1627
|
+
output.concat(handle_set_token(ctx, token, scope:))
|
|
1628
|
+
end
|
|
1629
|
+
ctx.editor.echo(output.join(" ")) unless output.empty?
|
|
1630
|
+
end
|
|
1631
|
+
|
|
1632
|
+
def handle_set_token(ctx, token, scope:)
|
|
1633
|
+
t = token.to_s
|
|
1634
|
+
return [] if t.empty?
|
|
1635
|
+
|
|
1636
|
+
if t.end_with?("?")
|
|
1637
|
+
name = t[0...-1]
|
|
1638
|
+
val = ctx.editor.get_option(name, scope: resolve_option_scope(ctx.editor, name, scope), window: ctx.window, buffer: ctx.buffer)
|
|
1639
|
+
return ["#{name}=#{format_option_scalar(val)}"]
|
|
1640
|
+
end
|
|
1641
|
+
|
|
1642
|
+
if t.start_with?("no")
|
|
1643
|
+
name = t[2..]
|
|
1644
|
+
return [apply_bool_option(ctx, name, false, scope:)]
|
|
1645
|
+
end
|
|
1646
|
+
|
|
1647
|
+
if t.start_with?("inv")
|
|
1648
|
+
name = t[3..]
|
|
1649
|
+
cur = !!ctx.editor.get_option(name, scope: :effective, window: ctx.window, buffer: ctx.buffer)
|
|
1650
|
+
return [apply_bool_option(ctx, name, !cur, scope:)]
|
|
1651
|
+
end
|
|
1652
|
+
|
|
1653
|
+
if t.include?("=")
|
|
1654
|
+
name, raw = t.split("=", 2)
|
|
1655
|
+
val = parse_option_value(ctx.editor, name, raw)
|
|
1656
|
+
applied = ctx.editor.set_option(name, val, scope: resolve_option_scope(ctx.editor, name, scope), window: ctx.window, buffer: ctx.buffer)
|
|
1657
|
+
return ["#{name}=#{format_option_scalar(applied)}"]
|
|
1658
|
+
end
|
|
1659
|
+
|
|
1660
|
+
if bool_option?(ctx.editor, t)
|
|
1661
|
+
return [apply_bool_option(ctx, t, true, scope:)]
|
|
1662
|
+
end
|
|
1663
|
+
|
|
1664
|
+
val = ctx.editor.get_option(t, scope: resolve_option_scope(ctx.editor, t, scope), window: ctx.window, buffer: ctx.buffer)
|
|
1665
|
+
["#{t}=#{format_option_scalar(val)}"]
|
|
1666
|
+
end
|
|
1667
|
+
|
|
1668
|
+
def apply_bool_option(ctx, name, value, scope:)
|
|
1669
|
+
unless bool_option?(ctx.editor, name)
|
|
1670
|
+
raise RuVim::CommandError, "#{name} is not a boolean option"
|
|
1671
|
+
end
|
|
1672
|
+
applied = ctx.editor.set_option(name, value, scope: resolve_option_scope(ctx.editor, name, scope), window: ctx.window, buffer: ctx.buffer)
|
|
1673
|
+
applied ? name.to_s : "no#{name}"
|
|
1674
|
+
end
|
|
1675
|
+
|
|
1676
|
+
def resolve_option_scope(editor, name, requested_scope)
|
|
1677
|
+
case requested_scope
|
|
1678
|
+
when :auto
|
|
1679
|
+
:auto
|
|
1680
|
+
when :global
|
|
1681
|
+
:global
|
|
1682
|
+
when :local
|
|
1683
|
+
editor.option_default_scope(name) == :buffer ? :buffer : :window
|
|
1684
|
+
else
|
|
1685
|
+
requested_scope
|
|
1686
|
+
end
|
|
1687
|
+
end
|
|
1688
|
+
|
|
1689
|
+
def parse_option_value(editor, name, raw)
|
|
1690
|
+
defn = editor.option_def(name)
|
|
1691
|
+
return raw unless defn
|
|
1692
|
+
|
|
1693
|
+
case defn[:type]
|
|
1694
|
+
when :bool
|
|
1695
|
+
parse_bool(raw)
|
|
1696
|
+
when :int
|
|
1697
|
+
Integer(raw)
|
|
1698
|
+
else
|
|
1699
|
+
raw
|
|
1700
|
+
end
|
|
1701
|
+
rescue ArgumentError
|
|
1702
|
+
raise RuVim::CommandError, "Invalid value for #{name}: #{raw}"
|
|
1703
|
+
end
|
|
1704
|
+
|
|
1705
|
+
def parse_bool(raw)
|
|
1706
|
+
case raw.to_s.downcase
|
|
1707
|
+
when "1", "true", "on", "yes" then true
|
|
1708
|
+
when "0", "false", "off", "no" then false
|
|
1709
|
+
else
|
|
1710
|
+
raise ArgumentError
|
|
1711
|
+
end
|
|
1712
|
+
end
|
|
1713
|
+
|
|
1714
|
+
def bool_option?(editor, name)
|
|
1715
|
+
editor.option_def(name)&.dig(:type) == :bool
|
|
1716
|
+
end
|
|
1717
|
+
|
|
1718
|
+
def format_option_value(name, value)
|
|
1719
|
+
if value == true
|
|
1720
|
+
name.to_s
|
|
1721
|
+
elsif value == false
|
|
1722
|
+
"no#{name}"
|
|
1723
|
+
else
|
|
1724
|
+
"#{name}=#{format_option_scalar(value)}"
|
|
1725
|
+
end
|
|
1726
|
+
end
|
|
1727
|
+
|
|
1728
|
+
def format_option_scalar(value)
|
|
1729
|
+
value.nil? ? "nil" : value.to_s
|
|
1730
|
+
end
|
|
1731
|
+
|
|
1732
|
+
def ex_command_help_line(spec)
|
|
1733
|
+
aliases = spec.aliases.empty? ? "" : " aliases=#{spec.aliases.join(',')}"
|
|
1734
|
+
nargs = " nargs=#{spec.nargs}"
|
|
1735
|
+
bang = spec.bang ? " !" : ""
|
|
1736
|
+
src = spec.source == :user ? " [user]" : ""
|
|
1737
|
+
":#{spec.name}#{bang} - #{spec.desc}#{aliases}#{nargs}#{src}"
|
|
1738
|
+
end
|
|
1739
|
+
|
|
1740
|
+
def option_help_line(name)
|
|
1741
|
+
case name
|
|
1742
|
+
when "number"
|
|
1743
|
+
"number (bool, window-local): line numbers. :set number / :set nonumber"
|
|
1744
|
+
when "tabstop"
|
|
1745
|
+
"tabstop (int, buffer-local): tab display width. ex: :set tabstop=4"
|
|
1746
|
+
when "relativenumber"
|
|
1747
|
+
"relativenumber (bool, window-local): show relative line numbers. ex: :set relativenumber"
|
|
1748
|
+
when "ignorecase"
|
|
1749
|
+
"ignorecase (bool, global): case-insensitive search unless smartcase + uppercase pattern"
|
|
1750
|
+
when "smartcase"
|
|
1751
|
+
"smartcase (bool, global): with ignorecase, uppercase in pattern makes search case-sensitive"
|
|
1752
|
+
when "hlsearch"
|
|
1753
|
+
"hlsearch (bool, global): highlight search matches on screen"
|
|
1754
|
+
when "filetype"
|
|
1755
|
+
"filetype (string, buffer-local): used for ftplugin and filetype-local keymaps"
|
|
1756
|
+
else
|
|
1757
|
+
"No option help: #{name}"
|
|
1758
|
+
end
|
|
1759
|
+
end
|
|
1760
|
+
|
|
1761
|
+
def help_text_to_lines(topic, text)
|
|
1762
|
+
[
|
|
1763
|
+
"RuVim help: #{topic}",
|
|
1764
|
+
"",
|
|
1765
|
+
*text.to_s.scan(/.{1,78}(?:\s+|$)|.{1,78}/).map(&:rstrip)
|
|
1766
|
+
]
|
|
1767
|
+
end
|
|
1768
|
+
|
|
1769
|
+
def paste_charwise(ctx, text, before:, count:)
|
|
1770
|
+
y = ctx.window.cursor_y
|
|
1771
|
+
x = ctx.window.cursor_x
|
|
1772
|
+
insert_col = before ? x : [x + 1, ctx.buffer.line_length(y)].min
|
|
1773
|
+
|
|
1774
|
+
ctx.buffer.begin_change_group
|
|
1775
|
+
count.times do
|
|
1776
|
+
y, insert_col = ctx.buffer.insert_text(y, insert_col, text)
|
|
1777
|
+
end
|
|
1778
|
+
ctx.buffer.end_change_group
|
|
1779
|
+
ctx.window.cursor_y = y
|
|
1780
|
+
ctx.window.cursor_x = [insert_col - 1, 0].max
|
|
1781
|
+
ctx.window.clamp_to_buffer(ctx.buffer)
|
|
1782
|
+
end
|
|
1783
|
+
|
|
1784
|
+
def repeat_search(ctx, forward:, count:)
|
|
1785
|
+
prev = ctx.editor.last_search
|
|
1786
|
+
unless prev
|
|
1787
|
+
ctx.editor.echo("No previous search")
|
|
1788
|
+
return
|
|
1789
|
+
end
|
|
1790
|
+
|
|
1791
|
+
direction = forward ? prev[:direction] : invert_direction(prev[:direction])
|
|
1792
|
+
move_to_search(ctx, pattern: prev[:pattern], direction:, count:)
|
|
1793
|
+
end
|
|
1794
|
+
|
|
1795
|
+
def invert_direction(direction)
|
|
1796
|
+
direction.to_sym == :forward ? :backward : :forward
|
|
1797
|
+
end
|
|
1798
|
+
|
|
1799
|
+
def move_to_search(ctx, pattern:, direction:, count:)
|
|
1800
|
+
count = 1 if count.to_i <= 0
|
|
1801
|
+
regex = compile_search_regex(pattern, editor: ctx.editor, window: ctx.window, buffer: ctx.buffer)
|
|
1802
|
+
count.times do
|
|
1803
|
+
match = find_next_match(ctx.buffer, ctx.window, regex, direction: direction)
|
|
1804
|
+
unless match
|
|
1805
|
+
ctx.editor.echo("Pattern not found: #{pattern}")
|
|
1806
|
+
return
|
|
1807
|
+
end
|
|
1808
|
+
ctx.window.cursor_y = match[:row]
|
|
1809
|
+
ctx.window.cursor_x = match[:col]
|
|
1810
|
+
end
|
|
1811
|
+
ctx.editor.echo("/#{pattern}")
|
|
1812
|
+
end
|
|
1813
|
+
|
|
1814
|
+
def find_next_match(buffer, window, regex, direction:)
|
|
1815
|
+
return nil unless regex
|
|
1816
|
+
|
|
1817
|
+
if direction.to_sym == :backward
|
|
1818
|
+
find_backward_match(buffer, window, regex)
|
|
1819
|
+
else
|
|
1820
|
+
find_forward_match(buffer, window, regex)
|
|
1821
|
+
end
|
|
1822
|
+
end
|
|
1823
|
+
|
|
1824
|
+
def find_forward_match(buffer, window, regex)
|
|
1825
|
+
start_row = window.cursor_y
|
|
1826
|
+
start_col = window.cursor_x + 1
|
|
1827
|
+
last_row = buffer.line_count - 1
|
|
1828
|
+
|
|
1829
|
+
(0..last_row).each do |offset|
|
|
1830
|
+
row = (start_row + offset) % (last_row + 1)
|
|
1831
|
+
line = buffer.line_at(row)
|
|
1832
|
+
col_from = row == start_row ? start_col : 0
|
|
1833
|
+
m = regex.match(line, col_from)
|
|
1834
|
+
return { row:, col: m.begin(0) } if m
|
|
1835
|
+
end
|
|
1836
|
+
nil
|
|
1837
|
+
end
|
|
1838
|
+
|
|
1839
|
+
def find_backward_match(buffer, window, regex)
|
|
1840
|
+
start_row = window.cursor_y
|
|
1841
|
+
start_col = [window.cursor_x - 1, buffer.line_length(start_row)].min
|
|
1842
|
+
last_row = buffer.line_count - 1
|
|
1843
|
+
|
|
1844
|
+
(0..last_row).each do |offset|
|
|
1845
|
+
row = (start_row - offset) % (last_row + 1)
|
|
1846
|
+
line = buffer.line_at(row)
|
|
1847
|
+
idx = last_regex_match_before(line, regex, row == start_row ? start_col : line.length)
|
|
1848
|
+
return { row:, col: idx } if idx
|
|
1849
|
+
end
|
|
1850
|
+
nil
|
|
1851
|
+
end
|
|
1852
|
+
|
|
1853
|
+
def last_regex_match_before(line, regex, max_col)
|
|
1854
|
+
idx = nil
|
|
1855
|
+
offset = 0
|
|
1856
|
+
while (m = regex.match(line, offset))
|
|
1857
|
+
break if m.begin(0) > max_col
|
|
1858
|
+
idx = m.begin(0) if m.begin(0) <= max_col
|
|
1859
|
+
next_offset = m.begin(0) == m.end(0) ? m.begin(0) + 1 : m.end(0)
|
|
1860
|
+
break if next_offset > line.length
|
|
1861
|
+
offset = next_offset
|
|
1862
|
+
end
|
|
1863
|
+
idx
|
|
1864
|
+
end
|
|
1865
|
+
|
|
1866
|
+
def compile_search_regex(pattern, editor: nil, window: nil, buffer: nil)
|
|
1867
|
+
flags = search_regexp_flags(pattern.to_s, editor:, window:, buffer:)
|
|
1868
|
+
Regexp.new(pattern.to_s, flags)
|
|
1869
|
+
rescue RegexpError => e
|
|
1870
|
+
raise RuVim::CommandError, "Invalid regex: #{e.message}"
|
|
1871
|
+
end
|
|
1872
|
+
|
|
1873
|
+
def search_regexp_flags(pattern, editor:, window:, buffer:)
|
|
1874
|
+
return 0 unless editor
|
|
1875
|
+
|
|
1876
|
+
ignorecase = !!editor.effective_option("ignorecase", window:, buffer:)
|
|
1877
|
+
return 0 unless ignorecase
|
|
1878
|
+
|
|
1879
|
+
smartcase = !!editor.effective_option("smartcase", window:, buffer:)
|
|
1880
|
+
if smartcase && pattern.match?(/[A-Z]/)
|
|
1881
|
+
0
|
|
1882
|
+
else
|
|
1883
|
+
Regexp::IGNORECASE
|
|
1884
|
+
end
|
|
1885
|
+
rescue StandardError
|
|
1886
|
+
0
|
|
1887
|
+
end
|
|
1888
|
+
end
|
|
1889
|
+
end
|