From f2495a6f98524f589b384db91d938c96c3c7819e Mon Sep 17 00:00:00 2001 From: Korbin de Man <113640462+korbindeman@users.noreply.github.com> Date: Fri, 19 Dec 2025 11:12:01 +0100 Subject: [PATCH] Add Restore File action in project_panel for git modified files (#42490) Co-authored-by: cameron --- Cargo.lock | 1 + crates/project_panel/Cargo.toml | 1 + crates/project_panel/src/project_panel.rs | 115 ++++++++++++++++++++++ 3 files changed, 117 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 3f7077e721e934cd6cb05af0cdaefef75602b429..4beb6c11f427fb86b5586c2833c50b7cd5b9dd01 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12570,6 +12570,7 @@ dependencies = [ "gpui", "language", "menu", + "notifications", "pretty_assertions", "project", "rayon", diff --git a/crates/project_panel/Cargo.toml b/crates/project_panel/Cargo.toml index 2c47efd0b0e2490bbfd6125069fa5ca1438ffb51..0385c3789e923da95a1eca7a5a469bad00020639 100644 --- a/crates/project_panel/Cargo.toml +++ b/crates/project_panel/Cargo.toml @@ -45,6 +45,7 @@ workspace.workspace = true language.workspace = true zed_actions.workspace = true telemetry.workspace = true +notifications.workspace = true [dev-dependencies] client = { workspace = true, features = ["test-support"] } diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 00aba96ef428eea643e8868e513ab9c3aaa1b910..43f63d90789a65bce54814f3adbc6f1d53235568 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -29,6 +29,7 @@ use gpui::{ }; use language::DiagnosticSeverity; use menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrevious}; +use notifications::status_toast::{StatusToast, ToastIcon}; use project::{ Entry, EntryKind, Fs, GitEntry, GitEntryRef, GitTraversal, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId, @@ -1140,6 +1141,12 @@ impl ProjectPanel { "Copy Relative Path", Box::new(zed_actions::workspace::CopyRelativePath), ) + .when(!is_dir && self.has_git_changes(entry_id), |menu| { + menu.separator().action( + "Restore File", + Box::new(git::RestoreFile { skip_prompt: false }), + ) + }) .when(has_git_repo, |menu| { menu.separator() .action("View File History", Box::new(git::FileHistory)) @@ -1180,6 +1187,19 @@ impl ProjectPanel { cx.notify(); } + fn has_git_changes(&self, entry_id: ProjectEntryId) -> bool { + for visible in &self.state.visible_entries { + if let Some(git_entry) = visible.entries.iter().find(|e| e.id == entry_id) { + let total_modified = + git_entry.git_summary.index.modified + git_entry.git_summary.worktree.modified; + let total_deleted = + git_entry.git_summary.index.deleted + git_entry.git_summary.worktree.deleted; + return total_modified > 0 || total_deleted > 0; + } + } + false + } + fn is_unfoldable(&self, entry: &Entry, worktree: &Worktree) -> bool { if !entry.is_dir() || self.state.unfolded_dir_ids.contains(&entry.id) { return false; @@ -2041,6 +2061,100 @@ impl ProjectPanel { self.remove(false, action.skip_prompt, window, cx); } + fn restore_file( + &mut self, + action: &git::RestoreFile, + window: &mut Window, + cx: &mut Context, + ) { + maybe!({ + let selection = self.state.selection?; + let project = self.project.read(cx); + + let (_worktree, entry) = self.selected_sub_entry(cx)?; + if entry.is_dir() { + return None; + } + + let project_path = project.path_for_entry(selection.entry_id, cx)?; + + let git_store = project.git_store(); + let (repository, repo_path) = git_store + .read(cx) + .repository_and_path_for_project_path(&project_path, cx)?; + + let snapshot = repository.read(cx).snapshot(); + let status = snapshot.status_for_path(&repo_path)?; + if !status.status.is_modified() && !status.status.is_deleted() { + return None; + } + + let file_name = entry.path.file_name()?.to_string(); + + let answer = if !action.skip_prompt { + let prompt = format!("Discard changes to {}?", file_name); + Some(window.prompt(PromptLevel::Info, &prompt, None, &["Restore", "Cancel"], cx)) + } else { + None + }; + + cx.spawn_in(window, async move |panel, cx| { + if let Some(answer) = answer + && answer.await != Ok(0) + { + return anyhow::Ok(()); + } + + let task = panel.update(cx, |_panel, cx| { + repository.update(cx, |repo, cx| { + repo.checkout_files("HEAD", vec![repo_path], cx) + }) + })?; + + if let Err(e) = task.await { + panel + .update(cx, |panel, cx| { + let message = format!("Failed to restore {}: {}", file_name, e); + let toast = StatusToast::new(message, cx, |this, _| { + this.icon(ToastIcon::new(IconName::XCircle).color(Color::Error)) + .dismiss_button(true) + }); + panel + .workspace + .update(cx, |workspace, cx| { + workspace.toggle_status_toast(toast, cx); + }) + .ok(); + }) + .ok(); + } + + panel + .update(cx, |panel, cx| { + panel.project.update(cx, |project, cx| { + if let Some(buffer_id) = project + .buffer_store() + .read(cx) + .buffer_id_for_project_path(&project_path) + { + if let Some(buffer) = project.buffer_for_id(*buffer_id, cx) { + buffer.update(cx, |buffer, cx| { + let _ = buffer.reload(cx); + }); + } + } + }) + }) + .ok(); + + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + + Some(()) + }); + } + fn remove( &mut self, trash: bool, @@ -5631,6 +5745,7 @@ impl Render for ProjectPanel { .on_action(cx.listener(Self::copy)) .on_action(cx.listener(Self::paste)) .on_action(cx.listener(Self::duplicate)) + .on_action(cx.listener(Self::restore_file)) .when(!project.is_remote(), |el| { el.on_action(cx.listener(Self::trash)) })