project_panel: Make rest of the project panel drag and drop target (#38008)

Smit Barmase created

Closes #25854

You can now drag-and-drop on the remaining space in the project panel to
drop entries/external paths in the last worktree.


https://github.com/user-attachments/assets/a7e14518-6065-4b0f-ba2c-823c70f154f4

Release Notes:

- Added support for drag-and-drop files and external paths into the
empty space of the project panel, placing them in the last folder you
have added to the project.

Change summary

crates/project_panel/src/project_panel.rs       | 559 ++++++++++++------
crates/project_panel/src/project_panel_tests.rs | 235 +++++++
2 files changed, 610 insertions(+), 184 deletions(-)

Detailed changes

crates/project_panel/src/project_panel.rs 🔗

@@ -95,7 +95,7 @@ pub struct ProjectPanel {
     ancestors: HashMap<ProjectEntryId, FoldedAncestors>,
     folded_directory_drag_target: Option<FoldedDirectoryDragTarget>,
     last_worktree_root_id: Option<ProjectEntryId>,
-    drag_target_entry: Option<DragTargetEntry>,
+    drag_target_entry: Option<DragTarget>,
     expanded_dir_ids: HashMap<WorktreeId, Vec<ProjectEntryId>>,
     unfolded_dir_ids: HashSet<ProjectEntryId>,
     // Currently selected leaf entry (see auto-folding for a definition of that) in a file tree
@@ -125,11 +125,16 @@ pub struct ProjectPanel {
     last_reported_update: Instant,
 }
 
-struct DragTargetEntry {
-    /// The entry currently under the mouse cursor during a drag operation
-    entry_id: ProjectEntryId,
-    /// Highlight this entry along with all of its children
-    highlight_entry_id: Option<ProjectEntryId>,
+enum DragTarget {
+    /// Dragging on an entry
+    Entry {
+        /// The entry currently under the mouse cursor during a drag operation
+        entry_id: ProjectEntryId,
+        /// Highlight this entry along with all of its children
+        highlight_entry_id: ProjectEntryId,
+    },
+    /// Dragging on background
+    Background,
 }
 
 #[derive(Copy, Clone, Debug)]
@@ -3966,7 +3971,9 @@ impl ProjectPanel {
 
         // In case of single item drag, we do not highlight existing
         // directory which item belongs too
-        if drag_state.items().count() == 1 {
+        if drag_state.items().count() == 1
+            && drag_state.active_selection.worktree_id == target_worktree.id()
+        {
             let active_entry_path = self
                 .project
                 .read(cx)
@@ -3995,6 +4002,44 @@ impl ProjectPanel {
         }
     }
 
+    fn should_highlight_background_for_selection_drag(
+        &self,
+        drag_state: &DraggedSelection,
+        last_root_id: ProjectEntryId,
+        cx: &App,
+    ) -> bool {
+        // Always highlight for multiple entries
+        if drag_state.items().count() > 1 {
+            return true;
+        }
+
+        // Since root will always have empty relative path
+        if let Some(entry_path) = self
+            .project
+            .read(cx)
+            .path_for_entry(drag_state.active_selection.entry_id, cx)
+        {
+            if let Some(parent_path) = entry_path.path.parent() {
+                if !parent_path.as_os_str().is_empty() {
+                    return true;
+                }
+            }
+        }
+
+        // If parent is empty, check if different worktree
+        if let Some(last_root_worktree_id) = self
+            .project
+            .read(cx)
+            .worktree_id_for_entry(last_root_id, cx)
+        {
+            if drag_state.active_selection.worktree_id != last_root_worktree_id {
+                return true;
+            }
+        }
+
+        false
+    }
+
     fn render_entry(
         &self,
         entry_id: ProjectEntryId,
@@ -4097,10 +4142,15 @@ impl ProjectPanel {
 
         let folded_directory_drag_target = self.folded_directory_drag_target;
         let is_highlighted = {
-            if let Some(highlight_entry_id) = self
-                .drag_target_entry
-                .as_ref()
-                .and_then(|drag_target| drag_target.highlight_entry_id)
+            if let Some(highlight_entry_id) =
+                self.drag_target_entry
+                    .as_ref()
+                    .and_then(|drag_target| match drag_target {
+                        DragTarget::Entry {
+                            highlight_entry_id, ..
+                        } => Some(*highlight_entry_id),
+                        DragTarget::Background => self.last_worktree_root_id,
+                    })
             {
                 // Highlight if same entry or it's children
                 if entry_id == highlight_entry_id {
@@ -4145,7 +4195,10 @@ impl ProjectPanel {
                 .on_drag_move::<ExternalPaths>(cx.listener(
                     move |this, event: &DragMoveEvent<ExternalPaths>, _, cx| {
                         let is_current_target = this.drag_target_entry.as_ref()
-                             .map(|entry| entry.entry_id) == Some(entry_id);
+                             .and_then(|entry| match entry {
+                                 DragTarget::Entry { entry_id: target_id, .. } => Some(*target_id),
+                                 DragTarget::Background { .. } => None,
+                             }) == Some(entry_id);
 
                         if !event.bounds.contains(&event.event.position) {
                             // Entry responsible for setting drag target is also responsible to
@@ -4160,20 +4213,22 @@ impl ProjectPanel {
                             return;
                         }
 
+                        this.marked_entries.clear();
+
                         let Some((entry_id, highlight_entry_id)) = maybe!({
                             let target_worktree = this.project.read(cx).worktree_for_id(selection.worktree_id, cx)?.read(cx);
                             let target_entry = target_worktree.entry_for_path(&path_for_external_paths)?;
-                            let highlight_entry_id = this.highlight_entry_for_external_drag(target_entry, target_worktree);
+                            let highlight_entry_id = this.highlight_entry_for_external_drag(target_entry, target_worktree)?;
                             Some((target_entry.id, highlight_entry_id))
                         }) else {
                             return;
                         };
 
-                        this.drag_target_entry = Some(DragTargetEntry {
+                        this.drag_target_entry = Some(DragTarget::Entry {
                             entry_id,
                             highlight_entry_id,
                         });
-                        this.marked_entries.clear();
+
                     },
                 ))
                 .on_drop(cx.listener(
@@ -4187,7 +4242,10 @@ impl ProjectPanel {
                 .on_drag_move::<DraggedSelection>(cx.listener(
                     move |this, event: &DragMoveEvent<DraggedSelection>, window, cx| {
                         let is_current_target = this.drag_target_entry.as_ref()
-                             .map(|entry| entry.entry_id) == Some(entry_id);
+                             .and_then(|entry| match entry {
+                                 DragTarget::Entry { entry_id: target_id, .. } => Some(*target_id),
+                                 DragTarget::Background { .. } => None,
+                             }) == Some(entry_id);
 
                         if !event.bounds.contains(&event.event.position) {
                             // Entry responsible for setting drag target is also responsible to
@@ -4203,23 +4261,26 @@ impl ProjectPanel {
                         }
 
                         let drag_state = event.drag(cx);
+
+                        if drag_state.items().count() == 1 {
+                            this.marked_entries.clear();
+                            this.marked_entries.push(drag_state.active_selection);
+                        }
+
                         let Some((entry_id, highlight_entry_id)) = maybe!({
                             let target_worktree = this.project.read(cx).worktree_for_id(selection.worktree_id, cx)?.read(cx);
                             let target_entry = target_worktree.entry_for_path(&path_for_dragged_selection)?;
-                            let highlight_entry_id = this.highlight_entry_for_selection_drag(target_entry, target_worktree, drag_state, cx);
+                            let highlight_entry_id = this.highlight_entry_for_selection_drag(target_entry, target_worktree, drag_state, cx)?;
                             Some((target_entry.id, highlight_entry_id))
                         }) else {
                             return;
                         };
 
-                        this.drag_target_entry = Some(DragTargetEntry {
+                        this.drag_target_entry = Some(DragTarget::Entry {
                             entry_id,
                             highlight_entry_id,
                         });
-                        if drag_state.items().count() == 1 {
-                            this.marked_entries.clear();
-                            this.marked_entries.push(drag_state.active_selection);
-                        }
+
                         this.hover_expand_task.take();
 
                         if !kind.is_dir()
@@ -4239,7 +4300,10 @@ impl ProjectPanel {
                                     .await;
                                 this.update_in(cx, |this, window, cx| {
                                     this.hover_expand_task.take();
-                                    if this.drag_target_entry.as_ref().map(|entry| entry.entry_id) == Some(entry_id)
+                                    if this.drag_target_entry.as_ref().and_then(|entry| match entry {
+                                        DragTarget::Entry { entry_id: target_id, .. } => Some(*target_id),
+                                        DragTarget::Background { .. } => None,
+                                    }) == Some(entry_id)
                                         && bounds.contains(&window.mouse_position())
                                     {
                                         this.expand_entry(worktree_id, entry_id, cx);
@@ -4270,7 +4334,7 @@ impl ProjectPanel {
                         this.drag_target_entry = None;
                         this.hover_scroll_task.take();
                         this.hover_expand_task.take();
-                        if  folded_directory_drag_target.is_some() {
+                        if folded_directory_drag_target.is_some() {
                             return;
                         }
                         this.drag_onto(selections, entry_id, kind.is_file(), window, cx);
@@ -5370,178 +5434,305 @@ impl Render for ProjectPanel {
                 )
                 .track_focus(&self.focus_handle(cx))
                 .child(
-                    uniform_list("entries", item_count, {
-                        cx.processor(|this, range: Range<usize>, window, cx| {
-                            let mut items = Vec::with_capacity(range.end - range.start);
-                            this.for_each_visible_entry(
-                                range,
-                                window,
-                                cx,
-                                |id, details, window, cx| {
-                                    items.push(this.render_entry(id, details, window, cx));
-                                },
-                            );
-                            items
-                        })
-                    })
-                    .when(show_indent_guides, |list| {
-                        list.with_decoration(
-                            ui::indent_guides(px(indent_size), IndentGuideColors::panel(cx))
-                                .with_compute_indents_fn(cx.entity(), |this, range, window, cx| {
-                                    let mut items =
-                                        SmallVec::with_capacity(range.end - range.start);
-                                    this.iter_visible_entries(
+                    v_flex()
+                        .child(
+                            uniform_list("entries", item_count, {
+                                cx.processor(|this, range: Range<usize>, window, cx| {
+                                    let mut items = Vec::with_capacity(range.end - range.start);
+                                    this.for_each_visible_entry(
                                         range,
                                         window,
                                         cx,
-                                        |entry, _, entries, _, _| {
-                                            let (depth, _) = Self::calculate_depth_and_difference(
-                                                entry, entries,
-                                            );
-                                            items.push(depth);
+                                        |id, details, window, cx| {
+                                            items.push(this.render_entry(id, details, window, cx));
                                         },
                                     );
                                     items
                                 })
-                                .on_click(cx.listener(
-                                    |this, active_indent_guide: &IndentGuideLayout, window, cx| {
-                                        if window.modifiers().secondary() {
-                                            let ix = active_indent_guide.offset.y;
-                                            let Some((target_entry, worktree)) = maybe!({
-                                                let (worktree_id, entry) =
-                                                    this.entry_at_index(ix)?;
-                                                let worktree = this
-                                                    .project
-                                                    .read(cx)
-                                                    .worktree_for_id(worktree_id, cx)?;
-                                                let target_entry = worktree
-                                                    .read(cx)
-                                                    .entry_for_path(&entry.path.parent()?)?;
-                                                Some((target_entry, worktree))
-                                            }) else {
-                                                return;
-                                            };
-
-                                            this.collapse_entry(target_entry.clone(), worktree, cx);
-                                        }
-                                    },
-                                ))
-                                .with_render_fn(cx.entity(), move |this, params, _, cx| {
-                                    const LEFT_OFFSET: Pixels = px(14.);
-                                    const PADDING_Y: Pixels = px(4.);
-                                    const HITBOX_OVERDRAW: Pixels = px(3.);
-
-                                    let active_indent_guide_index =
-                                        this.find_active_indent_guide(&params.indent_guides, cx);
-
-                                    let indent_size = params.indent_size;
-                                    let item_height = params.item_height;
-
-                                    params
-                                        .indent_guides
-                                        .into_iter()
-                                        .enumerate()
-                                        .map(|(idx, layout)| {
-                                            let offset = if layout.continues_offscreen {
-                                                px(0.)
-                                            } else {
-                                                PADDING_Y
-                                            };
-                                            let bounds = Bounds::new(
-                                                point(
-                                                    layout.offset.x * indent_size + LEFT_OFFSET,
-                                                    layout.offset.y * item_height + offset,
-                                                ),
-                                                size(
-                                                    px(1.),
-                                                    layout.length * item_height - offset * 2.,
-                                                ),
+                            })
+                            .when(show_indent_guides, |list| {
+                                list.with_decoration(
+                                    ui::indent_guides(
+                                        px(indent_size),
+                                        IndentGuideColors::panel(cx),
+                                    )
+                                    .with_compute_indents_fn(
+                                        cx.entity(),
+                                        |this, range, window, cx| {
+                                            let mut items =
+                                                SmallVec::with_capacity(range.end - range.start);
+                                            this.iter_visible_entries(
+                                                range,
+                                                window,
+                                                cx,
+                                                |entry, _, entries, _, _| {
+                                                    let (depth, _) =
+                                                        Self::calculate_depth_and_difference(
+                                                            entry, entries,
+                                                        );
+                                                    items.push(depth);
+                                                },
                                             );
-                                            ui::RenderedIndentGuide {
-                                                bounds,
-                                                layout,
-                                                is_active: Some(idx) == active_indent_guide_index,
-                                                hitbox: Some(Bounds::new(
-                                                    point(
-                                                        bounds.origin.x - HITBOX_OVERDRAW,
-                                                        bounds.origin.y,
-                                                    ),
-                                                    size(
-                                                        bounds.size.width + HITBOX_OVERDRAW * 2.,
-                                                        bounds.size.height,
-                                                    ),
-                                                )),
+                                            items
+                                        },
+                                    )
+                                    .on_click(cx.listener(
+                                        |this,
+                                         active_indent_guide: &IndentGuideLayout,
+                                         window,
+                                         cx| {
+                                            if window.modifiers().secondary() {
+                                                let ix = active_indent_guide.offset.y;
+                                                let Some((target_entry, worktree)) = maybe!({
+                                                    let (worktree_id, entry) =
+                                                        this.entry_at_index(ix)?;
+                                                    let worktree = this
+                                                        .project
+                                                        .read(cx)
+                                                        .worktree_for_id(worktree_id, cx)?;
+                                                    let target_entry = worktree
+                                                        .read(cx)
+                                                        .entry_for_path(&entry.path.parent()?)?;
+                                                    Some((target_entry, worktree))
+                                                }) else {
+                                                    return;
+                                                };
+
+                                                this.collapse_entry(
+                                                    target_entry.clone(),
+                                                    worktree,
+                                                    cx,
+                                                );
                                             }
-                                        })
-                                        .collect()
-                                }),
-                        )
-                    })
-                    .when(show_sticky_entries, |list| {
-                        let sticky_items = ui::sticky_items(
-                            cx.entity(),
-                            |this, range, window, cx| {
-                                let mut items = SmallVec::with_capacity(range.end - range.start);
-                                this.iter_visible_entries(
-                                    range,
-                                    window,
-                                    cx,
-                                    |entry, index, entries, _, _| {
-                                        let (depth, _) =
-                                            Self::calculate_depth_and_difference(entry, entries);
-                                        let candidate =
-                                            StickyProjectPanelCandidate { index, depth };
-                                        items.push(candidate);
+                                        },
+                                    ))
+                                    .with_render_fn(
+                                        cx.entity(),
+                                        move |this, params, _, cx| {
+                                            const LEFT_OFFSET: Pixels = px(14.);
+                                            const PADDING_Y: Pixels = px(4.);
+                                            const HITBOX_OVERDRAW: Pixels = px(3.);
+
+                                            let active_indent_guide_index = this
+                                                .find_active_indent_guide(
+                                                    &params.indent_guides,
+                                                    cx,
+                                                );
+
+                                            let indent_size = params.indent_size;
+                                            let item_height = params.item_height;
+
+                                            params
+                                                .indent_guides
+                                                .into_iter()
+                                                .enumerate()
+                                                .map(|(idx, layout)| {
+                                                    let offset = if layout.continues_offscreen {
+                                                        px(0.)
+                                                    } else {
+                                                        PADDING_Y
+                                                    };
+                                                    let bounds = Bounds::new(
+                                                        point(
+                                                            layout.offset.x * indent_size
+                                                                + LEFT_OFFSET,
+                                                            layout.offset.y * item_height + offset,
+                                                        ),
+                                                        size(
+                                                            px(1.),
+                                                            layout.length * item_height
+                                                                - offset * 2.,
+                                                        ),
+                                                    );
+                                                    ui::RenderedIndentGuide {
+                                                        bounds,
+                                                        layout,
+                                                        is_active: Some(idx)
+                                                            == active_indent_guide_index,
+                                                        hitbox: Some(Bounds::new(
+                                                            point(
+                                                                bounds.origin.x - HITBOX_OVERDRAW,
+                                                                bounds.origin.y,
+                                                            ),
+                                                            size(
+                                                                bounds.size.width
+                                                                    + HITBOX_OVERDRAW * 2.,
+                                                                bounds.size.height,
+                                                            ),
+                                                        )),
+                                                    }
+                                                })
+                                                .collect()
+                                        },
+                                    ),
+                                )
+                            })
+                            .when(show_sticky_entries, |list| {
+                                let sticky_items = ui::sticky_items(
+                                    cx.entity(),
+                                    |this, range, window, cx| {
+                                        let mut items =
+                                            SmallVec::with_capacity(range.end - range.start);
+                                        this.iter_visible_entries(
+                                            range,
+                                            window,
+                                            cx,
+                                            |entry, index, entries, _, _| {
+                                                let (depth, _) =
+                                                    Self::calculate_depth_and_difference(
+                                                        entry, entries,
+                                                    );
+                                                let candidate =
+                                                    StickyProjectPanelCandidate { index, depth };
+                                                items.push(candidate);
+                                            },
+                                        );
+                                        items
+                                    },
+                                    |this, marker_entry, window, cx| {
+                                        let sticky_entries =
+                                            this.render_sticky_entries(marker_entry, window, cx);
+                                        this.sticky_items_count = sticky_entries.len();
+                                        sticky_entries
                                     },
                                 );
-                                items
-                            },
-                            |this, marker_entry, window, cx| {
-                                let sticky_entries =
-                                    this.render_sticky_entries(marker_entry, window, cx);
-                                this.sticky_items_count = sticky_entries.len();
-                                sticky_entries
-                            },
-                        );
-                        list.with_decoration(if show_indent_guides {
-                            sticky_items.with_decoration(
-                                ui::indent_guides(px(indent_size), IndentGuideColors::panel(cx))
-                                    .with_render_fn(cx.entity(), move |_, params, _, _| {
-                                        const LEFT_OFFSET: Pixels = px(14.);
-
-                                        let indent_size = params.indent_size;
-                                        let item_height = params.item_height;
-
-                                        params
-                                            .indent_guides
-                                            .into_iter()
-                                            .map(|layout| {
-                                                let bounds = Bounds::new(
-                                                    point(
-                                                        layout.offset.x * indent_size + LEFT_OFFSET,
-                                                        layout.offset.y * item_height,
-                                                    ),
-                                                    size(px(1.), layout.length * item_height),
-                                                );
-                                                ui::RenderedIndentGuide {
-                                                    bounds,
-                                                    layout,
-                                                    is_active: false,
-                                                    hitbox: None,
-                                                }
-                                            })
-                                            .collect()
-                                    }),
+                                list.with_decoration(if show_indent_guides {
+                                    sticky_items.with_decoration(
+                                        ui::indent_guides(
+                                            px(indent_size),
+                                            IndentGuideColors::panel(cx),
+                                        )
+                                        .with_render_fn(
+                                            cx.entity(),
+                                            move |_, params, _, _| {
+                                                const LEFT_OFFSET: Pixels = px(14.);
+
+                                                let indent_size = params.indent_size;
+                                                let item_height = params.item_height;
+
+                                                params
+                                                    .indent_guides
+                                                    .into_iter()
+                                                    .map(|layout| {
+                                                        let bounds = Bounds::new(
+                                                            point(
+                                                                layout.offset.x * indent_size
+                                                                    + LEFT_OFFSET,
+                                                                layout.offset.y * item_height,
+                                                            ),
+                                                            size(
+                                                                px(1.),
+                                                                layout.length * item_height,
+                                                            ),
+                                                        );
+                                                        ui::RenderedIndentGuide {
+                                                            bounds,
+                                                            layout,
+                                                            is_active: false,
+                                                            hitbox: None,
+                                                        }
+                                                    })
+                                                    .collect()
+                                            },
+                                        ),
+                                    )
+                                } else {
+                                    sticky_items
+                                })
+                            })
+                            .with_sizing_behavior(ListSizingBehavior::Infer)
+                            .with_horizontal_sizing_behavior(
+                                ListHorizontalSizingBehavior::Unconstrained,
                             )
-                        } else {
-                            sticky_items
-                        })
-                    })
-                    .size_full()
-                    .with_sizing_behavior(ListSizingBehavior::Infer)
-                    .with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained)
-                    .with_width_from_item(self.max_width_item_index)
-                    .track_scroll(self.scroll_handle.clone()),
+                            .with_width_from_item(self.max_width_item_index)
+                            .track_scroll(self.scroll_handle.clone()),
+                        )
+                        .child(
+                            div()
+                                .block_mouse_except_scroll()
+                                .flex_grow()
+                                .when(
+                                    self.drag_target_entry.as_ref().is_some_and(
+                                        |entry| match entry {
+                                            DragTarget::Background => true,
+                                            DragTarget::Entry {
+                                                highlight_entry_id, ..
+                                            } => {
+                                                self.last_worktree_root_id.is_some_and(|root_id| {
+                                                    *highlight_entry_id == root_id
+                                                })
+                                            }
+                                        },
+                                    ),
+                                    |div| div.bg(cx.theme().colors().drop_target_background),
+                                )
+                                .on_drag_move::<ExternalPaths>(cx.listener(
+                                    move |this, event: &DragMoveEvent<ExternalPaths>, _, _| {
+                                        let Some(_last_root_id) = this.last_worktree_root_id else {
+                                            return;
+                                        };
+                                        if event.bounds.contains(&event.event.position) {
+                                            this.drag_target_entry = Some(DragTarget::Background);
+                                        } else {
+                                            if this.drag_target_entry.as_ref().is_some_and(|e| {
+                                                matches!(e, DragTarget::Background)
+                                            }) {
+                                                this.drag_target_entry = None;
+                                            }
+                                        }
+                                    },
+                                ))
+                                .on_drag_move::<DraggedSelection>(cx.listener(
+                                    move |this, event: &DragMoveEvent<DraggedSelection>, _, cx| {
+                                        let Some(last_root_id) = this.last_worktree_root_id else {
+                                            return;
+                                        };
+                                        if event.bounds.contains(&event.event.position) {
+                                            let drag_state = event.drag(cx);
+                                            if this.should_highlight_background_for_selection_drag(
+                                                &drag_state,
+                                                last_root_id,
+                                                cx,
+                                            ) {
+                                                this.drag_target_entry =
+                                                    Some(DragTarget::Background);
+                                            }
+                                        } else {
+                                            if this.drag_target_entry.as_ref().is_some_and(|e| {
+                                                matches!(e, DragTarget::Background)
+                                            }) {
+                                                this.drag_target_entry = None;
+                                            }
+                                        }
+                                    },
+                                ))
+                                .on_drop(cx.listener(
+                                    move |this, external_paths: &ExternalPaths, window, cx| {
+                                        this.drag_target_entry = None;
+                                        this.hover_scroll_task.take();
+                                        if let Some(entry_id) = this.last_worktree_root_id {
+                                            this.drop_external_files(
+                                                external_paths.paths(),
+                                                entry_id,
+                                                window,
+                                                cx,
+                                            );
+                                        }
+                                        cx.stop_propagation();
+                                    },
+                                ))
+                                .on_drop(cx.listener(
+                                    move |this, selections: &DraggedSelection, window, cx| {
+                                        this.drag_target_entry = None;
+                                        this.hover_scroll_task.take();
+                                        if let Some(entry_id) = this.last_worktree_root_id {
+                                            this.drag_onto(selections, entry_id, false, window, cx);
+                                        }
+                                        cx.stop_propagation();
+                                    },
+                                )),
+                        )
+                        .size_full(),
                 )
                 .children(self.render_vertical_scrollbar(cx))
                 .when_some(self.render_horizontal_scrollbar(cx), |this, scrollbar| {

crates/project_panel/src/project_panel_tests.rs 🔗

@@ -5643,6 +5643,241 @@ async fn test_highlight_entry_for_selection_drag(cx: &mut gpui::TestAppContext)
     });
 }
 
+#[gpui::test]
+async fn test_highlight_entry_for_selection_drag_cross_worktree(cx: &mut gpui::TestAppContext) {
+    init_test(cx);
+
+    let fs = FakeFs::new(cx.executor());
+    fs.insert_tree(
+        "/root1",
+        json!({
+            "src": {
+                "main.rs": "",
+                "lib.rs": ""
+            }
+        }),
+    )
+    .await;
+    fs.insert_tree(
+        "/root2",
+        json!({
+            "src": {
+                "main.rs": "",
+                "test.rs": ""
+            }
+        }),
+    )
+    .await;
+
+    let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".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();
+
+    panel.update(cx, |panel, cx| {
+        let project = panel.project.read(cx);
+        let worktrees: Vec<_> = project.visible_worktrees(cx).collect();
+
+        let worktree_a = &worktrees[0];
+        let main_rs_from_a = worktree_a.read(cx).entry_for_path("src/main.rs").unwrap();
+
+        let worktree_b = &worktrees[1];
+        let src_dir_from_b = worktree_b.read(cx).entry_for_path("src").unwrap();
+        let main_rs_from_b = worktree_b.read(cx).entry_for_path("src/main.rs").unwrap();
+
+        // Test dragging file from worktree A onto parent of file with same relative path in worktree B
+        let dragged_selection = DraggedSelection {
+            active_selection: SelectedEntry {
+                worktree_id: worktree_a.read(cx).id(),
+                entry_id: main_rs_from_a.id,
+            },
+            marked_selections: Arc::new([SelectedEntry {
+                worktree_id: worktree_a.read(cx).id(),
+                entry_id: main_rs_from_a.id,
+            }]),
+        };
+
+        let result = panel.highlight_entry_for_selection_drag(
+            src_dir_from_b,
+            worktree_b.read(cx),
+            &dragged_selection,
+            cx,
+        );
+        assert_eq!(
+            result,
+            Some(src_dir_from_b.id),
+            "Should highlight target directory from different worktree even with same relative path"
+        );
+
+        // Test dragging file from worktree A onto file with same relative path in worktree B
+        let result = panel.highlight_entry_for_selection_drag(
+            main_rs_from_b,
+            worktree_b.read(cx),
+            &dragged_selection,
+            cx,
+        );
+        assert_eq!(
+            result,
+            Some(src_dir_from_b.id),
+            "Should highlight parent of target file from different worktree"
+        );
+    });
+}
+
+#[gpui::test]
+async fn test_should_highlight_background_for_selection_drag(cx: &mut gpui::TestAppContext) {
+    init_test(cx);
+
+    let fs = FakeFs::new(cx.executor());
+    fs.insert_tree(
+        "/root1",
+        json!({
+            "parent_dir": {
+                "child_file.txt": "",
+                "nested_dir": {
+                    "nested_file.txt": ""
+                }
+            },
+            "root_file.txt": ""
+        }),
+    )
+    .await;
+
+    fs.insert_tree(
+        "/root2",
+        json!({
+            "other_dir": {
+                "other_file.txt": ""
+            }
+        }),
+    )
+    .await;
+
+    let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".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();
+
+    panel.update(cx, |panel, cx| {
+        let project = panel.project.read(cx);
+        let worktrees: Vec<_> = project.visible_worktrees(cx).collect();
+        let worktree1 = worktrees[0].read(cx);
+        let worktree2 = worktrees[1].read(cx);
+        let worktree1_id = worktree1.id();
+        let _worktree2_id = worktree2.id();
+
+        let root1_entry = worktree1.root_entry().unwrap();
+        let root2_entry = worktree2.root_entry().unwrap();
+        let _parent_dir = worktree1.entry_for_path("parent_dir").unwrap();
+        let child_file = worktree1
+            .entry_for_path("parent_dir/child_file.txt")
+            .unwrap();
+        let nested_file = worktree1
+            .entry_for_path("parent_dir/nested_dir/nested_file.txt")
+            .unwrap();
+        let root_file = worktree1.entry_for_path("root_file.txt").unwrap();
+
+        // Test 1: Multiple entries - should always highlight background
+        let multiple_dragged_selection = DraggedSelection {
+            active_selection: SelectedEntry {
+                worktree_id: worktree1_id,
+                entry_id: child_file.id,
+            },
+            marked_selections: Arc::new([
+                SelectedEntry {
+                    worktree_id: worktree1_id,
+                    entry_id: child_file.id,
+                },
+                SelectedEntry {
+                    worktree_id: worktree1_id,
+                    entry_id: nested_file.id,
+                },
+            ]),
+        };
+
+        let result = panel.should_highlight_background_for_selection_drag(
+            &multiple_dragged_selection,
+            root1_entry.id,
+            cx,
+        );
+        assert!(result, "Should highlight background for multiple entries");
+
+        // Test 2: Single entry with non-empty parent path - should highlight background
+        let nested_dragged_selection = DraggedSelection {
+            active_selection: SelectedEntry {
+                worktree_id: worktree1_id,
+                entry_id: nested_file.id,
+            },
+            marked_selections: Arc::new([SelectedEntry {
+                worktree_id: worktree1_id,
+                entry_id: nested_file.id,
+            }]),
+        };
+
+        let result = panel.should_highlight_background_for_selection_drag(
+            &nested_dragged_selection,
+            root1_entry.id,
+            cx,
+        );
+        assert!(result, "Should highlight background for nested file");
+
+        // Test 3: Single entry at root level, same worktree - should NOT highlight background
+        let root_file_dragged_selection = DraggedSelection {
+            active_selection: SelectedEntry {
+                worktree_id: worktree1_id,
+                entry_id: root_file.id,
+            },
+            marked_selections: Arc::new([SelectedEntry {
+                worktree_id: worktree1_id,
+                entry_id: root_file.id,
+            }]),
+        };
+
+        let result = panel.should_highlight_background_for_selection_drag(
+            &root_file_dragged_selection,
+            root1_entry.id,
+            cx,
+        );
+        assert!(
+            !result,
+            "Should NOT highlight background for root file in same worktree"
+        );
+
+        // Test 4: Single entry at root level, different worktree - should highlight background
+        let result = panel.should_highlight_background_for_selection_drag(
+            &root_file_dragged_selection,
+            root2_entry.id,
+            cx,
+        );
+        assert!(
+            result,
+            "Should highlight background for root file from different worktree"
+        );
+
+        // Test 5: Single entry in subdirectory - should highlight background
+        let child_file_dragged_selection = DraggedSelection {
+            active_selection: SelectedEntry {
+                worktree_id: worktree1_id,
+                entry_id: child_file.id,
+            },
+            marked_selections: Arc::new([SelectedEntry {
+                worktree_id: worktree1_id,
+                entry_id: child_file.id,
+            }]),
+        };
+
+        let result = panel.should_highlight_background_for_selection_drag(
+            &child_file_dragged_selection,
+            root1_entry.id,
+            cx,
+        );
+        assert!(
+            result,
+            "Should highlight background for file with non-empty parent path"
+        );
+    });
+}
+
 #[gpui::test]
 async fn test_hide_root(cx: &mut gpui::TestAppContext) {
     init_test(cx);