diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e43b0f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/README.md b/README.md index 60ab2e6..dd1c5c6 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,10 @@ - [Push Command Options](#push-command-options) - [Bookmark Management Command Options](#bookmark-management-command-options) - [Open PR/MR Command Options](#open-prmr-command-options) - - [Diff Split Views](#diff-split-views) + - [Diff Module](#diff-module) + - [Functions](#functions) + - [Log Buffer Integration](#log-buffer-integration) + - [Custom Diff Backends](#custom-diff-backends) - [Annotations](#annotations) - [Example config](#example-config) - [Requirements](#requirements) @@ -91,7 +94,11 @@ From the summary view, you can: ### Diff any change -You can diff any change in your log history by simply pressing `d` on its line, yeah just like that! +You can diff any change in your log history by pressing `` on its line or on a summary file change. You can also visually select multiple changes to diff between the first and last selected. + +> [!NOTE] +> Integrates with your preferred diff plugin or uses your native jj diff config. See [Diff Module](#diff-module). + ![Diff-from-log](https://github.com/NicolasGB/jj.nvim/raw/main/assets/diff-log.gif) ### Edit changes @@ -119,7 +126,7 @@ You can undo/redo changes directly from the log buffer: ### Abandon changes from the log buffer -You can abandon changes directly from the log buffer: +You can abandon changes directly from the log buffer, works in visual mode to abandon multiple changes: - `a` - Abandon the revision under the cursor @@ -128,7 +135,7 @@ You can abandon changes directly from the log buffer: You can fetch and push directly from the log buffer: - `f` - Fetch from remote -- `` - Push all changes to remote +- `` - Push from a boockmark from through the picker - `p` - Push bookmark of revision under cursor to remote ### Manage bookmarks from the log buffer @@ -299,6 +306,16 @@ The plugin also provides `:Jdiff`, `:Jvdiff`, and `:Jhdiff` commands for diffing cursor_render_delay = 10, }, + -- Configure diff module + diff = { + -- Default backend for viewing diffs + -- "native" - Built-in split diff using Neovim's diff mode (default) + -- "diffview" - Use diffview.nvim plugin (requires diffview.nvim) + -- "codediff" - Use codediff.nvim plugin (requires codediff.nvim) + -- Or any custom backend name you've registered + backend = "native", + }, + -- Configure cmd module (describe editor, keymaps) cmd = { -- Configure describe editor @@ -556,18 +573,96 @@ cmd.open_pr() -- Open PR for current change's bookmark cmd.open_pr({ list_bookmarks = true }) -- Select bookmark from all and open PR ``` -### Diff Split Views +### Diff Module -Use the `diff` module for opening splits: +The diff module provides a unified API for viewing diffs with pluggable backend support. + +The natively supported backends are: + +- Native (Diffs the current file in place and uses a floating buffer with your jj'diff command when diffing changes) +- [codediff](https://github.com/esmuellert/codediff.nvim) +- [diffview](https://github.com/sindrets/diffview.nvim) + +#### Functions ```lua local diff = require("jj.diff") -diff.open_diff() -- Vertical split diff against parent -diff.open_diff({ rev = "main" }) -- Vertical split against specific revision -diff.open_hsplit() -- Horizontal split diff -diff.open_hsplit({ rev = "@-2" }) -- Horizontal split against @-2 + +-- Diff current buffer against a revision (default: @-) +-- The `layout` is only supported for the native backend +diff.diff_current({ rev = "@-", layout = "vertical" }) + +-- Show what changed in a single revision +diff.show_revision({ rev = "abc123" }) + +-- Diff between two revisions +diff.diff_revisions({ left = "main", right = "@" }) + +-- Convenience functions (LEGACY FUNCTIONS) +diff.open_vdiff() -- Vertical split diff against parent +diff.open_vdiff({ rev = "main" }) -- Vertical split against specific revision +diff.open_hdiff() -- Horizontal split diff +diff.open_hdiff({ rev = "@-2" }) -- Horizontal split against @-2 ``` +#### Log Buffer Integration + +The diff module integrates seamlessly with the log buffer: + +- `` - Show diff for the revision under cursor in a floating window + +These actions use the configured diff backend, allowing you to leverage your preferred diff viewer directly from the log. + +### Custom Diff Backends + +The diff module supports pluggable backends. Built-in backends include `native`, `diffview`, and `codediff`. You can register your own backend: + +```lua +local diff = require("jj.diff") + +diff.register_backend("my-backend", { + -- Diff current buffer against a revision + diff_current = function(opts) + -- opts.rev: revision to diff against (default: "@-") + -- opts.path: file path (default: current buffer) + -- opts.layout: "vertical" or "horizontal" + end, + + -- Show what changed in a single revision + show_revision = function(opts) + -- opts.rev: revision to show + -- opts.path: optional file filter + -- opts.display: "floating", "tab", or "split" + end, + + -- Diff between two revisions + diff_revisions = function(opts) + -- opts.left: left/base revision + -- opts.right: right/target revision + -- opts.path: optional file filter + -- opts.display: "floating", "tab", or "split" + end, +}) +``` + +Set your backend as default in the config: + +```lua +require("jj").setup({ + diff = { + backend = "my-backend" + } +}) +``` + +Or use it per-call: + +```lua +diff.diff_current({ backend = "my-backend", rev = "main" }) +``` + +All three functions are optional—missing ones fall back to the `native` implementation. + ### Annotations View file blame and line history using the annotate module. Can be invoked via command or Lua API. diff --git a/assets/diff-log.gif b/assets/diff-log.gif index 6216e09..267528b 100644 Binary files a/assets/diff-log.gif and b/assets/diff-log.gif differ diff --git a/lua/jj/cmd/describe.lua b/lua/jj/cmd/describe.lua index e7899c9..e51c53a 100644 --- a/lua/jj/cmd/describe.lua +++ b/lua/jj/cmd/describe.lua @@ -83,13 +83,13 @@ function M.describe(description, revset, opts, on_close) -- Use buffer editor mode (defaults to "buffer" if not configured) local editor_mode = merged_opts.type or cmd.config.describe.editor.type or "buffer" if editor_mode == "buffer" then - local jj_cmd = "jj log -r " .. revset .. " --no-graph -T 'coalesce(description, \"\n\")'" + local jj_cmd = "jj log -r " .. revset .. " --quiet --no-graph -T 'coalesce(description, \"\n\")'" local old_description_raw, success = runner.execute_command(jj_cmd, "Failed to get old description") if not old_description_raw or not success then return end - local log_cmd = "jj log -r " .. revset .. " --no-graph -T 'self.diff().summary()'" + local log_cmd = "jj log -r " .. revset .. " --quiet --no-graph -T 'self.diff().summary()'" local status_result, success2 = runner.execute_command(log_cmd, "Error getting status") if not success2 then return diff --git a/lua/jj/cmd/init.lua b/lua/jj/cmd/init.lua index df6da2c..c6e97f3 100644 --- a/lua/jj/cmd/init.lua +++ b/lua/jj/cmd/init.lua @@ -378,19 +378,18 @@ function M.diff(opts) return end - local cmd = "jj diff" + local diff_module = require("jj.diff") if opts and opts.current then local file = vim.fn.expand("%:p") if file and file ~= "" then - cmd = string.format("%s %s", cmd, vim.fn.fnameescape(file)) + diff_module.diff_current({ path = file }) else utils.notify("Current buffer is not a file", vim.log.levels.ERROR) - return end + else + diff_module.show_revision({ rev = "@" }) end - - terminal.run(cmd, M.terminal_keymaps()) end -- Jujutsu rebase diff --git a/lua/jj/cmd/log.lua b/lua/jj/cmd/log.lua index 4c4a2d4..b23ee9c 100644 --- a/lua/jj/cmd/log.lua +++ b/lua/jj/cmd/log.lua @@ -6,6 +6,7 @@ local runner = require("jj.core.runner") local parser = require("jj.core.parser") local terminal = require("jj.ui.terminal") local buffer = require("jj.core.buffer") +local diff = require("jj.diff") local log_selected_hl_group = "JJLogSelectedHlGroup" local log_selected_ns_id = vim.api.nvim_create_namespace(log_selected_hl_group) @@ -332,13 +333,23 @@ end --- Handle diffing a log line function M.handle_log_diff() - local revset = get_revset() - - if revset then - local cmd = string.format("jj show --no-pager %s", revset) - terminal.run_floating(cmd, require("jj.cmd").floating_keymaps()) - else + local revsets = extract_revsets_from_terminal_buffer() + if not revsets then utils.notify("No valid revision found in the log line", vim.log.levels.ERROR) + return + end + + -- Delete the pipes from the revsets string since jj new expects space separated revsets + revsets = revsets:gsub("| ", "") + + local is_multiple = revsets:find(" ") ~= nil + + if not is_multiple then + diff.show_revision({ rev = revsets }) + else + -- Get the first and the last revset to diff + local list = vim.split(revsets, "%s+", { trimempty = true }) + diff.diff_revisions({ left = list[1], right = list[#list] }) end end @@ -731,40 +742,11 @@ function M.handle_summary_diff(revset) terminal.keep_tooltip_open(true) end - -- Add custom close behavior that returns to tooltip - local function close_and_return() - -- Close the floating diff - terminal.close_floating_buffer() - -- Return focus to tooltip if still valid - if terminal.state.tooltip_win and vim.api.nvim_win_is_valid(terminal.state.tooltip_win) then - vim.api.nvim_set_current_win(terminal.state.tooltip_win) - end - -- Clear suppress flag - if terminal.state.tooltip_buf and vim.api.nvim_buf_is_valid(terminal.state.tooltip_buf) then - terminal.keep_tooltip_open(false) - end - end - - local cmd = require("jj.cmd") - local cfg = cmd.config.keymaps.floating or {} - -- In this specific case we override the behaviour of the default terminal close and return to the old buffer - local specs = { - close = { - modes = { "n", "v" }, - handler = close_and_return, - desc = "Close diff and return to tooltip", - }, - hide = { - modes = { "n", "v" }, - handler = close_and_return, - desc = "Close diff and return to tooltip", - }, - } - - terminal.run_floating( - string.format("jj diff -r %s %s", revset, filepath.new_path), - cmd.resolve_keymaps_from_specs(cfg, specs) - ) + -- Use the diff module to show what changed in this revision for this file + diff.show_revision({ + rev = revset, + path = filepath.new_path, + }) end --- Handle edit action in summary tooltip (opens file after jj edit) @@ -936,7 +918,7 @@ function M.log_keymaps() diff = { desc = "Diff revision under cursor", handler = M.handle_log_diff, - modes = { "n" }, + modes = { "n", "v" }, }, new = { desc = "Create new change branching off revision under cursor", diff --git a/lua/jj/diff.lua b/lua/jj/diff.lua deleted file mode 100644 index a488cca..0000000 --- a/lua/jj/diff.lua +++ /dev/null @@ -1,99 +0,0 @@ ----@class jj.diff -local M = {} - -local utils = require("jj.utils") -local buffer = require("jj.core.buffer") - ---- Get the content of a file at a specific revision ---- @param rev string The revision ---- @param path string The file path ---- @return table lines The file content -local function get_file_content(rev, path) - local cmd = string.format("jj file show -r %s %s", vim.fn.shellescape(rev), vim.fn.shellescape(path)) - local content = vim.fn.system(cmd) - local success = vim.v.shell_error == 0 - if success then - return vim.split(content, "\n", { trimempty = true }) - else - -- File does not exist at revision - return {} - end -end - ---- Open a read-only buffer for a specific revision of a file ---- @param rev string The revision ---- @param path string The file path -function M.open_revision(rev, path) - local lines = get_file_content(rev, path) - - local buf = vim.api.nvim_create_buf(false, true) - - local buf_name = string.format("jj://%s/%s", rev, path) - vim.api.nvim_buf_set_name(buf, buf_name) - vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines) - - local ft = vim.filetype.match({ filename = path }) - if ft then - vim.bo[buf].filetype = ft - end - - vim.bo[buf].buftype = "nofile" - vim.bo[buf].bufhidden = "wipe" - vim.bo[buf].readonly = true - vim.bo[buf].swapfile = false - vim.bo[buf].modifiable = true - - vim.api.nvim_win_set_buf(0, buf) -end - ----@class jj.diff.diff_opts ----@field rev string the revision to diff against - ---- Open a diff split for a specific revision of the current file ---- @param split_fun function Split function for the diff ---- @param opts? jj.diff.diff_opts Any passed arguments -function M.open_diff(split_fun, opts) - if not utils.ensure_jj() then - return - end - - -- Get previus cursor position - local prev_buf = vim.api.nvim_get_current_buf() - local prev_cur_pos = buffer.get_cursor(prev_buf) - -- If no previous cursor position, set to start of file - if prev_cur_pos == nil then - prev_cur_pos = { 1, 0 } - end - - -- Ensure opts is a table to avoid indexing nil - opts = opts or {} - local rev = opts.rev or "@-" - local path = vim.api.nvim_buf_get_name(0) - - vim.cmd.diffthis() - split_fun({ mods = { split = "aboveleft" } }) - M.open_revision(rev, path) - vim.cmd.diffthis() - - -- Add an autocomnd to restore the buffer position to the previous one when exiting diff - vim.api.nvim_create_autocmd("BufWipeout", { - buffer = vim.api.nvim_get_current_buf(), - callback = function() - buffer.set_cursor(prev_buf, prev_cur_pos) - end, - }) -end - --- Open a vertical diff split for a specific revision of the current file ---- @param opts? jj.diff.diff_opts Any passed arguments -function M.open_vdiff(opts) - M.open_diff(vim.cmd.vsplit, opts) -end - --- Open a horizontal diff split for a specific revision of the current file ---- @param opts? jj.diff.diff_opts Any passed arguments -function M.open_hdiff(opts) - M.open_diff(vim.cmd.split, opts) -end - -return M diff --git a/lua/jj/diff/codediff.lua b/lua/jj/diff/codediff.lua new file mode 100644 index 0000000..163f527 --- /dev/null +++ b/lua/jj/diff/codediff.lua @@ -0,0 +1,64 @@ +local utils = require("jj.utils") + +---@type jj.diff +local diff = require("jj.diff") + +--- Givewn two changes, show their diff using codediff +--- @param left string +--- @param right string +local function diff_two_changes(left, right) + local commit_id_left = utils.get_commit_id(left) + if commit_id_left == nil then + return + end + + local commit_id_right = utils.get_commit_id(right) + if commit_id_right == nil then + return + end + + vim.cmd(string.format("CodeDiff %s %s", commit_id_right, commit_id_left)) +end + +----------------------------------------------------------------------- +-- Codediff Backend +----------------------------------------------------------------------- + +diff.register_backend("codediff", { + diff_current = function(opts) + if not utils.has_dependency("codediff") then + return + end + + -- Extract the commit id from opts.rev + local commit_id = "HEAD~1" + if opts.rev then + local t_commit_id = utils.get_commit_id(opts.rev) + if t_commit_id == nil then + return + end + commit_id = t_commit_id + end + + vim.cmd(string.format("CodeDiff file %s", commit_id)) + end, + + show_revision = function(opts) + if not utils.has_dependency("codediff") then + return + end + + -- When comparing a revision we always compare it to it's parent to get the diff + local right = string.format("%s-", opts.rev) + + diff_two_changes(opts.rev, right) + end, + + diff_revisions = function(opts) + if not utils.has_dependency("codediff") then + return + end + + diff_two_changes(opts.left, opts.right) + end, +}) diff --git a/lua/jj/diff/diffview.lua b/lua/jj/diff/diffview.lua new file mode 100644 index 0000000..db47209 --- /dev/null +++ b/lua/jj/diff/diffview.lua @@ -0,0 +1,64 @@ +local utils = require("jj.utils") + +---@type jj.diff +local diff = require("jj.diff") + +----------------------------------------------------------------------- +-- Diffvew Backend +----------------------------------------------------------------------- + +--- Givewn two changes, show their diff using diffview.nvim +--- @param left string +--- @param right string +local function diff_two_changes(left, right) + -- Extract the commit id from opts.rev + local commit_id_left = utils.get_commit_id(left) + if commit_id_left == nil then + return + end + local commit_id_right = utils.get_commit_id(right) + if commit_id_right == nil then + return + end + + vim.cmd(string.format("DiffviewOpen %s..%s", commit_id_right, commit_id_left)) +end + +-- Register the diffview backend +diff.register_backend("diffview", { + diff_current = function(opts) + if not utils.has_dependency("diffview") then + return + end + + -- Extract the commit id from opts.rev + local commit_id = "HEAD~1" + if opts.rev then + local t_commit_id = utils.get_commit_id(opts.rev) + if t_commit_id == nil then + return + end + commit_id = t_commit_id + end + + vim.cmd(string.format("DiffviewOpen %s -- %%", commit_id)) + vim.cmd("DiffviewToggleFiles") + end, + show_revision = function(opts) + if not utils.has_dependency("diffview") then + return + end + + -- When comparing a revision we always compare it to it's parent to get the diff + local right = string.format("%s-", opts.rev) + + diff_two_changes(opts.rev, right) + end, + diff_revisions = function(opts) + if not utils.has_dependency("diffview") then + return + end + + diff_two_changes(opts.left, opts.right) + end, +}) diff --git a/lua/jj/diff/init.lua b/lua/jj/diff/init.lua new file mode 100644 index 0000000..261db1f --- /dev/null +++ b/lua/jj/diff/init.lua @@ -0,0 +1,164 @@ +---@class jj.diff +local M = {} + +local utils = require("jj.utils") + +---@alias jj.diff.backend "native"|"diffview"|"codediff"|string + +---@class jj.diff.current_opts +---@field rev? string -- revision to diff against (default: "@-") +---@field path? string -- path to diff (default: current buffer path) +---@field backend? jj.diff.backend +---@field layout? "vertical"|"horizontal" -- only used by native backend + +---@class jj.diff.revision_opts +---@field rev string -- revision to show +---@field path? string -- optional single-file filter +---@field backend? jj.diff.backend +---@field display? "floating"|"tab"|"split" -- hint to backend + +---@class jj.diff.revisions_opts +---@field left string -- left/base revision +---@field right string -- right/target revision +---@field path? string -- optional single-file filter +---@field backend? jj.diff.backend +---@field display? "floating"|"tab"|"split" + +---@class jj.diff.BackendImpl +---@field diff_current? fun(opts: jj.diff.current_opts) +---@field show_revision? fun(opts: jj.diff.revision_opts) +---@field diff_revisions? fun(opts: jj.diff.revisions_opts) + +---@class jj.diff.config +---@field backend? jj.diff.backend +---@field backends? table + +---@class jj.diff.diff_opts +---@field rev string the revision to diff against + +---@type jj.diff.config +M.config = { + backend = "native", + backends = {}, +} + +---@type table +local backends = {} + +----------------------------------------------------------------------- +-- Backend Registry +----------------------------------------------------------------------- + +--- Register or override a backend implementation +---@param name string +---@param impl jj.diff.BackendImpl +function M.register_backend(name, impl) + backends[name] = impl +end + +--- Get the configured default backend name +---@return string +local function get_config_backend() + local ok, cfg = pcall(function() + return require("jj").config.diff + end) + if ok and cfg and cfg.backend then + return cfg.backend + end + return M.config.backend or "native" +end + +--- Get a backend implementation by name, falling back to native +---@param name? string +---@return jj.diff.BackendImpl +local function get_backend(name) + name = name or get_config_backend() + local impl = backends[name] + if not impl then + utils.notify( + string.format("[Diff] backend '%s' not available, falling back to 'native'", name), + vim.log.levels.WARN + ) + impl = backends.native + end + return impl +end + +--- Setup the diff module +---@param cfg? jj.diff.config +function M.setup(cfg) + M.config = vim.tbl_deep_extend("force", M.config, cfg or {}) + -- Laod default backends + pcall(require, "jj.diff.diffview") + pcall(require, "jj.diff.codediff") + pcall(require, "jj.diff.native") +end + +----------------------------------------------------------------------- +-- Unified Public API +----------------------------------------------------------------------- + +--- Single dispatcher (canonical entry point) +---@param kind "current"|"revision"|"revisions" +---@param opts table +function M.open(kind, opts) + opts = opts or {} + local backend = get_backend(opts.backend) + + if kind == "current" then + if backend.diff_current then + return backend.diff_current(opts) + end + return backends.native.diff_current(opts) + elseif kind == "revision" then + if backend.show_revision then + return backend.show_revision(opts) + end + return backends.native.show_revision(opts) + elseif kind == "revisions" then + if backend.diff_revisions then + return backend.diff_revisions(opts) + end + return backends.native.diff_revisions(opts) + else + utils.notify("[Diff] unknown diff kind: " .. tostring(kind), vim.log.levels.ERROR) + end +end + +--- Diff current buffer against a revision +---@param opts? jj.diff.current_opts +function M.diff_current(opts) + return M.open("current", opts or {}) +end + +--- Show what changed in a single revision +---@param opts jj.diff.revision_opts +function M.show_revision(opts) + return M.open("revision", opts) +end + +--- Diff between two revisions +---@param opts jj.diff.revisions_opts +function M.diff_revisions(opts) + return M.open("revisions", opts) +end + +--- +----------------------------------------------------------------------- +-- BACKWARDS COMPATIBLE API +----------------------------------------------------------------------- + +-- Open a vertical diff split for a specific revision of the current file +--- @param opts? jj.diff.diff_opts Any passed arguments +function M.open_vdiff(opts) + M.diff_current(vim.tbl_extend("force", { layout = "vertical" }, { rev = opts and opts.rev })) +end + +-- Open a horizontal diff split for a specific revision of the current file +--- @param opts? jj.diff.diff_opts Any passed arguments +function M.open_hdiff(opts) + M.diff_current(vim.tbl_extend("force", { layout = "horizontal" }, { rev = opts and opts.rev })) +end + +---@return jj.diff +return M diff --git a/lua/jj/diff/native.lua b/lua/jj/diff/native.lua new file mode 100644 index 0000000..f3da668 --- /dev/null +++ b/lua/jj/diff/native.lua @@ -0,0 +1,127 @@ +local utils = require("jj.utils") +local buffer = require("jj.core.buffer") + +local diff = require("jj.diff") + +--- Get the content of a file at a specific revision +--- @param rev string The revision +--- @param path string The file path +--- @return table lines The file content +local function get_file_content(rev, path) + local cmd = string.format("jj file show -r %s %s", vim.fn.shellescape(rev), vim.fn.shellescape(path)) + local content = vim.fn.system(cmd) + local success = vim.v.shell_error == 0 + if success then + return vim.split(content, "\n", { trimempty = true }) + else + return {} + end +end + +--- Open a read-only buffer for a specific revision of a file +--- @param rev string The revision +--- @param path string The file path +local function open_revision(rev, path) + local lines = get_file_content(rev, path) + + local buf = vim.api.nvim_create_buf(false, true) + + local buf_name = string.format("jj://%s/%s", rev, path) + vim.api.nvim_buf_set_name(buf, buf_name) + vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines) + + local ft = vim.filetype.match({ filename = path }) + if ft then + vim.bo[buf].filetype = ft + end + + vim.bo[buf].buftype = "nofile" + vim.bo[buf].bufhidden = "wipe" + vim.bo[buf].readonly = true + vim.bo[buf].swapfile = false + vim.bo[buf].modifiable = true + + vim.api.nvim_win_set_buf(0, buf) +end + +----------------------------------------------------------------------- +-- Native Backend +----------------------------------------------------------------------- + +diff.register_backend("native", { + --- Opens a side-by-side diff of the current buffer against a revision. + --- Creates a split with the revision content on the left and the current buffer on the right. + --- Closing either side will clean up both and restore the original cursor position. + diff_current = function(opts) + if not utils.ensure_jj() then + return + end + + -- Save current state to restore after diff is closed + local prev_buf = vim.api.nvim_get_current_buf() + local prev_cur_pos = buffer.get_cursor(prev_buf) or { 1, 0 } + + local rev = opts.rev or "@-" + local path = opts.path or vim.api.nvim_buf_get_name(0) + local layout = opts.layout or "vertical" + + local split_fun = layout == "horizontal" and vim.cmd.split or vim.cmd.vsplit + local orig_win = vim.api.nvim_get_current_win() + + -- Use better diff algorithm for code moves and indentation + local saved_diffopt = vim.o.diffopt + vim.opt.diffopt:append("algorithm:patience,indent-heuristic") + + -- Set up diff: current buffer on right, revision on left + vim.cmd.diffthis() + split_fun({ mods = { split = "aboveleft" } }) + open_revision(rev, path) + vim.cmd.diffthis() + + local rev_buf = vim.api.nvim_get_current_buf() + local augroup = vim.api.nvim_create_augroup("JJDiffCleanup" .. rev_buf, { clear = true }) + + -- Cleanup closes both sides, exits diff mode, and restores cursor. + local function cleanup() + vim.api.nvim_del_augroup_by_id(augroup) + vim.schedule(function() + if vim.api.nvim_buf_is_valid(rev_buf) then + vim.api.nvim_buf_delete(rev_buf, { force = true }) + end + if vim.api.nvim_win_is_valid(orig_win) then + vim.api.nvim_set_current_win(orig_win) + vim.cmd.diffoff() + end + buffer.set_cursor(prev_buf, prev_cur_pos) + vim.o.diffopt = saved_diffopt + end) + end + + -- Trigger cleanup when either the revision buffer or original window is closed + vim.api.nvim_create_autocmd({ "BufWipeout", "BufHidden" }, { + group = augroup, + buffer = rev_buf, + once = true, + callback = cleanup, + }) + + vim.api.nvim_create_autocmd("WinClosed", { + group = augroup, + pattern = tostring(orig_win), + once = true, + callback = cleanup, + }) + end, + show_revision = function(opts) + local terminal = require("jj.ui.terminal") + + local cmd = string.format("jj show -r %s --quiet --no-pager", opts.rev) + terminal.run_floating(cmd, require("jj.cmd").floating_keymaps()) + end, + diff_revisions = function(opts) + local terminal = require("jj.ui.terminal") + + local cmd = string.format("jj diff -f %s -t %s --quiet --no-pager", opts.left, opts.right) + terminal.run_floating(cmd, require("jj.cmd").floating_keymaps()) + end, +}) diff --git a/lua/jj/init.lua b/lua/jj/init.lua index 2a63715..e38870f 100644 --- a/lua/jj/init.lua +++ b/lua/jj/init.lua @@ -3,6 +3,7 @@ local cmd = require("jj.cmd") local picker = require("jj.picker") local editor = require("jj.ui.editor") local terminal = require("jj.ui.terminal") +local diff = require("jj.diff") --- Jujutsu plugin configuration --- @class jj.Config @@ -10,6 +11,7 @@ local terminal = require("jj.ui.terminal") --- @field picker? jj.picker.config Options for picker module --- @field terminal? jj.ui.terminal.opts Options for the terminal --- @field highlights? jj.highlights Options for the highlights +--- @field diff? jj.diff.config Options for the diff module --- @class jj.highlights --- @field editor? jj.ui.editor.highlights Highlight configuration for describe buffer @@ -29,6 +31,10 @@ M.config = { targeted = { fg = "#5a9e6f", ctermfg = "Green" }, }, }, + diff = { + backend = "native", + backends = {}, + }, } --- Setup the plugin @@ -41,6 +47,7 @@ function M.setup(opts) editor.setup({ highlights = M.config.highlights.editor }) cmd.setup(opts and opts.cmd or {}) terminal.setup(opts and opts.terminal or {}) + diff.setup(M.config.diff) cmd.register_command() end diff --git a/lua/jj/ui/terminal.lua b/lua/jj/ui/terminal.lua index 9e8fbc4..9a3dcc0 100644 --- a/lua/jj/ui/terminal.lua +++ b/lua/jj/ui/terminal.lua @@ -163,8 +163,13 @@ end --- Close the current tooltip buffer if it exists function M.close_tooltip() + if not state.tooltip_buf then + return + end buffer.close(state.tooltip_buf) - vim.api.nvim_del_autocmd(state.tooltip_close_autocmd) + if state.tooltip_close_autocmd then + pcall(vim.api.nvim_del_autocmd, state.tooltip_close_autocmd) + end state.tooltip_chan = nil state.tooltip_job_id = nil state.tooltip_buf = nil @@ -668,7 +673,8 @@ function M.run_tooltip(cmd, tool_opts) state.tooltip_close_autocmd = vim.api.nvim_create_autocmd("CursorMoved", { callback = function() if - vim.api.nvim_buf_is_valid(state.tooltip_buf) + state.tooltip_buf + and vim.api.nvim_buf_is_valid(state.tooltip_buf) and vim.api.nvim_win_is_valid(state.tooltip_win) and vim.api.nvim_get_current_win() ~= state.tooltip_win then @@ -676,8 +682,7 @@ function M.run_tooltip(cmd, tool_opts) if vim.b[state.tooltip_buf].jj_keep_open then return end - buffer.close(state.tooltip_buf, true) - return true + M.close_tooltip() end end, }) diff --git a/lua/jj/utils.lua b/lua/jj/utils.lua index 7bec0b8..1cb8c52 100644 --- a/lua/jj/utils.lua +++ b/lua/jj/utils.lua @@ -261,7 +261,7 @@ end --- @return boolean True if the change is immutable, false otherwise function M.is_change_immutable(revset) local output, success = runner.execute_command( - string.format("jj log --no-graph -r '%s' -T 'immutable'", revset), + string.format("jj log --no-graph -r '%s' -T 'immutable' --quiet", revset), "Error checking change immutability", nil, true @@ -273,5 +273,24 @@ function M.is_change_immutable(revset) return vim.trim(output) == "true" end +--- +--- Get the commit id from a given revision +--- @param revset string The revset to extract the commit id from +--- @return string|nil +function M.get_commit_id(revset) + local output, success = runner.execute_command( + string.format("jj log --no-graph -r '%s' -T 'commit_id' --quiet", revset), + "Error extracting commit id", + nil, + true + ) + -- Test quie + + if not success or not output then + return nil + end + + return vim.trim(output) +end return M