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

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.
This commit is contained in:
Smit Barmase
2025-09-11 23:25:10 +05:30
committed by GitHub
parent da8c7a1256
commit 116c6549f6
2 changed files with 609 additions and 183 deletions
+374 -183
View File
@@ -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;
};
})
.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);
},
);
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);
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.,
),
);
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
},
);
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,
)
.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;
}
}
},
))
.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,
),
)),
.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);
}
})
.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);
} else {
if this.drag_target_entry.as_ref().is_some_and(|e| {
matches!(e, DragTarget::Background)
}) {
this.drag_target_entry = None;
}
}
},
);
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()
}),
)
} 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()),
))
.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);