project_panel: Allow dragging folded directories onto other items (#38070)
In https://github.com/zed-industries/zed/pull/22983 we made it possible to drag items onto folded directories. This PR handles the reverse: dragging folded directories onto other items. Release Notes: - Improved drag-and-drop support by allowing folded directories to be dragged onto other items in Project Panel.
This commit is contained in:
@@ -310,6 +310,27 @@ impl FoldedAncestors {
|
||||
fn max_ancestor_depth(&self) -> usize {
|
||||
self.ancestors.len()
|
||||
}
|
||||
|
||||
/// Note: This returns None for last item in ancestors list
|
||||
fn active_ancestor(&self) -> Option<ProjectEntryId> {
|
||||
if self.current_ancestor_depth == 0 {
|
||||
return None;
|
||||
}
|
||||
self.ancestors.get(self.current_ancestor_depth).copied()
|
||||
}
|
||||
|
||||
fn active_index(&self) -> usize {
|
||||
self.max_ancestor_depth()
|
||||
.saturating_sub(1)
|
||||
.saturating_sub(self.current_ancestor_depth)
|
||||
}
|
||||
|
||||
fn active_component(&self, file_name: &str) -> Option<String> {
|
||||
Path::new(file_name)
|
||||
.components()
|
||||
.nth(self.active_index())
|
||||
.map(|comp| comp.as_os_str().to_string_lossy().into_owned())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_settings(cx: &mut App) {
|
||||
@@ -393,7 +414,8 @@ struct SerializedProjectPanel {
|
||||
|
||||
struct DraggedProjectEntryView {
|
||||
selection: SelectedEntry,
|
||||
details: EntryDetails,
|
||||
icon: Option<SharedString>,
|
||||
filename: String,
|
||||
click_offset: Point<Pixels>,
|
||||
selections: Arc<[SelectedEntry]>,
|
||||
}
|
||||
@@ -2943,13 +2965,7 @@ impl ProjectPanel {
|
||||
fn resolve_entry(&self, id: ProjectEntryId) -> ProjectEntryId {
|
||||
self.ancestors
|
||||
.get(&id)
|
||||
.and_then(|ancestors| {
|
||||
if ancestors.current_ancestor_depth == 0 {
|
||||
return None;
|
||||
}
|
||||
ancestors.ancestors.get(ancestors.current_ancestor_depth)
|
||||
})
|
||||
.copied()
|
||||
.and_then(|ancestors| ancestors.active_ancestor())
|
||||
.unwrap_or(id)
|
||||
}
|
||||
|
||||
@@ -4090,7 +4106,10 @@ impl ProjectPanel {
|
||||
let depth = details.depth;
|
||||
let worktree_id = details.worktree_id;
|
||||
let dragged_selection = DraggedSelection {
|
||||
active_selection: selection,
|
||||
active_selection: SelectedEntry {
|
||||
worktree_id: selection.worktree_id,
|
||||
entry_id: self.resolve_entry(selection.entry_id),
|
||||
},
|
||||
marked_selections: Arc::from(self.marked_entries.clone()),
|
||||
};
|
||||
|
||||
@@ -4320,14 +4339,19 @@ impl ProjectPanel {
|
||||
))
|
||||
.on_drag(
|
||||
dragged_selection,
|
||||
move |selection, click_offset, _window, cx| {
|
||||
cx.new(|_| DraggedProjectEntryView {
|
||||
details: details.clone(),
|
||||
click_offset,
|
||||
selection: selection.active_selection,
|
||||
selections: selection.marked_selections.clone(),
|
||||
})
|
||||
},
|
||||
{
|
||||
let active_component = self.ancestors.get(&entry_id).and_then(|ancestors| ancestors.active_component(&details.filename));
|
||||
move |selection, click_offset, _window, cx| {
|
||||
let filename = active_component.as_ref().unwrap_or_else(|| &details.filename);
|
||||
cx.new(|_| DraggedProjectEntryView {
|
||||
icon: details.icon.clone(),
|
||||
filename: filename.clone(),
|
||||
click_offset,
|
||||
selection: selection.active_selection,
|
||||
selections: selection.marked_selections.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
.on_drop(
|
||||
cx.listener(move |this, selections: &DraggedSelection, window, cx| {
|
||||
@@ -4530,19 +4554,13 @@ impl ProjectPanel {
|
||||
if let Some(folded_ancestors) = self.ancestors.get(&entry_id) {
|
||||
let components = Path::new(&file_name)
|
||||
.components()
|
||||
.map(|comp| {
|
||||
comp.as_os_str().to_string_lossy().into_owned()
|
||||
})
|
||||
.map(|comp| comp.as_os_str().to_string_lossy().into_owned())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let active_index = folded_ancestors.active_index();
|
||||
let components_len = components.len();
|
||||
// TODO this can underflow
|
||||
let active_index = components_len
|
||||
- 1
|
||||
- folded_ancestors.current_ancestor_depth;
|
||||
const DELIMITER: SharedString =
|
||||
SharedString::new_static(std::path::MAIN_SEPARATOR_STR);
|
||||
for (index, component) in components.into_iter().enumerate() {
|
||||
for (index, component) in components.iter().enumerate() {
|
||||
if index != 0 {
|
||||
let delimiter_target_index = index - 1;
|
||||
let target_entry_id = folded_ancestors.ancestors.get(components_len - 1 - delimiter_target_index).cloned();
|
||||
@@ -4643,16 +4661,19 @@ impl ProjectPanel {
|
||||
}))
|
||||
})
|
||||
})
|
||||
.on_click(cx.listener(move |this, _, _, cx| {
|
||||
if index != active_index
|
||||
&& let Some(folds) =
|
||||
this.ancestors.get_mut(&entry_id)
|
||||
{
|
||||
folds.current_ancestor_depth =
|
||||
components_len - 1 - index;
|
||||
cx.notify();
|
||||
}
|
||||
}))
|
||||
.on_mouse_down(
|
||||
MouseButton::Left,
|
||||
cx.listener(move |this, _, _, cx| {
|
||||
if index != active_index
|
||||
&& let Some(folds) =
|
||||
this.ancestors.get_mut(&entry_id)
|
||||
{
|
||||
folds.current_ancestor_depth =
|
||||
components_len - 1 - index;
|
||||
cx.notify();
|
||||
}
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Label::new(component)
|
||||
.single_line()
|
||||
@@ -5846,12 +5867,12 @@ impl Render for DraggedProjectEntryView {
|
||||
if self.selections.len() > 1 && self.selections.contains(&self.selection) {
|
||||
this.child(Label::new(format!("{} entries", self.selections.len())))
|
||||
} else {
|
||||
this.child(if let Some(icon) = &self.details.icon {
|
||||
this.child(if let Some(icon) = &self.icon {
|
||||
div().child(Icon::from_path(icon.clone()))
|
||||
} else {
|
||||
div()
|
||||
})
|
||||
.child(Label::new(self.details.filename.clone()))
|
||||
.child(Label::new(self.filename.clone()))
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -3162,6 +3162,182 @@ async fn test_multiple_marked_entries(cx: &mut gpui::TestAppContext) {
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_dragged_selection_resolve_entry(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
"a": {
|
||||
"b": {
|
||||
"c": {
|
||||
"d": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"target_destination": {}
|
||||
}),
|
||||
)
|
||||
.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();
|
||||
|
||||
// Case 1: Move last dir 'd' - should move only 'd', leaving 'a/b/c'
|
||||
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.selection.as_ref().unwrap().worktree_id,
|
||||
entry_id: panel.resolve_entry(panel.selection.as_ref().unwrap().entry_id),
|
||||
},
|
||||
marked_selections: Arc::new([*panel.selection.as_ref().unwrap()]),
|
||||
};
|
||||
let target_entry = panel
|
||||
.project
|
||||
.read(cx)
|
||||
.visible_worktrees(cx)
|
||||
.next()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.entry_for_path("target_destination")
|
||||
.unwrap();
|
||||
panel.drag_onto(&drag, target_entry.id, false, window, cx);
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&[
|
||||
"v root",
|
||||
" > a/b/c",
|
||||
" > target_destination/d <== selected"
|
||||
],
|
||||
"Moving last empty directory 'd' should leave 'a/b/c' and move only 'd'"
|
||||
);
|
||||
|
||||
// Reset
|
||||
select_path(&panel, "root/target_destination/d", cx);
|
||||
panel.update_in(cx, |panel, window, cx| {
|
||||
let drag = DraggedSelection {
|
||||
active_selection: SelectedEntry {
|
||||
worktree_id: panel.selection.as_ref().unwrap().worktree_id,
|
||||
entry_id: panel.resolve_entry(panel.selection.as_ref().unwrap().entry_id),
|
||||
},
|
||||
marked_selections: Arc::new([*panel.selection.as_ref().unwrap()]),
|
||||
};
|
||||
let target_entry = panel
|
||||
.project
|
||||
.read(cx)
|
||||
.visible_worktrees(cx)
|
||||
.next()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.entry_for_path("a/b/c")
|
||||
.unwrap();
|
||||
panel.drag_onto(&drag, target_entry.id, false, window, cx);
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
// Case 2: Move middle dir 'b' - should move 'b/c/d', leaving only 'a'
|
||||
select_path(&panel, "root/a/b", cx);
|
||||
panel.update_in(cx, |panel, window, cx| {
|
||||
let drag = DraggedSelection {
|
||||
active_selection: SelectedEntry {
|
||||
worktree_id: panel.selection.as_ref().unwrap().worktree_id,
|
||||
entry_id: panel.resolve_entry(panel.selection.as_ref().unwrap().entry_id),
|
||||
},
|
||||
marked_selections: Arc::new([*panel.selection.as_ref().unwrap()]),
|
||||
};
|
||||
let target_entry = panel
|
||||
.project
|
||||
.read(cx)
|
||||
.visible_worktrees(cx)
|
||||
.next()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.entry_for_path("target_destination")
|
||||
.unwrap();
|
||||
panel.drag_onto(&drag, target_entry.id, false, window, cx);
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&["v root", " v a", " > target_destination/b/c/d"],
|
||||
"Moving middle directory 'b' should leave only 'a' and move 'b/c/d'"
|
||||
);
|
||||
|
||||
// Reset
|
||||
select_path(&panel, "root/target_destination/b", cx);
|
||||
panel.update_in(cx, |panel, window, cx| {
|
||||
let drag = DraggedSelection {
|
||||
active_selection: SelectedEntry {
|
||||
worktree_id: panel.selection.as_ref().unwrap().worktree_id,
|
||||
entry_id: panel.resolve_entry(panel.selection.as_ref().unwrap().entry_id),
|
||||
},
|
||||
marked_selections: Arc::new([*panel.selection.as_ref().unwrap()]),
|
||||
};
|
||||
let target_entry = panel
|
||||
.project
|
||||
.read(cx)
|
||||
.visible_worktrees(cx)
|
||||
.next()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.entry_for_path("a")
|
||||
.unwrap();
|
||||
panel.drag_onto(&drag, target_entry.id, false, window, cx);
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
// Case 3: Move first dir 'a' - should move whole 'a/b/c/d'
|
||||
select_path(&panel, "root/a", cx);
|
||||
panel.update_in(cx, |panel, window, cx| {
|
||||
let drag = DraggedSelection {
|
||||
active_selection: SelectedEntry {
|
||||
worktree_id: panel.selection.as_ref().unwrap().worktree_id,
|
||||
entry_id: panel.resolve_entry(panel.selection.as_ref().unwrap().entry_id),
|
||||
},
|
||||
marked_selections: Arc::new([*panel.selection.as_ref().unwrap()]),
|
||||
};
|
||||
let target_entry = panel
|
||||
.project
|
||||
.read(cx)
|
||||
.visible_worktrees(cx)
|
||||
.next()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.entry_for_path("target_destination")
|
||||
.unwrap();
|
||||
panel.drag_onto(&drag, target_entry.id, false, window, cx);
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&["v root", " > target_destination/a/b/c/d"],
|
||||
"Moving first directory 'a' should move whole 'a/b/c/d' chain"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_autoreveal_and_gitignored_files(cx: &mut gpui::TestAppContext) {
|
||||
init_test_with_editor(cx);
|
||||
|
||||
Reference in New Issue
Block a user