Implement copy paste for ProjectPanel

This commit is contained in:
Antonio Scandurra 2022-05-30 14:52:34 +02:00
parent 37a0c7f046
commit 3336bc6ab3
7 changed files with 253 additions and 55 deletions

View File

@ -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)

View 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;

View File

@ -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>,

View File

@ -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>>,

View File

@ -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

View File

@ -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;

View File

@ -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,