From 3d10ae14bedf630988118caa0fbe4fb0ae8ac444 Mon Sep 17 00:00:00 2001 From: Nicolas GB Date: Fri, 24 Jul 2026 08:47:24 -0700 Subject: [PATCH] fix(status): Make restoring files available in visual mode (#144) --- README.md | 15 +++++---- lua/jj/cmd/log.lua | 6 +--- lua/jj/cmd/status.lua | 69 ++++++++++++++++++++++++++++-------------- lua/jj/core/parser.lua | 8 ++++- lua/jj/utils.lua | 22 ++++++++++++++ 5 files changed, 85 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index b65b0e9..cbdfb8c 100644 --- a/README.md +++ b/README.md @@ -372,15 +372,18 @@ Open the current buffer's file in your browser on the hosted remote (GitHub/GitL In Visual mode, select lines and run `:Jbrowse` to open a range. -### Open a changed file +### Open or restore changed files + +The `status` buffer supports these default keybindings: + +- `` — Open the file under the cursor in the current window. +- `` — Restore the file under the cursor. +- Visual mode + `` — Restore every changed file in the selected status lines at once. + +Restoring multiple files runs one `jj restore` command, refreshes the status buffer when it succeeds, and reports every restored path. Renamed files are handled correctly: both the old and new paths are restored. -Just press enter to open a file from the `status` output in your current window. ![Open-status](https://github.com/NicolasGB/jj.nvim/raw/main/assets/enter-status.gif) -### Restore a changed file - -Press `` on a file from the `status` output and that's it, it's restored. - ![Restore-status](https://github.com/NicolasGB/jj.nvim/raw/main/assets/x-status.gif) ### Status picker (Snacks.nvim) diff --git a/lua/jj/cmd/log.lua b/lua/jj/cmd/log.lua index cbe91ab..cf166db 100644 --- a/lua/jj/cmd/log.lua +++ b/lua/jj/cmd/log.lua @@ -188,11 +188,7 @@ local function get_highlight_marks() if mode == "v" or mode == "V" then -- Visual mode: get selected lines - local selected_start_line = vim.fn.line("v") - local selected_end_line = vim.fn.line(".") - if selected_start_line > selected_end_line then - selected_start_line, selected_end_line = selected_end_line, selected_start_line - end + local _, selected_start_line, selected_end_line = utils.get_visual_selection(buf) -- Get the lines based on revision content local start_line = get_prev_revision_line(selected_start_line) or selected_start_line diff --git a/lua/jj/cmd/status.lua b/lua/jj/cmd/status.lua index fc37049..f76d160 100644 --- a/lua/jj/cmd/status.lua +++ b/lua/jj/cmd/status.lua @@ -10,35 +10,54 @@ local jj_args = require("jj.core.args") --- Handle restoring a file from the jj status buffer --- Supports both renamed and non-renamed files function M.handle_status_restore() - local file_info = parser.parse_file_info_from_status_line(vim.api.nvim_get_current_line()) - if not file_info then + local lines = utils.get_visual_selection(terminal.state.buf) + if not lines then + lines = { vim.api.nvim_get_current_line() } + end + + --- @type jj.core.parser.status_file[] + local files = {} + + for _, line in ipairs(lines) do + local file_info = parser.parse_file_info_from_status_line(line) + if file_info then + table.insert(files, file_info) + end + end + + -- Return early if no files were found in the selection + if #files == 0 then + utils.notify("No files selected to restore", vim.log.levels.WARN) return end - if file_info.is_rename then - local restore_cmd = { - "jj", - "restore", - "--from", - "@-", - jj_args.fileset(file_info.old_path), - jj_args.fileset(file_info.new_path), - } - - local _, restore_success = runner.execute(restore_cmd, "Failed to restore original file") - if restore_success then - utils.notify("Reverted rename: " .. file_info.new_path .. " -> " .. file_info.old_path, vim.log.levels.INFO) - require("jj.cmd").status() + -- For each file found restore it + local cmd = { "jj", "restore" } + for _, file in ipairs(files) do + if file.is_rename then + -- If it's a rename add both old and new paths to the restore command + vim.list_extend(cmd, { jj_args.fileset(file.old_path), jj_args.fileset(file.new_path) }) + else + table.insert(cmd, jj_args.fileset(file.old_path)) end - else - -- For non-renamed files, use regular restore - local restore_cmd = { "jj", "restore", jj_args.fileset(file_info.old_path) } + end - local _, success = runner.execute(restore_cmd, "Failed to restore") - if success then - utils.notify("Restored: `" .. file_info.old_path .. "`", vim.log.levels.INFO) - require("jj.cmd").status() + local _, restore_success = runner.execute(cmd, "Failed to restore original file") + if restore_success then + local notif_msg = "Restored file:\n" + if #files > 1 then + notif_msg = "Restored files:\n" end + + for _, file in ipairs(files) do + if file.is_rename then + notif_msg = notif_msg .. "- `" .. file.old_path .. "` -> `" .. file.new_path .. "`\n" + else + notif_msg = notif_msg .. "- `" .. file.old_path .. "`\n" + end + end + utils.notify(notif_msg, vim.log.levels.INFO) + M.status() -- Refresh the status buffer after restoring files end end @@ -73,14 +92,18 @@ end function M.status_keymaps() local cmd = require("jj.cmd") local cfg = cmd.config.keymaps.status or {} + + --- @type jj.cmd.keymap_specs local specs = { open_file = { desc = "Open file under cursor", handler = M.handle_status_enter, + modes = { "n" }, }, restore_file = { desc = "Restore file under cursor", handler = M.handle_status_restore, + modes = { "n", "v" }, }, } diff --git a/lua/jj/core/parser.lua b/lua/jj/core/parser.lua index 1cbcf72..10743fb 100644 --- a/lua/jj/core/parser.lua +++ b/lua/jj/core/parser.lua @@ -1,4 +1,10 @@ --- @class jj.core.parser + +--- @class jj.core.parser.status_file +--- @field old_path string The original path of the file +--- @field new_path string The new path of the file (if renamed) +--- @field is_rename boolean Whether the file was renamed + local M = {} --- Checks if the given value is a list of strings. @@ -67,7 +73,7 @@ end --- Parse the current line in the jj status buffer to extract file information. --- 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 +--- @return jj.core.parser.status_file|nil A table with , or nil if parsing fails function M.parse_file_info_from_status_line(line) if not line then return nil diff --git a/lua/jj/utils.lua b/lua/jj/utils.lua index ea5265f..390597d 100644 --- a/lua/jj/utils.lua +++ b/lua/jj/utils.lua @@ -1064,4 +1064,26 @@ function M.open_first_conflicted_file(revset) end end +--- Extract selected lines from visual mode, and the marks for the start and end lines. +--- @param bufnr integer The buffer number to get the selection from +--- @return string[]|nil The selected lines, or nil if not in visual mode +--- @return integer|nil The start line of the selection (1-based) +--- @return integer|nil The end line of the selection (1-based) +function M.get_visual_selection(bufnr) + local mode = vim.fn.mode() + if mode ~= "v" and mode ~= "V" and mode ~= "\22" then + return nil, nil, nil + end + + local selected_start_line = vim.fn.line("v") + local selected_end_line = vim.fn.line(".") + if selected_start_line > selected_end_line then + selected_start_line, selected_end_line = selected_end_line, selected_start_line + end + + local lines = vim.api.nvim_buf_get_lines(bufnr, selected_start_line - 1, selected_end_line, false) + + return lines, selected_start_line, selected_end_line +end + return M