Implement dragging external files to remote projects (#28987)

Release Notes:

- Added the ability to copy external files into remote projects by
dragging them onto the project panel.

---------

Co-authored-by: Peter Tripp <petertripp@gmail.com>
This commit is contained in:
Max Brunsfeld 2025-04-17 11:06:56 -07:00 committed by GitHub
parent fade49a11a
commit 7e928dd615
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 275 additions and 82 deletions

View file

@ -7,7 +7,7 @@ use ::ignore::gitignore::{Gitignore, GitignoreBuilder};
use anyhow::{Context as _, Result, anyhow};
use clock::ReplicaId;
use collections::{HashMap, HashSet, VecDeque};
use fs::{Fs, MTime, PathEvent, RemoveOptions, Watcher, copy_recursive};
use fs::{Fs, MTime, PathEvent, RemoveOptions, Watcher, copy_recursive, read_dir_items};
use futures::{
FutureExt as _, Stream, StreamExt,
channel::{
@ -847,18 +847,20 @@ impl Worktree {
&mut self,
path: impl Into<Arc<Path>>,
is_directory: bool,
content: Option<Vec<u8>>,
cx: &Context<Worktree>,
) -> Task<Result<CreatedEntry>> {
let path: Arc<Path> = path.into();
let worktree_id = self.id();
match self {
Worktree::Local(this) => this.create_entry(path, is_directory, cx),
Worktree::Local(this) => this.create_entry(path, is_directory, content, cx),
Worktree::Remote(this) => {
let project_id = this.project_id;
let request = this.client.request(proto::CreateProjectEntry {
worktree_id: worktree_id.to_proto(),
project_id,
path: path.as_ref().to_proto(),
content,
is_directory,
});
cx.spawn(async move |this, cx| {
@ -979,18 +981,14 @@ impl Worktree {
pub fn copy_external_entries(
&mut self,
target_directory: PathBuf,
target_directory: Arc<Path>,
paths: Vec<Arc<Path>>,
overwrite_existing_files: bool,
fs: Arc<dyn Fs>,
cx: &Context<Worktree>,
) -> Task<Result<Vec<ProjectEntryId>>> {
match self {
Worktree::Local(this) => {
this.copy_external_entries(target_directory, paths, overwrite_existing_files, cx)
}
_ => Task::ready(Err(anyhow!(
"Copying external entries is not supported for remote worktrees"
))),
Worktree::Local(this) => this.copy_external_entries(target_directory, paths, cx),
Worktree::Remote(this) => this.copy_external_entries(target_directory, paths, fs, cx),
}
}
@ -1057,6 +1055,7 @@ impl Worktree {
this.create_entry(
Arc::<Path>::from_proto(request.path),
request.is_directory,
request.content,
cx,
),
)
@ -1585,6 +1584,7 @@ impl LocalWorktree {
&self,
path: impl Into<Arc<Path>>,
is_dir: bool,
content: Option<Vec<u8>>,
cx: &Context<Worktree>,
) -> Task<Result<CreatedEntry>> {
let path = path.into();
@ -1601,7 +1601,7 @@ impl LocalWorktree {
.await
.with_context(|| format!("creating directory {task_abs_path:?}"))
} else {
fs.save(&task_abs_path, &Rope::default(), LineEnding::default())
fs.write(&task_abs_path, content.as_deref().unwrap_or(&[]))
.await
.with_context(|| format!("creating file {task_abs_path:?}"))
}
@ -1877,11 +1877,13 @@ impl LocalWorktree {
pub fn copy_external_entries(
&self,
target_directory: PathBuf,
target_directory: Arc<Path>,
paths: Vec<Arc<Path>>,
overwrite_existing_files: bool,
cx: &Context<Worktree>,
) -> Task<Result<Vec<ProjectEntryId>>> {
let Ok(target_directory) = self.absolutize(&target_directory) else {
return Task::ready(Err(anyhow!("invalid target path")));
};
let worktree_path = self.abs_path().clone();
let fs = self.fs.clone();
let paths = paths
@ -1913,7 +1915,7 @@ impl LocalWorktree {
&source,
&target,
fs::CopyOptions {
overwrite: overwrite_existing_files,
overwrite: true,
..Default::default()
},
)
@ -2283,6 +2285,62 @@ impl RemoteWorktree {
}
})
}
fn copy_external_entries(
&self,
target_directory: Arc<Path>,
paths_to_copy: Vec<Arc<Path>>,
local_fs: Arc<dyn Fs>,
cx: &Context<Worktree>,
) -> Task<Result<Vec<ProjectEntryId>, anyhow::Error>> {
let client = self.client.clone();
let worktree_id = self.id().to_proto();
let project_id = self.project_id;
cx.background_spawn(async move {
let mut requests = Vec::new();
for root_path_to_copy in paths_to_copy {
let Some(filename) = root_path_to_copy.file_name() else {
continue;
};
for (abs_path, is_directory) in
read_dir_items(local_fs.as_ref(), &root_path_to_copy).await?
{
let Ok(relative_path) = abs_path.strip_prefix(&root_path_to_copy) else {
continue;
};
let content = if is_directory {
None
} else {
Some(local_fs.load_bytes(&abs_path).await?)
};
let mut target_path = target_directory.join(filename);
if relative_path.file_name().is_some() {
target_path.push(relative_path)
}
requests.push(proto::CreateProjectEntry {
project_id,
worktree_id,
path: target_path.to_string_lossy().to_string(),
is_directory,
content,
});
}
}
requests.sort_unstable_by(|a, b| a.path.cmp(&b.path));
requests.dedup();
let mut copied_entry_ids = Vec::new();
for request in requests {
let response = client.request(request).await?;
copied_entry_ids.extend(response.entry.map(|e| ProjectEntryId::from_proto(e.id)));
}
Ok(copied_entry_ids)
})
}
}
impl Snapshot {

View file

@ -1270,7 +1270,7 @@ async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
.update(cx, |tree, cx| {
tree.as_local_mut()
.unwrap()
.create_entry("a/e".as_ref(), true, cx)
.create_entry("a/e".as_ref(), true, None, cx)
})
.await
.unwrap()
@ -1319,7 +1319,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
.update(cx, |tree, cx| {
tree.as_local_mut()
.unwrap()
.create_entry("a/b/c/d.txt".as_ref(), false, cx)
.create_entry("a/b/c/d.txt".as_ref(), false, None, cx)
})
.await
.unwrap()
@ -1353,7 +1353,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
.update(cx, |tree, cx| {
tree.as_local_mut()
.unwrap()
.create_entry("a/b/c/d.txt".as_ref(), false, cx)
.create_entry("a/b/c/d.txt".as_ref(), false, None, cx)
})
.await
.unwrap()
@ -1373,7 +1373,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
.update(cx, |tree, cx| {
tree.as_local_mut()
.unwrap()
.create_entry("a/b/c/e.txt".as_ref(), false, cx)
.create_entry("a/b/c/e.txt".as_ref(), false, None, cx)
})
.await
.unwrap()
@ -1391,7 +1391,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
.update(cx, |tree, cx| {
tree.as_local_mut()
.unwrap()
.create_entry("d/e/f/g.txt".as_ref(), false, cx)
.create_entry("d/e/f/g.txt".as_ref(), false, None, cx)
})
.await
.unwrap()
@ -1739,7 +1739,7 @@ fn randomly_mutate_worktree(
if is_dir { "dir" } else { "file" },
child_path,
);
let task = worktree.create_entry(child_path, is_dir, cx);
let task = worktree.create_entry(child_path, is_dir, None, cx);
cx.background_spawn(async move {
task.await?;
Ok(())