ruvim 0.2.0 → 0.4.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 +96 -0
- data/CLAUDE.md +1 -0
- data/README.md +15 -1
- data/docs/binding.md +39 -0
- data/docs/command.md +163 -4
- data/docs/config.md +12 -4
- data/docs/done.md +21 -0
- data/docs/spec.md +214 -18
- data/docs/todo.md +1 -5
- data/docs/tutorial.md +24 -0
- data/docs/vim_diff.md +105 -173
- data/lib/ruvim/app.rb +1165 -70
- data/lib/ruvim/buffer.rb +47 -1
- data/lib/ruvim/cli.rb +18 -3
- data/lib/ruvim/clipboard.rb +2 -0
- data/lib/ruvim/command_invocation.rb +3 -1
- data/lib/ruvim/command_line.rb +2 -0
- data/lib/ruvim/command_registry.rb +2 -0
- data/lib/ruvim/config_dsl.rb +2 -0
- data/lib/ruvim/config_loader.rb +2 -0
- data/lib/ruvim/context.rb +2 -0
- data/lib/ruvim/dispatcher.rb +143 -13
- data/lib/ruvim/display_width.rb +3 -0
- data/lib/ruvim/editor.rb +466 -71
- data/lib/ruvim/ex_command_registry.rb +2 -0
- data/lib/ruvim/file_watcher.rb +243 -0
- data/lib/ruvim/git/blame.rb +245 -0
- data/lib/ruvim/git/branch.rb +97 -0
- data/lib/ruvim/git/commit.rb +102 -0
- data/lib/ruvim/git/diff.rb +129 -0
- data/lib/ruvim/git/handler.rb +84 -0
- data/lib/ruvim/git/log.rb +41 -0
- data/lib/ruvim/git/status.rb +103 -0
- data/lib/ruvim/global_commands.rb +1066 -105
- data/lib/ruvim/highlighter.rb +19 -22
- data/lib/ruvim/input.rb +40 -28
- data/lib/ruvim/keymap_manager.rb +83 -0
- data/lib/ruvim/keyword_chars.rb +2 -0
- data/lib/ruvim/lang/base.rb +25 -0
- data/lib/ruvim/lang/csv.rb +18 -0
- data/lib/ruvim/lang/diff.rb +41 -0
- data/lib/ruvim/lang/json.rb +52 -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/json_renderer.rb +131 -0
- data/lib/ruvim/rich_view/jsonl_renderer.rb +57 -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 +109 -0
- data/lib/ruvim/screen.rb +503 -109
- data/lib/ruvim/terminal.rb +18 -1
- data/lib/ruvim/text_metrics.rb +2 -0
- data/lib/ruvim/version.rb +1 -1
- data/lib/ruvim/window.rb +2 -0
- data/lib/ruvim.rb +24 -0
- data/test/app_completion_test.rb +98 -0
- data/test/app_dot_repeat_test.rb +13 -0
- data/test/app_motion_test.rb +13 -0
- data/test/app_scenario_test.rb +898 -1
- data/test/app_startup_test.rb +187 -0
- data/test/arglist_test.rb +113 -0
- data/test/buffer_test.rb +49 -30
- data/test/cli_test.rb +14 -0
- data/test/clipboard_test.rb +67 -0
- data/test/command_line_test.rb +118 -0
- data/test/config_dsl_test.rb +87 -0
- data/test/dispatcher_test.rb +322 -0
- data/test/display_width_test.rb +41 -0
- data/test/editor_register_test.rb +23 -0
- data/test/file_watcher_test.rb +197 -0
- data/test/follow_test.rb +199 -0
- data/test/git_blame_test.rb +713 -0
- data/test/highlighter_test.rb +165 -0
- data/test/indent_test.rb +287 -0
- data/test/input_screen_integration_test.rb +40 -2
- data/test/markdown_renderer_test.rb +279 -0
- data/test/on_save_hook_test.rb +150 -0
- data/test/rich_view_test.rb +734 -0
- data/test/screen_test.rb +304 -0
- data/test/search_option_test.rb +19 -0
- data/test/test_helper.rb +9 -0
- metadata +49 -2
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require "tempfile"
|
|
2
4
|
require "open3"
|
|
3
5
|
|
|
@@ -5,7 +7,7 @@ module RuVim
|
|
|
5
7
|
class GlobalCommands
|
|
6
8
|
include Singleton
|
|
7
9
|
|
|
8
|
-
def call(spec_call, ctx, argv: [], kwargs: {}, bang: false, count:
|
|
10
|
+
def call(spec_call, ctx, argv: [], kwargs: {}, bang: false, count: nil)
|
|
9
11
|
case spec_call
|
|
10
12
|
when Symbol, String
|
|
11
13
|
public_send(spec_call.to_sym, ctx, argv: argv, kwargs: kwargs, bang: bang, count: count)
|
|
@@ -23,11 +25,11 @@ module RuVim
|
|
|
23
25
|
end
|
|
24
26
|
|
|
25
27
|
def cursor_up(ctx, count:, **)
|
|
26
|
-
ctx.window.move_up(ctx.buffer, count)
|
|
28
|
+
ctx.window.move_up(ctx.buffer, normalized_count(count))
|
|
27
29
|
end
|
|
28
30
|
|
|
29
31
|
def cursor_down(ctx, count:, **)
|
|
30
|
-
ctx.window.move_down(ctx.buffer, count)
|
|
32
|
+
ctx.window.move_down(ctx.buffer, normalized_count(count))
|
|
31
33
|
end
|
|
32
34
|
|
|
33
35
|
def cursor_page_up(ctx, kwargs:, count:, **)
|
|
@@ -72,6 +74,18 @@ module RuVim
|
|
|
72
74
|
call(:window_scroll_down, ctx, count:, bang:, kwargs: { lines: 1, view_height: current_view_height(ctx) + 1 })
|
|
73
75
|
end
|
|
74
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
|
+
|
|
75
89
|
def cursor_line_start(ctx, **)
|
|
76
90
|
ctx.window.cursor_x = 0
|
|
77
91
|
ctx.window.clamp_to_buffer(ctx.buffer)
|
|
@@ -91,7 +105,7 @@ module RuVim
|
|
|
91
105
|
|
|
92
106
|
def cursor_buffer_start(ctx, count:, **)
|
|
93
107
|
record_jump(ctx)
|
|
94
|
-
target_row = [count.to_i - 1, 0].max
|
|
108
|
+
target_row = [normalized_count(count).to_i - 1, 0].max
|
|
95
109
|
target_row = [target_row, ctx.buffer.line_count - 1].min
|
|
96
110
|
ctx.window.cursor_y = target_row
|
|
97
111
|
cursor_first_nonblank(ctx)
|
|
@@ -99,11 +113,7 @@ module RuVim
|
|
|
99
113
|
|
|
100
114
|
def cursor_buffer_end(ctx, count:, **)
|
|
101
115
|
record_jump(ctx)
|
|
102
|
-
|
|
103
|
-
target_row = [count - 1, ctx.buffer.line_count - 1].min
|
|
104
|
-
else
|
|
105
|
-
target_row = ctx.buffer.line_count - 1
|
|
106
|
-
end
|
|
116
|
+
target_row = count.nil? ? (ctx.buffer.line_count - 1) : [normalized_count(count) - 1, ctx.buffer.line_count - 1].min
|
|
107
117
|
ctx.window.cursor_y = target_row
|
|
108
118
|
cursor_first_nonblank(ctx)
|
|
109
119
|
end
|
|
@@ -156,6 +166,7 @@ module RuVim
|
|
|
156
166
|
|
|
157
167
|
def enter_insert_mode(ctx, **)
|
|
158
168
|
materialize_intro_buffer_if_needed(ctx)
|
|
169
|
+
ensure_modifiable_for_insert!(ctx)
|
|
159
170
|
ctx.buffer.begin_change_group
|
|
160
171
|
ctx.editor.enter_insert_mode
|
|
161
172
|
ctx.editor.echo("-- INSERT --")
|
|
@@ -180,6 +191,7 @@ module RuVim
|
|
|
180
191
|
|
|
181
192
|
def open_line_below(ctx, **)
|
|
182
193
|
materialize_intro_buffer_if_needed(ctx)
|
|
194
|
+
ensure_modifiable_for_insert!(ctx)
|
|
183
195
|
y = ctx.window.cursor_y
|
|
184
196
|
x = ctx.buffer.line_length(y)
|
|
185
197
|
ctx.buffer.begin_change_group
|
|
@@ -193,6 +205,7 @@ module RuVim
|
|
|
193
205
|
|
|
194
206
|
def open_line_above(ctx, **)
|
|
195
207
|
materialize_intro_buffer_if_needed(ctx)
|
|
208
|
+
ensure_modifiable_for_insert!(ctx)
|
|
196
209
|
y = ctx.window.cursor_y
|
|
197
210
|
ctx.buffer.begin_change_group
|
|
198
211
|
_new_y, new_x = ctx.buffer.insert_newline(y, 0)
|
|
@@ -250,6 +263,42 @@ module RuVim
|
|
|
250
263
|
ctx.editor.focus_window_direction(:down)
|
|
251
264
|
end
|
|
252
265
|
|
|
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)
|
|
281
|
+
end
|
|
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)
|
|
288
|
+
else
|
|
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
|
+
|
|
253
302
|
def tab_new(ctx, argv:, **)
|
|
254
303
|
path = argv[0]
|
|
255
304
|
if ctx.buffer.modified? && !ctx.editor.effective_option("hidden", window: ctx.window, buffer: ctx.buffer)
|
|
@@ -258,24 +307,39 @@ module RuVim
|
|
|
258
307
|
return
|
|
259
308
|
end
|
|
260
309
|
end
|
|
261
|
-
|
|
262
|
-
if path && !path.empty?
|
|
263
|
-
b = ctx.editor.current_buffer
|
|
264
|
-
ctx.editor.echo("tab #{ctx.editor.current_tabpage_number}/#{ctx.editor.tabpage_count}: #{b.path || '[No Name]'}")
|
|
265
|
-
else
|
|
266
|
-
ctx.editor.echo("tab #{ctx.editor.current_tabpage_number}/#{ctx.editor.tabpage_count}")
|
|
267
|
-
end
|
|
268
|
-
tab
|
|
310
|
+
ctx.editor.tabnew(path: path)
|
|
269
311
|
end
|
|
270
312
|
|
|
271
313
|
def tab_next(ctx, count:, **)
|
|
314
|
+
count = normalized_count(count)
|
|
272
315
|
ctx.editor.tabnext(count)
|
|
273
|
-
ctx.editor.echo("tab #{ctx.editor.current_tabpage_number}/#{ctx.editor.tabpage_count}")
|
|
274
316
|
end
|
|
275
317
|
|
|
276
318
|
def tab_prev(ctx, count:, **)
|
|
319
|
+
count = normalized_count(count)
|
|
277
320
|
ctx.editor.tabprev(count)
|
|
278
|
-
|
|
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)
|
|
279
343
|
end
|
|
280
344
|
|
|
281
345
|
def enter_command_line_mode(ctx, **)
|
|
@@ -295,6 +359,7 @@ module RuVim
|
|
|
295
359
|
|
|
296
360
|
def delete_char(ctx, count:, **)
|
|
297
361
|
materialize_intro_buffer_if_needed(ctx)
|
|
362
|
+
count = normalized_count(count)
|
|
298
363
|
ctx.buffer.begin_change_group
|
|
299
364
|
deleted = +""
|
|
300
365
|
count.times do
|
|
@@ -308,8 +373,80 @@ module RuVim
|
|
|
308
373
|
ctx.window.clamp_to_buffer(ctx.buffer)
|
|
309
374
|
end
|
|
310
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
|
+
|
|
311
447
|
def delete_line(ctx, count:, **)
|
|
312
448
|
materialize_intro_buffer_if_needed(ctx)
|
|
449
|
+
count = normalized_count(count)
|
|
313
450
|
ctx.buffer.begin_change_group
|
|
314
451
|
deleted_lines = []
|
|
315
452
|
count.times { deleted_lines << ctx.buffer.delete_line(ctx.window.cursor_y) }
|
|
@@ -321,14 +458,17 @@ module RuVim
|
|
|
321
458
|
def delete_motion(ctx, count:, kwargs:, **)
|
|
322
459
|
materialize_intro_buffer_if_needed(ctx)
|
|
323
460
|
motion = (kwargs[:motion] || kwargs["motion"]).to_s
|
|
461
|
+
ncount = normalized_count(count)
|
|
324
462
|
handled =
|
|
325
463
|
case motion
|
|
326
|
-
when "h" then delete_chars_left(ctx,
|
|
327
|
-
when "l" then delete_chars_right(ctx,
|
|
328
|
-
when "j" then delete_lines_down(ctx,
|
|
329
|
-
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)
|
|
330
468
|
when "$" then delete_to_end_of_line(ctx)
|
|
331
|
-
when "w" then delete_word_forward(ctx,
|
|
469
|
+
when "w" then delete_word_forward(ctx, ncount)
|
|
470
|
+
when "G" then delete_lines_to_end(ctx)
|
|
471
|
+
when "gg" then delete_lines_to_start(ctx)
|
|
332
472
|
when "iw" then delete_text_object_word(ctx, around: false)
|
|
333
473
|
when "aw" then delete_text_object_word(ctx, around: true)
|
|
334
474
|
else
|
|
@@ -340,9 +480,22 @@ module RuVim
|
|
|
340
480
|
|
|
341
481
|
def change_motion(ctx, count:, kwargs:, **)
|
|
342
482
|
materialize_intro_buffer_if_needed(ctx)
|
|
343
|
-
|
|
344
|
-
|
|
483
|
+
motion = (kwargs[:motion] || kwargs["motion"]).to_s
|
|
484
|
+
result = delete_motion(ctx, count:, kwargs:)
|
|
485
|
+
return unless result
|
|
345
486
|
|
|
487
|
+
if result == :linewise
|
|
488
|
+
case motion
|
|
489
|
+
when "G"
|
|
490
|
+
y = ctx.buffer.lines.length
|
|
491
|
+
ctx.buffer.insert_lines_at(y, [""])
|
|
492
|
+
ctx.window.cursor_y = y
|
|
493
|
+
when "gg"
|
|
494
|
+
ctx.buffer.insert_lines_at(0, [""])
|
|
495
|
+
ctx.window.cursor_y = 0
|
|
496
|
+
end
|
|
497
|
+
ctx.window.cursor_x = 0
|
|
498
|
+
end
|
|
346
499
|
enter_insert_mode(ctx)
|
|
347
500
|
end
|
|
348
501
|
|
|
@@ -433,6 +586,7 @@ module RuVim
|
|
|
433
586
|
|
|
434
587
|
def replace_char(ctx, argv:, count:, **)
|
|
435
588
|
materialize_intro_buffer_if_needed(ctx)
|
|
589
|
+
count = normalized_count(count)
|
|
436
590
|
ch = argv[0].to_s
|
|
437
591
|
raise RuVim::CommandError, "replace requires a character" if ch.empty?
|
|
438
592
|
|
|
@@ -453,6 +607,7 @@ module RuVim
|
|
|
453
607
|
end
|
|
454
608
|
|
|
455
609
|
def yank_line(ctx, count:, **)
|
|
610
|
+
count = normalized_count(count)
|
|
456
611
|
start = ctx.window.cursor_y
|
|
457
612
|
text = ctx.buffer.line_block_text(start, count)
|
|
458
613
|
store_yank_register(ctx, text:, type: :linewise)
|
|
@@ -470,6 +625,10 @@ module RuVim
|
|
|
470
625
|
text = ctx.buffer.span_text(y, x, target[:row], target[:col])
|
|
471
626
|
store_yank_register(ctx, text:, type: :charwise)
|
|
472
627
|
ctx.editor.echo("yanked")
|
|
628
|
+
when "G"
|
|
629
|
+
yank_lines_to_end(ctx)
|
|
630
|
+
when "gg"
|
|
631
|
+
yank_lines_to_start(ctx)
|
|
473
632
|
when "iw"
|
|
474
633
|
yank_text_object_word(ctx, around: false)
|
|
475
634
|
when "aw"
|
|
@@ -557,6 +716,45 @@ module RuVim
|
|
|
557
716
|
ctx.editor.enter_normal_mode
|
|
558
717
|
end
|
|
559
718
|
|
|
719
|
+
def indent_lines(ctx, count:, **)
|
|
720
|
+
count = normalized_count(count)
|
|
721
|
+
start_row = ctx.window.cursor_y
|
|
722
|
+
end_row = [start_row + count - 1, ctx.buffer.line_count - 1].min
|
|
723
|
+
reindent_range(ctx, start_row, end_row)
|
|
724
|
+
end
|
|
725
|
+
|
|
726
|
+
def indent_motion(ctx, count:, kwargs:, **)
|
|
727
|
+
motion = (kwargs[:motion] || kwargs["motion"]).to_s
|
|
728
|
+
ncount = normalized_count(count)
|
|
729
|
+
start_row = ctx.window.cursor_y
|
|
730
|
+
case motion
|
|
731
|
+
when "j"
|
|
732
|
+
end_row = [start_row + ncount, ctx.buffer.line_count - 1].min
|
|
733
|
+
when "k"
|
|
734
|
+
end_row = start_row
|
|
735
|
+
start_row = [start_row - ncount, 0].max
|
|
736
|
+
when "G"
|
|
737
|
+
end_row = ctx.buffer.line_count - 1
|
|
738
|
+
when "gg"
|
|
739
|
+
end_row = start_row
|
|
740
|
+
start_row = 0
|
|
741
|
+
else
|
|
742
|
+
ctx.editor.echo("Unsupported motion for =: #{motion}")
|
|
743
|
+
return
|
|
744
|
+
end
|
|
745
|
+
reindent_range(ctx, start_row, end_row)
|
|
746
|
+
end
|
|
747
|
+
|
|
748
|
+
def visual_indent(ctx, **)
|
|
749
|
+
sel = ctx.editor.visual_selection
|
|
750
|
+
return unless sel
|
|
751
|
+
|
|
752
|
+
start_row = sel[:start_row]
|
|
753
|
+
end_row = sel[:end_row]
|
|
754
|
+
reindent_range(ctx, start_row, end_row)
|
|
755
|
+
ctx.editor.enter_normal_mode
|
|
756
|
+
end
|
|
757
|
+
|
|
560
758
|
def visual_select_text_object(ctx, kwargs:, **)
|
|
561
759
|
motion = (kwargs[:motion] || kwargs["motion"]).to_s
|
|
562
760
|
span = text_object_span(ctx.buffer, ctx.window, motion)
|
|
@@ -577,14 +775,38 @@ module RuVim
|
|
|
577
775
|
end
|
|
578
776
|
|
|
579
777
|
def file_write(ctx, argv:, bang:, **)
|
|
778
|
+
if ctx.buffer.kind == :git_commit
|
|
779
|
+
git_commit_execute(ctx)
|
|
780
|
+
return
|
|
781
|
+
end
|
|
782
|
+
|
|
580
783
|
path = argv[0]
|
|
581
784
|
target = ctx.buffer.write_to(path)
|
|
582
785
|
size = File.exist?(target) ? File.size(target) : 0
|
|
583
786
|
suffix = bang ? " (force accepted)" : ""
|
|
584
787
|
ctx.editor.echo("\"#{target}\" #{ctx.buffer.line_count}L, #{size}B written#{suffix}")
|
|
788
|
+
if ctx.editor.get_option("onsavehook")
|
|
789
|
+
ctx.buffer.lang_module.on_save(ctx, target)
|
|
790
|
+
end
|
|
585
791
|
end
|
|
586
792
|
|
|
587
793
|
def app_quit(ctx, bang:, **)
|
|
794
|
+
if ctx.buffer.kind == :filter
|
|
795
|
+
saved_y = ctx.buffer.options["filter_source_cursor_y"]
|
|
796
|
+
saved_x = ctx.buffer.options["filter_source_cursor_x"]
|
|
797
|
+
saved_row_offset = ctx.buffer.options["filter_source_row_offset"]
|
|
798
|
+
saved_col_offset = ctx.buffer.options["filter_source_col_offset"]
|
|
799
|
+
ctx.editor.delete_buffer(ctx.buffer.id)
|
|
800
|
+
if saved_y
|
|
801
|
+
win = ctx.editor.current_window
|
|
802
|
+
win.cursor_y = saved_y
|
|
803
|
+
win.cursor_x = saved_x || 0
|
|
804
|
+
win.row_offset = saved_row_offset || 0
|
|
805
|
+
win.col_offset = saved_col_offset || 0
|
|
806
|
+
end
|
|
807
|
+
return
|
|
808
|
+
end
|
|
809
|
+
|
|
588
810
|
if ctx.editor.window_count > 1
|
|
589
811
|
ctx.editor.close_current_window
|
|
590
812
|
ctx.editor.echo("closed window")
|
|
@@ -605,6 +827,25 @@ module RuVim
|
|
|
605
827
|
ctx.editor.request_quit!
|
|
606
828
|
end
|
|
607
829
|
|
|
830
|
+
def app_quit_all(ctx, bang:, **)
|
|
831
|
+
unless bang
|
|
832
|
+
modified = ctx.editor.buffers.values.select { |b| b.file_buffer? && b.modified? }
|
|
833
|
+
unless modified.empty?
|
|
834
|
+
ctx.editor.echo_error("#{modified.size} buffer(s) have unsaved changes (add ! to override)")
|
|
835
|
+
return
|
|
836
|
+
end
|
|
837
|
+
end
|
|
838
|
+
ctx.editor.request_quit!
|
|
839
|
+
end
|
|
840
|
+
|
|
841
|
+
def file_write_quit_all(ctx, bang:, **)
|
|
842
|
+
ctx.editor.buffers.each_value do |buf|
|
|
843
|
+
next unless buf.file_buffer? && buf.modified? && buf.path
|
|
844
|
+
buf.write_to(buf.path)
|
|
845
|
+
end
|
|
846
|
+
app_quit_all(ctx, bang: true)
|
|
847
|
+
end
|
|
848
|
+
|
|
608
849
|
def file_write_quit(ctx, argv:, bang:, **)
|
|
609
850
|
file_write(ctx, argv:, bang:)
|
|
610
851
|
return unless ctx.editor.running?
|
|
@@ -639,9 +880,7 @@ module RuVim
|
|
|
639
880
|
end
|
|
640
881
|
end
|
|
641
882
|
|
|
642
|
-
|
|
643
|
-
ctx.editor.switch_to_buffer(new_buffer.id)
|
|
644
|
-
ctx.editor.echo(File.exist?(path) ? "\"#{path}\" #{new_buffer.line_count}L" : "\"#{path}\" [New File]")
|
|
883
|
+
ctx.editor.open_path(path)
|
|
645
884
|
end
|
|
646
885
|
|
|
647
886
|
def file_goto_under_cursor(ctx, **)
|
|
@@ -651,9 +890,10 @@ module RuVim
|
|
|
651
890
|
return
|
|
652
891
|
end
|
|
653
892
|
|
|
654
|
-
|
|
893
|
+
target = parse_gf_target(token)
|
|
894
|
+
path = resolve_gf_path(ctx, target[:path])
|
|
655
895
|
unless path
|
|
656
|
-
ctx.editor.echo_error("File not found: #{
|
|
896
|
+
ctx.editor.echo_error("File not found: #{target[:path]}")
|
|
657
897
|
return
|
|
658
898
|
end
|
|
659
899
|
|
|
@@ -665,6 +905,7 @@ module RuVim
|
|
|
665
905
|
end
|
|
666
906
|
|
|
667
907
|
ctx.editor.open_path(path)
|
|
908
|
+
move_cursor_to_gf_line(ctx, target[:line]) if target[:line]
|
|
668
909
|
end
|
|
669
910
|
|
|
670
911
|
def buffer_list(ctx, **)
|
|
@@ -672,23 +913,28 @@ module RuVim
|
|
|
672
913
|
alt_id = ctx.editor.alternate_buffer_id
|
|
673
914
|
items = ctx.editor.buffer_ids.map do |id|
|
|
674
915
|
b = ctx.editor.buffers.fetch(id)
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
916
|
+
indicator = id == current_id ? "%a" : " "
|
|
917
|
+
indicator = "# " if id == alt_id && id != current_id
|
|
918
|
+
mod = b.modified? ? "+" : " "
|
|
919
|
+
name = b.path ? "\"#{b.path}\"" : "[No Name]"
|
|
920
|
+
line_info = "line #{b.respond_to?(:cursor_line) ? b.cursor_line : 0}"
|
|
921
|
+
# Find the window showing this buffer to get cursor line
|
|
922
|
+
win = ctx.editor.windows.values.find { |w| w.buffer_id == id }
|
|
923
|
+
line_info = "line #{win ? win.cursor_y + 1 : 0}"
|
|
924
|
+
"%3d %s %s %-30s %s" % [id, indicator, mod, name, line_info]
|
|
681
925
|
end
|
|
682
|
-
ctx.editor.
|
|
926
|
+
ctx.editor.echo_multiline(items)
|
|
683
927
|
end
|
|
684
928
|
|
|
685
929
|
def buffer_next(ctx, count:, bang:, **)
|
|
930
|
+
count = normalized_count(count)
|
|
686
931
|
target = ctx.editor.current_buffer.id
|
|
687
932
|
count.times { target = ctx.editor.next_buffer_id_from(target, 1) }
|
|
688
933
|
switch_buffer_id(ctx, target, bang:)
|
|
689
934
|
end
|
|
690
935
|
|
|
691
936
|
def buffer_prev(ctx, count:, bang:, **)
|
|
937
|
+
count = normalized_count(count)
|
|
692
938
|
target = ctx.editor.current_buffer.id
|
|
693
939
|
count.times { target = ctx.editor.next_buffer_id_from(target, -1) }
|
|
694
940
|
switch_buffer_id(ctx, target, bang:)
|
|
@@ -768,7 +1014,7 @@ module RuVim
|
|
|
768
1014
|
when "config"
|
|
769
1015
|
"Config: XDG Ruby DSL at ~/.config/ruvim/init.rb and ftplugin/<filetype>.rb"
|
|
770
1016
|
when "bindings", "keys", "keymap"
|
|
771
|
-
"Bindings:
|
|
1017
|
+
"Bindings: use :bindings (current effective key bindings by layer). Docs: docs/binding.md"
|
|
772
1018
|
when "number", "relativenumber", "ignorecase", "smartcase", "hlsearch", "tabstop", "filetype"
|
|
773
1019
|
option_help_line(key)
|
|
774
1020
|
else
|
|
@@ -784,8 +1030,17 @@ module RuVim
|
|
|
784
1030
|
def ex_define_command(ctx, argv:, bang:, **)
|
|
785
1031
|
registry = RuVim::ExCommandRegistry.instance
|
|
786
1032
|
if argv.empty?
|
|
787
|
-
|
|
788
|
-
|
|
1033
|
+
user_cmds = registry.all.select { |spec| spec.source == :user }
|
|
1034
|
+
if user_cmds.empty?
|
|
1035
|
+
ctx.editor.echo("No user commands")
|
|
1036
|
+
else
|
|
1037
|
+
header = " Name Definition"
|
|
1038
|
+
items = [header] + user_cmds.map { |spec|
|
|
1039
|
+
body = spec.respond_to?(:body) ? spec.body.to_s : spec.name.to_s
|
|
1040
|
+
" %-12s%s" % [spec.name, body]
|
|
1041
|
+
}
|
|
1042
|
+
ctx.editor.echo_multiline(items)
|
|
1043
|
+
end
|
|
789
1044
|
return
|
|
790
1045
|
end
|
|
791
1046
|
|
|
@@ -912,14 +1167,33 @@ module RuVim
|
|
|
912
1167
|
end
|
|
913
1168
|
|
|
914
1169
|
def ex_commands(ctx, **)
|
|
915
|
-
|
|
1170
|
+
rows = RuVim::ExCommandRegistry.instance.all.map do |spec|
|
|
916
1171
|
alias_text = spec.aliases.empty? ? "" : " (#{spec.aliases.join(', ')})"
|
|
917
1172
|
source = spec.source == :user ? " [user]" : ""
|
|
918
|
-
"#{spec.name}#{alias_text}#{source}"
|
|
1173
|
+
name = "#{spec.name}#{alias_text}#{source}"
|
|
1174
|
+
desc = spec.desc.to_s
|
|
1175
|
+
keys = ex_command_binding_labels(ctx.editor, spec)
|
|
1176
|
+
[name, desc, keys]
|
|
1177
|
+
end
|
|
1178
|
+
name_width = rows.map { |name, _desc, _keys| name.length }.max || 0
|
|
1179
|
+
items = rows.map do |name, desc, keys|
|
|
1180
|
+
line = "#{name.ljust(name_width)} #{desc}"
|
|
1181
|
+
line += " keys: #{keys.join(', ')}" unless keys.empty?
|
|
1182
|
+
line
|
|
919
1183
|
end
|
|
920
1184
|
ctx.editor.show_help_buffer!(title: "[Commands]", lines: ["Ex commands", "", *items])
|
|
921
1185
|
end
|
|
922
1186
|
|
|
1187
|
+
def ex_bindings(ctx, argv: [], **)
|
|
1188
|
+
keymaps = ctx.editor.keymap_manager
|
|
1189
|
+
raise RuVim::CommandError, "Keymap manager is unavailable" unless keymaps
|
|
1190
|
+
|
|
1191
|
+
mode_filter, sort = parse_bindings_args(argv)
|
|
1192
|
+
entries = keymaps.binding_entries_for_context(ctx.editor, mode: mode_filter)
|
|
1193
|
+
lines = bindings_buffer_lines(ctx.editor, entries, mode_filter:, sort:)
|
|
1194
|
+
ctx.editor.show_help_buffer!(title: "[Bindings]", lines:)
|
|
1195
|
+
end
|
|
1196
|
+
|
|
923
1197
|
def ex_set(ctx, argv:, **)
|
|
924
1198
|
ex_set_common(ctx, argv, scope: :auto)
|
|
925
1199
|
end
|
|
@@ -942,6 +1216,7 @@ module RuVim
|
|
|
942
1216
|
end
|
|
943
1217
|
|
|
944
1218
|
ctx.editor.set_quickfix_list(items)
|
|
1219
|
+
ctx.editor.select_quickfix(0)
|
|
945
1220
|
ctx.editor.jump_to_location(ctx.editor.current_quickfix_item)
|
|
946
1221
|
ctx.editor.echo("quickfix: #{items.length} item(s)")
|
|
947
1222
|
end
|
|
@@ -956,6 +1231,7 @@ module RuVim
|
|
|
956
1231
|
end
|
|
957
1232
|
|
|
958
1233
|
ctx.editor.set_location_list(items, window_id: ctx.window.id)
|
|
1234
|
+
ctx.editor.select_location_list(0, window_id: ctx.window.id)
|
|
959
1235
|
ctx.editor.jump_to_location(ctx.editor.current_location_list_item(ctx.window.id))
|
|
960
1236
|
ctx.editor.echo("location list: #{items.length} item(s)")
|
|
961
1237
|
end
|
|
@@ -975,6 +1251,7 @@ module RuVim
|
|
|
975
1251
|
return
|
|
976
1252
|
end
|
|
977
1253
|
ctx.editor.jump_to_location(item)
|
|
1254
|
+
refresh_list_window(ctx.editor, :quickfix)
|
|
978
1255
|
ctx.editor.echo(quickfix_item_echo(ctx.editor))
|
|
979
1256
|
end
|
|
980
1257
|
|
|
@@ -985,6 +1262,7 @@ module RuVim
|
|
|
985
1262
|
return
|
|
986
1263
|
end
|
|
987
1264
|
ctx.editor.jump_to_location(item)
|
|
1265
|
+
refresh_list_window(ctx.editor, :quickfix)
|
|
988
1266
|
ctx.editor.echo(quickfix_item_echo(ctx.editor))
|
|
989
1267
|
end
|
|
990
1268
|
|
|
@@ -1004,6 +1282,7 @@ module RuVim
|
|
|
1004
1282
|
return
|
|
1005
1283
|
end
|
|
1006
1284
|
ctx.editor.jump_to_location(item)
|
|
1285
|
+
refresh_list_window(ctx.editor, :location_list)
|
|
1007
1286
|
ctx.editor.echo(location_item_echo(ctx.editor, ctx.window.id))
|
|
1008
1287
|
end
|
|
1009
1288
|
|
|
@@ -1014,37 +1293,152 @@ module RuVim
|
|
|
1014
1293
|
return
|
|
1015
1294
|
end
|
|
1016
1295
|
ctx.editor.jump_to_location(item)
|
|
1296
|
+
refresh_list_window(ctx.editor, :location_list)
|
|
1017
1297
|
ctx.editor.echo(location_item_echo(ctx.editor, ctx.window.id))
|
|
1018
1298
|
end
|
|
1019
1299
|
|
|
1020
|
-
def ex_substitute(ctx, pattern:, replacement:, global: false, **)
|
|
1300
|
+
def ex_substitute(ctx, pattern:, replacement:, flags_str: nil, range_start: nil, range_end: nil, global: false, **)
|
|
1021
1301
|
materialize_intro_buffer_if_needed(ctx)
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
end
|
|
1035
|
-
end
|
|
1302
|
+
flags = parse_substitute_flags(flags_str, default_global: global)
|
|
1303
|
+
raise RuVim::CommandError, "Confirm flag (:s///c) is not yet supported" if flags[:confirm]
|
|
1304
|
+
|
|
1305
|
+
regex = build_substitute_regex(pattern, flags, ctx)
|
|
1306
|
+
|
|
1307
|
+
r_start = range_start || 0
|
|
1308
|
+
r_end = range_end || (ctx.buffer.line_count - 1)
|
|
1309
|
+
|
|
1310
|
+
if flags[:count_only]
|
|
1311
|
+
total = count_matches_in_range(ctx.buffer, regex, r_start, r_end, flags[:global])
|
|
1312
|
+
ctx.editor.echo("#{total} match(es)")
|
|
1313
|
+
return
|
|
1036
1314
|
end
|
|
1037
1315
|
|
|
1316
|
+
changed = substitute_range(ctx, regex, replacement, r_start, r_end, flags)
|
|
1317
|
+
|
|
1038
1318
|
if changed.positive?
|
|
1039
|
-
ctx.buffer.begin_change_group
|
|
1040
|
-
ctx.buffer.replace_all_lines!(new_lines)
|
|
1041
|
-
ctx.buffer.end_change_group
|
|
1042
1319
|
ctx.editor.echo("#{changed} substitution(s)")
|
|
1320
|
+
elsif flags[:no_error]
|
|
1321
|
+
ctx.editor.echo("Pattern not found: #{pattern}")
|
|
1043
1322
|
else
|
|
1044
1323
|
ctx.editor.echo("Pattern not found: #{pattern}")
|
|
1045
1324
|
end
|
|
1046
1325
|
end
|
|
1047
1326
|
|
|
1327
|
+
def ex_grep(ctx, argv:, kwargs: {}, **)
|
|
1328
|
+
run_external_grep(ctx, argv:, target: :quickfix)
|
|
1329
|
+
end
|
|
1330
|
+
|
|
1331
|
+
def ex_lgrep(ctx, argv:, kwargs: {}, **)
|
|
1332
|
+
run_external_grep(ctx, argv:, target: :location_list)
|
|
1333
|
+
end
|
|
1334
|
+
|
|
1335
|
+
def ex_delete_lines(ctx, kwargs: {}, **)
|
|
1336
|
+
materialize_intro_buffer_if_needed(ctx)
|
|
1337
|
+
r_start = kwargs[:range_start]
|
|
1338
|
+
r_end = kwargs[:range_end]
|
|
1339
|
+
unless r_start && r_end
|
|
1340
|
+
# Default to current line
|
|
1341
|
+
r_start = r_end = ctx.window.cursor_y
|
|
1342
|
+
end
|
|
1343
|
+
|
|
1344
|
+
count = r_end - r_start + 1
|
|
1345
|
+
deleted_text = ctx.buffer.line_block_text(r_start, count)
|
|
1346
|
+
ctx.buffer.begin_change_group
|
|
1347
|
+
count.times { ctx.buffer.delete_line(r_start) }
|
|
1348
|
+
ctx.buffer.end_change_group
|
|
1349
|
+
store_delete_register(ctx, text: deleted_text, type: :linewise)
|
|
1350
|
+
ctx.window.cursor_y = [r_start, ctx.buffer.line_count - 1].min
|
|
1351
|
+
ctx.window.cursor_x = 0
|
|
1352
|
+
ctx.window.clamp_to_buffer(ctx.buffer)
|
|
1353
|
+
ctx.editor.echo("#{count} line(s) deleted")
|
|
1354
|
+
end
|
|
1355
|
+
|
|
1356
|
+
def ex_yank_lines(ctx, kwargs: {}, **)
|
|
1357
|
+
materialize_intro_buffer_if_needed(ctx)
|
|
1358
|
+
r_start = kwargs[:range_start]
|
|
1359
|
+
r_end = kwargs[:range_end]
|
|
1360
|
+
unless r_start && r_end
|
|
1361
|
+
r_start = r_end = ctx.window.cursor_y
|
|
1362
|
+
end
|
|
1363
|
+
|
|
1364
|
+
count = r_end - r_start + 1
|
|
1365
|
+
text = ctx.buffer.line_block_text(r_start, count)
|
|
1366
|
+
store_yank_register(ctx, text:, type: :linewise)
|
|
1367
|
+
ctx.editor.echo("#{count} line(s) yanked")
|
|
1368
|
+
end
|
|
1369
|
+
|
|
1370
|
+
def ex_rich(ctx, argv: [], **)
|
|
1371
|
+
format = argv.first
|
|
1372
|
+
RuVim::RichView.toggle!(ctx.editor, format: format)
|
|
1373
|
+
end
|
|
1374
|
+
|
|
1375
|
+
def rich_toggle(ctx, **)
|
|
1376
|
+
RuVim::RichView.toggle!(ctx.editor)
|
|
1377
|
+
end
|
|
1378
|
+
|
|
1379
|
+
def rich_view_close_buffer(ctx, **)
|
|
1380
|
+
ctx.editor.delete_buffer(ctx.buffer.id)
|
|
1381
|
+
end
|
|
1382
|
+
|
|
1383
|
+
def search_filter(ctx, **)
|
|
1384
|
+
editor = ctx.editor
|
|
1385
|
+
search = editor.last_search
|
|
1386
|
+
unless search
|
|
1387
|
+
editor.echo_error("No search pattern")
|
|
1388
|
+
return
|
|
1389
|
+
end
|
|
1390
|
+
|
|
1391
|
+
regex = compile_search_regex(search[:pattern], editor: editor, window: ctx.window, buffer: ctx.buffer)
|
|
1392
|
+
source_buffer = ctx.buffer
|
|
1393
|
+
|
|
1394
|
+
# Collect matching lines with origin mapping
|
|
1395
|
+
origins = []
|
|
1396
|
+
matching_lines = []
|
|
1397
|
+
source_buffer.lines.each_with_index do |line, row|
|
|
1398
|
+
if regex.match?(line)
|
|
1399
|
+
# If source is a filter buffer, chain back to the original
|
|
1400
|
+
if source_buffer.kind == :filter && source_buffer.options["filter_origins"]
|
|
1401
|
+
origins << source_buffer.options["filter_origins"][row]
|
|
1402
|
+
else
|
|
1403
|
+
origins << { buffer_id: source_buffer.id, row: row }
|
|
1404
|
+
end
|
|
1405
|
+
matching_lines << line
|
|
1406
|
+
end
|
|
1407
|
+
end
|
|
1408
|
+
|
|
1409
|
+
if matching_lines.empty?
|
|
1410
|
+
editor.echo_error("Pattern not found: #{search[:pattern]}")
|
|
1411
|
+
return
|
|
1412
|
+
end
|
|
1413
|
+
|
|
1414
|
+
filetype = source_buffer.options["filetype"]
|
|
1415
|
+
filter_buf = editor.add_virtual_buffer(
|
|
1416
|
+
kind: :filter,
|
|
1417
|
+
name: "[Filter: /#{search[:pattern]}/]",
|
|
1418
|
+
lines: matching_lines,
|
|
1419
|
+
filetype: filetype,
|
|
1420
|
+
readonly: false,
|
|
1421
|
+
modifiable: false
|
|
1422
|
+
)
|
|
1423
|
+
filter_buf.options["filter_origins"] = origins
|
|
1424
|
+
filter_buf.options["filter_source_buffer_id"] = source_buffer.id
|
|
1425
|
+
filter_buf.options["filter_source_cursor_y"] = ctx.window.cursor_y
|
|
1426
|
+
filter_buf.options["filter_source_cursor_x"] = ctx.window.cursor_x
|
|
1427
|
+
filter_buf.options["filter_source_row_offset"] = ctx.window.row_offset
|
|
1428
|
+
filter_buf.options["filter_source_col_offset"] = ctx.window.col_offset
|
|
1429
|
+
editor.switch_to_buffer(filter_buf.id)
|
|
1430
|
+
editor.echo("filter: #{matching_lines.length} line(s)")
|
|
1431
|
+
end
|
|
1432
|
+
|
|
1433
|
+
def ex_filter(ctx, argv:, **)
|
|
1434
|
+
if argv.any?
|
|
1435
|
+
pattern = parse_vimgrep_pattern(argv.join(" "))
|
|
1436
|
+
editor = ctx.editor
|
|
1437
|
+
editor.set_last_search(pattern: pattern, direction: :forward)
|
|
1438
|
+
end
|
|
1439
|
+
search_filter(ctx)
|
|
1440
|
+
end
|
|
1441
|
+
|
|
1048
1442
|
def submit_search(ctx, pattern:, direction:)
|
|
1049
1443
|
text = pattern.to_s
|
|
1050
1444
|
if text.empty?
|
|
@@ -1058,8 +1452,39 @@ module RuVim
|
|
|
1058
1452
|
move_to_search(ctx, pattern: text, direction:, count: 1)
|
|
1059
1453
|
end
|
|
1060
1454
|
|
|
1455
|
+
include RuVim::Git::Handler
|
|
1456
|
+
|
|
1061
1457
|
private
|
|
1062
1458
|
|
|
1459
|
+
def reindent_range(ctx, start_row, end_row)
|
|
1460
|
+
buf = ctx.buffer
|
|
1461
|
+
lang_mod = buf.lang_module
|
|
1462
|
+
|
|
1463
|
+
sw = ctx.editor.effective_option("shiftwidth", buffer: buf).to_i
|
|
1464
|
+
sw = 2 if sw <= 0
|
|
1465
|
+
|
|
1466
|
+
buf.begin_change_group
|
|
1467
|
+
(start_row..end_row).each do |row|
|
|
1468
|
+
target_indent = lang_mod.calculate_indent(buf.lines, row, sw)
|
|
1469
|
+
next unless target_indent
|
|
1470
|
+
|
|
1471
|
+
line = buf.line_at(row)
|
|
1472
|
+
current_indent = line[/\A */].to_s.length
|
|
1473
|
+
next if current_indent == target_indent
|
|
1474
|
+
|
|
1475
|
+
buf.delete_span(row, 0, row, current_indent) if current_indent > 0
|
|
1476
|
+
buf.insert_text(row, 0, " " * target_indent) if target_indent > 0
|
|
1477
|
+
end
|
|
1478
|
+
buf.end_change_group
|
|
1479
|
+
|
|
1480
|
+
ctx.window.cursor_y = start_row
|
|
1481
|
+
line = buf.line_at(start_row)
|
|
1482
|
+
ctx.window.cursor_x = (line[/\A */]&.length || 0)
|
|
1483
|
+
ctx.window.clamp_to_buffer(buf)
|
|
1484
|
+
count = end_row - start_row + 1
|
|
1485
|
+
ctx.editor.echo("#{count} line#{"s" if count > 1} indented")
|
|
1486
|
+
end
|
|
1487
|
+
|
|
1063
1488
|
def parse_vimgrep_pattern(argv)
|
|
1064
1489
|
raw = Array(argv).join(" ").strip
|
|
1065
1490
|
raise RuVim::CommandError, "Usage: :vimgrep /pattern/" if raw.empty?
|
|
@@ -1071,6 +1496,70 @@ module RuVim
|
|
|
1071
1496
|
end
|
|
1072
1497
|
end
|
|
1073
1498
|
|
|
1499
|
+
def run_external_grep(ctx, argv:, target:)
|
|
1500
|
+
args = Array(argv).join(" ").strip
|
|
1501
|
+
raise RuVim::CommandError, "Usage: :grep pattern [files...]" if args.empty?
|
|
1502
|
+
|
|
1503
|
+
grepprg = ctx.editor.effective_option("grepprg", window: ctx.window, buffer: ctx.buffer) || "grep -n"
|
|
1504
|
+
cmd = "#{grepprg} #{args}"
|
|
1505
|
+
|
|
1506
|
+
stdout, stderr, status = Open3.capture3(cmd)
|
|
1507
|
+
if stdout.strip.empty? && !status.success?
|
|
1508
|
+
msg = stderr.strip.empty? ? "No matches found" : stderr.strip
|
|
1509
|
+
ctx.editor.echo_error(msg)
|
|
1510
|
+
return
|
|
1511
|
+
end
|
|
1512
|
+
|
|
1513
|
+
items = parse_grep_output(ctx, stdout)
|
|
1514
|
+
if items.empty?
|
|
1515
|
+
ctx.editor.echo_error("No matches found")
|
|
1516
|
+
return
|
|
1517
|
+
end
|
|
1518
|
+
|
|
1519
|
+
case target
|
|
1520
|
+
when :quickfix
|
|
1521
|
+
ctx.editor.set_quickfix_list(items)
|
|
1522
|
+
ctx.editor.select_quickfix(0)
|
|
1523
|
+
ctx.editor.jump_to_location(ctx.editor.current_quickfix_item)
|
|
1524
|
+
ctx.editor.echo("quickfix: #{items.length} item(s)")
|
|
1525
|
+
when :location_list
|
|
1526
|
+
ctx.editor.set_location_list(items, window_id: ctx.window.id)
|
|
1527
|
+
ctx.editor.select_location_list(0, window_id: ctx.window.id)
|
|
1528
|
+
ctx.editor.jump_to_location(ctx.editor.current_location_list_item(ctx.window.id))
|
|
1529
|
+
ctx.editor.echo("location list: #{items.length} item(s)")
|
|
1530
|
+
end
|
|
1531
|
+
end
|
|
1532
|
+
|
|
1533
|
+
def parse_grep_output(ctx, output)
|
|
1534
|
+
items = []
|
|
1535
|
+
output.each_line do |line|
|
|
1536
|
+
line = line.chomp
|
|
1537
|
+
# Parse filename:lineno:text format
|
|
1538
|
+
if (m = line.match(/\A(.+?):(\d+):(.*)?\z/))
|
|
1539
|
+
filepath = m[1]
|
|
1540
|
+
lineno = m[2].to_i - 1 # 0-based
|
|
1541
|
+
text = m[3].to_s
|
|
1542
|
+
buf = ensure_buffer_for_grep_file(ctx, filepath)
|
|
1543
|
+
items << { buffer_id: buf.id, row: lineno, col: 0, text: text }
|
|
1544
|
+
end
|
|
1545
|
+
end
|
|
1546
|
+
items
|
|
1547
|
+
end
|
|
1548
|
+
|
|
1549
|
+
def ensure_buffer_for_grep_file(ctx, filepath)
|
|
1550
|
+
abspath = File.expand_path(filepath)
|
|
1551
|
+
# Check if buffer already exists for this file
|
|
1552
|
+
existing = ctx.editor.buffers.values.find { |b| b.path && File.expand_path(b.path) == abspath }
|
|
1553
|
+
return existing if existing
|
|
1554
|
+
|
|
1555
|
+
# Create buffer for the file
|
|
1556
|
+
if File.exist?(abspath)
|
|
1557
|
+
ctx.editor.add_buffer_from_file(abspath)
|
|
1558
|
+
else
|
|
1559
|
+
ctx.editor.add_empty_buffer(path: abspath)
|
|
1560
|
+
end
|
|
1561
|
+
end
|
|
1562
|
+
|
|
1074
1563
|
def grep_items_for_buffers(buffers, regex)
|
|
1075
1564
|
Array(buffers).flat_map do |buffer|
|
|
1076
1565
|
buffer.lines.each_with_index.flat_map do |line, row|
|
|
@@ -1136,6 +1625,22 @@ module RuVim
|
|
|
1136
1625
|
editor.echo("#{kind} closed")
|
|
1137
1626
|
end
|
|
1138
1627
|
|
|
1628
|
+
def refresh_list_window(editor, kind)
|
|
1629
|
+
wids = editor.find_window_ids_by_buffer_kind(kind)
|
|
1630
|
+
return if wids.empty?
|
|
1631
|
+
|
|
1632
|
+
lines = case kind
|
|
1633
|
+
when :quickfix then quickfix_buffer_lines(editor)
|
|
1634
|
+
when :location_list then location_list_buffer_lines(editor, editor.current_window_id)
|
|
1635
|
+
end
|
|
1636
|
+
wids.each do |wid|
|
|
1637
|
+
buf = editor.buffers[editor.windows[wid].buffer_id]
|
|
1638
|
+
next unless buf
|
|
1639
|
+
# Bypass modifiable check — this is an internal refresh of a readonly list buffer
|
|
1640
|
+
buf.instance_variable_set(:@lines, Array(lines).map(&:dup))
|
|
1641
|
+
end
|
|
1642
|
+
end
|
|
1643
|
+
|
|
1139
1644
|
def quickfix_item_echo(editor)
|
|
1140
1645
|
item = editor.current_quickfix_item
|
|
1141
1646
|
list_item_echo(editor, item, editor.quickfix_index, editor.quickfix_items.length, label: "qf")
|
|
@@ -1323,6 +1828,48 @@ module RuVim
|
|
|
1323
1828
|
true
|
|
1324
1829
|
end
|
|
1325
1830
|
|
|
1831
|
+
def delete_lines_to_end(ctx)
|
|
1832
|
+
y = ctx.window.cursor_y
|
|
1833
|
+
total = ctx.buffer.lines.length - y
|
|
1834
|
+
deleted = ctx.buffer.line_block_text(y, total)
|
|
1835
|
+
ctx.buffer.begin_change_group
|
|
1836
|
+
total.times { ctx.buffer.delete_line(y) }
|
|
1837
|
+
ctx.buffer.end_change_group
|
|
1838
|
+
store_delete_register(ctx, text: deleted, type: :linewise)
|
|
1839
|
+
ctx.window.clamp_to_buffer(ctx.buffer)
|
|
1840
|
+
:linewise
|
|
1841
|
+
end
|
|
1842
|
+
|
|
1843
|
+
def delete_lines_to_start(ctx)
|
|
1844
|
+
y = ctx.window.cursor_y
|
|
1845
|
+
total = y + 1
|
|
1846
|
+
deleted = ctx.buffer.line_block_text(0, total)
|
|
1847
|
+
ctx.buffer.begin_change_group
|
|
1848
|
+
total.times { ctx.buffer.delete_line(0) }
|
|
1849
|
+
ctx.buffer.end_change_group
|
|
1850
|
+
store_delete_register(ctx, text: deleted, type: :linewise)
|
|
1851
|
+
ctx.window.cursor_y = 0
|
|
1852
|
+
ctx.window.cursor_x = 0
|
|
1853
|
+
ctx.window.clamp_to_buffer(ctx.buffer)
|
|
1854
|
+
:linewise
|
|
1855
|
+
end
|
|
1856
|
+
|
|
1857
|
+
def yank_lines_to_end(ctx)
|
|
1858
|
+
y = ctx.window.cursor_y
|
|
1859
|
+
total = ctx.buffer.lines.length - y
|
|
1860
|
+
text = ctx.buffer.line_block_text(y, total)
|
|
1861
|
+
store_yank_register(ctx, text: text, type: :linewise)
|
|
1862
|
+
ctx.editor.echo("#{total} line(s) yanked")
|
|
1863
|
+
end
|
|
1864
|
+
|
|
1865
|
+
def yank_lines_to_start(ctx)
|
|
1866
|
+
y = ctx.window.cursor_y
|
|
1867
|
+
total = y + 1
|
|
1868
|
+
text = ctx.buffer.line_block_text(0, total)
|
|
1869
|
+
store_yank_register(ctx, text: text, type: :linewise)
|
|
1870
|
+
ctx.editor.echo("#{total} line(s) yanked")
|
|
1871
|
+
end
|
|
1872
|
+
|
|
1326
1873
|
def delete_to_end_of_line(ctx)
|
|
1327
1874
|
y = ctx.window.cursor_y
|
|
1328
1875
|
x = ctx.window.cursor_x
|
|
@@ -1354,22 +1901,22 @@ module RuVim
|
|
|
1354
1901
|
end
|
|
1355
1902
|
|
|
1356
1903
|
def delete_text_object_word(ctx, around:)
|
|
1357
|
-
|
|
1358
|
-
return false unless span
|
|
1359
|
-
|
|
1360
|
-
text = ctx.buffer.span_text(span[:start_row], span[:start_col], span[:end_row], span[:end_col])
|
|
1361
|
-
ctx.buffer.begin_change_group
|
|
1362
|
-
ctx.buffer.delete_span(span[:start_row], span[:start_col], span[:end_row], span[:end_col])
|
|
1363
|
-
ctx.buffer.end_change_group
|
|
1364
|
-
store_delete_register(ctx, text:, type: :charwise) unless text.empty?
|
|
1365
|
-
ctx.window.cursor_y = span[:start_row]
|
|
1366
|
-
ctx.window.cursor_x = span[:start_col]
|
|
1367
|
-
ctx.window.clamp_to_buffer(ctx.buffer)
|
|
1368
|
-
true
|
|
1904
|
+
delete_span(ctx, word_object_span(ctx.buffer, ctx.window, around:))
|
|
1369
1905
|
end
|
|
1370
1906
|
|
|
1371
1907
|
def delete_text_object(ctx, motion)
|
|
1372
|
-
|
|
1908
|
+
delete_span(ctx, text_object_span(ctx.buffer, ctx.window, motion))
|
|
1909
|
+
end
|
|
1910
|
+
|
|
1911
|
+
def yank_text_object_word(ctx, around:)
|
|
1912
|
+
yank_span(ctx, word_object_span(ctx.buffer, ctx.window, around:))
|
|
1913
|
+
end
|
|
1914
|
+
|
|
1915
|
+
def yank_text_object(ctx, motion)
|
|
1916
|
+
yank_span(ctx, text_object_span(ctx.buffer, ctx.window, motion))
|
|
1917
|
+
end
|
|
1918
|
+
|
|
1919
|
+
def delete_span(ctx, span)
|
|
1373
1920
|
return false unless span
|
|
1374
1921
|
|
|
1375
1922
|
text = ctx.buffer.span_text(span[:start_row], span[:start_col], span[:end_row], span[:end_col])
|
|
@@ -1383,18 +1930,7 @@ module RuVim
|
|
|
1383
1930
|
true
|
|
1384
1931
|
end
|
|
1385
1932
|
|
|
1386
|
-
def
|
|
1387
|
-
span = word_object_span(ctx.buffer, ctx.window, around:)
|
|
1388
|
-
return false unless span
|
|
1389
|
-
|
|
1390
|
-
text = ctx.buffer.span_text(span[:start_row], span[:start_col], span[:end_row], span[:end_col])
|
|
1391
|
-
store_yank_register(ctx, text:, type: :charwise) unless text.empty?
|
|
1392
|
-
ctx.editor.echo("yanked")
|
|
1393
|
-
true
|
|
1394
|
-
end
|
|
1395
|
-
|
|
1396
|
-
def yank_text_object(ctx, motion)
|
|
1397
|
-
span = text_object_span(ctx.buffer, ctx.window, motion)
|
|
1933
|
+
def yank_span(ctx, span)
|
|
1398
1934
|
return false unless span
|
|
1399
1935
|
|
|
1400
1936
|
text = ctx.buffer.span_text(span[:start_row], span[:start_col], span[:end_row], span[:end_col])
|
|
@@ -1408,6 +1944,7 @@ module RuVim
|
|
|
1408
1944
|
flat = cursor_to_offset(buffer, row, col)
|
|
1409
1945
|
idx = flat
|
|
1410
1946
|
keyword_rx = keyword_char_regex(editor, buffer, window)
|
|
1947
|
+
count = normalized_count(count)
|
|
1411
1948
|
count.times do
|
|
1412
1949
|
idx = next_word_start_offset(text, idx, keyword_rx)
|
|
1413
1950
|
return nil unless idx
|
|
@@ -1419,7 +1956,7 @@ module RuVim
|
|
|
1419
1956
|
buffer = ctx.buffer
|
|
1420
1957
|
row = ctx.window.cursor_y
|
|
1421
1958
|
col = ctx.window.cursor_x
|
|
1422
|
-
count =
|
|
1959
|
+
count = normalized_count(count)
|
|
1423
1960
|
target = { row:, col: }
|
|
1424
1961
|
count.times do
|
|
1425
1962
|
target =
|
|
@@ -1460,6 +1997,14 @@ module RuVim
|
|
|
1460
1997
|
idx = cursor_to_offset(buffer, row, col)
|
|
1461
1998
|
n = text.length
|
|
1462
1999
|
keyword_rx = keyword_char_regex(editor, buffer, window)
|
|
2000
|
+
|
|
2001
|
+
# Vim-like `e`: if already on the end of a word, move to the next word's end.
|
|
2002
|
+
if idx < n
|
|
2003
|
+
cur_cls = char_class(text[idx], keyword_rx)
|
|
2004
|
+
next_cls = (idx + 1 < n) ? char_class(text[idx + 1], keyword_rx) : nil
|
|
2005
|
+
idx += 1 if cur_cls != :space && next_cls != cur_cls
|
|
2006
|
+
end
|
|
2007
|
+
|
|
1463
2008
|
while idx < n && char_class(text[idx], keyword_rx) == :space
|
|
1464
2009
|
idx += 1
|
|
1465
2010
|
end
|
|
@@ -1570,9 +2115,9 @@ module RuVim
|
|
|
1570
2115
|
x = [window.cursor_x, line.length - 1].min
|
|
1571
2116
|
return nil if x.negative?
|
|
1572
2117
|
|
|
1573
|
-
left =
|
|
2118
|
+
left = find_quote(line, x, quote, :left)
|
|
1574
2119
|
right_from = [x, (left ? left + 1 : 0)].max
|
|
1575
|
-
right =
|
|
2120
|
+
right = find_quote(line, right_from, quote, :right)
|
|
1576
2121
|
return nil unless left && right && left < right
|
|
1577
2122
|
|
|
1578
2123
|
if around
|
|
@@ -1644,20 +2189,18 @@ module RuVim
|
|
|
1644
2189
|
end
|
|
1645
2190
|
end
|
|
1646
2191
|
|
|
1647
|
-
def
|
|
2192
|
+
def find_quote(line, x, quote, direction)
|
|
1648
2193
|
i = x
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
return i if line[i] == quote && !escaped?(line, i)
|
|
1660
|
-
i += 1
|
|
2194
|
+
if direction == :left
|
|
2195
|
+
while i >= 0
|
|
2196
|
+
return i if line[i] == quote && !escaped?(line, i)
|
|
2197
|
+
i -= 1
|
|
2198
|
+
end
|
|
2199
|
+
else
|
|
2200
|
+
while i < line.length
|
|
2201
|
+
return i if line[i] == quote && !escaped?(line, i)
|
|
2202
|
+
i += 1
|
|
2203
|
+
end
|
|
1661
2204
|
end
|
|
1662
2205
|
nil
|
|
1663
2206
|
end
|
|
@@ -1846,6 +2389,30 @@ module RuVim
|
|
|
1846
2389
|
ctx.window.clamp_to_buffer(ctx.buffer)
|
|
1847
2390
|
end
|
|
1848
2391
|
|
|
2392
|
+
def place_cursor_line_in_window(ctx, where:, count:)
|
|
2393
|
+
if count
|
|
2394
|
+
target_row = [[normalized_count(count) - 1, 0].max, ctx.buffer.line_count - 1].min
|
|
2395
|
+
ctx.window.cursor_y = target_row
|
|
2396
|
+
ctx.window.clamp_to_buffer(ctx.buffer)
|
|
2397
|
+
end
|
|
2398
|
+
|
|
2399
|
+
height = current_view_height(ctx)
|
|
2400
|
+
max_row_offset = [ctx.buffer.line_count - height, 0].max
|
|
2401
|
+
desired =
|
|
2402
|
+
case where
|
|
2403
|
+
when :top
|
|
2404
|
+
ctx.window.cursor_y
|
|
2405
|
+
when :center
|
|
2406
|
+
ctx.window.cursor_y - (height / 2)
|
|
2407
|
+
when :bottom
|
|
2408
|
+
ctx.window.cursor_y - height + 1
|
|
2409
|
+
else
|
|
2410
|
+
ctx.window.row_offset
|
|
2411
|
+
end
|
|
2412
|
+
ctx.window.row_offset = [[desired, 0].max, max_row_offset].min
|
|
2413
|
+
ctx.window.clamp_to_buffer(ctx.buffer)
|
|
2414
|
+
end
|
|
2415
|
+
|
|
1849
2416
|
def cursor_to_offset(buffer, row, col)
|
|
1850
2417
|
offset = 0
|
|
1851
2418
|
row.times { |r| offset += buffer.line_length(r) + 1 }
|
|
@@ -1881,6 +2448,7 @@ module RuVim
|
|
|
1881
2448
|
lines = text.sub(/\n\z/, "").split("\n", -1)
|
|
1882
2449
|
return if lines.empty?
|
|
1883
2450
|
|
|
2451
|
+
count = normalized_count(count)
|
|
1884
2452
|
insert_at = before ? ctx.window.cursor_y : (ctx.window.cursor_y + 1)
|
|
1885
2453
|
ctx.buffer.begin_change_group
|
|
1886
2454
|
count.times { ctx.buffer.insert_lines_at(insert_at, lines) }
|
|
@@ -1956,6 +2524,16 @@ module RuVim
|
|
|
1956
2524
|
nil
|
|
1957
2525
|
end
|
|
1958
2526
|
|
|
2527
|
+
def normalized_count(count, default: 1)
|
|
2528
|
+
n = count.nil? ? default : count.to_i
|
|
2529
|
+
n = default if n <= 0
|
|
2530
|
+
n
|
|
2531
|
+
end
|
|
2532
|
+
|
|
2533
|
+
def ensure_modifiable_for_insert!(ctx)
|
|
2534
|
+
raise RuVim::CommandError, "Buffer is not modifiable" unless ctx.buffer.modifiable?
|
|
2535
|
+
end
|
|
2536
|
+
|
|
1959
2537
|
def maybe_autowrite_before_switch(ctx)
|
|
1960
2538
|
return false unless ctx.editor.effective_option("autowrite", window: ctx.window, buffer: ctx.buffer)
|
|
1961
2539
|
return false unless ctx.buffer.file_buffer?
|
|
@@ -1972,7 +2550,7 @@ module RuVim
|
|
|
1972
2550
|
return nil if line.empty?
|
|
1973
2551
|
|
|
1974
2552
|
x = [[window.cursor_x, 0].max, [line.length - 1, 0].max].min
|
|
1975
|
-
file_char = /[[:alnum:]_
|
|
2553
|
+
file_char = /[[:alnum:]_\.\/~:-]/
|
|
1976
2554
|
if line[x] !~ file_char
|
|
1977
2555
|
left = x - 1
|
|
1978
2556
|
right = x + 1
|
|
@@ -1992,6 +2570,25 @@ module RuVim
|
|
|
1992
2570
|
line[s...e]
|
|
1993
2571
|
end
|
|
1994
2572
|
|
|
2573
|
+
def parse_gf_target(token)
|
|
2574
|
+
raw = token.to_s
|
|
2575
|
+
if (m = /\A(.+):(\d+)\z/.match(raw))
|
|
2576
|
+
return { path: m[1], line: m[2].to_i } unless m[1].end_with?(":")
|
|
2577
|
+
end
|
|
2578
|
+
{ path: raw, line: nil }
|
|
2579
|
+
end
|
|
2580
|
+
|
|
2581
|
+
def move_cursor_to_gf_line(ctx, line_no)
|
|
2582
|
+
line = line_no.to_i
|
|
2583
|
+
return if line <= 0
|
|
2584
|
+
|
|
2585
|
+
w = ctx.editor.current_window
|
|
2586
|
+
b = ctx.editor.current_buffer
|
|
2587
|
+
w.cursor_y = [line - 1, b.line_count - 1].min
|
|
2588
|
+
w.cursor_x = 0
|
|
2589
|
+
w.clamp_to_buffer(b)
|
|
2590
|
+
end
|
|
2591
|
+
|
|
1995
2592
|
def resolve_gf_path(ctx, token)
|
|
1996
2593
|
candidates = gf_candidate_paths(ctx, token.to_s)
|
|
1997
2594
|
candidates.find { |p| File.file?(p) || File.directory?(p) }
|
|
@@ -2063,7 +2660,7 @@ module RuVim
|
|
|
2063
2660
|
items = editor.option_snapshot(window: ctx.window, buffer: ctx.buffer).map do |opt|
|
|
2064
2661
|
format_option_value(opt[:name], opt[:effective])
|
|
2065
2662
|
end
|
|
2066
|
-
ctx.editor.
|
|
2663
|
+
ctx.editor.echo_multiline(items)
|
|
2067
2664
|
return
|
|
2068
2665
|
end
|
|
2069
2666
|
|
|
@@ -2211,7 +2808,235 @@ module RuVim
|
|
|
2211
2808
|
]
|
|
2212
2809
|
end
|
|
2213
2810
|
|
|
2811
|
+
def parse_bindings_args(argv)
|
|
2812
|
+
mode_filter = nil
|
|
2813
|
+
sort = "key"
|
|
2814
|
+
|
|
2815
|
+
Array(argv).each do |raw|
|
|
2816
|
+
token = raw.to_s.strip
|
|
2817
|
+
next if token.empty?
|
|
2818
|
+
|
|
2819
|
+
if token.include?("=")
|
|
2820
|
+
key, value = token.split("=", 2).map(&:strip)
|
|
2821
|
+
case key.downcase
|
|
2822
|
+
when "sort"
|
|
2823
|
+
sort = parse_bindings_sort(value)
|
|
2824
|
+
else
|
|
2825
|
+
raise RuVim::CommandError, "Unknown option for :bindings: #{key}"
|
|
2826
|
+
end
|
|
2827
|
+
next
|
|
2828
|
+
end
|
|
2829
|
+
|
|
2830
|
+
raise RuVim::CommandError, "Too many positional args for :bindings" if mode_filter
|
|
2831
|
+
|
|
2832
|
+
mode_filter = parse_bindings_mode_filter(token)
|
|
2833
|
+
end
|
|
2834
|
+
|
|
2835
|
+
[mode_filter, sort]
|
|
2836
|
+
end
|
|
2837
|
+
|
|
2838
|
+
def parse_bindings_sort(raw)
|
|
2839
|
+
token = raw.to_s.strip.downcase
|
|
2840
|
+
case token
|
|
2841
|
+
when "", "key", "keys" then "key"
|
|
2842
|
+
when "command", "cmd" then "command"
|
|
2843
|
+
else
|
|
2844
|
+
raise RuVim::CommandError, "Unknown sort for :bindings: #{raw}"
|
|
2845
|
+
end
|
|
2846
|
+
end
|
|
2847
|
+
|
|
2848
|
+
def parse_bindings_mode_filter(raw)
|
|
2849
|
+
return nil if raw.nil? || raw.to_s.strip.empty?
|
|
2850
|
+
|
|
2851
|
+
token = raw.to_s.strip.downcase
|
|
2852
|
+
case token
|
|
2853
|
+
when "n", "normal" then :normal
|
|
2854
|
+
when "i", "insert" then :insert
|
|
2855
|
+
when "v", "visual", "visual_char" then :visual_char
|
|
2856
|
+
when "vl", "visual_line" then :visual_line
|
|
2857
|
+
when "vb", "visual_block", "x" then :visual_block
|
|
2858
|
+
when "o", "operator", "operator_pending" then :operator_pending
|
|
2859
|
+
when "c", "cmdline", "command", "command_line" then :command_line
|
|
2860
|
+
else
|
|
2861
|
+
raise RuVim::CommandError, "Unknown mode for :bindings: #{raw}"
|
|
2862
|
+
end
|
|
2863
|
+
end
|
|
2864
|
+
|
|
2865
|
+
def bindings_buffer_lines(editor, entries, mode_filter:, sort:)
|
|
2866
|
+
buffer = editor.current_buffer
|
|
2867
|
+
filetype = buffer.options["filetype"].to_s
|
|
2868
|
+
filetype = nil if filetype.empty?
|
|
2869
|
+
|
|
2870
|
+
lines = [
|
|
2871
|
+
"Bindings",
|
|
2872
|
+
"",
|
|
2873
|
+
"Buffer: #{buffer.display_name}",
|
|
2874
|
+
"Filetype: #{filetype || '-'}",
|
|
2875
|
+
"Mode filter: #{mode_filter || 'all'}",
|
|
2876
|
+
"Sort: #{sort}",
|
|
2877
|
+
""
|
|
2878
|
+
]
|
|
2879
|
+
|
|
2880
|
+
any = false
|
|
2881
|
+
%i[buffer filetype app].each do |layer|
|
|
2882
|
+
layer_entries = entries.select { |e| e.layer == layer }
|
|
2883
|
+
next if layer_entries.empty?
|
|
2884
|
+
|
|
2885
|
+
any = true
|
|
2886
|
+
lines << "Layer: #{layer}"
|
|
2887
|
+
append_binding_entries_grouped!(lines, layer_entries, layer:, sort:)
|
|
2888
|
+
lines << ""
|
|
2889
|
+
end
|
|
2890
|
+
|
|
2891
|
+
lines << "(no bindings)" unless any
|
|
2892
|
+
lines
|
|
2893
|
+
end
|
|
2894
|
+
|
|
2895
|
+
def append_binding_entries_grouped!(lines, entries, layer:, sort:)
|
|
2896
|
+
groups = entries.group_by do |e|
|
|
2897
|
+
if layer == :app && e.scope == :global
|
|
2898
|
+
[:global, nil]
|
|
2899
|
+
elsif e.mode
|
|
2900
|
+
[:mode, e.mode]
|
|
2901
|
+
else
|
|
2902
|
+
[:plain, nil]
|
|
2903
|
+
end
|
|
2904
|
+
end
|
|
2905
|
+
|
|
2906
|
+
groups.keys.sort_by { |kind, mode| binding_group_sort_key(kind, mode) }.each do |kind, mode|
|
|
2907
|
+
group_entries = groups[[kind, mode]]
|
|
2908
|
+
next if group_entries.nil? || group_entries.empty?
|
|
2909
|
+
|
|
2910
|
+
if kind == :global
|
|
2911
|
+
lines << " [global]"
|
|
2912
|
+
elsif mode
|
|
2913
|
+
lines << " [#{mode}]"
|
|
2914
|
+
end
|
|
2915
|
+
|
|
2916
|
+
group_entries = sort_binding_entries(group_entries, sort:)
|
|
2917
|
+
parts = group_entries.map { |entry| binding_entry_display_parts(entry) }
|
|
2918
|
+
rhs_width = parts.map { |_, rhs, _| rhs.length }.max || 0
|
|
2919
|
+
parts.each do |lhs, rhs, desc|
|
|
2920
|
+
lines << format_binding_entry_line(lhs, rhs, desc, rhs_width:)
|
|
2921
|
+
end
|
|
2922
|
+
end
|
|
2923
|
+
end
|
|
2924
|
+
|
|
2925
|
+
def binding_group_sort_key(kind, mode)
|
|
2926
|
+
rank =
|
|
2927
|
+
case kind
|
|
2928
|
+
when :plain then 0
|
|
2929
|
+
when :mode then 1
|
|
2930
|
+
when :global then 2
|
|
2931
|
+
else 9
|
|
2932
|
+
end
|
|
2933
|
+
[rank, binding_mode_order_index(mode), mode.to_s]
|
|
2934
|
+
end
|
|
2935
|
+
|
|
2936
|
+
def binding_mode_order_index(mode)
|
|
2937
|
+
return -1 if mode.nil?
|
|
2938
|
+
|
|
2939
|
+
order = {
|
|
2940
|
+
normal: 0,
|
|
2941
|
+
insert: 1,
|
|
2942
|
+
visual_char: 2,
|
|
2943
|
+
visual_line: 3,
|
|
2944
|
+
visual_block: 4,
|
|
2945
|
+
operator_pending: 5,
|
|
2946
|
+
command_line: 6
|
|
2947
|
+
}
|
|
2948
|
+
order.fetch(mode.to_sym, 99)
|
|
2949
|
+
end
|
|
2950
|
+
|
|
2951
|
+
def sort_binding_entries(entries, sort:)
|
|
2952
|
+
case sort.to_s
|
|
2953
|
+
when "command"
|
|
2954
|
+
entries.sort_by do |e|
|
|
2955
|
+
[e.id.to_s, format_binding_tokens(e.tokens), e.bang ? 1 : 0, e.argv.inspect, e.kwargs.inspect]
|
|
2956
|
+
end
|
|
2957
|
+
else
|
|
2958
|
+
entries
|
|
2959
|
+
end
|
|
2960
|
+
end
|
|
2961
|
+
|
|
2962
|
+
def binding_entry_display_parts(entry)
|
|
2963
|
+
lhs = format_binding_tokens(entry.tokens)
|
|
2964
|
+
rhs = entry.id.to_s
|
|
2965
|
+
rhs += "!" if entry.bang
|
|
2966
|
+
rhs += " argv=#{entry.argv.inspect}" unless entry.argv.nil? || entry.argv.empty?
|
|
2967
|
+
rhs += " kwargs=#{entry.kwargs.inspect}" unless entry.kwargs.nil? || entry.kwargs.empty?
|
|
2968
|
+
desc = binding_command_desc(entry.id)
|
|
2969
|
+
[lhs, rhs, desc.to_s]
|
|
2970
|
+
end
|
|
2971
|
+
|
|
2972
|
+
def format_binding_entry_line(lhs, rhs, desc, rhs_width:)
|
|
2973
|
+
line = " #{lhs.ljust(18)} #{rhs.ljust(rhs_width)}"
|
|
2974
|
+
line += " #{desc}" unless desc.to_s.empty?
|
|
2975
|
+
line
|
|
2976
|
+
end
|
|
2977
|
+
|
|
2978
|
+
def format_binding_tokens(tokens)
|
|
2979
|
+
Array(tokens).map { |t| format_binding_token(t) }.join
|
|
2980
|
+
end
|
|
2981
|
+
|
|
2982
|
+
def format_binding_token(token)
|
|
2983
|
+
case token.to_s
|
|
2984
|
+
when "\e" then "<Esc>"
|
|
2985
|
+
when "\t" then "<Tab>"
|
|
2986
|
+
when "\r" then "<CR>"
|
|
2987
|
+
else token.to_s
|
|
2988
|
+
end
|
|
2989
|
+
end
|
|
2990
|
+
|
|
2991
|
+
def binding_command_desc(command_id)
|
|
2992
|
+
RuVim::CommandRegistry.instance.fetch(command_id).desc.to_s
|
|
2993
|
+
rescue StandardError
|
|
2994
|
+
""
|
|
2995
|
+
end
|
|
2996
|
+
|
|
2997
|
+
def ex_command_binding_labels(editor, ex_spec)
|
|
2998
|
+
keymaps = editor.keymap_manager
|
|
2999
|
+
return [] unless keymaps
|
|
3000
|
+
|
|
3001
|
+
command_ids = command_ids_for_ex_callable(ex_spec.call)
|
|
3002
|
+
return [] if command_ids.empty?
|
|
3003
|
+
|
|
3004
|
+
entries = keymaps.binding_entries_for_context(editor).select do |entry|
|
|
3005
|
+
entry.layer == :app && command_ids.include?(entry.id.to_s)
|
|
3006
|
+
end
|
|
3007
|
+
entries.sort_by do |entry|
|
|
3008
|
+
[binding_mode_order_index(entry.mode), entry.scope == :global ? 1 : 0, format_binding_tokens(entry.tokens)]
|
|
3009
|
+
end.map do |entry|
|
|
3010
|
+
format_ex_command_binding_label(entry)
|
|
3011
|
+
end.uniq
|
|
3012
|
+
end
|
|
3013
|
+
|
|
3014
|
+
def command_ids_for_ex_callable(callable)
|
|
3015
|
+
RuVim::CommandRegistry.instance.all.filter_map do |spec|
|
|
3016
|
+
spec.id if same_command_callable?(spec.call, callable)
|
|
3017
|
+
end
|
|
3018
|
+
end
|
|
3019
|
+
|
|
3020
|
+
def same_command_callable?(a, b)
|
|
3021
|
+
if (a.is_a?(Symbol) || a.is_a?(String)) && (b.is_a?(Symbol) || b.is_a?(String))
|
|
3022
|
+
return a.to_sym == b.to_sym
|
|
3023
|
+
end
|
|
3024
|
+
a.equal?(b)
|
|
3025
|
+
end
|
|
3026
|
+
|
|
3027
|
+
def format_ex_command_binding_label(entry)
|
|
3028
|
+
lhs = format_binding_tokens(entry.tokens)
|
|
3029
|
+
if entry.scope == :global
|
|
3030
|
+
"global:#{lhs}"
|
|
3031
|
+
elsif entry.mode && entry.mode.to_sym != :normal
|
|
3032
|
+
"#{entry.mode}:#{lhs}"
|
|
3033
|
+
else
|
|
3034
|
+
lhs
|
|
3035
|
+
end
|
|
3036
|
+
end
|
|
3037
|
+
|
|
2214
3038
|
def paste_charwise(ctx, text, before:, count:)
|
|
3039
|
+
count = normalized_count(count)
|
|
2215
3040
|
y = ctx.window.cursor_y
|
|
2216
3041
|
x = ctx.window.cursor_x
|
|
2217
3042
|
insert_col = before ? x : [x + 1, ctx.buffer.line_length(y)].min
|
|
@@ -2242,7 +3067,7 @@ module RuVim
|
|
|
2242
3067
|
end
|
|
2243
3068
|
|
|
2244
3069
|
def move_to_search(ctx, pattern:, direction:, count:)
|
|
2245
|
-
count =
|
|
3070
|
+
count = normalized_count(count)
|
|
2246
3071
|
regex = compile_search_regex(pattern, editor: ctx.editor, window: ctx.window, buffer: ctx.buffer)
|
|
2247
3072
|
count.times do
|
|
2248
3073
|
match = find_next_match(ctx.buffer, ctx.window, regex, direction: direction)
|
|
@@ -2330,5 +3155,141 @@ module RuVim
|
|
|
2330
3155
|
rescue StandardError
|
|
2331
3156
|
0
|
|
2332
3157
|
end
|
|
3158
|
+
|
|
3159
|
+
def parse_substitute_flags(flags_str, default_global: false)
|
|
3160
|
+
flags = { global: default_global, ignore_case: false, match_case: false, count_only: false, no_error: false, confirm: false }
|
|
3161
|
+
return flags if flags_str.nil? || flags_str.empty?
|
|
3162
|
+
|
|
3163
|
+
flags_str.each_char do |ch|
|
|
3164
|
+
case ch
|
|
3165
|
+
when "g" then flags[:global] = true
|
|
3166
|
+
when "i" then flags[:ignore_case] = true
|
|
3167
|
+
when "I" then flags[:match_case] = true
|
|
3168
|
+
when "n" then flags[:count_only] = true
|
|
3169
|
+
when "e" then flags[:no_error] = true
|
|
3170
|
+
when "c" then flags[:confirm] = true
|
|
3171
|
+
end
|
|
3172
|
+
end
|
|
3173
|
+
flags
|
|
3174
|
+
end
|
|
3175
|
+
|
|
3176
|
+
def build_substitute_regex(pattern, flags, ctx)
|
|
3177
|
+
if flags[:match_case]
|
|
3178
|
+
# I flag: force case-sensitive
|
|
3179
|
+
Regexp.new(pattern.to_s)
|
|
3180
|
+
elsif flags[:ignore_case]
|
|
3181
|
+
# i flag: force case-insensitive
|
|
3182
|
+
Regexp.new(pattern.to_s, Regexp::IGNORECASE)
|
|
3183
|
+
else
|
|
3184
|
+
compile_search_regex(pattern, editor: ctx.editor, window: ctx.window, buffer: ctx.buffer)
|
|
3185
|
+
end
|
|
3186
|
+
rescue RegexpError => e
|
|
3187
|
+
raise RuVim::CommandError, "Invalid regex: #{e.message}"
|
|
3188
|
+
end
|
|
3189
|
+
|
|
3190
|
+
def substitute_range(ctx, regex, replacement, r_start, r_end, flags)
|
|
3191
|
+
changed = 0
|
|
3192
|
+
new_lines = ctx.buffer.lines.each_with_index.map do |line, idx|
|
|
3193
|
+
if idx >= r_start && idx <= r_end
|
|
3194
|
+
if flags[:global]
|
|
3195
|
+
line.scan(regex) { changed += 1 }
|
|
3196
|
+
line.gsub(regex, replacement)
|
|
3197
|
+
else
|
|
3198
|
+
if line.match?(regex)
|
|
3199
|
+
changed += 1
|
|
3200
|
+
line.sub(regex, replacement)
|
|
3201
|
+
else
|
|
3202
|
+
line
|
|
3203
|
+
end
|
|
3204
|
+
end
|
|
3205
|
+
else
|
|
3206
|
+
line
|
|
3207
|
+
end
|
|
3208
|
+
end
|
|
3209
|
+
|
|
3210
|
+
if changed.positive?
|
|
3211
|
+
ctx.buffer.begin_change_group
|
|
3212
|
+
ctx.buffer.replace_all_lines!(new_lines)
|
|
3213
|
+
ctx.buffer.end_change_group
|
|
3214
|
+
end
|
|
3215
|
+
changed
|
|
3216
|
+
end
|
|
3217
|
+
|
|
3218
|
+
def count_matches_in_range(buffer, regex, r_start, r_end, global)
|
|
3219
|
+
total = 0
|
|
3220
|
+
(r_start..r_end).each do |idx|
|
|
3221
|
+
line = buffer.line_at(idx)
|
|
3222
|
+
if global
|
|
3223
|
+
line.scan(regex) { total += 1 }
|
|
3224
|
+
else
|
|
3225
|
+
total += 1 if line.match?(regex)
|
|
3226
|
+
end
|
|
3227
|
+
end
|
|
3228
|
+
total
|
|
3229
|
+
end
|
|
3230
|
+
|
|
3231
|
+
public
|
|
3232
|
+
|
|
3233
|
+
def arglist_show(ctx, **)
|
|
3234
|
+
arglist = ctx.editor.arglist
|
|
3235
|
+
if arglist.empty?
|
|
3236
|
+
ctx.editor.echo("No arguments")
|
|
3237
|
+
return
|
|
3238
|
+
end
|
|
3239
|
+
|
|
3240
|
+
current_index = ctx.editor.arglist_index
|
|
3241
|
+
items = arglist.map.with_index do |path, i|
|
|
3242
|
+
if i == current_index
|
|
3243
|
+
"[#{path}]"
|
|
3244
|
+
else
|
|
3245
|
+
" #{path}"
|
|
3246
|
+
end
|
|
3247
|
+
end
|
|
3248
|
+
ctx.editor.echo_multiline(items)
|
|
3249
|
+
end
|
|
3250
|
+
|
|
3251
|
+
def arglist_next(ctx, count:, **)
|
|
3252
|
+
count = normalized_count(count)
|
|
3253
|
+
path = ctx.editor.arglist_next(count)
|
|
3254
|
+
switch_to_file(ctx, path)
|
|
3255
|
+
ctx.editor.echo("Argument #{ctx.editor.arglist_index + 1} of #{ctx.editor.arglist.length}: #{path}")
|
|
3256
|
+
end
|
|
3257
|
+
|
|
3258
|
+
def arglist_prev(ctx, count:, **)
|
|
3259
|
+
count = normalized_count(count)
|
|
3260
|
+
path = ctx.editor.arglist_prev(count)
|
|
3261
|
+
switch_to_file(ctx, path)
|
|
3262
|
+
ctx.editor.echo("Argument #{ctx.editor.arglist_index + 1} of #{ctx.editor.arglist.length}: #{path}")
|
|
3263
|
+
end
|
|
3264
|
+
|
|
3265
|
+
def arglist_first(ctx, **)
|
|
3266
|
+
path = ctx.editor.arglist_first
|
|
3267
|
+
return ctx.editor.error("No arguments") unless path
|
|
3268
|
+
switch_to_file(ctx, path)
|
|
3269
|
+
ctx.editor.echo("Argument 1 of #{ctx.editor.arglist.length}: #{path}")
|
|
3270
|
+
end
|
|
3271
|
+
|
|
3272
|
+
def arglist_last(ctx, **)
|
|
3273
|
+
path = ctx.editor.arglist_last
|
|
3274
|
+
return ctx.editor.error("No arguments") unless path
|
|
3275
|
+
switch_to_file(ctx, path)
|
|
3276
|
+
ctx.editor.echo("Argument #{ctx.editor.arglist.length} of #{ctx.editor.arglist.length}: #{path}")
|
|
3277
|
+
end
|
|
3278
|
+
|
|
3279
|
+
private
|
|
3280
|
+
|
|
3281
|
+
def switch_to_file(ctx, path)
|
|
3282
|
+
existing_buffer = ctx.editor.buffers.values.find { |buf| buf.path == path }
|
|
3283
|
+
if existing_buffer
|
|
3284
|
+
ctx.editor.set_alternate_buffer_id(ctx.editor.current_buffer.id)
|
|
3285
|
+
ctx.editor.activate_buffer(existing_buffer.id)
|
|
3286
|
+
existing_buffer.id
|
|
3287
|
+
else
|
|
3288
|
+
ctx.editor.set_alternate_buffer_id(ctx.editor.current_buffer.id)
|
|
3289
|
+
buffer = ctx.editor.add_buffer_from_file(path)
|
|
3290
|
+
ctx.editor.current_window.buffer_id = buffer.id
|
|
3291
|
+
buffer.id
|
|
3292
|
+
end
|
|
3293
|
+
end
|
|
2333
3294
|
end
|
|
2334
3295
|
end
|