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.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +4 -0
  3. data/AGENTS.md +96 -0
  4. data/CLAUDE.md +1 -0
  5. data/README.md +15 -1
  6. data/docs/binding.md +39 -0
  7. data/docs/command.md +163 -4
  8. data/docs/config.md +12 -4
  9. data/docs/done.md +21 -0
  10. data/docs/spec.md +214 -18
  11. data/docs/todo.md +1 -5
  12. data/docs/tutorial.md +24 -0
  13. data/docs/vim_diff.md +105 -173
  14. data/lib/ruvim/app.rb +1165 -70
  15. data/lib/ruvim/buffer.rb +47 -1
  16. data/lib/ruvim/cli.rb +18 -3
  17. data/lib/ruvim/clipboard.rb +2 -0
  18. data/lib/ruvim/command_invocation.rb +3 -1
  19. data/lib/ruvim/command_line.rb +2 -0
  20. data/lib/ruvim/command_registry.rb +2 -0
  21. data/lib/ruvim/config_dsl.rb +2 -0
  22. data/lib/ruvim/config_loader.rb +2 -0
  23. data/lib/ruvim/context.rb +2 -0
  24. data/lib/ruvim/dispatcher.rb +143 -13
  25. data/lib/ruvim/display_width.rb +3 -0
  26. data/lib/ruvim/editor.rb +466 -71
  27. data/lib/ruvim/ex_command_registry.rb +2 -0
  28. data/lib/ruvim/file_watcher.rb +243 -0
  29. data/lib/ruvim/git/blame.rb +245 -0
  30. data/lib/ruvim/git/branch.rb +97 -0
  31. data/lib/ruvim/git/commit.rb +102 -0
  32. data/lib/ruvim/git/diff.rb +129 -0
  33. data/lib/ruvim/git/handler.rb +84 -0
  34. data/lib/ruvim/git/log.rb +41 -0
  35. data/lib/ruvim/git/status.rb +103 -0
  36. data/lib/ruvim/global_commands.rb +1066 -105
  37. data/lib/ruvim/highlighter.rb +19 -22
  38. data/lib/ruvim/input.rb +40 -28
  39. data/lib/ruvim/keymap_manager.rb +83 -0
  40. data/lib/ruvim/keyword_chars.rb +2 -0
  41. data/lib/ruvim/lang/base.rb +25 -0
  42. data/lib/ruvim/lang/csv.rb +18 -0
  43. data/lib/ruvim/lang/diff.rb +41 -0
  44. data/lib/ruvim/lang/json.rb +52 -0
  45. data/lib/ruvim/lang/markdown.rb +170 -0
  46. data/lib/ruvim/lang/ruby.rb +236 -0
  47. data/lib/ruvim/lang/scheme.rb +44 -0
  48. data/lib/ruvim/lang/tsv.rb +19 -0
  49. data/lib/ruvim/rich_view/json_renderer.rb +131 -0
  50. data/lib/ruvim/rich_view/jsonl_renderer.rb +57 -0
  51. data/lib/ruvim/rich_view/markdown_renderer.rb +248 -0
  52. data/lib/ruvim/rich_view/table_renderer.rb +176 -0
  53. data/lib/ruvim/rich_view.rb +109 -0
  54. data/lib/ruvim/screen.rb +503 -109
  55. data/lib/ruvim/terminal.rb +18 -1
  56. data/lib/ruvim/text_metrics.rb +2 -0
  57. data/lib/ruvim/version.rb +1 -1
  58. data/lib/ruvim/window.rb +2 -0
  59. data/lib/ruvim.rb +24 -0
  60. data/test/app_completion_test.rb +98 -0
  61. data/test/app_dot_repeat_test.rb +13 -0
  62. data/test/app_motion_test.rb +13 -0
  63. data/test/app_scenario_test.rb +898 -1
  64. data/test/app_startup_test.rb +187 -0
  65. data/test/arglist_test.rb +113 -0
  66. data/test/buffer_test.rb +49 -30
  67. data/test/cli_test.rb +14 -0
  68. data/test/clipboard_test.rb +67 -0
  69. data/test/command_line_test.rb +118 -0
  70. data/test/config_dsl_test.rb +87 -0
  71. data/test/dispatcher_test.rb +322 -0
  72. data/test/display_width_test.rb +41 -0
  73. data/test/editor_register_test.rb +23 -0
  74. data/test/file_watcher_test.rb +197 -0
  75. data/test/follow_test.rb +199 -0
  76. data/test/git_blame_test.rb +713 -0
  77. data/test/highlighter_test.rb +165 -0
  78. data/test/indent_test.rb +287 -0
  79. data/test/input_screen_integration_test.rb +40 -2
  80. data/test/markdown_renderer_test.rb +279 -0
  81. data/test/on_save_hook_test.rb +150 -0
  82. data/test/rich_view_test.rb +734 -0
  83. data/test/screen_test.rb +304 -0
  84. data/test/search_option_test.rb +19 -0
  85. data/test/test_helper.rb +9 -0
  86. 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: 1)
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
- if count && count > 1
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
- tab = ctx.editor.tabnew(path: path)
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
- ctx.editor.echo("tab #{ctx.editor.current_tabpage_number}/#{ctx.editor.tabpage_count}")
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, count)
327
- when "l" then delete_chars_right(ctx, count)
328
- when "j" then delete_lines_down(ctx, count)
329
- when "k" then delete_lines_up(ctx, count)
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, count)
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
- handled = delete_motion(ctx, count:, kwargs:)
344
- return unless handled
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
- new_buffer = ctx.editor.add_buffer_from_file(path)
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
- path = resolve_gf_path(ctx, token)
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: #{token}")
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
- flags = ""
676
- flags << "%" if id == current_id
677
- flags << "#" if id == alt_id
678
- flags << "+" if b.modified?
679
- path = b.path || "[No Name]"
680
- "#{id}#{flags} #{path}"
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.echo(items.join(" | "))
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: see docs/binding.md. Ex complement: Tab, insert completion: Ctrl-n/Ctrl-p"
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
- items = registry.all.select { |spec| spec.source == :user }.map(&:name)
788
- ctx.editor.echo(items.empty? ? "No user commands" : "User commands: #{items.join(', ')}")
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
- items = RuVim::ExCommandRegistry.instance.all.map do |spec|
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
- regex = compile_search_regex(pattern, editor: ctx.editor, window: ctx.window, buffer: ctx.buffer)
1023
- changed = 0
1024
- new_lines = ctx.buffer.lines.map do |line|
1025
- if global
1026
- line.scan(regex) { changed += 1 }
1027
- line.gsub(regex, replacement)
1028
- else
1029
- if line.match?(regex)
1030
- changed += 1
1031
- line.sub(regex, replacement)
1032
- else
1033
- line
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
- span = word_object_span(ctx.buffer, ctx.window, around:)
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
- span = text_object_span(ctx.buffer, ctx.window, motion)
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 yank_text_object_word(ctx, around:)
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 = 1 if count.to_i <= 0
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 = find_left_quote(line, x, quote)
2118
+ left = find_quote(line, x, quote, :left)
1574
2119
  right_from = [x, (left ? left + 1 : 0)].max
1575
- right = find_right_quote(line, right_from, quote)
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 find_left_quote(line, x, quote)
2192
+ def find_quote(line, x, quote, direction)
1648
2193
  i = x
1649
- while i >= 0
1650
- return i if line[i] == quote && !escaped?(line, i)
1651
- i -= 1
1652
- end
1653
- nil
1654
- end
1655
-
1656
- def find_right_quote(line, x, quote)
1657
- i = x
1658
- while i < line.length
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.echo(items.join(" "))
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 = 1 if count.to_i <= 0
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