@@ -2176,7 +2176,7 @@ impl ProjectPanel {
cx: &mut Context<ProjectPanel>,
) {
maybe!({
- let items_to_delete = self.disjoint_entries(cx);
+ let items_to_delete = self.disjoint_effective_entries(cx);
if items_to_delete.is_empty() {
return None;
}
@@ -2830,7 +2830,7 @@ impl ProjectPanel {
}
fn cut(&mut self, _: &Cut, _: &mut Window, cx: &mut Context<Self>) {
- let entries = self.disjoint_entries(cx);
+ let entries = self.disjoint_effective_entries(cx);
if !entries.is_empty() {
self.clipboard = Some(ClipboardEntry::Cut(entries));
cx.notify();
@@ -2838,7 +2838,7 @@ impl ProjectPanel {
}
fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context<Self>) {
- let entries = self.disjoint_entries(cx);
+ let entries = self.disjoint_effective_entries(cx);
if !entries.is_empty() {
self.clipboard = Some(ClipboardEntry::Copied(entries));
cx.notify();
@@ -3291,15 +3291,22 @@ impl ProjectPanel {
self.index_for_entry(selection.entry_id, selection.worktree_id)
}
- fn disjoint_entries(&self, cx: &App) -> BTreeSet<SelectedEntry> {
- let marked_entries = self.effective_entries();
+ fn disjoint_effective_entries(&self, cx: &App) -> BTreeSet<SelectedEntry> {
+ self.disjoint_entries(self.effective_entries(), cx)
+ }
+
+ fn disjoint_entries(
+ &self,
+ entries: BTreeSet<SelectedEntry>,
+ cx: &App,
+ ) -> BTreeSet<SelectedEntry> {
let mut sanitized_entries = BTreeSet::new();
- if marked_entries.is_empty() {
+ if entries.is_empty() {
return sanitized_entries;
}
let project = self.project.read(cx);
- let marked_entries_by_worktree: HashMap<WorktreeId, Vec<SelectedEntry>> = marked_entries
+ let entries_by_worktree: HashMap<WorktreeId, Vec<SelectedEntry>> = entries
.into_iter()
.filter(|entry| !project.entry_is_worktree_root(entry.entry_id, cx))
.fold(HashMap::default(), |mut map, entry| {
@@ -3307,10 +3314,10 @@ impl ProjectPanel {
map
});
- for (worktree_id, marked_entries) in marked_entries_by_worktree {
+ for (worktree_id, worktree_entries) in entries_by_worktree {
if let Some(worktree) = project.worktree_for_id(worktree_id, cx) {
let worktree = worktree.read(cx);
- let marked_dir_paths = marked_entries
+ let dir_paths = worktree_entries
.iter()
.filter_map(|entry| {
worktree.entry_for_id(entry.entry_id).and_then(|entry| {
@@ -3323,15 +3330,15 @@ impl ProjectPanel {
})
.collect::<BTreeSet<_>>();
- sanitized_entries.extend(marked_entries.into_iter().filter(|entry| {
+ sanitized_entries.extend(worktree_entries.into_iter().filter(|entry| {
let Some(entry_info) = worktree.entry_for_id(entry.entry_id) else {
return false;
};
let entry_path = entry_info.path.as_ref();
- let inside_marked_dir = marked_dir_paths.iter().any(|&marked_dir_path| {
- entry_path != marked_dir_path && entry_path.starts_with(marked_dir_path)
+ let inside_selected_dir = dir_paths.iter().any(|&dir_path| {
+ entry_path != dir_path && entry_path.starts_with(dir_path)
});
- !inside_marked_dir
+ !inside_selected_dir
}));
}
}
@@ -3923,6 +3930,15 @@ impl ProjectPanel {
window: &mut Window,
cx: &mut Context<Self>,
) {
+ let resolved_selections = selections
+ .items()
+ .map(|entry| SelectedEntry {
+ entry_id: self.resolve_entry(entry.entry_id),
+ worktree_id: entry.worktree_id,
+ })
+ .collect::<BTreeSet<SelectedEntry>>();
+ let entries = self.disjoint_entries(resolved_selections, cx);
+
if Self::is_copy_modifier_set(&window.modifiers()) {
let _ = maybe!({
let project = self.project.read(cx);
@@ -3935,7 +3951,7 @@ impl ProjectPanel {
let mut copy_tasks = Vec::new();
let mut disambiguation_range = None;
- for selection in selections.items() {
+ for selection in &entries {
let (new_path, new_disambiguation_range) = self.create_paste_path(
selection,
(target_worktree.clone(), &target_entry),
@@ -3979,8 +3995,8 @@ impl ProjectPanel {
Some(())
});
} else {
- for selection in selections.items() {
- self.move_entry(selection.entry_id, target_entry_id, is_file, cx);
+ for entry in entries {
+ self.move_entry(entry.entry_id, target_entry_id, is_file, cx);
}
}
}
@@ -4604,7 +4620,7 @@ impl ProjectPanel {
let dragged_selection = DraggedSelection {
active_selection: SelectedEntry {
worktree_id: selection.worktree_id,
- entry_id: self.resolve_entry(selection.entry_id),
+ entry_id: selection.entry_id,
},
marked_selections: Arc::from(self.marked_entries.clone()),
};
@@ -4053,10 +4053,7 @@ async fn test_dragged_selection_resolve_entry(cx: &mut gpui::TestAppContext) {
select_path(&panel, "root/a/b/c/d", cx);
panel.update_in(cx, |panel, window, cx| {
let drag = DraggedSelection {
- active_selection: SelectedEntry {
- worktree_id: panel.state.selection.as_ref().unwrap().worktree_id,
- entry_id: panel.resolve_entry(panel.state.selection.as_ref().unwrap().entry_id),
- },
+ active_selection: *panel.state.selection.as_ref().unwrap(),
marked_selections: Arc::new([*panel.state.selection.as_ref().unwrap()]),
};
let target_entry = panel
@@ -4086,10 +4083,7 @@ async fn test_dragged_selection_resolve_entry(cx: &mut gpui::TestAppContext) {
select_path(&panel, "root/target_destination/d", cx);
panel.update_in(cx, |panel, window, cx| {
let drag = DraggedSelection {
- active_selection: SelectedEntry {
- worktree_id: panel.state.selection.as_ref().unwrap().worktree_id,
- entry_id: panel.resolve_entry(panel.state.selection.as_ref().unwrap().entry_id),
- },
+ active_selection: *panel.state.selection.as_ref().unwrap(),
marked_selections: Arc::new([*panel.state.selection.as_ref().unwrap()]),
};
let target_entry = panel
@@ -4109,10 +4103,7 @@ async fn test_dragged_selection_resolve_entry(cx: &mut gpui::TestAppContext) {
select_path(&panel, "root/a/b", cx);
panel.update_in(cx, |panel, window, cx| {
let drag = DraggedSelection {
- active_selection: SelectedEntry {
- worktree_id: panel.state.selection.as_ref().unwrap().worktree_id,
- entry_id: panel.resolve_entry(panel.state.selection.as_ref().unwrap().entry_id),
- },
+ active_selection: *panel.state.selection.as_ref().unwrap(),
marked_selections: Arc::new([*panel.state.selection.as_ref().unwrap()]),
};
let target_entry = panel
@@ -4138,10 +4129,7 @@ async fn test_dragged_selection_resolve_entry(cx: &mut gpui::TestAppContext) {
select_path(&panel, "root/target_destination/b", cx);
panel.update_in(cx, |panel, window, cx| {
let drag = DraggedSelection {
- active_selection: SelectedEntry {
- worktree_id: panel.state.selection.as_ref().unwrap().worktree_id,
- entry_id: panel.resolve_entry(panel.state.selection.as_ref().unwrap().entry_id),
- },
+ active_selection: *panel.state.selection.as_ref().unwrap(),
marked_selections: Arc::new([*panel.state.selection.as_ref().unwrap()]),
};
let target_entry = panel
@@ -4161,10 +4149,7 @@ async fn test_dragged_selection_resolve_entry(cx: &mut gpui::TestAppContext) {
select_path(&panel, "root/a", cx);
panel.update_in(cx, |panel, window, cx| {
let drag = DraggedSelection {
- active_selection: SelectedEntry {
- worktree_id: panel.state.selection.as_ref().unwrap().worktree_id,
- entry_id: panel.resolve_entry(panel.state.selection.as_ref().unwrap().entry_id),
- },
+ active_selection: *panel.state.selection.as_ref().unwrap(),
marked_selections: Arc::new([*panel.state.selection.as_ref().unwrap()]),
};
let target_entry = panel
@@ -4187,6 +4172,88 @@ async fn test_dragged_selection_resolve_entry(cx: &mut gpui::TestAppContext) {
);
}
+#[gpui::test]
+async fn test_drag_marked_entries_in_folded_directories(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(cx.executor());
+ fs.insert_tree(
+ "/root",
+ json!({
+ "a": {
+ "b": {
+ "c": {}
+ }
+ },
+ "e": {
+ "f": {
+ "g": {}
+ }
+ },
+ "target": {}
+ }),
+ )
+ .await;
+
+ let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
+ let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
+ let cx = &mut VisualTestContext::from_window(*workspace, cx);
+
+ cx.update(|_, cx| {
+ let settings = *ProjectPanelSettings::get_global(cx);
+ ProjectPanelSettings::override_global(
+ ProjectPanelSettings {
+ auto_fold_dirs: true,
+ ..settings
+ },
+ cx,
+ );
+ });
+
+ let panel = workspace.update(cx, ProjectPanel::new).unwrap();
+ cx.run_until_parked();
+
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &["v root", " > a/b/c", " > e/f/g", " > target"]
+ );
+
+ select_folded_path_with_mark(&panel, "root/a/b/c", "root/a/b", cx);
+ select_folded_path_with_mark(&panel, "root/e/f/g", "root/e/f", cx);
+
+ panel.update_in(cx, |panel, window, cx| {
+ let drag = DraggedSelection {
+ active_selection: *panel.state.selection.as_ref().unwrap(),
+ marked_selections: panel.marked_entries.clone().into(),
+ };
+ let target_entry = panel
+ .project
+ .read(cx)
+ .visible_worktrees(cx)
+ .next()
+ .unwrap()
+ .read(cx)
+ .entry_for_path(rel_path("target"))
+ .unwrap();
+ panel.drag_onto(&drag, target_entry.id, false, window, cx);
+ });
+ cx.executor().run_until_parked();
+
+ // After dragging 'b/c' and 'f/g' should be moved to target
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ "v root",
+ " > a",
+ " > e",
+ " v target",
+ " > b/c",
+ " > f/g <== selected <== marked"
+ ],
+ "Should move 'b/c' and 'f/g' to target, leaving 'a' and 'e'"
+ );
+}
+
#[gpui::test]
async fn test_drag_entries_between_different_worktrees(cx: &mut gpui::TestAppContext) {
init_test(cx);
@@ -4287,6 +4354,89 @@ async fn test_drag_entries_between_different_worktrees(cx: &mut gpui::TestAppCon
);
}
+#[gpui::test]
+async fn test_drag_multiple_entries(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(cx.executor());
+ fs.insert_tree(
+ "/root",
+ json!({
+ "src": {
+ "folder1": {
+ "mod.rs": "// folder1 mod"
+ },
+ "folder2": {
+ "mod.rs": "// folder2 mod"
+ },
+ "folder3": {
+ "mod.rs": "// folder3 mod",
+ "helper.rs": "// helper"
+ },
+ "main.rs": ""
+ }
+ }),
+ )
+ .await;
+
+ let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
+ let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
+ let cx = &mut VisualTestContext::from_window(*workspace, cx);
+ let panel = workspace.update(cx, ProjectPanel::new).unwrap();
+ cx.run_until_parked();
+
+ toggle_expand_dir(&panel, "root/src", cx);
+ toggle_expand_dir(&panel, "root/src/folder1", cx);
+ toggle_expand_dir(&panel, "root/src/folder2", cx);
+ toggle_expand_dir(&panel, "root/src/folder3", cx);
+ cx.run_until_parked();
+
+ // Case 1: Dragging a folder and a file from a sibling folder together.
+ panel.update(cx, |panel, _| panel.marked_entries.clear());
+ select_path_with_mark(&panel, "root/src/folder1", cx);
+ select_path_with_mark(&panel, "root/src/folder2/mod.rs", cx);
+
+ drag_selection_to(&panel, "root", false, cx);
+
+ assert!(
+ find_project_entry(&panel, "root/folder1", cx).is_some(),
+ "folder1 should be at root after drag"
+ );
+ assert!(
+ find_project_entry(&panel, "root/folder1/mod.rs", cx).is_some(),
+ "folder1/mod.rs should still be inside folder1 after drag"
+ );
+ assert_eq!(
+ find_project_entry(&panel, "root/src/folder1", cx),
+ None,
+ "folder1 should no longer be in src"
+ );
+ assert!(
+ find_project_entry(&panel, "root/mod.rs", cx).is_some(),
+ "mod.rs from folder2 should be at root"
+ );
+
+ // Case 2: Dragging a folder and its own child together.
+ panel.update(cx, |panel, _| panel.marked_entries.clear());
+ select_path_with_mark(&panel, "root/src/folder3", cx);
+ select_path_with_mark(&panel, "root/src/folder3/mod.rs", cx);
+
+ drag_selection_to(&panel, "root", false, cx);
+
+ assert!(
+ find_project_entry(&panel, "root/folder3", cx).is_some(),
+ "folder3 should be at root after drag"
+ );
+ assert!(
+ find_project_entry(&panel, "root/folder3/mod.rs", cx).is_some(),
+ "folder3/mod.rs should still be inside folder3"
+ );
+ assert!(
+ find_project_entry(&panel, "root/folder3/helper.rs", cx).is_some(),
+ "folder3/helper.rs should still be inside folder3"
+ );
+}
+
#[gpui::test]
async fn test_autoreveal_and_gitignored_files(cx: &mut gpui::TestAppContext) {
init_test_with_editor(cx);
@@ -7678,6 +7828,40 @@ fn select_path_with_mark(panel: &Entity<ProjectPanel>, path: &str, cx: &mut Visu
});
}
+/// `leaf_path` is the full path to the leaf entry (e.g., "root/a/b/c")
+/// `active_ancestor_path` is the path to the ancestor that should be "active" (e.g., "root/a/b")
+fn select_folded_path_with_mark(
+ panel: &Entity<ProjectPanel>,
+ leaf_path: &str,
+ active_ancestor_path: &str,
+ cx: &mut VisualTestContext,
+) {
+ select_path_with_mark(panel, leaf_path, cx);
+ let active_ancestor_path = rel_path(active_ancestor_path);
+ panel.update(cx, |panel, cx| {
+ let leaf_entry_id = panel.state.selection.unwrap().entry_id;
+ if let Some(folded_ancestors) = panel.state.ancestors.get_mut(&leaf_entry_id) {
+ for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
+ let worktree = worktree.read(cx);
+ if let Ok(active_relative_path) =
+ active_ancestor_path.strip_prefix(worktree.root_name())
+ {
+ let active_entry_id = worktree.entry_for_path(active_relative_path).unwrap().id;
+ if let Some(index) = folded_ancestors
+ .ancestors
+ .iter()
+ .position(|&id| id == active_entry_id)
+ {
+ folded_ancestors.current_ancestor_depth =
+ folded_ancestors.ancestors.len() - 1 - index;
+ }
+ return;
+ }
+ }
+ }
+ });
+}
+
fn drag_selection_to(
panel: &Entity<ProjectPanel>,
target_path: &str,