Add ability to clone item when using workspace::MoveItemToPane (#32895)

This PR adds an optional `clone: bool` argument to
`workspace::MoveItemToPane` and `workspace::MoveItemToPaneInDirection`
which causes the item to be cloned into the destination pane rather than
moved. It provides similar functionality to
`workbench.action.splitEditorToRightGroup` in vscode.

This PR supercedes #25030.

Closes #24889

Release Notes:

- Add optional `clone: bool` (default: `false`) to
`workspace::MoveItemToPane` and `workspace::MoveItemToPaneInDirection`
which causes the item to be cloned into the destination pane rather than
moved.
This commit is contained in:
Carl Sverre 2025-06-23 16:41:46 -07:00 committed by GitHub
parent 324cbecb74
commit 95f10fd187
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -222,6 +222,8 @@ pub struct MoveItemToPane {
pub destination: usize, pub destination: usize,
#[serde(default = "default_true")] #[serde(default = "default_true")]
pub focus: bool, pub focus: bool,
#[serde(default)]
pub clone: bool,
} }
#[derive(Clone, Deserialize, PartialEq, JsonSchema)] #[derive(Clone, Deserialize, PartialEq, JsonSchema)]
@ -230,6 +232,8 @@ pub struct MoveItemToPaneInDirection {
pub direction: SplitDirection, pub direction: SplitDirection,
#[serde(default = "default_true")] #[serde(default = "default_true")]
pub focus: bool, pub focus: bool,
#[serde(default)]
pub clone: bool,
} }
#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema)] #[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema)]
@ -3355,7 +3359,7 @@ impl Workspace {
let destination = match panes.get(action.destination) { let destination = match panes.get(action.destination) {
Some(&destination) => destination.clone(), Some(&destination) => destination.clone(),
None => { None => {
if self.active_pane.read(cx).items_len() < 2 { if !action.clone && self.active_pane.read(cx).items_len() < 2 {
return; return;
} }
let direction = SplitDirection::Right; let direction = SplitDirection::Right;
@ -3375,6 +3379,16 @@ impl Workspace {
} }
}; };
if action.clone {
clone_active_item(
self.database_id(),
&self.active_pane,
&destination,
action.focus,
window,
cx,
)
} else {
move_active_item( move_active_item(
&self.active_pane, &self.active_pane,
&destination, &destination,
@ -3384,6 +3398,7 @@ impl Workspace {
cx, cx,
) )
} }
}
pub fn activate_next_pane(&mut self, window: &mut Window, cx: &mut App) { pub fn activate_next_pane(&mut self, window: &mut Window, cx: &mut App) {
let panes = self.center.panes(); let panes = self.center.panes();
@ -3526,7 +3541,7 @@ impl Workspace {
let destination = match self.find_pane_in_direction(action.direction, cx) { let destination = match self.find_pane_in_direction(action.direction, cx) {
Some(destination) => destination, Some(destination) => destination,
None => { None => {
if self.active_pane.read(cx).items_len() < 2 { if !action.clone && self.active_pane.read(cx).items_len() < 2 {
return; return;
} }
let new_pane = self.add_pane(window, cx); let new_pane = self.add_pane(window, cx);
@ -3542,6 +3557,16 @@ impl Workspace {
} }
}; };
if action.clone {
clone_active_item(
self.database_id(),
&self.active_pane,
&destination,
action.focus,
window,
cx,
)
} else {
move_active_item( move_active_item(
&self.active_pane, &self.active_pane,
&destination, &destination,
@ -3551,6 +3576,7 @@ impl Workspace {
cx, cx,
); );
} }
}
pub fn bounding_box_for_pane(&self, pane: &Entity<Pane>) -> Option<Bounds<Pixels>> { pub fn bounding_box_for_pane(&self, pane: &Entity<Pane>) -> Option<Bounds<Pixels>> {
self.center.bounding_box_for_pane(pane) self.center.bounding_box_for_pane(pane)
@ -7631,6 +7657,35 @@ pub fn move_active_item(
}); });
} }
pub fn clone_active_item(
workspace_id: Option<WorkspaceId>,
source: &Entity<Pane>,
destination: &Entity<Pane>,
focus_destination: bool,
window: &mut Window,
cx: &mut App,
) {
if source == destination {
return;
}
let Some(active_item) = source.read(cx).active_item() else {
return;
};
destination.update(cx, |target_pane, cx| {
let Some(clone) = active_item.clone_on_split(workspace_id, window, cx) else {
return;
};
target_pane.add_item(
clone,
focus_destination,
focus_destination,
Some(target_pane.items_len()),
window,
cx,
);
});
}
#[derive(Debug)] #[derive(Debug)]
pub struct WorkspacePosition { pub struct WorkspacePosition {
pub window_bounds: Option<WindowBounds>, pub window_bounds: Option<WindowBounds>,
@ -9814,6 +9869,7 @@ mod tests {
&MoveItemToPaneInDirection { &MoveItemToPaneInDirection {
direction: SplitDirection::Right, direction: SplitDirection::Right,
focus: true, focus: true,
clone: false,
}, },
window, window,
cx, cx,
@ -9822,6 +9878,7 @@ mod tests {
&MoveItemToPane { &MoveItemToPane {
destination: 3, destination: 3,
focus: true, focus: true,
clone: false,
}, },
window, window,
cx, cx,
@ -9848,6 +9905,7 @@ mod tests {
&MoveItemToPaneInDirection { &MoveItemToPaneInDirection {
direction: SplitDirection::Right, direction: SplitDirection::Right,
focus: true, focus: true,
clone: false,
}, },
window, window,
cx, cx,
@ -9884,6 +9942,7 @@ mod tests {
&MoveItemToPane { &MoveItemToPane {
destination: 3, destination: 3,
focus: true, focus: true,
clone: false,
}, },
window, window,
cx, cx,
@ -9907,6 +9966,61 @@ mod tests {
}); });
} }
#[gpui::test]
async fn test_moving_items_can_clone_panes(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, [], cx).await;
let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
let item_1 = cx.new(|cx| {
TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "first.txt", cx)])
});
workspace.update_in(cx, |workspace, window, cx| {
workspace.add_item_to_active_pane(Box::new(item_1), None, true, window, cx);
workspace.move_item_to_pane_in_direction(
&MoveItemToPaneInDirection {
direction: SplitDirection::Right,
focus: true,
clone: true,
},
window,
cx,
);
workspace.move_item_to_pane_at_index(
&MoveItemToPane {
destination: 3,
focus: true,
clone: true,
},
window,
cx,
);
assert_eq!(workspace.panes.len(), 3, "Two new panes were created");
for pane in workspace.panes() {
assert_eq!(
pane_items_paths(pane, cx),
vec!["first.txt".to_string()],
"Single item exists in all panes"
);
}
});
// verify that the active pane has been updated after waiting for the
// pane focus event to fire and resolve
workspace.read_with(cx, |workspace, _app| {
assert_eq!(
workspace.active_pane(),
&workspace.panes[2],
"The third pane should be the active one: {:?}",
workspace.panes
);
})
}
mod register_project_item_tests { mod register_project_item_tests {
use super::*; use super::*;