ruvim 0.3.0 → 0.6.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/AGENTS.md +68 -7
- data/README.md +30 -7
- data/Rakefile +7 -0
- data/benchmark/cext_compare.rb +165 -0
- data/benchmark/chunked_load.rb +256 -0
- data/benchmark/file_load.rb +140 -0
- data/benchmark/hotspots.rb +178 -0
- data/docs/binding.md +18 -1
- data/docs/command.md +156 -10
- data/docs/config.md +10 -2
- data/docs/done.md +23 -0
- data/docs/spec.md +162 -25
- data/docs/todo.md +9 -0
- data/docs/tutorial.md +33 -1
- data/docs/vim_diff.md +31 -8
- data/ext/ruvim/extconf.rb +5 -0
- data/ext/ruvim/ruvim_ext.c +519 -0
- data/lib/ruvim/app.rb +246 -2525
- data/lib/ruvim/browser.rb +104 -0
- data/lib/ruvim/buffer.rb +43 -20
- data/lib/ruvim/cli.rb +6 -0
- data/lib/ruvim/command_invocation.rb +2 -2
- data/lib/ruvim/completion_manager.rb +708 -0
- data/lib/ruvim/dispatcher.rb +14 -8
- data/lib/ruvim/display_width.rb +91 -45
- data/lib/ruvim/editor.rb +74 -80
- data/lib/ruvim/ex_command_registry.rb +3 -1
- data/lib/ruvim/file_watcher.rb +243 -0
- data/lib/ruvim/gh/link.rb +207 -0
- data/lib/ruvim/git/blame.rb +255 -0
- data/lib/ruvim/git/branch.rb +112 -0
- data/lib/ruvim/git/commit.rb +102 -0
- data/lib/ruvim/git/diff.rb +129 -0
- data/lib/ruvim/git/grep.rb +107 -0
- data/lib/ruvim/git/handler.rb +125 -0
- data/lib/ruvim/git/log.rb +41 -0
- data/lib/ruvim/git/status.rb +103 -0
- data/lib/ruvim/global_commands.rb +351 -77
- data/lib/ruvim/highlighter.rb +4 -11
- data/lib/ruvim/input.rb +1 -0
- data/lib/ruvim/key_handler.rb +1510 -0
- data/lib/ruvim/keymap_manager.rb +7 -7
- data/lib/ruvim/lang/base.rb +5 -0
- data/lib/ruvim/lang/c.rb +116 -0
- data/lib/ruvim/lang/cpp.rb +107 -0
- data/lib/ruvim/lang/csv.rb +4 -1
- data/lib/ruvim/lang/diff.rb +43 -0
- data/lib/ruvim/lang/dockerfile.rb +36 -0
- data/lib/ruvim/lang/elixir.rb +85 -0
- data/lib/ruvim/lang/erb.rb +30 -0
- data/lib/ruvim/lang/go.rb +83 -0
- data/lib/ruvim/lang/html.rb +34 -0
- data/lib/ruvim/lang/javascript.rb +83 -0
- data/lib/ruvim/lang/json.rb +40 -0
- data/lib/ruvim/lang/lua.rb +76 -0
- data/lib/ruvim/lang/makefile.rb +36 -0
- data/lib/ruvim/lang/markdown.rb +3 -4
- data/lib/ruvim/lang/ocaml.rb +77 -0
- data/lib/ruvim/lang/perl.rb +91 -0
- data/lib/ruvim/lang/python.rb +85 -0
- data/lib/ruvim/lang/registry.rb +102 -0
- data/lib/ruvim/lang/ruby.rb +7 -0
- data/lib/ruvim/lang/rust.rb +95 -0
- data/lib/ruvim/lang/scheme.rb +5 -0
- data/lib/ruvim/lang/sh.rb +76 -0
- data/lib/ruvim/lang/sql.rb +52 -0
- data/lib/ruvim/lang/toml.rb +36 -0
- data/lib/ruvim/lang/tsv.rb +4 -1
- data/lib/ruvim/lang/typescript.rb +53 -0
- data/lib/ruvim/lang/yaml.rb +62 -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/table_renderer.rb +3 -3
- data/lib/ruvim/rich_view.rb +30 -7
- data/lib/ruvim/screen.rb +135 -84
- data/lib/ruvim/stream/file_load.rb +85 -0
- data/lib/ruvim/stream/follow.rb +40 -0
- data/lib/ruvim/stream/git.rb +43 -0
- data/lib/ruvim/stream/run.rb +74 -0
- data/lib/ruvim/stream/stdin.rb +55 -0
- data/lib/ruvim/stream.rb +35 -0
- data/lib/ruvim/stream_mixer.rb +394 -0
- data/lib/ruvim/terminal.rb +18 -4
- data/lib/ruvim/text_metrics.rb +84 -65
- data/lib/ruvim/version.rb +1 -1
- data/lib/ruvim/window.rb +5 -5
- data/lib/ruvim.rb +31 -4
- data/test/app_command_test.rb +382 -0
- data/test/app_completion_test.rb +65 -16
- data/test/app_dot_repeat_test.rb +27 -3
- data/test/app_ex_command_test.rb +154 -0
- data/test/app_motion_test.rb +13 -12
- data/test/app_register_test.rb +2 -1
- data/test/app_scenario_test.rb +182 -8
- data/test/app_startup_test.rb +70 -27
- data/test/app_text_object_test.rb +2 -1
- data/test/app_unicode_behavior_test.rb +3 -2
- data/test/browser_test.rb +88 -0
- data/test/buffer_test.rb +24 -0
- data/test/cli_test.rb +77 -0
- data/test/clipboard_test.rb +67 -0
- data/test/command_invocation_test.rb +33 -0
- data/test/command_line_test.rb +118 -0
- data/test/config_dsl_test.rb +134 -0
- data/test/dispatcher_test.rb +74 -4
- data/test/display_width_test.rb +41 -0
- data/test/ex_command_registry_test.rb +106 -0
- data/test/file_watcher_test.rb +197 -0
- data/test/follow_test.rb +198 -0
- data/test/gh_link_test.rb +141 -0
- data/test/git_blame_test.rb +792 -0
- data/test/git_grep_test.rb +64 -0
- data/test/highlighter_test.rb +169 -0
- data/test/indent_test.rb +223 -0
- data/test/input_screen_integration_test.rb +1 -1
- data/test/keyword_chars_test.rb +85 -0
- data/test/lang_test.rb +634 -0
- data/test/markdown_renderer_test.rb +5 -5
- data/test/on_save_hook_test.rb +12 -8
- data/test/render_snapshot_test.rb +78 -0
- data/test/rich_view_test.rb +279 -23
- data/test/run_command_test.rb +307 -0
- data/test/screen_test.rb +68 -5
- data/test/search_option_test.rb +19 -0
- data/test/stream_test.rb +165 -0
- data/test/test_helper.rb +9 -0
- data/test/window_test.rb +59 -0
- metadata +68 -2
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
|
|
5
|
+
module RuVim
|
|
6
|
+
module Git
|
|
7
|
+
module Status
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
# Run git status.
|
|
11
|
+
# Returns [lines, root, error_message].
|
|
12
|
+
def run(file_path)
|
|
13
|
+
root, err = Git.repo_root(file_path)
|
|
14
|
+
return [nil, nil, err] unless root
|
|
15
|
+
|
|
16
|
+
out, err, status = Open3.capture3("git", "status", chdir: root)
|
|
17
|
+
unless status.success?
|
|
18
|
+
return [nil, nil, err.strip]
|
|
19
|
+
end
|
|
20
|
+
[out.lines(chomp: true), root, nil]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Extract filename from a git status output line.
|
|
24
|
+
# Returns relative path or nil.
|
|
25
|
+
def parse_filename(line)
|
|
26
|
+
stripped = line.to_s.strip
|
|
27
|
+
case stripped
|
|
28
|
+
when /\A(?:modified|new file|deleted|renamed|copied|typechange):\s+(.+)/
|
|
29
|
+
$1.strip
|
|
30
|
+
when /\A(\S.+)/
|
|
31
|
+
# Untracked file lines (no prefix keyword)
|
|
32
|
+
path = $1.strip
|
|
33
|
+
# Skip section headers and hints
|
|
34
|
+
return nil if path.start_with?("(")
|
|
35
|
+
return nil if path.match?(/\A[A-Z]/)
|
|
36
|
+
path
|
|
37
|
+
else
|
|
38
|
+
nil
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Command handler methods
|
|
43
|
+
module HandlerMethods
|
|
44
|
+
def git_status(ctx, **)
|
|
45
|
+
file_path = git_resolve_path(ctx)
|
|
46
|
+
unless file_path
|
|
47
|
+
ctx.editor.echo_error("No file or directory to resolve git repo")
|
|
48
|
+
return
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
lines, root, err = Status.run(file_path)
|
|
52
|
+
unless lines
|
|
53
|
+
ctx.editor.echo_error("git status: #{err}")
|
|
54
|
+
return
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
buf = ctx.editor.add_virtual_buffer(
|
|
58
|
+
kind: :git_status,
|
|
59
|
+
name: "[Git Status]",
|
|
60
|
+
lines: lines,
|
|
61
|
+
readonly: true,
|
|
62
|
+
modifiable: false
|
|
63
|
+
)
|
|
64
|
+
buf.options["git_repo_root"] = root
|
|
65
|
+
ctx.editor.switch_to_buffer(buf.id)
|
|
66
|
+
bind_git_buffer_keys(ctx.editor, buf.id)
|
|
67
|
+
ctx.editor.echo("[Git Status]")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def git_status_open_file(ctx, **)
|
|
71
|
+
buf = ctx.buffer
|
|
72
|
+
unless buf.kind == :git_status
|
|
73
|
+
ctx.editor.echo_error("Not a git status buffer")
|
|
74
|
+
return
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
line = buf.line_at(ctx.window.cursor_y)
|
|
78
|
+
filename = Status.parse_filename(line)
|
|
79
|
+
unless filename
|
|
80
|
+
ctx.editor.echo_error("No file on this line")
|
|
81
|
+
return
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
root = buf.options["git_repo_root"]
|
|
85
|
+
full_path = File.join(root, filename)
|
|
86
|
+
unless File.exist?(full_path)
|
|
87
|
+
ctx.editor.echo_error("File not found: #{filename}")
|
|
88
|
+
return
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
existing = ctx.editor.buffers.values.find { |b| b.path == full_path }
|
|
92
|
+
if existing
|
|
93
|
+
ctx.editor.switch_to_buffer(existing.id)
|
|
94
|
+
else
|
|
95
|
+
new_buf = ctx.editor.add_buffer_from_file(full_path)
|
|
96
|
+
ctx.editor.switch_to_buffer(new_buf.id)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -366,7 +366,7 @@ module RuVim
|
|
|
366
366
|
chunk = char_at_cursor_for_delete(ctx.buffer, ctx.window.cursor_y, ctx.window.cursor_x)
|
|
367
367
|
ok = ctx.buffer.delete_char(ctx.window.cursor_y, ctx.window.cursor_x)
|
|
368
368
|
break unless ok
|
|
369
|
-
deleted << chunk
|
|
369
|
+
deleted << chunk
|
|
370
370
|
end
|
|
371
371
|
ctx.buffer.end_change_group
|
|
372
372
|
store_delete_register(ctx, text: deleted, type: :charwise) unless deleted.empty?
|
|
@@ -391,7 +391,7 @@ module RuVim
|
|
|
391
391
|
break if x >= line.length
|
|
392
392
|
|
|
393
393
|
ch = line[x]
|
|
394
|
-
swapped = ch.
|
|
394
|
+
swapped = ch.swapcase
|
|
395
395
|
if !swapped.empty? && swapped != ch
|
|
396
396
|
ctx.buffer.delete_span(y, x, y, x + 1)
|
|
397
397
|
ctx.buffer.insert_char(y, x, swapped[0])
|
|
@@ -467,6 +467,8 @@ module RuVim
|
|
|
467
467
|
when "k" then delete_lines_up(ctx, ncount)
|
|
468
468
|
when "$" then delete_to_end_of_line(ctx)
|
|
469
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)
|
|
470
472
|
when "iw" then delete_text_object_word(ctx, around: false)
|
|
471
473
|
when "aw" then delete_text_object_word(ctx, around: true)
|
|
472
474
|
else
|
|
@@ -478,9 +480,22 @@ module RuVim
|
|
|
478
480
|
|
|
479
481
|
def change_motion(ctx, count:, kwargs:, **)
|
|
480
482
|
materialize_intro_buffer_if_needed(ctx)
|
|
481
|
-
|
|
482
|
-
|
|
483
|
+
motion = (kwargs[:motion] || kwargs["motion"]).to_s
|
|
484
|
+
result = delete_motion(ctx, count:, kwargs:)
|
|
485
|
+
return unless result
|
|
483
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
|
|
484
499
|
enter_insert_mode(ctx)
|
|
485
500
|
end
|
|
486
501
|
|
|
@@ -610,6 +625,10 @@ module RuVim
|
|
|
610
625
|
text = ctx.buffer.span_text(y, x, target[:row], target[:col])
|
|
611
626
|
store_yank_register(ctx, text:, type: :charwise)
|
|
612
627
|
ctx.editor.echo("yanked")
|
|
628
|
+
when "G"
|
|
629
|
+
yank_lines_to_end(ctx)
|
|
630
|
+
when "gg"
|
|
631
|
+
yank_lines_to_start(ctx)
|
|
613
632
|
when "iw"
|
|
614
633
|
yank_text_object_word(ctx, around: false)
|
|
615
634
|
when "aw"
|
|
@@ -755,7 +774,18 @@ module RuVim
|
|
|
755
774
|
ctx.editor.clear_message
|
|
756
775
|
end
|
|
757
776
|
|
|
758
|
-
def file_write(ctx, argv:, bang:, **)
|
|
777
|
+
def file_write(ctx, argv:, bang:, kwargs: {}, **)
|
|
778
|
+
if ctx.buffer.kind == :git_commit
|
|
779
|
+
git_commit_execute(ctx)
|
|
780
|
+
return
|
|
781
|
+
end
|
|
782
|
+
|
|
783
|
+
arg = argv.join(" ")
|
|
784
|
+
if arg.start_with?("!")
|
|
785
|
+
file_write_to_shell(ctx, command: arg[1..].strip, kwargs: kwargs)
|
|
786
|
+
return
|
|
787
|
+
end
|
|
788
|
+
|
|
759
789
|
path = argv[0]
|
|
760
790
|
target = ctx.buffer.write_to(path)
|
|
761
791
|
size = File.exist?(target) ? File.size(target) : 0
|
|
@@ -766,7 +796,41 @@ module RuVim
|
|
|
766
796
|
end
|
|
767
797
|
end
|
|
768
798
|
|
|
799
|
+
def file_write_to_shell(ctx, command:, kwargs: {})
|
|
800
|
+
raise RuVim::CommandError, "Usage: :w !<command>" if command.empty?
|
|
801
|
+
|
|
802
|
+
r_start = kwargs[:range_start] || 0
|
|
803
|
+
r_end = kwargs[:range_end] || (ctx.buffer.line_count - 1)
|
|
804
|
+
lines = (r_start..r_end).map { |i| ctx.buffer.lines[i] }
|
|
805
|
+
input = lines.join("\n") + "\n"
|
|
806
|
+
|
|
807
|
+
shell = ENV["SHELL"].to_s
|
|
808
|
+
shell = "/bin/sh" if shell.empty?
|
|
809
|
+
_stdout, stderr_text, status = Open3.capture3(shell, "-c", command, stdin_data: input)
|
|
810
|
+
unless stderr_text.empty?
|
|
811
|
+
ctx.editor.echo_error(stderr_text.lines(chomp: true).first)
|
|
812
|
+
return
|
|
813
|
+
end
|
|
814
|
+
ctx.editor.echo("#{lines.length} line(s) written to !#{command}, exit #{status.exitstatus}")
|
|
815
|
+
end
|
|
816
|
+
|
|
769
817
|
def app_quit(ctx, bang:, **)
|
|
818
|
+
if ctx.buffer.kind == :filter
|
|
819
|
+
saved_y = ctx.buffer.options["filter_source_cursor_y"]
|
|
820
|
+
saved_x = ctx.buffer.options["filter_source_cursor_x"]
|
|
821
|
+
saved_row_offset = ctx.buffer.options["filter_source_row_offset"]
|
|
822
|
+
saved_col_offset = ctx.buffer.options["filter_source_col_offset"]
|
|
823
|
+
ctx.editor.delete_buffer(ctx.buffer.id)
|
|
824
|
+
if saved_y
|
|
825
|
+
win = ctx.editor.current_window
|
|
826
|
+
win.cursor_y = saved_y
|
|
827
|
+
win.cursor_x = saved_x || 0
|
|
828
|
+
win.row_offset = saved_row_offset || 0
|
|
829
|
+
win.col_offset = saved_col_offset || 0
|
|
830
|
+
end
|
|
831
|
+
return
|
|
832
|
+
end
|
|
833
|
+
|
|
770
834
|
if ctx.editor.window_count > 1
|
|
771
835
|
ctx.editor.close_current_window
|
|
772
836
|
ctx.editor.echo("closed window")
|
|
@@ -1057,8 +1121,8 @@ module RuVim
|
|
|
1057
1121
|
errf.flush
|
|
1058
1122
|
outf.rewind
|
|
1059
1123
|
errf.rewind
|
|
1060
|
-
stdout_text = outf.read
|
|
1061
|
-
stderr_text = errf.read
|
|
1124
|
+
stdout_text = outf.read
|
|
1125
|
+
stderr_text = errf.read
|
|
1062
1126
|
end
|
|
1063
1127
|
end
|
|
1064
1128
|
if !stdout_text.empty? || !stderr_text.empty?
|
|
@@ -1094,44 +1158,155 @@ module RuVim
|
|
|
1094
1158
|
$stderr = (defined?(original_g_stderr) && original_g_stderr) ? original_g_stderr : STDERR
|
|
1095
1159
|
end
|
|
1096
1160
|
|
|
1097
|
-
def
|
|
1098
|
-
|
|
1161
|
+
def ex_run(ctx, argv:, **)
|
|
1162
|
+
editor = ctx.editor
|
|
1163
|
+
source_buffer = ctx.buffer
|
|
1099
1164
|
|
|
1100
|
-
|
|
1101
|
-
|
|
1165
|
+
if argv.empty?
|
|
1166
|
+
# No args: use last command for this buffer, or runprg
|
|
1167
|
+
command = editor.run_history[source_buffer.id]
|
|
1168
|
+
if command.nil?
|
|
1169
|
+
runprg = editor.get_option("runprg", buffer: source_buffer)
|
|
1170
|
+
raise RuVim::CommandError, "No runprg set and no previous :run command" unless runprg
|
|
1171
|
+
command = runprg
|
|
1172
|
+
end
|
|
1173
|
+
else
|
|
1174
|
+
command = argv.first
|
|
1175
|
+
end
|
|
1102
1176
|
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1177
|
+
# Auto-save modified buffer before running
|
|
1178
|
+
if source_buffer.modified? && source_buffer.path
|
|
1179
|
+
source_buffer.write_to
|
|
1180
|
+
end
|
|
1106
1181
|
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1182
|
+
expanded = expand_run_command(command, source_buffer)
|
|
1183
|
+
editor.run_history[source_buffer.id] = command
|
|
1184
|
+
|
|
1185
|
+
# Find or create [Shell Output] buffer
|
|
1186
|
+
output_buf = if editor.run_output_buffer_id
|
|
1187
|
+
editor.buffers[editor.run_output_buffer_id]
|
|
1188
|
+
end
|
|
1189
|
+
|
|
1190
|
+
if output_buf
|
|
1191
|
+
# Reuse: clear content
|
|
1192
|
+
output_buf.replace_all_lines!([""])
|
|
1193
|
+
else
|
|
1194
|
+
output_buf = editor.add_virtual_buffer(
|
|
1195
|
+
kind: :run_output,
|
|
1196
|
+
name: "[Shell Output]",
|
|
1197
|
+
lines: [""],
|
|
1198
|
+
readonly: true,
|
|
1199
|
+
modifiable: false
|
|
1200
|
+
)
|
|
1201
|
+
editor.run_output_buffer_id = output_buf.id
|
|
1202
|
+
end
|
|
1203
|
+
# Open output buffer in a split (reuse existing window if present)
|
|
1204
|
+
existing_win = editor.windows.values.find { |w| w.buffer_id == output_buf.id }
|
|
1205
|
+
if existing_win
|
|
1206
|
+
editor.current_window_id = existing_win.id
|
|
1207
|
+
else
|
|
1208
|
+
win = editor.split_current_window(layout: :horizontal, place: :after)
|
|
1209
|
+
win.buffer_id = output_buf.id
|
|
1210
|
+
win.cursor_x = 0
|
|
1211
|
+
win.cursor_y = 0
|
|
1212
|
+
win.row_offset = 0
|
|
1213
|
+
end
|
|
1214
|
+
|
|
1215
|
+
# Start streaming (handler creates Stream::Run and sets buf.stream)
|
|
1216
|
+
handler = editor.run_stream_handler
|
|
1217
|
+
if handler
|
|
1218
|
+
handler.call(output_buf, expanded)
|
|
1122
1219
|
else
|
|
1220
|
+
# Fallback for tests without stream handler: synchronous execution
|
|
1221
|
+
shell = ENV["SHELL"].to_s
|
|
1222
|
+
shell = "/bin/sh" if shell.empty?
|
|
1223
|
+
output, _status = Open3.capture2e(shell, "-c", expanded)
|
|
1224
|
+
output_buf.replace_all_lines!(output.lines(chomp: true))
|
|
1225
|
+
end
|
|
1226
|
+
end
|
|
1227
|
+
|
|
1228
|
+
def expand_run_command(command, buffer)
|
|
1229
|
+
return command unless command.include?("%")
|
|
1230
|
+
|
|
1231
|
+
path = buffer.path
|
|
1232
|
+
raise RuVim::CommandError, "No file name (use % in :run requires a file)" unless path
|
|
1233
|
+
|
|
1234
|
+
command.gsub("%", path)
|
|
1235
|
+
end
|
|
1236
|
+
|
|
1237
|
+
def ex_shell(ctx, command:, **)
|
|
1238
|
+
raise RuVim::CommandError, "Restricted mode: :! is disabled" if ctx.editor.respond_to?(:restricted_mode?) && ctx.editor.restricted_mode?
|
|
1239
|
+
|
|
1240
|
+
raise RuVim::CommandError, "Usage: :!<command>" if command.strip.empty?
|
|
1241
|
+
|
|
1242
|
+
executor = ctx.editor.shell_executor
|
|
1243
|
+
if executor
|
|
1244
|
+
status = executor.call(command)
|
|
1123
1245
|
ctx.editor.echo("shell exit #{status.exitstatus}")
|
|
1246
|
+
else
|
|
1247
|
+
shell = ENV["SHELL"].to_s
|
|
1248
|
+
shell = "/bin/sh" if shell.empty?
|
|
1249
|
+
stdout_text, stderr_text, status = Open3.capture3(shell, "-c", command)
|
|
1250
|
+
|
|
1251
|
+
if !stdout_text.empty? || !stderr_text.empty?
|
|
1252
|
+
lines = ["Shell output", "", "[command]", command, ""]
|
|
1253
|
+
unless stdout_text.empty?
|
|
1254
|
+
lines << "[stdout]"
|
|
1255
|
+
lines.concat(stdout_text.lines(chomp: true))
|
|
1256
|
+
lines << ""
|
|
1257
|
+
end
|
|
1258
|
+
unless stderr_text.empty?
|
|
1259
|
+
lines << "[stderr]"
|
|
1260
|
+
lines.concat(stderr_text.lines(chomp: true))
|
|
1261
|
+
lines << ""
|
|
1262
|
+
end
|
|
1263
|
+
lines << "[status]"
|
|
1264
|
+
lines << "exit #{status.exitstatus}"
|
|
1265
|
+
ctx.editor.show_help_buffer!(title: "[Shell Output]", lines:, filetype: "sh")
|
|
1266
|
+
else
|
|
1267
|
+
ctx.editor.echo("shell exit #{status.exitstatus}")
|
|
1268
|
+
end
|
|
1124
1269
|
end
|
|
1125
1270
|
rescue Errno::ENOENT => e
|
|
1126
1271
|
raise RuVim::CommandError, "Shell error: #{e.message}"
|
|
1127
1272
|
end
|
|
1128
1273
|
|
|
1274
|
+
def ex_read(ctx, argv:, kwargs:, **)
|
|
1275
|
+
arg = argv.join(" ")
|
|
1276
|
+
raise RuVim::CommandError, "Usage: :r[ead] [file] or :r[ead] !command" if arg.strip.empty?
|
|
1277
|
+
|
|
1278
|
+
insert_line = kwargs[:range_start] || ctx.window.cursor_y
|
|
1279
|
+
new_lines = if arg.start_with?("!")
|
|
1280
|
+
command = arg[1..].strip
|
|
1281
|
+
raise RuVim::CommandError, "Usage: :r !<command>" if command.empty?
|
|
1282
|
+
|
|
1283
|
+
shell = ENV["SHELL"].to_s
|
|
1284
|
+
shell = "/bin/sh" if shell.empty?
|
|
1285
|
+
stdout_text, stderr_text, _status = Open3.capture3(shell, "-c", command)
|
|
1286
|
+
unless stderr_text.empty?
|
|
1287
|
+
ctx.editor.echo_error(stderr_text.lines(chomp: true).first)
|
|
1288
|
+
end
|
|
1289
|
+
stdout_text.lines(chomp: true)
|
|
1290
|
+
else
|
|
1291
|
+
path = File.expand_path(arg.strip)
|
|
1292
|
+
raise RuVim::CommandError, "File not found: #{arg.strip}" unless File.exist?(path)
|
|
1293
|
+
|
|
1294
|
+
File.read(path).lines(chomp: true)
|
|
1295
|
+
end
|
|
1296
|
+
|
|
1297
|
+
return if new_lines.empty?
|
|
1298
|
+
|
|
1299
|
+
ctx.buffer.insert_lines_at(insert_line + 1, new_lines)
|
|
1300
|
+
ctx.window.cursor_y = insert_line + new_lines.length
|
|
1301
|
+
ctx.editor.echo("#{new_lines.length} line(s) inserted")
|
|
1302
|
+
end
|
|
1303
|
+
|
|
1129
1304
|
def ex_commands(ctx, **)
|
|
1130
1305
|
rows = RuVim::ExCommandRegistry.instance.all.map do |spec|
|
|
1131
1306
|
alias_text = spec.aliases.empty? ? "" : " (#{spec.aliases.join(', ')})"
|
|
1132
1307
|
source = spec.source == :user ? " [user]" : ""
|
|
1133
1308
|
name = "#{spec.name}#{alias_text}#{source}"
|
|
1134
|
-
desc = spec.desc
|
|
1309
|
+
desc = spec.desc
|
|
1135
1310
|
keys = ex_command_binding_labels(ctx.editor, spec)
|
|
1136
1311
|
[name, desc, keys]
|
|
1137
1312
|
end
|
|
@@ -1336,6 +1511,69 @@ module RuVim
|
|
|
1336
1511
|
RuVim::RichView.toggle!(ctx.editor)
|
|
1337
1512
|
end
|
|
1338
1513
|
|
|
1514
|
+
def rich_view_close_buffer(ctx, **)
|
|
1515
|
+
ctx.editor.delete_buffer(ctx.buffer.id)
|
|
1516
|
+
end
|
|
1517
|
+
|
|
1518
|
+
def search_filter(ctx, **)
|
|
1519
|
+
editor = ctx.editor
|
|
1520
|
+
search = editor.last_search
|
|
1521
|
+
unless search
|
|
1522
|
+
editor.echo_error("No search pattern")
|
|
1523
|
+
return
|
|
1524
|
+
end
|
|
1525
|
+
|
|
1526
|
+
regex = compile_search_regex(search[:pattern], editor: editor, window: ctx.window, buffer: ctx.buffer)
|
|
1527
|
+
source_buffer = ctx.buffer
|
|
1528
|
+
|
|
1529
|
+
# Collect matching lines with origin mapping
|
|
1530
|
+
origins = []
|
|
1531
|
+
matching_lines = []
|
|
1532
|
+
source_buffer.lines.each_with_index do |line, row|
|
|
1533
|
+
if regex.match?(line)
|
|
1534
|
+
# If source is a filter buffer, chain back to the original
|
|
1535
|
+
if source_buffer.kind == :filter && source_buffer.options["filter_origins"]
|
|
1536
|
+
origins << source_buffer.options["filter_origins"][row]
|
|
1537
|
+
else
|
|
1538
|
+
origins << { buffer_id: source_buffer.id, row: row }
|
|
1539
|
+
end
|
|
1540
|
+
matching_lines << line
|
|
1541
|
+
end
|
|
1542
|
+
end
|
|
1543
|
+
|
|
1544
|
+
if matching_lines.empty?
|
|
1545
|
+
editor.echo_error("Pattern not found: #{search[:pattern]}")
|
|
1546
|
+
return
|
|
1547
|
+
end
|
|
1548
|
+
|
|
1549
|
+
filetype = source_buffer.options["filetype"]
|
|
1550
|
+
filter_buf = editor.add_virtual_buffer(
|
|
1551
|
+
kind: :filter,
|
|
1552
|
+
name: "[Filter: /#{search[:pattern]}/]",
|
|
1553
|
+
lines: matching_lines,
|
|
1554
|
+
filetype: filetype,
|
|
1555
|
+
readonly: false,
|
|
1556
|
+
modifiable: false
|
|
1557
|
+
)
|
|
1558
|
+
filter_buf.options["filter_origins"] = origins
|
|
1559
|
+
filter_buf.options["filter_source_buffer_id"] = source_buffer.id
|
|
1560
|
+
filter_buf.options["filter_source_cursor_y"] = ctx.window.cursor_y
|
|
1561
|
+
filter_buf.options["filter_source_cursor_x"] = ctx.window.cursor_x
|
|
1562
|
+
filter_buf.options["filter_source_row_offset"] = ctx.window.row_offset
|
|
1563
|
+
filter_buf.options["filter_source_col_offset"] = ctx.window.col_offset
|
|
1564
|
+
editor.switch_to_buffer(filter_buf.id)
|
|
1565
|
+
editor.echo("filter: #{matching_lines.length} line(s)")
|
|
1566
|
+
end
|
|
1567
|
+
|
|
1568
|
+
def ex_filter(ctx, argv:, **)
|
|
1569
|
+
if argv.any?
|
|
1570
|
+
pattern = parse_vimgrep_pattern(argv.join(" "))
|
|
1571
|
+
editor = ctx.editor
|
|
1572
|
+
editor.set_last_search(pattern: pattern, direction: :forward)
|
|
1573
|
+
end
|
|
1574
|
+
search_filter(ctx)
|
|
1575
|
+
end
|
|
1576
|
+
|
|
1339
1577
|
def submit_search(ctx, pattern:, direction:)
|
|
1340
1578
|
text = pattern.to_s
|
|
1341
1579
|
if text.empty?
|
|
@@ -1349,6 +1587,8 @@ module RuVim
|
|
|
1349
1587
|
move_to_search(ctx, pattern: text, direction:, count: 1)
|
|
1350
1588
|
end
|
|
1351
1589
|
|
|
1590
|
+
include RuVim::Git::Handler
|
|
1591
|
+
|
|
1352
1592
|
private
|
|
1353
1593
|
|
|
1354
1594
|
def reindent_range(ctx, start_row, end_row)
|
|
@@ -1392,13 +1632,18 @@ module RuVim
|
|
|
1392
1632
|
end
|
|
1393
1633
|
|
|
1394
1634
|
def run_external_grep(ctx, argv:, target:)
|
|
1395
|
-
|
|
1635
|
+
if ctx.editor.respond_to?(:restricted_mode?) && ctx.editor.restricted_mode?
|
|
1636
|
+
raise RuVim::CommandError, "Restricted mode: :grep is disabled"
|
|
1637
|
+
end
|
|
1638
|
+
|
|
1639
|
+
args = Array(argv)
|
|
1396
1640
|
raise RuVim::CommandError, "Usage: :grep pattern [files...]" if args.empty?
|
|
1397
1641
|
|
|
1398
1642
|
grepprg = ctx.editor.effective_option("grepprg", window: ctx.window, buffer: ctx.buffer) || "grep -n"
|
|
1399
|
-
|
|
1643
|
+
cmd_parts = Shellwords.shellsplit(grepprg)
|
|
1644
|
+
expanded_args = args.flat_map { |a| (g = Dir.glob(a)).empty? ? [a] : g }
|
|
1400
1645
|
|
|
1401
|
-
stdout, stderr, status = Open3.capture3(
|
|
1646
|
+
stdout, stderr, status = Open3.capture3(*cmd_parts, *expanded_args)
|
|
1402
1647
|
if stdout.strip.empty? && !status.success?
|
|
1403
1648
|
msg = stderr.strip.empty? ? "No matches found" : stderr.strip
|
|
1404
1649
|
ctx.editor.echo_error(msg)
|
|
@@ -1723,6 +1968,48 @@ module RuVim
|
|
|
1723
1968
|
true
|
|
1724
1969
|
end
|
|
1725
1970
|
|
|
1971
|
+
def delete_lines_to_end(ctx)
|
|
1972
|
+
y = ctx.window.cursor_y
|
|
1973
|
+
total = ctx.buffer.lines.length - y
|
|
1974
|
+
deleted = ctx.buffer.line_block_text(y, total)
|
|
1975
|
+
ctx.buffer.begin_change_group
|
|
1976
|
+
total.times { ctx.buffer.delete_line(y) }
|
|
1977
|
+
ctx.buffer.end_change_group
|
|
1978
|
+
store_delete_register(ctx, text: deleted, type: :linewise)
|
|
1979
|
+
ctx.window.clamp_to_buffer(ctx.buffer)
|
|
1980
|
+
:linewise
|
|
1981
|
+
end
|
|
1982
|
+
|
|
1983
|
+
def delete_lines_to_start(ctx)
|
|
1984
|
+
y = ctx.window.cursor_y
|
|
1985
|
+
total = y + 1
|
|
1986
|
+
deleted = ctx.buffer.line_block_text(0, total)
|
|
1987
|
+
ctx.buffer.begin_change_group
|
|
1988
|
+
total.times { ctx.buffer.delete_line(0) }
|
|
1989
|
+
ctx.buffer.end_change_group
|
|
1990
|
+
store_delete_register(ctx, text: deleted, type: :linewise)
|
|
1991
|
+
ctx.window.cursor_y = 0
|
|
1992
|
+
ctx.window.cursor_x = 0
|
|
1993
|
+
ctx.window.clamp_to_buffer(ctx.buffer)
|
|
1994
|
+
:linewise
|
|
1995
|
+
end
|
|
1996
|
+
|
|
1997
|
+
def yank_lines_to_end(ctx)
|
|
1998
|
+
y = ctx.window.cursor_y
|
|
1999
|
+
total = ctx.buffer.lines.length - y
|
|
2000
|
+
text = ctx.buffer.line_block_text(y, total)
|
|
2001
|
+
store_yank_register(ctx, text: text, type: :linewise)
|
|
2002
|
+
ctx.editor.echo("#{total} line(s) yanked")
|
|
2003
|
+
end
|
|
2004
|
+
|
|
2005
|
+
def yank_lines_to_start(ctx)
|
|
2006
|
+
y = ctx.window.cursor_y
|
|
2007
|
+
total = y + 1
|
|
2008
|
+
text = ctx.buffer.line_block_text(0, total)
|
|
2009
|
+
store_yank_register(ctx, text: text, type: :linewise)
|
|
2010
|
+
ctx.editor.echo("#{total} line(s) yanked")
|
|
2011
|
+
end
|
|
2012
|
+
|
|
1726
2013
|
def delete_to_end_of_line(ctx)
|
|
1727
2014
|
y = ctx.window.cursor_y
|
|
1728
2015
|
x = ctx.window.cursor_x
|
|
@@ -1754,22 +2041,22 @@ module RuVim
|
|
|
1754
2041
|
end
|
|
1755
2042
|
|
|
1756
2043
|
def delete_text_object_word(ctx, around:)
|
|
1757
|
-
|
|
1758
|
-
return false unless span
|
|
1759
|
-
|
|
1760
|
-
text = ctx.buffer.span_text(span[:start_row], span[:start_col], span[:end_row], span[:end_col])
|
|
1761
|
-
ctx.buffer.begin_change_group
|
|
1762
|
-
ctx.buffer.delete_span(span[:start_row], span[:start_col], span[:end_row], span[:end_col])
|
|
1763
|
-
ctx.buffer.end_change_group
|
|
1764
|
-
store_delete_register(ctx, text:, type: :charwise) unless text.empty?
|
|
1765
|
-
ctx.window.cursor_y = span[:start_row]
|
|
1766
|
-
ctx.window.cursor_x = span[:start_col]
|
|
1767
|
-
ctx.window.clamp_to_buffer(ctx.buffer)
|
|
1768
|
-
true
|
|
2044
|
+
delete_span(ctx, word_object_span(ctx.buffer, ctx.window, around:))
|
|
1769
2045
|
end
|
|
1770
2046
|
|
|
1771
2047
|
def delete_text_object(ctx, motion)
|
|
1772
|
-
|
|
2048
|
+
delete_span(ctx, text_object_span(ctx.buffer, ctx.window, motion))
|
|
2049
|
+
end
|
|
2050
|
+
|
|
2051
|
+
def yank_text_object_word(ctx, around:)
|
|
2052
|
+
yank_span(ctx, word_object_span(ctx.buffer, ctx.window, around:))
|
|
2053
|
+
end
|
|
2054
|
+
|
|
2055
|
+
def yank_text_object(ctx, motion)
|
|
2056
|
+
yank_span(ctx, text_object_span(ctx.buffer, ctx.window, motion))
|
|
2057
|
+
end
|
|
2058
|
+
|
|
2059
|
+
def delete_span(ctx, span)
|
|
1773
2060
|
return false unless span
|
|
1774
2061
|
|
|
1775
2062
|
text = ctx.buffer.span_text(span[:start_row], span[:start_col], span[:end_row], span[:end_col])
|
|
@@ -1783,18 +2070,7 @@ module RuVim
|
|
|
1783
2070
|
true
|
|
1784
2071
|
end
|
|
1785
2072
|
|
|
1786
|
-
def
|
|
1787
|
-
span = word_object_span(ctx.buffer, ctx.window, around:)
|
|
1788
|
-
return false unless span
|
|
1789
|
-
|
|
1790
|
-
text = ctx.buffer.span_text(span[:start_row], span[:start_col], span[:end_row], span[:end_col])
|
|
1791
|
-
store_yank_register(ctx, text:, type: :charwise) unless text.empty?
|
|
1792
|
-
ctx.editor.echo("yanked")
|
|
1793
|
-
true
|
|
1794
|
-
end
|
|
1795
|
-
|
|
1796
|
-
def yank_text_object(ctx, motion)
|
|
1797
|
-
span = text_object_span(ctx.buffer, ctx.window, motion)
|
|
2073
|
+
def yank_span(ctx, span)
|
|
1798
2074
|
return false unless span
|
|
1799
2075
|
|
|
1800
2076
|
text = ctx.buffer.span_text(span[:start_row], span[:start_col], span[:end_row], span[:end_col])
|
|
@@ -1979,9 +2255,9 @@ module RuVim
|
|
|
1979
2255
|
x = [window.cursor_x, line.length - 1].min
|
|
1980
2256
|
return nil if x.negative?
|
|
1981
2257
|
|
|
1982
|
-
left =
|
|
2258
|
+
left = find_quote(line, x, quote, :left)
|
|
1983
2259
|
right_from = [x, (left ? left + 1 : 0)].max
|
|
1984
|
-
right =
|
|
2260
|
+
right = find_quote(line, right_from, quote, :right)
|
|
1985
2261
|
return nil unless left && right && left < right
|
|
1986
2262
|
|
|
1987
2263
|
if around
|
|
@@ -2053,20 +2329,18 @@ module RuVim
|
|
|
2053
2329
|
end
|
|
2054
2330
|
end
|
|
2055
2331
|
|
|
2056
|
-
def
|
|
2332
|
+
def find_quote(line, x, quote, direction)
|
|
2057
2333
|
i = x
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
return i if line[i] == quote && !escaped?(line, i)
|
|
2069
|
-
i += 1
|
|
2334
|
+
if direction == :left
|
|
2335
|
+
while i >= 0
|
|
2336
|
+
return i if line[i] == quote && !escaped?(line, i)
|
|
2337
|
+
i -= 1
|
|
2338
|
+
end
|
|
2339
|
+
else
|
|
2340
|
+
while i < line.length
|
|
2341
|
+
return i if line[i] == quote && !escaped?(line, i)
|
|
2342
|
+
i += 1
|
|
2343
|
+
end
|
|
2070
2344
|
end
|
|
2071
2345
|
nil
|
|
2072
2346
|
end
|
|
@@ -2811,7 +3085,7 @@ module RuVim
|
|
|
2811
3085
|
operator_pending: 5,
|
|
2812
3086
|
command_line: 6
|
|
2813
3087
|
}
|
|
2814
|
-
order.fetch(mode
|
|
3088
|
+
order.fetch(mode, 99)
|
|
2815
3089
|
end
|
|
2816
3090
|
|
|
2817
3091
|
def sort_binding_entries(entries, sort:)
|
|
@@ -2894,7 +3168,7 @@ module RuVim
|
|
|
2894
3168
|
lhs = format_binding_tokens(entry.tokens)
|
|
2895
3169
|
if entry.scope == :global
|
|
2896
3170
|
"global:#{lhs}"
|
|
2897
|
-
elsif entry.mode && entry.mode
|
|
3171
|
+
elsif entry.mode && entry.mode != :normal
|
|
2898
3172
|
"#{entry.mode}:#{lhs}"
|
|
2899
3173
|
else
|
|
2900
3174
|
lhs
|
|
@@ -2929,7 +3203,7 @@ module RuVim
|
|
|
2929
3203
|
end
|
|
2930
3204
|
|
|
2931
3205
|
def invert_direction(direction)
|
|
2932
|
-
direction
|
|
3206
|
+
direction == :forward ? :backward : :forward
|
|
2933
3207
|
end
|
|
2934
3208
|
|
|
2935
3209
|
def move_to_search(ctx, pattern:, direction:, count:)
|
|
@@ -2950,7 +3224,7 @@ module RuVim
|
|
|
2950
3224
|
def find_next_match(buffer, window, regex, direction:)
|
|
2951
3225
|
return nil unless regex
|
|
2952
3226
|
|
|
2953
|
-
if direction
|
|
3227
|
+
if direction == :backward
|
|
2954
3228
|
find_backward_match(buffer, window, regex)
|
|
2955
3229
|
else
|
|
2956
3230
|
find_forward_match(buffer, window, regex)
|