Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,14 @@ will have the index buffer on the left side, and the entries under "Staged
changes" will have it on the right side). Once you write to an index buffer the
index will be updated.

For operating on entire files, you can use the file panel with multi-file
selection. Press `w` on a file to toggle selection (marked with ✓), then press
`-` or `s` to stage/unstage, or `X` to restore/discard all selected files at
once. If no files are selected, operations will work on the file under the
cursor. After performing an operation, all selections are automatically cleared.
This is useful when you need to operate on multiple files that are not
consecutive in the list.

### `:[range]DiffviewFileHistory [paths] [options]`

Opens a new file history view that lists all commits that affected the given
Expand Down
53 changes: 44 additions & 9 deletions doc/diffview.txt
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,17 @@ COMMANDS *diffview-commands*
entries under "Staged changes" will have it on the right side). Once
you write to an index buffer the index will be updated.

For operating on entire files, you can use the file panel with multi-
file selection. Press `w` on a file to toggle selection (marked with
✓), then press `-` or `s` to stage/unstage, or `X` to restore/discard
all selected files at once. If no files are selected, operations will
work on the file under the cursor. After performing an operation, all
selections are automatically cleared. This is useful when you need to
operate on multiple files that are not consecutive in the list.

See also: |diffview-actions-toggle_select_entry|,
|diffview-actions-toggle_stage_entry|, |diffview-actions-restore_entry|

*diffview-merge-tool*
If you call `:DiffviewOpen` during a merge or a rebase, the view will
list the conflicted files in their own section. When opening a
Expand Down Expand Up @@ -1129,7 +1140,10 @@ restore_entry *diffview-actions-restore_entry*

Revert the selected file entry to the state from the left side of
the diff. This only works if the right side of the diff is showing
the local state of the file.
the local state of the file. If multiple files are selected using
|diffview-actions-toggle_select_entry|, this will restore all
selected files at once. After restoring selected files, the
selection will be automatically cleared.

NOTE: "Restoring" an untracked file will delete it from disk. It
still gets backed up in the local git database, so it is
Expand Down Expand Up @@ -1200,6 +1214,17 @@ stage_all *diffview-actions-stage_all*

Stage all changes.

toggle_select_entry *diffview-actions-toggle_select_entry*
Contexts: `file_panel`

Toggle file selection for multi-file operations. Selected files are
marked with a checkmark (✓) in the file panel. When multiple files are
selected, actions like |diffview-actions-toggle_stage_entry| and
|diffview-actions-restore_entry| will operate on all selected files at
once. After performing an operation, the selection is automatically
cleared. The cursor will automatically move to the next file entry
after toggling selection.

toggle_files *diffview-actions-toggle_files*
Contexts: `view`, `file_panel`, `file_history_panel`

Expand All @@ -1219,7 +1244,10 @@ toggle_fold *diffview-actions-toggle_fold*
toggle_stage_entry *diffview-actions-toggle_stage_entry*
Contexts: `diff_view`, `file_panel`

Stage / unstage the subject.
Stage / unstage the subject. If multiple files are selected using
|diffview-actions-toggle_select_entry|, this will stage/unstage all
selected files at once. After staging selected files, the selection
will be automatically cleared.

unstage_all *diffview-actions-unstage_all*
Contexts: `diff_view`, `file_panel`
Expand Down Expand Up @@ -1381,8 +1409,15 @@ o Open the diff for the selected file entry.
<CR>
<2-LeftMouse>

*diffview-maps-toggle_select_entry*
w Toggle file selection for multi-file operations. Selected
files are marked with a checkmark (✓). When files are
selected, operations like staging (-/s) and restoring (X)
will apply to all selected files.

*diffview-maps-toggle_stage_entry*
- Stage/unstage the selected file entry.
- Stage/unstage the selected file entry. If multiple files
are selected, this will stage/unstage all selected files.

*diffview-maps-stage_all*
S Stage all entries.
Expand All @@ -1391,12 +1426,12 @@ S Stage all entries.
U Unstage all entries.

*diffview-maps-restore_entry*
X Revert the selected file entry to the state from the
left side of the diff. This only works if the right
side of the diff is showing the local state of the
file. A command is echoed that shows how to undo the
change. Check |:messages| or |:DiffviewLog| to see it
again.
X Revert the selected file entry (or all selected files
if multiple are selected) to the state from the left
side of the diff. This only works if the right side of
the diff is showing the local state of the file. A
command is echoed that shows how to undo the change.
Check |:messages| or |:DiffviewLog| to see it again.

