@@ -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(¶ms.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(
+ ¶ms.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| {
@@ -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);