mirror of
https://github.com/zoriya/jj.nvim.git
synced 2026-08-06 11:16:08 +00:00
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.
228 lines
6.5 KiB
Lua
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
|