Skip to content

Commit 1af1567

Browse files
authored
feat: move cursor to the nearest hunk (#71)
Adapted from patch in #66
1 parent b87b8c8 commit 1af1567

File tree

3 files changed

+80
-12
lines changed

3 files changed

+80
-12
lines changed

lua/blame/blame_stack.lua

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ local mappings = require("blame.mappings")
1616
---@field file_path string
1717
---@field cwd string
1818
---@field commit_stack Porcelain[]
19+
---@field cursor_stack unknown[]
20+
---@field original_cursor unknown
1921
local BlameStack = {}
2022

2123
---@return BlameStack
@@ -43,6 +45,7 @@ function BlameStack:new(config, blame_view, file_path, cwd)
4345
end)
4446

4547
o.commit_stack = {}
48+
o.cursor_stack = {}
4649

4750
return o
4851
end
@@ -55,7 +58,7 @@ function BlameStack:push(commit)
5558
return
5659
end
5760

58-
self:get_show_file_content(commit, true, function(file_content)
61+
self:get_prev_file_content(commit, function(file_content, line)
5962
self:get_blame_for_commit(commit, true, function(blame_lines)
6063
if self.stack_buffer == nil then
6164
self:create_blame_buf()
@@ -72,9 +75,16 @@ function BlameStack:push(commit)
7275
file_content
7376
)
7477
table.insert(self.commit_stack, commit)
78+
table.insert(self.cursor_stack, vim.api.nvim_win_get_cursor(self.blame_window))
79+
if #self.cursor_stack == 1 then
80+
self.original_cursor = self.cursor_stack[1]
81+
end
7582
self:open_stack_info_float()
7683

7784
self.blame_view:open(blame_lines)
85+
if line ~= nil then
86+
vim.api.nvim_win_set_cursor(self.blame_window, { line, 0 })
87+
end
7888
end)
7989
end, function()
8090
vim.notify(
@@ -93,11 +103,11 @@ function BlameStack:pop()
93103
return
94104
end
95105
table.remove(self.commit_stack, nil)
106+
local cursor = table.remove(self.cursor_stack, nil)
96107
self:open_stack_info_float()
97-
self:get_show_file_content(
108+
self:get_prev_file_content(
98109
self.commit_stack[#self.commit_stack],
99-
true,
100-
function(file_content)
110+
function(file_content, line)
101111
vim.api.nvim_buf_set_lines(
102112
self.stack_buffer,
103113
0,
@@ -112,6 +122,7 @@ function BlameStack:pop()
112122
function(blame_lines)
113123
vim.schedule(function()
114124
self.blame_view:open(blame_lines)
125+
vim.api.nvim_win_set_cursor(self.blame_window, cursor)
115126
end)
116127
end
117128
)
@@ -162,6 +173,7 @@ end
162173

163174
function BlameStack:reset_to_original_buf()
164175
self.commit_stack = {}
176+
self.cursor_stack = {}
165177
vim.api.nvim_set_current_win(self.original_window)
166178
vim.api.nvim_set_current_buf(self.original_buffer)
167179
if self.stack_buffer and vim.api.nvim_buf_is_valid(self.stack_buffer) then
@@ -171,6 +183,9 @@ function BlameStack:reset_to_original_buf()
171183
self:get_blame_for_commit({}, false, function(blame_lines)
172184
vim.schedule(function()
173185
self.blame_view:open(blame_lines)
186+
if self.original_cursor ~= nil then
187+
vim.api.nvim_win_set_cursor(self.blame_window, self.original_cursor)
188+
end
174189
end)
175190
end)
176191
self.stack_buffer = nil
@@ -324,20 +339,35 @@ function BlameStack:get_blame_for_commit(commit, prev, cb, err_cb)
324339
end
325340

326341
---@param commit Porcelain
327-
---@param prev boolean should show previous commit
328-
---@param cb fun(any) callback on show command end
342+
---@param cb fun(any, number?) callback on show command end
329343
---@param err_cb nil | fun(err) callback on error show command
330-
function BlameStack:get_show_file_content(commit, prev, cb, err_cb)
331-
local hash, filepath = self:get_hash_and_filepath(commit, prev)
344+
function BlameStack:get_prev_file_content(commit, cb, err_cb)
345+
local hash, filepath = self:get_hash_and_filepath(commit, true)
332346
self.git_client:show(filepath, self.git_root, hash, function(file_content)
333-
vim.schedule(function()
347+
self.git_client:diff(filepath, self.git_root, hash, commit.hash, function(diff)
334348
-- most of the time empty line is inserted from git-show. Might create issues but for now this crude check works
335349
if file_content[#file_content] == "" then
336350
table.remove(file_content)
337351
end
338352

339-
if cb ~= nil then
340-
cb(file_content)
353+
local line
354+
for _, hunk in ipairs(porcelain_parser.parse_hunks(diff)) do
355+
if hunk.curr_line <= commit.original_line and commit.original_line < hunk.curr_line + hunk.curr_count then
356+
line = hunk.prev_line
357+
break
358+
end
359+
end
360+
361+
vim.schedule(function()
362+
if cb ~= nil then
363+
cb(file_content, line)
364+
end
365+
end)
366+
end, function(err)
367+
if err_cb then
368+
err_cb(err)
369+
else
370+
vim.notify(err, vim.log.levels.INFO)
341371
end
342372
end)
343373
end, function(err)

lua/blame/git.lua

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,4 +87,16 @@ function Git:show(file_path, cwd, commit, callback, err_cb)
8787
end
8888
end
8989

90+
---Execute git blame
91+
---@param file_path string relative file path
92+
---@param cwd any where to execute the command
93+
---@param a_commit string commit hash
94+
---@param b_commit string commit hash
95+
---@param callback fun(diff string[]) callback on exiting command with output string
96+
---@param err_cb nil | fun(err) callback on error
97+
function Git:diff(file_path, cwd, a_commit, b_commit, callback, err_cb)
98+
local diff_command = { "git", "--no-pager", "diff", "--unified=0", a_commit, b_commit, "--", file_path }
99+
execute_command(diff_command, cwd, callback, err_cb)
100+
end
101+
90102
return Git

lua/blame/porcelain_parser.lua

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ local M = {}
1414
---@field previous string|nil
1515
---@field summary string
1616
---@field content string
17+
---@field original_line number
1718

1819
---Parses raw porcelain data (string[]) into an array of tables for each line containing the commit data
1920
---@param blame_porcelain string[]
@@ -25,7 +26,7 @@ M.parse_porcelain = function(blame_porcelain)
2526
if not ident then
2627
all_lines[#all_lines].content = entry
2728
elseif #ident == 40 then
28-
table.insert(all_lines, { hash = ident })
29+
table.insert(all_lines, { hash = ident, original_line = tonumber(entry:match("^%S+ (%d+)")) })
2930
else
3031
ident = ident:gsub("-", "_")
3132

@@ -40,4 +41,29 @@ M.parse_porcelain = function(blame_porcelain)
4041
return all_lines
4142
end
4243

44+
---@class Hunk
45+
---@field prev_line number
46+
---@field prev_count number
47+
---@field curr_line number
48+
---@field curr_count number
49+
50+
---Parses git diff hunks to extract line number mappings
51+
---@param diff string[]
52+
---@return Hunk[]
53+
M.parse_hunks = function(diff)
54+
local hunks = {}
55+
for _, line in ipairs(diff) do
56+
local prev_line, prev_lines, curr_line, curr_lines = line:match("^@@ %-(%d+),?(%d*) %+(%d+),?(%d*)")
57+
if prev_line then
58+
table.insert(hunks, {
59+
prev_line = tonumber(prev_line),
60+
prev_count = tonumber(prev_lines) or 1,
61+
curr_line = tonumber(curr_line),
62+
curr_count = tonumber(curr_lines) or 1
63+
})
64+
end
65+
end
66+
return hunks
67+
end
68+
4369
return M

0 commit comments

Comments
 (0)