Cargo.lock 🔗
@@ -3330,7 +3330,9 @@ name = "project_panel"
version = "0.1.0"
dependencies = [
"editor",
+ "futures",
"gpui",
+ "postage",
"project",
"serde_json",
"settings",
Max Brunsfeld created
Cargo.lock | 2
assets/keymaps/default.json | 3
crates/collab/src/rpc.rs | 53 ++++++++++++++++++
crates/project/src/project.rs | 45 +++++++++++++++
crates/project/src/worktree.rs | 72 ++++++++++++++++++++++++
crates/project_panel/Cargo.toml | 2
crates/project_panel/src/project_panel.rs | 25 +++++++-
crates/rpc/proto/zed.proto | 3
crates/rpc/src/proto.rs | 1
9 files changed, 198 insertions(+), 8 deletions(-)
@@ -3330,7 +3330,9 @@ name = "project_panel"
version = "0.1.0"
dependencies = [
"editor",
+ "futures",
"gpui",
+ "postage",
"project",
"serde_json",
"settings",
@@ -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"
}
}
]
@@ -128,6 +128,7 @@ impl Server {
.add_request_handler(Server::forward_project_request::<proto::FormatBuffers>)
.add_request_handler(Server::forward_project_request::<proto::CreateProjectEntry>)
.add_request_handler(Server::forward_project_request::<proto::RenameProjectEntry>)
+ .add_request_handler(Server::forward_project_request::<proto::DeleteProjectEntry>)
.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::<Vec<_>>(),
+ [".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::<Vec<_>>(),
+ [".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::<Vec<_>>(),
+ [".zed.toml", "a.txt", "b.txt"]
+ );
+ });
+ worktree_b.read_with(cx_b, |worktree, _| {
+ assert_eq!(
+ worktree
+ .paths()
+ .map(|p| p.to_string_lossy())
+ .collect::<Vec<_>>(),
+ [".zed.toml", "a.txt", "b.txt"]
+ );
+ });
}
#[gpui::test(iterations = 10)]
@@ -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<Self>,
+ ) -> Option<Task<Result<()>>> {
+ 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<Self>,
+ envelope: TypedEnvelope<proto::DeleteProjectEntry>,
+ _: Arc<Client>,
+ mut cx: AsyncAppContext,
+ ) -> Result<proto::Ack> {
+ 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<Self>,
envelope: TypedEnvelope<proto::UpdateDiagnosticSummary>,
@@ -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<Worktree>,
+ ) -> Option<Task<Result<()>>> {
+ 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<Worktree>,
+ ) -> Task<Result<()>> {
+ 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();
@@ -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]
@@ -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<Self>) -> Option<Task<Result<()>>> {
+ 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<Self>) {
if let Some(selection) = self.selection {
let (mut worktree_ix, mut entry_ix, _) =
@@ -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 {
@@ -225,6 +225,7 @@ request_messages!(
ApplyCompletionAdditionalEditsResponse
),
(CreateProjectEntry, ProjectEntryResponse),
+ (DeleteProjectEntry, Ack),
(Follow, FollowResponse),
(FormatBuffers, FormatBuffersResponse),
(GetChannelMessages, GetChannelMessagesResponse),