Problem: diffs.nvim blanked 18 Neogit highlight groups globally on attach and fell back to getcwd() plus a subprocess call for repo root detection, with no cache refresh when Neogit lazy-loads new hunks. Solution: adopt three APIs from Neogit PR #1897: - set vim.b.neogit_disable_hunk_highlight = true on attach instead of overriding 18 hl groups globally - read vim.b.neogit_git_dir for reliable repo root resolution - register User NeogitDiffLoaded per-buffer to refresh hunk cache Closes #128
315 lines
8.7 KiB
Lua
315 lines
8.7 KiB
Lua
---@class diffs.Hunk
|
|
---@field filename string
|
|
---@field ft string?
|
|
---@field lang string?
|
|
---@field start_line integer
|
|
---@field header_context string?
|
|
---@field header_context_col integer?
|
|
---@field lines string[]
|
|
---@field header_start_line integer?
|
|
---@field header_lines string[]?
|
|
---@field file_old_start integer?
|
|
---@field file_old_count integer?
|
|
---@field file_new_start integer?
|
|
---@field file_new_count integer?
|
|
---@field prefix_width integer
|
|
---@field repo_root string?
|
|
|
|
local M = {}
|
|
|
|
local dbg = require('diffs.log').dbg
|
|
|
|
---@type table<string, {ft: string?, lang: string?}>
|
|
local ft_lang_cache = {}
|
|
|
|
---@param filepath string
|
|
---@param n integer
|
|
---@return string[]?
|
|
local function read_first_lines(filepath, n)
|
|
local f = io.open(filepath, 'r')
|
|
if not f then
|
|
return nil
|
|
end
|
|
local lines = {}
|
|
for _ = 1, n do
|
|
local line = f:read('*l')
|
|
if not line then
|
|
break
|
|
end
|
|
table.insert(lines, line)
|
|
end
|
|
f:close()
|
|
return #lines > 0 and lines or nil
|
|
end
|
|
|
|
---@param filename string
|
|
---@param repo_root string?
|
|
---@return string?
|
|
local function get_ft_from_filename(filename, repo_root)
|
|
if repo_root then
|
|
local full_path = vim.fs.joinpath(repo_root, filename)
|
|
|
|
local buf = vim.fn.bufnr(full_path)
|
|
if buf ~= -1 then
|
|
local ft = vim.api.nvim_get_option_value('filetype', { buf = buf })
|
|
if ft and ft ~= '' then
|
|
dbg('filetype from existing buffer %d: %s', buf, ft)
|
|
return ft
|
|
end
|
|
end
|
|
end
|
|
|
|
local ft = vim.filetype.match({ filename = filename })
|
|
if ft then
|
|
dbg('filetype from filename: %s', ft)
|
|
return ft
|
|
end
|
|
|
|
if repo_root then
|
|
local full_path = vim.fs.joinpath(repo_root, filename)
|
|
local contents = read_first_lines(full_path, 10)
|
|
if contents then
|
|
ft = vim.filetype.match({ filename = filename, contents = contents })
|
|
if ft then
|
|
dbg('filetype from file content: %s', ft)
|
|
return ft
|
|
end
|
|
end
|
|
end
|
|
|
|
dbg('no filetype for: %s', filename)
|
|
return nil
|
|
end
|
|
|
|
---@param ft string
|
|
---@return string?
|
|
local function get_lang_from_ft(ft)
|
|
local lang = vim.treesitter.language.get_lang(ft)
|
|
if lang then
|
|
local ok = pcall(vim.treesitter.language.inspect, lang)
|
|
if ok then
|
|
return lang
|
|
end
|
|
dbg('no parser for lang: %s (ft: %s)', lang, ft)
|
|
else
|
|
dbg('no ts lang for filetype: %s', ft)
|
|
end
|
|
return nil
|
|
end
|
|
|
|
---@param bufnr integer
|
|
---@return string?
|
|
local function get_repo_root(bufnr)
|
|
local ok, repo_root = pcall(vim.api.nvim_buf_get_var, bufnr, 'diffs_repo_root')
|
|
if ok and repo_root then
|
|
return repo_root
|
|
end
|
|
|
|
local ok2, git_dir = pcall(vim.api.nvim_buf_get_var, bufnr, 'git_dir')
|
|
if ok2 and git_dir then
|
|
return vim.fn.fnamemodify(git_dir, ':h')
|
|
end
|
|
|
|
local ok3, neogit_git_dir = pcall(vim.api.nvim_buf_get_var, bufnr, 'neogit_git_dir')
|
|
if ok3 and neogit_git_dir then
|
|
return vim.fn.fnamemodify(neogit_git_dir, ':h')
|
|
end
|
|
|
|
local cwd = vim.fn.getcwd()
|
|
local git = require('diffs.git')
|
|
return git.get_repo_root(cwd .. '/.')
|
|
end
|
|
|
|
---@param bufnr integer
|
|
---@return diffs.Hunk[]
|
|
function M.parse_buffer(bufnr)
|
|
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
|
local repo_root = get_repo_root(bufnr)
|
|
---@type diffs.Hunk[]
|
|
local hunks = {}
|
|
|
|
---@type string?
|
|
local current_filename = nil
|
|
---@type string?
|
|
local current_ft = nil
|
|
---@type string?
|
|
local current_lang = nil
|
|
---@type integer?
|
|
local hunk_start = nil
|
|
---@type string?
|
|
local hunk_header_context = nil
|
|
---@type integer?
|
|
local hunk_header_context_col = nil
|
|
---@type string[]
|
|
local hunk_lines = {}
|
|
---@type integer?
|
|
local hunk_count = nil
|
|
---@type integer
|
|
local hunk_prefix_width = 1
|
|
---@type integer?
|
|
local header_start = nil
|
|
---@type string[]
|
|
local header_lines = {}
|
|
---@type integer?
|
|
local file_old_start = nil
|
|
---@type integer?
|
|
local file_old_count = nil
|
|
---@type integer?
|
|
local file_new_start = nil
|
|
---@type integer?
|
|
local file_new_count = nil
|
|
---@type integer?
|
|
local old_remaining = nil
|
|
---@type integer?
|
|
local new_remaining = nil
|
|
local is_unified_diff = false
|
|
|
|
local function flush_hunk()
|
|
if hunk_start and #hunk_lines > 0 then
|
|
local hunk = {
|
|
filename = current_filename,
|
|
ft = current_ft,
|
|
lang = current_lang,
|
|
start_line = hunk_start,
|
|
header_context = hunk_header_context,
|
|
header_context_col = hunk_header_context_col,
|
|
lines = hunk_lines,
|
|
prefix_width = hunk_prefix_width,
|
|
file_old_start = file_old_start,
|
|
file_old_count = file_old_count,
|
|
file_new_start = file_new_start,
|
|
file_new_count = file_new_count,
|
|
repo_root = repo_root,
|
|
}
|
|
if hunk_count == 1 and header_start and #header_lines > 0 then
|
|
hunk.header_start_line = header_start
|
|
hunk.header_lines = header_lines
|
|
end
|
|
table.insert(hunks, hunk)
|
|
end
|
|
hunk_start = nil
|
|
hunk_header_context = nil
|
|
hunk_header_context_col = nil
|
|
hunk_lines = {}
|
|
file_old_start = nil
|
|
file_old_count = nil
|
|
file_new_start = nil
|
|
file_new_count = nil
|
|
old_remaining = nil
|
|
new_remaining = nil
|
|
end
|
|
|
|
for i, line in ipairs(lines) do
|
|
local diff_git_file = line:match('^diff %-%-git a/.+ b/(.+)$')
|
|
local neogit_file = line:match('^modified%s+(.+)$')
|
|
or (not line:match('^new file mode') and line:match('^new file%s+(.+)$'))
|
|
or (not line:match('^deleted file mode') and line:match('^deleted%s+(.+)$'))
|
|
or line:match('^renamed%s+(.+)$')
|
|
or line:match('^copied%s+(.+)$')
|
|
local bare_file = not hunk_start and line:match('^([^%s]+%.[^%s]+)$')
|
|
local filename = line:match('^[MADRCU%?!]%s+(.+)$') or diff_git_file or neogit_file or bare_file
|
|
if filename then
|
|
is_unified_diff = diff_git_file ~= nil
|
|
flush_hunk()
|
|
current_filename = filename
|
|
local cache_key = (repo_root or '') .. '\0' .. filename
|
|
local cached = ft_lang_cache[cache_key]
|
|
if cached then
|
|
current_ft = cached.ft
|
|
current_lang = cached.lang
|
|
else
|
|
current_ft = get_ft_from_filename(filename, repo_root)
|
|
current_lang = current_ft and get_lang_from_ft(current_ft) or nil
|
|
if current_ft or vim.fn.did_filetype() == 0 then
|
|
ft_lang_cache[cache_key] = { ft = current_ft, lang = current_lang }
|
|
end
|
|
end
|
|
if current_lang then
|
|
dbg('file: %s -> lang: %s', filename, current_lang)
|
|
elseif current_ft then
|
|
dbg('file: %s -> ft: %s (no ts parser)', filename, current_ft)
|
|
end
|
|
hunk_count = 0
|
|
hunk_prefix_width = 1
|
|
header_start = i
|
|
header_lines = {}
|
|
elseif line:match('^@@+') then
|
|
flush_hunk()
|
|
hunk_start = i
|
|
local at_prefix = line:match('^(@@+)')
|
|
hunk_prefix_width = #at_prefix - 1
|
|
if #at_prefix == 2 then
|
|
local hs, hc, hs2, hc2 = line:match('^@@ %-(%d+),?(%d*) %+(%d+),?(%d*) @@')
|
|
if hs then
|
|
file_old_start = tonumber(hs)
|
|
file_old_count = tonumber(hc) or 1
|
|
file_new_start = tonumber(hs2)
|
|
file_new_count = tonumber(hc2) or 1
|
|
old_remaining = file_old_count
|
|
new_remaining = file_new_count
|
|
end
|
|
else
|
|
local hs2, hc2 = line:match('%+(%d+),?(%d*) @@')
|
|
if hs2 then
|
|
file_new_start = tonumber(hs2)
|
|
file_new_count = tonumber(hc2) or 1
|
|
end
|
|
end
|
|
local at_end, context = line:match('^(@@+.-@@+%s*)(.*)')
|
|
if context and context ~= '' then
|
|
hunk_header_context = context
|
|
hunk_header_context_col = #at_end
|
|
end
|
|
if hunk_count then
|
|
hunk_count = hunk_count + 1
|
|
end
|
|
elseif hunk_start then
|
|
local prefix = line:sub(1, 1)
|
|
if prefix == ' ' or prefix == '+' or prefix == '-' then
|
|
table.insert(hunk_lines, line)
|
|
if old_remaining and (prefix == ' ' or prefix == '-') then
|
|
old_remaining = old_remaining - 1
|
|
end
|
|
if new_remaining and (prefix == ' ' or prefix == '+') then
|
|
new_remaining = new_remaining - 1
|
|
end
|
|
elseif
|
|
line == ''
|
|
and is_unified_diff
|
|
and old_remaining
|
|
and old_remaining > 0
|
|
and new_remaining
|
|
and new_remaining > 0
|
|
then
|
|
table.insert(hunk_lines, ' ')
|
|
old_remaining = old_remaining - 1
|
|
new_remaining = new_remaining - 1
|
|
elseif
|
|
line == ''
|
|
or line:match('^[MADRC%?!]%s+')
|
|
or line:match('^diff ')
|
|
or line:match('^index ')
|
|
or line:match('^Binary ')
|
|
then
|
|
flush_hunk()
|
|
current_filename = nil
|
|
current_ft = nil
|
|
current_lang = nil
|
|
header_start = nil
|
|
end
|
|
end
|
|
if header_start and not hunk_start then
|
|
table.insert(header_lines, line)
|
|
end
|
|
end
|
|
|
|
flush_hunk()
|
|
|
|
return hunks
|
|
end
|
|
|
|
M._test = {
|
|
ft_lang_cache = ft_lang_cache,
|
|
}
|
|
|
|
return M
|