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.
This commit is contained in:
NicolasGB
2026-05-19 18:05:17 +02:00
parent 5558c22471
commit 71f640be45
4 changed files with 75 additions and 78 deletions
+28 -31
View File
@@ -55,50 +55,47 @@ end
--- Handles renamed files and regular status lines.
--- @return {old_path : string, new_path : string, is_rename : boolean}|nil A table with , or nil if parsing fails
function M.parse_file_info_from_status_line(line)
-- Handle renamed files: "R path/{old_name => new_name}" or "R old_path => new_path"
local rename_pattern_curly = "^R (.*)/{(.*) => ([^}]+)}"
local dir_path, old_name, new_name = line:match(rename_pattern_curly)
if not line then
return nil
end
line = vim.trim(line)
-- Handle renamed files in nested path form:
-- R dir/{old_name => new_name}
local dir_path, old_name, new_name = line:match("^R%s+(.*)/{(.*)%s=>%s([^}]+)}$")
if dir_path and old_name and new_name then
return {
old_path = dir_path .. "/" .. old_name,
new_path = dir_path .. "/" .. new_name,
is_rename = true,
}
else
-- Try simple rename pattern: "R old_path => new_path"
local rename_pattern_simple = "^R (.*) => (.+)$"
local old_path, new_path = line:match(rename_pattern_simple)
if old_path and new_path then
return {
old_path = old_path,
new_path = new_path,
is_rename = true,
}
end
end
-- Not a rename, try regular status patterns
local filepath
-- Handle renamed files: "R path/{old_name => new_name}" or "R old_path => new_path"
local rename_pattern_curly_new = "^R (.*)/{.* => ([^}]+)}"
local dir_path_new, renamed_file = line:match(rename_pattern_curly_new)
if dir_path_new and renamed_file then
filepath = dir_path_new .. "/" .. renamed_file
else
-- Try simple rename pattern: "R old_path => new_path"
local rename_pattern_simple_new = "^R .* => (.+)$"
filepath = line:match(rename_pattern_simple_new)
-- Handle renamed files in top-level brace form:
-- R {old_name => new_name}
local old_top, new_top = line:match("^R%s+{(.-)%s=>%s([^}]+)}$")
if old_top and new_top then
return {
old_path = old_top,
new_path = new_top,
is_rename = true,
}
end
if not filepath then
-- jj status format: "M filename" or "A filename"
-- Match lines that start with status letter followed by space and filename
local pattern = "^[MAD?!] (.+)$"
filepath = line:match(pattern)
-- Handle simple rename form:
-- R old_path => new_path
local old_path, new_path = line:match("^R%s+(.+)%s=>%s(.+)$")
if old_path and new_path then
return {
old_path = vim.trim(old_path),
new_path = vim.trim(new_path),
is_rename = true,
}
end
-- Regular status lines (M/A/D/?/!)
local filepath = line:match("^[MAD?!]%s+(.+)$")
if filepath then
return {
old_path = filepath,
+21 -9
View File
@@ -1,5 +1,6 @@
local utils = require("jj.utils")
local runner = require("jj.core.runner")
local parser = require("jj.core.parser")
--- @class jj.picker
@@ -7,8 +8,9 @@ local runner = require("jj.core.runner")
--- @field snacks table|boolean The snacks config
--- @class jj.picker.file
--- @field file string The path of the file
--- @field change string The type of change in the file
--- @field file string The current path of the file
--- @field status string JJ-style status code (e.g. "M ", "R ") for picker formatting
--- @field rename? string Previous path when this item is a rename
--- @field diff_cmd string The command to get the diff of the file
--- @class jj.picker.log_line
@@ -51,14 +53,24 @@ local function get_files()
local lines = vim.split(diff_ouptut, "\n", { trimempty = true })
for _, line in ipairs(lines) do
local change, file_path = line:match("^(%a)%s(.+)$")
local change = line:match("^(%a)%s")
local file_info = parser.parse_file_info_from_status_line(line)
table.insert(files, {
text = file_path,
file = file_path,
change = change,
diff_cmd = string.format("jj diff %s", file_path),
})
if change and file_info and file_info.new_path then
local file_path = file_info.new_path
local item = {
text = line:sub(3),
file = file_path,
status = change .. " ",
diff_cmd = string.format("jj diff %s", vim.fn.shellescape(file_path)),
}
if change == "R" and file_info.old_path and file_info.old_path ~= file_info.new_path then
item.rename = file_info.old_path
end
table.insert(files, item)
end
end
return files
+1
View File
@@ -28,6 +28,7 @@ function M.status(opts, files)
source = "jj",
items = files,
title = "JJ Status",
format = "git_status",
actions = {
open_and_diff = function(picker, item)
picker:close()
+25 -38
View File
@@ -51,7 +51,8 @@ local function init_highlights()
vim.api.nvim_set_hl(0, "Removed", M.opts.highlights.deleted)
end
-- this one will always be executed since the default nvim highlight group does not exist for renames
-- 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
@@ -79,40 +80,28 @@ function M.open_editor(initial_text, on_write, on_unload, keymaps)
-- Initialize highlight groups once
init_highlights()
-- Create a namespace for our 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")
-- Function to apply highlights to the buffer
-- Apply explicit highlight for renamed entries in JJ header lines.
-- Example line: "JJ: R path/to/file"
local function apply_highlights(buf)
-- Clear existing highlights
vim.api.nvim_buf_clear_namespace(buf, ns_id, 0, -1)
if not vim.api.nvim_buf_is_valid(buf) then
return
end
-- Get all lines
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 line_idx = i - 1 -- 0-indexed
-- First, check if line starts with JJ: and highlight it as comment
if line:match("^JJ:") then
-- Then check for rename status indicator
local status_pos = line:find("[R] ", 4) -- Find status after "JJ:"
if status_pos then
local status = line:sub(status_pos, status_pos) -- Get the status character
local hl_group = nil
if status == "R" then
hl_group = "jjRenamed"
end
if hl_group then
-- Highlight from the status character to the end of the line
vim.api.nvim_buf_set_extmark(buf, ns_id, line_idx, status_pos - 1, {
end_col = #line,
hl_group = hl_group,
})
end
end
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
@@ -124,10 +113,16 @@ function M.open_editor(initial_text, on_write, on_unload, keymaps)
-- Set buffer content
vim.api.nvim_buf_set_lines(buf, 0, -1, false, initial_text)
-- Apply highlights initially
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()
@@ -142,14 +137,6 @@ function M.open_editor(initial_text, on_write, on_unload, keymaps)
end)
end
-- Reapply highlights when text changes
vim.api.nvim_create_autocmd({ "TextChanged", "TextChangedI" }, {
buffer = buf,
callback = function()
apply_highlights(buf)
end,
})
-- Handle :w and :wq commands
vim.api.nvim_create_autocmd("BufWriteCmd", {
buffer = buf,