diff --git a/lua/jj/diff/codediff.lua b/lua/jj/diff/codediff.lua index 0761e73..3bcf651 100644 --- a/lua/jj/diff/codediff.lua +++ b/lua/jj/diff/codediff.lua @@ -1,8 +1,111 @@ local utils = require("jj.utils") +local file = require("jj.file") ---@type jj.diff local diff = require("jj.diff") +---@type {dirs: string[], files: string[]}[] +local pending_launches = {} +---@type table +local artifacts_by_tab = {} +local temp_cleanup_registered = false + +local function cleanup_artifacts(artifacts) + if not artifacts then + return + end + + for _, file_path in ipairs(artifacts.files or {}) do + local bufnr = vim.fn.bufnr(file_path) + if bufnr ~= -1 and vim.api.nvim_buf_is_valid(bufnr) then + pcall(vim.api.nvim_buf_delete, bufnr, { force = true }) + end + end + + for _, dir_path in ipairs(artifacts.dirs or {}) do + vim.fn.delete(dir_path, "rf") + end +end + +local function register_cleanup_hooks() + if temp_cleanup_registered then + return + end + temp_cleanup_registered = true + + local group = vim.api.nvim_create_augroup("JJCodediffTempCleanup", { clear = true }) + + vim.api.nvim_create_autocmd("User", { + group = group, + pattern = "CodeDiffOpen", + callback = function(args) + local artifacts = table.remove(pending_launches, 1) + if not artifacts then + return + end + local tabpage = (args.data and args.data.tabpage) or vim.api.nvim_get_current_tabpage() + artifacts_by_tab[tabpage] = artifacts + end, + }) + + vim.api.nvim_create_autocmd("User", { + group = group, + pattern = "CodeDiffClose", + callback = function(args) + local tabpage = (args.data and args.data.tabpage) or vim.api.nvim_get_current_tabpage() + local artifacts = artifacts_by_tab[tabpage] + if artifacts then + cleanup_artifacts(artifacts) + artifacts_by_tab[tabpage] = nil + end + end, + }) + + vim.api.nvim_create_autocmd("VimLeavePre", { + group = group, + callback = function() + for _, artifacts in pairs(artifacts_by_tab) do + cleanup_artifacts(artifacts) + end + for _, artifacts in ipairs(pending_launches) do + cleanup_artifacts(artifacts) + end + artifacts_by_tab = {} + pending_launches = {} + end, + }) +end + +--- Write text lines to a temp file while preserving filename/extension for filetype detection. +--- @param original_path string +--- @param lines string[] +--- @param had_eol boolean +--- @return string|nil file_path +--- @return string|nil dir_path +local function write_temp_file_like(original_path, lines, had_eol) + local dir = vim.fn.tempname() + vim.fn.mkdir(dir, "p") + + local basename = vim.fs.basename(original_path or "") + if not basename or basename == "" then + basename = "jj-codediff.tmp" + end + + local path = dir .. "/" .. basename + local fh = io.open(path, "w") + if not fh then + vim.fn.delete(dir, "rf") + return nil, nil + end + fh:write(table.concat(lines, "\n")) + if had_eol then + fh:write("\n") + end + fh:close() + + return path, dir +end + --- Givewn two changes, show their diff using codediff --- @param left string --- @param right string @@ -53,8 +156,71 @@ diff.register_backend("codediff", { return end - -- Extract the commit id from opts.rev - local revset = opts.rev or "@-" + local buf_name = vim.api.nvim_buf_get_name(0) + local change_id, jj_path = utils.parse_jj_uri(buf_name) + local revset = opts.rev or (change_id and (change_id .. "-")) or "@-" + + -- For jj:// buffers, compare the current in-memory buffer against for the same file. + if change_id and not opts.path then + local path = jj_path + if not path or path == "" then + utils.notify("Invalid jj:// buffer path", vim.log.levels.ERROR) + return + end + + local base_lines, base_had_eol, ok_read = file.get_file_content(revset, path) + if not ok_read then + utils.notify(string.format("Could not read `%s` from `%s` for CodeDiff", path, revset), vim.log.levels.ERROR) + return + end + local cur_lines = vim.api.nvim_buf_get_lines(0, 0, -1, false) + local cur_had_eol = vim.bo[0].eol + + local base_file, base_dir = write_temp_file_like(path, base_lines, base_had_eol) + local cur_file, cur_dir = write_temp_file_like(path, cur_lines, cur_had_eol) + if not base_file or not cur_file or not base_dir or not cur_dir then + if base_dir then + vim.fn.delete(base_dir, "rf") + end + if cur_dir then + vim.fn.delete(cur_dir, "rf") + end + utils.notify("Failed to create temporary files for CodeDiff", vim.log.levels.ERROR) + return + end + + register_cleanup_hooks() + local artifacts = { + dirs = { base_dir, cur_dir }, + files = { base_file, cur_file }, + } + table.insert(pending_launches, artifacts) + + local ok_cmd, err = pcall(function() + vim.cmd( + string.format("CodeDiff file %s %s", vim.fn.fnameescape(base_file), vim.fn.fnameescape(cur_file)) + ) + end) + + if not ok_cmd then + -- remove the just-enqueued launch so the next CodeDiffOpen doesn't consume it + if pending_launches[#pending_launches] == artifacts then + table.remove(pending_launches) + else + for i = #pending_launches, 1, -1 do + if pending_launches[i] == artifacts then + table.remove(pending_launches, i) + break + end + end + end + + cleanup_artifacts(artifacts) + utils.notify(err or "Could not launch CodeDiff", vim.log.levels.ERROR) + end + + return + end local commit_id = utils.get_commit_id(revset) if not commit_id then diff --git a/lua/jj/diff/diffview.lua b/lua/jj/diff/diffview.lua index af8d83d..6e7ff81 100644 --- a/lua/jj/diff/diffview.lua +++ b/lua/jj/diff/diffview.lua @@ -46,15 +46,23 @@ diff.register_backend("diffview", { return end - -- Extract the commit id from opts.rev - local revset = opts.rev or "@-" + local buf_name = vim.api.nvim_buf_get_name(0) + local change_id, jj_path = utils.parse_jj_uri(buf_name) + local revset = opts.rev or (change_id and (change_id .. "-")) or "@-" local commit_id = utils.get_commit_id(revset) if not commit_id then return end - vim.cmd(string.format("DiffviewOpen %s -- %%", commit_id)) + local raw_path = opts.path or jj_path or "%" + local path, err = utils.normalize_repo_path(raw_path) + if not path then + utils.notify(err or "Could not resolve file path for Diffview", vim.log.levels.ERROR) + return + end + + vim.cmd(string.format("DiffviewOpen %s -- %s", commit_id, vim.fn.fnameescape(path))) vim.cmd("DiffviewToggleFiles") end, show_revision = function(opts) diff --git a/lua/jj/diff/native.lua b/lua/jj/diff/native.lua index bb7c4d2..bbf085e 100644 --- a/lua/jj/diff/native.lua +++ b/lua/jj/diff/native.lua @@ -27,7 +27,11 @@ local function open_revision(rev, path) return end - local lines, had_eol = file.get_file_content(change_id, rel_path) + local lines, had_eol, ok_read = file.get_file_content(change_id, rel_path) + if not ok_read then + utils.notify(string.format("Could not read `%s` from `%s`", rel_path, change_id), vim.log.levels.ERROR) + return + end local buf = vim.api.nvim_create_buf(false, true) diff --git a/lua/jj/file.lua b/lua/jj/file.lua index 8c6541b..b8a687c 100644 --- a/lua/jj/file.lua +++ b/lua/jj/file.lua @@ -21,6 +21,7 @@ local parser = require("jj.core.parser") --- @param path string Repository-relative path --- @return string[] lines --- @return boolean had_eol Whether the content had a trailing newline +--- @return boolean ok Whether the command succeeded local function get_file_content(rev, path) local content, ok = runner.execute_command( string.format("jj file show -r %s %s", vim.fn.shellescape(rev), vim.fn.shellescape(path)), @@ -29,14 +30,14 @@ local function get_file_content(rev, path) true ) if not ok or not content then - return {}, false + return {}, false, false end local lines = vim.split(content, "\n", { plain = true, trimempty = false }) local had_eol = #lines > 0 and lines[#lines] == "" if had_eol then table.remove(lines, #lines) end - return lines, had_eol + return lines, had_eol, true end M.get_file_content = get_file_content @@ -146,7 +147,11 @@ function M.open_target(opts) end local change_id = ids[1] - local lines, had_eol = get_file_content(change_id, path) + local lines, had_eol, ok_read = get_file_content(change_id, path) + if not ok_read then + utils.notify(string.format("Could not read `%s` from `%s`", path, change_id), vim.log.levels.ERROR) + return + end local ft = vim.filetype.match({ filename = path }) local buf, _ = buffer.create({ @@ -219,7 +224,11 @@ function M.register_command() local name = vim.api.nvim_buf_get_name(0) local change_id, path = utils.parse_jj_uri(name) if not change_id then return end - local lines, had_eol = get_file_content(change_id, path) + local lines, had_eol, ok_read = get_file_content(change_id, path) + if not ok_read then + utils.notify(string.format("Could not read `%s` from `%s`", path, change_id), vim.log.levels.ERROR) + return + end local buf = vim.api.nvim_get_current_buf() vim.bo[buf].modifiable = true vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)