diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 551cd6c5fbd..e456d7b2fea 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -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) { 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, _, _| { - 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, _, _| { + 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, _, _| { - 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, _, _| { + this.update_folded_directory_drag_target( + event.bounds.contains(&event.event.position), + drag_target, + ); + }, + )) }) }) .child( diff --git a/crates/project_panel/src/project_panel_tests.rs b/crates/project_panel/src/project_panel_tests.rs index 23e6f60b76b..e51bb45c41a 100644 --- a/crates/project_panel/src/project_panel_tests.rs +++ b/crates/project_panel/src/project_panel_tests.rs @@ -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, cx: &mut TestAppContext,