diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 7ea925fce7..a09a2b1f33 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -172,6 +172,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::forward_project_request::) .add_request_handler(Server::update_buffer) .add_message_handler(Server::update_buffer_file) diff --git a/crates/project/src/fs.rs b/crates/project/src/fs.rs index 7da2a38a83..2eec02d66d 100644 --- a/crates/project/src/fs.rs +++ b/crates/project/src/fs.rs @@ -15,6 +15,7 @@ use text::Rope; pub trait Fs: Send + Sync { async fn create_dir(&self, path: &Path) -> Result<()>; async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()>; + async fn copy(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()>; async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()>; async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()>; async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()>; @@ -44,6 +45,12 @@ pub struct CreateOptions { pub ignore_if_exists: bool, } +#[derive(Copy, Clone, Default)] +pub struct CopyOptions { + pub overwrite: bool, + pub ignore_if_exists: bool, +} + #[derive(Copy, Clone, Default)] pub struct RenameOptions { pub overwrite: bool, @@ -84,6 +91,35 @@ impl Fs for RealFs { Ok(()) } + async fn copy(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()> { + if !options.overwrite && smol::fs::metadata(target).await.is_ok() { + if options.ignore_if_exists { + return Ok(()); + } else { + return Err(anyhow!("{target:?} already exists")); + } + } + + let metadata = smol::fs::metadata(source).await?; + let _ = smol::fs::remove_dir_all(target).await; + if metadata.is_dir() { + self.create_dir(target).await?; + let mut children = smol::fs::read_dir(source).await?; + while let Some(child) = children.next().await { + if let Ok(child) = child { + let child_source_path = child.path(); + let child_target_path = target.join(child.file_name()); + self.copy(&child_source_path, &child_target_path, options) + .await?; + } + } + } else { + smol::fs::copy(source, target).await?; + } + + Ok(()) + } + async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()> { if !options.overwrite && smol::fs::metadata(target).await.is_ok() { if options.ignore_if_exists { @@ -511,6 +547,40 @@ impl Fs for FakeFs { Ok(()) } + async fn copy(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()> { + let source = normalize_path(source); + let target = normalize_path(target); + + let mut state = self.state.lock().await; + state.validate_path(&source)?; + state.validate_path(&target)?; + + if !options.overwrite && state.entries.contains_key(&target) { + if options.ignore_if_exists { + return Ok(()); + } else { + return Err(anyhow!("{target:?} already exists")); + } + } + + let mut new_entries = Vec::new(); + for (path, entry) in &state.entries { + if let Ok(relative_path) = path.strip_prefix(&source) { + new_entries.push((relative_path.to_path_buf(), entry.clone())); + } + } + + let mut events = Vec::new(); + for (relative_path, entry) in new_entries { + let new_path = normalize_path(&target.join(relative_path)); + events.push(new_path.clone()); + state.entries.insert(new_path, entry); + } + + state.emit_event(&events).await; + Ok(()) + } + async fn remove_dir(&self, dir_path: &Path, options: RemoveOptions) -> Result<()> { let dir_path = normalize_path(dir_path); let mut state = self.state.lock().await; diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index abcd667293..923c188ffc 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -281,6 +281,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_copy_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); @@ -778,6 +779,49 @@ impl Project { } } + pub fn copy_entry( + &mut self, + entry_id: ProjectEntryId, + new_path: impl Into>, + cx: &mut ModelContext, + ) -> Option>> { + let worktree = self.worktree_for_entry(entry_id, cx)?; + let new_path = new_path.into(); + if self.is_local() { + worktree.update(cx, |worktree, cx| { + worktree + .as_local_mut() + .unwrap() + .copy_entry(entry_id, new_path, cx) + }) + } else { + let client = self.client.clone(); + let project_id = self.remote_id().unwrap(); + + Some(cx.spawn_weak(|_, mut cx| async move { + let response = client + .request(proto::CopyProjectEntry { + project_id, + entry_id: entry_id.to_proto(), + new_path: new_path.as_os_str().as_bytes().to_vec(), + }) + .await?; + let entry = response + .entry + .ok_or_else(|| anyhow!("missing entry in response"))?; + worktree + .update(&mut cx, |worktree, cx| { + worktree.as_remote().unwrap().insert_entry( + entry, + response.worktree_scan_id as usize, + cx, + ) + }) + .await + })) + } + } + pub fn rename_entry( &mut self, entry_id: ProjectEntryId, @@ -4027,6 +4071,34 @@ impl Project { }) } + async fn handle_copy_project_entry( + this: ModelHandle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id); + let worktree = this.read_with(&cx, |this, cx| { + this.worktree_for_entry(entry_id, cx) + .ok_or_else(|| anyhow!("worktree not found")) + })?; + let worktree_scan_id = worktree.read_with(&cx, |worktree, _| worktree.scan_id()); + let entry = worktree + .update(&mut cx, |worktree, cx| { + let new_path = PathBuf::from(OsString::from_vec(envelope.payload.new_path)); + worktree + .as_local_mut() + .unwrap() + .copy_entry(entry_id, new_path, cx) + .ok_or_else(|| anyhow!("invalid entry")) + })? + .await?; + Ok(proto::ProjectEntryResponse { + entry: Some((&entry).into()), + worktree_scan_id: worktree_scan_id as u64, + }) + } + async fn handle_delete_project_entry( this: ModelHandle, envelope: TypedEnvelope, diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 039ee0d838..8eef61f213 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -774,6 +774,46 @@ impl LocalWorktree { })) } + pub fn copy_entry( + &self, + entry_id: ProjectEntryId, + new_path: impl Into>, + cx: &mut ModelContext, + ) -> Option>> { + let old_path = self.entry_for_id(entry_id)?.path.clone(); + let new_path = new_path.into(); + let abs_old_path = self.absolutize(&old_path); + let abs_new_path = self.absolutize(&new_path); + let copy = cx.background().spawn({ + let fs = self.fs.clone(); + let abs_new_path = abs_new_path.clone(); + async move { + fs.copy(&abs_old_path, &abs_new_path, Default::default()) + .await + } + }); + + Some(cx.spawn(|this, mut cx| async move { + copy.await?; + let entry = this + .update(&mut cx, |this, cx| { + this.as_local_mut().unwrap().refresh_entry( + new_path.clone(), + abs_new_path, + None, + cx, + ) + }) + .await?; + this.update(&mut cx, |this, cx| { + this.poll_snapshot(cx); + this.as_local().unwrap().broadcast_snapshot() + }) + .await; + Ok(entry) + })) + } + fn write_entry_internal( &self, path: impl Into>, diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index bd678f37ac..6cc59728e6 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -698,6 +698,11 @@ impl ProjectPanel { }) .map(|task| task.detach_and_log_err(cx)); } else { + self.project + .update(cx, |project, cx| { + project.copy_entry(clipboard_entry.entry_id(), new_path, cx) + }) + .map(|task| task.detach_and_log_err(cx)); } } None diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 0fee451c0d..2f6ed1f318 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -41,66 +41,67 @@ message Envelope { CreateProjectEntry create_project_entry = 33; RenameProjectEntry rename_project_entry = 34; - DeleteProjectEntry delete_project_entry = 35; - ProjectEntryResponse project_entry_response = 36; + CopyProjectEntry copy_project_entry = 35; + DeleteProjectEntry delete_project_entry = 36; + ProjectEntryResponse project_entry_response = 37; - UpdateDiagnosticSummary update_diagnostic_summary = 37; - StartLanguageServer start_language_server = 38; - UpdateLanguageServer update_language_server = 39; + UpdateDiagnosticSummary update_diagnostic_summary = 38; + StartLanguageServer start_language_server = 39; + UpdateLanguageServer update_language_server = 40; - OpenBufferById open_buffer_by_id = 40; - OpenBufferByPath open_buffer_by_path = 41; - OpenBufferResponse open_buffer_response = 42; - UpdateBuffer update_buffer = 43; - UpdateBufferFile update_buffer_file = 44; - SaveBuffer save_buffer = 45; - BufferSaved buffer_saved = 46; - BufferReloaded buffer_reloaded = 47; - ReloadBuffers reload_buffers = 48; - ReloadBuffersResponse reload_buffers_response = 49; - FormatBuffers format_buffers = 50; - FormatBuffersResponse format_buffers_response = 51; - GetCompletions get_completions = 52; - GetCompletionsResponse get_completions_response = 53; - ApplyCompletionAdditionalEdits apply_completion_additional_edits = 54; - ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 55; - GetCodeActions get_code_actions = 56; - GetCodeActionsResponse get_code_actions_response = 57; - ApplyCodeAction apply_code_action = 58; - ApplyCodeActionResponse apply_code_action_response = 59; - PrepareRename prepare_rename = 60; - PrepareRenameResponse prepare_rename_response = 61; - PerformRename perform_rename = 62; - PerformRenameResponse perform_rename_response = 63; - SearchProject search_project = 64; - SearchProjectResponse search_project_response = 65; + OpenBufferById open_buffer_by_id = 41; + OpenBufferByPath open_buffer_by_path = 42; + OpenBufferResponse open_buffer_response = 43; + UpdateBuffer update_buffer = 44; + UpdateBufferFile update_buffer_file = 45; + SaveBuffer save_buffer = 46; + BufferSaved buffer_saved = 47; + BufferReloaded buffer_reloaded = 48; + ReloadBuffers reload_buffers = 49; + ReloadBuffersResponse reload_buffers_response = 50; + FormatBuffers format_buffers = 51; + FormatBuffersResponse format_buffers_response = 52; + GetCompletions get_completions = 53; + GetCompletionsResponse get_completions_response = 54; + ApplyCompletionAdditionalEdits apply_completion_additional_edits = 55; + ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 56; + GetCodeActions get_code_actions = 57; + GetCodeActionsResponse get_code_actions_response = 58; + ApplyCodeAction apply_code_action = 59; + ApplyCodeActionResponse apply_code_action_response = 60; + PrepareRename prepare_rename = 61; + PrepareRenameResponse prepare_rename_response = 62; + PerformRename perform_rename = 63; + PerformRenameResponse perform_rename_response = 64; + SearchProject search_project = 65; + SearchProjectResponse search_project_response = 66; - GetChannels get_channels = 66; - GetChannelsResponse get_channels_response = 67; - JoinChannel join_channel = 68; - JoinChannelResponse join_channel_response = 69; - LeaveChannel leave_channel = 70; - SendChannelMessage send_channel_message = 71; - SendChannelMessageResponse send_channel_message_response = 72; - ChannelMessageSent channel_message_sent = 73; - GetChannelMessages get_channel_messages = 74; - GetChannelMessagesResponse get_channel_messages_response = 75; + GetChannels get_channels = 67; + GetChannelsResponse get_channels_response = 68; + JoinChannel join_channel = 69; + JoinChannelResponse join_channel_response = 70; + LeaveChannel leave_channel = 71; + SendChannelMessage send_channel_message = 72; + SendChannelMessageResponse send_channel_message_response = 73; + ChannelMessageSent channel_message_sent = 74; + GetChannelMessages get_channel_messages = 75; + GetChannelMessagesResponse get_channel_messages_response = 76; - UpdateContacts update_contacts = 76; - UpdateInviteInfo update_invite_info = 77; - ShowContacts show_contacts = 78; + UpdateContacts update_contacts = 77; + UpdateInviteInfo update_invite_info = 78; + ShowContacts show_contacts = 79; - GetUsers get_users = 79; - FuzzySearchUsers fuzzy_search_users = 80; - UsersResponse users_response = 81; - RequestContact request_contact = 82; - RespondToContactRequest respond_to_contact_request = 83; - RemoveContact remove_contact = 84; + GetUsers get_users = 80; + FuzzySearchUsers fuzzy_search_users = 81; + UsersResponse users_response = 82; + RequestContact request_contact = 83; + RespondToContactRequest respond_to_contact_request = 84; + RemoveContact remove_contact = 85; - Follow follow = 85; - FollowResponse follow_response = 86; - UpdateFollowers update_followers = 87; - Unfollow unfollow = 88; + Follow follow = 86; + FollowResponse follow_response = 87; + UpdateFollowers update_followers = 88; + Unfollow unfollow = 89; } } @@ -210,6 +211,12 @@ message RenameProjectEntry { bytes new_path = 3; } +message CopyProjectEntry { + uint64 project_id = 1; + uint64 entry_id = 2; + bytes new_path = 3; +} + message DeleteProjectEntry { uint64 project_id = 1; uint64 entry_id = 2; diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 67a12fcd87..7fe715064f 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -84,6 +84,7 @@ messages!( (BufferSaved, Foreground), (RemoveContact, Foreground), (ChannelMessageSent, Foreground), + (CopyProjectEntry, Foreground), (CreateProjectEntry, Foreground), (DeleteProjectEntry, Foreground), (Error, Foreground), @@ -167,6 +168,7 @@ request_messages!( ApplyCompletionAdditionalEdits, ApplyCompletionAdditionalEditsResponse ), + (CopyProjectEntry, ProjectEntryResponse), (CreateProjectEntry, ProjectEntryResponse), (DeleteProjectEntry, ProjectEntryResponse), (Follow, FollowResponse), @@ -211,8 +213,8 @@ entity_messages!( ApplyCompletionAdditionalEdits, BufferReloaded, BufferSaved, + CopyProjectEntry, CreateProjectEntry, - RenameProjectEntry, DeleteProjectEntry, Follow, FormatBuffers, @@ -233,6 +235,7 @@ entity_messages!( ProjectUnshared, ReloadBuffers, RemoveProjectCollaborator, + RenameProjectEntry, RequestJoinProject, SaveBuffer, SearchProject,