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,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
|
|
5
|
+
module RuVim
|
|
6
|
+
module Git
|
|
7
|
+
module Branch
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
# Run git branch listing sorted by most recent commit.
|
|
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(
|
|
17
|
+
"git", "branch", "-a",
|
|
18
|
+
"--sort=-committerdate",
|
|
19
|
+
"--format=%(if)%(HEAD)%(then)* %(else) %(end)%(refname:short)\t%(committerdate:short)\t%(subject)",
|
|
20
|
+
chdir: root
|
|
21
|
+
)
|
|
22
|
+
unless status.success?
|
|
23
|
+
return [nil, nil, err.strip]
|
|
24
|
+
end
|
|
25
|
+
[out.lines(chomp: true), root, nil]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Parse branch name from a branch list line.
|
|
29
|
+
# Returns branch name or nil.
|
|
30
|
+
def parse_branch_name(line)
|
|
31
|
+
stripped = line.to_s.strip
|
|
32
|
+
return nil if stripped.empty?
|
|
33
|
+
|
|
34
|
+
# Remove leading "* " marker for current branch
|
|
35
|
+
name = stripped.sub(/\A\*\s*/, "")
|
|
36
|
+
# Branch name is everything before the first tab
|
|
37
|
+
name = name.split("\t", 2).first
|
|
38
|
+
name&.strip
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Command handler methods
|
|
42
|
+
module HandlerMethods
|
|
43
|
+
def git_branch(ctx, **)
|
|
44
|
+
file_path = git_resolve_path(ctx)
|
|
45
|
+
unless file_path
|
|
46
|
+
ctx.editor.echo_error("No file or directory to resolve git repo")
|
|
47
|
+
return
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
lines, root, err = Branch.run(file_path)
|
|
51
|
+
unless lines
|
|
52
|
+
ctx.editor.echo_error("git branch: #{err}")
|
|
53
|
+
return
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
buf = ctx.editor.add_virtual_buffer(
|
|
57
|
+
kind: :git_branch,
|
|
58
|
+
name: "[Git Branch]",
|
|
59
|
+
lines: lines,
|
|
60
|
+
readonly: true,
|
|
61
|
+
modifiable: false
|
|
62
|
+
)
|
|
63
|
+
buf.options["git_repo_root"] = root
|
|
64
|
+
ctx.editor.switch_to_buffer(buf.id)
|
|
65
|
+
bind_git_buffer_keys(ctx.editor, buf.id)
|
|
66
|
+
ctx.editor.echo("[Git Branch]")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def git_branch_checkout(ctx, **)
|
|
70
|
+
buf = ctx.buffer
|
|
71
|
+
unless buf.kind == :git_branch
|
|
72
|
+
ctx.editor.echo_error("Not a git branch buffer")
|
|
73
|
+
return
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
line = buf.line_at(ctx.window.cursor_y)
|
|
77
|
+
branch = Branch.parse_branch_name(line)
|
|
78
|
+
unless branch
|
|
79
|
+
ctx.editor.echo_error("No branch on this line")
|
|
80
|
+
return
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
ctx.editor.enter_command_line_mode(":")
|
|
84
|
+
ctx.editor.command_line.replace_text("git checkout #{branch}")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def git_branch_execute_checkout(ctx, argv: [], **)
|
|
88
|
+
branch = argv.first.to_s.strip
|
|
89
|
+
raise RuVim::CommandError, "Usage: :git checkout <branch>" if branch.empty?
|
|
90
|
+
|
|
91
|
+
root = ctx.buffer.kind == :git_branch ? ctx.buffer.options["git_repo_root"] : nil
|
|
92
|
+
unless root
|
|
93
|
+
file_path = git_resolve_path(ctx)
|
|
94
|
+
root, err = Git.repo_root(file_path) if file_path
|
|
95
|
+
raise RuVim::CommandError, "Not in a git repository" unless root
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
_out, err, status = Open3.capture3("git", "checkout", branch, chdir: root)
|
|
99
|
+
unless status.success?
|
|
100
|
+
raise RuVim::CommandError, "git checkout: #{err.strip}"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Close git branch buffer if we're in one
|
|
104
|
+
if ctx.buffer.kind == :git_branch
|
|
105
|
+
ctx.editor.delete_buffer(ctx.buffer.id)
|
|
106
|
+
end
|
|
107
|
+
ctx.editor.echo("Switched to branch '#{branch}'")
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
|
|
5
|
+
module RuVim
|
|
6
|
+
module Git
|
|
7
|
+
module Commit
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
# Build the initial content for the commit message buffer.
|
|
11
|
+
# Returns [lines, root, error_message].
|
|
12
|
+
def prepare(file_path)
|
|
13
|
+
root, err = Git.repo_root(file_path)
|
|
14
|
+
return [nil, nil, err] unless root
|
|
15
|
+
|
|
16
|
+
status_out, err, status = Open3.capture3("git", "status", chdir: root)
|
|
17
|
+
unless status.success?
|
|
18
|
+
return [nil, nil, err.strip]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
lines = [""]
|
|
22
|
+
lines << "# Enter commit message above. Lines starting with '#' are ignored."
|
|
23
|
+
lines << "# Close with :wq to commit, :q! to cancel."
|
|
24
|
+
lines << "#"
|
|
25
|
+
status_out.each_line(chomp: true) { |l| lines << "# #{l}" }
|
|
26
|
+
[lines, root, nil]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Extract commit message from buffer lines (skip # comment lines and trim).
|
|
30
|
+
def extract_message(lines)
|
|
31
|
+
msg_lines = lines.reject { |l| l.start_with?("#") }
|
|
32
|
+
# Strip trailing blank lines
|
|
33
|
+
msg_lines.pop while msg_lines.last&.empty?
|
|
34
|
+
msg_lines.join("\n")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Execute git commit with the given message.
|
|
38
|
+
# Returns [success, output_or_error].
|
|
39
|
+
def execute(root, message)
|
|
40
|
+
out, err, status = Open3.capture3("git", "commit", "-m", message, chdir: root)
|
|
41
|
+
if status.success?
|
|
42
|
+
[true, out.strip]
|
|
43
|
+
else
|
|
44
|
+
[false, err.strip]
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Command handler methods
|
|
49
|
+
module HandlerMethods
|
|
50
|
+
def git_commit(ctx, **)
|
|
51
|
+
file_path = git_resolve_path(ctx)
|
|
52
|
+
unless file_path
|
|
53
|
+
ctx.editor.echo_error("No file or directory to resolve git repo")
|
|
54
|
+
return
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
lines, root, err = Commit.prepare(file_path)
|
|
58
|
+
unless lines
|
|
59
|
+
ctx.editor.echo_error("git commit: #{err}")
|
|
60
|
+
return
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
buf = ctx.editor.add_virtual_buffer(
|
|
64
|
+
kind: :git_commit,
|
|
65
|
+
name: "[Commit Message]",
|
|
66
|
+
lines: lines,
|
|
67
|
+
readonly: false,
|
|
68
|
+
modifiable: true
|
|
69
|
+
)
|
|
70
|
+
buf.options["git_repo_root"] = root
|
|
71
|
+
ctx.editor.switch_to_buffer(buf.id)
|
|
72
|
+
ctx.editor.enter_insert_mode
|
|
73
|
+
ctx.editor.echo("[Commit Message] :wq to commit, :q! to cancel")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def git_commit_execute(ctx, **)
|
|
77
|
+
buf = ctx.buffer
|
|
78
|
+
unless buf.kind == :git_commit
|
|
79
|
+
ctx.editor.echo_error("Not a git commit buffer")
|
|
80
|
+
return
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
message = Commit.extract_message(buf.lines)
|
|
84
|
+
if message.empty?
|
|
85
|
+
ctx.editor.echo_error("Empty commit message, aborting")
|
|
86
|
+
return
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
root = buf.options["git_repo_root"]
|
|
90
|
+
success, output = Commit.execute(root, message)
|
|
91
|
+
ctx.editor.delete_buffer(buf.id)
|
|
92
|
+
|
|
93
|
+
if success
|
|
94
|
+
ctx.editor.echo(output)
|
|
95
|
+
else
|
|
96
|
+
ctx.editor.echo_error("git commit: #{output}")
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
|
|
5
|
+
module RuVim
|
|
6
|
+
module Git
|
|
7
|
+
module Diff
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
# Run git diff with optional extra args.
|
|
11
|
+
# Returns [lines, root, error_message].
|
|
12
|
+
def run(file_path, args: [])
|
|
13
|
+
root, err = Git.repo_root(file_path)
|
|
14
|
+
return [nil, nil, err] unless root
|
|
15
|
+
|
|
16
|
+
cmd = ["git", "diff", *args]
|
|
17
|
+
out, err, status = Open3.capture3(*cmd, chdir: root)
|
|
18
|
+
unless status.success?
|
|
19
|
+
return [nil, nil, err.strip]
|
|
20
|
+
end
|
|
21
|
+
[out.lines(chomp: true), root, nil]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Parse diff output to find file and line number at cursor_y.
|
|
25
|
+
# Returns [filename, line_number] or nil.
|
|
26
|
+
def parse_location(lines, cursor_y)
|
|
27
|
+
return nil if lines.empty? || cursor_y < 0 || cursor_y >= lines.length
|
|
28
|
+
|
|
29
|
+
current_file = nil
|
|
30
|
+
new_line = nil
|
|
31
|
+
|
|
32
|
+
(0..cursor_y).each do |i|
|
|
33
|
+
l = lines[i]
|
|
34
|
+
case l
|
|
35
|
+
when /\Adiff --git a\/.+ b\/(.+)/
|
|
36
|
+
current_file = $1
|
|
37
|
+
when /\A\+\+\+ b\/(.+)/
|
|
38
|
+
current_file = $1
|
|
39
|
+
when /\A@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/
|
|
40
|
+
new_line = $1.to_i
|
|
41
|
+
when /\A[ +]/
|
|
42
|
+
# Context or added line: current new_line is this line's number
|
|
43
|
+
new_line += 1 if new_line && i < cursor_y
|
|
44
|
+
end
|
|
45
|
+
# Deleted lines ("-") don't advance new_line
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
return nil unless current_file
|
|
49
|
+
|
|
50
|
+
l = lines[cursor_y]
|
|
51
|
+
case l
|
|
52
|
+
when /\A@@ /
|
|
53
|
+
# On hunk header: new_line already set
|
|
54
|
+
when /\Adiff --git /, /\A---/, /\A\+\+\+/, /\Aindex /
|
|
55
|
+
new_line ||= 1
|
|
56
|
+
when /\A-/
|
|
57
|
+
# Deleted line: point to current new-side position
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
[current_file, new_line || 1]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Command handler methods
|
|
64
|
+
module HandlerMethods
|
|
65
|
+
def git_diff(ctx, argv: [], **)
|
|
66
|
+
file_path = git_resolve_path(ctx)
|
|
67
|
+
unless file_path
|
|
68
|
+
ctx.editor.echo_error("No file or directory to resolve git repo")
|
|
69
|
+
return
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
lines, root, err = Diff.run(file_path, args: argv)
|
|
73
|
+
unless lines
|
|
74
|
+
ctx.editor.echo_error("git diff: #{err}")
|
|
75
|
+
return
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
if lines.empty?
|
|
79
|
+
ctx.editor.echo("No diff output (working tree clean)")
|
|
80
|
+
return
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
buf = ctx.editor.add_virtual_buffer(
|
|
84
|
+
kind: :git_diff,
|
|
85
|
+
name: "[Git Diff]",
|
|
86
|
+
lines: lines,
|
|
87
|
+
filetype: "diff",
|
|
88
|
+
readonly: true,
|
|
89
|
+
modifiable: false
|
|
90
|
+
)
|
|
91
|
+
buf.options["git_repo_root"] = root
|
|
92
|
+
ctx.editor.switch_to_buffer(buf.id)
|
|
93
|
+
bind_git_buffer_keys(ctx.editor, buf.id)
|
|
94
|
+
ctx.editor.echo("[Git Diff]")
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def git_diff_open_file(ctx, **)
|
|
98
|
+
buf = ctx.buffer
|
|
99
|
+
unless buf.kind == :git_diff || buf.kind == :git_log
|
|
100
|
+
ctx.editor.echo_error("Not a git diff buffer")
|
|
101
|
+
return
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
filename, line_num = Diff.parse_location(buf.lines, ctx.window.cursor_y)
|
|
105
|
+
unless filename
|
|
106
|
+
ctx.editor.echo_error("No file on this line")
|
|
107
|
+
return
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
root = buf.options["git_repo_root"]
|
|
111
|
+
full_path = File.join(root, filename)
|
|
112
|
+
unless File.exist?(full_path)
|
|
113
|
+
ctx.editor.echo_error("File not found: #{filename}")
|
|
114
|
+
return
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
existing = ctx.editor.buffers.values.find { |b| b.path == full_path }
|
|
118
|
+
if existing
|
|
119
|
+
ctx.editor.switch_to_buffer(existing.id)
|
|
120
|
+
else
|
|
121
|
+
new_buf = ctx.editor.add_buffer_from_file(full_path)
|
|
122
|
+
ctx.editor.switch_to_buffer(new_buf.id)
|
|
123
|
+
end
|
|
124
|
+
ctx.editor.current_window.cursor_y = [line_num - 1, 0].max
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
|
|
5
|
+
module RuVim
|
|
6
|
+
module Git
|
|
7
|
+
module Grep
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
# Run git grep -n with extra args.
|
|
11
|
+
# Returns [lines, root, error_message].
|
|
12
|
+
def run(file_path, args: [])
|
|
13
|
+
root, err = Git.repo_root(file_path)
|
|
14
|
+
return [nil, nil, err] unless root
|
|
15
|
+
|
|
16
|
+
cmd = ["git", "grep", "-n", *args]
|
|
17
|
+
out, err, status = Open3.capture3(*cmd, chdir: root)
|
|
18
|
+
# git grep exits 1 when no matches found (not an error)
|
|
19
|
+
unless status.success? || status.exitstatus == 1
|
|
20
|
+
return [nil, nil, err.strip]
|
|
21
|
+
end
|
|
22
|
+
[out.lines(chomp: true), root, nil]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Parse a git grep output line (file:line:content).
|
|
26
|
+
# Returns [filename, line_number] or nil.
|
|
27
|
+
def parse_location(line)
|
|
28
|
+
return nil if line.empty? || line == "--"
|
|
29
|
+
|
|
30
|
+
m = line.match(/\A(.+?):(\d+):/)
|
|
31
|
+
return nil unless m
|
|
32
|
+
|
|
33
|
+
[m[1], m[2].to_i]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
module HandlerMethods
|
|
37
|
+
def git_grep(ctx, argv: [], **)
|
|
38
|
+
if argv.empty?
|
|
39
|
+
ctx.editor.echo_error("Usage: :git grep <pattern> [<args>...]")
|
|
40
|
+
return
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
file_path = git_resolve_path(ctx)
|
|
44
|
+
unless file_path
|
|
45
|
+
ctx.editor.echo_error("No file or directory to resolve git repo")
|
|
46
|
+
return
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
lines, root, err = Grep.run(file_path, args: argv)
|
|
50
|
+
unless lines
|
|
51
|
+
ctx.editor.echo_error("git grep: #{err}")
|
|
52
|
+
return
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
if lines.empty?
|
|
56
|
+
ctx.editor.echo("No matches found")
|
|
57
|
+
return
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
buf = ctx.editor.add_virtual_buffer(
|
|
61
|
+
kind: :git_grep,
|
|
62
|
+
name: "[Git Grep]",
|
|
63
|
+
lines: lines,
|
|
64
|
+
readonly: true,
|
|
65
|
+
modifiable: false
|
|
66
|
+
)
|
|
67
|
+
buf.options["git_repo_root"] = root
|
|
68
|
+
ctx.editor.switch_to_buffer(buf.id)
|
|
69
|
+
bind_git_buffer_keys(ctx.editor, buf.id)
|
|
70
|
+
ctx.editor.echo("[Git Grep] #{lines.length} match(es)")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def git_grep_open_file(ctx, **)
|
|
74
|
+
buf = ctx.buffer
|
|
75
|
+
unless buf.kind == :git_grep
|
|
76
|
+
ctx.editor.echo_error("Not a git grep buffer")
|
|
77
|
+
return
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
line = buf.lines[ctx.window.cursor_y]
|
|
81
|
+
location = Grep.parse_location(line)
|
|
82
|
+
unless location
|
|
83
|
+
ctx.editor.echo_error("No file on this line")
|
|
84
|
+
return
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
filename, line_num = location
|
|
88
|
+
root = buf.options["git_repo_root"]
|
|
89
|
+
full_path = File.join(root, filename)
|
|
90
|
+
unless File.exist?(full_path)
|
|
91
|
+
ctx.editor.echo_error("File not found: #{filename}")
|
|
92
|
+
return
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
existing = ctx.editor.buffers.values.find { |b| b.path == full_path }
|
|
96
|
+
if existing
|
|
97
|
+
ctx.editor.switch_to_buffer(existing.id)
|
|
98
|
+
else
|
|
99
|
+
new_buf = ctx.editor.add_buffer_from_file(full_path)
|
|
100
|
+
ctx.editor.switch_to_buffer(new_buf.id)
|
|
101
|
+
end
|
|
102
|
+
ctx.editor.current_window.cursor_y = [line_num - 1, 0].max
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
|
|
5
|
+
module RuVim
|
|
6
|
+
module Git
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
# Find git repository root from a file path.
|
|
10
|
+
# Returns [root_path, error_message].
|
|
11
|
+
def repo_root(file_path)
|
|
12
|
+
dir = File.directory?(file_path) ? file_path : File.dirname(file_path)
|
|
13
|
+
out, err, status = Open3.capture3("git", "rev-parse", "--show-toplevel", chdir: dir)
|
|
14
|
+
unless status.success?
|
|
15
|
+
return [nil, err.strip]
|
|
16
|
+
end
|
|
17
|
+
[out.strip, nil]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
module Handler
|
|
21
|
+
GIT_SUBCOMMANDS = {
|
|
22
|
+
"blame" => :git_blame,
|
|
23
|
+
"blameprev" => :git_blame_prev,
|
|
24
|
+
"blameback" => :git_blame_back,
|
|
25
|
+
"blamecommit" => :git_blame_commit,
|
|
26
|
+
"status" => :git_status,
|
|
27
|
+
"diff" => :git_diff,
|
|
28
|
+
"log" => :git_log,
|
|
29
|
+
"branch" => :git_branch,
|
|
30
|
+
"checkout" => :git_branch_execute_checkout,
|
|
31
|
+
"commit" => :git_commit,
|
|
32
|
+
"grep" => :git_grep,
|
|
33
|
+
}.freeze
|
|
34
|
+
|
|
35
|
+
include Blame::HandlerMethods
|
|
36
|
+
include Status::HandlerMethods
|
|
37
|
+
include Diff::HandlerMethods
|
|
38
|
+
include Log::HandlerMethods
|
|
39
|
+
include Branch::HandlerMethods
|
|
40
|
+
include Commit::HandlerMethods
|
|
41
|
+
include Grep::HandlerMethods
|
|
42
|
+
include Gh::Link::HandlerMethods
|
|
43
|
+
|
|
44
|
+
GH_SUBCOMMANDS = {
|
|
45
|
+
"link" => :gh_link,
|
|
46
|
+
"browse" => :gh_browse,
|
|
47
|
+
"pr" => :gh_pr,
|
|
48
|
+
}.freeze
|
|
49
|
+
|
|
50
|
+
def enter_git_command_mode(ctx, **)
|
|
51
|
+
ctx.editor.enter_command_line_mode(":")
|
|
52
|
+
ctx.editor.command_line.replace_text("git ")
|
|
53
|
+
ctx.editor.clear_message
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def ex_git(ctx, argv: [], **)
|
|
57
|
+
raise RuVim::CommandError, "Restricted mode: :git is disabled" if ctx.editor.respond_to?(:restricted_mode?) && ctx.editor.restricted_mode?
|
|
58
|
+
|
|
59
|
+
sub = argv.first.to_s.downcase
|
|
60
|
+
if sub.empty?
|
|
61
|
+
ctx.editor.echo("Git subcommands: #{GIT_SUBCOMMANDS.keys.join(', ')}")
|
|
62
|
+
return
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
method = GIT_SUBCOMMANDS[sub]
|
|
66
|
+
unless method
|
|
67
|
+
run_shell_fallback(ctx, "git", argv)
|
|
68
|
+
return
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
public_send(method, ctx, argv: argv[1..], kwargs: {}, bang: false, count: 1)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def ex_gh(ctx, argv: [], kwargs: {}, **)
|
|
75
|
+
raise RuVim::CommandError, "Restricted mode: :gh is disabled" if ctx.editor.respond_to?(:restricted_mode?) && ctx.editor.restricted_mode?
|
|
76
|
+
|
|
77
|
+
sub = argv.first.to_s.downcase
|
|
78
|
+
if sub.empty?
|
|
79
|
+
ctx.editor.echo("GitHub subcommands: #{GH_SUBCOMMANDS.keys.join(', ')}")
|
|
80
|
+
return
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
method = GH_SUBCOMMANDS[sub]
|
|
84
|
+
unless method
|
|
85
|
+
run_shell_fallback(ctx, "gh", argv)
|
|
86
|
+
return
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
public_send(method, ctx, argv: argv[1..], kwargs: kwargs, bang: false, count: 1)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def git_close_buffer(ctx, **)
|
|
93
|
+
buf_id = ctx.buffer.id
|
|
94
|
+
ctx.editor.git_stream_stop_handler&.call(buf_id)
|
|
95
|
+
ctx.editor.delete_buffer(buf_id)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
def run_shell_fallback(ctx, cmd, argv)
|
|
101
|
+
command = ([cmd] + argv).join(" ")
|
|
102
|
+
executor = ctx.editor.shell_executor
|
|
103
|
+
if executor
|
|
104
|
+
status = executor.call(command)
|
|
105
|
+
ctx.editor.echo("shell exit #{status.exitstatus}")
|
|
106
|
+
else
|
|
107
|
+
ctx.editor.echo_error("Unknown #{cmd} subcommand: #{argv.first}")
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def git_resolve_path(ctx)
|
|
112
|
+
path = ctx.buffer.path
|
|
113
|
+
return path if path && File.exist?(path)
|
|
114
|
+
dir = Dir.pwd
|
|
115
|
+
File.directory?(dir) ? dir : nil
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def bind_git_buffer_keys(editor, buffer_id)
|
|
119
|
+
km = editor.keymap_manager
|
|
120
|
+
km.bind_buffer(buffer_id, "\e", "git.close_buffer")
|
|
121
|
+
km.bind_buffer(buffer_id, "<C-c>", "git.close_buffer")
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuVim
|
|
4
|
+
module Git
|
|
5
|
+
module Log
|
|
6
|
+
# Command handler methods
|
|
7
|
+
module HandlerMethods
|
|
8
|
+
def git_log(ctx, argv: [], **)
|
|
9
|
+
file_path = git_resolve_path(ctx)
|
|
10
|
+
unless file_path
|
|
11
|
+
ctx.editor.echo_error("No file or directory to resolve git repo")
|
|
12
|
+
return
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
root, err = Git.repo_root(file_path)
|
|
16
|
+
unless root
|
|
17
|
+
ctx.editor.echo_error("git log: #{err}")
|
|
18
|
+
return
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
filetype = argv.include?("-p") ? "diff" : nil
|
|
22
|
+
buf = ctx.editor.add_virtual_buffer(
|
|
23
|
+
kind: :git_log,
|
|
24
|
+
name: "[Git Log]",
|
|
25
|
+
lines: [""],
|
|
26
|
+
filetype: filetype,
|
|
27
|
+
readonly: true,
|
|
28
|
+
modifiable: false
|
|
29
|
+
)
|
|
30
|
+
buf.options["git_repo_root"] = root
|
|
31
|
+
ctx.editor.switch_to_buffer(buf.id)
|
|
32
|
+
bind_git_buffer_keys(ctx.editor, buf.id)
|
|
33
|
+
ctx.editor.echo("[Git Log] loading...")
|
|
34
|
+
|
|
35
|
+
cmd = ["git", "log", *argv]
|
|
36
|
+
ctx.editor.git_stream_handler&.call(buf.id, cmd, root)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|