commit c6113da5a9992881eddf98c985e6da888db76bc6
parent 638d44ded8f6ea20965ee32f1897d766b6e1e913
Author: Tomas Slusny <slusnucky@gmail.com>
Date: Sun, 12 Oct 2025 20:25:14 +0200
fix(difftool): fully resolve symlinks when comparing paths #36147
Fixes issue on mac where it was constantly reloading buffers as paths
were not being normalized and resolved correctly (in relation to buffer
name).
Quickfix entry:
/var/folders/pt/2s7dzyw12v36tsslrghfgpkr0000gn/T/git-difftool.m95lj8/right/app.vue
Buffer name:
/private/var/folders/pt/2s7dzyw12v36tsslrghfgpkr0000gn/T/git-difftool.m95lj8/right/app.vue
/var was synlinked to /private/var and this was not being properly
handled.
Also added lazy redraw to avoid too many redraws when this happens in
future and added test for symlink handling.
Signed-off-by: Tomas Slusny <slusnucky@gmail.com>
Diffstat:
3 files changed, 52 insertions(+), 10 deletions(-)
diff --git a/runtime/lua/vim/_core/util.lua b/runtime/lua/vim/_core/util.lua
@@ -5,12 +5,20 @@ local M = {}
--- @param file string
--- @return number buffer number of the edited buffer
M.edit_in = function(winnr, file)
+ local function resolved_path(path)
+ if not path or path == '' then
+ return ''
+ end
+ return vim.fn.resolve(vim.fs.abspath(path))
+ end
+
return vim.api.nvim_win_call(winnr, function()
- local current = vim.fs.abspath(vim.api.nvim_buf_get_name(vim.api.nvim_win_get_buf(winnr)))
+ local current_buf = vim.api.nvim_win_get_buf(winnr)
+ local current = resolved_path(vim.api.nvim_buf_get_name(current_buf))
-- Check if the current buffer is already the target file
- if current == (file and vim.fs.abspath(file) or '') then
- return vim.api.nvim_get_current_buf()
+ if current == resolved_path(file) then
+ return current_buf
end
-- Read the file into the buffer
diff --git a/runtime/pack/dist/opt/nvim.difftool/lua/difftool.lua b/runtime/pack/dist/opt/nvim.difftool/lua/difftool.lua
@@ -140,14 +140,16 @@ local function diff_dirs_diffr(left_dir, right_dir, opt)
elseif right_exists then
status = 'A'
end
+ local left = vim.fn.resolve(vim.fs.abspath(modified_left))
+ local right = vim.fn.resolve(vim.fs.abspath(modified_right))
table.insert(qf_entries, {
- filename = modified_right,
+ filename = right,
text = status,
user_data = {
diff = true,
rel = vim.fs.relpath(left_dir, modified_left),
- left = vim.fs.abspath(modified_left),
- right = vim.fs.abspath(modified_right),
+ left = left,
+ right = right,
},
})
end
@@ -426,7 +428,7 @@ function M.open(left, right, opt)
layout.group = vim.api.nvim_create_augroup('nvim.difftool.events', { clear = true })
local hl_id = vim.api.nvim_create_namespace('nvim.difftool.hl')
- local function get_diff_entry()
+ local function get_diff_entry(bufnr)
--- @type {idx: number, items: table[], size: number}
local qf_info = vim.fn.getqflist({ idx = 0, items = 1, size = 1 })
if qf_info.size == 0 then
@@ -434,7 +436,12 @@ function M.open(left, right, opt)
end
local entry = qf_info.items[qf_info.idx]
- if not entry or not entry.user_data or not entry.user_data.diff then
+ if
+ not entry
+ or not entry.user_data
+ or not entry.user_data.diff
+ or (bufnr and entry.bufnr ~= bufnr)
+ then
return nil
end
@@ -466,14 +473,16 @@ function M.open(left, right, opt)
vim.api.nvim_create_autocmd('BufWinEnter', {
group = layout.group,
pattern = '*',
- callback = function()
- local entry = get_diff_entry()
+ callback = function(args)
+ local entry = get_diff_entry(args.buf)
if not entry then
return
end
+ vim.w.lazyredraw = true
vim.schedule(function()
diff_files(entry.user_data.left, entry.user_data.right, true)
+ vim.w.lazyredraw = false
end)
end,
})
diff --git a/test/functional/plugin/difftool_spec.lua b/test/functional/plugin/difftool_spec.lua
@@ -61,6 +61,31 @@ describe('nvim.difftool', function()
)
end)
+ it('handles symlinks', function()
+ -- Create a symlink in right dir pointing to file2.txt in left dir
+ local symlink_path = vim.fs.joinpath(testdir_right, 'file2.txt')
+ local target_path = vim.fs.joinpath('..', testdir_left, 'file2.txt')
+ assert(vim.uv.fs_symlink(target_path, symlink_path) == true)
+ finally(function()
+ os.remove(symlink_path)
+ end)
+ assert(fn.getftype(symlink_path) == 'link')
+
+ -- Run difftool
+ command(('DiffTool %s %s'):format(testdir_left, testdir_right))
+ local qflist = fn.getqflist()
+ local entries = {}
+ for _, item in ipairs(qflist) do
+ table.insert(entries, { text = item.text, rel = item.user_data and item.user_data.rel })
+ end
+
+ -- file2.txt should not be reported as added or deleted anymore
+ eq({
+ { text = 'M', rel = 'file1.txt' },
+ { text = 'A', rel = 'file3.txt' },
+ }, entries)
+ end)
+
it('has autocmds when diff window is opened', function()
command(('DiffTool %s %s'):format(testdir_left, testdir_right))
local autocmds = fn.nvim_get_autocmds({ group = 'nvim.difftool.events' })