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:
@@ -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(
|
||||
¶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
|
||||
},
|
||||
);
|
||||
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(¶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,
|
||||
),
|
||||
)),
|
||||
.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);
|
||||
|
||||
Reference in New Issue
Block a user