Merge pull request #70 from barrettruth/feat/hunk-line-position
feat(fugitive): line position tracking for keymaps
This commit is contained in:
commit
0e6871b167
5 changed files with 247 additions and 8 deletions
|
|
@ -69,4 +69,5 @@ luarocks install diffs.nvim
|
||||||
- [`vim-fugitive`](https://github.com/tpope/vim-fugitive)
|
- [`vim-fugitive`](https://github.com/tpope/vim-fugitive)
|
||||||
- [`codediff.nvim`](https://github.com/esmuellert/codediff.nvim)
|
- [`codediff.nvim`](https://github.com/esmuellert/codediff.nvim)
|
||||||
- [`diffview.nvim`](https://github.com/sindrets/diffview.nvim)
|
- [`diffview.nvim`](https://github.com/sindrets/diffview.nvim)
|
||||||
- [@phanen](https://github.com/phanen) - diff header highlighting
|
- [@phanen](https://github.com/phanen) - diff header highlighting, unknown
|
||||||
|
filetype fix, shebang/modeline detection
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,18 @@ local M = {}
|
||||||
local git = require('diffs.git')
|
local git = require('diffs.git')
|
||||||
local dbg = require('diffs.log').dbg
|
local dbg = require('diffs.log').dbg
|
||||||
|
|
||||||
|
---@param diff_lines string[]
|
||||||
|
---@param hunk_position { hunk_header: string, offset: integer }
|
||||||
|
---@return integer?
|
||||||
|
function M.find_hunk_line(diff_lines, hunk_position)
|
||||||
|
for i, line in ipairs(diff_lines) do
|
||||||
|
if line == hunk_position.hunk_header then
|
||||||
|
return i + hunk_position.offset
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
---@param old_lines string[]
|
---@param old_lines string[]
|
||||||
---@param new_lines string[]
|
---@param new_lines string[]
|
||||||
---@param old_name string
|
---@param old_name string
|
||||||
|
|
@ -98,6 +110,7 @@ end
|
||||||
---@field staged? boolean
|
---@field staged? boolean
|
||||||
---@field untracked? boolean
|
---@field untracked? boolean
|
||||||
---@field old_filepath? string
|
---@field old_filepath? string
|
||||||
|
---@field hunk_position? { hunk_header: string, offset: integer }
|
||||||
|
|
||||||
---@param filepath string
|
---@param filepath string
|
||||||
---@param opts? diffs.GdiffFileOpts
|
---@param opts? diffs.GdiffFileOpts
|
||||||
|
|
@ -175,6 +188,14 @@ function M.gdiff_file(filepath, opts)
|
||||||
vim.cmd(opts.vertical and 'vsplit' or 'split')
|
vim.cmd(opts.vertical and 'vsplit' or 'split')
|
||||||
vim.api.nvim_win_set_buf(0, diff_buf)
|
vim.api.nvim_win_set_buf(0, diff_buf)
|
||||||
|
|
||||||
|
if opts.hunk_position then
|
||||||
|
local target_line = M.find_hunk_line(diff_lines, opts.hunk_position)
|
||||||
|
if target_line then
|
||||||
|
vim.api.nvim_win_set_cursor(0, { target_line, 0 })
|
||||||
|
dbg('jumped to line %d for hunk', target_line)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
dbg('opened diff buffer %d for %s (%s)', diff_buf, rel_path, diff_label)
|
dbg('opened diff buffer %d for %s (%s)', diff_buf, rel_path, diff_label)
|
||||||
|
|
||||||
vim.schedule(function()
|
vim.schedule(function()
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,42 @@ function M.get_file_at_line(bufnr, lnum)
|
||||||
return nil, nil, false, nil
|
return nil, nil, false, nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@class diffs.HunkPosition
|
||||||
|
---@field hunk_header string
|
||||||
|
---@field offset integer
|
||||||
|
|
||||||
|
---@param bufnr integer
|
||||||
|
---@param lnum integer
|
||||||
|
---@return diffs.HunkPosition?
|
||||||
|
function M.get_hunk_position(bufnr, lnum)
|
||||||
|
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, lnum, false)
|
||||||
|
local current = lines[lnum]
|
||||||
|
|
||||||
|
if not current then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local prefix = current:sub(1, 1)
|
||||||
|
if prefix ~= '+' and prefix ~= '-' and prefix ~= ' ' then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
for i = lnum - 1, 1, -1 do
|
||||||
|
local line = lines[i]
|
||||||
|
if line:match('^@@.-@@') then
|
||||||
|
return {
|
||||||
|
hunk_header = line,
|
||||||
|
offset = lnum - i,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
if line:match('^[MADRCU?!]%s') or line:match('^%w+ %(') then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
---@param bufnr integer
|
---@param bufnr integer
|
||||||
---@return string?
|
---@return string?
|
||||||
local function get_repo_root_from_fugitive(bufnr)
|
local function get_repo_root_from_fugitive(bufnr)
|
||||||
|
|
@ -142,12 +178,14 @@ function M.diff_file_under_cursor(vertical)
|
||||||
|
|
||||||
local filepath = repo_root .. '/' .. filename
|
local filepath = repo_root .. '/' .. filename
|
||||||
local old_filepath = old_filename and (repo_root .. '/' .. old_filename) or nil
|
local old_filepath = old_filename and (repo_root .. '/' .. old_filename) or nil
|
||||||
|
local hunk_position = M.get_hunk_position(bufnr, lnum)
|
||||||
|
|
||||||
dbg(
|
dbg(
|
||||||
'diff_file_under_cursor: %s (section: %s, old: %s)',
|
'diff_file_under_cursor: %s (section: %s, old: %s, hunk_offset: %s)',
|
||||||
filename,
|
filename,
|
||||||
section or 'unknown',
|
section or 'unknown',
|
||||||
old_filename or 'none'
|
old_filename or 'none',
|
||||||
|
hunk_position and tostring(hunk_position.offset) or 'none'
|
||||||
)
|
)
|
||||||
|
|
||||||
commands.gdiff_file(filepath, {
|
commands.gdiff_file(filepath, {
|
||||||
|
|
@ -155,6 +193,7 @@ function M.diff_file_under_cursor(vertical)
|
||||||
staged = section == 'staged',
|
staged = section == 'staged',
|
||||||
untracked = section == 'untracked',
|
untracked = section == 'untracked',
|
||||||
old_filepath = old_filepath,
|
old_filepath = old_filepath,
|
||||||
|
hunk_position = hunk_position,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
require('spec.helpers')
|
require('spec.helpers')
|
||||||
|
|
||||||
|
local commands = require('diffs.commands')
|
||||||
|
|
||||||
describe('commands', function()
|
describe('commands', function()
|
||||||
describe('setup', function()
|
describe('setup', function()
|
||||||
it('registers Gdiff, Gvdiff, and Ghdiff commands', function()
|
it('registers Gdiff, Gvdiff, and Ghdiff commands', function()
|
||||||
require('diffs.commands').setup()
|
commands.setup()
|
||||||
local commands = vim.api.nvim_get_commands({})
|
local cmds = vim.api.nvim_get_commands({})
|
||||||
assert.is_not_nil(commands.Gdiff)
|
assert.is_not_nil(cmds.Gdiff)
|
||||||
assert.is_not_nil(commands.Gvdiff)
|
assert.is_not_nil(cmds.Gvdiff)
|
||||||
assert.is_not_nil(commands.Ghdiff)
|
assert.is_not_nil(cmds.Ghdiff)
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|
@ -37,4 +39,60 @@ describe('commands', function()
|
||||||
assert.are.equal('', diff_output)
|
assert.are.equal('', diff_output)
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
describe('find_hunk_line', function()
|
||||||
|
it('finds matching @@ header and returns target line', function()
|
||||||
|
local diff_lines = {
|
||||||
|
'diff --git a/file.lua b/file.lua',
|
||||||
|
'--- a/file.lua',
|
||||||
|
'+++ b/file.lua',
|
||||||
|
'@@ -1,3 +1,4 @@',
|
||||||
|
' local M = {}',
|
||||||
|
'+local new = true',
|
||||||
|
' return M',
|
||||||
|
}
|
||||||
|
local hunk_position = {
|
||||||
|
hunk_header = '@@ -1,3 +1,4 @@',
|
||||||
|
offset = 2,
|
||||||
|
}
|
||||||
|
local target_line = commands.find_hunk_line(diff_lines, hunk_position)
|
||||||
|
assert.equals(6, target_line)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('returns nil when hunk header not found', function()
|
||||||
|
local diff_lines = {
|
||||||
|
'diff --git a/file.lua b/file.lua',
|
||||||
|
'@@ -1,3 +1,4 @@',
|
||||||
|
' local M = {}',
|
||||||
|
}
|
||||||
|
local hunk_position = {
|
||||||
|
hunk_header = '@@ -99,3 +99,4 @@',
|
||||||
|
offset = 1,
|
||||||
|
}
|
||||||
|
local target_line = commands.find_hunk_line(diff_lines, hunk_position)
|
||||||
|
assert.is_nil(target_line)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('handles multiple hunks and finds correct one', function()
|
||||||
|
local diff_lines = {
|
||||||
|
'diff --git a/file.lua b/file.lua',
|
||||||
|
'--- a/file.lua',
|
||||||
|
'+++ b/file.lua',
|
||||||
|
'@@ -1,3 +1,4 @@',
|
||||||
|
' local M = {}',
|
||||||
|
'+local x = 1',
|
||||||
|
' ',
|
||||||
|
'@@ -10,3 +11,4 @@',
|
||||||
|
' function M.foo()',
|
||||||
|
'+ print("hello")',
|
||||||
|
' end',
|
||||||
|
}
|
||||||
|
local hunk_position = {
|
||||||
|
hunk_header = '@@ -10,3 +11,4 @@',
|
||||||
|
offset = 2,
|
||||||
|
}
|
||||||
|
local target_line = commands.find_hunk_line(diff_lines, hunk_position)
|
||||||
|
assert.equals(10, target_line)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
|
||||||
|
|
@ -357,4 +357,124 @@ describe('fugitive', function()
|
||||||
vim.api.nvim_buf_delete(buf, { force = true })
|
vim.api.nvim_buf_delete(buf, { force = true })
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
describe('get_hunk_position', function()
|
||||||
|
local function create_status_buffer(lines)
|
||||||
|
local buf = vim.api.nvim_create_buf(false, true)
|
||||||
|
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
|
||||||
|
return buf
|
||||||
|
end
|
||||||
|
|
||||||
|
it('returns nil when on file header line', function()
|
||||||
|
local buf = create_status_buffer({
|
||||||
|
'Unstaged (1)',
|
||||||
|
'M file.lua',
|
||||||
|
'@@ -1,3 +1,4 @@',
|
||||||
|
' local M = {}',
|
||||||
|
'+local new = true',
|
||||||
|
})
|
||||||
|
local pos = fugitive.get_hunk_position(buf, 2)
|
||||||
|
assert.is_nil(pos)
|
||||||
|
vim.api.nvim_buf_delete(buf, { force = true })
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('returns nil when on @@ header line', function()
|
||||||
|
local buf = create_status_buffer({
|
||||||
|
'Unstaged (1)',
|
||||||
|
'M file.lua',
|
||||||
|
'@@ -1,3 +1,4 @@',
|
||||||
|
' local M = {}',
|
||||||
|
})
|
||||||
|
local pos = fugitive.get_hunk_position(buf, 3)
|
||||||
|
assert.is_nil(pos)
|
||||||
|
vim.api.nvim_buf_delete(buf, { force = true })
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('returns hunk header and offset for + line', function()
|
||||||
|
local buf = create_status_buffer({
|
||||||
|
'Unstaged (1)',
|
||||||
|
'M file.lua',
|
||||||
|
'@@ -1,3 +1,4 @@',
|
||||||
|
' local M = {}',
|
||||||
|
'+local new = true',
|
||||||
|
' return M',
|
||||||
|
})
|
||||||
|
local pos = fugitive.get_hunk_position(buf, 5)
|
||||||
|
assert.is_not_nil(pos)
|
||||||
|
assert.equals('@@ -1,3 +1,4 @@', pos.hunk_header)
|
||||||
|
assert.equals(2, pos.offset)
|
||||||
|
vim.api.nvim_buf_delete(buf, { force = true })
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('returns hunk header and offset for - line', function()
|
||||||
|
local buf = create_status_buffer({
|
||||||
|
'Unstaged (1)',
|
||||||
|
'M file.lua',
|
||||||
|
'@@ -1,3 +1,3 @@',
|
||||||
|
' local M = {}',
|
||||||
|
'-local old = false',
|
||||||
|
' return M',
|
||||||
|
})
|
||||||
|
local pos = fugitive.get_hunk_position(buf, 5)
|
||||||
|
assert.is_not_nil(pos)
|
||||||
|
assert.equals('@@ -1,3 +1,3 @@', pos.hunk_header)
|
||||||
|
assert.equals(2, pos.offset)
|
||||||
|
vim.api.nvim_buf_delete(buf, { force = true })
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('returns hunk header and offset for context line', function()
|
||||||
|
local buf = create_status_buffer({
|
||||||
|
'Unstaged (1)',
|
||||||
|
'M file.lua',
|
||||||
|
'@@ -1,3 +1,4 @@',
|
||||||
|
' local M = {}',
|
||||||
|
'+local new = true',
|
||||||
|
' return M',
|
||||||
|
})
|
||||||
|
local pos = fugitive.get_hunk_position(buf, 6)
|
||||||
|
assert.is_not_nil(pos)
|
||||||
|
assert.equals('@@ -1,3 +1,4 @@', pos.hunk_header)
|
||||||
|
assert.equals(3, pos.offset)
|
||||||
|
vim.api.nvim_buf_delete(buf, { force = true })
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('returns correct offset for first line after @@', function()
|
||||||
|
local buf = create_status_buffer({
|
||||||
|
'Unstaged (1)',
|
||||||
|
'M file.lua',
|
||||||
|
'@@ -1,3 +1,4 @@',
|
||||||
|
' local M = {}',
|
||||||
|
})
|
||||||
|
local pos = fugitive.get_hunk_position(buf, 4)
|
||||||
|
assert.is_not_nil(pos)
|
||||||
|
assert.equals(1, pos.offset)
|
||||||
|
vim.api.nvim_buf_delete(buf, { force = true })
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('handles @@ header with context text', function()
|
||||||
|
local buf = create_status_buffer({
|
||||||
|
'Unstaged (1)',
|
||||||
|
'M file.lua',
|
||||||
|
'@@ -10,3 +10,4 @@ function M.hello()',
|
||||||
|
' print("hi")',
|
||||||
|
'+ print("world")',
|
||||||
|
})
|
||||||
|
local pos = fugitive.get_hunk_position(buf, 5)
|
||||||
|
assert.is_not_nil(pos)
|
||||||
|
assert.equals('@@ -10,3 +10,4 @@ function M.hello()', pos.hunk_header)
|
||||||
|
assert.equals(2, pos.offset)
|
||||||
|
vim.api.nvim_buf_delete(buf, { force = true })
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('returns nil when section header interrupts search', function()
|
||||||
|
local buf = create_status_buffer({
|
||||||
|
'Unstaged (1)',
|
||||||
|
'M file.lua',
|
||||||
|
' some orphan line',
|
||||||
|
})
|
||||||
|
local pos = fugitive.get_hunk_position(buf, 3)
|
||||||
|
assert.is_nil(pos)
|
||||||
|
vim.api.nvim_buf_delete(buf, { force = true })
|
||||||
|
end)
|
||||||
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue