mirror of
https://github.com/zoriya/jj.nvim.git
synced 2026-08-04 18:26:08 +00:00
feat(log): Implement squash --interactive from the log buffer (#145)
This commit is contained in:
@@ -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)
|
||||
- `<S-s>` - 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
|
||||
- `<S-s>` - Immediately squash the revision under cursor into its parent
|
||||
- `<S-i><S-s>` - 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:
|
||||
|
||||
- `<CR>` - Squash into (`-t`) the revision under cursor
|
||||
- `<S-CR>` - Squash into (`-t`) ignoring immutability
|
||||
- `<S-CR>` - Squash into (`-t`) while ignoring immutability
|
||||
- `<Esc>` or `<C-c>` - 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 `<CR>` (or `<S-CR>`), `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 `<S-s>` 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:** `<S-s>` performs the non-interactive operation and `<S-i><S-s>` opens jj's interactive UI. Both target the current revision's parent and ignore immutability.
|
||||
|
||||

|
||||
|
||||
@@ -692,13 +696,15 @@ revision via `jj diffedit`. Immutable revisions show an error on write.
|
||||
before_immutable = "<S-b>", -- Duplicate before revision under cursor (ignore immutability)
|
||||
exit_mode = { "<Esc>", "<C-c>" }, -- 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 = "<CR>", -- Squash into revision under cursor
|
||||
into_immutable = "<S-CR>", -- Squash into revision under cursor (ignore immutability)
|
||||
exit_mode = { "<Esc>", "<C-c>" }, -- Exit squash mode
|
||||
exit_mode = { "<Esc>", "<C-c>" }, -- Exit standard or interactive squash mode
|
||||
},
|
||||
quick_squash = "<S-s>", -- Quick squash revision under cursor into its parent (ignore immutability)
|
||||
quick_interactive_squash = "<S-i><S-s>", -- Quick interactive squash into parent (ignore immutability)
|
||||
split = "<C-s>", -- Split the revision under cursor
|
||||
resolve = "gr", -- Resolve conflicts for revision under cursor
|
||||
history = "<S-h>", -- Show a history-aware diff for the selected revision range
|
||||
|
||||
+5
-1
@@ -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 = { "<Esc>", "<C-c>" },
|
||||
},
|
||||
squash = "s",
|
||||
squash_interactive = "is",
|
||||
squash_mode = {
|
||||
into = "<CR>",
|
||||
into_immutable = "<S-CR>",
|
||||
exit_mode = { "<Esc>", "<C-c>" },
|
||||
},
|
||||
quick_squash = "<S-s>",
|
||||
quick_interactive_squash = "<S-i><S-s>",
|
||||
duplicate = "<C-y>",
|
||||
duplicate_mode = {
|
||||
onto = { "<CR>", "o" },
|
||||
@@ -220,7 +225,6 @@ M.config = {
|
||||
before_immutable = "<S-b>",
|
||||
exit_mode = { "<Esc>", "<C-c>" },
|
||||
},
|
||||
quick_squash = "<S-s>",
|
||||
summary = "<S-k>",
|
||||
summary_tooltip = {
|
||||
diff = "<S-d>",
|
||||
|
||||
+139
-49
@@ -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.")
|
||||
|
||||
@@ -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 = "<S-a>", rhs = function() end },
|
||||
{ modes = { "n", "v" }, lhs = "<S-i>", 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 = "<S-a>", rhs = function() end },
|
||||
{ modes = { "n", "v" }, lhs = "<S-i>", rhs = function() end },
|
||||
{ modes = { "n", "v" }, lhs = "u", rhs = function() end },
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user