diff --git a/Cargo.lock b/Cargo.lock index 77dd2908ba..6d7ce98341 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3330,7 +3330,9 @@ name = "project_panel" version = "0.1.0" dependencies = [ "editor", + "futures", "gpui", + "postage", "project", "serde_json", "settings", diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index b7d390062f..dd83b91ed0 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -332,7 +332,8 @@ "bindings": { "left": "project_panel::CollapseSelectedEntry", "right": "project_panel::ExpandSelectedEntry", - "f2": "project_panel::Rename" + "f2": "project_panel::Rename", + "backspace": "project_panel::Delete" } } ] \ No newline at end of file diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 624eba59a3..2367532d28 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -128,6 +128,7 @@ impl Server { .add_request_handler(Server::forward_project_request::) .add_request_handler(Server::forward_project_request::) .add_request_handler(Server::forward_project_request::) + .add_request_handler(Server::forward_project_request::) .add_request_handler(Server::update_buffer) .add_message_handler(Server::update_buffer_file) .add_message_handler(Server::buffer_reloaded) @@ -1900,7 +1901,7 @@ mod tests { ); }); - project_b + let dir_entry = project_b .update(cx_b, |project, cx| { project .create_entry((worktree_id, "DIR"), true, cx) @@ -1926,6 +1927,56 @@ mod tests { [".zed.toml", "DIR", "a.txt", "b.txt", "d.txt"] ); }); + + project_b + .update(cx_b, |project, cx| { + project.delete_entry(dir_entry.id, cx).unwrap() + }) + .await + .unwrap(); + worktree_a.read_with(cx_a, |worktree, _| { + assert_eq!( + worktree + .paths() + .map(|p| p.to_string_lossy()) + .collect::>(), + [".zed.toml", "a.txt", "b.txt", "d.txt"] + ); + }); + worktree_b.read_with(cx_b, |worktree, _| { + assert_eq!( + worktree + .paths() + .map(|p| p.to_string_lossy()) + .collect::>(), + [".zed.toml", "a.txt", "b.txt", "d.txt"] + ); + }); + + project_b + .update(cx_b, |project, cx| { + project.delete_entry(entry.id, cx).unwrap() + }) + .await + .unwrap(); + worktree_a.read_with(cx_a, |worktree, _| { + assert_eq!( + worktree + .paths() + .map(|p| p.to_string_lossy()) + .collect::>(), + [".zed.toml", "a.txt", "b.txt"] + ); + }); + worktree_b.read_with(cx_b, |worktree, _| { + assert_eq!( + worktree + .paths() + .map(|p| p.to_string_lossy()) + .collect::>(), + [".zed.toml", "a.txt", "b.txt"] + ); + }); } #[gpui::test(iterations = 10)] diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 5590c61762..7af1199ec1 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -263,6 +263,7 @@ impl Project { client.add_model_message_handler(Self::handle_update_worktree); client.add_model_request_handler(Self::handle_create_project_entry); client.add_model_request_handler(Self::handle_rename_project_entry); + client.add_model_request_handler(Self::handle_delete_project_entry); client.add_model_request_handler(Self::handle_apply_additional_edits_for_completion); client.add_model_request_handler(Self::handle_apply_code_action); client.add_model_request_handler(Self::handle_reload_buffers); @@ -768,6 +769,35 @@ impl Project { } } + pub fn delete_entry( + &mut self, + entry_id: ProjectEntryId, + cx: &mut ModelContext, + ) -> Option>> { + let worktree = self.worktree_for_entry(entry_id, cx)?; + if self.is_local() { + worktree.update(cx, |worktree, cx| { + worktree.as_local_mut().unwrap().delete_entry(entry_id, cx) + }) + } else { + let client = self.client.clone(); + let project_id = self.remote_id().unwrap(); + Some(cx.spawn_weak(|_, mut cx| async move { + client + .request(proto::DeleteProjectEntry { + project_id, + entry_id: entry_id.to_proto(), + }) + .await?; + worktree + .update(&mut cx, move |worktree, cx| { + worktree.as_remote().unwrap().delete_entry(entry_id, cx) + }) + .await + })) + } + } + pub fn can_share(&self, cx: &AppContext) -> bool { self.is_local() && self.visible_worktrees(cx).next().is_some() } @@ -3858,6 +3888,21 @@ impl Project { }) } + async fn handle_delete_project_entry( + this: ModelHandle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + this.update(&mut cx, |this, cx| { + let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id); + this.delete_entry(entry_id, cx) + .ok_or_else(|| anyhow!("invalid entry")) + })? + .await?; + Ok(proto::Ack {}) + } + async fn handle_update_diagnostic_summary( this: ModelHandle, envelope: TypedEnvelope, diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index db4769b7aa..2a1808457c 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -1,4 +1,4 @@ -use crate::ProjectEntryId; +use crate::{ProjectEntryId, RemoveOptions}; use super::{ fs::{self, Fs}, @@ -712,6 +712,44 @@ impl LocalWorktree { self.write_entry_internal(path, Some(text), cx) } + pub fn delete_entry( + &self, + entry_id: ProjectEntryId, + cx: &mut ModelContext, + ) -> Option>> { + let entry = self.entry_for_id(entry_id)?.clone(); + let abs_path = self.absolutize(&entry.path); + let delete = cx.background().spawn({ + let fs = self.fs.clone(); + let abs_path = abs_path.clone(); + async move { + if entry.is_file() { + fs.remove_file(&abs_path, Default::default()).await + } else { + fs.remove_dir( + &abs_path, + RemoveOptions { + recursive: true, + ignore_if_not_exists: false, + }, + ) + .await + } + } + }); + + Some(cx.spawn(|this, mut cx| async move { + delete.await?; + this.update(&mut cx, |this, _| { + let this = this.as_local_mut().unwrap(); + let mut snapshot = this.background_snapshot.lock(); + snapshot.delete_entry(entry_id); + }); + this.update(&mut cx, |this, cx| this.poll_snapshot(cx)); + Ok(()) + })) + } + pub fn rename_entry( &self, entry_id: ProjectEntryId, @@ -1019,6 +1057,29 @@ impl RemoteWorktree { }) }) } + + pub(crate) fn delete_entry( + &self, + id: ProjectEntryId, + cx: &mut ModelContext, + ) -> Task> { + cx.spawn(|this, mut cx| async move { + this.update(&mut cx, |worktree, _| { + worktree + .as_remote_mut() + .unwrap() + .finish_pending_remote_updates() + }) + .await; + this.update(&mut cx, |worktree, _| { + let worktree = worktree.as_remote_mut().unwrap(); + let mut snapshot = worktree.background_snapshot.lock(); + snapshot.delete_entry(id); + worktree.snapshot = snapshot.clone(); + }); + Ok(()) + }) + } } impl Snapshot { @@ -1048,6 +1109,15 @@ impl Snapshot { Ok(entry) } + fn delete_entry(&mut self, entry_id: ProjectEntryId) -> bool { + if let Some(entry) = self.entries_by_id.remove(&entry_id, &()) { + self.entries_by_path.remove(&PathKey(entry.path), &()); + true + } else { + false + } + } + pub(crate) fn apply_remote_update(&mut self, update: proto::UpdateWorktree) -> Result<()> { let mut entries_by_path_edits = Vec::new(); let mut entries_by_id_edits = Vec::new(); diff --git a/crates/project_panel/Cargo.toml b/crates/project_panel/Cargo.toml index 4b78f2a1fa..e431db45dd 100644 --- a/crates/project_panel/Cargo.toml +++ b/crates/project_panel/Cargo.toml @@ -15,6 +15,8 @@ settings = { path = "../settings" } theme = { path = "../theme" } util = { path = "../util" } workspace = { path = "../workspace" } +postage = { version = "0.4.1", features = ["futures-traits"] } +futures = "0.3" unicase = "2.6" [dev-dependencies] diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 18ff22cd95..6bb4f640ee 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1,15 +1,16 @@ use editor::{Cancel, Editor}; +use futures::stream::StreamExt; use gpui::{ actions, - anyhow::Result, + anyhow::{anyhow, Result}, elements::{ ChildView, ConstrainedBox, Empty, Flex, Label, MouseEventHandler, ParentElement, ScrollTarget, Svg, UniformList, UniformListState, }, impl_internal_actions, keymap, platform::CursorStyle, - AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, Task, View, - ViewContext, ViewHandle, WeakViewHandle, + AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, PromptLevel, Task, + View, ViewContext, ViewHandle, WeakViewHandle, }; use project::{Entry, EntryKind, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId}; use settings::Settings; @@ -77,6 +78,7 @@ actions!( CollapseSelectedEntry, AddDirectory, AddFile, + Delete, Rename ] ); @@ -92,6 +94,7 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(ProjectPanel::add_file); cx.add_action(ProjectPanel::add_directory); cx.add_action(ProjectPanel::rename); + cx.add_async_action(ProjectPanel::delete); cx.add_async_action(ProjectPanel::confirm); cx.add_action(ProjectPanel::cancel); } @@ -432,6 +435,22 @@ impl ProjectPanel { } } + fn delete(&mut self, _: &Delete, cx: &mut ViewContext) -> Option>> { + let Selection { entry_id, .. } = self.selection?; + let mut answer = cx.prompt(PromptLevel::Info, "Delete?", &["Delete", "Cancel"]); + Some(cx.spawn(|this, mut cx| async move { + if answer.next().await != Some(0) { + return Ok(()); + } + this.update(&mut cx, |this, cx| { + this.project + .update(cx, |project, cx| project.delete_entry(entry_id, cx)) + .ok_or_else(|| anyhow!("no such entry")) + })? + .await + })) + } + fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { if let Some(selection) = self.selection { let (mut worktree_ix, mut entry_ix, _) = diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index ffa2443537..8a30278920 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -179,8 +179,7 @@ message RenameProjectEntry { message DeleteProjectEntry { uint64 project_id = 1; - uint64 worktree_id = 2; - string path = 3; + uint64 entry_id = 2; } message ProjectEntryResponse { diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 59053997d3..428eb13a42 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -225,6 +225,7 @@ request_messages!( ApplyCompletionAdditionalEditsResponse ), (CreateProjectEntry, ProjectEntryResponse), + (DeleteProjectEntry, Ack), (Follow, FollowResponse), (FormatBuffers, FormatBuffersResponse), (GetChannelMessages, GetChannelMessagesResponse),