Allow empty splits on panes (#40245)
Draft as a base for continuing the discussion in #8008 : adds a `SplitOperation` enum to support bindings like `["pane::SplitLeft", {"operation": "Clear"}]` To be discussed @MrSubidubi and others: - Naming: Generally not happy with names yet and specifically `Empty` is unclear, e.g., what does this mean for terminal panes? Added placeholder code to split without cloning, but unsure what users would expect in this case. - ~~I removed `SplitAndMoveXyz` actions but I guess we should keep them for backwards compatibility?~~ - May have missed details in the move implementation. Will check the code again for opportunities to refactor more code after we agree on the approach. - ~~Tests should go to `crates/collab/src/tests/integration_tests.rs`?~~ Closes #8008 Release Notes: - Add `pane::Split` mode (`{ClonePane,EmptyPane,MovePane}`) to allow creating an empty buffer. --------- Co-authored-by: Finn Evers <finn.evers@outlook.de> Co-authored-by: MrSubidubi <finn@zed.dev>
This commit is contained in:
@@ -6745,8 +6745,13 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
|
||||
});
|
||||
|
||||
// Split pane to the right
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.split(workspace::SplitDirection::Right, cx);
|
||||
pane.update_in(cx, |pane, window, cx| {
|
||||
pane.split(
|
||||
workspace::SplitDirection::Right,
|
||||
workspace::SplitMode::default(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
let right_pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
|
||||
|
||||
@@ -1760,16 +1760,19 @@ impl PickerDelegate for FileFinderDelegate {
|
||||
menu.context(focus_handle)
|
||||
.action(
|
||||
"Split Left",
|
||||
pane::SplitLeft.boxed_clone(),
|
||||
pane::SplitLeft::default().boxed_clone(),
|
||||
)
|
||||
.action(
|
||||
"Split Right",
|
||||
pane::SplitRight.boxed_clone(),
|
||||
pane::SplitRight::default().boxed_clone(),
|
||||
)
|
||||
.action(
|
||||
"Split Up",
|
||||
pane::SplitUp::default().boxed_clone(),
|
||||
)
|
||||
.action("Split Up", pane::SplitUp.boxed_clone())
|
||||
.action(
|
||||
"Split Down",
|
||||
pane::SplitDown.boxed_clone(),
|
||||
pane::SplitDown::default().boxed_clone(),
|
||||
)
|
||||
}
|
||||
}))
|
||||
|
||||
@@ -30,8 +30,8 @@ use workspace::{
|
||||
ActivateNextPane, ActivatePane, ActivatePaneDown, ActivatePaneLeft, ActivatePaneRight,
|
||||
ActivatePaneUp, ActivatePreviousPane, DraggedSelection, DraggedTab, ItemId, MoveItemToPane,
|
||||
MoveItemToPaneInDirection, MovePaneDown, MovePaneLeft, MovePaneRight, MovePaneUp, NewTerminal,
|
||||
Pane, PaneGroup, SplitDirection, SplitDown, SplitLeft, SplitRight, SplitUp, SwapPaneDown,
|
||||
SwapPaneLeft, SwapPaneRight, SwapPaneUp, ToggleZoom, Workspace,
|
||||
Pane, PaneGroup, SplitDirection, SplitDown, SplitLeft, SplitMode, SplitRight, SplitUp,
|
||||
SwapPaneDown, SwapPaneLeft, SwapPaneRight, SwapPaneUp, ToggleZoom, Workspace,
|
||||
dock::{DockPosition, Panel, PanelEvent, PanelHandle},
|
||||
item::SerializableItem,
|
||||
move_active_item, move_item, pane,
|
||||
@@ -192,10 +192,10 @@ impl TerminalPanel {
|
||||
split_context.clone(),
|
||||
|menu, split_context| menu.context(split_context),
|
||||
)
|
||||
.action("Split Right", SplitRight.boxed_clone())
|
||||
.action("Split Left", SplitLeft.boxed_clone())
|
||||
.action("Split Up", SplitUp.boxed_clone())
|
||||
.action("Split Down", SplitDown.boxed_clone())
|
||||
.action("Split Right", SplitRight::default().boxed_clone())
|
||||
.action("Split Left", SplitLeft::default().boxed_clone())
|
||||
.action("Split Up", SplitUp::default().boxed_clone())
|
||||
.action("Split Down", SplitDown::default().boxed_clone())
|
||||
})
|
||||
.into()
|
||||
}
|
||||
@@ -380,47 +380,49 @@ impl TerminalPanel {
|
||||
}
|
||||
self.serialize(cx);
|
||||
}
|
||||
&pane::Event::Split {
|
||||
direction,
|
||||
clone_active_item,
|
||||
} => {
|
||||
if clone_active_item {
|
||||
let fut = self.new_pane_with_cloned_active_terminal(window, cx);
|
||||
let pane = pane.clone();
|
||||
cx.spawn_in(window, async move |panel, cx| {
|
||||
let Some(new_pane) = fut.await else {
|
||||
&pane::Event::Split { direction, mode } => {
|
||||
match mode {
|
||||
SplitMode::ClonePane | SplitMode::EmptyPane => {
|
||||
let clone = matches!(mode, SplitMode::ClonePane);
|
||||
let new_pane = self.new_pane_with_active_terminal(clone, window, cx);
|
||||
let pane = pane.clone();
|
||||
cx.spawn_in(window, async move |panel, cx| {
|
||||
let Some(new_pane) = new_pane.await else {
|
||||
return;
|
||||
};
|
||||
panel
|
||||
.update_in(cx, |panel, window, cx| {
|
||||
panel
|
||||
.center
|
||||
.split(&pane, &new_pane, direction, cx)
|
||||
.log_err();
|
||||
window.focus(&new_pane.focus_handle(cx), cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
SplitMode::MovePane => {
|
||||
let Some(item) =
|
||||
pane.update(cx, |pane, cx| pane.take_active_item(window, cx))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
panel
|
||||
.update_in(cx, |panel, window, cx| {
|
||||
panel
|
||||
.center
|
||||
.split(&pane, &new_pane, direction, cx)
|
||||
.log_err();
|
||||
window.focus(&new_pane.focus_handle(cx), cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
} else {
|
||||
let Some(item) = pane.update(cx, |pane, cx| pane.take_active_item(window, cx))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Ok(project) = self
|
||||
.workspace
|
||||
.update(cx, |workspace, _| workspace.project().clone())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let new_pane =
|
||||
new_terminal_pane(self.workspace.clone(), project, false, window, cx);
|
||||
new_pane.update(cx, |pane, cx| {
|
||||
pane.add_item(item, true, true, None, window, cx);
|
||||
});
|
||||
self.center.split(&pane, &new_pane, direction, cx).log_err();
|
||||
window.focus(&new_pane.focus_handle(cx), cx);
|
||||
}
|
||||
let Ok(project) = self
|
||||
.workspace
|
||||
.update(cx, |workspace, _| workspace.project().clone())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let new_pane =
|
||||
new_terminal_pane(self.workspace.clone(), project, false, window, cx);
|
||||
new_pane.update(cx, |pane, cx| {
|
||||
pane.add_item(item, true, true, None, window, cx);
|
||||
});
|
||||
self.center.split(&pane, &new_pane, direction, cx).log_err();
|
||||
window.focus(&new_pane.focus_handle(cx), cx);
|
||||
}
|
||||
};
|
||||
}
|
||||
pane::Event::Focus => {
|
||||
self.active_pane = pane.clone();
|
||||
@@ -433,8 +435,9 @@ impl TerminalPanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn new_pane_with_cloned_active_terminal(
|
||||
fn new_pane_with_active_terminal(
|
||||
&mut self,
|
||||
clone: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Option<Entity<Pane>>> {
|
||||
@@ -446,21 +449,34 @@ impl TerminalPanel {
|
||||
let weak_workspace = self.workspace.clone();
|
||||
let project = workspace.project().clone();
|
||||
let active_pane = &self.active_pane;
|
||||
let terminal_view = active_pane
|
||||
.read(cx)
|
||||
.active_item()
|
||||
.and_then(|item| item.downcast::<TerminalView>());
|
||||
let working_directory = terminal_view
|
||||
.as_ref()
|
||||
.and_then(|terminal_view| {
|
||||
terminal_view
|
||||
.read(cx)
|
||||
.terminal()
|
||||
.read(cx)
|
||||
.working_directory()
|
||||
})
|
||||
.or_else(|| default_working_directory(workspace, cx));
|
||||
let is_zoomed = active_pane.read(cx).is_zoomed();
|
||||
let terminal_view = if clone {
|
||||
active_pane
|
||||
.read(cx)
|
||||
.active_item()
|
||||
.and_then(|item| item.downcast::<TerminalView>())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let working_directory = if clone {
|
||||
terminal_view
|
||||
.as_ref()
|
||||
.and_then(|terminal_view| {
|
||||
terminal_view
|
||||
.read(cx)
|
||||
.terminal()
|
||||
.read(cx)
|
||||
.working_directory()
|
||||
})
|
||||
.or_else(|| default_working_directory(workspace, cx))
|
||||
} else {
|
||||
default_working_directory(workspace, cx)
|
||||
};
|
||||
|
||||
let is_zoomed = if clone {
|
||||
active_pane.read(cx).is_zoomed()
|
||||
} else {
|
||||
false
|
||||
};
|
||||
cx.spawn_in(window, async move |panel, cx| {
|
||||
let terminal = project
|
||||
.update(cx, |project, cx| match terminal_view {
|
||||
@@ -1482,7 +1498,7 @@ impl Render for TerminalPanel {
|
||||
window.focus(&pane.read(cx).focus_handle(cx), cx);
|
||||
} else {
|
||||
let future =
|
||||
terminal_panel.new_pane_with_cloned_active_terminal(window, cx);
|
||||
terminal_panel.new_pane_with_active_terminal(true, window, cx);
|
||||
cx.spawn_in(window, async move |terminal_panel, cx| {
|
||||
if let Some(new_pane) = future.await {
|
||||
_ = terminal_panel.update_in(
|
||||
|
||||
@@ -1468,24 +1468,28 @@ fn generate_commands(_: &App) -> Vec<VimCommand> {
|
||||
action.range.replace(range.clone());
|
||||
Some(Box::new(action))
|
||||
}),
|
||||
VimCommand::new(("sp", "lit"), workspace::SplitHorizontal).filename(|_, filename| {
|
||||
Some(
|
||||
VimSplit {
|
||||
vertical: false,
|
||||
filename,
|
||||
}
|
||||
.boxed_clone(),
|
||||
)
|
||||
}),
|
||||
VimCommand::new(("vs", "plit"), workspace::SplitVertical).filename(|_, filename| {
|
||||
Some(
|
||||
VimSplit {
|
||||
vertical: true,
|
||||
filename,
|
||||
}
|
||||
.boxed_clone(),
|
||||
)
|
||||
}),
|
||||
VimCommand::new(("sp", "lit"), workspace::SplitHorizontal::default()).filename(
|
||||
|_, filename| {
|
||||
Some(
|
||||
VimSplit {
|
||||
vertical: false,
|
||||
filename,
|
||||
}
|
||||
.boxed_clone(),
|
||||
)
|
||||
},
|
||||
),
|
||||
VimCommand::new(("vs", "plit"), workspace::SplitVertical::default()).filename(
|
||||
|_, filename| {
|
||||
Some(
|
||||
VimSplit {
|
||||
vertical: true,
|
||||
filename,
|
||||
}
|
||||
.boxed_clone(),
|
||||
)
|
||||
},
|
||||
),
|
||||
VimCommand::new(("tabe", "dit"), workspace::NewFile)
|
||||
.filename(|_action, filename| Some(VimEdit { filename }.boxed_clone())),
|
||||
VimCommand::new(("tabnew", ""), workspace::NewFile)
|
||||
|
||||
@@ -197,6 +197,41 @@ pub struct DeploySearch {
|
||||
pub excluded_files: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Debug, Deserialize, JsonSchema, Default)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub enum SplitMode {
|
||||
/// Clone the current pane.
|
||||
#[default]
|
||||
ClonePane,
|
||||
/// Create an empty new pane.
|
||||
EmptyPane,
|
||||
/// Move the item into a new pane. This will map to nop if only one pane exists.
|
||||
MovePane,
|
||||
}
|
||||
|
||||
macro_rules! split_structs {
|
||||
($($name:ident => $doc:literal),* $(,)?) => {
|
||||
$(
|
||||
#[doc = $doc]
|
||||
#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default, Action)]
|
||||
#[action(namespace = pane)]
|
||||
#[serde(deny_unknown_fields, default)]
|
||||
pub struct $name {
|
||||
pub mode: SplitMode,
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
split_structs!(
|
||||
SplitLeft => "Splits the pane to the left.",
|
||||
SplitRight => "Splits the pane to the right.",
|
||||
SplitUp => "Splits the pane upward.",
|
||||
SplitDown => "Splits the pane downward.",
|
||||
SplitHorizontal => "Splits the pane horizontally.",
|
||||
SplitVertical => "Splits the pane vertically."
|
||||
);
|
||||
|
||||
actions!(
|
||||
pane,
|
||||
[
|
||||
@@ -218,14 +253,6 @@ actions!(
|
||||
JoinAll,
|
||||
/// Reopens the most recently closed item.
|
||||
ReopenClosedItem,
|
||||
/// Splits the pane to the left, cloning the current item.
|
||||
SplitLeft,
|
||||
/// Splits the pane upward, cloning the current item.
|
||||
SplitUp,
|
||||
/// Splits the pane to the right, cloning the current item.
|
||||
SplitRight,
|
||||
/// Splits the pane downward, cloning the current item.
|
||||
SplitDown,
|
||||
/// Splits the pane to the left, moving the current item.
|
||||
SplitAndMoveLeft,
|
||||
/// Splits the pane upward, moving the current item.
|
||||
@@ -234,10 +261,6 @@ actions!(
|
||||
SplitAndMoveRight,
|
||||
/// Splits the pane downward, moving the current item.
|
||||
SplitAndMoveDown,
|
||||
/// Splits the pane horizontally.
|
||||
SplitHorizontal,
|
||||
/// Splits the pane vertically.
|
||||
SplitVertical,
|
||||
/// Swaps the current item with the one to the left.
|
||||
SwapItemLeft,
|
||||
/// Swaps the current item with the one to the right.
|
||||
@@ -279,7 +302,7 @@ pub enum Event {
|
||||
},
|
||||
Split {
|
||||
direction: SplitDirection,
|
||||
clone_active_item: bool,
|
||||
mode: SplitMode,
|
||||
},
|
||||
ItemPinned,
|
||||
ItemUnpinned,
|
||||
@@ -311,13 +334,10 @@ impl fmt::Debug for Event {
|
||||
.debug_struct("RemovedItem")
|
||||
.field("item", &item.item_id())
|
||||
.finish(),
|
||||
Event::Split {
|
||||
direction,
|
||||
clone_active_item,
|
||||
} => f
|
||||
Event::Split { direction, mode } => f
|
||||
.debug_struct("Split")
|
||||
.field("direction", direction)
|
||||
.field("clone_active_item", clone_active_item)
|
||||
.field("mode", mode)
|
||||
.finish(),
|
||||
Event::JoinAll => f.write_str("JoinAll"),
|
||||
Event::JoinIntoNext => f.write_str("JoinIntoNext"),
|
||||
@@ -2295,10 +2315,7 @@ impl Pane {
|
||||
let save_task = if let Some(project_path) = project_path {
|
||||
let (worktree, path) = project_path.await?;
|
||||
let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id())?;
|
||||
let new_path = ProjectPath {
|
||||
worktree_id,
|
||||
path: path,
|
||||
};
|
||||
let new_path = ProjectPath { worktree_id, path };
|
||||
|
||||
pane.update_in(cx, |pane, window, cx| {
|
||||
if let Some(item) = pane.item_for_path(new_path.clone(), cx) {
|
||||
@@ -2357,19 +2374,30 @@ impl Pane {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn split(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
|
||||
cx.emit(Event::Split {
|
||||
direction,
|
||||
clone_active_item: true,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn split_and_move(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
|
||||
if self.items.len() > 1 {
|
||||
pub fn split(
|
||||
&mut self,
|
||||
direction: SplitDirection,
|
||||
mode: SplitMode,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.items.len() <= 1 && mode == SplitMode::MovePane {
|
||||
// MovePane with only one pane present behaves like a SplitEmpty in the opposite direction
|
||||
let active_item = self.active_item();
|
||||
cx.emit(Event::Split {
|
||||
direction,
|
||||
clone_active_item: false,
|
||||
direction: direction.opposite(),
|
||||
mode: SplitMode::EmptyPane,
|
||||
});
|
||||
// ensure that we focus the moved pane
|
||||
// in this case we know that the window is the same as the active_item
|
||||
if let Some(active_item) = active_item {
|
||||
cx.defer_in(window, move |_, window, cx| {
|
||||
let focus_handle = active_item.item_focus_handle(cx);
|
||||
window.focus(&focus_handle, cx);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
cx.emit(Event::Split { direction, mode });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3824,16 +3852,17 @@ fn default_render_tab_bar_buttons(
|
||||
.with_handle(pane.split_item_context_menu_handle.clone())
|
||||
.menu(move |window, cx| {
|
||||
ContextMenu::build(window, cx, |menu, _, _| {
|
||||
let mode = SplitMode::MovePane;
|
||||
if can_split_move {
|
||||
menu.action("Split Right", SplitAndMoveRight.boxed_clone())
|
||||
.action("Split Left", SplitAndMoveLeft.boxed_clone())
|
||||
.action("Split Up", SplitAndMoveUp.boxed_clone())
|
||||
.action("Split Down", SplitAndMoveDown.boxed_clone())
|
||||
menu.action("Split Right", SplitRight { mode }.boxed_clone())
|
||||
.action("Split Left", SplitLeft { mode }.boxed_clone())
|
||||
.action("Split Up", SplitUp { mode }.boxed_clone())
|
||||
.action("Split Down", SplitDown { mode }.boxed_clone())
|
||||
} else {
|
||||
menu.action("Split Right", SplitRight.boxed_clone())
|
||||
.action("Split Left", SplitLeft.boxed_clone())
|
||||
.action("Split Up", SplitUp.boxed_clone())
|
||||
.action("Split Down", SplitDown.boxed_clone())
|
||||
menu.action("Split Right", SplitRight::default().boxed_clone())
|
||||
.action("Split Left", SplitLeft::default().boxed_clone())
|
||||
.action("Split Up", SplitUp::default().boxed_clone())
|
||||
.action("Split Down", SplitDown::default().boxed_clone())
|
||||
}
|
||||
})
|
||||
.into()
|
||||
@@ -3892,33 +3921,35 @@ impl Render for Pane {
|
||||
.size_full()
|
||||
.flex_none()
|
||||
.overflow_hidden()
|
||||
.on_action(
|
||||
cx.listener(|pane, _: &SplitLeft, _, cx| pane.split(SplitDirection::Left, cx)),
|
||||
)
|
||||
.on_action(cx.listener(|pane, _: &SplitUp, _, cx| pane.split(SplitDirection::Up, cx)))
|
||||
.on_action(cx.listener(|pane, _: &SplitHorizontal, _, cx| {
|
||||
pane.split(SplitDirection::horizontal(cx), cx)
|
||||
.on_action(cx.listener(|pane, split: &SplitLeft, window, cx| {
|
||||
pane.split(SplitDirection::Left, split.mode, window, cx)
|
||||
}))
|
||||
.on_action(cx.listener(|pane, _: &SplitVertical, _, cx| {
|
||||
pane.split(SplitDirection::vertical(cx), cx)
|
||||
.on_action(cx.listener(|pane, split: &SplitUp, window, cx| {
|
||||
pane.split(SplitDirection::Up, split.mode, window, cx)
|
||||
}))
|
||||
.on_action(
|
||||
cx.listener(|pane, _: &SplitRight, _, cx| pane.split(SplitDirection::Right, cx)),
|
||||
)
|
||||
.on_action(
|
||||
cx.listener(|pane, _: &SplitDown, _, cx| pane.split(SplitDirection::Down, cx)),
|
||||
)
|
||||
.on_action(cx.listener(|pane, _: &SplitAndMoveUp, _, cx| {
|
||||
pane.split_and_move(SplitDirection::Up, cx)
|
||||
.on_action(cx.listener(|pane, split: &SplitHorizontal, window, cx| {
|
||||
pane.split(SplitDirection::horizontal(cx), split.mode, window, cx)
|
||||
}))
|
||||
.on_action(cx.listener(|pane, _: &SplitAndMoveDown, _, cx| {
|
||||
pane.split_and_move(SplitDirection::Down, cx)
|
||||
.on_action(cx.listener(|pane, split: &SplitVertical, window, cx| {
|
||||
pane.split(SplitDirection::vertical(cx), split.mode, window, cx)
|
||||
}))
|
||||
.on_action(cx.listener(|pane, _: &SplitAndMoveLeft, _, cx| {
|
||||
pane.split_and_move(SplitDirection::Left, cx)
|
||||
.on_action(cx.listener(|pane, split: &SplitRight, window, cx| {
|
||||
pane.split(SplitDirection::Right, split.mode, window, cx)
|
||||
}))
|
||||
.on_action(cx.listener(|pane, _: &SplitAndMoveRight, _, cx| {
|
||||
pane.split_and_move(SplitDirection::Right, cx)
|
||||
.on_action(cx.listener(|pane, split: &SplitDown, window, cx| {
|
||||
pane.split(SplitDirection::Down, split.mode, window, cx)
|
||||
}))
|
||||
.on_action(cx.listener(|pane, _: &SplitAndMoveUp, window, cx| {
|
||||
pane.split(SplitDirection::Up, SplitMode::MovePane, window, cx)
|
||||
}))
|
||||
.on_action(cx.listener(|pane, _: &SplitAndMoveDown, window, cx| {
|
||||
pane.split(SplitDirection::Down, SplitMode::MovePane, window, cx)
|
||||
}))
|
||||
.on_action(cx.listener(|pane, _: &SplitAndMoveLeft, window, cx| {
|
||||
pane.split(SplitDirection::Left, SplitMode::MovePane, window, cx)
|
||||
}))
|
||||
.on_action(cx.listener(|pane, _: &SplitAndMoveRight, window, cx| {
|
||||
pane.split(SplitDirection::Right, SplitMode::MovePane, window, cx)
|
||||
}))
|
||||
.on_action(cx.listener(|_, _: &JoinIntoNext, _, cx| {
|
||||
cx.emit(Event::JoinIntoNext);
|
||||
@@ -4443,11 +4474,14 @@ impl Render for DraggedTab {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::num::NonZero;
|
||||
use std::{iter::zip, num::NonZero};
|
||||
|
||||
use super::*;
|
||||
use crate::item::test::{TestItem, TestProjectItem};
|
||||
use gpui::{TestAppContext, VisualTestContext, size};
|
||||
use crate::{
|
||||
Member,
|
||||
item::test::{TestItem, TestProjectItem},
|
||||
};
|
||||
use gpui::{AppContext, Axis, TestAppContext, VisualTestContext, size};
|
||||
use project::FakeFs;
|
||||
use settings::SettingsStore;
|
||||
use theme::LoadThemes;
|
||||
@@ -7125,6 +7159,32 @@ mod tests {
|
||||
assert_item_labels(&pane, ["A", "C*", "B"], cx);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_split_empty(cx: &mut TestAppContext) {
|
||||
for split_direction in SplitDirection::all() {
|
||||
test_single_pane_split(["A"], split_direction, SplitMode::EmptyPane, cx).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_split_clone(cx: &mut TestAppContext) {
|
||||
for split_direction in SplitDirection::all() {
|
||||
test_single_pane_split(["A"], split_direction, SplitMode::ClonePane, cx).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_split_move_right_on_single_pane(cx: &mut TestAppContext) {
|
||||
test_single_pane_split(["A"], SplitDirection::Right, SplitMode::MovePane, cx).await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_split_move(cx: &mut TestAppContext) {
|
||||
for split_direction in SplitDirection::all() {
|
||||
test_single_pane_split(["A", "B"], split_direction, SplitMode::MovePane, cx).await;
|
||||
}
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
@@ -7220,4 +7280,163 @@ mod tests {
|
||||
"pane items do not match expectation"
|
||||
);
|
||||
}
|
||||
|
||||
// Assert the item label, with the active item label expected active index
|
||||
#[track_caller]
|
||||
fn assert_item_labels_active_index(
|
||||
pane: &Entity<Pane>,
|
||||
expected_states: &[&str],
|
||||
expected_active_idx: usize,
|
||||
cx: &mut VisualTestContext,
|
||||
) {
|
||||
let actual_states = pane.update(cx, |pane, cx| {
|
||||
pane.items
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, item)| {
|
||||
let mut state = item
|
||||
.to_any_view()
|
||||
.downcast::<TestItem>()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.label
|
||||
.clone();
|
||||
if ix == pane.active_item_index {
|
||||
assert_eq!(ix, expected_active_idx);
|
||||
}
|
||||
if item.is_dirty(cx) {
|
||||
state.push('^');
|
||||
}
|
||||
if pane.is_tab_pinned(ix) {
|
||||
state.push('!');
|
||||
}
|
||||
state
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
assert_eq!(
|
||||
actual_states, expected_states,
|
||||
"pane items do not match expectation"
|
||||
);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_pane_ids_on_axis<const COUNT: usize>(
|
||||
workspace: &Entity<Workspace>,
|
||||
expected_ids: [&EntityId; COUNT],
|
||||
expected_axis: Axis,
|
||||
cx: &mut VisualTestContext,
|
||||
) {
|
||||
workspace.read_with(cx, |workspace, _| match &workspace.center.root {
|
||||
Member::Axis(axis) => {
|
||||
assert_eq!(axis.axis, expected_axis);
|
||||
assert_eq!(axis.members.len(), expected_ids.len());
|
||||
assert!(
|
||||
zip(expected_ids, &axis.members).all(|(e, a)| {
|
||||
if let Member::Pane(p) = a {
|
||||
p.entity_id() == *e
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}),
|
||||
"pane ids do not match expectation: {expected_ids:?} != {actual_ids:?}",
|
||||
actual_ids = axis.members
|
||||
);
|
||||
}
|
||||
Member::Pane(_) => panic!("expected axis"),
|
||||
});
|
||||
}
|
||||
|
||||
async fn test_single_pane_split<const COUNT: usize>(
|
||||
pane_labels: [&str; COUNT],
|
||||
direction: SplitDirection,
|
||||
operation: SplitMode,
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let project = Project::test(fs, None, cx).await;
|
||||
let (workspace, cx) =
|
||||
cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
|
||||
|
||||
let mut pane_before =
|
||||
workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
|
||||
for label in pane_labels {
|
||||
add_labeled_item(&pane_before, label, false, cx);
|
||||
}
|
||||
pane_before.update_in(cx, |pane, window, cx| {
|
||||
pane.split(direction, operation, window, cx)
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
let pane_after = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
|
||||
|
||||
let num_labels = pane_labels.len();
|
||||
let last_as_active = format!("{}*", String::from(pane_labels[num_labels - 1]));
|
||||
|
||||
// check labels for all split operations
|
||||
match operation {
|
||||
SplitMode::EmptyPane => {
|
||||
assert_item_labels_active_index(&pane_before, &pane_labels, num_labels - 1, cx);
|
||||
assert_item_labels(&pane_after, [], cx);
|
||||
}
|
||||
SplitMode::ClonePane => {
|
||||
assert_item_labels_active_index(&pane_before, &pane_labels, num_labels - 1, cx);
|
||||
assert_item_labels(&pane_after, [&last_as_active], cx);
|
||||
}
|
||||
SplitMode::MovePane => {
|
||||
let head = &pane_labels[..(num_labels - 1)];
|
||||
if num_labels == 1 {
|
||||
// We special-case this behavior and actually execute an empty pane command
|
||||
// followed by a refocus of the old pane for this case.
|
||||
pane_before = workspace.read_with(cx, |workspace, _cx| {
|
||||
workspace
|
||||
.panes()
|
||||
.into_iter()
|
||||
.find(|pane| *pane != &pane_after)
|
||||
.unwrap()
|
||||
.clone()
|
||||
});
|
||||
};
|
||||
|
||||
assert_item_labels_active_index(
|
||||
&pane_before,
|
||||
&head,
|
||||
head.len().saturating_sub(1),
|
||||
cx,
|
||||
);
|
||||
assert_item_labels(&pane_after, [&last_as_active], cx);
|
||||
pane_after.update_in(cx, |pane, window, cx| {
|
||||
window.focused(cx).is_some_and(|focus_handle| {
|
||||
focus_handle == pane.active_item().unwrap().item_focus_handle(cx)
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// expected axis depends on split direction
|
||||
let expected_axis = match direction {
|
||||
SplitDirection::Right | SplitDirection::Left => Axis::Horizontal,
|
||||
SplitDirection::Up | SplitDirection::Down => Axis::Vertical,
|
||||
};
|
||||
|
||||
// expected ids depends on split direction
|
||||
let expected_ids = match direction {
|
||||
SplitDirection::Right | SplitDirection::Down => {
|
||||
[&pane_before.entity_id(), &pane_after.entity_id()]
|
||||
}
|
||||
SplitDirection::Left | SplitDirection::Up => {
|
||||
[&pane_after.entity_id(), &pane_before.entity_id()]
|
||||
}
|
||||
};
|
||||
|
||||
// check pane axes for all operations
|
||||
match operation {
|
||||
SplitMode::EmptyPane | SplitMode::ClonePane => {
|
||||
assert_pane_ids_on_axis(&workspace, expected_ids, expected_axis, cx);
|
||||
}
|
||||
SplitMode::MovePane => {
|
||||
assert_pane_ids_on_axis(&workspace, expected_ids, expected_axis, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4262,16 +4262,19 @@ impl Workspace {
|
||||
item: item.boxed_clone(),
|
||||
});
|
||||
}
|
||||
pane::Event::Split {
|
||||
direction,
|
||||
clone_active_item,
|
||||
} => {
|
||||
if *clone_active_item {
|
||||
self.split_and_clone(pane.clone(), *direction, window, cx)
|
||||
.detach();
|
||||
} else {
|
||||
self.split_and_move(pane.clone(), *direction, window, cx);
|
||||
}
|
||||
pane::Event::Split { direction, mode } => {
|
||||
match mode {
|
||||
SplitMode::ClonePane => {
|
||||
self.split_and_clone(pane.clone(), *direction, window, cx)
|
||||
.detach();
|
||||
}
|
||||
SplitMode::EmptyPane => {
|
||||
self.split_pane(pane.clone(), *direction, window, cx);
|
||||
}
|
||||
SplitMode::MovePane => {
|
||||
self.split_and_move(pane.clone(), *direction, window, cx);
|
||||
}
|
||||
};
|
||||
}
|
||||
pane::Event::JoinIntoNext => {
|
||||
self.join_pane_into_next(pane.clone(), window, cx);
|
||||
|
||||
@@ -3817,7 +3817,7 @@ mod tests {
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
cx.dispatch_action(window.into(), pane::SplitRight);
|
||||
cx.dispatch_action(window.into(), pane::SplitRight::default());
|
||||
let editor_2 = cx.update(|cx| {
|
||||
let pane_2 = workspace.read(cx).active_pane().clone();
|
||||
assert_ne!(pane_1, pane_2);
|
||||
|
||||
@@ -32,10 +32,10 @@ pub fn app_menus(cx: &mut App) -> Vec<Menu> {
|
||||
MenuItem::submenu(Menu {
|
||||
name: "Editor Layout".into(),
|
||||
items: vec![
|
||||
MenuItem::action("Split Up", workspace::SplitUp),
|
||||
MenuItem::action("Split Down", workspace::SplitDown),
|
||||
MenuItem::action("Split Left", workspace::SplitLeft),
|
||||
MenuItem::action("Split Right", workspace::SplitRight),
|
||||
MenuItem::action("Split Up", workspace::SplitUp::default()),
|
||||
MenuItem::action("Split Down", workspace::SplitDown::default()),
|
||||
MenuItem::action("Split Left", workspace::SplitLeft::default()),
|
||||
MenuItem::action("Split Right", workspace::SplitRight::default()),
|
||||
],
|
||||
}),
|
||||
MenuItem::separator(),
|
||||
|
||||
Reference in New Issue
Block a user