*diffview-maps-refresh_files*
R Update the stats and entries in the file list.
Expand Down
1 change: 1 addition & 0 deletions doc/diffview_defaults.txt
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ DEFAULT CONFIG *diffview.defaults*
{ "n", "o", actions.select_entry, { desc = "Open the diff for the selected entry" } },
{ "n", "l", actions.select_entry, { desc = "Open the diff for the selected entry" } },
{ "n", "<2-LeftMouse>", actions.select_entry, { desc = "Open the diff for the selected entry" } },
{ "n", "w", actions.toggle_select_entry, { desc = "Toggle file selection (for multi-file operations)" } },
{ "n", "-", actions.toggle_stage_entry, { desc = "Stage / unstage the selected entry" } },
{ "n", "s", actions.toggle_stage_entry, { desc = "Stage / unstage the selected entry" } },
{ "n", "S", actions.stage_all, { desc = "Stage all entries" } },
Expand Down
1 change: 1 addition & 0 deletions lua/diffview/actions.lua
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,7 @@ local action_names = {
"toggle_files",
"toggle_flatten_dirs",
"toggle_fold",
"toggle_select_entry",
"toggle_stage_entry",
"unstage_all",
}
Expand Down
1 change: 1 addition & 0 deletions lua/diffview/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ M.defaults = {
{ "n", "<c-f>", actions.scroll_view(0.25), { desc = "Scroll the view down" } },
{ "n", "<tab>", actions.select_next_entry, { desc = "Open the diff for the next file" } },
{ "n", "<s-tab>", actions.select_prev_entry, { desc = "Open the diff for the previous file" } },
{ "n", "w", actions.toggle_select_entry, { desc = "Toggle file selection (for multi-file operations)" } },
{ "n", "[F", actions.select_first_entry, { desc = "Open the diff for the first file" } },
{ "n", "]F", actions.select_last_entry, { desc = "Open the diff for the last file" } },
{ "n", "gf", actions.goto_file_edit, { desc = "Open the file in the previous tabpage" } },
Expand Down
2 changes: 2 additions & 0 deletions lua/diffview/scene/file_entry.lua
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ local fstat_cache = {}
---@field merge_ctx vcs.MergeContext?
---@field active boolean
---@field opened boolean
---@field selected boolean
local FileEntry = oop.create_class("FileEntry")

---@class FileEntry.init.Opt
Expand Down Expand Up @@ -75,6 +76,7 @@ function FileEntry:init(opt)
self.merge_ctx = opt.merge_ctx
self.active = false
self.opened = false
self.selected = false
end

function FileEntry:destroy()
Expand Down
32 changes: 32 additions & 0 deletions lua/diffview/scene/views/diff/file_panel.lua
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ local M = {}
---@field components CompStruct
---@field constrain_cursor function
---@field help_mapping string
---@field selected_files table<FileEntry, boolean>
local FilePanel = oop.create_class("FilePanel", Panel)

FilePanel.winopts = vim.tbl_extend("force", Panel.winopts, {
Expand Down Expand Up @@ -58,6 +59,7 @@ function FilePanel:init(adapter, files, path_args, rev_pretty_name)
self.rev_pretty_name = rev_pretty_name
self.listing_style = conf.file_panel.listing_style
self.tree_options = conf.file_panel.tree_options
self.selected_files = {}

self:on_autocmd("BufNew", {
callback = function()
Expand Down Expand Up @@ -400,5 +402,35 @@ function FilePanel:render()
require("diffview.scene.views.diff.render")(self)
end

---Toggle selection for a file entry
---@param file FileEntry
function FilePanel:toggle_file_selection(file)
if self.selected_files[file] then
self.selected_files[file] = nil
file.selected = false
else
self.selected_files[file] = true
file.selected = true
end
end

---Get all currently selected files as a list
---@return FileEntry[]
function FilePanel:get_selected_files()
local selected = {}
for file, _ in pairs(self.selected_files) do
table.insert(selected, file)
end
return selected
end

---Clear all file selections
function FilePanel:clear_selections()
for file, _ in pairs(self.selected_files) do
file.selected = false
end
self.selected_files = {}
end

M.FilePanel = FilePanel
return M
127 changes: 105 additions & 22 deletions lua/diffview/scene/views/diff/listeners.lua
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,21 @@ local await = async.await

---@param view DiffView
return function(view)
-- Helper function to get selected files or fall back to current file
local function get_target_files()
local selected_files = view.panel:get_selected_files()
if #selected_files > 0 then
return selected_files
end

local item = view:infer_cur_file(true)
if item then
return { item }
end

return {}
end

return {
tab_enter = function()
local file = view.panel.cur_file
Expand Down Expand Up @@ -102,6 +117,20 @@ return function(view)
end
end
end,
toggle_select_entry = function()
if view.panel:is_open() then
---@type any
local item = view.panel:get_item_at_cursor()
if item and type(item.collapsed) ~= "boolean" then
---@cast item FileEntry
view.panel:toggle_file_selection(item)
view.panel:render()
view.panel:redraw()
-- Move cursor to next file
view.panel:highlight_next_file()
end
end
end,
focus_entry = function()
if view.panel:is_open() then
---@type any
Expand Down Expand Up @@ -132,20 +161,51 @@ return function(view)
return
end

local item = view:infer_cur_file(true)
if item then
local success
if item.kind == "working" or item.kind == "conflicting" then
success = view.adapter:add_files({ item.path })
elseif item.kind == "staged" then
success = view.adapter:reset_files({ item.path })
local items_to_process = get_target_files()
if #items_to_process == 0 then
return
end

-- Process each item
local files_to_stage = {}
local files_to_unstage = {}

for _, item in ipairs(items_to_process) do
-- Skip directories
if type(item.collapsed) ~= "boolean" then
---@cast item FileEntry
if item.kind == "working" or item.kind == "conflicting" then
table.insert(files_to_stage, item.path)
elseif item.kind == "staged" then
table.insert(files_to_unstage, item.path)
end
end
end

-- Stage files
if #files_to_stage > 0 then
local success = view.adapter:add_files(files_to_stage)
if not success then
utils.err("Failed to stage files!")
return
end
end

-- Unstage files
if #files_to_unstage > 0 then
local success = view.adapter:reset_files(files_to_unstage)
if not success then
utils.err(("Failed to stage/unstage file: '%s'"):format(item.path))
utils.err("Failed to unstage files!")
return
end
end

-- Clear selections after staging
view.panel:clear_selections()

-- Navigate to next file if only one item was processed
if #items_to_process == 1 then
local item = items_to_process[1]
if type(item.collapsed) == "boolean" then
---@cast item DirData
---@type FileTree
Expand Down Expand Up @@ -180,14 +240,14 @@ return function(view)
view.panel:set_cur_file(item)
view:next_file()
end

view:update_files(
vim.schedule_wrap(function()
view.panel:highlight_cur_file()
end)
)
view.emitter:emit(EventName.FILES_STAGED, view)
end

view:update_files(
vim.schedule_wrap(function()
view.panel:highlight_cur_file()
end)
)
view.emitter:emit(EventName.FILES_STAGED, view)
end,
stage_all = function()
local args = vim.tbl_map(function(file)
Expand Down Expand Up @@ -226,22 +286,45 @@ return function(view)
end

local commit

if view.left.type ~= RevType.STAGE then
commit = view.left.commit
end

local file = view:infer_cur_file()
if not file then return end
local items_to_process = get_target_files()
if #items_to_process == 0 then
return
end

local bufid = utils.find_file_buffer(file.path)
-- Filter to only file entries (not directories)
local files_to_restore = {}
for _, item in ipairs(items_to_process) do
if type(item.collapsed) ~= "boolean" then
---@cast item FileEntry
table.insert(files_to_restore, item)
end
end

if bufid and vim.bo[bufid].modified then
utils.err("The file is open with unsaved changes! Aborting file restoration.")
if #files_to_restore == 0 then
return
end

await(vcs_utils.restore_file(view.adapter, file.path, file.kind, commit))
-- Check for unsaved changes in any of the files
for _, file in ipairs(files_to_restore) do
local bufid = utils.find_file_buffer(file.path)
if bufid and vim.bo[bufid].modified then
utils.err(("File '%s' is open with unsaved changes! Aborting file restoration."):format(file.path))
return
end
end

-- Restore all files
for _, file in ipairs(files_to_restore) do
await(vcs_utils.restore_file(view.adapter, file.path, file.kind, commit))
end

-- Clear selections after restoring
view.panel:clear_selections()

view:update_files()
end),
listing_style = function()
Expand Down
5 changes: 5 additions & 0 deletions lua/diffview/scene/views/diff/render.lua
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ local function render_file(comp, show_path, depth)
comp:add_text(string.rep(" ", depth * 2 + 2))
end

-- Add selection indicator
if file.selected then
comp:add_text("✓ ", "DiffviewFilePanelConflicts")
end

local icon, icon_hl = hl.get_file_icon(file.basename, file.extension)
comp:add_text(icon, icon_hl)
comp:add_text(file.basename, file.active and "DiffviewFilePanelSelected" or "DiffviewFilePanelFileName")
Expand Down