mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
Refresh worktree-drag highlights on modifier change and in copy mode
- `should_highlight_background_for_selection_drag` now takes `is_copy_mode` and returns false for pure-root drags in copy mode, matching the existing per-row highlight logic. The blank-area on_drag_move handler reads the modifier and threads it through. - The on_modifiers_changed listener now also clears the cached drag target so the next drag-move event recomputes the highlight under the new mode. Without this, the on_drag_move fast path skipped the recomputation while the pointer stayed put on the same row. - Extracted the modifier-changed logic into `ProjectPanel::handle_drag_modifiers_changed` so it can be tested directly. - Adds tests for both fixes.
This commit is contained in:
parent
48cfd34f0c
commit
0611b29559
2 changed files with 151 additions and 12 deletions
|
|
@ -4467,6 +4467,22 @@ impl ProjectPanel {
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_drag_modifiers_changed(
|
||||||
|
&mut self,
|
||||||
|
modifiers: &Modifiers,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.refresh_drag_cursor_style(modifiers, window, cx);
|
||||||
|
// The copy modifier flips highlight semantics for worktree-root
|
||||||
|
// drags. Drop the cached target so the next drag-move event
|
||||||
|
// recomputes under the new mode — otherwise the on_drag_move
|
||||||
|
// fast path skips the recomputation while the pointer stays put.
|
||||||
|
if self.drag_target_entry.take().is_some() {
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn refresh_drag_cursor_style(
|
fn refresh_drag_cursor_style(
|
||||||
&self,
|
&self,
|
||||||
modifiers: &Modifiers,
|
modifiers: &Modifiers,
|
||||||
|
|
@ -5342,19 +5358,28 @@ impl ProjectPanel {
|
||||||
&self,
|
&self,
|
||||||
drag_state: &DraggedSelection,
|
drag_state: &DraggedSelection,
|
||||||
last_root_id: ProjectEntryId,
|
last_root_id: ProjectEntryId,
|
||||||
|
is_copy_mode: bool,
|
||||||
cx: &App,
|
cx: &App,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
|
let project = self.project.read(cx);
|
||||||
|
|
||||||
|
// Worktree roots can't be copied, so a pure-root copy drag is a
|
||||||
|
// guaranteed no-op — don't advertise the background as a target.
|
||||||
|
if is_copy_mode
|
||||||
|
&& drag_state
|
||||||
|
.items()
|
||||||
|
.all(|entry| project.entry_is_worktree_root(entry.entry_id, cx))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Always highlight for multiple entries
|
// Always highlight for multiple entries
|
||||||
if drag_state.items().count() > 1 {
|
if drag_state.items().count() > 1 {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Since root will always have empty relative path
|
// Since root will always have empty relative path
|
||||||
if let Some(entry_path) = self
|
if let Some(entry_path) = project.path_for_entry(drag_state.active_selection.entry_id, cx) {
|
||||||
.project
|
|
||||||
.read(cx)
|
|
||||||
.path_for_entry(drag_state.active_selection.entry_id, cx)
|
|
||||||
{
|
|
||||||
if let Some(parent_path) = entry_path.path.parent() {
|
if let Some(parent_path) = entry_path.path.parent() {
|
||||||
if !parent_path.is_empty() {
|
if !parent_path.is_empty() {
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -5363,11 +5388,7 @@ impl ProjectPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
// If parent is empty, check if different worktree
|
// If parent is empty, check if different worktree
|
||||||
if let Some(last_root_worktree_id) = self
|
if let Some(last_root_worktree_id) = project.worktree_id_for_entry(last_root_id, cx) {
|
||||||
.project
|
|
||||||
.read(cx)
|
|
||||||
.worktree_id_for_entry(last_root_id, cx)
|
|
||||||
{
|
|
||||||
if drag_state.active_selection.worktree_id != last_root_worktree_id {
|
if drag_state.active_selection.worktree_id != last_root_worktree_id {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -6751,7 +6772,7 @@ impl Render for ProjectPanel {
|
||||||
.relative()
|
.relative()
|
||||||
.on_modifiers_changed(cx.listener(
|
.on_modifiers_changed(cx.listener(
|
||||||
|this, event: &ModifiersChangedEvent, window, cx| {
|
|this, event: &ModifiersChangedEvent, window, cx| {
|
||||||
this.refresh_drag_cursor_style(&event.modifiers, window, cx);
|
this.handle_drag_modifiers_changed(&event.modifiers, window, cx);
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
.key_context(self.dispatch_context(window, cx))
|
.key_context(self.dispatch_context(window, cx))
|
||||||
|
|
@ -7094,16 +7115,23 @@ impl Render for ProjectPanel {
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
.on_drag_move::<DraggedSelection>(cx.listener(
|
.on_drag_move::<DraggedSelection>(cx.listener(
|
||||||
move |this, event: &DragMoveEvent<DraggedSelection>, _, cx| {
|
move |this,
|
||||||
|
event: &DragMoveEvent<DraggedSelection>,
|
||||||
|
window,
|
||||||
|
cx| {
|
||||||
let Some(last_root_id) = this.state.last_worktree_root_id
|
let Some(last_root_id) = this.state.last_worktree_root_id
|
||||||
else {
|
else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if event.bounds.contains(&event.event.position) {
|
if event.bounds.contains(&event.event.position) {
|
||||||
let drag_state = event.drag(cx);
|
let drag_state = event.drag(cx);
|
||||||
|
let is_copy_mode = Self::is_copy_modifier_set(
|
||||||
|
&window.modifiers(),
|
||||||
|
);
|
||||||
if this.should_highlight_background_for_selection_drag(
|
if this.should_highlight_background_for_selection_drag(
|
||||||
&drag_state,
|
&drag_state,
|
||||||
last_root_id,
|
last_root_id,
|
||||||
|
is_copy_mode,
|
||||||
cx,
|
cx,
|
||||||
) {
|
) {
|
||||||
this.drag_target_entry =
|
this.drag_target_entry =
|
||||||
|
|
|
||||||
|
|
@ -9480,6 +9480,7 @@ async fn test_should_highlight_background_for_selection_drag(cx: &mut gpui::Test
|
||||||
let result = panel.should_highlight_background_for_selection_drag(
|
let result = panel.should_highlight_background_for_selection_drag(
|
||||||
&multiple_dragged_selection,
|
&multiple_dragged_selection,
|
||||||
root1_entry.id,
|
root1_entry.id,
|
||||||
|
false,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
assert!(result, "Should highlight background for multiple entries");
|
assert!(result, "Should highlight background for multiple entries");
|
||||||
|
|
@ -9499,6 +9500,7 @@ async fn test_should_highlight_background_for_selection_drag(cx: &mut gpui::Test
|
||||||
let result = panel.should_highlight_background_for_selection_drag(
|
let result = panel.should_highlight_background_for_selection_drag(
|
||||||
&nested_dragged_selection,
|
&nested_dragged_selection,
|
||||||
root1_entry.id,
|
root1_entry.id,
|
||||||
|
false,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
assert!(result, "Should highlight background for nested file");
|
assert!(result, "Should highlight background for nested file");
|
||||||
|
|
@ -9518,6 +9520,7 @@ async fn test_should_highlight_background_for_selection_drag(cx: &mut gpui::Test
|
||||||
let result = panel.should_highlight_background_for_selection_drag(
|
let result = panel.should_highlight_background_for_selection_drag(
|
||||||
&root_file_dragged_selection,
|
&root_file_dragged_selection,
|
||||||
root1_entry.id,
|
root1_entry.id,
|
||||||
|
false,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
|
|
@ -9529,6 +9532,7 @@ async fn test_should_highlight_background_for_selection_drag(cx: &mut gpui::Test
|
||||||
let result = panel.should_highlight_background_for_selection_drag(
|
let result = panel.should_highlight_background_for_selection_drag(
|
||||||
&root_file_dragged_selection,
|
&root_file_dragged_selection,
|
||||||
root2_entry.id,
|
root2_entry.id,
|
||||||
|
false,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
|
|
@ -9551,6 +9555,7 @@ async fn test_should_highlight_background_for_selection_drag(cx: &mut gpui::Test
|
||||||
let result = panel.should_highlight_background_for_selection_drag(
|
let result = panel.should_highlight_background_for_selection_drag(
|
||||||
&child_file_dragged_selection,
|
&child_file_dragged_selection,
|
||||||
root1_entry.id,
|
root1_entry.id,
|
||||||
|
false,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
|
|
@ -9560,6 +9565,112 @@ async fn test_should_highlight_background_for_selection_drag(cx: &mut gpui::Test
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// In copy mode, a pure worktree-root drag is a no-op, so the blank area
|
||||||
|
// below the panel should not light up as a valid drop target.
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_should_highlight_background_suppressed_for_root_drag_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_id = r1.root_entry().unwrap().id;
|
||||||
|
let r2_root_id = r2.root_entry().unwrap().id;
|
||||||
|
|
||||||
|
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: in non-copy mode, the background highlights for a cross-
|
||||||
|
// worktree root drag (so the user can drop at the end).
|
||||||
|
assert!(
|
||||||
|
panel.should_highlight_background_for_selection_drag(
|
||||||
|
&drag,
|
||||||
|
r2_root_id,
|
||||||
|
false,
|
||||||
|
cx,
|
||||||
|
),
|
||||||
|
"non-copy root drag should highlight background"
|
||||||
|
);
|
||||||
|
|
||||||
|
// In copy mode, the same drag is a no-op → no background highlight.
|
||||||
|
assert!(
|
||||||
|
!panel.should_highlight_background_for_selection_drag(
|
||||||
|
&drag,
|
||||||
|
r2_root_id,
|
||||||
|
true,
|
||||||
|
cx,
|
||||||
|
),
|
||||||
|
"copy-mode root drag should suppress background highlight"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggling the copy modifier while the cursor stays still must invalidate
|
||||||
|
// the cached drag target so the next drag-move event recomputes the
|
||||||
|
// highlight under the new mode (otherwise the row stays in its old
|
||||||
|
// highlight state until the pointer leaves and re-enters).
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_modifier_change_clears_drag_target_entry(
|
||||||
|
cx: &mut gpui::TestAppContext,
|
||||||
|
) {
|
||||||
|
init_test(cx);
|
||||||
|
|
||||||
|
let fs = FakeFs::new(cx.executor());
|
||||||
|
fs.insert_tree("/root1", json!({ "a.txt": "" })).await;
|
||||||
|
|
||||||
|
let project = Project::test(fs.clone(), ["/root1".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_in(cx, |panel, window, cx| {
|
||||||
|
panel.drag_target_entry = Some(DragTarget::Background);
|
||||||
|
panel.handle_drag_modifiers_changed(
|
||||||
|
&gpui::Modifiers {
|
||||||
|
alt: true,
|
||||||
|
control: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
panel.drag_target_entry.is_none(),
|
||||||
|
"modifier change should clear drag_target_entry so the next \
|
||||||
|
drag-move can recompute highlights under the new mode"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_hide_root(cx: &mut gpui::TestAppContext) {
|
async fn test_hide_root(cx: &mut gpui::TestAppContext) {
|
||||||
init_test(cx);
|
init_test(cx);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue