From d9a000d3a3adb6607291cbe48414a1ac70c45216 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Mon, 10 Jun 2024 17:41:23 +0200 Subject: [PATCH 01/11] WIP --- crates/project/src/project.rs | 25 +++++++ crates/project_panel/src/project_panel.rs | 48 ++++++++++++-- crates/worktree/src/worktree.rs | 81 +++++++++++++++++++++++ 3 files changed, 150 insertions(+), 4 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 50e79263d9c6e7..31027266ff3509 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1510,6 +1510,31 @@ impl Project { } } + pub fn copy_external_entries( + &self, + copy_to_entry_id: ProjectEntryId, + external_paths: Vec>, + cx: &mut ModelContext, + ) -> Task>> { + let Some(worktree) = self.worktree_for_entry(copy_to_entry_id, cx) else { + return Task::ready(Ok(Vec::new())); + }; + + if !worktree.read(cx).is_local() { + return Task::ready(Ok(Vec::new())); + }; + + cx.spawn(move |_, mut cx| async move { + LocalWorktree::copy_external_entries( + worktree, + copy_to_entry_id, + external_paths, + &mut cx, + ) + .await + }) + } + pub fn rename_entry( &mut self, entry_id: ProjectEntryId, diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 2f225b2881b172..047830916bf110 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -12,10 +12,10 @@ use git::repository::GitFileStatus; use gpui::{ actions, anchored, deferred, div, impl_actions, px, uniform_list, Action, AnyElement, AppContext, AssetSource, AsyncWindowContext, ClipboardItem, DismissEvent, Div, EventEmitter, - FocusHandle, FocusableView, InteractiveElement, KeyContext, ListSizingBehavior, Model, - MouseButton, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render, Stateful, - Styled, Subscription, Task, UniformListScrollHandle, View, ViewContext, VisualContext as _, - WeakView, WindowContext, + ExternalPaths, FocusHandle, FocusableView, InteractiveElement, KeyContext, ListSizingBehavior, + Model, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render, + Stateful, Styled, Subscription, Task, UniformListScrollHandle, View, ViewContext, + VisualContext as _, WeakView, WindowContext, }; use menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev}; use project::{Entry, EntryKind, Fs, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId}; @@ -1737,6 +1737,37 @@ impl ProjectPanel { }); } + fn drop_entry( + &mut self, + external_paths: &ExternalPaths, + entry_id: ProjectEntryId, + cx: &mut ViewContext, + ) { + let paths: Vec<_> = external_paths + .paths() + .to_owned() + .into_iter() + .map(|path| Arc::from(path)) + .collect(); + + let task = self.project.update(cx, |project, cx| { + project.copy_external_entries(entry_id, paths, cx) + }); + + cx.spawn(|this, mut cx| { + async move { + let opened_entries = task.await?; + this.update(&mut cx, |this, cx| { + if opened_entries.len() == 1 { + this.open_entry(opened_entries[0], true, true, false, cx); + } + }) + } + .log_err() + }) + .detach() + } + fn drag_onto( &mut self, selections: &DraggedSelection, @@ -1988,6 +2019,15 @@ impl ProjectPanel { }; div() .id(entry_id.to_proto() as usize) + .drag_over(|style, _: &ExternalPaths, cx| { + style.bg(cx.theme().colors().drop_target_background) + }) + .on_drop( + cx.listener(move |project_panel, external_paths: &ExternalPaths, cx| { + project_panel.drop_entry(external_paths, entry_id, cx); + cx.stop_propagation(); + }), + ) .on_drag(dragged_selection, move |selection, cx| { cx.new_view(|_| DraggedProjectEntryView { details: details.clone(), diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 11527dbd998703..ead04b423666ec 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -1569,6 +1569,87 @@ impl LocalWorktree { }) } + pub async fn copy_external_entries( + this: Model, + copy_to_entry_id: ProjectEntryId, + external_paths: Vec>, + cx: &mut AsyncAppContext, + ) -> Result> { + let (fs, worktree_path, copy_to_path) = cx + .read_model(&this, |this, _| { + let local_worktree = this.as_local()?; + let mut abs_path = local_worktree.abs_path().to_path_buf(); + abs_path.push(&local_worktree.entry_for_id(copy_to_entry_id)?.path); + Some(( + local_worktree.fs.clone(), + local_worktree.abs_path().clone(), + abs_path, + )) + })? + .with_context(|| "Failed to get local worktree")?; + + let copy_into_entry = if copy_to_path.is_dir() { + copy_to_path + } else { + copy_to_path + .parent() + .with_context(|| "Failed to get parent of {copy_to_path:?}")? + .to_owned() + }; + + let paths: Vec<(Arc, PathBuf)> = external_paths + .into_iter() + .filter_map(|source| { + let file_name = source.file_name()?; + let mut target = copy_into_entry.clone(); + target.push(file_name); + Some((source, target)) + }) + .collect::>(); + + let paths_to_refresh = paths + .iter() + .filter_map(|(_, target)| Some(target.strip_prefix(&worktree_path).ok()?.into())) + .collect::>(); + + cx.background_executor() + .spawn(async move { + for (source, target) in paths { + fs.copy_file(source.as_ref(), target.as_ref(), fs::CopyOptions::default()) + .await + .with_context(|| { + anyhow!("Failed to copy file from {source:?} to {target:?}") + })?; + } + Ok::<(), anyhow::Error>(()) + }) + .await + .log_err(); + + let mut refresh = cx + .read_model(&this, |this, _| { + Some( + this.as_local()? + .refresh_entries_for_paths(paths_to_refresh.clone()), + ) + })? + .with_context(|| "Failed to get local worktree")?; + cx.background_executor() + .spawn(async move { + refresh.next().await; + Ok::<(), anyhow::Error>(()) + }) + .await + .log_err(); + + Ok(cx.read_model(&this, |this, _| { + paths_to_refresh + .iter() + .filter_map(|path| Some(this.entry_for_path(path)?.id)) + .collect() + })?) + } + pub fn expand_entry( &mut self, entry_id: ProjectEntryId, From f563cca89d6b1a7a281bea697d0080f0f843566d Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Mon, 10 Jun 2024 20:08:21 +0200 Subject: [PATCH 02/11] WIP --- crates/project/src/project.rs | 1 + crates/project_panel/src/project_panel.rs | 58 ++++++++++++++++++++--- crates/worktree/src/worktree.rs | 24 +++++++--- 3 files changed, 69 insertions(+), 14 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 31027266ff3509..5634ffe046ab1e 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1529,6 +1529,7 @@ impl Project { worktree, copy_to_entry_id, external_paths, + true, &mut cx, ) .await diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 047830916bf110..279f0bd0e8059f 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1743,29 +1743,73 @@ impl ProjectPanel { entry_id: ProjectEntryId, cx: &mut ViewContext, ) { - let paths: Vec<_> = external_paths + let mut paths: Vec> = external_paths .paths() .to_owned() .into_iter() .map(|path| Arc::from(path)) - .collect(); + .collect::>(); - let task = self.project.update(cx, |project, cx| { - project.copy_external_entries(entry_id, paths, cx) - }); + let open_file_after_drop = paths.len() == 1 && paths[0].is_file(); + + let Some(target_path) = maybe!({ + let target_worktree = self.project.read(cx).worktree_for_entry(entry_id, cx)?; + let mut target_path = target_worktree.read(cx).abs_path().to_path_buf(); + target_path.push(&target_worktree.read(cx).entry_for_id(entry_id)?.path); + Some(target_path) + }) else { + return; + }; + + let mut paths_to_replace = Vec::new(); + for path in &paths { + if let Some(name) = path.file_name() { + let mut target_path = target_path.clone(); + target_path.push(name); + if target_path.exists() { + paths_to_replace.push((name.to_string_lossy().to_string(), path.clone())); + } + } + } cx.spawn(|this, mut cx| { async move { + for (filename, original_path) in &paths_to_replace { + let answer = cx + .prompt( + PromptLevel::Info, + format!("A file or folder with name {filename} already exists in the destination folder. Do you want to replace it?").as_str(), + None, + &["Replace", "Cancel"], + ) + .await?; + if answer == 1 { + if let Some(item_idx) = paths.iter().position(|p| p == original_path) { + paths.remove(item_idx); + } + } + } + + if paths.is_empty() { + return Ok(()); + } + + let task = this.update(&mut cx, |this, cx| { + this.project.update(cx, |project, cx| { + project.copy_external_entries(entry_id, paths, cx) + }) + })?; + let opened_entries = task.await?; this.update(&mut cx, |this, cx| { - if opened_entries.len() == 1 { + if open_file_after_drop && !opened_entries.is_empty() { this.open_entry(opened_entries[0], true, true, false, cx); } }) } .log_err() }) - .detach() + .detach(); } fn drag_onto( diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index ead04b423666ec..aee3c5bcadd89d 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -1572,7 +1572,8 @@ impl LocalWorktree { pub async fn copy_external_entries( this: Model, copy_to_entry_id: ProjectEntryId, - external_paths: Vec>, + paths: Vec>, + overwrite_existing_files: bool, cx: &mut AsyncAppContext, ) -> Result> { let (fs, worktree_path, copy_to_path) = cx @@ -1597,7 +1598,7 @@ impl LocalWorktree { .to_owned() }; - let paths: Vec<(Arc, PathBuf)> = external_paths + let paths = paths .into_iter() .filter_map(|source| { let file_name = source.file_name()?; @@ -1615,11 +1616,19 @@ impl LocalWorktree { cx.background_executor() .spawn(async move { for (source, target) in paths { - fs.copy_file(source.as_ref(), target.as_ref(), fs::CopyOptions::default()) - .await - .with_context(|| { - anyhow!("Failed to copy file from {source:?} to {target:?}") - })?; + copy_recursive( + fs.as_ref(), + &source, + &target, + fs::CopyOptions { + overwrite: overwrite_existing_files, + ..Default::default() + }, + ) + .await + .with_context(|| { + anyhow!("Failed to copy file from {source:?} to {target:?}") + })?; } Ok::<(), anyhow::Error>(()) }) @@ -1634,6 +1643,7 @@ impl LocalWorktree { ) })? .with_context(|| "Failed to get local worktree")?; + cx.background_executor() .spawn(async move { refresh.next().await; From 23ddca43e0875ecaaae2c78417dbe0df3b05a766 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Mon, 10 Jun 2024 20:25:27 +0200 Subject: [PATCH 03/11] Cleanup --- crates/project/src/project.rs | 9 +++----- crates/project_panel/src/project_panel.rs | 25 ++++++++++------------- crates/worktree/src/worktree.rs | 23 ++++----------------- 3 files changed, 18 insertions(+), 39 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 5634ffe046ab1e..ddf9db9957734d 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1512,14 +1512,11 @@ impl Project { pub fn copy_external_entries( &self, - copy_to_entry_id: ProjectEntryId, + worktree: Model, + target_directory: PathBuf, external_paths: Vec>, cx: &mut ModelContext, ) -> Task>> { - let Some(worktree) = self.worktree_for_entry(copy_to_entry_id, cx) else { - return Task::ready(Ok(Vec::new())); - }; - if !worktree.read(cx).is_local() { return Task::ready(Ok(Vec::new())); }; @@ -1527,7 +1524,7 @@ impl Project { cx.spawn(move |_, mut cx| async move { LocalWorktree::copy_external_entries( worktree, - copy_to_entry_id, + target_directory, external_paths, true, &mut cx, diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 279f0bd0e8059f..cc9964a5f1cc7d 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1739,24 +1739,21 @@ impl ProjectPanel { fn drop_entry( &mut self, - external_paths: &ExternalPaths, + paths: &[PathBuf], entry_id: ProjectEntryId, cx: &mut ViewContext, ) { - let mut paths: Vec> = external_paths - .paths() - .to_owned() + let mut paths: Vec> = paths .into_iter() - .map(|path| Arc::from(path)) - .collect::>(); + .map(|path| Arc::from(path.clone())) + .collect(); let open_file_after_drop = paths.len() == 1 && paths[0].is_file(); - let Some(target_path) = maybe!({ - let target_worktree = self.project.read(cx).worktree_for_entry(entry_id, cx)?; - let mut target_path = target_worktree.read(cx).abs_path().to_path_buf(); - target_path.push(&target_worktree.read(cx).entry_for_id(entry_id)?.path); - Some(target_path) + let Some((target_directory, worktree)) = maybe!({ + let worktree = self.project.read(cx).worktree_for_entry(entry_id, cx)?; + let entry = worktree.read(cx).entry_for_id(entry_id)?; + Some((worktree.read(cx).absolutize(&entry.path).ok()?, worktree)) }) else { return; }; @@ -1764,7 +1761,7 @@ impl ProjectPanel { let mut paths_to_replace = Vec::new(); for path in &paths { if let Some(name) = path.file_name() { - let mut target_path = target_path.clone(); + let mut target_path = target_directory.clone(); target_path.push(name); if target_path.exists() { paths_to_replace.push((name.to_string_lossy().to_string(), path.clone())); @@ -1796,7 +1793,7 @@ impl ProjectPanel { let task = this.update(&mut cx, |this, cx| { this.project.update(cx, |project, cx| { - project.copy_external_entries(entry_id, paths, cx) + project.copy_external_entries(worktree, target_directory, paths, cx) }) })?; @@ -2068,7 +2065,7 @@ impl ProjectPanel { }) .on_drop( cx.listener(move |project_panel, external_paths: &ExternalPaths, cx| { - project_panel.drop_entry(external_paths, entry_id, cx); + project_panel.drop_entry(external_paths.paths(), entry_id, cx); cx.stop_propagation(); }), ) diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index aee3c5bcadd89d..5df0c5713ea5a1 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -1571,38 +1571,23 @@ impl LocalWorktree { pub async fn copy_external_entries( this: Model, - copy_to_entry_id: ProjectEntryId, + target_directory: PathBuf, paths: Vec>, overwrite_existing_files: bool, cx: &mut AsyncAppContext, ) -> Result> { - let (fs, worktree_path, copy_to_path) = cx + let (fs, worktree_path) = cx .read_model(&this, |this, _| { let local_worktree = this.as_local()?; - let mut abs_path = local_worktree.abs_path().to_path_buf(); - abs_path.push(&local_worktree.entry_for_id(copy_to_entry_id)?.path); - Some(( - local_worktree.fs.clone(), - local_worktree.abs_path().clone(), - abs_path, - )) + Some((local_worktree.fs.clone(), local_worktree.abs_path().clone())) })? .with_context(|| "Failed to get local worktree")?; - let copy_into_entry = if copy_to_path.is_dir() { - copy_to_path - } else { - copy_to_path - .parent() - .with_context(|| "Failed to get parent of {copy_to_path:?}")? - .to_owned() - }; - let paths = paths .into_iter() .filter_map(|source| { let file_name = source.file_name()?; - let mut target = copy_into_entry.clone(); + let mut target = target_directory.clone(); target.push(file_name); Some((source, target)) }) From c2096a602fc18ba7184c30be98bacd13e646e335 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Tue, 11 Jun 2024 10:52:33 +0200 Subject: [PATCH 04/11] Highlight containing folder and children in the project panel when dragging a file/folders into it. --- crates/project_panel/src/project_panel.rs | 80 +++++++++++++++++++---- 1 file changed, 68 insertions(+), 12 deletions(-) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index cc9964a5f1cc7d..b19adce8b7a347 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -11,11 +11,11 @@ use collections::{hash_map, BTreeSet, HashMap}; use git::repository::GitFileStatus; use gpui::{ actions, anchored, deferred, div, impl_actions, px, uniform_list, Action, AnyElement, - AppContext, AssetSource, AsyncWindowContext, ClipboardItem, DismissEvent, Div, EventEmitter, - ExternalPaths, FocusHandle, FocusableView, InteractiveElement, KeyContext, ListSizingBehavior, - Model, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render, - Stateful, Styled, Subscription, Task, UniformListScrollHandle, View, ViewContext, - VisualContext as _, WeakView, WindowContext, + AppContext, AssetSource, AsyncWindowContext, ClipboardItem, DismissEvent, Div, DragMoveEvent, + EventEmitter, ExternalPaths, FocusHandle, FocusableView, InteractiveElement, KeyContext, + ListSizingBehavior, Model, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, + PromptLevel, Render, Stateful, Styled, Subscription, Task, UniformListScrollHandle, View, + ViewContext, VisualContext as _, WeakView, WindowContext, }; use menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev}; use project::{Entry, EntryKind, Fs, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId}; @@ -50,6 +50,7 @@ pub struct ProjectPanel { focus_handle: FocusHandle, visible_entries: Vec<(WorktreeId, Vec)>, last_worktree_root_id: Option, + last_drag_over_external_entry: Option, expanded_dir_ids: HashMap>, unfolded_dir_ids: HashSet, // Currently selected entry in a file tree @@ -280,6 +281,7 @@ impl ProjectPanel { focus_handle, visible_entries: Default::default(), last_worktree_root_id: Default::default(), + last_drag_over_external_entry: None, expanded_dir_ids: Default::default(), unfolded_dir_ids: Default::default(), selection: None, @@ -1737,7 +1739,7 @@ impl ProjectPanel { }); } - fn drop_entry( + fn drop_external_files( &mut self, paths: &[PathBuf], entry_id: ProjectEntryId, @@ -1753,7 +1755,13 @@ impl ProjectPanel { let Some((target_directory, worktree)) = maybe!({ let worktree = self.project.read(cx).worktree_for_entry(entry_id, cx)?; let entry = worktree.read(cx).entry_for_id(entry_id)?; - Some((worktree.read(cx).absolutize(&entry.path).ok()?, worktree)) + let path = worktree.read(cx).absolutize(&entry.path).ok()?; + let target_directory = if path.is_dir() { + path + } else { + path.parent()?.to_path_buf() + }; + Some((target_directory, worktree)) }) else { return; }; @@ -2049,6 +2057,7 @@ impl ProjectPanel { .canonical_path .as_ref() .map(|f| f.to_string_lossy().to_string()); + let path = details.path.clone(); let depth = details.depth; let worktree_id = details.worktree_id; @@ -2060,12 +2069,59 @@ impl ProjectPanel { }; div() .id(entry_id.to_proto() as usize) - .drag_over(|style, _: &ExternalPaths, cx| { - style.bg(cx.theme().colors().drop_target_background) - }) + .on_drag_move::(cx.listener( + move |this, event: &DragMoveEvent, cx| { + if event.bounds.contains(&event.event.position) { + if this.last_drag_over_external_entry == Some(entry_id) { + return; + } + this.last_drag_over_external_entry = Some(entry_id); + this.marked_entries.clear(); + + let Some(worktree) = this + .project + .read(cx) + .worktree_for_id(selection.worktree_id, cx) + else { + return; + }; + let worktree = worktree.read(cx); + let Some(abs_path) = worktree.absolutize(&path).log_err() else { + return; + }; + + let path = if abs_path.is_dir() { + path.as_ref() + } else if let Some(parent) = path.parent() { + parent + } else { + return; + }; + + let Some(entry) = worktree.entry_for_path(path) else { + return; + }; + + this.marked_entries.insert(SelectedEntry { + entry_id: entry.id, + worktree_id: worktree.id(), + }); + + for entry in worktree.child_entries(path) { + this.marked_entries.insert(SelectedEntry { + entry_id: entry.id, + worktree_id: worktree.id(), + }); + } + + cx.notify(); + } + }, + )) .on_drop( - cx.listener(move |project_panel, external_paths: &ExternalPaths, cx| { - project_panel.drop_entry(external_paths.paths(), entry_id, cx); + cx.listener(move |this, external_paths: &ExternalPaths, cx| { + this.last_drag_over_external_entry = None; + this.drop_external_files(external_paths.paths(), entry_id, cx); cx.stop_propagation(); }), ) From 47656879c774bd68815846fa4426f4bce8944694 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Tue, 11 Jun 2024 10:58:38 +0200 Subject: [PATCH 05/11] Cleanup --- crates/project_panel/src/project_panel.rs | 37 ++++++++++------------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index b19adce8b7a347..84f19f77546538 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -2078,27 +2078,21 @@ impl ProjectPanel { this.last_drag_over_external_entry = Some(entry_id); this.marked_entries.clear(); - let Some(worktree) = this - .project - .read(cx) - .worktree_for_id(selection.worktree_id, cx) - else { - return; - }; - let worktree = worktree.read(cx); - let Some(abs_path) = worktree.absolutize(&path).log_err() else { - return; - }; - - let path = if abs_path.is_dir() { - path.as_ref() - } else if let Some(parent) = path.parent() { - parent - } else { - return; - }; - - let Some(entry) = worktree.entry_for_path(path) else { + let Some((worktree, path, entry)) = maybe!({ + let worktree = this + .project + .read(cx) + .worktree_for_id(selection.worktree_id, cx)?; + let worktree = worktree.read(cx); + let abs_path = worktree.absolutize(&path).log_err()?; + let path = if abs_path.is_dir() { + path.as_ref() + } else { + path.parent()? + }; + let entry = worktree.entry_for_path(path)?; + Some((worktree, path, entry)) + }) else { return; }; @@ -2121,6 +2115,7 @@ impl ProjectPanel { .on_drop( cx.listener(move |this, external_paths: &ExternalPaths, cx| { this.last_drag_over_external_entry = None; + this.marked_entries.clear(); this.drop_external_files(external_paths.paths(), entry_id, cx); cx.stop_propagation(); }), From cfe67830688bbd99dd5d5ae8f586fc0647fbe6aa Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Tue, 11 Jun 2024 11:13:17 +0200 Subject: [PATCH 06/11] Allow opening new project by dropping a directory inside the empty project panel --- crates/project_panel/src/project_panel.rs | 24 +++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 84f19f77546538..4ff695078c9270 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -2417,6 +2417,30 @@ impl Render for ProjectPanel { .log_err(); })), ) + .drag_over::(|style, _, cx| { + style.bg(cx.theme().colors().drop_target_background) + }) + .on_drop( + cx.listener(move |this, external_paths: &ExternalPaths, cx| { + this.last_drag_over_external_entry = None; + this.marked_entries.clear(); + let Some(state) = workspace::AppState::global(cx).upgrade() else { + return; + }; + + workspace::open_paths( + external_paths.paths(), + state, + workspace::OpenOptions { + open_new_workspace: Some(false), + replace_window: None, + }, + cx, + ) + .detach_and_log_err(cx); + cx.stop_propagation(); + }), + ) } } } From b9d6b208a98388b1bf93fbb848831ffebec4c5a3 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Tue, 11 Jun 2024 11:30:20 +0200 Subject: [PATCH 07/11] Do not open new window when opening a new workspace --- crates/project_panel/src/project_panel.rs | 6 ++++-- crates/worktree/src/worktree.rs | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 1a30f6206f17bd..1d51e59d72de2d 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -2412,8 +2412,10 @@ impl Render for ProjectPanel { external_paths.paths(), state, workspace::OpenOptions { - open_new_workspace: Some(false), - replace_window: None, + open_new_workspace: Some(true), + replace_window: cx + .active_window() + .and_then(|window| window.downcast::()), }, cx, ) diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 2794cafb2b2908..666cc116ea8f31 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -1647,12 +1647,12 @@ impl LocalWorktree { .await .log_err(); - Ok(cx.read_model(&this, |this, _| { + cx.read_model(&this, |this, _| { paths_to_refresh .iter() .filter_map(|path| Some(this.entry_for_path(path)?.id)) .collect() - })?) + }) } fn expand_entry( From 4371d37befbcb63e6081411e97aaf7fefd00084f Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Tue, 11 Jun 2024 11:47:15 +0200 Subject: [PATCH 08/11] Adjust code to match new worktree structure --- crates/project/src/project.rs | 15 +--- crates/worktree/src/worktree.rs | 120 ++++++++++++++++++-------------- 2 files changed, 70 insertions(+), 65 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 269f255fb8c49c..b4e2c8d7325ebe 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1484,19 +1484,8 @@ impl Project { external_paths: Vec>, cx: &mut ModelContext, ) -> Task>> { - if !worktree.read(cx).is_local() { - return Task::ready(Ok(Vec::new())); - }; - - cx.spawn(move |_, mut cx| async move { - LocalWorktree::copy_external_entries( - worktree, - target_directory, - external_paths, - true, - &mut cx, - ) - .await + worktree.update(cx, |worktree, cx| { + worktree.copy_external_entries(target_directory, external_paths, true, cx) }) } diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 666cc116ea8f31..967937f2f05629 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -838,6 +838,23 @@ impl Worktree { } } + pub fn copy_external_entries( + &mut self, + target_directory: PathBuf, + paths: Vec>, + overwrite_existing_files: bool, + cx: &mut ModelContext, + ) -> Task>> { + match self { + Worktree::Local(this) => { + this.copy_external_entries(target_directory, paths, overwrite_existing_files, cx) + } + _ => Task::ready(Err(anyhow!( + "Dropping external entries is not supported for remote worktrees" + ))), + } + } + pub fn expand_entry( &mut self, entry_id: ProjectEntryId, @@ -1579,20 +1596,15 @@ impl LocalWorktree { }) } - pub async fn copy_external_entries( - this: Model, + pub fn copy_external_entries( + &mut self, target_directory: PathBuf, paths: Vec>, overwrite_existing_files: bool, - cx: &mut AsyncAppContext, - ) -> Result> { - let (fs, worktree_path) = cx - .read_model(&this, |this, _| { - let local_worktree = this.as_local()?; - Some((local_worktree.fs.clone(), local_worktree.abs_path().clone())) - })? - .with_context(|| "Failed to get local worktree")?; - + cx: &mut ModelContext, + ) -> Task>> { + let worktree_path = self.abs_path().clone(); + let fs = self.fs.clone(); let paths = paths .into_iter() .filter_map(|source| { @@ -1608,50 +1620,54 @@ impl LocalWorktree { .filter_map(|(_, target)| Some(target.strip_prefix(&worktree_path).ok()?.into())) .collect::>(); - cx.background_executor() - .spawn(async move { - for (source, target) in paths { - copy_recursive( - fs.as_ref(), - &source, - &target, - fs::CopyOptions { - overwrite: overwrite_existing_files, - ..Default::default() - }, + cx.spawn(|this, mut cx| async move { + cx.background_executor() + .spawn(async move { + for (source, target) in paths { + copy_recursive( + fs.as_ref(), + &source, + &target, + fs::CopyOptions { + overwrite: overwrite_existing_files, + ..Default::default() + }, + ) + .await + .with_context(|| { + anyhow!("Failed to copy file from {source:?} to {target:?}") + })?; + } + Ok::<(), anyhow::Error>(()) + }) + .await + .log_err(); + let mut refresh = cx.read_model( + &this.upgrade().with_context(|| "Dropped worktree")?, + |this, cx| { + Ok::( + this.as_local() + .with_context(|| "Worktree is not local")? + .refresh_entries_for_paths(paths_to_refresh.clone()), ) - .await - .with_context(|| { - anyhow!("Failed to copy file from {source:?} to {target:?}") - })?; - } - Ok::<(), anyhow::Error>(()) - }) - .await - .log_err(); - - let mut refresh = cx - .read_model(&this, |this, _| { - Some( - this.as_local()? - .refresh_entries_for_paths(paths_to_refresh.clone()), - ) - })? - .with_context(|| "Failed to get local worktree")?; + }, + )??; - cx.background_executor() - .spawn(async move { - refresh.next().await; - Ok::<(), anyhow::Error>(()) + cx.background_executor() + .spawn(async move { + refresh.next().await; + Ok::<(), anyhow::Error>(()) + }) + .await + .log_err(); + + let this = this.upgrade().with_context(|| "Dropped worktree")?; + cx.read_model(&this, |this, _| { + paths_to_refresh + .iter() + .filter_map(|path| Some(this.entry_for_path(path)?.id)) + .collect() }) - .await - .log_err(); - - cx.read_model(&this, |this, _| { - paths_to_refresh - .iter() - .filter_map(|path| Some(this.entry_for_path(path)?.id)) - .collect() }) } From f379cfbf42e6fbb3577149b72f6d85fadff6da04 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Tue, 11 Jun 2024 12:07:59 +0200 Subject: [PATCH 09/11] Simplify code --- crates/project/src/project.rs | 12 ------------ crates/project_panel/src/project_panel.rs | 6 ++---- crates/worktree/src/worktree.rs | 4 ++-- 3 files changed, 4 insertions(+), 18 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index b4e2c8d7325ebe..a70e15ac753fd9 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1477,18 +1477,6 @@ impl Project { }) } - pub fn copy_external_entries( - &self, - worktree: Model, - target_directory: PathBuf, - external_paths: Vec>, - cx: &mut ModelContext, - ) -> Task>> { - worktree.update(cx, |worktree, cx| { - worktree.copy_external_entries(target_directory, external_paths, true, cx) - }) - } - pub fn rename_entry( &mut self, entry_id: ProjectEntryId, diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 1d51e59d72de2d..51c3e9edeb4018 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1779,10 +1779,8 @@ impl ProjectPanel { return Ok(()); } - let task = this.update(&mut cx, |this, cx| { - this.project.update(cx, |project, cx| { - project.copy_external_entries(worktree, target_directory, paths, cx) - }) + let task = worktree.update(&mut cx, |worktree, cx| { + worktree.copy_external_entries(target_directory, paths, true, cx) })?; let opened_entries = task.await?; diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 967937f2f05629..0796685b240f16 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -1620,7 +1620,7 @@ impl LocalWorktree { .filter_map(|(_, target)| Some(target.strip_prefix(&worktree_path).ok()?.into())) .collect::>(); - cx.spawn(|this, mut cx| async move { + cx.spawn(|this, cx| async move { cx.background_executor() .spawn(async move { for (source, target) in paths { @@ -1644,7 +1644,7 @@ impl LocalWorktree { .log_err(); let mut refresh = cx.read_model( &this.upgrade().with_context(|| "Dropped worktree")?, - |this, cx| { + |this, _| { Ok::( this.as_local() .with_context(|| "Worktree is not local")? From cf70890c8065f949e431f55e391e8b6a45e5a997 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Tue, 11 Jun 2024 12:28:51 +0200 Subject: [PATCH 10/11] Do not open new window when dropping folder --- crates/project_panel/src/project_panel.rs | 41 +++++++++++------------ crates/worktree/src/worktree.rs | 2 +- 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 51c3e9edeb4018..3855657d3ded41 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -50,7 +50,7 @@ pub struct ProjectPanel { focus_handle: FocusHandle, visible_entries: Vec<(WorktreeId, Vec)>, last_worktree_root_id: Option, - last_drag_over_external_entry: Option, + last_external_paths_drag_over_entry: Option, expanded_dir_ids: HashMap>, unfolded_dir_ids: HashSet, // Currently selected entry in a file tree @@ -261,7 +261,7 @@ impl ProjectPanel { focus_handle, visible_entries: Default::default(), last_worktree_root_id: Default::default(), - last_drag_over_external_entry: None, + last_external_paths_drag_over_entry: None, expanded_dir_ids: Default::default(), unfolded_dir_ids: Default::default(), selection: None, @@ -2050,10 +2050,10 @@ impl ProjectPanel { .on_drag_move::(cx.listener( move |this, event: &DragMoveEvent, cx| { if event.bounds.contains(&event.event.position) { - if this.last_drag_over_external_entry == Some(entry_id) { + if this.last_external_paths_drag_over_entry == Some(entry_id) { return; } - this.last_drag_over_external_entry = Some(entry_id); + this.last_external_paths_drag_over_entry = Some(entry_id); this.marked_entries.clear(); let Some((worktree, path, entry)) = maybe!({ @@ -2092,7 +2092,7 @@ impl ProjectPanel { )) .on_drop( cx.listener(move |this, external_paths: &ExternalPaths, cx| { - this.last_drag_over_external_entry = None; + this.last_external_paths_drag_over_entry = None; this.marked_entries.clear(); this.drop_external_files(external_paths.paths(), entry_id, cx); cx.stop_propagation(); @@ -2400,24 +2400,21 @@ impl Render for ProjectPanel { }) .on_drop( cx.listener(move |this, external_paths: &ExternalPaths, cx| { - this.last_drag_over_external_entry = None; + this.last_external_paths_drag_over_entry = None; this.marked_entries.clear(); - let Some(state) = workspace::AppState::global(cx).upgrade() else { - return; - }; - - workspace::open_paths( - external_paths.paths(), - state, - workspace::OpenOptions { - open_new_workspace: Some(true), - replace_window: cx - .active_window() - .and_then(|window| window.downcast::()), - }, - cx, - ) - .detach_and_log_err(cx); + if let Some(task) = this + .workspace + .update(cx, |workspace, cx| { + workspace.open_workspace_for_paths( + true, + external_paths.paths().to_owned(), + cx, + ) + }) + .log_err() + { + task.detach_and_log_err(cx); + } cx.stop_propagation(); }), ) diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 0796685b240f16..9d00a50c91c392 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -850,7 +850,7 @@ impl Worktree { this.copy_external_entries(target_directory, paths, overwrite_existing_files, cx) } _ => Task::ready(Err(anyhow!( - "Dropping external entries is not supported for remote worktrees" + "Copying external entries is not supported for remote worktrees" ))), } } From 04247cfc32bb71cf5be0eb054a9f22ba7d8ddef1 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Thu, 13 Jun 2024 20:34:14 +0200 Subject: [PATCH 11/11] Do not allow copying a file into itself --- crates/worktree/src/worktree.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 9d00a50c91c392..797acfebe1b3ff 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -1611,7 +1611,13 @@ impl LocalWorktree { let file_name = source.file_name()?; let mut target = target_directory.clone(); target.push(file_name); - Some((source, target)) + + // Do not allow copying the same file to itself. + if source.as_ref() != target.as_path() { + Some((source, target)) + } else { + None + } }) .collect::>();