Files
jj.nvim/lua/jj/cmd/log.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

228 lines
6.5 KiB
Lua

--- @class jj.cmd.log
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")
--- @class jj.cmd.log_opts
--- @field summary? boolean
--- @field reversed? boolean
--- @field no_graph? boolean
--- @field limit? uinteger
--- @field revisions? string
--- @field raw_flags? string
---@type jj.cmd.log_opts
local default_log_opts = { summary = false, reversed = false, no_graph = false, limit = 20, raw_flags = nil }
--- Jujutsu log
--- @param opts? jj.cmd.log_opts Optional command options
function M.log(opts)
if not utils.ensure_jj() then
return
end
-- If a log was already being displayed before this command we will want to maintain the cursor position
if terminal.state.buf_cmd == "log" then
terminal.store_cursor_position()
end
local jj_cmd = "jj log"
local merged_opts = vim.tbl_extend("force", default_log_opts, opts or {})
-- If a raw has been given simply execute it as is
if merged_opts.raw_flags then
return terminal.run(string.format("%s %s", jj_cmd, merged_opts.raw_flags), M.log_keymaps())
end
for key, value in pairs(merged_opts) do
key = key:gsub("_", "-")
if key == "limit" and value then
jj_cmd = string.format("%s --%s %d", jj_cmd, key, value)
elseif key == "revisions" and value then
jj_cmd = string.format("%s --%s %s", jj_cmd, key, value)
elseif value then
jj_cmd = string.format("%s --%s", jj_cmd, key)
end
end
terminal.run(jj_cmd, M.log_keymaps())
end
---
--- Create a new change relative to the revision under the cursor in a jj log buffer.
--- Behavior:
--- flag == nil -> branch off the current revision
--- flag == "after" -> create a new change after the current revision (-A)
--- If ignore_immut is true, adds --ignore-immutable to the command.
--- Silently returns if no revision is found or the jj command fails.
--- On success, notifies and refreshes the log buffer.
--- @param flag? 'after' Position relative to the current revision; nil to branch off.
--- @param ignore_immut? boolean Pass --ignore-immutable to jj when true.
function M.handle_log_new(flag, ignore_immut)
local line = vim.api.nvim_get_current_line()
local revset = parser.get_rev_from_log_line(line)
if not revset or revset == "" then
return
end
-- Mapping for flag-specific options and messages.
local flag_map = {
after = {
opt = "-A",
err = "Error creating new change after: `%s`",
ok = "Successfully created change after: `%s`",
},
default = {
opt = "",
err = "Error creating new change branching off `%s`",
ok = "Successfully created change branching off `%s`",
},
}
local cfg = flag_map[flag] or flag_map.default
-- Build command parts
local cmd_parts = { "jj", "new" }
if cfg.opt ~= "" then
table.insert(cmd_parts, cfg.opt)
end
table.insert(cmd_parts, revset)
if ignore_immut then
table.insert(cmd_parts, "--ignore-immutable")
end
local cmd = table.concat(cmd_parts, " ")
local _, success = runner.execute_command(cmd, string.format(cfg.err, revset))
if not success then
return
end
utils.notify(string.format(cfg.ok, revset), vim.log.levels.INFO)
-- Refresh the log buffer after creating the change.
require("jj.cmd").log()
end
--- Handle diffing a log line
function M.handle_log_diff()
local line = vim.api.nvim_get_current_line()
local revset = parser.get_rev_from_log_line(line)
if revset then
local cmd = string.format("jj show %s", revset)
terminal.run_floating(cmd, require("jj.cmd").floating_keymaps())
else
utils.notify("No valid revision found in the log line", vim.log.levels.ERROR)
end
end
--- Handle describing a log line
function M.handle_log_describe()
local line = vim.api.nvim_get_current_line()
local revset = parser.get_rev_from_log_line(line)
if revset then
require("jj.cmd").describe(nil, revset)
else
utils.notify("No valid revision found in the log line", vim.log.levels.ERROR)
end
end
--- Handle keypress enter on `jj log` buffer to edit a revision.
--- If ignore_immut is true, adds --ignore-immutable to the command.
--- Silently returns if no revision is found or the jj command fails.
--- On success, notifies and refreshes the log buffer.
--- @param ignore_immut? boolean Pass --ignore-immutable to jj edit when true.
function M.handle_log_enter(ignore_immut)
local line = vim.api.nvim_get_current_line()
local revset = parser.get_rev_from_log_line(line)
if not revset or revset == "" then
return
end
-- If we found a revision, edit it.
-- Build command parts.
local cmd_parts = { "jj", "edit" }
if ignore_immut then
table.insert(cmd_parts, "--ignore-immutable")
end
table.insert(cmd_parts, revset)
-- Build cmd string
local cmd = table.concat(cmd_parts, " ")
-- Try to execute cmd
local _, success = runner.execute_command(cmd, "Error editing change")
if not success then
return
end
utils.notify(string.format("Editing change: `%s`", revset), vim.log.levels.INFO)
-- Close the terminal buffer
terminal.close_terminal_buffer()
end
--- Resolve log keymaps from config, filtering out nil values
--- @return jj.core.buffer.keymap[]
function M.log_keymaps()
local cmd = require("jj.cmd")
-- Reduce repetition by declaring a specification table.
-- Each entry maps the config key name to:
-- desc: Keybind description
-- handler: function to call
-- args: optional list of arguments passed to handler
local cfg = cmd.config.keymaps.log or {}
local specs = {
edit = {
desc = "Checkout revision under cursor",
handler = M.handle_log_enter,
args = { false },
},
edit_immutable = {
desc = "Checkout revision under cursor (ignores immutability)",
handler = M.handle_log_enter,
args = { true },
},
describe = {
desc = "Describe revision under cursor",
handler = M.handle_log_describe,
},
diff = {
desc = "Diff revision under cursor",
handler = M.handle_log_diff,
},
new = {
desc = "Create new change branching off revision under cursor",
handler = M.handle_log_new,
args = { nil, false },
},
new_after = {
desc = "Create new change after revision under cursor",
handler = M.handle_log_new,
args = { "after", false },
},
new_after_immutable = {
desc = "Create new change after revision under cursor (ignore immutable)",
handler = M.handle_log_new,
args = { "after", true },
},
undo = {
desc = "Undo last change",
handler = cmd.undo,
},
redo = {
desc = "Redo last undone change",
handler = cmd.redo,
},
}
return cmd.merge_keymaps(cmd.resolve_keymaps_from_specs(cfg, specs), cmd.terminal_keymaps())
end
return M