From 71f640be456f33b103e657bcbb0c41057977dc57 Mon Sep 17 00:00:00 2001 From: NicolasGB Date: Tue, 19 May 2026 17:31:34 +0200 Subject: [PATCH] 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. --- lua/jj/core/parser.lua | 59 ++++++++++++++++++------------------- lua/jj/picker.lua | 30 +++++++++++++------ lua/jj/picker/snacks.lua | 1 + lua/jj/ui/editor.lua | 63 ++++++++++++++++------------------------ 4 files changed, 75 insertions(+), 78 deletions(-) diff --git a/lua/jj/core/parser.lua b/lua/jj/core/parser.lua index b32ca0e..41b8c7b 100644 --- a/lua/jj/core/parser.lua +++ b/lua/jj/core/parser.lua @@ -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, diff --git a/lua/jj/picker.lua b/lua/jj/picker.lua index 04f2c56..4d4c239 100644 --- a/lua/jj/picker.lua +++ b/lua/jj/picker.lua @@ -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 diff --git a/lua/jj/picker/snacks.lua b/lua/jj/picker/snacks.lua index 3390773..6435e1b 100644 --- a/lua/jj/picker/snacks.lua +++ b/lua/jj/picker/snacks.lua @@ -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() diff --git a/lua/jj/ui/editor.lua b/lua/jj/ui/editor.lua index 259e5b4..af08095 100644 --- a/lua/jj/ui/editor.lua +++ b/lua/jj/ui/editor.lua @@ -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,