ruvim 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +4 -0
- data/AGENTS.md +84 -0
- data/CLAUDE.md +1 -0
- data/docs/binding.md +29 -0
- data/docs/command.md +101 -0
- data/docs/config.md +203 -84
- data/docs/done.md +21 -0
- data/docs/lib_cleanup_report.md +79 -0
- data/docs/plugin.md +13 -15
- data/docs/spec.md +195 -33
- data/docs/todo.md +183 -10
- data/docs/tutorial.md +1 -1
- data/docs/vim_diff.md +94 -171
- data/lib/ruvim/app.rb +1543 -172
- data/lib/ruvim/buffer.rb +35 -1
- data/lib/ruvim/cli.rb +12 -3
- data/lib/ruvim/clipboard.rb +2 -0
- data/lib/ruvim/command_invocation.rb +3 -1
- data/lib/ruvim/command_line.rb +2 -0
- data/lib/ruvim/command_registry.rb +2 -0
- data/lib/ruvim/config_dsl.rb +2 -0
- data/lib/ruvim/config_loader.rb +21 -5
- data/lib/ruvim/context.rb +2 -7
- data/lib/ruvim/dispatcher.rb +153 -13
- data/lib/ruvim/display_width.rb +28 -2
- data/lib/ruvim/editor.rb +622 -69
- data/lib/ruvim/ex_command_registry.rb +2 -0
- data/lib/ruvim/global_commands.rb +1386 -114
- data/lib/ruvim/highlighter.rb +16 -21
- data/lib/ruvim/input.rb +52 -29
- data/lib/ruvim/keymap_manager.rb +83 -0
- data/lib/ruvim/keyword_chars.rb +48 -0
- data/lib/ruvim/lang/base.rb +25 -0
- data/lib/ruvim/lang/csv.rb +18 -0
- data/lib/ruvim/lang/json.rb +18 -0
- data/lib/ruvim/lang/markdown.rb +170 -0
- data/lib/ruvim/lang/ruby.rb +236 -0
- data/lib/ruvim/lang/scheme.rb +44 -0
- data/lib/ruvim/lang/tsv.rb +19 -0
- data/lib/ruvim/rich_view/markdown_renderer.rb +248 -0
- data/lib/ruvim/rich_view/table_renderer.rb +176 -0
- data/lib/ruvim/rich_view.rb +93 -0
- data/lib/ruvim/screen.rb +851 -119
- data/lib/ruvim/terminal.rb +18 -1
- data/lib/ruvim/text_metrics.rb +28 -0
- data/lib/ruvim/version.rb +2 -2
- data/lib/ruvim/window.rb +37 -10
- data/lib/ruvim.rb +15 -0
- data/test/app_completion_test.rb +174 -0
- data/test/app_dot_repeat_test.rb +13 -0
- data/test/app_motion_test.rb +110 -2
- data/test/app_scenario_test.rb +998 -0
- data/test/app_startup_test.rb +197 -0
- data/test/arglist_test.rb +113 -0
- data/test/buffer_test.rb +49 -30
- data/test/config_loader_test.rb +37 -0
- data/test/dispatcher_test.rb +438 -0
- data/test/display_width_test.rb +18 -0
- data/test/editor_register_test.rb +23 -0
- data/test/fixtures/render_basic_snapshot.txt +7 -8
- data/test/fixtures/render_basic_snapshot_nonumber.txt +1 -2
- data/test/fixtures/render_unicode_scrolled_snapshot.txt +6 -7
- data/test/highlighter_test.rb +121 -0
- data/test/indent_test.rb +201 -0
- data/test/input_screen_integration_test.rb +65 -14
- data/test/markdown_renderer_test.rb +279 -0
- data/test/on_save_hook_test.rb +150 -0
- data/test/rich_view_test.rb +478 -0
- data/test/screen_test.rb +470 -0
- data/test/window_test.rb +26 -0
- metadata +37 -2
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tempfile"
|
|
4
|
+
require "open3"
|
|
5
|
+
|
|
1
6
|
module RuVim
|
|
2
7
|
class GlobalCommands
|
|
3
8
|
include Singleton
|
|
4
9
|
|
|
5
|
-
def call(spec_call, ctx, argv: [], kwargs: {}, bang: false, count:
|
|
10
|
+
def call(spec_call, ctx, argv: [], kwargs: {}, bang: false, count: nil)
|
|
6
11
|
case spec_call
|
|
7
12
|
when Symbol, String
|
|
8
13
|
public_send(spec_call.to_sym, ctx, argv: argv, kwargs: kwargs, bang: bang, count: count)
|
|
@@ -12,19 +17,19 @@ module RuVim
|
|
|
12
17
|
end
|
|
13
18
|
|
|
14
19
|
def cursor_left(ctx, count:, **)
|
|
15
|
-
|
|
20
|
+
move_cursor_horizontally(ctx, direction: :left, count:)
|
|
16
21
|
end
|
|
17
22
|
|
|
18
23
|
def cursor_right(ctx, count:, **)
|
|
19
|
-
|
|
24
|
+
move_cursor_horizontally(ctx, direction: :right, count:)
|
|
20
25
|
end
|
|
21
26
|
|
|
22
27
|
def cursor_up(ctx, count:, **)
|
|
23
|
-
ctx.window.move_up(ctx.buffer, count)
|
|
28
|
+
ctx.window.move_up(ctx.buffer, normalized_count(count))
|
|
24
29
|
end
|
|
25
30
|
|
|
26
31
|
def cursor_down(ctx, count:, **)
|
|
27
|
-
ctx.window.move_down(ctx.buffer, count)
|
|
32
|
+
ctx.window.move_down(ctx.buffer, normalized_count(count))
|
|
28
33
|
end
|
|
29
34
|
|
|
30
35
|
def cursor_page_up(ctx, kwargs:, count:, **)
|
|
@@ -37,6 +42,50 @@ module RuVim
|
|
|
37
42
|
ctx.window.move_down(ctx.buffer, page_lines * [count.to_i, 1].max)
|
|
38
43
|
end
|
|
39
44
|
|
|
45
|
+
def cursor_page_up_default(ctx, count:, bang:, **)
|
|
46
|
+
call(:cursor_page_up, ctx, count:, bang:, kwargs: { page_lines: current_page_step_lines(ctx) })
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def cursor_page_down_default(ctx, count:, bang:, **)
|
|
50
|
+
call(:cursor_page_down, ctx, count:, bang:, kwargs: { page_lines: current_page_step_lines(ctx) })
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def cursor_page_up_half(ctx, count:, bang:, **)
|
|
54
|
+
call(:cursor_page_up, ctx, count:, bang:, kwargs: { page_lines: current_half_page_step_lines(ctx) })
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def cursor_page_down_half(ctx, count:, bang:, **)
|
|
58
|
+
call(:cursor_page_down, ctx, count:, bang:, kwargs: { page_lines: current_half_page_step_lines(ctx) })
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def window_scroll_up(ctx, kwargs:, count:, **)
|
|
62
|
+
scroll_window_vertically(ctx, direction: :up, lines: kwargs[:lines] || kwargs["lines"], view_height: kwargs[:view_height] || kwargs["view_height"], count:)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def window_scroll_down(ctx, kwargs:, count:, **)
|
|
66
|
+
scroll_window_vertically(ctx, direction: :down, lines: kwargs[:lines] || kwargs["lines"], view_height: kwargs[:view_height] || kwargs["view_height"], count:)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def window_scroll_up_line(ctx, count:, bang:, **)
|
|
70
|
+
call(:window_scroll_up, ctx, count:, bang:, kwargs: { lines: 1, view_height: current_view_height(ctx) + 1 })
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def window_scroll_down_line(ctx, count:, bang:, **)
|
|
74
|
+
call(:window_scroll_down, ctx, count:, bang:, kwargs: { lines: 1, view_height: current_view_height(ctx) + 1 })
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def window_cursor_line_top(ctx, count:, **)
|
|
78
|
+
place_cursor_line_in_window(ctx, where: :top, count:)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def window_cursor_line_center(ctx, count:, **)
|
|
82
|
+
place_cursor_line_in_window(ctx, where: :center, count:)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def window_cursor_line_bottom(ctx, count:, **)
|
|
86
|
+
place_cursor_line_in_window(ctx, where: :bottom, count:)
|
|
87
|
+
end
|
|
88
|
+
|
|
40
89
|
def cursor_line_start(ctx, **)
|
|
41
90
|
ctx.window.cursor_x = 0
|
|
42
91
|
ctx.window.clamp_to_buffer(ctx.buffer)
|
|
@@ -56,7 +105,7 @@ module RuVim
|
|
|
56
105
|
|
|
57
106
|
def cursor_buffer_start(ctx, count:, **)
|
|
58
107
|
record_jump(ctx)
|
|
59
|
-
target_row = [count.to_i - 1, 0].max
|
|
108
|
+
target_row = [normalized_count(count).to_i - 1, 0].max
|
|
60
109
|
target_row = [target_row, ctx.buffer.line_count - 1].min
|
|
61
110
|
ctx.window.cursor_y = target_row
|
|
62
111
|
cursor_first_nonblank(ctx)
|
|
@@ -64,11 +113,7 @@ module RuVim
|
|
|
64
113
|
|
|
65
114
|
def cursor_buffer_end(ctx, count:, **)
|
|
66
115
|
record_jump(ctx)
|
|
67
|
-
|
|
68
|
-
target_row = [count - 1, ctx.buffer.line_count - 1].min
|
|
69
|
-
else
|
|
70
|
-
target_row = ctx.buffer.line_count - 1
|
|
71
|
-
end
|
|
116
|
+
target_row = count.nil? ? (ctx.buffer.line_count - 1) : [normalized_count(count) - 1, ctx.buffer.line_count - 1].min
|
|
72
117
|
ctx.window.cursor_y = target_row
|
|
73
118
|
cursor_first_nonblank(ctx)
|
|
74
119
|
end
|
|
@@ -121,6 +166,7 @@ module RuVim
|
|
|
121
166
|
|
|
122
167
|
def enter_insert_mode(ctx, **)
|
|
123
168
|
materialize_intro_buffer_if_needed(ctx)
|
|
169
|
+
ensure_modifiable_for_insert!(ctx)
|
|
124
170
|
ctx.buffer.begin_change_group
|
|
125
171
|
ctx.editor.enter_insert_mode
|
|
126
172
|
ctx.editor.echo("-- INSERT --")
|
|
@@ -145,10 +191,12 @@ module RuVim
|
|
|
145
191
|
|
|
146
192
|
def open_line_below(ctx, **)
|
|
147
193
|
materialize_intro_buffer_if_needed(ctx)
|
|
194
|
+
ensure_modifiable_for_insert!(ctx)
|
|
148
195
|
y = ctx.window.cursor_y
|
|
149
196
|
x = ctx.buffer.line_length(y)
|
|
150
197
|
ctx.buffer.begin_change_group
|
|
151
198
|
new_y, new_x = ctx.buffer.insert_newline(y, x)
|
|
199
|
+
new_x = apply_autoindent_to_newline(ctx, row: new_y, previous_row: y, start_col: new_x)
|
|
152
200
|
ctx.window.cursor_y = new_y
|
|
153
201
|
ctx.window.cursor_x = new_x
|
|
154
202
|
ctx.editor.enter_insert_mode
|
|
@@ -157,10 +205,13 @@ module RuVim
|
|
|
157
205
|
|
|
158
206
|
def open_line_above(ctx, **)
|
|
159
207
|
materialize_intro_buffer_if_needed(ctx)
|
|
208
|
+
ensure_modifiable_for_insert!(ctx)
|
|
160
209
|
y = ctx.window.cursor_y
|
|
161
210
|
ctx.buffer.begin_change_group
|
|
162
|
-
ctx.buffer.insert_newline(y, 0)
|
|
163
|
-
|
|
211
|
+
_new_y, new_x = ctx.buffer.insert_newline(y, 0)
|
|
212
|
+
new_x = apply_autoindent_to_newline(ctx, row: y, previous_row: y + 1, start_col: 0)
|
|
213
|
+
ctx.window.cursor_y = y
|
|
214
|
+
ctx.window.cursor_x = new_x
|
|
164
215
|
ctx.editor.enter_insert_mode
|
|
165
216
|
ctx.editor.echo("-- INSERT --")
|
|
166
217
|
end
|
|
@@ -181,12 +232,14 @@ module RuVim
|
|
|
181
232
|
end
|
|
182
233
|
|
|
183
234
|
def window_split(ctx, **)
|
|
184
|
-
ctx.editor.
|
|
235
|
+
place = ctx.editor.effective_option("splitbelow", window: ctx.window, buffer: ctx.buffer) ? :after : :before
|
|
236
|
+
ctx.editor.split_current_window(layout: :horizontal, place:)
|
|
185
237
|
ctx.editor.echo("split")
|
|
186
238
|
end
|
|
187
239
|
|
|
188
240
|
def window_vsplit(ctx, **)
|
|
189
|
-
ctx.editor.
|
|
241
|
+
place = ctx.editor.effective_option("splitright", window: ctx.window, buffer: ctx.buffer) ? :after : :before
|
|
242
|
+
ctx.editor.split_current_window(layout: :vertical, place:)
|
|
190
243
|
ctx.editor.echo("vsplit")
|
|
191
244
|
end
|
|
192
245
|
|
|
@@ -210,30 +263,83 @@ module RuVim
|
|
|
210
263
|
ctx.editor.focus_window_direction(:down)
|
|
211
264
|
end
|
|
212
265
|
|
|
213
|
-
def
|
|
214
|
-
|
|
215
|
-
if
|
|
216
|
-
|
|
217
|
-
|
|
266
|
+
def window_focus_or_split_left(ctx, **)
|
|
267
|
+
ed = ctx.editor
|
|
268
|
+
if ed.has_split_ancestor_on_axis?(:left)
|
|
269
|
+
ed.focus_window_direction(:left)
|
|
270
|
+
else
|
|
271
|
+
ed.split_current_window(layout: :vertical, place: :before)
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def window_focus_or_split_right(ctx, **)
|
|
276
|
+
ed = ctx.editor
|
|
277
|
+
if ed.has_split_ancestor_on_axis?(:right)
|
|
278
|
+
ed.focus_window_direction(:right)
|
|
279
|
+
else
|
|
280
|
+
ed.split_current_window(layout: :vertical, place: :after)
|
|
218
281
|
end
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def window_focus_or_split_up(ctx, **)
|
|
285
|
+
ed = ctx.editor
|
|
286
|
+
if ed.has_split_ancestor_on_axis?(:up)
|
|
287
|
+
ed.focus_window_direction(:up)
|
|
223
288
|
else
|
|
224
|
-
|
|
289
|
+
ed.split_current_window(layout: :horizontal, place: :before)
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def window_focus_or_split_down(ctx, **)
|
|
294
|
+
ed = ctx.editor
|
|
295
|
+
if ed.has_split_ancestor_on_axis?(:down)
|
|
296
|
+
ed.focus_window_direction(:down)
|
|
297
|
+
else
|
|
298
|
+
ed.split_current_window(layout: :horizontal, place: :after)
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def tab_new(ctx, argv:, **)
|
|
303
|
+
path = argv[0]
|
|
304
|
+
if ctx.buffer.modified? && !ctx.editor.effective_option("hidden", window: ctx.window, buffer: ctx.buffer)
|
|
305
|
+
unless maybe_autowrite_before_switch(ctx)
|
|
306
|
+
ctx.editor.echo_error("Unsaved changes (use :w or :q!)")
|
|
307
|
+
return
|
|
308
|
+
end
|
|
225
309
|
end
|
|
226
|
-
|
|
310
|
+
ctx.editor.tabnew(path: path)
|
|
227
311
|
end
|
|
228
312
|
|
|
229
313
|
def tab_next(ctx, count:, **)
|
|
314
|
+
count = normalized_count(count)
|
|
230
315
|
ctx.editor.tabnext(count)
|
|
231
|
-
ctx.editor.echo("tab #{ctx.editor.current_tabpage_number}/#{ctx.editor.tabpage_count}")
|
|
232
316
|
end
|
|
233
317
|
|
|
234
318
|
def tab_prev(ctx, count:, **)
|
|
319
|
+
count = normalized_count(count)
|
|
235
320
|
ctx.editor.tabprev(count)
|
|
236
|
-
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def tab_list(ctx, **)
|
|
324
|
+
editor = ctx.editor
|
|
325
|
+
items = []
|
|
326
|
+
# For current tab, use live window_order; for others, use saved snapshot
|
|
327
|
+
editor.tabpages.each_with_index do |tab, i|
|
|
328
|
+
is_current = (i == editor.current_tabpage_index)
|
|
329
|
+
current_marker = is_current ? ">" : " "
|
|
330
|
+
items << "#{current_marker}Tab page #{i + 1}"
|
|
331
|
+
win_ids = is_current ? editor.window_order : editor.tabpage_windows(tab)
|
|
332
|
+
win_ids.each do |wid|
|
|
333
|
+
win = editor.windows[wid]
|
|
334
|
+
next unless win
|
|
335
|
+
buf = editor.buffers[win.buffer_id]
|
|
336
|
+
next unless buf
|
|
337
|
+
active = (is_current && wid == editor.current_window_id) ? ">" : " "
|
|
338
|
+
name = buf.display_name
|
|
339
|
+
items << " #{active} #{name}"
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
editor.echo_multiline(items)
|
|
237
343
|
end
|
|
238
344
|
|
|
239
345
|
def enter_command_line_mode(ctx, **)
|
|
@@ -253,6 +359,7 @@ module RuVim
|
|
|
253
359
|
|
|
254
360
|
def delete_char(ctx, count:, **)
|
|
255
361
|
materialize_intro_buffer_if_needed(ctx)
|
|
362
|
+
count = normalized_count(count)
|
|
256
363
|
ctx.buffer.begin_change_group
|
|
257
364
|
deleted = +""
|
|
258
365
|
count.times do
|
|
@@ -266,8 +373,80 @@ module RuVim
|
|
|
266
373
|
ctx.window.clamp_to_buffer(ctx.buffer)
|
|
267
374
|
end
|
|
268
375
|
|
|
376
|
+
def substitute_char(ctx, count:, bang:, **)
|
|
377
|
+
call(:change_motion, ctx, count:, bang:, kwargs: { motion: "l" })
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
def swapcase_char(ctx, count:, **)
|
|
381
|
+
materialize_intro_buffer_if_needed(ctx)
|
|
382
|
+
count = normalized_count(count)
|
|
383
|
+
|
|
384
|
+
y = ctx.window.cursor_y
|
|
385
|
+
x = ctx.window.cursor_x
|
|
386
|
+
processed = false
|
|
387
|
+
|
|
388
|
+
ctx.buffer.begin_change_group
|
|
389
|
+
count.times do
|
|
390
|
+
line = ctx.buffer.line_at(y)
|
|
391
|
+
break if x >= line.length
|
|
392
|
+
|
|
393
|
+
ch = line[x]
|
|
394
|
+
swapped = ch.to_s.swapcase
|
|
395
|
+
if !swapped.empty? && swapped != ch
|
|
396
|
+
ctx.buffer.delete_span(y, x, y, x + 1)
|
|
397
|
+
ctx.buffer.insert_char(y, x, swapped[0])
|
|
398
|
+
end
|
|
399
|
+
processed = true
|
|
400
|
+
x += 1
|
|
401
|
+
end
|
|
402
|
+
ctx.buffer.end_change_group
|
|
403
|
+
|
|
404
|
+
ctx.window.cursor_y = y
|
|
405
|
+
ctx.window.cursor_x = processed ? x : ctx.window.cursor_x
|
|
406
|
+
ctx.window.clamp_to_buffer(ctx.buffer)
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
def join_lines(ctx, count:, **)
|
|
410
|
+
materialize_intro_buffer_if_needed(ctx)
|
|
411
|
+
joins = [normalized_count(count) - 1, 1].max
|
|
412
|
+
y = ctx.window.cursor_y
|
|
413
|
+
x = ctx.window.cursor_x
|
|
414
|
+
changed = false
|
|
415
|
+
|
|
416
|
+
ctx.buffer.begin_change_group
|
|
417
|
+
joins.times do
|
|
418
|
+
break if y >= ctx.buffer.line_count - 1
|
|
419
|
+
|
|
420
|
+
left = ctx.buffer.line_at(y)
|
|
421
|
+
right = ctx.buffer.line_at(y + 1)
|
|
422
|
+
join_col = left.length
|
|
423
|
+
|
|
424
|
+
# Join raw lines first.
|
|
425
|
+
break unless ctx.buffer.delete_char(y, join_col)
|
|
426
|
+
|
|
427
|
+
right_trimmed = right.sub(/\A\s+/, "")
|
|
428
|
+
trimmed_count = right.length - right_trimmed.length
|
|
429
|
+
if trimmed_count.positive?
|
|
430
|
+
ctx.buffer.delete_span(y, join_col, y, join_col + trimmed_count)
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
need_space = !left.empty? && !left.match?(/\s\z/) && !right_trimmed.empty? && !right_trimmed.match?(/\A\s/)
|
|
434
|
+
ctx.buffer.insert_char(y, join_col, " ") if need_space
|
|
435
|
+
|
|
436
|
+
x = join_col
|
|
437
|
+
changed = true
|
|
438
|
+
end
|
|
439
|
+
ctx.buffer.end_change_group
|
|
440
|
+
|
|
441
|
+
ctx.window.cursor_y = y
|
|
442
|
+
ctx.window.cursor_x = x
|
|
443
|
+
ctx.window.clamp_to_buffer(ctx.buffer)
|
|
444
|
+
ctx.editor.echo("joined") if changed
|
|
445
|
+
end
|
|
446
|
+
|
|
269
447
|
def delete_line(ctx, count:, **)
|
|
270
448
|
materialize_intro_buffer_if_needed(ctx)
|
|
449
|
+
count = normalized_count(count)
|
|
271
450
|
ctx.buffer.begin_change_group
|
|
272
451
|
deleted_lines = []
|
|
273
452
|
count.times { deleted_lines << ctx.buffer.delete_line(ctx.window.cursor_y) }
|
|
@@ -279,14 +458,15 @@ module RuVim
|
|
|
279
458
|
def delete_motion(ctx, count:, kwargs:, **)
|
|
280
459
|
materialize_intro_buffer_if_needed(ctx)
|
|
281
460
|
motion = (kwargs[:motion] || kwargs["motion"]).to_s
|
|
461
|
+
ncount = normalized_count(count)
|
|
282
462
|
handled =
|
|
283
463
|
case motion
|
|
284
|
-
when "h" then delete_chars_left(ctx,
|
|
285
|
-
when "l" then delete_chars_right(ctx,
|
|
286
|
-
when "j" then delete_lines_down(ctx,
|
|
287
|
-
when "k" then delete_lines_up(ctx,
|
|
464
|
+
when "h" then delete_chars_left(ctx, ncount)
|
|
465
|
+
when "l" then delete_chars_right(ctx, ncount)
|
|
466
|
+
when "j" then delete_lines_down(ctx, ncount)
|
|
467
|
+
when "k" then delete_lines_up(ctx, ncount)
|
|
288
468
|
when "$" then delete_to_end_of_line(ctx)
|
|
289
|
-
when "w" then delete_word_forward(ctx,
|
|
469
|
+
when "w" then delete_word_forward(ctx, ncount)
|
|
290
470
|
when "iw" then delete_text_object_word(ctx, around: false)
|
|
291
471
|
when "aw" then delete_text_object_word(ctx, around: true)
|
|
292
472
|
else
|
|
@@ -391,6 +571,7 @@ module RuVim
|
|
|
391
571
|
|
|
392
572
|
def replace_char(ctx, argv:, count:, **)
|
|
393
573
|
materialize_intro_buffer_if_needed(ctx)
|
|
574
|
+
count = normalized_count(count)
|
|
394
575
|
ch = argv[0].to_s
|
|
395
576
|
raise RuVim::CommandError, "replace requires a character" if ch.empty?
|
|
396
577
|
|
|
@@ -411,6 +592,7 @@ module RuVim
|
|
|
411
592
|
end
|
|
412
593
|
|
|
413
594
|
def yank_line(ctx, count:, **)
|
|
595
|
+
count = normalized_count(count)
|
|
414
596
|
start = ctx.window.cursor_y
|
|
415
597
|
text = ctx.buffer.line_block_text(start, count)
|
|
416
598
|
store_yank_register(ctx, text:, type: :linewise)
|
|
@@ -423,7 +605,7 @@ module RuVim
|
|
|
423
605
|
when "w"
|
|
424
606
|
y = ctx.window.cursor_y
|
|
425
607
|
x = ctx.window.cursor_x
|
|
426
|
-
target = advance_word_forward(ctx.buffer, y, x, count)
|
|
608
|
+
target = advance_word_forward(ctx.buffer, y, x, count, editor: ctx.editor, window: ctx.window)
|
|
427
609
|
target ||= { row: y, col: x }
|
|
428
610
|
text = ctx.buffer.span_text(y, x, target[:row], target[:col])
|
|
429
611
|
store_yank_register(ctx, text:, type: :charwise)
|
|
@@ -515,6 +697,45 @@ module RuVim
|
|
|
515
697
|
ctx.editor.enter_normal_mode
|
|
516
698
|
end
|
|
517
699
|
|
|
700
|
+
def indent_lines(ctx, count:, **)
|
|
701
|
+
count = normalized_count(count)
|
|
702
|
+
start_row = ctx.window.cursor_y
|
|
703
|
+
end_row = [start_row + count - 1, ctx.buffer.line_count - 1].min
|
|
704
|
+
reindent_range(ctx, start_row, end_row)
|
|
705
|
+
end
|
|
706
|
+
|
|
707
|
+
def indent_motion(ctx, count:, kwargs:, **)
|
|
708
|
+
motion = (kwargs[:motion] || kwargs["motion"]).to_s
|
|
709
|
+
ncount = normalized_count(count)
|
|
710
|
+
start_row = ctx.window.cursor_y
|
|
711
|
+
case motion
|
|
712
|
+
when "j"
|
|
713
|
+
end_row = [start_row + ncount, ctx.buffer.line_count - 1].min
|
|
714
|
+
when "k"
|
|
715
|
+
end_row = start_row
|
|
716
|
+
start_row = [start_row - ncount, 0].max
|
|
717
|
+
when "G"
|
|
718
|
+
end_row = ctx.buffer.line_count - 1
|
|
719
|
+
when "gg"
|
|
720
|
+
end_row = start_row
|
|
721
|
+
start_row = 0
|
|
722
|
+
else
|
|
723
|
+
ctx.editor.echo("Unsupported motion for =: #{motion}")
|
|
724
|
+
return
|
|
725
|
+
end
|
|
726
|
+
reindent_range(ctx, start_row, end_row)
|
|
727
|
+
end
|
|
728
|
+
|
|
729
|
+
def visual_indent(ctx, **)
|
|
730
|
+
sel = ctx.editor.visual_selection
|
|
731
|
+
return unless sel
|
|
732
|
+
|
|
733
|
+
start_row = sel[:start_row]
|
|
734
|
+
end_row = sel[:end_row]
|
|
735
|
+
reindent_range(ctx, start_row, end_row)
|
|
736
|
+
ctx.editor.enter_normal_mode
|
|
737
|
+
end
|
|
738
|
+
|
|
518
739
|
def visual_select_text_object(ctx, kwargs:, **)
|
|
519
740
|
motion = (kwargs[:motion] || kwargs["motion"]).to_s
|
|
520
741
|
span = text_object_span(ctx.buffer, ctx.window, motion)
|
|
@@ -540,6 +761,9 @@ module RuVim
|
|
|
540
761
|
size = File.exist?(target) ? File.size(target) : 0
|
|
541
762
|
suffix = bang ? " (force accepted)" : ""
|
|
542
763
|
ctx.editor.echo("\"#{target}\" #{ctx.buffer.line_count}L, #{size}B written#{suffix}")
|
|
764
|
+
if ctx.editor.get_option("onsavehook")
|
|
765
|
+
ctx.buffer.lang_module.on_save(ctx, target)
|
|
766
|
+
end
|
|
543
767
|
end
|
|
544
768
|
|
|
545
769
|
def app_quit(ctx, bang:, **)
|
|
@@ -563,6 +787,25 @@ module RuVim
|
|
|
563
787
|
ctx.editor.request_quit!
|
|
564
788
|
end
|
|
565
789
|
|
|
790
|
+
def app_quit_all(ctx, bang:, **)
|
|
791
|
+
unless bang
|
|
792
|
+
modified = ctx.editor.buffers.values.select { |b| b.file_buffer? && b.modified? }
|
|
793
|
+
unless modified.empty?
|
|
794
|
+
ctx.editor.echo_error("#{modified.size} buffer(s) have unsaved changes (add ! to override)")
|
|
795
|
+
return
|
|
796
|
+
end
|
|
797
|
+
end
|
|
798
|
+
ctx.editor.request_quit!
|
|
799
|
+
end
|
|
800
|
+
|
|
801
|
+
def file_write_quit_all(ctx, bang:, **)
|
|
802
|
+
ctx.editor.buffers.each_value do |buf|
|
|
803
|
+
next unless buf.file_buffer? && buf.modified? && buf.path
|
|
804
|
+
buf.write_to(buf.path)
|
|
805
|
+
end
|
|
806
|
+
app_quit_all(ctx, bang: true)
|
|
807
|
+
end
|
|
808
|
+
|
|
566
809
|
def file_write_quit(ctx, argv:, bang:, **)
|
|
567
810
|
file_write(ctx, argv:, bang:)
|
|
568
811
|
return unless ctx.editor.running?
|
|
@@ -587,13 +830,42 @@ module RuVim
|
|
|
587
830
|
end
|
|
588
831
|
|
|
589
832
|
if ctx.buffer.modified? && !bang
|
|
590
|
-
ctx.editor.
|
|
833
|
+
if ctx.editor.effective_option("hidden", window: ctx.window, buffer: ctx.buffer)
|
|
834
|
+
# hidden permits abandoning a modified buffer without forcing write.
|
|
835
|
+
elsif maybe_autowrite_before_switch(ctx)
|
|
836
|
+
# autowrite handled
|
|
837
|
+
else
|
|
838
|
+
ctx.editor.echo_error("Unsaved changes (use :e! to discard and open)")
|
|
839
|
+
return
|
|
840
|
+
end
|
|
841
|
+
end
|
|
842
|
+
|
|
843
|
+
ctx.editor.open_path(path)
|
|
844
|
+
end
|
|
845
|
+
|
|
846
|
+
def file_goto_under_cursor(ctx, **)
|
|
847
|
+
token = file_token_under_cursor(ctx.buffer, ctx.window)
|
|
848
|
+
if token.nil? || token.empty?
|
|
849
|
+
ctx.editor.echo_error("No file under cursor")
|
|
591
850
|
return
|
|
592
851
|
end
|
|
593
852
|
|
|
594
|
-
|
|
595
|
-
ctx
|
|
596
|
-
|
|
853
|
+
target = parse_gf_target(token)
|
|
854
|
+
path = resolve_gf_path(ctx, target[:path])
|
|
855
|
+
unless path
|
|
856
|
+
ctx.editor.echo_error("File not found: #{target[:path]}")
|
|
857
|
+
return
|
|
858
|
+
end
|
|
859
|
+
|
|
860
|
+
if ctx.buffer.modified? && !ctx.editor.effective_option("hidden", window: ctx.window, buffer: ctx.buffer)
|
|
861
|
+
unless maybe_autowrite_before_switch(ctx)
|
|
862
|
+
ctx.editor.echo_error("Unsaved changes (set hidden or :w)")
|
|
863
|
+
return
|
|
864
|
+
end
|
|
865
|
+
end
|
|
866
|
+
|
|
867
|
+
ctx.editor.open_path(path)
|
|
868
|
+
move_cursor_to_gf_line(ctx, target[:line]) if target[:line]
|
|
597
869
|
end
|
|
598
870
|
|
|
599
871
|
def buffer_list(ctx, **)
|
|
@@ -601,23 +873,28 @@ module RuVim
|
|
|
601
873
|
alt_id = ctx.editor.alternate_buffer_id
|
|
602
874
|
items = ctx.editor.buffer_ids.map do |id|
|
|
603
875
|
b = ctx.editor.buffers.fetch(id)
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
876
|
+
indicator = id == current_id ? "%a" : " "
|
|
877
|
+
indicator = "# " if id == alt_id && id != current_id
|
|
878
|
+
mod = b.modified? ? "+" : " "
|
|
879
|
+
name = b.path ? "\"#{b.path}\"" : "[No Name]"
|
|
880
|
+
line_info = "line #{b.respond_to?(:cursor_line) ? b.cursor_line : 0}"
|
|
881
|
+
# Find the window showing this buffer to get cursor line
|
|
882
|
+
win = ctx.editor.windows.values.find { |w| w.buffer_id == id }
|
|
883
|
+
line_info = "line #{win ? win.cursor_y + 1 : 0}"
|
|
884
|
+
"%3d %s %s %-30s %s" % [id, indicator, mod, name, line_info]
|
|
610
885
|
end
|
|
611
|
-
ctx.editor.
|
|
886
|
+
ctx.editor.echo_multiline(items)
|
|
612
887
|
end
|
|
613
888
|
|
|
614
889
|
def buffer_next(ctx, count:, bang:, **)
|
|
890
|
+
count = normalized_count(count)
|
|
615
891
|
target = ctx.editor.current_buffer.id
|
|
616
892
|
count.times { target = ctx.editor.next_buffer_id_from(target, 1) }
|
|
617
893
|
switch_buffer_id(ctx, target, bang:)
|
|
618
894
|
end
|
|
619
895
|
|
|
620
896
|
def buffer_prev(ctx, count:, bang:, **)
|
|
897
|
+
count = normalized_count(count)
|
|
621
898
|
target = ctx.editor.current_buffer.id
|
|
622
899
|
count.times { target = ctx.editor.next_buffer_id_from(target, -1) }
|
|
623
900
|
switch_buffer_id(ctx, target, bang:)
|
|
@@ -639,6 +916,28 @@ module RuVim
|
|
|
639
916
|
switch_buffer_id(ctx, target_id, bang:)
|
|
640
917
|
end
|
|
641
918
|
|
|
919
|
+
def buffer_delete(ctx, argv:, bang:, **)
|
|
920
|
+
arg = argv[0]
|
|
921
|
+
target_id =
|
|
922
|
+
if arg.nil? || arg.empty?
|
|
923
|
+
ctx.buffer.id
|
|
924
|
+
elsif arg == "#"
|
|
925
|
+
ctx.editor.alternate_buffer_id || raise(RuVim::CommandError, "No alternate buffer")
|
|
926
|
+
elsif arg.match?(/\A\d+\z/)
|
|
927
|
+
arg.to_i
|
|
928
|
+
else
|
|
929
|
+
find_buffer_by_name(ctx.editor, arg)&.id || raise(RuVim::CommandError, "No such buffer: #{arg}")
|
|
930
|
+
end
|
|
931
|
+
|
|
932
|
+
target = ctx.editor.buffers[target_id] || raise(RuVim::CommandError, "No such buffer: #{target_id}")
|
|
933
|
+
if target.modified? && !bang
|
|
934
|
+
raise RuVim::CommandError, "No write since last change (use :bdelete! to discard)"
|
|
935
|
+
end
|
|
936
|
+
|
|
937
|
+
ctx.editor.delete_buffer(target_id)
|
|
938
|
+
ctx.editor.echo("buffer #{target_id} deleted")
|
|
939
|
+
end
|
|
940
|
+
|
|
642
941
|
def ex_help(ctx, argv: [], **)
|
|
643
942
|
topic = argv.first.to_s
|
|
644
943
|
registry = RuVim::ExCommandRegistry.instance
|
|
@@ -675,7 +974,7 @@ module RuVim
|
|
|
675
974
|
when "config"
|
|
676
975
|
"Config: XDG Ruby DSL at ~/.config/ruvim/init.rb and ftplugin/<filetype>.rb"
|
|
677
976
|
when "bindings", "keys", "keymap"
|
|
678
|
-
"Bindings:
|
|
977
|
+
"Bindings: use :bindings (current effective key bindings by layer). Docs: docs/binding.md"
|
|
679
978
|
when "number", "relativenumber", "ignorecase", "smartcase", "hlsearch", "tabstop", "filetype"
|
|
680
979
|
option_help_line(key)
|
|
681
980
|
else
|
|
@@ -691,8 +990,17 @@ module RuVim
|
|
|
691
990
|
def ex_define_command(ctx, argv:, bang:, **)
|
|
692
991
|
registry = RuVim::ExCommandRegistry.instance
|
|
693
992
|
if argv.empty?
|
|
694
|
-
|
|
695
|
-
|
|
993
|
+
user_cmds = registry.all.select { |spec| spec.source == :user }
|
|
994
|
+
if user_cmds.empty?
|
|
995
|
+
ctx.editor.echo("No user commands")
|
|
996
|
+
else
|
|
997
|
+
header = " Name Definition"
|
|
998
|
+
items = [header] + user_cmds.map { |spec|
|
|
999
|
+
body = spec.respond_to?(:body) ? spec.body.to_s : spec.name.to_s
|
|
1000
|
+
" %-12s%s" % [spec.name, body]
|
|
1001
|
+
}
|
|
1002
|
+
ctx.editor.echo_multiline(items)
|
|
1003
|
+
end
|
|
696
1004
|
return
|
|
697
1005
|
end
|
|
698
1006
|
|
|
@@ -724,24 +1032,128 @@ module RuVim
|
|
|
724
1032
|
raise RuVim::CommandError, "Usage: :ruby <code>" if code.strip.empty?
|
|
725
1033
|
|
|
726
1034
|
b = binding
|
|
1035
|
+
# Use local_variable_set for eval locals to avoid "assigned but unused variable"
|
|
1036
|
+
# warnings while still exposing editor/buffer/window in :ruby.
|
|
727
1037
|
b.local_variable_set(:editor, ctx.editor)
|
|
728
1038
|
b.local_variable_set(:buffer, ctx.buffer)
|
|
729
1039
|
b.local_variable_set(:window, ctx.window)
|
|
730
|
-
|
|
731
|
-
|
|
1040
|
+
saved_stdout = STDOUT.dup
|
|
1041
|
+
saved_stderr = STDERR.dup
|
|
1042
|
+
original_g_stdout = $stdout
|
|
1043
|
+
original_g_stderr = $stderr
|
|
1044
|
+
result = nil
|
|
1045
|
+
stdout_text = ""
|
|
1046
|
+
stderr_text = ""
|
|
1047
|
+
Tempfile.create("ruvim-ruby-stdout") do |outf|
|
|
1048
|
+
Tempfile.create("ruvim-ruby-stderr") do |errf|
|
|
1049
|
+
STDOUT.reopen(outf)
|
|
1050
|
+
STDERR.reopen(errf)
|
|
1051
|
+
$stdout = STDOUT
|
|
1052
|
+
$stderr = STDERR
|
|
1053
|
+
result = eval(code, b) # rubocop:disable Security/Eval
|
|
1054
|
+
STDOUT.flush
|
|
1055
|
+
STDERR.flush
|
|
1056
|
+
outf.flush
|
|
1057
|
+
errf.flush
|
|
1058
|
+
outf.rewind
|
|
1059
|
+
errf.rewind
|
|
1060
|
+
stdout_text = outf.read.to_s
|
|
1061
|
+
stderr_text = errf.read.to_s
|
|
1062
|
+
end
|
|
1063
|
+
end
|
|
1064
|
+
if !stdout_text.empty? || !stderr_text.empty?
|
|
1065
|
+
lines = ["Ruby output", ""]
|
|
1066
|
+
unless stdout_text.empty?
|
|
1067
|
+
lines << "[stdout]"
|
|
1068
|
+
lines.concat(stdout_text.lines(chomp: true))
|
|
1069
|
+
lines << ""
|
|
1070
|
+
end
|
|
1071
|
+
unless stderr_text.empty?
|
|
1072
|
+
lines << "[stderr]"
|
|
1073
|
+
lines.concat(stderr_text.lines(chomp: true))
|
|
1074
|
+
lines << ""
|
|
1075
|
+
end
|
|
1076
|
+
lines << "[result]"
|
|
1077
|
+
lines << (result.nil? ? "nil" : result.inspect)
|
|
1078
|
+
ctx.editor.show_help_buffer!(title: "[Ruby Output]", lines:, filetype: "ruby")
|
|
1079
|
+
else
|
|
1080
|
+
ctx.editor.echo(result.nil? ? "ruby: nil" : "ruby: #{result.inspect}")
|
|
1081
|
+
end
|
|
732
1082
|
rescue StandardError => e
|
|
733
1083
|
raise RuVim::CommandError, "Ruby error: #{e.class}: #{e.message}"
|
|
1084
|
+
ensure
|
|
1085
|
+
if defined?(saved_stdout) && saved_stdout
|
|
1086
|
+
STDOUT.reopen(saved_stdout)
|
|
1087
|
+
saved_stdout.close unless saved_stdout.closed?
|
|
1088
|
+
end
|
|
1089
|
+
if defined?(saved_stderr) && saved_stderr
|
|
1090
|
+
STDERR.reopen(saved_stderr)
|
|
1091
|
+
saved_stderr.close unless saved_stderr.closed?
|
|
1092
|
+
end
|
|
1093
|
+
$stdout = (defined?(original_g_stdout) && original_g_stdout) ? original_g_stdout : STDOUT
|
|
1094
|
+
$stderr = (defined?(original_g_stderr) && original_g_stderr) ? original_g_stderr : STDERR
|
|
1095
|
+
end
|
|
1096
|
+
|
|
1097
|
+
def ex_shell(ctx, command:, **)
|
|
1098
|
+
raise RuVim::CommandError, "Restricted mode: :! is disabled" if ctx.editor.respond_to?(:restricted_mode?) && ctx.editor.restricted_mode?
|
|
1099
|
+
|
|
1100
|
+
cmd = command.to_s
|
|
1101
|
+
raise RuVim::CommandError, "Usage: :!<command>" if cmd.strip.empty?
|
|
1102
|
+
|
|
1103
|
+
shell = ENV["SHELL"].to_s
|
|
1104
|
+
shell = "/bin/sh" if shell.empty?
|
|
1105
|
+
stdout_text, stderr_text, status = Open3.capture3(shell, "-c", cmd)
|
|
1106
|
+
|
|
1107
|
+
if !stdout_text.to_s.empty? || !stderr_text.to_s.empty?
|
|
1108
|
+
lines = ["Shell output", "", "[command]", cmd, ""]
|
|
1109
|
+
unless stdout_text.to_s.empty?
|
|
1110
|
+
lines << "[stdout]"
|
|
1111
|
+
lines.concat(stdout_text.to_s.lines(chomp: true))
|
|
1112
|
+
lines << ""
|
|
1113
|
+
end
|
|
1114
|
+
unless stderr_text.to_s.empty?
|
|
1115
|
+
lines << "[stderr]"
|
|
1116
|
+
lines.concat(stderr_text.to_s.lines(chomp: true))
|
|
1117
|
+
lines << ""
|
|
1118
|
+
end
|
|
1119
|
+
lines << "[status]"
|
|
1120
|
+
lines << "exit #{status.exitstatus}"
|
|
1121
|
+
ctx.editor.show_help_buffer!(title: "[Shell Output]", lines:, filetype: "sh")
|
|
1122
|
+
else
|
|
1123
|
+
ctx.editor.echo("shell exit #{status.exitstatus}")
|
|
1124
|
+
end
|
|
1125
|
+
rescue Errno::ENOENT => e
|
|
1126
|
+
raise RuVim::CommandError, "Shell error: #{e.message}"
|
|
734
1127
|
end
|
|
735
1128
|
|
|
736
1129
|
def ex_commands(ctx, **)
|
|
737
|
-
|
|
1130
|
+
rows = RuVim::ExCommandRegistry.instance.all.map do |spec|
|
|
738
1131
|
alias_text = spec.aliases.empty? ? "" : " (#{spec.aliases.join(', ')})"
|
|
739
1132
|
source = spec.source == :user ? " [user]" : ""
|
|
740
|
-
"#{spec.name}#{alias_text}#{source}"
|
|
1133
|
+
name = "#{spec.name}#{alias_text}#{source}"
|
|
1134
|
+
desc = spec.desc.to_s
|
|
1135
|
+
keys = ex_command_binding_labels(ctx.editor, spec)
|
|
1136
|
+
[name, desc, keys]
|
|
1137
|
+
end
|
|
1138
|
+
name_width = rows.map { |name, _desc, _keys| name.length }.max || 0
|
|
1139
|
+
items = rows.map do |name, desc, keys|
|
|
1140
|
+
line = "#{name.ljust(name_width)} #{desc}"
|
|
1141
|
+
line += " keys: #{keys.join(', ')}" unless keys.empty?
|
|
1142
|
+
line
|
|
741
1143
|
end
|
|
742
1144
|
ctx.editor.show_help_buffer!(title: "[Commands]", lines: ["Ex commands", "", *items])
|
|
743
1145
|
end
|
|
744
1146
|
|
|
1147
|
+
def ex_bindings(ctx, argv: [], **)
|
|
1148
|
+
keymaps = ctx.editor.keymap_manager
|
|
1149
|
+
raise RuVim::CommandError, "Keymap manager is unavailable" unless keymaps
|
|
1150
|
+
|
|
1151
|
+
mode_filter, sort = parse_bindings_args(argv)
|
|
1152
|
+
entries = keymaps.binding_entries_for_context(ctx.editor, mode: mode_filter)
|
|
1153
|
+
lines = bindings_buffer_lines(ctx.editor, entries, mode_filter:, sort:)
|
|
1154
|
+
ctx.editor.show_help_buffer!(title: "[Bindings]", lines:)
|
|
1155
|
+
end
|
|
1156
|
+
|
|
745
1157
|
def ex_set(ctx, argv:, **)
|
|
746
1158
|
ex_set_common(ctx, argv, scope: :auto)
|
|
747
1159
|
end
|
|
@@ -764,6 +1176,7 @@ module RuVim
|
|
|
764
1176
|
end
|
|
765
1177
|
|
|
766
1178
|
ctx.editor.set_quickfix_list(items)
|
|
1179
|
+
ctx.editor.select_quickfix(0)
|
|
767
1180
|
ctx.editor.jump_to_location(ctx.editor.current_quickfix_item)
|
|
768
1181
|
ctx.editor.echo("quickfix: #{items.length} item(s)")
|
|
769
1182
|
end
|
|
@@ -778,12 +1191,13 @@ module RuVim
|
|
|
778
1191
|
end
|
|
779
1192
|
|
|
780
1193
|
ctx.editor.set_location_list(items, window_id: ctx.window.id)
|
|
1194
|
+
ctx.editor.select_location_list(0, window_id: ctx.window.id)
|
|
781
1195
|
ctx.editor.jump_to_location(ctx.editor.current_location_list_item(ctx.window.id))
|
|
782
1196
|
ctx.editor.echo("location list: #{items.length} item(s)")
|
|
783
1197
|
end
|
|
784
1198
|
|
|
785
1199
|
def ex_copen(ctx, **)
|
|
786
|
-
open_list_window(ctx, kind: :quickfix, title: "[Quickfix]", lines: quickfix_buffer_lines(ctx.editor))
|
|
1200
|
+
open_list_window(ctx, kind: :quickfix, title: "[Quickfix]", lines: quickfix_buffer_lines(ctx.editor), source_window_id: ctx.window.id)
|
|
787
1201
|
end
|
|
788
1202
|
|
|
789
1203
|
def ex_cclose(ctx, **)
|
|
@@ -797,6 +1211,7 @@ module RuVim
|
|
|
797
1211
|
return
|
|
798
1212
|
end
|
|
799
1213
|
ctx.editor.jump_to_location(item)
|
|
1214
|
+
refresh_list_window(ctx.editor, :quickfix)
|
|
800
1215
|
ctx.editor.echo(quickfix_item_echo(ctx.editor))
|
|
801
1216
|
end
|
|
802
1217
|
|
|
@@ -807,11 +1222,13 @@ module RuVim
|
|
|
807
1222
|
return
|
|
808
1223
|
end
|
|
809
1224
|
ctx.editor.jump_to_location(item)
|
|
1225
|
+
refresh_list_window(ctx.editor, :quickfix)
|
|
810
1226
|
ctx.editor.echo(quickfix_item_echo(ctx.editor))
|
|
811
1227
|
end
|
|
812
1228
|
|
|
813
1229
|
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)
|
|
1230
|
+
open_list_window(ctx, kind: :location_list, title: "[Location List]", lines: location_list_buffer_lines(ctx.editor, ctx.window.id),
|
|
1231
|
+
source_window_id: ctx.window.id)
|
|
815
1232
|
end
|
|
816
1233
|
|
|
817
1234
|
def ex_lclose(ctx, **)
|
|
@@ -825,6 +1242,7 @@ module RuVim
|
|
|
825
1242
|
return
|
|
826
1243
|
end
|
|
827
1244
|
ctx.editor.jump_to_location(item)
|
|
1245
|
+
refresh_list_window(ctx.editor, :location_list)
|
|
828
1246
|
ctx.editor.echo(location_item_echo(ctx.editor, ctx.window.id))
|
|
829
1247
|
end
|
|
830
1248
|
|
|
@@ -835,37 +1253,89 @@ module RuVim
|
|
|
835
1253
|
return
|
|
836
1254
|
end
|
|
837
1255
|
ctx.editor.jump_to_location(item)
|
|
1256
|
+
refresh_list_window(ctx.editor, :location_list)
|
|
838
1257
|
ctx.editor.echo(location_item_echo(ctx.editor, ctx.window.id))
|
|
839
1258
|
end
|
|
840
1259
|
|
|
841
|
-
def ex_substitute(ctx, pattern:, replacement:, global: false, **)
|
|
1260
|
+
def ex_substitute(ctx, pattern:, replacement:, flags_str: nil, range_start: nil, range_end: nil, global: false, **)
|
|
842
1261
|
materialize_intro_buffer_if_needed(ctx)
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
end
|
|
856
|
-
end
|
|
1262
|
+
flags = parse_substitute_flags(flags_str, default_global: global)
|
|
1263
|
+
raise RuVim::CommandError, "Confirm flag (:s///c) is not yet supported" if flags[:confirm]
|
|
1264
|
+
|
|
1265
|
+
regex = build_substitute_regex(pattern, flags, ctx)
|
|
1266
|
+
|
|
1267
|
+
r_start = range_start || 0
|
|
1268
|
+
r_end = range_end || (ctx.buffer.line_count - 1)
|
|
1269
|
+
|
|
1270
|
+
if flags[:count_only]
|
|
1271
|
+
total = count_matches_in_range(ctx.buffer, regex, r_start, r_end, flags[:global])
|
|
1272
|
+
ctx.editor.echo("#{total} match(es)")
|
|
1273
|
+
return
|
|
857
1274
|
end
|
|
858
1275
|
|
|
1276
|
+
changed = substitute_range(ctx, regex, replacement, r_start, r_end, flags)
|
|
1277
|
+
|
|
859
1278
|
if changed.positive?
|
|
860
|
-
ctx.buffer.begin_change_group
|
|
861
|
-
ctx.buffer.replace_all_lines!(new_lines)
|
|
862
|
-
ctx.buffer.end_change_group
|
|
863
1279
|
ctx.editor.echo("#{changed} substitution(s)")
|
|
1280
|
+
elsif flags[:no_error]
|
|
1281
|
+
ctx.editor.echo("Pattern not found: #{pattern}")
|
|
864
1282
|
else
|
|
865
1283
|
ctx.editor.echo("Pattern not found: #{pattern}")
|
|
866
1284
|
end
|
|
867
1285
|
end
|
|
868
1286
|
|
|
1287
|
+
def ex_grep(ctx, argv:, kwargs: {}, **)
|
|
1288
|
+
run_external_grep(ctx, argv:, target: :quickfix)
|
|
1289
|
+
end
|
|
1290
|
+
|
|
1291
|
+
def ex_lgrep(ctx, argv:, kwargs: {}, **)
|
|
1292
|
+
run_external_grep(ctx, argv:, target: :location_list)
|
|
1293
|
+
end
|
|
1294
|
+
|
|
1295
|
+
def ex_delete_lines(ctx, kwargs: {}, **)
|
|
1296
|
+
materialize_intro_buffer_if_needed(ctx)
|
|
1297
|
+
r_start = kwargs[:range_start]
|
|
1298
|
+
r_end = kwargs[:range_end]
|
|
1299
|
+
unless r_start && r_end
|
|
1300
|
+
# Default to current line
|
|
1301
|
+
r_start = r_end = ctx.window.cursor_y
|
|
1302
|
+
end
|
|
1303
|
+
|
|
1304
|
+
count = r_end - r_start + 1
|
|
1305
|
+
deleted_text = ctx.buffer.line_block_text(r_start, count)
|
|
1306
|
+
ctx.buffer.begin_change_group
|
|
1307
|
+
count.times { ctx.buffer.delete_line(r_start) }
|
|
1308
|
+
ctx.buffer.end_change_group
|
|
1309
|
+
store_delete_register(ctx, text: deleted_text, type: :linewise)
|
|
1310
|
+
ctx.window.cursor_y = [r_start, ctx.buffer.line_count - 1].min
|
|
1311
|
+
ctx.window.cursor_x = 0
|
|
1312
|
+
ctx.window.clamp_to_buffer(ctx.buffer)
|
|
1313
|
+
ctx.editor.echo("#{count} line(s) deleted")
|
|
1314
|
+
end
|
|
1315
|
+
|
|
1316
|
+
def ex_yank_lines(ctx, kwargs: {}, **)
|
|
1317
|
+
materialize_intro_buffer_if_needed(ctx)
|
|
1318
|
+
r_start = kwargs[:range_start]
|
|
1319
|
+
r_end = kwargs[:range_end]
|
|
1320
|
+
unless r_start && r_end
|
|
1321
|
+
r_start = r_end = ctx.window.cursor_y
|
|
1322
|
+
end
|
|
1323
|
+
|
|
1324
|
+
count = r_end - r_start + 1
|
|
1325
|
+
text = ctx.buffer.line_block_text(r_start, count)
|
|
1326
|
+
store_yank_register(ctx, text:, type: :linewise)
|
|
1327
|
+
ctx.editor.echo("#{count} line(s) yanked")
|
|
1328
|
+
end
|
|
1329
|
+
|
|
1330
|
+
def ex_rich(ctx, argv: [], **)
|
|
1331
|
+
format = argv.first
|
|
1332
|
+
RuVim::RichView.toggle!(ctx.editor, format: format)
|
|
1333
|
+
end
|
|
1334
|
+
|
|
1335
|
+
def rich_toggle(ctx, **)
|
|
1336
|
+
RuVim::RichView.toggle!(ctx.editor)
|
|
1337
|
+
end
|
|
1338
|
+
|
|
869
1339
|
def submit_search(ctx, pattern:, direction:)
|
|
870
1340
|
text = pattern.to_s
|
|
871
1341
|
if text.empty?
|
|
@@ -881,6 +1351,35 @@ module RuVim
|
|
|
881
1351
|
|
|
882
1352
|
private
|
|
883
1353
|
|
|
1354
|
+
def reindent_range(ctx, start_row, end_row)
|
|
1355
|
+
buf = ctx.buffer
|
|
1356
|
+
lang_mod = buf.lang_module
|
|
1357
|
+
|
|
1358
|
+
sw = ctx.editor.effective_option("shiftwidth", buffer: buf).to_i
|
|
1359
|
+
sw = 2 if sw <= 0
|
|
1360
|
+
|
|
1361
|
+
buf.begin_change_group
|
|
1362
|
+
(start_row..end_row).each do |row|
|
|
1363
|
+
target_indent = lang_mod.calculate_indent(buf.lines, row, sw)
|
|
1364
|
+
next unless target_indent
|
|
1365
|
+
|
|
1366
|
+
line = buf.line_at(row)
|
|
1367
|
+
current_indent = line[/\A */].to_s.length
|
|
1368
|
+
next if current_indent == target_indent
|
|
1369
|
+
|
|
1370
|
+
buf.delete_span(row, 0, row, current_indent) if current_indent > 0
|
|
1371
|
+
buf.insert_text(row, 0, " " * target_indent) if target_indent > 0
|
|
1372
|
+
end
|
|
1373
|
+
buf.end_change_group
|
|
1374
|
+
|
|
1375
|
+
ctx.window.cursor_y = start_row
|
|
1376
|
+
line = buf.line_at(start_row)
|
|
1377
|
+
ctx.window.cursor_x = (line[/\A */]&.length || 0)
|
|
1378
|
+
ctx.window.clamp_to_buffer(buf)
|
|
1379
|
+
count = end_row - start_row + 1
|
|
1380
|
+
ctx.editor.echo("#{count} line#{"s" if count > 1} indented")
|
|
1381
|
+
end
|
|
1382
|
+
|
|
884
1383
|
def parse_vimgrep_pattern(argv)
|
|
885
1384
|
raw = Array(argv).join(" ").strip
|
|
886
1385
|
raise RuVim::CommandError, "Usage: :vimgrep /pattern/" if raw.empty?
|
|
@@ -892,6 +1391,70 @@ module RuVim
|
|
|
892
1391
|
end
|
|
893
1392
|
end
|
|
894
1393
|
|
|
1394
|
+
def run_external_grep(ctx, argv:, target:)
|
|
1395
|
+
args = Array(argv).join(" ").strip
|
|
1396
|
+
raise RuVim::CommandError, "Usage: :grep pattern [files...]" if args.empty?
|
|
1397
|
+
|
|
1398
|
+
grepprg = ctx.editor.effective_option("grepprg", window: ctx.window, buffer: ctx.buffer) || "grep -n"
|
|
1399
|
+
cmd = "#{grepprg} #{args}"
|
|
1400
|
+
|
|
1401
|
+
stdout, stderr, status = Open3.capture3(cmd)
|
|
1402
|
+
if stdout.strip.empty? && !status.success?
|
|
1403
|
+
msg = stderr.strip.empty? ? "No matches found" : stderr.strip
|
|
1404
|
+
ctx.editor.echo_error(msg)
|
|
1405
|
+
return
|
|
1406
|
+
end
|
|
1407
|
+
|
|
1408
|
+
items = parse_grep_output(ctx, stdout)
|
|
1409
|
+
if items.empty?
|
|
1410
|
+
ctx.editor.echo_error("No matches found")
|
|
1411
|
+
return
|
|
1412
|
+
end
|
|
1413
|
+
|
|
1414
|
+
case target
|
|
1415
|
+
when :quickfix
|
|
1416
|
+
ctx.editor.set_quickfix_list(items)
|
|
1417
|
+
ctx.editor.select_quickfix(0)
|
|
1418
|
+
ctx.editor.jump_to_location(ctx.editor.current_quickfix_item)
|
|
1419
|
+
ctx.editor.echo("quickfix: #{items.length} item(s)")
|
|
1420
|
+
when :location_list
|
|
1421
|
+
ctx.editor.set_location_list(items, window_id: ctx.window.id)
|
|
1422
|
+
ctx.editor.select_location_list(0, window_id: ctx.window.id)
|
|
1423
|
+
ctx.editor.jump_to_location(ctx.editor.current_location_list_item(ctx.window.id))
|
|
1424
|
+
ctx.editor.echo("location list: #{items.length} item(s)")
|
|
1425
|
+
end
|
|
1426
|
+
end
|
|
1427
|
+
|
|
1428
|
+
def parse_grep_output(ctx, output)
|
|
1429
|
+
items = []
|
|
1430
|
+
output.each_line do |line|
|
|
1431
|
+
line = line.chomp
|
|
1432
|
+
# Parse filename:lineno:text format
|
|
1433
|
+
if (m = line.match(/\A(.+?):(\d+):(.*)?\z/))
|
|
1434
|
+
filepath = m[1]
|
|
1435
|
+
lineno = m[2].to_i - 1 # 0-based
|
|
1436
|
+
text = m[3].to_s
|
|
1437
|
+
buf = ensure_buffer_for_grep_file(ctx, filepath)
|
|
1438
|
+
items << { buffer_id: buf.id, row: lineno, col: 0, text: text }
|
|
1439
|
+
end
|
|
1440
|
+
end
|
|
1441
|
+
items
|
|
1442
|
+
end
|
|
1443
|
+
|
|
1444
|
+
def ensure_buffer_for_grep_file(ctx, filepath)
|
|
1445
|
+
abspath = File.expand_path(filepath)
|
|
1446
|
+
# Check if buffer already exists for this file
|
|
1447
|
+
existing = ctx.editor.buffers.values.find { |b| b.path && File.expand_path(b.path) == abspath }
|
|
1448
|
+
return existing if existing
|
|
1449
|
+
|
|
1450
|
+
# Create buffer for the file
|
|
1451
|
+
if File.exist?(abspath)
|
|
1452
|
+
ctx.editor.add_buffer_from_file(abspath)
|
|
1453
|
+
else
|
|
1454
|
+
ctx.editor.add_empty_buffer(path: abspath)
|
|
1455
|
+
end
|
|
1456
|
+
end
|
|
1457
|
+
|
|
895
1458
|
def grep_items_for_buffers(buffers, regex)
|
|
896
1459
|
Array(buffers).flat_map do |buffer|
|
|
897
1460
|
buffer.lines.each_with_index.flat_map do |line, row|
|
|
@@ -933,10 +1496,11 @@ module RuVim
|
|
|
933
1496
|
]
|
|
934
1497
|
end
|
|
935
1498
|
|
|
936
|
-
def open_list_window(ctx, kind:, title:, lines:)
|
|
1499
|
+
def open_list_window(ctx, kind:, title:, lines:, source_window_id:)
|
|
937
1500
|
editor = ctx.editor
|
|
938
1501
|
editor.split_current_window(layout: :horizontal)
|
|
939
1502
|
buffer = editor.add_virtual_buffer(kind:, name: title, lines:, filetype: "qf", readonly: true, modifiable: false)
|
|
1503
|
+
buffer.options["ruvim_list_source_window_id"] = source_window_id
|
|
940
1504
|
editor.switch_to_buffer(buffer.id)
|
|
941
1505
|
editor.echo(title)
|
|
942
1506
|
buffer
|
|
@@ -956,6 +1520,22 @@ module RuVim
|
|
|
956
1520
|
editor.echo("#{kind} closed")
|
|
957
1521
|
end
|
|
958
1522
|
|
|
1523
|
+
def refresh_list_window(editor, kind)
|
|
1524
|
+
wids = editor.find_window_ids_by_buffer_kind(kind)
|
|
1525
|
+
return if wids.empty?
|
|
1526
|
+
|
|
1527
|
+
lines = case kind
|
|
1528
|
+
when :quickfix then quickfix_buffer_lines(editor)
|
|
1529
|
+
when :location_list then location_list_buffer_lines(editor, editor.current_window_id)
|
|
1530
|
+
end
|
|
1531
|
+
wids.each do |wid|
|
|
1532
|
+
buf = editor.buffers[editor.windows[wid].buffer_id]
|
|
1533
|
+
next unless buf
|
|
1534
|
+
# Bypass modifiable check — this is an internal refresh of a readonly list buffer
|
|
1535
|
+
buf.instance_variable_set(:@lines, Array(lines).map(&:dup))
|
|
1536
|
+
end
|
|
1537
|
+
end
|
|
1538
|
+
|
|
959
1539
|
def quickfix_item_echo(editor)
|
|
960
1540
|
item = editor.current_quickfix_item
|
|
961
1541
|
list_item_echo(editor, item, editor.quickfix_index, editor.quickfix_items.length, label: "qf")
|
|
@@ -979,9 +1559,11 @@ module RuVim
|
|
|
979
1559
|
raise RuVim::CommandError, "No such buffer: #{buffer_id}"
|
|
980
1560
|
end
|
|
981
1561
|
|
|
982
|
-
if ctx.buffer.modified? && ctx.buffer.id != buffer_id && !bang
|
|
983
|
-
|
|
984
|
-
|
|
1562
|
+
if ctx.buffer.modified? && ctx.buffer.id != buffer_id && !bang && !ctx.editor.effective_option("hidden", window: ctx.window, buffer: ctx.buffer)
|
|
1563
|
+
unless maybe_autowrite_before_switch(ctx)
|
|
1564
|
+
ctx.editor.echo_error("Unsaved changes (use :w or :buffer! / :bnext! / :bprev!)")
|
|
1565
|
+
return
|
|
1566
|
+
end
|
|
985
1567
|
end
|
|
986
1568
|
|
|
987
1569
|
record_jump(ctx)
|
|
@@ -1017,28 +1599,55 @@ module RuVim
|
|
|
1017
1599
|
end.join("\n")
|
|
1018
1600
|
end
|
|
1019
1601
|
|
|
1602
|
+
def apply_autoindent_to_newline(ctx, row:, previous_row:, start_col: 0)
|
|
1603
|
+
return start_col unless ctx.editor.effective_option("autoindent", window: ctx.window, buffer: ctx.buffer)
|
|
1604
|
+
|
|
1605
|
+
prev = ctx.buffer.line_at(previous_row)
|
|
1606
|
+
indent = prev[/\A[ \t]*/].to_s
|
|
1607
|
+
|
|
1608
|
+
if ctx.editor.effective_option("smartindent", window: ctx.window, buffer: ctx.buffer)
|
|
1609
|
+
trimmed = prev.rstrip
|
|
1610
|
+
if trimmed.end_with?("{", "[", "(")
|
|
1611
|
+
sw = ctx.editor.effective_option("shiftwidth", window: ctx.window, buffer: ctx.buffer).to_i
|
|
1612
|
+
sw = 2 if sw <= 0
|
|
1613
|
+
indent += " " * sw
|
|
1614
|
+
end
|
|
1615
|
+
end
|
|
1616
|
+
|
|
1617
|
+
return start_col if indent.empty?
|
|
1618
|
+
|
|
1619
|
+
_y, x = ctx.buffer.insert_text(row, start_col, indent)
|
|
1620
|
+
x
|
|
1621
|
+
end
|
|
1622
|
+
|
|
1020
1623
|
def search_current_word(ctx, exact:, direction:)
|
|
1021
|
-
|
|
1624
|
+
keyword_rx = keyword_char_regex(ctx.editor, ctx.buffer, ctx.window)
|
|
1625
|
+
word = current_word_under_cursor(ctx.buffer, ctx.window, keyword_rx:)
|
|
1022
1626
|
if word.nil? || word.empty?
|
|
1023
1627
|
ctx.editor.echo("No word under cursor")
|
|
1024
1628
|
return
|
|
1025
1629
|
end
|
|
1026
1630
|
|
|
1027
|
-
pattern =
|
|
1631
|
+
pattern =
|
|
1632
|
+
if exact
|
|
1633
|
+
"(?<!#{keyword_rx.source})#{Regexp.escape(word)}(?!#{keyword_rx.source})"
|
|
1634
|
+
else
|
|
1635
|
+
Regexp.escape(word)
|
|
1636
|
+
end
|
|
1028
1637
|
ctx.editor.set_last_search(pattern:, direction:)
|
|
1029
1638
|
move_to_search(ctx, pattern:, direction:, count: 1)
|
|
1030
1639
|
end
|
|
1031
1640
|
|
|
1032
|
-
def current_word_under_cursor(buffer, window)
|
|
1641
|
+
def current_word_under_cursor(buffer, window, keyword_rx: /[[:alnum:]_]/)
|
|
1033
1642
|
line = buffer.line_at(window.cursor_y)
|
|
1034
1643
|
return nil if line.empty?
|
|
1035
1644
|
|
|
1036
1645
|
x = [window.cursor_x, line.length - 1].min
|
|
1037
1646
|
return nil if x.negative?
|
|
1038
1647
|
|
|
1039
|
-
if line[x]
|
|
1648
|
+
if !keyword_char?(line[x], keyword_rx)
|
|
1040
1649
|
left = x - 1
|
|
1041
|
-
if left >= 0 && line[left]
|
|
1650
|
+
if left >= 0 && keyword_char?(line[left], keyword_rx)
|
|
1042
1651
|
x = left
|
|
1043
1652
|
else
|
|
1044
1653
|
return nil
|
|
@@ -1046,9 +1655,9 @@ module RuVim
|
|
|
1046
1655
|
end
|
|
1047
1656
|
|
|
1048
1657
|
s = x
|
|
1049
|
-
s -= 1 while s.positive? && line[s - 1]
|
|
1658
|
+
s -= 1 while s.positive? && keyword_char?(line[s - 1], keyword_rx)
|
|
1050
1659
|
e = x + 1
|
|
1051
|
-
e += 1 while e < line.length && line[e]
|
|
1660
|
+
e += 1 while e < line.length && keyword_char?(line[e], keyword_rx)
|
|
1052
1661
|
line[s...e]
|
|
1053
1662
|
end
|
|
1054
1663
|
|
|
@@ -1132,7 +1741,7 @@ module RuVim
|
|
|
1132
1741
|
def delete_word_forward(ctx, count)
|
|
1133
1742
|
y = ctx.window.cursor_y
|
|
1134
1743
|
x = ctx.window.cursor_x
|
|
1135
|
-
target = advance_word_forward(ctx.buffer, y, x, count)
|
|
1744
|
+
target = advance_word_forward(ctx.buffer, y, x, count, editor: ctx.editor, window: ctx.window)
|
|
1136
1745
|
return true unless target
|
|
1137
1746
|
|
|
1138
1747
|
deleted = ctx.buffer.span_text(y, x, target[:row], target[:col])
|
|
@@ -1194,12 +1803,14 @@ module RuVim
|
|
|
1194
1803
|
true
|
|
1195
1804
|
end
|
|
1196
1805
|
|
|
1197
|
-
def advance_word_forward(buffer, row, col, count)
|
|
1806
|
+
def advance_word_forward(buffer, row, col, count, editor: nil, window: nil)
|
|
1198
1807
|
text = buffer.lines.join("\n")
|
|
1199
1808
|
flat = cursor_to_offset(buffer, row, col)
|
|
1200
1809
|
idx = flat
|
|
1810
|
+
keyword_rx = keyword_char_regex(editor, buffer, window)
|
|
1811
|
+
count = normalized_count(count)
|
|
1201
1812
|
count.times do
|
|
1202
|
-
idx = next_word_start_offset(text, idx)
|
|
1813
|
+
idx = next_word_start_offset(text, idx, keyword_rx)
|
|
1203
1814
|
return nil unless idx
|
|
1204
1815
|
end
|
|
1205
1816
|
offset_to_cursor(buffer, idx)
|
|
@@ -1209,14 +1820,14 @@ module RuVim
|
|
|
1209
1820
|
buffer = ctx.buffer
|
|
1210
1821
|
row = ctx.window.cursor_y
|
|
1211
1822
|
col = ctx.window.cursor_x
|
|
1212
|
-
count =
|
|
1823
|
+
count = normalized_count(count)
|
|
1213
1824
|
target = { row:, col: }
|
|
1214
1825
|
count.times do
|
|
1215
1826
|
target =
|
|
1216
1827
|
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)
|
|
1828
|
+
when :forward_start then advance_word_forward(buffer, target[:row], target[:col], 1, editor: ctx.editor, window: ctx.window)
|
|
1829
|
+
when :backward_start then advance_word_backward(buffer, target[:row], target[:col], 1, editor: ctx.editor, window: ctx.window)
|
|
1830
|
+
when :forward_end then advance_word_end(buffer, target[:row], target[:col], 1, editor: ctx.editor, window: ctx.window)
|
|
1220
1831
|
end
|
|
1221
1832
|
break unless target
|
|
1222
1833
|
end
|
|
@@ -1227,60 +1838,70 @@ module RuVim
|
|
|
1227
1838
|
ctx.window.clamp_to_buffer(buffer)
|
|
1228
1839
|
end
|
|
1229
1840
|
|
|
1230
|
-
def advance_word_backward(buffer, row, col, _count)
|
|
1841
|
+
def advance_word_backward(buffer, row, col, _count, editor: nil, window: nil)
|
|
1231
1842
|
text = buffer.lines.join("\n")
|
|
1232
1843
|
idx = cursor_to_offset(buffer, row, col)
|
|
1233
1844
|
idx = [idx - 1, 0].max
|
|
1234
|
-
|
|
1845
|
+
keyword_rx = keyword_char_regex(editor, buffer, window)
|
|
1846
|
+
while idx > 0 && char_class(text[idx], keyword_rx) == :space
|
|
1235
1847
|
idx -= 1
|
|
1236
1848
|
end
|
|
1237
|
-
cls = char_class(text[idx])
|
|
1238
|
-
while idx > 0 && char_class(text[idx - 1]) == cls && cls != :space
|
|
1849
|
+
cls = char_class(text[idx], keyword_rx)
|
|
1850
|
+
while idx > 0 && char_class(text[idx - 1], keyword_rx) == cls && cls != :space
|
|
1239
1851
|
idx -= 1
|
|
1240
1852
|
end
|
|
1241
|
-
while idx > 0 && char_class(text[idx]) == :space
|
|
1853
|
+
while idx > 0 && char_class(text[idx], keyword_rx) == :space
|
|
1242
1854
|
idx += 1
|
|
1243
1855
|
end
|
|
1244
1856
|
offset_to_cursor(buffer, idx)
|
|
1245
1857
|
end
|
|
1246
1858
|
|
|
1247
|
-
def advance_word_end(buffer, row, col, _count)
|
|
1859
|
+
def advance_word_end(buffer, row, col, _count, editor: nil, window: nil)
|
|
1248
1860
|
text = buffer.lines.join("\n")
|
|
1249
1861
|
idx = cursor_to_offset(buffer, row, col)
|
|
1250
1862
|
n = text.length
|
|
1251
|
-
|
|
1863
|
+
keyword_rx = keyword_char_regex(editor, buffer, window)
|
|
1864
|
+
|
|
1865
|
+
# Vim-like `e`: if already on the end of a word, move to the next word's end.
|
|
1866
|
+
if idx < n
|
|
1867
|
+
cur_cls = char_class(text[idx], keyword_rx)
|
|
1868
|
+
next_cls = (idx + 1 < n) ? char_class(text[idx + 1], keyword_rx) : nil
|
|
1869
|
+
idx += 1 if cur_cls != :space && next_cls != cur_cls
|
|
1870
|
+
end
|
|
1871
|
+
|
|
1872
|
+
while idx < n && char_class(text[idx], keyword_rx) == :space
|
|
1252
1873
|
idx += 1
|
|
1253
1874
|
end
|
|
1254
1875
|
return nil if idx >= n
|
|
1255
1876
|
|
|
1256
|
-
cls = char_class(text[idx])
|
|
1257
|
-
idx += 1 while idx + 1 < n && char_class(text[idx + 1]) == cls && cls != :space
|
|
1877
|
+
cls = char_class(text[idx], keyword_rx)
|
|
1878
|
+
idx += 1 while idx + 1 < n && char_class(text[idx + 1], keyword_rx) == cls && cls != :space
|
|
1258
1879
|
offset_to_cursor(buffer, idx)
|
|
1259
1880
|
end
|
|
1260
1881
|
|
|
1261
|
-
def next_word_start_offset(text, from_offset)
|
|
1882
|
+
def next_word_start_offset(text, from_offset, keyword_rx = nil)
|
|
1262
1883
|
i = [from_offset, 0].max
|
|
1263
1884
|
n = text.length
|
|
1264
1885
|
return nil if i >= n
|
|
1265
1886
|
|
|
1266
|
-
cls = char_class(text[i])
|
|
1887
|
+
cls = char_class(text[i], keyword_rx)
|
|
1267
1888
|
if cls == :word
|
|
1268
|
-
i += 1 while i < n && char_class(text[i]) == :word
|
|
1889
|
+
i += 1 while i < n && char_class(text[i], keyword_rx) == :word
|
|
1269
1890
|
elsif cls == :space
|
|
1270
|
-
i += 1 while i < n && char_class(text[i]) == :space
|
|
1891
|
+
i += 1 while i < n && char_class(text[i], keyword_rx) == :space
|
|
1271
1892
|
else
|
|
1272
1893
|
i += 1
|
|
1273
1894
|
end
|
|
1274
|
-
i += 1 while i < n && char_class(text[i]) == :space
|
|
1895
|
+
i += 1 while i < n && char_class(text[i], keyword_rx) == :space
|
|
1275
1896
|
return n if i > n
|
|
1276
1897
|
|
|
1277
1898
|
i <= n ? i : nil
|
|
1278
1899
|
end
|
|
1279
1900
|
|
|
1280
|
-
def char_class(ch)
|
|
1901
|
+
def char_class(ch, keyword_rx = nil)
|
|
1281
1902
|
return :space if ch == "\n"
|
|
1282
1903
|
return :space if ch =~ /\s/
|
|
1283
|
-
return :word if ch
|
|
1904
|
+
return :word if keyword_char?(ch, keyword_rx)
|
|
1284
1905
|
:punct
|
|
1285
1906
|
end
|
|
1286
1907
|
|
|
@@ -1306,11 +1927,12 @@ module RuVim
|
|
|
1306
1927
|
x = nxt
|
|
1307
1928
|
end
|
|
1308
1929
|
|
|
1309
|
-
|
|
1930
|
+
keyword_rx = keyword_char_regex(nil, buffer, window)
|
|
1931
|
+
cls = keyword_char?(line[x], keyword_rx) ? :word : :punct
|
|
1310
1932
|
start_col = x
|
|
1311
|
-
start_col -= 1 while start_col.positive? && same_word_class?(line[start_col - 1], cls)
|
|
1933
|
+
start_col -= 1 while start_col.positive? && same_word_class?(line[start_col - 1], cls, keyword_rx)
|
|
1312
1934
|
end_col = x + 1
|
|
1313
|
-
end_col += 1 while end_col < line.length && same_word_class?(line[end_col], cls)
|
|
1935
|
+
end_col += 1 while end_col < line.length && same_word_class?(line[end_col], cls, keyword_rx)
|
|
1314
1936
|
|
|
1315
1937
|
if around
|
|
1316
1938
|
while end_col < line.length && line[end_col] =~ /\s/
|
|
@@ -1493,15 +2115,170 @@ module RuVim
|
|
|
1493
2115
|
nil
|
|
1494
2116
|
end
|
|
1495
2117
|
|
|
1496
|
-
def same_word_class?(ch, cls)
|
|
2118
|
+
def same_word_class?(ch, cls, keyword_rx = nil)
|
|
1497
2119
|
return false if ch.nil?
|
|
1498
2120
|
case cls
|
|
1499
|
-
when :word then ch
|
|
1500
|
-
when :punct then !(ch =~
|
|
2121
|
+
when :word then keyword_char?(ch, keyword_rx)
|
|
2122
|
+
when :punct then !(keyword_char?(ch, keyword_rx) || ch =~ /\s/)
|
|
1501
2123
|
else false
|
|
1502
2124
|
end
|
|
1503
2125
|
end
|
|
1504
2126
|
|
|
2127
|
+
def keyword_char?(ch, keyword_rx = nil)
|
|
2128
|
+
return false if ch.nil?
|
|
2129
|
+
|
|
2130
|
+
(keyword_rx || /[[:alnum:]_]/).match?(ch)
|
|
2131
|
+
end
|
|
2132
|
+
|
|
2133
|
+
def keyword_char_regex(editor, buffer, window)
|
|
2134
|
+
win = window || editor&.current_window
|
|
2135
|
+
buf = buffer || editor&.current_buffer
|
|
2136
|
+
raw =
|
|
2137
|
+
if editor
|
|
2138
|
+
editor.effective_option("iskeyword", window: win, buffer: buf).to_s
|
|
2139
|
+
else
|
|
2140
|
+
buf&.options&.fetch("iskeyword", nil).to_s
|
|
2141
|
+
end
|
|
2142
|
+
RuVim::KeywordChars.regex(raw)
|
|
2143
|
+
end
|
|
2144
|
+
|
|
2145
|
+
def move_cursor_horizontally(ctx, direction:, count:)
|
|
2146
|
+
count = [count.to_i, 1].max
|
|
2147
|
+
allow_wrap = whichwrap_allows?(ctx, direction)
|
|
2148
|
+
virtualedit_mode = virtualedit_mode(ctx)
|
|
2149
|
+
count.times do
|
|
2150
|
+
line = ctx.buffer.line_at(ctx.window.cursor_y)
|
|
2151
|
+
if direction == :left
|
|
2152
|
+
if ctx.window.cursor_x > line.length && virtualedit_mode
|
|
2153
|
+
ctx.window.cursor_x -= 1
|
|
2154
|
+
elsif ctx.window.cursor_x.positive?
|
|
2155
|
+
ctx.window.cursor_x = RuVim::TextMetrics.previous_grapheme_char_index(line, ctx.window.cursor_x)
|
|
2156
|
+
elsif allow_wrap && ctx.window.cursor_y.positive?
|
|
2157
|
+
ctx.window.cursor_y -= 1
|
|
2158
|
+
ctx.window.cursor_x = ctx.buffer.line_length(ctx.window.cursor_y)
|
|
2159
|
+
end
|
|
2160
|
+
else
|
|
2161
|
+
max_right =
|
|
2162
|
+
case virtualedit_mode
|
|
2163
|
+
when :all then line.length + count + 1024 # practical cap; clamp below uses current cursor
|
|
2164
|
+
when :onemore then line.length + 1
|
|
2165
|
+
else line.length
|
|
2166
|
+
end
|
|
2167
|
+
if ctx.window.cursor_x < max_right
|
|
2168
|
+
ctx.window.cursor_x =
|
|
2169
|
+
if virtualedit_mode == :onemore && ctx.window.cursor_x == line.length
|
|
2170
|
+
line.length + 1
|
|
2171
|
+
elsif virtualedit_mode == :all && ctx.window.cursor_x >= line.length
|
|
2172
|
+
ctx.window.cursor_x + 1
|
|
2173
|
+
else
|
|
2174
|
+
RuVim::TextMetrics.next_grapheme_char_index(line, ctx.window.cursor_x)
|
|
2175
|
+
end
|
|
2176
|
+
ctx.window.cursor_x = [ctx.window.cursor_x, max_right].min
|
|
2177
|
+
elsif allow_wrap && ctx.window.cursor_y < ctx.buffer.line_count - 1
|
|
2178
|
+
ctx.window.cursor_y += 1
|
|
2179
|
+
ctx.window.cursor_x = 0
|
|
2180
|
+
end
|
|
2181
|
+
end
|
|
2182
|
+
end
|
|
2183
|
+
extra =
|
|
2184
|
+
case virtualedit_mode
|
|
2185
|
+
when :all
|
|
2186
|
+
[ctx.window.cursor_x - ctx.buffer.line_length(ctx.window.cursor_y), 0].max
|
|
2187
|
+
when :onemore
|
|
2188
|
+
1
|
|
2189
|
+
else
|
|
2190
|
+
0
|
|
2191
|
+
end
|
|
2192
|
+
ctx.window.clamp_to_buffer(ctx.buffer, max_extra_col: extra)
|
|
2193
|
+
end
|
|
2194
|
+
|
|
2195
|
+
def whichwrap_allows?(ctx, direction)
|
|
2196
|
+
toks = ctx.editor.effective_option("whichwrap", window: ctx.window, buffer: ctx.buffer).to_s
|
|
2197
|
+
.split(",").map { |s| s.strip.downcase }.reject(&:empty?)
|
|
2198
|
+
return false if toks.empty?
|
|
2199
|
+
|
|
2200
|
+
if direction == :left
|
|
2201
|
+
toks.include?("h") || toks.include?("<") || toks.include?("left")
|
|
2202
|
+
else
|
|
2203
|
+
toks.include?("l") || toks.include?(">") || toks.include?("right")
|
|
2204
|
+
end
|
|
2205
|
+
end
|
|
2206
|
+
|
|
2207
|
+
def virtualedit_mode(ctx)
|
|
2208
|
+
spec = ctx.editor.effective_option("virtualedit", window: ctx.window, buffer: ctx.buffer).to_s
|
|
2209
|
+
toks = spec.split(",").map { |s| s.strip.downcase }
|
|
2210
|
+
return :all if toks.include?("all")
|
|
2211
|
+
return :onemore if toks.include?("onemore")
|
|
2212
|
+
|
|
2213
|
+
nil
|
|
2214
|
+
end
|
|
2215
|
+
|
|
2216
|
+
def current_view_height(ctx)
|
|
2217
|
+
hint = ctx.editor.respond_to?(:current_window_view_height_hint) ? ctx.editor.current_window_view_height_hint : nil
|
|
2218
|
+
[hint.to_i, 1].max
|
|
2219
|
+
rescue StandardError
|
|
2220
|
+
1
|
|
2221
|
+
end
|
|
2222
|
+
|
|
2223
|
+
def current_page_step_lines(ctx)
|
|
2224
|
+
[current_view_height(ctx) - 1, 1].max
|
|
2225
|
+
end
|
|
2226
|
+
|
|
2227
|
+
def current_half_page_step_lines(ctx)
|
|
2228
|
+
[current_view_height(ctx) / 2, 1].max
|
|
2229
|
+
end
|
|
2230
|
+
|
|
2231
|
+
def scroll_window_vertically(ctx, direction:, lines:, view_height:, count:)
|
|
2232
|
+
step = [[lines.to_i, 1].max * [count.to_i, 1].max, 1].max
|
|
2233
|
+
height = [view_height.to_i, 1].max
|
|
2234
|
+
max_row_offset = [ctx.buffer.line_count - height, 0].max
|
|
2235
|
+
|
|
2236
|
+
before = ctx.window.row_offset.to_i
|
|
2237
|
+
after =
|
|
2238
|
+
if direction == :up
|
|
2239
|
+
[before - step, 0].max
|
|
2240
|
+
else
|
|
2241
|
+
[before + step, max_row_offset].min
|
|
2242
|
+
end
|
|
2243
|
+
return if after == before
|
|
2244
|
+
|
|
2245
|
+
ctx.window.row_offset = after
|
|
2246
|
+
|
|
2247
|
+
# Vim-like behavior: scroll viewport first, then keep cursor inside it.
|
|
2248
|
+
top = after
|
|
2249
|
+
bottom = after + height - 1
|
|
2250
|
+
if ctx.window.cursor_y < top
|
|
2251
|
+
ctx.window.cursor_y = top
|
|
2252
|
+
elsif ctx.window.cursor_y > bottom
|
|
2253
|
+
ctx.window.cursor_y = bottom
|
|
2254
|
+
end
|
|
2255
|
+
ctx.window.clamp_to_buffer(ctx.buffer)
|
|
2256
|
+
end
|
|
2257
|
+
|
|
2258
|
+
def place_cursor_line_in_window(ctx, where:, count:)
|
|
2259
|
+
if count
|
|
2260
|
+
target_row = [[normalized_count(count) - 1, 0].max, ctx.buffer.line_count - 1].min
|
|
2261
|
+
ctx.window.cursor_y = target_row
|
|
2262
|
+
ctx.window.clamp_to_buffer(ctx.buffer)
|
|
2263
|
+
end
|
|
2264
|
+
|
|
2265
|
+
height = current_view_height(ctx)
|
|
2266
|
+
max_row_offset = [ctx.buffer.line_count - height, 0].max
|
|
2267
|
+
desired =
|
|
2268
|
+
case where
|
|
2269
|
+
when :top
|
|
2270
|
+
ctx.window.cursor_y
|
|
2271
|
+
when :center
|
|
2272
|
+
ctx.window.cursor_y - (height / 2)
|
|
2273
|
+
when :bottom
|
|
2274
|
+
ctx.window.cursor_y - height + 1
|
|
2275
|
+
else
|
|
2276
|
+
ctx.window.row_offset
|
|
2277
|
+
end
|
|
2278
|
+
ctx.window.row_offset = [[desired, 0].max, max_row_offset].min
|
|
2279
|
+
ctx.window.clamp_to_buffer(ctx.buffer)
|
|
2280
|
+
end
|
|
2281
|
+
|
|
1505
2282
|
def cursor_to_offset(buffer, row, col)
|
|
1506
2283
|
offset = 0
|
|
1507
2284
|
row.times { |r| offset += buffer.line_length(r) + 1 }
|
|
@@ -1537,6 +2314,7 @@ module RuVim
|
|
|
1537
2314
|
lines = text.sub(/\n\z/, "").split("\n", -1)
|
|
1538
2315
|
return if lines.empty?
|
|
1539
2316
|
|
|
2317
|
+
count = normalized_count(count)
|
|
1540
2318
|
insert_at = before ? ctx.window.cursor_y : (ctx.window.cursor_y + 1)
|
|
1541
2319
|
ctx.buffer.begin_change_group
|
|
1542
2320
|
count.times { ctx.buffer.insert_lines_at(insert_at, lines) }
|
|
@@ -1612,13 +2390,143 @@ module RuVim
|
|
|
1612
2390
|
nil
|
|
1613
2391
|
end
|
|
1614
2392
|
|
|
2393
|
+
def normalized_count(count, default: 1)
|
|
2394
|
+
n = count.nil? ? default : count.to_i
|
|
2395
|
+
n = default if n <= 0
|
|
2396
|
+
n
|
|
2397
|
+
end
|
|
2398
|
+
|
|
2399
|
+
def ensure_modifiable_for_insert!(ctx)
|
|
2400
|
+
raise RuVim::CommandError, "Buffer is not modifiable" unless ctx.buffer.modifiable?
|
|
2401
|
+
end
|
|
2402
|
+
|
|
2403
|
+
def maybe_autowrite_before_switch(ctx)
|
|
2404
|
+
return false unless ctx.editor.effective_option("autowrite", window: ctx.window, buffer: ctx.buffer)
|
|
2405
|
+
return false unless ctx.buffer.file_buffer?
|
|
2406
|
+
return false unless ctx.buffer.path && !ctx.buffer.path.empty?
|
|
2407
|
+
|
|
2408
|
+
ctx.buffer.write_to(ctx.buffer.path)
|
|
2409
|
+
true
|
|
2410
|
+
rescue StandardError
|
|
2411
|
+
false
|
|
2412
|
+
end
|
|
2413
|
+
|
|
2414
|
+
def file_token_under_cursor(buffer, window)
|
|
2415
|
+
line = buffer.line_at(window.cursor_y)
|
|
2416
|
+
return nil if line.empty?
|
|
2417
|
+
|
|
2418
|
+
x = [[window.cursor_x, 0].max, [line.length - 1, 0].max].min
|
|
2419
|
+
file_char = /[[:alnum:]_\.\/~:-]/
|
|
2420
|
+
if line[x] !~ file_char
|
|
2421
|
+
left = x - 1
|
|
2422
|
+
right = x + 1
|
|
2423
|
+
if left >= 0 && line[left] =~ file_char
|
|
2424
|
+
x = left
|
|
2425
|
+
elsif right < line.length && line[right] =~ file_char
|
|
2426
|
+
x = right
|
|
2427
|
+
else
|
|
2428
|
+
return nil
|
|
2429
|
+
end
|
|
2430
|
+
end
|
|
2431
|
+
|
|
2432
|
+
s = x
|
|
2433
|
+
e = x + 1
|
|
2434
|
+
s -= 1 while s.positive? && line[s - 1] =~ file_char
|
|
2435
|
+
e += 1 while e < line.length && line[e] =~ file_char
|
|
2436
|
+
line[s...e]
|
|
2437
|
+
end
|
|
2438
|
+
|
|
2439
|
+
def parse_gf_target(token)
|
|
2440
|
+
raw = token.to_s
|
|
2441
|
+
if (m = /\A(.+):(\d+)\z/.match(raw))
|
|
2442
|
+
return { path: m[1], line: m[2].to_i } unless m[1].end_with?(":")
|
|
2443
|
+
end
|
|
2444
|
+
{ path: raw, line: nil }
|
|
2445
|
+
end
|
|
2446
|
+
|
|
2447
|
+
def move_cursor_to_gf_line(ctx, line_no)
|
|
2448
|
+
line = line_no.to_i
|
|
2449
|
+
return if line <= 0
|
|
2450
|
+
|
|
2451
|
+
w = ctx.editor.current_window
|
|
2452
|
+
b = ctx.editor.current_buffer
|
|
2453
|
+
w.cursor_y = [line - 1, b.line_count - 1].min
|
|
2454
|
+
w.cursor_x = 0
|
|
2455
|
+
w.clamp_to_buffer(b)
|
|
2456
|
+
end
|
|
2457
|
+
|
|
2458
|
+
def resolve_gf_path(ctx, token)
|
|
2459
|
+
candidates = gf_candidate_paths(ctx, token.to_s)
|
|
2460
|
+
candidates.find { |p| File.file?(p) || File.directory?(p) }
|
|
2461
|
+
end
|
|
2462
|
+
|
|
2463
|
+
def gf_candidate_paths(ctx, token)
|
|
2464
|
+
suffixes = gf_suffixes(ctx)
|
|
2465
|
+
names = [token]
|
|
2466
|
+
if File.extname(token).empty?
|
|
2467
|
+
suffixes.each { |suf| names << "#{token}#{suf}" }
|
|
2468
|
+
end
|
|
2469
|
+
names.uniq!
|
|
2470
|
+
|
|
2471
|
+
if token.start_with?("/", "~/")
|
|
2472
|
+
return names.map { |n| File.expand_path(n) }.uniq
|
|
2473
|
+
end
|
|
2474
|
+
|
|
2475
|
+
base_dirs = gf_search_dirs(ctx)
|
|
2476
|
+
base_dirs.product(names).map { |dir, name| File.expand_path(name, dir) }.uniq
|
|
2477
|
+
end
|
|
2478
|
+
|
|
2479
|
+
def gf_search_dirs(ctx)
|
|
2480
|
+
current_dir = if ctx.buffer.path && !ctx.buffer.path.empty?
|
|
2481
|
+
File.dirname(File.expand_path(ctx.buffer.path))
|
|
2482
|
+
else
|
|
2483
|
+
Dir.pwd
|
|
2484
|
+
end
|
|
2485
|
+
raw = ctx.editor.effective_option("path", window: ctx.window, buffer: ctx.buffer).to_s
|
|
2486
|
+
dirs = raw.split(",").map(&:strip).reject(&:empty?)
|
|
2487
|
+
dirs = ["."] if dirs.empty?
|
|
2488
|
+
dirs.flat_map do |dir|
|
|
2489
|
+
expand_gf_path_entry(dir, current_dir)
|
|
2490
|
+
end.uniq
|
|
2491
|
+
rescue StandardError
|
|
2492
|
+
[Dir.pwd]
|
|
2493
|
+
end
|
|
2494
|
+
|
|
2495
|
+
def gf_suffixes(ctx)
|
|
2496
|
+
raw = ctx.editor.effective_option("suffixesadd", window: ctx.window, buffer: ctx.buffer).to_s
|
|
2497
|
+
raw.split(",").map(&:strip).reject(&:empty?).map do |s|
|
|
2498
|
+
s.start_with?(".") ? s : ".#{s}"
|
|
2499
|
+
end
|
|
2500
|
+
end
|
|
2501
|
+
|
|
2502
|
+
def expand_gf_path_entry(entry, current_dir)
|
|
2503
|
+
dir = entry.to_s
|
|
2504
|
+
return [current_dir] if dir.empty? || dir == "."
|
|
2505
|
+
|
|
2506
|
+
expanded = File.expand_path(dir, current_dir)
|
|
2507
|
+
if expanded.end_with?("/**")
|
|
2508
|
+
base = expanded[0...-3]
|
|
2509
|
+
[base, *Dir.glob(File.join(base, "**", "*"))].select { |p| File.directory?(p) }
|
|
2510
|
+
elsif expanded.end_with?("**")
|
|
2511
|
+
base = expanded.sub(/\*\*\z/, "")
|
|
2512
|
+
base = base.sub(%r{/+\z}, "")
|
|
2513
|
+
[base, *Dir.glob(File.join(base, "**", "*"))].select { |p| File.directory?(p) }
|
|
2514
|
+
elsif expanded.match?(/[*?\[]/)
|
|
2515
|
+
Dir.glob(expanded).select { |p| File.directory?(p) }
|
|
2516
|
+
else
|
|
2517
|
+
[expanded]
|
|
2518
|
+
end
|
|
2519
|
+
rescue StandardError
|
|
2520
|
+
[expanded || File.expand_path(dir, current_dir)]
|
|
2521
|
+
end
|
|
2522
|
+
|
|
1615
2523
|
def ex_set_common(ctx, argv, scope:)
|
|
1616
2524
|
editor = ctx.editor
|
|
1617
2525
|
if argv.empty?
|
|
1618
2526
|
items = editor.option_snapshot(window: ctx.window, buffer: ctx.buffer).map do |opt|
|
|
1619
2527
|
format_option_value(opt[:name], opt[:effective])
|
|
1620
2528
|
end
|
|
1621
|
-
ctx.editor.
|
|
2529
|
+
ctx.editor.echo_multiline(items)
|
|
1622
2530
|
return
|
|
1623
2531
|
end
|
|
1624
2532
|
|
|
@@ -1766,7 +2674,235 @@ module RuVim
|
|
|
1766
2674
|
]
|
|
1767
2675
|
end
|
|
1768
2676
|
|
|
2677
|
+
def parse_bindings_args(argv)
|
|
2678
|
+
mode_filter = nil
|
|
2679
|
+
sort = "key"
|
|
2680
|
+
|
|
2681
|
+
Array(argv).each do |raw|
|
|
2682
|
+
token = raw.to_s.strip
|
|
2683
|
+
next if token.empty?
|
|
2684
|
+
|
|
2685
|
+
if token.include?("=")
|
|
2686
|
+
key, value = token.split("=", 2).map(&:strip)
|
|
2687
|
+
case key.downcase
|
|
2688
|
+
when "sort"
|
|
2689
|
+
sort = parse_bindings_sort(value)
|
|
2690
|
+
else
|
|
2691
|
+
raise RuVim::CommandError, "Unknown option for :bindings: #{key}"
|
|
2692
|
+
end
|
|
2693
|
+
next
|
|
2694
|
+
end
|
|
2695
|
+
|
|
2696
|
+
raise RuVim::CommandError, "Too many positional args for :bindings" if mode_filter
|
|
2697
|
+
|
|
2698
|
+
mode_filter = parse_bindings_mode_filter(token)
|
|
2699
|
+
end
|
|
2700
|
+
|
|
2701
|
+
[mode_filter, sort]
|
|
2702
|
+
end
|
|
2703
|
+
|
|
2704
|
+
def parse_bindings_sort(raw)
|
|
2705
|
+
token = raw.to_s.strip.downcase
|
|
2706
|
+
case token
|
|
2707
|
+
when "", "key", "keys" then "key"
|
|
2708
|
+
when "command", "cmd" then "command"
|
|
2709
|
+
else
|
|
2710
|
+
raise RuVim::CommandError, "Unknown sort for :bindings: #{raw}"
|
|
2711
|
+
end
|
|
2712
|
+
end
|
|
2713
|
+
|
|
2714
|
+
def parse_bindings_mode_filter(raw)
|
|
2715
|
+
return nil if raw.nil? || raw.to_s.strip.empty?
|
|
2716
|
+
|
|
2717
|
+
token = raw.to_s.strip.downcase
|
|
2718
|
+
case token
|
|
2719
|
+
when "n", "normal" then :normal
|
|
2720
|
+
when "i", "insert" then :insert
|
|
2721
|
+
when "v", "visual", "visual_char" then :visual_char
|
|
2722
|
+
when "vl", "visual_line" then :visual_line
|
|
2723
|
+
when "vb", "visual_block", "x" then :visual_block
|
|
2724
|
+
when "o", "operator", "operator_pending" then :operator_pending
|
|
2725
|
+
when "c", "cmdline", "command", "command_line" then :command_line
|
|
2726
|
+
else
|
|
2727
|
+
raise RuVim::CommandError, "Unknown mode for :bindings: #{raw}"
|
|
2728
|
+
end
|
|
2729
|
+
end
|
|
2730
|
+
|
|
2731
|
+
def bindings_buffer_lines(editor, entries, mode_filter:, sort:)
|
|
2732
|
+
buffer = editor.current_buffer
|
|
2733
|
+
filetype = buffer.options["filetype"].to_s
|
|
2734
|
+
filetype = nil if filetype.empty?
|
|
2735
|
+
|
|
2736
|
+
lines = [
|
|
2737
|
+
"Bindings",
|
|
2738
|
+
"",
|
|
2739
|
+
"Buffer: #{buffer.display_name}",
|
|
2740
|
+
"Filetype: #{filetype || '-'}",
|
|
2741
|
+
"Mode filter: #{mode_filter || 'all'}",
|
|
2742
|
+
"Sort: #{sort}",
|
|
2743
|
+
""
|
|
2744
|
+
]
|
|
2745
|
+
|
|
2746
|
+
any = false
|
|
2747
|
+
%i[buffer filetype app].each do |layer|
|
|
2748
|
+
layer_entries = entries.select { |e| e.layer == layer }
|
|
2749
|
+
next if layer_entries.empty?
|
|
2750
|
+
|
|
2751
|
+
any = true
|
|
2752
|
+
lines << "Layer: #{layer}"
|
|
2753
|
+
append_binding_entries_grouped!(lines, layer_entries, layer:, sort:)
|
|
2754
|
+
lines << ""
|
|
2755
|
+
end
|
|
2756
|
+
|
|
2757
|
+
lines << "(no bindings)" unless any
|
|
2758
|
+
lines
|
|
2759
|
+
end
|
|
2760
|
+
|
|
2761
|
+
def append_binding_entries_grouped!(lines, entries, layer:, sort:)
|
|
2762
|
+
groups = entries.group_by do |e|
|
|
2763
|
+
if layer == :app && e.scope == :global
|
|
2764
|
+
[:global, nil]
|
|
2765
|
+
elsif e.mode
|
|
2766
|
+
[:mode, e.mode]
|
|
2767
|
+
else
|
|
2768
|
+
[:plain, nil]
|
|
2769
|
+
end
|
|
2770
|
+
end
|
|
2771
|
+
|
|
2772
|
+
groups.keys.sort_by { |kind, mode| binding_group_sort_key(kind, mode) }.each do |kind, mode|
|
|
2773
|
+
group_entries = groups[[kind, mode]]
|
|
2774
|
+
next if group_entries.nil? || group_entries.empty?
|
|
2775
|
+
|
|
2776
|
+
if kind == :global
|
|
2777
|
+
lines << " [global]"
|
|
2778
|
+
elsif mode
|
|
2779
|
+
lines << " [#{mode}]"
|
|
2780
|
+
end
|
|
2781
|
+
|
|
2782
|
+
group_entries = sort_binding_entries(group_entries, sort:)
|
|
2783
|
+
parts = group_entries.map { |entry| binding_entry_display_parts(entry) }
|
|
2784
|
+
rhs_width = parts.map { |_, rhs, _| rhs.length }.max || 0
|
|
2785
|
+
parts.each do |lhs, rhs, desc|
|
|
2786
|
+
lines << format_binding_entry_line(lhs, rhs, desc, rhs_width:)
|
|
2787
|
+
end
|
|
2788
|
+
end
|
|
2789
|
+
end
|
|
2790
|
+
|
|
2791
|
+
def binding_group_sort_key(kind, mode)
|
|
2792
|
+
rank =
|
|
2793
|
+
case kind
|
|
2794
|
+
when :plain then 0
|
|
2795
|
+
when :mode then 1
|
|
2796
|
+
when :global then 2
|
|
2797
|
+
else 9
|
|
2798
|
+
end
|
|
2799
|
+
[rank, binding_mode_order_index(mode), mode.to_s]
|
|
2800
|
+
end
|
|
2801
|
+
|
|
2802
|
+
def binding_mode_order_index(mode)
|
|
2803
|
+
return -1 if mode.nil?
|
|
2804
|
+
|
|
2805
|
+
order = {
|
|
2806
|
+
normal: 0,
|
|
2807
|
+
insert: 1,
|
|
2808
|
+
visual_char: 2,
|
|
2809
|
+
visual_line: 3,
|
|
2810
|
+
visual_block: 4,
|
|
2811
|
+
operator_pending: 5,
|
|
2812
|
+
command_line: 6
|
|
2813
|
+
}
|
|
2814
|
+
order.fetch(mode.to_sym, 99)
|
|
2815
|
+
end
|
|
2816
|
+
|
|
2817
|
+
def sort_binding_entries(entries, sort:)
|
|
2818
|
+
case sort.to_s
|
|
2819
|
+
when "command"
|
|
2820
|
+
entries.sort_by do |e|
|
|
2821
|
+
[e.id.to_s, format_binding_tokens(e.tokens), e.bang ? 1 : 0, e.argv.inspect, e.kwargs.inspect]
|
|
2822
|
+
end
|
|
2823
|
+
else
|
|
2824
|
+
entries
|
|
2825
|
+
end
|
|
2826
|
+
end
|
|
2827
|
+
|
|
2828
|
+
def binding_entry_display_parts(entry)
|
|
2829
|
+
lhs = format_binding_tokens(entry.tokens)
|
|
2830
|
+
rhs = entry.id.to_s
|
|
2831
|
+
rhs += "!" if entry.bang
|
|
2832
|
+
rhs += " argv=#{entry.argv.inspect}" unless entry.argv.nil? || entry.argv.empty?
|
|
2833
|
+
rhs += " kwargs=#{entry.kwargs.inspect}" unless entry.kwargs.nil? || entry.kwargs.empty?
|
|
2834
|
+
desc = binding_command_desc(entry.id)
|
|
2835
|
+
[lhs, rhs, desc.to_s]
|
|
2836
|
+
end
|
|
2837
|
+
|
|
2838
|
+
def format_binding_entry_line(lhs, rhs, desc, rhs_width:)
|
|
2839
|
+
line = " #{lhs.ljust(18)} #{rhs.ljust(rhs_width)}"
|
|
2840
|
+
line += " #{desc}" unless desc.to_s.empty?
|
|
2841
|
+
line
|
|
2842
|
+
end
|
|
2843
|
+
|
|
2844
|
+
def format_binding_tokens(tokens)
|
|
2845
|
+
Array(tokens).map { |t| format_binding_token(t) }.join
|
|
2846
|
+
end
|
|
2847
|
+
|
|
2848
|
+
def format_binding_token(token)
|
|
2849
|
+
case token.to_s
|
|
2850
|
+
when "\e" then "<Esc>"
|
|
2851
|
+
when "\t" then "<Tab>"
|
|
2852
|
+
when "\r" then "<CR>"
|
|
2853
|
+
else token.to_s
|
|
2854
|
+
end
|
|
2855
|
+
end
|
|
2856
|
+
|
|
2857
|
+
def binding_command_desc(command_id)
|
|
2858
|
+
RuVim::CommandRegistry.instance.fetch(command_id).desc.to_s
|
|
2859
|
+
rescue StandardError
|
|
2860
|
+
""
|
|
2861
|
+
end
|
|
2862
|
+
|
|
2863
|
+
def ex_command_binding_labels(editor, ex_spec)
|
|
2864
|
+
keymaps = editor.keymap_manager
|
|
2865
|
+
return [] unless keymaps
|
|
2866
|
+
|
|
2867
|
+
command_ids = command_ids_for_ex_callable(ex_spec.call)
|
|
2868
|
+
return [] if command_ids.empty?
|
|
2869
|
+
|
|
2870
|
+
entries = keymaps.binding_entries_for_context(editor).select do |entry|
|
|
2871
|
+
entry.layer == :app && command_ids.include?(entry.id.to_s)
|
|
2872
|
+
end
|
|
2873
|
+
entries.sort_by do |entry|
|
|
2874
|
+
[binding_mode_order_index(entry.mode), entry.scope == :global ? 1 : 0, format_binding_tokens(entry.tokens)]
|
|
2875
|
+
end.map do |entry|
|
|
2876
|
+
format_ex_command_binding_label(entry)
|
|
2877
|
+
end.uniq
|
|
2878
|
+
end
|
|
2879
|
+
|
|
2880
|
+
def command_ids_for_ex_callable(callable)
|
|
2881
|
+
RuVim::CommandRegistry.instance.all.filter_map do |spec|
|
|
2882
|
+
spec.id if same_command_callable?(spec.call, callable)
|
|
2883
|
+
end
|
|
2884
|
+
end
|
|
2885
|
+
|
|
2886
|
+
def same_command_callable?(a, b)
|
|
2887
|
+
if (a.is_a?(Symbol) || a.is_a?(String)) && (b.is_a?(Symbol) || b.is_a?(String))
|
|
2888
|
+
return a.to_sym == b.to_sym
|
|
2889
|
+
end
|
|
2890
|
+
a.equal?(b)
|
|
2891
|
+
end
|
|
2892
|
+
|
|
2893
|
+
def format_ex_command_binding_label(entry)
|
|
2894
|
+
lhs = format_binding_tokens(entry.tokens)
|
|
2895
|
+
if entry.scope == :global
|
|
2896
|
+
"global:#{lhs}"
|
|
2897
|
+
elsif entry.mode && entry.mode.to_sym != :normal
|
|
2898
|
+
"#{entry.mode}:#{lhs}"
|
|
2899
|
+
else
|
|
2900
|
+
lhs
|
|
2901
|
+
end
|
|
2902
|
+
end
|
|
2903
|
+
|
|
1769
2904
|
def paste_charwise(ctx, text, before:, count:)
|
|
2905
|
+
count = normalized_count(count)
|
|
1770
2906
|
y = ctx.window.cursor_y
|
|
1771
2907
|
x = ctx.window.cursor_x
|
|
1772
2908
|
insert_col = before ? x : [x + 1, ctx.buffer.line_length(y)].min
|
|
@@ -1797,7 +2933,7 @@ module RuVim
|
|
|
1797
2933
|
end
|
|
1798
2934
|
|
|
1799
2935
|
def move_to_search(ctx, pattern:, direction:, count:)
|
|
1800
|
-
count =
|
|
2936
|
+
count = normalized_count(count)
|
|
1801
2937
|
regex = compile_search_regex(pattern, editor: ctx.editor, window: ctx.window, buffer: ctx.buffer)
|
|
1802
2938
|
count.times do
|
|
1803
2939
|
match = find_next_match(ctx.buffer, ctx.window, regex, direction: direction)
|
|
@@ -1885,5 +3021,141 @@ module RuVim
|
|
|
1885
3021
|
rescue StandardError
|
|
1886
3022
|
0
|
|
1887
3023
|
end
|
|
3024
|
+
|
|
3025
|
+
def parse_substitute_flags(flags_str, default_global: false)
|
|
3026
|
+
flags = { global: default_global, ignore_case: false, match_case: false, count_only: false, no_error: false, confirm: false }
|
|
3027
|
+
return flags if flags_str.nil? || flags_str.empty?
|
|
3028
|
+
|
|
3029
|
+
flags_str.each_char do |ch|
|
|
3030
|
+
case ch
|
|
3031
|
+
when "g" then flags[:global] = true
|
|
3032
|
+
when "i" then flags[:ignore_case] = true
|
|
3033
|
+
when "I" then flags[:match_case] = true
|
|
3034
|
+
when "n" then flags[:count_only] = true
|
|
3035
|
+
when "e" then flags[:no_error] = true
|
|
3036
|
+
when "c" then flags[:confirm] = true
|
|
3037
|
+
end
|
|
3038
|
+
end
|
|
3039
|
+
flags
|
|
3040
|
+
end
|
|
3041
|
+
|
|
3042
|
+
def build_substitute_regex(pattern, flags, ctx)
|
|
3043
|
+
if flags[:match_case]
|
|
3044
|
+
# I flag: force case-sensitive
|
|
3045
|
+
Regexp.new(pattern.to_s)
|
|
3046
|
+
elsif flags[:ignore_case]
|
|
3047
|
+
# i flag: force case-insensitive
|
|
3048
|
+
Regexp.new(pattern.to_s, Regexp::IGNORECASE)
|
|
3049
|
+
else
|
|
3050
|
+
compile_search_regex(pattern, editor: ctx.editor, window: ctx.window, buffer: ctx.buffer)
|
|
3051
|
+
end
|
|
3052
|
+
rescue RegexpError => e
|
|
3053
|
+
raise RuVim::CommandError, "Invalid regex: #{e.message}"
|
|
3054
|
+
end
|
|
3055
|
+
|
|
3056
|
+
def substitute_range(ctx, regex, replacement, r_start, r_end, flags)
|
|
3057
|
+
changed = 0
|
|
3058
|
+
new_lines = ctx.buffer.lines.each_with_index.map do |line, idx|
|
|
3059
|
+
if idx >= r_start && idx <= r_end
|
|
3060
|
+
if flags[:global]
|
|
3061
|
+
line.scan(regex) { changed += 1 }
|
|
3062
|
+
line.gsub(regex, replacement)
|
|
3063
|
+
else
|
|
3064
|
+
if line.match?(regex)
|
|
3065
|
+
changed += 1
|
|
3066
|
+
line.sub(regex, replacement)
|
|
3067
|
+
else
|
|
3068
|
+
line
|
|
3069
|
+
end
|
|
3070
|
+
end
|
|
3071
|
+
else
|
|
3072
|
+
line
|
|
3073
|
+
end
|
|
3074
|
+
end
|
|
3075
|
+
|
|
3076
|
+
if changed.positive?
|
|
3077
|
+
ctx.buffer.begin_change_group
|
|
3078
|
+
ctx.buffer.replace_all_lines!(new_lines)
|
|
3079
|
+
ctx.buffer.end_change_group
|
|
3080
|
+
end
|
|
3081
|
+
changed
|
|
3082
|
+
end
|
|
3083
|
+
|
|
3084
|
+
def count_matches_in_range(buffer, regex, r_start, r_end, global)
|
|
3085
|
+
total = 0
|
|
3086
|
+
(r_start..r_end).each do |idx|
|
|
3087
|
+
line = buffer.line_at(idx)
|
|
3088
|
+
if global
|
|
3089
|
+
line.scan(regex) { total += 1 }
|
|
3090
|
+
else
|
|
3091
|
+
total += 1 if line.match?(regex)
|
|
3092
|
+
end
|
|
3093
|
+
end
|
|
3094
|
+
total
|
|
3095
|
+
end
|
|
3096
|
+
|
|
3097
|
+
public
|
|
3098
|
+
|
|
3099
|
+
def arglist_show(ctx, **)
|
|
3100
|
+
arglist = ctx.editor.arglist
|
|
3101
|
+
if arglist.empty?
|
|
3102
|
+
ctx.editor.echo("No arguments")
|
|
3103
|
+
return
|
|
3104
|
+
end
|
|
3105
|
+
|
|
3106
|
+
current_index = ctx.editor.arglist_index
|
|
3107
|
+
items = arglist.map.with_index do |path, i|
|
|
3108
|
+
if i == current_index
|
|
3109
|
+
"[#{path}]"
|
|
3110
|
+
else
|
|
3111
|
+
" #{path}"
|
|
3112
|
+
end
|
|
3113
|
+
end
|
|
3114
|
+
ctx.editor.echo_multiline(items)
|
|
3115
|
+
end
|
|
3116
|
+
|
|
3117
|
+
def arglist_next(ctx, count:, **)
|
|
3118
|
+
count = normalized_count(count)
|
|
3119
|
+
path = ctx.editor.arglist_next(count)
|
|
3120
|
+
switch_to_file(ctx, path)
|
|
3121
|
+
ctx.editor.echo("Argument #{ctx.editor.arglist_index + 1} of #{ctx.editor.arglist.length}: #{path}")
|
|
3122
|
+
end
|
|
3123
|
+
|
|
3124
|
+
def arglist_prev(ctx, count:, **)
|
|
3125
|
+
count = normalized_count(count)
|
|
3126
|
+
path = ctx.editor.arglist_prev(count)
|
|
3127
|
+
switch_to_file(ctx, path)
|
|
3128
|
+
ctx.editor.echo("Argument #{ctx.editor.arglist_index + 1} of #{ctx.editor.arglist.length}: #{path}")
|
|
3129
|
+
end
|
|
3130
|
+
|
|
3131
|
+
def arglist_first(ctx, **)
|
|
3132
|
+
path = ctx.editor.arglist_first
|
|
3133
|
+
return ctx.editor.error("No arguments") unless path
|
|
3134
|
+
switch_to_file(ctx, path)
|
|
3135
|
+
ctx.editor.echo("Argument 1 of #{ctx.editor.arglist.length}: #{path}")
|
|
3136
|
+
end
|
|
3137
|
+
|
|
3138
|
+
def arglist_last(ctx, **)
|
|
3139
|
+
path = ctx.editor.arglist_last
|
|
3140
|
+
return ctx.editor.error("No arguments") unless path
|
|
3141
|
+
switch_to_file(ctx, path)
|
|
3142
|
+
ctx.editor.echo("Argument #{ctx.editor.arglist.length} of #{ctx.editor.arglist.length}: #{path}")
|
|
3143
|
+
end
|
|
3144
|
+
|
|
3145
|
+
private
|
|
3146
|
+
|
|
3147
|
+
def switch_to_file(ctx, path)
|
|
3148
|
+
existing_buffer = ctx.editor.buffers.values.find { |buf| buf.path == path }
|
|
3149
|
+
if existing_buffer
|
|
3150
|
+
ctx.editor.set_alternate_buffer_id(ctx.editor.current_buffer.id)
|
|
3151
|
+
ctx.editor.activate_buffer(existing_buffer.id)
|
|
3152
|
+
existing_buffer.id
|
|
3153
|
+
else
|
|
3154
|
+
ctx.editor.set_alternate_buffer_id(ctx.editor.current_buffer.id)
|
|
3155
|
+
buffer = ctx.editor.add_buffer_from_file(path)
|
|
3156
|
+
ctx.editor.current_window.buffer_id = buffer.id
|
|
3157
|
+
buffer.id
|
|
3158
|
+
end
|
|
3159
|
+
end
|
|
1888
3160
|
end
|
|
1889
3161
|
end
|