Files
jj.nvim/lua/jj/cmd/describe.lua
T
NicolasGB 7329e89a85 feat(terminal): add configurable cursor render delay for terminal buffers
Add cursor position restoration with configurable timing to handle terminal
     buffer rendering asynchronously. This fixes cursor column being reset to 0
     when refreshing jj log commands.

     Changes:
     - Add `terminal.cursor_render_delay` configuration option (default: 10ms)
     - Add buffer.get_cursor() and buffer.set_cursor() helpers in core/buffer
       * buffer.set_cursor() uses defer_fn with configurable delay for terminal buffers
       * Automatic position validation with clamping to buffer bounds
       * Line number clamped to [1, line_count]
       * Column clamped to [0, line_length] based on actual line content
       * Validation happens inside deferred callback to check against final rendered content
     - Extract clamp_cursor_position() helper to eliminate code duplication
     - Refactor terminal module to use new buffer cursor helpers
     - Store and restore cursor position when cycling through log commands
     - Update README with terminal configuration section and example usage

     The delay is necessary because nvim_open_term() + nvim_chan_send() have
     asynchronous rendering in the terminal emulator layer. Setting the cursor
     before rendering completes results in the column being reset to 0. Users
     experiencing issues can increase the delay value if needed.
2025-11-25 09:10:12 +01:00

154 lines
4.7 KiB
Lua

--- @class jj.cmd.describe
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")
--- @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
--- Resolve describe editor keymaps from config
--- @return jj.core.buffer.keymap
local function describe_editor_keymaps()
local cmd = require("jj.cmd")
local cfg = cmd.config.describe.editor.keymaps or {}
return cmd.resolve_keymaps_from_specs(cfg, {
close = {
desc = "Close describe editor without saving",
handler = "<cmd>close!<CR>",
},
})
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
local cmd = require("jj.cmd")
-- Use buffer editor mode (defaults to "buffer" if not configured)
local editor_mode = 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 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 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)
-- Split description into lines to preserve multiline descriptions
local description_lines = vim.split(old_description, "\n")
local text = {}
for _, line in ipairs(description_lines) do
table.insert(text, line)
end
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')
-- Check if we're coming from the log view so we can reopen it after editing
local open_log_on_close = terminal.state.buf_cmd == "log"
-- Close the terminal buffer before opening editor
terminal.close_terminal_buffer()
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)
-- Once editing is done, reopen the log if we came from there
end, function()
if open_log_on_close then
vim.schedule(function()
cmd.log({})
end)
end
end, describe_editor_keymaps())
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
cmd.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
return M