diff --git a/README.md b/README.md index cbdfb8c..7a7106f 100644 --- a/README.md +++ b/README.md @@ -203,26 +203,30 @@ Deleting and pushing tags (for colocated repositories) is also supported and cha ### Squash changes from the log buffer -Enter an interactive squash mode to squash one or more changes into a destination: +Enter squash mode from the log buffer to squash one or more changes into a destination: -- `s` - Enter squash mode targeting the revision under cursor (in normal mode) or selected revisions (in visual mode) -- `` - Quick squash the revision under cursor into its parent +- `s` - Enter standard squash mode for the revision under cursor (Normal mode) or selected revisions (Visual mode) +- `is` - Enter **interactive** squash mode for the revision under cursor or Visual selection +- `` - Immediately squash the revision under cursor into its parent +- `` - Interactively squash the revision under cursor into its parent -Once in squash mode, the interface highlights your selection and the current squash destination: +Both standard and interactive squash modes highlight the operation before it runs: -- Selected changes are highlighted in your configured `selected_hl` color (default: dark magenta) -- The cursor position (potential squash destination) is highlighted in your configured `targeted_hl` color (default: green) -- Move the cursor to preview different squash destinations with live highlighting +- Selected changes use `highlights.log.selected` (dark magenta by default). +- The potential destination under the cursor uses `highlights.log.targeted` (green by default). +- Move the cursor to preview another destination; the target highlight updates live. A selected revision is not also shown as a target. -From squash mode, choose how to squash: +From either squash mode, choose the destination: - `` - Squash into (`-t`) the revision under cursor -- `` - Squash into (`-t`) ignoring immutability +- `` - Squash into (`-t`) while ignoring immutability - `` or `` - Exit squash mode without making changes -**Visual mode selection:** Select multiple revisions in visual mode before pressing `s` to squash them all at once. The plugin extracts each selected revision and squashes them together. +**Interactive squash:** After choosing a destination with `is` and `` (or ``), `jj.nvim` opens `jj squash --interactive` in a floating terminal. Use jj's interactive UI to select the content to squash; on success, the log is refreshed. The `is` mapping is intentionally a two-key sequence so it does not conflict with the normal `s` mapping. -**Quick squash:** In normal mode, press `` to quickly squash the current revision into its parent. This ignores immutability. +**Visual mode selection:** Select multiple revisions in Visual mode before pressing `s` or `is` to squash them all at once. The plugin extracts each selected revision and squashes them together. + +**Quick squash:** `` performs the non-interactive operation and `` opens jj's interactive UI. Both target the current revision's parent and ignore immutability. ![Squash-from-log](https://github.com/NicolasGB/jj.nvim/raw/main/assets/squash.gif) @@ -692,13 +696,15 @@ revision via `jj diffedit`. Immutable revisions show an error on write. before_immutable = "", -- Duplicate before revision under cursor (ignore immutability) exit_mode = { "", "" }, -- Exit duplicate mode }, - squash = "s", -- Enter squash mode targeting revision under cursor or selected revisions + squash = "s", -- Enter standard squash mode for revision under cursor or selected revisions + squash_interactive = "is", -- Enter interactive squash mode; choose a target, then use jj's interactive UI squash_mode = { into = "", -- Squash into revision under cursor into_immutable = "", -- Squash into revision under cursor (ignore immutability) - exit_mode = { "", "" }, -- Exit squash mode + exit_mode = { "", "" }, -- Exit standard or interactive squash mode }, quick_squash = "", -- Quick squash revision under cursor into its parent (ignore immutability) + quick_interactive_squash = "", -- Quick interactive squash into parent (ignore immutability) split = "", -- Split the revision under cursor resolve = "gr", -- Resolve conflicts for revision under cursor history = "", -- Show a history-aware diff for the selected revision range diff --git a/lua/jj/cmd/init.lua b/lua/jj/cmd/init.lua index 1f89d39..b83fcaf 100644 --- a/lua/jj/cmd/init.lua +++ b/lua/jj/cmd/init.lua @@ -52,8 +52,10 @@ local resolve_module = require("jj.cmd.resolve") --- @field rebase? string|string[] --- @field rebase_mode? jj.cmd.rebase.keymaps --- @field squash? string|string[] +--- @field squash_interactive? string|string[] --- @field squash_mode? jj.cmd.squash.keymaps --- @field quick_squash? string|string[] +--- @field quick_interactive_squash? string|string[] --- @field summary? string|string[] --- @field summary_tooltip? jj.cmd.summary_tooltip.keymaps --- @field tag_set? string|string[] @@ -205,11 +207,14 @@ M.config = { exit_mode = { "", "" }, }, squash = "s", + squash_interactive = "is", squash_mode = { into = "", into_immutable = "", exit_mode = { "", "" }, }, + quick_squash = "", + quick_interactive_squash = "", duplicate = "", duplicate_mode = { onto = { "", "o" }, @@ -220,7 +225,6 @@ M.config = { before_immutable = "", exit_mode = { "", "" }, }, - quick_squash = "", summary = "", summary_tooltip = { diff = "", diff --git a/lua/jj/cmd/log.lua b/lua/jj/cmd/log.lua index cf166db..05d52d5 100644 --- a/lua/jj/cmd/log.lua +++ b/lua/jj/cmd/log.lua @@ -951,6 +951,21 @@ function M.handle_log_squash() utils.notify("Squash `started`.", vim.log.levels.INFO, 500) end +function M.handle_log_squash_interactive() + local revsets_str = extract_revsets_from_terminal_buffer() + -- Validate revsets + if not revsets_str or revsets_str == "" then + return + end + + vim.b.jj_squash_revsets = revsets_str + -- Set highlights + setup_selected_highlights() + + M.transition_mode("squash_interactive") + utils.notify("Interactive squash `started`.", vim.log.levels.INFO, 500) +end + --- Duplicate bookmark(s) function M.handle_log_duplicate() local revsets_str = extract_revsets_from_terminal_buffer() @@ -967,19 +982,52 @@ function M.handle_log_duplicate() utils.notify("Duplication `started`.", vim.log.levels.INFO, 500) end ---- Quick squash the bookmark under the cursor into it's parent -function M.handle_log_quick_squash() +--- Quick squash the bookmark under the cursor into its parent +--- @param interactive boolean|nil If true, will add the --interactive flag to the jj squash command +function M.handle_log_quick_squash(interactive) local revset = get_revset() if not revset or revset == "" then return end local cmd = { "jj", "squash", "-r", revset, "-u", "--ignore-immutable" } - utils.notify(string.format("Squashing `%s` into it's parent...", revset), vim.log.levels.INFO) - runner.execute_async(cmd, function() - utils.notify(string.format("Successfully squashed `%s` into it's parent", revset), vim.log.levels.INFO) - M.log({}) - end, string.format("Error squashing `%s` into it's parent", revset)) + + if interactive then + table.insert(cmd, "--interactive") + + terminal.run_floating(cmd, nil, { + title = "Squash Interactive", + modifiable = true, + keep_modifiable = true, + interactive = true, + on_exit = function(exit_code) + if exit_code == 0 then + utils.notify( + string.format("Successfully squashed `%s` into it's parent", revset), + vim.log.levels.INFO + ) + M.log({}) + else + utils.notify( + string.format("Cancelled squashing `%s` into it's parent", revset), + vim.log.levels.WARN + ) + -- Since we previously replaced the floating with the squash we actually want to re run the log cmd + if require("jj").config.terminal.window.type == "floating" then + vim.schedule(function() + M.log({}) + end) + end + end + end, + }) + else + utils.notify(string.format("Squashing `%s` into it's parent...", revset), vim.log.levels.INFO) + runner.execute_async(cmd, function() + utils.notify(string.format("Successfully squashed `%s` into it's parent", revset), vim.log.levels.INFO) + M.log({}) + end, string.format("Error squashing `%s` into it's parent", revset)) + end end --- Handle log split @@ -1402,16 +1450,27 @@ function M.log_keymaps() handler = M.handle_log_squash, modes = { "n", "v" }, }, + squash_interactive = { + desc = "Squash bookmark(s) interactively", + handler = M.handle_log_squash_interactive, + modes = { "n", "v" }, + }, duplicate = { desc = "Duplicate bookmark(s)", handler = M.handle_log_duplicate, modes = { "n", "v" }, }, quick_squash = { - desc = "Squash the bookmark under the cursor into it's parent (-r) keeping parent's message (-u), alwas ignores immutability", + desc = "Squash the bookmark under the cursor into its parent (-r) keeping parent's message (-u), always ignores immutability", handler = M.handle_log_quick_squash, modes = { "n" }, }, + quick_interactive_squash = { + desc = "Squash the bookmark under the cursor into its parent (-r) keeping parent's message (-u), always ignores immutability, but opens an interactive prompt", + handler = M.handle_log_quick_squash, + args = { true }, + modes = { "n" }, + }, summary = { desc = "Show summary tooltip for revision under cursor", handler = M.handle_log_summary, @@ -1504,7 +1563,7 @@ function M.rebase_keymaps() }, exit_mode = { desc = "Exit rebase to normal mode", - handler = M.handle_special_mode_exit, + handler = M.exit_special_mode, args = { "Rebase" }, modes = { "n" }, }, @@ -1514,8 +1573,9 @@ function M.rebase_keymaps() end --- Squash mode keymaps +--- @param interactive boolean --- @return jj.core.buffer.keymap[] -function M.squash_keymaps() +function M.squash_keymaps(interactive) local cmd = require("jj.cmd") local keymaps = cmd.config.keymaps.log.squash_mode or {} @@ -1524,18 +1584,18 @@ function M.squash_keymaps() into = { desc = "Squash into (-t) the revision under cursor", handler = M.handle_squash_execute, - args = { "into" }, + args = { "into", false, interactive }, modes = { "n" }, }, into_immutable = { desc = "Squash onto (-i) the revision under cursor (ignores immutability)", handler = M.handle_squash_execute, - args = { "into", true }, + args = { "into", true, interactive }, modes = { "n" }, }, exit_mode = { desc = "Exit squash to normal mode", - handler = M.handle_special_mode_exit, + handler = M.exit_special_mode, args = { "Squash" }, modes = { "n" }, }, @@ -1591,7 +1651,7 @@ function M.duplicate_keymaps() }, exit_mode = { desc = "Exit normal to normal mode", - handler = M.handle_special_mode_exit, + handler = M.exit_special_mode, args = { "Duplication" }, modes = { "n" }, }, @@ -1609,7 +1669,9 @@ function M.get_keymaps_for_mode(mode) elseif mode == "rebase" then return M.rebase_keymaps() elseif mode == "squash" then - return M.squash_keymaps() + return M.squash_keymaps(false) + elseif mode == "squash_interactive" then + return M.squash_keymaps(true) elseif mode == "duplicate" then return M.duplicate_keymaps() end @@ -1617,7 +1679,7 @@ function M.get_keymaps_for_mode(mode) end --- Transition between buffer modes by swapping keymaps ---- @param target_mode "normal"|"rebase"|"squash"|"duplicate" Target mode name (e.g., "normal", "rebase") +--- @param target_mode "normal"|"rebase"|"squash"|"squash_interactive"|"duplicate" Target mode name (e.g., "normal", "rebase") function M.transition_mode(target_mode) -- Get the mode keymaps if target_mode == vim.b.jj_mode then @@ -1650,7 +1712,7 @@ end --- Handle special mode exit --- @param mode "Rebase"|"Squash"|"Duplicate" The mode that is being exited, used for notification message -function M.handle_special_mode_exit(mode) +function M.exit_special_mode(mode) -- Clear stored revsets vim.b.jj_rebase_revsets = nil vim.b.jj_squash_revsets = nil @@ -1667,7 +1729,7 @@ end --- Handle rebase execution with mode --- @param mode "onto" | "after" | "before" Rebase mode ---- @param ignore_immut boolean? Wether or not to ignore immutability +--- @param ignore_immut boolean? Whether or not to ignore immutability function M.handle_rebase_execute(mode, ignore_immut) -- Get all revsets in the format "xx xy xz" local revsets = vim.b.jj_rebase_revsets @@ -1704,14 +1766,8 @@ function M.handle_rebase_execute(mode, ignore_immut) string.format("Rebased `%s` %s `%s` successfully", revsets, mode, destination_revset), vim.log.levels.INFO ) - vim.b.jj_rebase_revsets = nil - -- Clear all highlighting before transitioning - local buf = terminal.state.buf or 0 - vim.api.nvim_buf_clear_namespace(buf, log_selected_ns_id, 0, -1) - vim.api.nvim_buf_clear_namespace(buf, log_special_mode_target_ns_id, 0, -1) - - M.transition_mode("normal") + M.exit_special_mode("Rebase") -- Refresh log M.log({}) end, "Error during rebase.") @@ -1719,8 +1775,9 @@ end --- Handle squash execution --- @param mode "into" Squash mode ---- @param ignore_immut boolean? Wether or not to ignore immutability -function M.handle_squash_execute(mode, ignore_immut) +--- @param ignore_immut boolean? Whether or not to ignore immutability +--- @param interactive boolean|nil If true, will add the --interactive flag to the jj squash command +function M.handle_squash_execute(mode, ignore_immut, interactive) -- Get all revsets in the format "xx xy xz" local revsets = vim.b.jj_squash_revsets local destination_revset = get_revset() @@ -1742,27 +1799,66 @@ function M.handle_squash_execute(mode, ignore_immut) table.insert(cmd, "--ignore-immutable") end - runner.execute_async(cmd, function() - utils.notify( - string.format("Squashed `%s` into `%s` successfully", revsets, destination_revset), - vim.log.levels.INFO - ) - vim.b.jj_squash_revsets = nil + if interactive then + -- If interactive is true, run the command in a floating terminal with --interactive flag + table.insert(cmd, "--interactive") + terminal.run_floating(cmd, nil, { + title = " JJ Squash ", + modifiable = true, + keep_modifiable = true, + interactive = true, + on_exit = function(exit_code) + if exit_code == 0 then + utils.notify( + string.format("Squashed `%s` into `%s` successfully", revsets, destination_revset), + vim.log.levels.INFO + ) + vim.b.jj_squash_revsets = nil - -- Clear all highlighting before transitioning - local buf = terminal.state.buf or 0 - vim.api.nvim_buf_clear_namespace(buf, log_selected_ns_id, 0, -1) - vim.api.nvim_buf_clear_namespace(buf, log_special_mode_target_ns_id, 0, -1) + -- Clear all highlighting before transitioning + M.exit_special_mode("Squash") + -- Refresh log + M.log({}) + else + utils.notify( + string.format("Cancelled squashing `%s` into `%s`", revsets, destination_revset), + vim.log.levels.WARN + ) - M.transition_mode("normal") - -- Refresh log - M.log({}) - end, "Error during squashing.") + M.exit_special_mode("Squash") + -- Since we previously replaced the floating with the squash we actually want to re run the log cmd + if require("jj").config.terminal.window.type == "floating" then + vim.schedule(function() + M.log({}) + end) + end + end + end, + }) + else + -- Otherwise, run the command asynchronously without --interactive flag + runner.execute_async(cmd, function() + utils.notify( + string.format("Squashed `%s` into `%s` successfully", revsets, destination_revset), + vim.log.levels.INFO + ) + vim.b.jj_squash_revsets = nil + + -- Clear all highlighting before transitioning + local buf = terminal.state.buf or 0 + vim.api.nvim_buf_clear_namespace(buf, log_selected_ns_id, 0, -1) + vim.api.nvim_buf_clear_namespace(buf, log_special_mode_target_ns_id, 0, -1) + + M.exit_special_mode("Squash") + -- Refresh log + M.log({}) + end, "Error during squashing.") + end end -- Handle duplicate execution --- @param mode "onto" | "after" | "before" Rebase mode ---- @param ignore_immut boolean? Wether or not to ignore immutability +--- @param ignore_immut boolean? Whether or not to ignore immutability function M.handle_duplicate_execute(mode, ignore_immut) -- Get all revsets in the format "xx xy xz" local revsets = vim.b.jj_duplicate_revsets @@ -1792,14 +1888,8 @@ function M.handle_duplicate_execute(mode, ignore_immut) string.format("Duplicated `%s` %s `%s` successfully", revsets, mode, destination_revset), vim.log.levels.INFO ) - vim.b.jj_duplicate_revsets = nil - -- Clear all highlighting before transitioning - local buf = terminal.state.buf or 0 - vim.api.nvim_buf_clear_namespace(buf, log_selected_ns_id, 0, -1) - vim.api.nvim_buf_clear_namespace(buf, log_special_mode_target_ns_id, 0, -1) - - M.transition_mode("normal") + M.exit_special_mode("Duplicate") -- Refresh log M.log({}) end, "Error during duplication.") diff --git a/lua/jj/ui/terminal.lua b/lua/jj/ui/terminal.lua index a19996f..3b95c55 100644 --- a/lua/jj/ui/terminal.lua +++ b/lua/jj/ui/terminal.lua @@ -426,6 +426,7 @@ function M.run_floating(cmd, keymaps, float_opts) { modes = { "n", "v" }, lhs = "c", rhs = function() end }, { modes = { "n", "v" }, lhs = "a", rhs = function() end }, { modes = { "n", "v" }, lhs = "", rhs = function() end }, + { modes = { "n", "v" }, lhs = "", rhs = function() end }, { modes = { "n", "v" }, lhs = "u", rhs = function() end }, } -- IF it's interactive do not block them @@ -599,6 +600,7 @@ function M.run(cmd, keymaps) { modes = { "n", "v" }, lhs = "c", rhs = function() end }, { modes = { "n", "v" }, lhs = "a", rhs = function() end }, { modes = { "n", "v" }, lhs = "", rhs = function() end }, + { modes = { "n", "v" }, lhs = "", rhs = function() end }, { modes = { "n", "v" }, lhs = "u", rhs = function() end }, })