mirror of
https://github.com/zed-industries/zed.git
synced 2026-10-20 03:24:42 +00:00
project_panel: Allow dropping external files on folded directory components (#65259)
## Summary Closes #65091 In the project panel, a folded directory path like `a/b/c` is rendered as separate components. Internal drags (`DraggedSelection`) could already be dropped on any component or separator, but external files dragged in from the OS could only be dropped on the deepest directory. This is the case from the issue: files dragged onto `upper_folder` in a folded `upper_folder/lower_folder` always ended up in `lower_folder`. This adds `ExternalPaths` drag-move and drop handlers to each folded path component and to the separators between them, so external files land in the directory that was actually hovered. The hover highlight follows the same path as for internal drags. The duplicated drag-target bookkeeping in the `DraggedSelection` handlers is moved into `update_folded_directory_drag_target`, so both drag types share it. Before: <img width="1100" height="688" alt="before" src="https://github.com/user-attachments/assets/94af5bb9-f727-4bd0-80ed-5713790b7f65" /> After: <img width="1100" height="688" alt="after" src="https://github.com/user-attachments/assets/dee1f581-ae7d-48b0-9d08-19e671fc63e5" /> ## Testing Added `test_drop_external_files_on_folded_directory_components`, which drops an external file on each of `a`, `b` and `c` in a folded `a/b/c` and checks that it lands in that directory and that the drag state is cleared afterwards. The test fails without the new drop handlers (the drop on `b` ends up in the wrong directory). Manually tested on macOS 27. The issue was reported on Windows, which I have not tested. 1. Enable `auto_fold_dirs` and open a project containing a nested chain of single-child directories, e.g. `a/b/c`. 2. Drag a file from the OS file manager onto `a`, `b`, `c` and onto a `/` separator. 3. The file is created in the hovered directory, and only that component is highlighted while dragging. ## Self-review - [x] I've reviewed my diff for quality, security, reliability, and performance. - [x] UI changes follow the [checklist](https://zed.dev/docs/development/ui-checklist). - [x] Tests cover the new or changed behavior. Release Notes: - Fixed dropping external files onto a specific directory in a folded path in the project panel --------- Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
This commit is contained in:
parent
dc000222a9
commit
eb466341a6
2 changed files with 148 additions and 38 deletions
|
|
@ -222,7 +222,7 @@ enum DragTarget {
|
|||
Background,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
struct FoldedDirectoryDragTarget {
|
||||
entry_id: ProjectEntryId,
|
||||
index: usize,
|
||||
|
|
@ -5046,6 +5046,18 @@ impl ProjectPanel {
|
|||
}
|
||||
}
|
||||
|
||||
fn update_folded_directory_drag_target(
|
||||
&mut self,
|
||||
is_hovered: bool,
|
||||
target: FoldedDirectoryDragTarget,
|
||||
) {
|
||||
if is_hovered {
|
||||
self.folded_directory_drag_target = Some(target);
|
||||
} else if self.folded_directory_drag_target == Some(target) {
|
||||
self.folded_directory_drag_target = None;
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_drag_state(&mut self, cx: &mut Context<Self>) {
|
||||
let had_drag_state = self.drag_target_entry.take().is_some()
|
||||
| self.folded_directory_drag_target.take().is_some()
|
||||
|
|
@ -6749,32 +6761,21 @@ impl ProjectPanel {
|
|||
.ancestors
|
||||
.get(components_len - 1 - index)
|
||||
.cloned();
|
||||
let drag_target = FoldedDirectoryDragTarget {
|
||||
entry_id,
|
||||
index,
|
||||
is_delimiter_target: false,
|
||||
};
|
||||
div.when(drag_and_drop_enabled, |div| {
|
||||
div.on_drag_move(cx.listener(
|
||||
move |this,
|
||||
event: &DragMoveEvent<DraggedSelection>,
|
||||
_,
|
||||
_| {
|
||||
if event.bounds.contains(&event.event.position) {
|
||||
this.folded_directory_drag_target =
|
||||
Some(FoldedDirectoryDragTarget {
|
||||
entry_id,
|
||||
index,
|
||||
is_delimiter_target: false,
|
||||
});
|
||||
} else {
|
||||
let is_current_target = this
|
||||
.folded_directory_drag_target
|
||||
.as_ref()
|
||||
.is_some_and(|target| {
|
||||
target.entry_id == entry_id
|
||||
&& target.index == index
|
||||
&& !target.is_delimiter_target
|
||||
});
|
||||
if is_current_target {
|
||||
this.folded_directory_drag_target = None;
|
||||
}
|
||||
}
|
||||
this.update_folded_directory_drag_target(
|
||||
event.bounds.contains(&event.event.position),
|
||||
drag_target,
|
||||
);
|
||||
},
|
||||
))
|
||||
.on_drop(cx.listener(
|
||||
|
|
@ -6791,6 +6792,27 @@ impl ProjectPanel {
|
|||
}
|
||||
},
|
||||
))
|
||||
.on_drag_move(cx.listener(
|
||||
move |this, event: &DragMoveEvent<ExternalPaths>, _, _| {
|
||||
this.update_folded_directory_drag_target(
|
||||
event.bounds.contains(&event.event.position),
|
||||
drag_target,
|
||||
);
|
||||
},
|
||||
))
|
||||
.on_drop(cx.listener(
|
||||
move |this, external_paths: &ExternalPaths, window, cx| {
|
||||
this.clear_drag_state(cx);
|
||||
if let Some(target_entry_id) = target_entry_id {
|
||||
this.drop_external_files(
|
||||
external_paths.paths(),
|
||||
target_entry_id,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
},
|
||||
))
|
||||
.when(
|
||||
folded_directory_drag_target.is_some_and(|target| {
|
||||
target.entry_id == entry_id && target.index == index
|
||||
|
|
@ -6820,6 +6842,7 @@ impl ProjectPanel {
|
|||
}
|
||||
}),
|
||||
)
|
||||
.debug_selector(|| format!("project_panel_path_component_{component}"))
|
||||
.child(
|
||||
Label::new(component)
|
||||
.single_line()
|
||||
|
|
@ -6871,6 +6894,11 @@ impl ProjectPanel {
|
|||
.ancestors
|
||||
.get(components_len - 1 - delimiter_target_index)
|
||||
.cloned();
|
||||
let drag_target = FoldedDirectoryDragTarget {
|
||||
entry_id,
|
||||
index: delimiter_target_index,
|
||||
is_delimiter_target: true,
|
||||
};
|
||||
div()
|
||||
.when(!is_sticky, |div| {
|
||||
div.when(drag_and_drop_enabled, |div| {
|
||||
|
|
@ -6884,26 +6912,33 @@ impl ProjectPanel {
|
|||
))
|
||||
.on_drag_move(cx.listener(
|
||||
move |this, event: &DragMoveEvent<DraggedSelection>, _, _| {
|
||||
if event.bounds.contains(&event.event.position) {
|
||||
this.folded_directory_drag_target =
|
||||
Some(FoldedDirectoryDragTarget {
|
||||
entry_id,
|
||||
index: delimiter_target_index,
|
||||
is_delimiter_target: true,
|
||||
});
|
||||
} else {
|
||||
let is_current_target =
|
||||
this.folded_directory_drag_target.is_some_and(|target| {
|
||||
target.entry_id == entry_id
|
||||
&& target.index == delimiter_target_index
|
||||
&& target.is_delimiter_target
|
||||
});
|
||||
if is_current_target {
|
||||
this.folded_directory_drag_target = None;
|
||||
}
|
||||
this.update_folded_directory_drag_target(
|
||||
event.bounds.contains(&event.event.position),
|
||||
drag_target,
|
||||
);
|
||||
},
|
||||
))
|
||||
.on_drop(cx.listener(
|
||||
move |this, external_paths: &ExternalPaths, window, cx| {
|
||||
this.clear_drag_state(cx);
|
||||
if let Some(target_entry_id) = target_entry_id {
|
||||
this.drop_external_files(
|
||||
external_paths.paths(),
|
||||
target_entry_id,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
},
|
||||
))
|
||||
.on_drag_move(cx.listener(
|
||||
move |this, event: &DragMoveEvent<ExternalPaths>, _, _| {
|
||||
this.update_folded_directory_drag_target(
|
||||
event.bounds.contains(&event.event.position),
|
||||
drag_target,
|
||||
);
|
||||
},
|
||||
))
|
||||
})
|
||||
})
|
||||
.child(
|
||||
|
|
|
|||
|
|
@ -13086,6 +13086,81 @@ async fn test_file_drag_state_clears_on_render_after_drag_stops(cx: &mut TestApp
|
|||
.expect("window is open");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_drop_external_files_on_folded_directory_components(cx: &mut TestAppContext) {
|
||||
init_test_with_editor(cx);
|
||||
cx.update(|cx| {
|
||||
let settings = *ProjectPanelSettings::get_global(cx);
|
||||
ProjectPanelSettings::override_global(
|
||||
ProjectPanelSettings {
|
||||
auto_fold_dirs: true,
|
||||
..settings
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(path!("/root"), json!({ "a": { "b": { "c": {} } } }))
|
||||
.await;
|
||||
|
||||
let temp_dir = tempfile::tempdir().expect("temporary directory is created");
|
||||
let deepest_drop_path = temp_dir.path().join("deepest.txt");
|
||||
let middle_drop_path = temp_dir.path().join("middle.txt");
|
||||
let topmost_drop_path = temp_dir.path().join("topmost.txt");
|
||||
for path in [&deepest_drop_path, &middle_drop_path, &topmost_drop_path] {
|
||||
std::fs::write(path, "").expect("external file is written");
|
||||
}
|
||||
fs.insert_tree_from_real_fs(temp_dir.path(), temp_dir.path())
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
let (window, panel) = create_drag_test_panel(&project, cx);
|
||||
let cx = &mut VisualTestContext::from_window(window, cx);
|
||||
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&["v root", " > a/b/c"]
|
||||
);
|
||||
|
||||
for (selector, drop_path) in [
|
||||
("project_panel_path_component_c", &deepest_drop_path),
|
||||
("project_panel_path_component_b", &middle_drop_path),
|
||||
("project_panel_path_component_a", &topmost_drop_path),
|
||||
] {
|
||||
let position = cx
|
||||
.debug_bounds(selector)
|
||||
.expect("folded path component is rendered")
|
||||
.center();
|
||||
cx.simulate_event(FileDropEvent::Entered {
|
||||
position,
|
||||
paths: ExternalPaths([drop_path.clone()].into_iter().collect()),
|
||||
});
|
||||
cx.simulate_event(FileDropEvent::Submit { position });
|
||||
cx.run_until_parked();
|
||||
cx.update(|window, cx| window.draw(cx).clear(cx));
|
||||
|
||||
panel.read_with(cx, |panel, _| assert_drag_state_cleared(panel));
|
||||
}
|
||||
|
||||
assert!(
|
||||
find_project_entry(&panel, "root/a/b/c/deepest.txt", cx).is_some(),
|
||||
"dropping on `c` should create the file in root/a/b/c"
|
||||
);
|
||||
assert!(
|
||||
find_project_entry(&panel, "root/a/b/middle.txt", cx).is_some(),
|
||||
"dropping on `b` should create the file in root/a/b"
|
||||
);
|
||||
assert!(
|
||||
find_project_entry(&panel, "root/a/topmost.txt", cx).is_some(),
|
||||
"dropping on `a` should create the file in root/a"
|
||||
);
|
||||
assert!(
|
||||
find_project_entry(&panel, "root/a/b/c/topmost.txt", cx).is_none(),
|
||||
"dropping on `a` should not fall through to the deepest directory"
|
||||
);
|
||||
}
|
||||
|
||||
fn create_drag_test_panel(
|
||||
project: &Entity<Project>,
|
||||
cx: &mut TestAppContext,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue