Detailed changes
@@ -172,6 +172,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::CopyProjectEntry>)
.add_request_handler(Server::forward_project_request::<proto::DeleteProjectEntry>)
.add_request_handler(Server::update_buffer)
.add_message_handler(Server::update_buffer_file)
@@ -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;
@@ -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<Arc<Path>>,
+ cx: &mut ModelContext<Self>,
+ ) -> Option<Task<Result<Entry>>> {
+ 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<Self>,
+ envelope: TypedEnvelope<proto::CopyProjectEntry>,
+ _: Arc<Client>,
+ mut cx: AsyncAppContext,
+ ) -> Result<proto::ProjectEntryResponse> {
+ 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<Self>,
envelope: TypedEnvelope<proto::DeleteProjectEntry>,
@@ -774,6 +774,46 @@ impl LocalWorktree {
}))
}
+ pub fn copy_entry(
+ &self,
+ entry_id: ProjectEntryId,
+ new_path: impl Into<Arc<Path>>,
+ cx: &mut ModelContext<Worktree>,
+ ) -> Option<Task<Result<Entry>>> {
+ 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<Arc<Path>>,
@@ -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
@@ -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;
-
- UpdateDiagnosticSummary update_diagnostic_summary = 37;
- StartLanguageServer start_language_server = 38;
- UpdateLanguageServer update_language_server = 39;
-
- 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;
-
- 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;
-
- UpdateContacts update_contacts = 76;
- UpdateInviteInfo update_invite_info = 77;
- ShowContacts show_contacts = 78;
-
- 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;
-
- Follow follow = 85;
- FollowResponse follow_response = 86;
- UpdateFollowers update_followers = 87;
- Unfollow unfollow = 88;
+ CopyProjectEntry copy_project_entry = 35;
+ DeleteProjectEntry delete_project_entry = 36;
+ ProjectEntryResponse project_entry_response = 37;
+
+ UpdateDiagnosticSummary update_diagnostic_summary = 38;
+ StartLanguageServer start_language_server = 39;
+ UpdateLanguageServer update_language_server = 40;
+
+ 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 = 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 = 77;
+ UpdateInviteInfo update_invite_info = 78;
+ ShowContacts show_contacts = 79;
+
+ 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 = 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;
@@ -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,