feat(fugitive): add status buffer keymaps for unified diffs
Adds du/dU keymaps to fugitive's :Git status buffer for opening unified diffs instead of side-by-side diffs: - du opens horizontal split (mirrors dd) - dU opens vertical split (mirrors dv) Parses status buffer lines to extract filename and detect section (staged/unstaged/untracked). For staged files, diffs index vs HEAD. For unstaged files, diffs working tree vs index. Configurable via vim.g.diffs.fugitive.horizontal/vertical (set to false to disable).
This commit is contained in:
parent
ea60ab8d01
commit
9289f33639
3 changed files with 186 additions and 0 deletions
145
lua/diffs/fugitive.lua
Normal file
145
lua/diffs/fugitive.lua
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
local M = {}
|
||||
|
||||
local commands = require('diffs.commands')
|
||||
local git = require('diffs.git')
|
||||
local dbg = require('diffs.log').dbg
|
||||
|
||||
---@alias diffs.FugitiveSection 'staged' | 'unstaged' | 'untracked' | nil
|
||||
|
||||
---@param bufnr integer
|
||||
---@param lnum integer
|
||||
---@return diffs.FugitiveSection
|
||||
function M.get_section_at_line(bufnr, lnum)
|
||||
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, lnum, false)
|
||||
|
||||
for i = #lines, 1, -1 do
|
||||
local line = lines[i]
|
||||
if line:match('^Staged ') then
|
||||
return 'staged'
|
||||
elseif line:match('^Unstaged ') then
|
||||
return 'unstaged'
|
||||
elseif line:match('^Untracked ') then
|
||||
return 'untracked'
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
---@param line string
|
||||
---@return string?
|
||||
local function parse_file_line(line)
|
||||
local renamed = line:match('^R[%s%d]*[^%s]+%s*->%s*(.+)$')
|
||||
if renamed then
|
||||
return vim.trim(renamed)
|
||||
end
|
||||
|
||||
local filename = line:match('^[MADRCU?][MADRCU%s]*%s+(.+)$')
|
||||
if filename then
|
||||
return vim.trim(filename)
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
---@param bufnr integer
|
||||
---@param lnum integer
|
||||
---@return string?, diffs.FugitiveSection
|
||||
function M.get_file_at_line(bufnr, lnum)
|
||||
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||
local current_line = lines[lnum]
|
||||
|
||||
if not current_line then
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
local filename = parse_file_line(current_line)
|
||||
if filename then
|
||||
local section = M.get_section_at_line(bufnr, lnum)
|
||||
return filename, section
|
||||
end
|
||||
|
||||
local prefix = current_line:sub(1, 1)
|
||||
if prefix == '+' or prefix == '-' or prefix == ' ' then
|
||||
for i = lnum - 1, 1, -1 do
|
||||
local prev_line = lines[i]
|
||||
filename = parse_file_line(prev_line)
|
||||
if filename then
|
||||
local section = M.get_section_at_line(bufnr, i)
|
||||
return filename, section
|
||||
end
|
||||
if prev_line:match('^%w+ %(') or prev_line == '' then
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
---@param bufnr integer
|
||||
---@return string?
|
||||
local function get_repo_root_from_fugitive(bufnr)
|
||||
local bufname = vim.api.nvim_buf_get_name(bufnr)
|
||||
local fugitive_path = bufname:match('^fugitive://(.+)///')
|
||||
if fugitive_path then
|
||||
return fugitive_path
|
||||
end
|
||||
|
||||
local cwd = vim.fn.getcwd()
|
||||
local root = git.get_repo_root(cwd .. '/.')
|
||||
return root
|
||||
end
|
||||
|
||||
---@param vertical boolean
|
||||
function M.diff_file_under_cursor(vertical)
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
local lnum = vim.api.nvim_win_get_cursor(0)[1]
|
||||
|
||||
local filename, section = M.get_file_at_line(bufnr, lnum)
|
||||
|
||||
if not filename then
|
||||
vim.notify('[diffs.nvim]: no file under cursor', vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
|
||||
local repo_root = get_repo_root_from_fugitive(bufnr)
|
||||
if not repo_root then
|
||||
vim.notify('[diffs.nvim]: could not determine repository root', vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
local filepath = repo_root .. '/' .. filename
|
||||
|
||||
dbg('diff_file_under_cursor: %s (section: %s)', filename, section or 'unknown')
|
||||
|
||||
if section == 'untracked' then
|
||||
vim.notify('[diffs.nvim]: cannot diff untracked file (no base version)', vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
|
||||
commands.gdiff_file(filepath, {
|
||||
vertical = vertical,
|
||||
staged = section == 'staged',
|
||||
})
|
||||
end
|
||||
|
||||
---@param bufnr integer
|
||||
---@param config { horizontal: string|false, vertical: string|false }
|
||||
function M.setup_keymaps(bufnr, config)
|
||||
if config.horizontal and config.horizontal ~= '' then
|
||||
vim.keymap.set('n', config.horizontal, function()
|
||||
M.diff_file_under_cursor(false)
|
||||
end, { buffer = bufnr, desc = 'Unified diff (horizontal)' })
|
||||
dbg('set keymap %s for buffer %d', config.horizontal, bufnr)
|
||||
end
|
||||
|
||||
if config.vertical and config.vertical ~= '' then
|
||||
vim.keymap.set('n', config.vertical, function()
|
||||
M.diff_file_under_cursor(true)
|
||||
end, { buffer = bufnr, desc = 'Unified diff (vertical)' })
|
||||
dbg('set keymap %s for buffer %d', config.vertical, bufnr)
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
@ -12,11 +12,16 @@
|
|||
---@field treesitter diffs.TreesitterConfig
|
||||
---@field vim diffs.VimConfig
|
||||
|
||||
---@class diffs.FugitiveConfig
|
||||
---@field horizontal string|false
|
||||
---@field vertical string|false
|
||||
|
||||
---@class diffs.Config
|
||||
---@field debug boolean
|
||||
---@field debounce_ms integer
|
||||
---@field hide_prefix boolean
|
||||
---@field highlights diffs.Highlights
|
||||
---@field fugitive diffs.FugitiveConfig
|
||||
|
||||
---@class diffs
|
||||
---@field attach fun(bufnr?: integer)
|
||||
|
|
@ -78,6 +83,10 @@ local default_config = {
|
|||
max_lines = 200,
|
||||
},
|
||||
},
|
||||
fugitive = {
|
||||
horizontal = 'du',
|
||||
vertical = 'dU',
|
||||
},
|
||||
}
|
||||
|
||||
---@type diffs.Config
|
||||
|
|
@ -219,6 +228,25 @@ local function init()
|
|||
end
|
||||
end
|
||||
|
||||
if opts.fugitive then
|
||||
vim.validate({
|
||||
['fugitive.horizontal'] = {
|
||||
opts.fugitive.horizontal,
|
||||
function(v)
|
||||
return v == false or type(v) == 'string'
|
||||
end,
|
||||
'string or false',
|
||||
},
|
||||
['fugitive.vertical'] = {
|
||||
opts.fugitive.vertical,
|
||||
function(v)
|
||||
return v == false or type(v) == 'string'
|
||||
end,
|
||||
'string or false',
|
||||
},
|
||||
})
|
||||
end
|
||||
|
||||
if opts.debounce_ms and opts.debounce_ms < 0 then
|
||||
error('diffs: debounce_ms must be >= 0')
|
||||
end
|
||||
|
|
@ -354,4 +382,10 @@ function M.detach_diff()
|
|||
end
|
||||
end
|
||||
|
||||
---@return diffs.FugitiveConfig
|
||||
function M.get_fugitive_config()
|
||||
init()
|
||||
return config.fugitive
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue