mirror of
https://github.com/zoriya/jj.nvim.git
synced 2026-08-05 10:46:08 +00:00
527 lines
13 KiB
Lua
527 lines
13 KiB
Lua
--- @class jj.cmd
|
|
local M = {}
|
|
|
|
local utils = require("jj.utils")
|
|
local runner = require("jj.core.runner")
|
|
local parser = require("jj.core.parser")
|
|
local terminal = require("jj.ui.terminal")
|
|
local editor = require("jj.ui.editor")
|
|
local diff = require("jj.diff")
|
|
|
|
-- Config for cmd module
|
|
--- @class jj.cmd.opts
|
|
M.config = {
|
|
--- @type "buffer"|"input" Editor mode for describe command: "buffer" (Git-style editor) or "input" (simple input prompt)
|
|
describe_editor = "buffer", -- "buffer" or "input"
|
|
}
|
|
|
|
--- @class jj.cmd.describe_opts
|
|
--- @field with_status boolean: Whether or not `jj st` should be displayed in a buffer while describing the commit
|
|
--- @type jj.cmd.describe_opts
|
|
local default_describe_opts = {
|
|
with_status = true,
|
|
}
|
|
|
|
--- Execute jj describe command with the given description
|
|
--- @param description string The description text
|
|
--- @param revset? string The revision to describe
|
|
local function execute_describe(description, revset)
|
|
if not description or description == "" then
|
|
utils.notify("Description cannot be empty", vim.log.levels.ERROR)
|
|
return
|
|
end
|
|
|
|
local cmd = "jj describe"
|
|
if revset then
|
|
cmd = cmd .. " -r " .. revset
|
|
end
|
|
cmd = cmd .. " --stdin"
|
|
|
|
-- Use --stdin to properly handle multi-line and special characters
|
|
local _, success = runner.execute_command(cmd, "Failed to describe", description)
|
|
if success then
|
|
utils.notify("Description set.", vim.log.levels.INFO)
|
|
end
|
|
end
|
|
|
|
-- Jujutsu describe
|
|
--- @param description? string Optional description text
|
|
--- @param revset? string The revision to describe
|
|
--- @param opts? jj.cmd.describe_opts Optional command options
|
|
function M.describe(description, revset, opts)
|
|
if not utils.ensure_jj() then
|
|
return
|
|
end
|
|
|
|
-- Check if a description was provided otherwise require for input
|
|
if description then
|
|
-- Description provided directly
|
|
execute_describe(description, revset)
|
|
return
|
|
end
|
|
|
|
if not revset then
|
|
revset = "@"
|
|
end
|
|
|
|
-- Use buffer editor mode
|
|
if M.config.describe_editor == "buffer" then
|
|
local cmd = "jj log -r " .. revset .. " --no-graph -T 'coalesce(description, \"\n\")'"
|
|
local old_description_raw, success = runner.execute_command(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 status_result, success2 = runner.execute_command(log_cmd, "Error getting status")
|
|
if not success2 then
|
|
return
|
|
end
|
|
|
|
local status_files = parser.get_status_files(status_result)
|
|
local old_description = vim.trim(old_description_raw)
|
|
|
|
local text = { old_description }
|
|
table.insert(text, "") -- Empty line to separate from user input
|
|
table.insert(text, "JJ: Change ID: " .. revset)
|
|
table.insert(text, "JJ: This commit contains the following changes:")
|
|
for _, item in ipairs(status_files) do
|
|
table.insert(text, string.format("JJ: %s %s", item.status, item.file))
|
|
end
|
|
table.insert(text, "JJ:") -- blank line
|
|
table.insert(text, 'JJ: Lines starting with "JJ:" (like this one) will be removed')
|
|
|
|
editor.open_editor(text, function(buf_lines)
|
|
local user_lines = {}
|
|
for _, line in ipairs(buf_lines) do
|
|
if not line:match("^JJ:") then
|
|
table.insert(user_lines, line)
|
|
end
|
|
end
|
|
-- Join lines and trim leading/trailing whitespace
|
|
local trimmed_description = table.concat(user_lines, "\n"):gsub("^%s+", ""):gsub("%s+$", "")
|
|
execute_describe(trimmed_description, revset)
|
|
end)
|
|
terminal.close_terminal_buffer()
|
|
else
|
|
-- Use input mode
|
|
local merged_opts = vim.tbl_deep_extend("force", default_describe_opts, opts or {})
|
|
if merged_opts.with_status then
|
|
-- Show the status in a terminal buffer
|
|
M.status()
|
|
end
|
|
|
|
vim.ui.input({
|
|
prompt = "Description: ",
|
|
default = "",
|
|
}, function(input)
|
|
-- If the user inputed something, execute the describe command
|
|
if input then
|
|
execute_describe(input, revset)
|
|
end
|
|
-- Close the current terminal when finished
|
|
terminal.close_terminal_buffer()
|
|
end)
|
|
end
|
|
end
|
|
|
|
-- Jujutsu status.
|
|
function M.status(opts)
|
|
if not utils.ensure_jj() then
|
|
return
|
|
end
|
|
|
|
local cmd = "jj st"
|
|
|
|
if opts and opts.notify then
|
|
local output, success = runner.execute_command(cmd, "Failed to get status")
|
|
if success then
|
|
utils.notify(output and output or "", vim.log.levels.INFO)
|
|
end
|
|
else
|
|
-- Default behavior: show in buffer
|
|
terminal.run(cmd)
|
|
end
|
|
end
|
|
|
|
--- @class jj.cmd.new_opts
|
|
--- @field show_log? boolean Whether or not to display the log command after creating a new
|
|
--- @field with_input? boolean Whether or not to use nvim input to decide the parent of the new commit
|
|
--- @field args? string The arguments to append to the new command
|
|
|
|
-- Jujutsu new
|
|
--- @param opts? jj.cmd.new_opts
|
|
function M.new(opts)
|
|
if not utils.ensure_jj() then
|
|
return
|
|
end
|
|
|
|
opts = opts or {}
|
|
|
|
--- @param cmd string
|
|
local function execute_new(cmd)
|
|
runner.execute_command(cmd, "Failed to create new change")
|
|
utils.notify("Command `new` was succesful.", vim.log.levels.INFO)
|
|
-- Show the updated log if the user requested it
|
|
if opts.show_log then
|
|
M.log()
|
|
end
|
|
end
|
|
|
|
-- If the user wants use input mode
|
|
if opts.with_input then
|
|
if opts.show_log then
|
|
M.log()
|
|
end
|
|
|
|
vim.ui.input({
|
|
prompt = "Parent(s) of the new change [default: @]",
|
|
}, function(input)
|
|
if input then
|
|
execute_new(string.format("jj new %s", input))
|
|
end
|
|
terminal.close_terminal_buffer()
|
|
end)
|
|
else
|
|
-- Otherwise follow a classic flow for inputing
|
|
local cmd = "jj new"
|
|
if opts.args then
|
|
cmd = string.format("jj new %s", opts.args)
|
|
end
|
|
|
|
execute_new(cmd)
|
|
-- If the show log is enabled show log
|
|
if opts.show_log then
|
|
M.log()
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Jujutsu edit
|
|
function M.edit()
|
|
if not utils.ensure_jj() then
|
|
return
|
|
end
|
|
M.log({})
|
|
vim.ui.input({
|
|
prompt = "Change to edit: ",
|
|
default = "",
|
|
}, function(input)
|
|
if input then
|
|
local _, success = runner.execute_command(string.format("jj edit %s", input), "Error editing change")
|
|
if not success then
|
|
return
|
|
end
|
|
M.log({})
|
|
else
|
|
terminal.close_terminal_buffer()
|
|
end
|
|
end)
|
|
end
|
|
|
|
-- Jujutsu squash
|
|
function M.squash()
|
|
if not utils.ensure_jj() then
|
|
return
|
|
end
|
|
|
|
local cmd = "jj squash"
|
|
local _, success = runner.execute_command(cmd, "Failed to squash")
|
|
if success then
|
|
utils.notify("Command `squash` was succesful.", vim.log.levels.INFO)
|
|
if terminal.state.buf_cmd == "log" then
|
|
M.log()
|
|
end
|
|
end
|
|
end
|
|
|
|
--- @class jj.cmd.log_opts
|
|
--- @field summary? boolean
|
|
--- @field reversed? boolean
|
|
--- @field no_graph? boolean
|
|
--- @field limit? uinteger
|
|
--- @field revisions? string
|
|
|
|
local default_log_opts = { summary = false, reversed = false, no_graph = false, limit = 20 }
|
|
|
|
-- Jujutsu log
|
|
--- @param opts? jj.cmd.log_opts
|
|
function M.log(opts)
|
|
if not utils.ensure_jj() then
|
|
return
|
|
end
|
|
|
|
local cmd = "jj log"
|
|
local merged_opts = vim.tbl_extend("force", default_log_opts, opts or {})
|
|
|
|
for key, value in pairs(merged_opts) do
|
|
key = key:gsub("_", "-")
|
|
if key == "limit" and value then
|
|
cmd = string.format("%s --%s %d", cmd, key, value)
|
|
elseif key == "revisions" and value then
|
|
cmd = string.format("%s --%s %s", cmd, key, value)
|
|
elseif value then
|
|
cmd = string.format("%s --%s", cmd, key)
|
|
end
|
|
end
|
|
|
|
terminal.run(cmd)
|
|
end
|
|
|
|
--- @class jj.cmd.diff_opts
|
|
--- @field current boolean Wether or not to only diff the current buffer
|
|
|
|
-- Jujutsu diff
|
|
--- @param opts? jj.cmd.diff_opts The options for the diff command
|
|
function M.diff(opts)
|
|
if not utils.ensure_jj() then
|
|
return
|
|
end
|
|
|
|
local cmd = "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))
|
|
else
|
|
utils.notify("Current buffer is not a file", vim.log.levels.ERROR)
|
|
return
|
|
end
|
|
end
|
|
|
|
terminal.run(cmd)
|
|
end
|
|
|
|
-- Jujutsu rebase
|
|
function M.rebase()
|
|
if not utils.ensure_jj() then
|
|
return
|
|
end
|
|
|
|
M.log({})
|
|
vim.ui.input({
|
|
prompt = "Rebase destination: ",
|
|
default = "trunk()",
|
|
}, function(input)
|
|
if input then
|
|
local cmd = string.format("jj rebase -d '%s'", input)
|
|
utils.notify(string.format("Beginning rebase on %s", input), vim.log.levels.INFO)
|
|
local _, success = runner.execute_command(cmd, "Error rebasing")
|
|
if success then
|
|
utils.notify("Rebase successful.", vim.log.levels.INFO)
|
|
M.log({})
|
|
end
|
|
else
|
|
terminal.close_terminal_buffer()
|
|
end
|
|
end)
|
|
end
|
|
|
|
-- Jujutsu create bookmark
|
|
function M.bookmark_create()
|
|
if not utils.ensure_jj() then
|
|
return
|
|
end
|
|
|
|
M.log({})
|
|
vim.ui.input({
|
|
prompt = "Bookmark name: ",
|
|
}, function(input)
|
|
if input then
|
|
local cmd = string.format("jj b c %s", input)
|
|
local _, success = runner.execute_command(cmd, "Error creating bookmark")
|
|
if success then
|
|
utils.notify(string.format("Bookmark `%s` created successfully for @", input), vim.log.levels.INFO)
|
|
M.log({})
|
|
end
|
|
else
|
|
terminal.close_terminal_buffer()
|
|
end
|
|
end)
|
|
end
|
|
|
|
-- Jujutsu delete bookmark
|
|
function M.bookmark_delete()
|
|
if not utils.ensure_jj() then
|
|
return
|
|
end
|
|
|
|
M.log({})
|
|
vim.ui.input({
|
|
prompt = "Bookmark name: ",
|
|
}, function(input)
|
|
if input then
|
|
local cmd = string.format("jj b d %s", input)
|
|
local _, success = runner.execute_command(cmd, "Error deleting bookmark")
|
|
if success then
|
|
utils.notify(string.format("Bookmark `%s` deleted successfully.", input), vim.log.levels.INFO)
|
|
M.log({})
|
|
end
|
|
else
|
|
terminal.close_terminal_buffer()
|
|
end
|
|
end)
|
|
end
|
|
|
|
-- Jujutsu undo
|
|
function M.undo()
|
|
if not utils.ensure_jj() then
|
|
return
|
|
end
|
|
|
|
local cmd = "jj undo"
|
|
local _, success = runner.execute_command(cmd, "Failed to undo")
|
|
if success then
|
|
utils.notify("Command `undo` was succesful.", vim.log.levels.INFO)
|
|
if terminal.state.buf_cmd == "log" then
|
|
M.log({})
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Jujutsu redo
|
|
function M.redo()
|
|
if not utils.ensure_jj() then
|
|
return
|
|
end
|
|
|
|
local cmd = "jj redo"
|
|
local _, success = runner.execute_command(cmd, "Failed to redo")
|
|
if success then
|
|
utils.notify("Command `redo` was succesful.", vim.log.levels.INFO)
|
|
if terminal.state.buf_cmd == "log" then
|
|
M.log({})
|
|
end
|
|
end
|
|
end
|
|
|
|
--- @param args string|string[] jj command arguments
|
|
function M.j(args)
|
|
if not utils.ensure_jj() then
|
|
return
|
|
end
|
|
|
|
if #args == 0 then
|
|
local default_cmd_str, success = runner.execute_command(
|
|
"jj config get ui.default-command",
|
|
"Error getting user's default command",
|
|
nil,
|
|
true
|
|
)
|
|
if not success then
|
|
terminal.run("jj")
|
|
return
|
|
end
|
|
|
|
local default_cmd = parser.parse_default_cmd(default_cmd_str and default_cmd_str or "")
|
|
if default_cmd == nil then
|
|
terminal.run("jj")
|
|
return
|
|
end
|
|
args = default_cmd
|
|
end
|
|
|
|
if type(args) == "string" then
|
|
args = vim.split(args, "%s+")
|
|
end
|
|
|
|
local subcommand = args[1]
|
|
local remaining_args = vim.list_slice(args, 2)
|
|
local cmd = string.format("jj %s", table.concat(args, " "))
|
|
local remaining_args_str = table.concat(remaining_args, " ")
|
|
|
|
local handlers = {
|
|
describe = function()
|
|
M.describe(remaining_args_str ~= "" and remaining_args_str or nil)
|
|
end,
|
|
desc = function()
|
|
M.describe(remaining_args_str ~= "" and remaining_args_str or nil)
|
|
end,
|
|
edit = function()
|
|
if #remaining_args == 0 then
|
|
M.edit()
|
|
else
|
|
terminal.run(cmd)
|
|
end
|
|
end,
|
|
new = function()
|
|
M.new({ show_log = true, args = remaining_args_str, with_input = false })
|
|
end,
|
|
rebase = function()
|
|
M.rebase()
|
|
end,
|
|
undo = function()
|
|
M.undo()
|
|
end,
|
|
redo = function()
|
|
M.redo()
|
|
end,
|
|
}
|
|
|
|
if handlers[subcommand] then
|
|
handlers[subcommand]()
|
|
else
|
|
terminal.run(cmd)
|
|
end
|
|
end
|
|
|
|
-- Handle J command with subcommands and direct jj passthrough
|
|
--- @param opts table Command options from nvim_create_user_command
|
|
local function handle_j_command(opts)
|
|
M.j(opts.fargs)
|
|
end
|
|
|
|
-- Register the J and Jdiff commands
|
|
|
|
function M.register_command()
|
|
vim.api.nvim_create_user_command("J", handle_j_command, {
|
|
nargs = "*",
|
|
complete = function(arglead, _, _)
|
|
local subcommands = {
|
|
"log",
|
|
"status",
|
|
"st",
|
|
"diff",
|
|
"describe",
|
|
"new",
|
|
"squash",
|
|
"bookmark",
|
|
"edit",
|
|
"abandon",
|
|
"b",
|
|
"git",
|
|
"rebase",
|
|
"abandon",
|
|
"undo",
|
|
"redo",
|
|
}
|
|
local matches = {}
|
|
for _, cmd in ipairs(subcommands) do
|
|
if cmd:match("^" .. vim.pesc(arglead)) then
|
|
table.insert(matches, cmd)
|
|
end
|
|
end
|
|
return matches
|
|
end,
|
|
desc = "Execute jj commands with subcommand support",
|
|
})
|
|
|
|
local function create_diff_command(name, fn, desc)
|
|
vim.api.nvim_create_user_command(name, function(opts)
|
|
local rev = opts.fargs[1]
|
|
if rev then
|
|
fn({ rev = rev })
|
|
else
|
|
fn()
|
|
end
|
|
end, { nargs = "?", desc = desc .. " (optionally pass jj revision)" })
|
|
end
|
|
|
|
create_diff_command("Jdiff", diff.open_vdiff, "Vertical diff against jj revision")
|
|
create_diff_command("Jhdiff", diff.open_hdiff, "Horizontal diff against jj revision")
|
|
create_diff_command("Jvdiff", diff.open_vdiff, "Vertical diff against jj revision")
|
|
end
|
|
|
|
return M
|