ruvim 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +4 -0
- data/AGENTS.md +96 -0
- data/CLAUDE.md +1 -0
- data/README.md +15 -1
- data/docs/binding.md +39 -0
- data/docs/command.md +163 -4
- data/docs/config.md +12 -4
- data/docs/done.md +21 -0
- data/docs/spec.md +214 -18
- data/docs/todo.md +1 -5
- data/docs/tutorial.md +24 -0
- data/docs/vim_diff.md +105 -173
- data/lib/ruvim/app.rb +1165 -70
- data/lib/ruvim/buffer.rb +47 -1
- data/lib/ruvim/cli.rb +18 -3
- data/lib/ruvim/clipboard.rb +2 -0
- data/lib/ruvim/command_invocation.rb +3 -1
- data/lib/ruvim/command_line.rb +2 -0
- data/lib/ruvim/command_registry.rb +2 -0
- data/lib/ruvim/config_dsl.rb +2 -0
- data/lib/ruvim/config_loader.rb +2 -0
- data/lib/ruvim/context.rb +2 -0
- data/lib/ruvim/dispatcher.rb +143 -13
- data/lib/ruvim/display_width.rb +3 -0
- data/lib/ruvim/editor.rb +466 -71
- data/lib/ruvim/ex_command_registry.rb +2 -0
- data/lib/ruvim/file_watcher.rb +243 -0
- data/lib/ruvim/git/blame.rb +245 -0
- data/lib/ruvim/git/branch.rb +97 -0
- data/lib/ruvim/git/commit.rb +102 -0
- data/lib/ruvim/git/diff.rb +129 -0
- data/lib/ruvim/git/handler.rb +84 -0
- data/lib/ruvim/git/log.rb +41 -0
- data/lib/ruvim/git/status.rb +103 -0
- data/lib/ruvim/global_commands.rb +1066 -105
- data/lib/ruvim/highlighter.rb +19 -22
- data/lib/ruvim/input.rb +40 -28
- data/lib/ruvim/keymap_manager.rb +83 -0
- data/lib/ruvim/keyword_chars.rb +2 -0
- data/lib/ruvim/lang/base.rb +25 -0
- data/lib/ruvim/lang/csv.rb +18 -0
- data/lib/ruvim/lang/diff.rb +41 -0
- data/lib/ruvim/lang/json.rb +52 -0
- data/lib/ruvim/lang/markdown.rb +170 -0
- data/lib/ruvim/lang/ruby.rb +236 -0
- data/lib/ruvim/lang/scheme.rb +44 -0
- data/lib/ruvim/lang/tsv.rb +19 -0
- data/lib/ruvim/rich_view/json_renderer.rb +131 -0
- data/lib/ruvim/rich_view/jsonl_renderer.rb +57 -0
- data/lib/ruvim/rich_view/markdown_renderer.rb +248 -0
- data/lib/ruvim/rich_view/table_renderer.rb +176 -0
- data/lib/ruvim/rich_view.rb +109 -0
- data/lib/ruvim/screen.rb +503 -109
- data/lib/ruvim/terminal.rb +18 -1
- data/lib/ruvim/text_metrics.rb +2 -0
- data/lib/ruvim/version.rb +1 -1
- data/lib/ruvim/window.rb +2 -0
- data/lib/ruvim.rb +24 -0
- data/test/app_completion_test.rb +98 -0
- data/test/app_dot_repeat_test.rb +13 -0
- data/test/app_motion_test.rb +13 -0
- data/test/app_scenario_test.rb +898 -1
- data/test/app_startup_test.rb +187 -0
- data/test/arglist_test.rb +113 -0
- data/test/buffer_test.rb +49 -30
- data/test/cli_test.rb +14 -0
- data/test/clipboard_test.rb +67 -0
- data/test/command_line_test.rb +118 -0
- data/test/config_dsl_test.rb +87 -0
- data/test/dispatcher_test.rb +322 -0
- data/test/display_width_test.rb +41 -0
- data/test/editor_register_test.rb +23 -0
- data/test/file_watcher_test.rb +197 -0
- data/test/follow_test.rb +199 -0
- data/test/git_blame_test.rb +713 -0
- data/test/highlighter_test.rb +165 -0
- data/test/indent_test.rb +287 -0
- data/test/input_screen_integration_test.rb +40 -2
- data/test/markdown_renderer_test.rb +279 -0
- data/test/on_save_hook_test.rb +150 -0
- data/test/rich_view_test.rb +734 -0
- data/test/screen_test.rb +304 -0
- data/test/search_option_test.rb +19 -0
- data/test/test_helper.rb +9 -0
- metadata +49 -2
|
@@ -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,84 @@
|
|
|
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
|
+
"commit" => :git_commit,
|
|
31
|
+
}.freeze
|
|
32
|
+
|
|
33
|
+
include Blame::HandlerMethods
|
|
34
|
+
include Status::HandlerMethods
|
|
35
|
+
include Diff::HandlerMethods
|
|
36
|
+
include Log::HandlerMethods
|
|
37
|
+
include Branch::HandlerMethods
|
|
38
|
+
include Commit::HandlerMethods
|
|
39
|
+
|
|
40
|
+
def enter_git_command_mode(ctx, **)
|
|
41
|
+
ctx.editor.enter_command_line_mode(":")
|
|
42
|
+
ctx.editor.command_line.replace_text("git ")
|
|
43
|
+
ctx.editor.clear_message
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def ex_git(ctx, argv: [], **)
|
|
47
|
+
sub = argv.first.to_s.downcase
|
|
48
|
+
if sub.empty?
|
|
49
|
+
ctx.editor.echo("Git subcommands: #{GIT_SUBCOMMANDS.keys.join(', ')}")
|
|
50
|
+
return
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
method = GIT_SUBCOMMANDS[sub]
|
|
54
|
+
unless method
|
|
55
|
+
ctx.editor.echo_error("Unknown Git subcommand: #{sub}")
|
|
56
|
+
return
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
public_send(method, ctx, argv: argv[1..], kwargs: {}, bang: false, count: 1)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def git_close_buffer(ctx, **)
|
|
63
|
+
buf_id = ctx.buffer.id
|
|
64
|
+
ctx.editor.git_stream_stop_handler&.call(buf_id)
|
|
65
|
+
ctx.editor.delete_buffer(buf_id)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def git_resolve_path(ctx)
|
|
71
|
+
path = ctx.buffer.path
|
|
72
|
+
return path if path && File.exist?(path)
|
|
73
|
+
dir = Dir.pwd
|
|
74
|
+
File.directory?(dir) ? dir : nil
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def bind_git_buffer_keys(editor, buffer_id)
|
|
78
|
+
km = editor.keymap_manager
|
|
79
|
+
km.bind_buffer(buffer_id, "\e", "git.close_buffer")
|
|
80
|
+
km.bind_buffer(buffer_id, "<C-c>", "git.close_buffer")
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
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
|
|
@@ -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
|