diff --git a/lua/diffs/fugitive.lua b/lua/diffs/fugitive.lua new file mode 100644 index 0000000..0a8f297 --- /dev/null +++ b/lua/diffs/fugitive.lua @@ -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 diff --git a/lua/diffs/init.lua b/lua/diffs/init.lua index 66a8098..b9944a4 100644 --- a/lua/diffs/init.lua +++ b/lua/diffs/init.lua @@ -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 diff --git a/plugin/diffs.lua b/plugin/diffs.lua index 8a11067..35aa223 100644 --- a/plugin/diffs.lua +++ b/plugin/diffs.lua @@ -13,6 +13,13 @@ vim.api.nvim_create_autocmd('FileType', { return end diffs.attach(args.buf) + + if args.match == 'fugitive' then + local fugitive_config = diffs.get_fugitive_config() + if fugitive_config.horizontal or fugitive_config.vertical then + require('diffs.fugitive').setup_keymaps(args.buf, fugitive_config) + end + end end, })