mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
Honor copy modifier in worktree-root drag feedback and blank-area path
- The blank-area "send group to end" fast path now only triggers for move gestures. Holding the copy modifier falls through to `drag_onto` so the copy branch's existing root-filter no-op stays in effect. - `highlight_entry_for_selection_drag` takes an `is_copy_mode` flag and suppresses every target for pure-root drags in copy mode, so users no longer see a copy cursor with a root-row highlight for an operation that will silently do nothing. - Tighten the doc comment on `move_worktrees` to describe the actual fallback (earliest source in worktree order) and trim the implementation-leaky doc on `move_worktrees_to_end`. - Add `test_highlight_entry_for_root_drag_suppressed_in_copy_mode`.
This commit is contained in:
parent
15117d76dc
commit
e495df4172
3 changed files with 99 additions and 17 deletions
|
|
@ -1045,8 +1045,9 @@ impl WorktreeStore {
|
|||
/// preserving their relative order. The `active_source` (if provided and
|
||||
/// in `sources`) decides whether the group lands before or after the
|
||||
/// destination, mirroring the single-source semantics: a source originally
|
||||
/// before destination ends up after it, and vice versa. When no active
|
||||
/// source is supplied, the first source's position is used.
|
||||
/// before destination ends up after it, and vice versa. When no usable
|
||||
/// active source is supplied, the earliest source in the current worktree
|
||||
/// order is used as the direction reference.
|
||||
pub fn move_worktrees(
|
||||
&mut self,
|
||||
sources: &[WorktreeId],
|
||||
|
|
@ -1135,9 +1136,8 @@ impl WorktreeStore {
|
|||
}
|
||||
|
||||
/// Removes every source from the worktree list and appends them to the
|
||||
/// end in their original relative order. Intended for "drop on the empty
|
||||
/// area below the panel" gestures, where the user's intent is "send this
|
||||
/// group to the end" rather than reordering relative to a specific row.
|
||||
/// end in their original relative order. Returns early when the sources
|
||||
/// already form a contiguous suffix.
|
||||
pub fn move_worktrees_to_end(
|
||||
&mut self,
|
||||
sources: &[WorktreeId],
|
||||
|
|
|
|||
|
|
@ -5273,6 +5273,7 @@ impl ProjectPanel {
|
|||
target_entry: &Entry,
|
||||
target_worktree: &Worktree,
|
||||
drag_state: &DraggedSelection,
|
||||
is_copy_mode: bool,
|
||||
cx: &Context<Self>,
|
||||
) -> Option<ProjectEntryId> {
|
||||
// Pure worktree-root drags are only meaningful when dropped on
|
||||
|
|
@ -5284,6 +5285,11 @@ impl ProjectPanel {
|
|||
.items()
|
||||
.all(|entry| project.entry_is_worktree_root(entry.entry_id, cx));
|
||||
if drag_is_root_only {
|
||||
// Worktree roots can't be copied; in copy mode the drop is a
|
||||
// guaranteed no-op, so don't highlight any target.
|
||||
if is_copy_mode {
|
||||
return None;
|
||||
}
|
||||
let root_id = target_worktree.root_entry()?.id;
|
||||
// Hovering any worktree that's part of the drag (active or just
|
||||
// marked) is a no-op in `move_worktrees`, so don't highlight it.
|
||||
|
|
@ -5629,6 +5635,7 @@ impl ProjectPanel {
|
|||
this.marked_entries.push(drag_state.active_selection);
|
||||
}
|
||||
|
||||
let is_copy_mode = Self::is_copy_modifier_set(&window.modifiers());
|
||||
let Some((entry_id, highlight_entry_id)) = maybe!({
|
||||
let target_worktree = this
|
||||
.project
|
||||
|
|
@ -5641,6 +5648,7 @@ impl ProjectPanel {
|
|||
target_entry,
|
||||
target_worktree,
|
||||
drag_state,
|
||||
is_copy_mode,
|
||||
cx,
|
||||
)?;
|
||||
Some((target_entry.id, highlight_entry_id))
|
||||
|
|
@ -7133,8 +7141,16 @@ impl Render for ProjectPanel {
|
|||
// last worktree would otherwise hit
|
||||
// the self-drop guard in
|
||||
// `move_worktrees` and become a no-op.
|
||||
// Send the group to the end instead.
|
||||
if this.drag_includes_last_worktree(selections, cx) {
|
||||
// Send the group to the end — but
|
||||
// only for move gestures; copy mode
|
||||
// can't operate on roots and should
|
||||
// continue to no-op via `drag_onto`.
|
||||
let is_copy_mode = Self::is_copy_modifier_set(
|
||||
&window.modifiers(),
|
||||
);
|
||||
if !is_copy_mode
|
||||
&& this.drag_includes_last_worktree(selections, cx)
|
||||
{
|
||||
this.reorder_worktree_roots_to_end(selections, cx);
|
||||
} else if let Some(entry_id) =
|
||||
this.state.last_worktree_root_id
|
||||
|
|
|
|||
|
|
@ -9077,7 +9077,7 @@ async fn test_highlight_entry_for_selection_drag(cx: &mut gpui::TestAppContext)
|
|||
}]),
|
||||
};
|
||||
let result =
|
||||
panel.highlight_entry_for_selection_drag(parent_dir, worktree, &dragged_selection, cx);
|
||||
panel.highlight_entry_for_selection_drag(parent_dir, worktree, &dragged_selection, false, cx);
|
||||
assert_eq!(result, None, "Should not highlight parent of dragged item");
|
||||
|
||||
// Test 2: Single item drag, don't highlight sibling files
|
||||
|
|
@ -9085,13 +9085,14 @@ async fn test_highlight_entry_for_selection_drag(cx: &mut gpui::TestAppContext)
|
|||
sibling_file,
|
||||
worktree,
|
||||
&dragged_selection,
|
||||
false,
|
||||
cx,
|
||||
);
|
||||
assert_eq!(result, None, "Should not highlight sibling files");
|
||||
|
||||
// Test 3: Single item drag, highlight unrelated directory
|
||||
let result =
|
||||
panel.highlight_entry_for_selection_drag(other_dir, worktree, &dragged_selection, cx);
|
||||
panel.highlight_entry_for_selection_drag(other_dir, worktree, &dragged_selection, false, cx);
|
||||
assert_eq!(
|
||||
result,
|
||||
Some(other_dir.id),
|
||||
|
|
@ -9100,7 +9101,7 @@ async fn test_highlight_entry_for_selection_drag(cx: &mut gpui::TestAppContext)
|
|||
|
||||
// Test 4: Single item drag, highlight sibling directory
|
||||
let result =
|
||||
panel.highlight_entry_for_selection_drag(child_dir, worktree, &dragged_selection, cx);
|
||||
panel.highlight_entry_for_selection_drag(child_dir, worktree, &dragged_selection, false, cx);
|
||||
assert_eq!(
|
||||
result,
|
||||
Some(child_dir.id),
|
||||
|
|
@ -9125,7 +9126,7 @@ async fn test_highlight_entry_for_selection_drag(cx: &mut gpui::TestAppContext)
|
|||
]),
|
||||
};
|
||||
let result =
|
||||
panel.highlight_entry_for_selection_drag(parent_dir, worktree, &dragged_selection, cx);
|
||||
panel.highlight_entry_for_selection_drag(parent_dir, worktree, &dragged_selection, false, cx);
|
||||
assert_eq!(
|
||||
result,
|
||||
Some(parent_dir.id),
|
||||
|
|
@ -9134,7 +9135,7 @@ async fn test_highlight_entry_for_selection_drag(cx: &mut gpui::TestAppContext)
|
|||
|
||||
// Test 6: Target is file in different directory, highlight parent
|
||||
let result =
|
||||
panel.highlight_entry_for_selection_drag(other_file, worktree, &dragged_selection, cx);
|
||||
panel.highlight_entry_for_selection_drag(other_file, worktree, &dragged_selection, false, cx);
|
||||
assert_eq!(
|
||||
result,
|
||||
Some(other_dir.id),
|
||||
|
|
@ -9143,7 +9144,7 @@ async fn test_highlight_entry_for_selection_drag(cx: &mut gpui::TestAppContext)
|
|||
|
||||
// Test 7: Target is directory, always highlight
|
||||
let result =
|
||||
panel.highlight_entry_for_selection_drag(child_dir, worktree, &dragged_selection, cx);
|
||||
panel.highlight_entry_for_selection_drag(child_dir, worktree, &dragged_selection, false, cx);
|
||||
assert_eq!(
|
||||
result,
|
||||
Some(child_dir.id),
|
||||
|
|
@ -9220,6 +9221,7 @@ async fn test_highlight_entry_for_selection_drag_cross_worktree(cx: &mut gpui::T
|
|||
src_dir_from_b,
|
||||
worktree_b.read(cx),
|
||||
&dragged_selection,
|
||||
false,
|
||||
cx,
|
||||
);
|
||||
assert_eq!(
|
||||
|
|
@ -9233,6 +9235,7 @@ async fn test_highlight_entry_for_selection_drag_cross_worktree(cx: &mut gpui::T
|
|||
main_rs_from_b,
|
||||
worktree_b.read(cx),
|
||||
&dragged_selection,
|
||||
false,
|
||||
cx,
|
||||
);
|
||||
assert_eq!(
|
||||
|
|
@ -9308,34 +9311,97 @@ async fn test_highlight_entry_for_multi_root_drag_excludes_marked_worktrees(
|
|||
|
||||
// Hovering r2 (not in the drag): highlight allowed.
|
||||
assert_eq!(
|
||||
panel.highlight_entry_for_selection_drag(r2_root, r2, &drag, cx),
|
||||
panel.highlight_entry_for_selection_drag(r2_root, r2, &drag, false, cx),
|
||||
Some(r2_root.id),
|
||||
"non-dragged worktree root should highlight"
|
||||
);
|
||||
|
||||
// Hovering r4 (not in the drag): highlight allowed.
|
||||
assert_eq!(
|
||||
panel.highlight_entry_for_selection_drag(r4_root, r4, &drag, cx),
|
||||
panel.highlight_entry_for_selection_drag(r4_root, r4, &drag, false, cx),
|
||||
Some(r4_root.id),
|
||||
"non-dragged worktree root should highlight"
|
||||
);
|
||||
|
||||
// Hovering r1 (active source): no-op, no highlight.
|
||||
assert_eq!(
|
||||
panel.highlight_entry_for_selection_drag(r1_root, r1, &drag, cx),
|
||||
panel.highlight_entry_for_selection_drag(r1_root, r1, &drag, false, cx),
|
||||
None,
|
||||
"active source's worktree should not highlight"
|
||||
);
|
||||
|
||||
// Hovering r3 (marked but not active): no-op, no highlight.
|
||||
assert_eq!(
|
||||
panel.highlight_entry_for_selection_drag(r3_root, r3, &drag, cx),
|
||||
panel.highlight_entry_for_selection_drag(r3_root, r3, &drag, false, cx),
|
||||
None,
|
||||
"marked source's worktree should not highlight"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// In copy mode, a pure worktree-root drag is a guaranteed no-op (the copy
|
||||
// branch in `drag_onto` filters worktree roots out). The highlight should
|
||||
// reflect this by suppressing every target instead of advertising root
|
||||
// rows as valid drops.
|
||||
#[gpui::test]
|
||||
async fn test_highlight_entry_for_root_drag_suppressed_in_copy_mode(
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree("/root1", json!({ "a.txt": "" })).await;
|
||||
fs.insert_tree("/root2", json!({ "b.txt": "" })).await;
|
||||
|
||||
let project =
|
||||
Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
|
||||
let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
|
||||
let workspace = window
|
||||
.read_with(cx, |mw, _| mw.workspace().clone())
|
||||
.unwrap();
|
||||
let cx = &mut VisualTestContext::from_window(window.into(), cx);
|
||||
let panel = workspace.update_in(cx, ProjectPanel::new);
|
||||
cx.run_until_parked();
|
||||
|
||||
panel.update(cx, |panel, cx| {
|
||||
let worktrees: Vec<_> = panel.project.read(cx).visible_worktrees(cx).collect();
|
||||
let r1 = worktrees[0].read(cx);
|
||||
let r2 = worktrees[1].read(cx);
|
||||
let r1_root = r1.root_entry().unwrap();
|
||||
let r2_root = r2.root_entry().unwrap();
|
||||
|
||||
let drag = DraggedSelection {
|
||||
active_selection: SelectedEntry {
|
||||
worktree_id: r1.id(),
|
||||
entry_id: r1_root.id,
|
||||
},
|
||||
marked_selections: Arc::new([SelectedEntry {
|
||||
worktree_id: r1.id(),
|
||||
entry_id: r1_root.id,
|
||||
}]),
|
||||
};
|
||||
|
||||
// Sanity: without the copy modifier, hovering r2's root highlights it.
|
||||
assert_eq!(
|
||||
panel.highlight_entry_for_selection_drag(r2_root, r2, &drag, false, cx),
|
||||
Some(r2_root.id),
|
||||
"non-copy root drag should highlight a different worktree's root"
|
||||
);
|
||||
|
||||
// With the copy modifier, no target highlights — copy can't act on roots.
|
||||
assert_eq!(
|
||||
panel.highlight_entry_for_selection_drag(r2_root, r2, &drag, true, cx),
|
||||
None,
|
||||
"copy-mode root drag should suppress highlights on other roots"
|
||||
);
|
||||
assert_eq!(
|
||||
panel.highlight_entry_for_selection_drag(r1_root, r1, &drag, true, cx),
|
||||
None,
|
||||
"copy-mode root drag should suppress highlights on its own worktree"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_should_highlight_background_for_selection_drag(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
|
|
|
|||
Loading…
Reference in a new issue