Files
NicolasGB 71f640be45 feat(status,highlights): Handle renamed files in restore and picker flows
- Improve status-line parsing to support top-level and nested rename 
        syntaxes, so actions no longer include trailing braces in paths.
    - Update the Snacks status picker integration to pass structured 
        rename/status metadata and use git_status formatting, preserving 
        icons/highlighting while showing rename pairs correctly.
    - Also tighten renamed-line highlighting in the editor buffer for consistent jjRenamed behavior.
2026-05-19 18:05:17 +02:00

210 lines
6.4 KiB
Lua

--- @class jj.ui.editor
local M = {}
local buffer = require("jj.core.buffer")
--- @class jj.ui.editor.opts
--- @field auto_insert? boolean Smart insert: enter insert mode when description is empty, stay in normal mode when one exists
--- @field highlights? jj.ui.editor.highlights
--- @field window? jj.ui.editor.window
--- @class jj.ui.editor.highlights
---@field added? table Highlight settings for added lines
---@field modified? table Highlight settings for modified lines
---@field deleted? table Highlight settings for deleted lines
---@field renamed? table Highlight settings for renamed lines
--- @class jj.ui.editor.window
--- @field type? "hsplit"|"vsplit"|"floating"|"tab" Type of window the terminal is displayed in
--- @field split_size? number Size % of the split window, either height (hsplit) or width (vsplit) (between 0.1 and 1.0)
--- @field floating_width? number Width % of the floating window (between 0.1 and 1.0)
--- @field floating_height? number Height % of the floating window (between 0.1 and 1.0)
--- @type jj.ui.editor.opts
M.opts = {
highlights = {
-- Only init this one by default since it's not handled natively by neovim
renamed = { fg = "#d29922", ctermfg = "Yellow" },
},
auto_insert = true,
}
--- Private module state to track highlights initialization
local highlights_initialized = false
-- Initialize highlight groups once
local function init_highlights()
if highlights_initialized then
return
end
-- Override the highlight groups if user provided custom settings
if M.opts.highlights.added then
vim.api.nvim_set_hl(0, "Added", M.opts.highlights.added)
end
if M.opts.highlights.modified then
vim.api.nvim_set_hl(0, "Changed", M.opts.highlights.modified)
end
if M.opts.highlights.deleted then
vim.api.nvim_set_hl(0, "Removed", M.opts.highlights.deleted)
end
-- Neovim's jj syntax uses jjRenamed for renamed files.
-- Also define Renamed for backward compatibility with existing user configs.
if M.opts.highlights.renamed then
vim.api.nvim_set_hl(0, "jjRenamed", M.opts.highlights.renamed)
end
highlights_initialized = true
end
--- Setup function to configure highlights and other options
---@param user_opts? jj.ui.editor.opts Configuration options
function M.setup(user_opts)
M.opts = vim.tbl_deep_extend("force", M.opts, user_opts or {})
-- Reset highlights flag to force re-initialization with new highlights
if highlights_initialized then
highlights_initialized = false
init_highlights()
end
end
---@param initial_text string[] Lines to initialize the buffer with
---@param on_write fun(buf: string[])? Optional callback called with user text on buffer write
---@param on_unload? fun(buf: string[])? Optional callback with user text called when the buffer is closed
---@param keymaps? jj.core.buffer.keymap[] Optional keymaps for the buffer
function M.open_editor(initial_text, on_write, on_unload, keymaps)
-- Initialize highlight groups once
init_highlights()
-- Create a namespace for explicit renamed-line highlighting (independent of syntax file support)
local ns_id = vim.api.nvim_create_namespace("jj_describe_highlights")
-- Apply explicit highlight for renamed entries in JJ header lines.
-- Example line: "JJ: R path/to/file"
local function apply_highlights(buf)
if not vim.api.nvim_buf_is_valid(buf) then
return
end
vim.api.nvim_buf_clear_namespace(buf, ns_id, 0, -1)
local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false)
for i, line in ipairs(lines) do
local status_pos = line:match("^JJ:%s+()R%s")
if status_pos then
-- Highlight only the status letter (R), like A/M/D syntax groups do.
vim.api.nvim_buf_set_extmark(buf, ns_id, i - 1, status_pos - 1, {
end_col = status_pos,
hl_group = "jjRenamed",
priority = 200,
})
end
end
end
-- Create buffer
local buf = M.create_buffer()
-- Set keymaps before setting content to avoid triggering them during setup
buffer.set_keymaps(buf, keymaps or {})
-- Set buffer content
vim.api.nvim_buf_set_lines(buf, 0, -1, false, initial_text)
apply_highlights(buf)
-- Keep explicit highlights up-to-date while editing
vim.api.nvim_create_autocmd({ "TextChanged", "TextChangedI", "BufEnter" }, {
buffer = buf,
callback = function()
apply_highlights(buf)
end,
})
-- Smart insert mode: insert when description is empty, normal mode otherwise
if M.opts.auto_insert then
vim.schedule(function()
if not vim.api.nvim_buf_is_valid(buf) then
return
end
local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false)
local desc = require("jj.utils").extract_description_from_describe(lines)
if not desc or desc == "" then
vim.cmd("startinsert")
end
end)
end
-- Handle :w and :wq commands
vim.api.nvim_create_autocmd("BufWriteCmd", {
buffer = buf,
callback = function()
-- Get current buffer lines
local buf_lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false)
-- Update the last written lines variable
vim.b[buf].jj_last_written_lines = buf_lines
vim.bo[buf].modified = false
-- Call the on_write callback if provided
if on_write then
on_write(buf_lines)
end
end,
})
-- Register the on_close callback
if on_unload then
vim.api.nvim_create_autocmd("BufWipeout", {
buffer = buf,
callback = function()
local last_written = vim.b[buf].jj_last_written_lines
on_unload(last_written)
end,
})
end
end
--- Create the buffer based on the module options.
--- @return number buf Buffer number of the created buffer
function M.create_buffer()
if M.opts.window.type == "floating" then
local buf, _ = buffer.create_float({
title = "JJ editor",
title_pos = "center",
filetype = "jjdescription",
buftype = "acwrite",
bufhidden = "wipe",
enter = true,
modifiable = true,
height = math.floor(vim.o.lines * M.opts.window.floating_height),
width = math.floor(vim.o.columns * M.opts.window.floating_width),
win_options = {
wrap = true,
number = false,
relativenumber = false,
cursorline = false,
signcolumn = "no",
winfixbuf = true,
},
})
return buf
else
-- Get the lines/columns based on the direction of the split
local full_size = M.opts.window.type == "hsplit" and vim.o.lines or vim.o.columns
return buffer.create({
name = "jj:///DESCRIBE_EDITMSG",
filetype = "jjdescription",
buftype = "acwrite",
bufhidden = "wipe",
modifiable = true,
split = buffer.resolve_split(M.opts.window.type),
size = math.floor(full_size * M.opts.window.split_size),
})
end
end
return M