diff --git a/crates/herdr-commands/src/lib.rs b/crates/herdr-commands/src/lib.rs index c0ec2c67d..b8d3a154b 100644 --- a/crates/herdr-commands/src/lib.rs +++ b/crates/herdr-commands/src/lib.rs @@ -113,6 +113,8 @@ pub enum Command { ResizeUp, ResizeDown, ResizeMode, + /// Evens every split in the focused tab, as a double-click on a border does. + EqualizeSplits, RenamePane, MovePane, PreviousWorkspace, @@ -374,6 +376,11 @@ pub const COMMANDS: &[CommandInfo] = &[ name: "resize_mode", label: "Resize Mode", }, + CommandInfo { + command: Command::EqualizeSplits, + name: "equalize_splits", + label: "Equalize Splits", + }, CommandInfo { command: Command::RenamePane, name: "rename_pane", @@ -911,6 +918,7 @@ pub fn request(command: Command, snapshot: &ClientShellSnapshot) -> Option<(Meth | Command::Back | Command::Forward | Command::ResizeMode + | Command::EqualizeSplits | Command::RenamePane | Command::MovePane | Command::PreviousWorkspace diff --git a/crates/herdr-commands/src/tests.rs b/crates/herdr-commands/src/tests.rs index 697b82687..04e8ad309 100644 --- a/crates/herdr-commands/src/tests.rs +++ b/crates/herdr-commands/src/tests.rs @@ -59,6 +59,7 @@ fn catalog_has_all_native_commands_and_gpui_shortcuts() { (ResizeUp, &[]), (ResizeDown, &[]), (ResizeMode, &[]), + (EqualizeSplits, &[]), (RenamePane, &[]), (MovePane, &[]), (Zoom, &["cmd-shift-enter"]), @@ -217,6 +218,7 @@ fn gui_commands_never_send_daemon_requests() { Command::Back, Command::Forward, Command::ResizeMode, + Command::EqualizeSplits, Command::RenamePane, Command::MovePane, Command::PreviousWorkspace, diff --git a/crates/herdr-gpui/README.md b/crates/herdr-gpui/README.md index 37367476d..0d454175d 100644 --- a/crates/herdr-gpui/README.md +++ b/crates/herdr-gpui/README.md @@ -4289,7 +4289,9 @@ Ctrl+Shift+U is left unbound for `[pane_keys]` entry on the symbol, keeps its key even when a custom command is bound to the shifted key. `resize_mode` (`prefix+r`) makes h, j, k, l or the arrows resize the focused pane until Escape, Enter, - or the shortcut again; the status bar shows it. Renames and + or the shortcut again; the status bar shows it. `equalize_splits` evens + every split in the focused tab, as a double-click on a border between + panes does; it has no default shortcut. Renames and `close_workspace` open the same dialogs as their menus, and `reload_config` reloads both the daemon's config and this one. The daemon's `[[keys.command]]` shortcuts, prefix chords or direct keys, run their diff --git a/crates/herdr-gpui/src/endpoint/lifecycle_tests/split_equalize.rs b/crates/herdr-gpui/src/endpoint/lifecycle_tests/split_equalize.rs index bd0234c17..cc19d9c0d 100644 --- a/crates/herdr-gpui/src/endpoint/lifecycle_tests/split_equalize.rs +++ b/crates/herdr-gpui/src/endpoint/lifecycle_tests/split_equalize.rs @@ -357,3 +357,5 @@ fn a_tab_switch_drops_the_rest_of_an_even_queue(cx: &mut gpui::TestAppContext) { assert_eq!(path, serde_json::json!([])); assert!((ratio - 0.25).abs() < 1e-5, "ratio was {ratio}"); } + +mod command; diff --git a/crates/herdr-gpui/src/endpoint/lifecycle_tests/split_equalize/command.rs b/crates/herdr-gpui/src/endpoint/lifecycle_tests/split_equalize/command.rs new file mode 100644 index 000000000..9fd11b2b4 --- /dev/null +++ b/crates/herdr-gpui/src/endpoint/lifecycle_tests/split_equalize/command.rs @@ -0,0 +1,61 @@ +use super::*; + +fn run(view: &gpui::Entity, cx: &mut gpui::VisualTestContext) { + cx.update(|window, cx| { + view.update(cx, |view, cx| { + view.command(Command::EqualizeSplits, window, cx); + }); + }); +} + +#[gpui::test] +fn the_command_evens_every_split_in_the_tab(cx: &mut gpui::TestAppContext) { + let (view, mut server, cx) = with_borders(cx, "ssh:even-command"); + run(&view, cx); + assert!(cx.update(|_, cx| view.read(cx).live.drag_request.is_some())); + assert_even(&evened(&view, &mut server, cx)); + // Nothing is left held, so the next border press starts fresh. + cx.update(|_, cx| { + view.update(cx, |view, cx| view.flush_split(cx)); + assert!(view.read(cx).split_drag.is_none()); + }); +} + +#[gpui::test] +fn the_command_leaves_a_zoomed_tab_alone(cx: &mut gpui::TestAppContext) { + let (view, _server, cx) = with_borders(cx, "ssh:even-zoomed"); + cx.update(|_, cx| view.update(cx, |view, _| set_zoomed(view, true))); + run(&view, cx); + cx.update(|_, cx| { + let view = view.read(cx); + assert!(view.split_drag.is_none()); + assert!(view.live.drag_request.is_none()); + }); +} + +#[gpui::test] +fn the_command_leaves_a_held_border_drag_alone(cx: &mut gpui::TestAppContext) { + let (view, _server, cx) = with_borders(cx, "ssh:even-held"); + cx.update(|_, cx| { + view.update(cx, |view, cx| assert!(down(view, 40.5, 5., 1, cx))); + }); + run(&view, cx); + // Evening would have sent its first ratio at once; the held drag sends + // nothing until the pointer moves. + cx.update(|_, cx| { + view.update(cx, |view, cx| { + assert!(view.split_drag.is_some()); + assert!(view.live.drag_request.is_none()); + release(view, 40.5, 5., cx); + }); + }); +} + +#[gpui::test] +fn running_it_again_mid_way_does_not_restart_the_queue(cx: &mut gpui::TestAppContext) { + let (view, mut server, cx) = with_borders(cx, "ssh:even-twice"); + run(&view, cx); + run(&view, cx); + // One ratio per split, not a second round on top of the first. + assert_even(&evened(&view, &mut server, cx)); +} diff --git a/crates/herdr-gpui/src/menu/settings.rs b/crates/herdr-gpui/src/menu/settings.rs index f98938812..e9fa28b22 100644 --- a/crates/herdr-gpui/src/menu/settings.rs +++ b/crates/herdr-gpui/src/menu/settings.rs @@ -345,6 +345,7 @@ impl HerdrWindow { | Command::ResizeUp | Command::ResizeDown | Command::ResizeMode + | Command::EqualizeSplits | Command::RenamePane | Command::MovePane | Command::RenameWorkspace diff --git a/crates/herdr-gpui/src/window/commands.rs b/crates/herdr-gpui/src/window/commands.rs index ef8b8d461..58f39ab0b 100644 --- a/crates/herdr-gpui/src/window/commands.rs +++ b/crates/herdr-gpui/src/window/commands.rs @@ -639,6 +639,10 @@ impl HerdrWindow { cx.notify(); return; } + Command::EqualizeSplits => { + self.equalize_splits(cx); + return; + } Command::ResizeMode => { self.prefix_armed = false; self.resize_mode = true; diff --git a/crates/herdr-gpui/src/window/mouse.rs b/crates/herdr-gpui/src/window/mouse.rs index 10d231990..985411b2d 100644 --- a/crates/herdr-gpui/src/window/mouse.rs +++ b/crates/herdr-gpui/src/window/mouse.rs @@ -771,6 +771,40 @@ impl HerdrWindow { true } + /// Evens every split in the focused tab, as a double-click on a border + /// does, from a command. A zoomed tab, or one with a border still being + /// dragged or evened, is left alone. + pub(crate) fn equalize_splits(&mut self, cx: &mut Context) { + if self.split_drag.is_some() { + return; + } + let (Some(snapshot), Some(surface)) = (&self.live.snapshot, self.live.surface.as_deref()) + else { + return; + }; + let Some(tab) = snapshot.focused_tab_id.clone() else { + return; + }; + let zoomed = snapshot + .tabs + .iter() + .any(|item| item.tab_id == tab && item.zoomed); + // Only a pointer drag moves this split; evening sends every ratio. + let Some(split) = surface.splits.first().cloned().filter(|_| !zoomed) else { + return; + }; + self.split_drag = Some(SplitDrag { + boot: snapshot.boot_id.clone(), + tab, + topology: splits::topology(surface), + split, + held: false, + motion: SplitMotion::Even(splits::even_targets(&surface.splits)), + }); + self.flush_split(cx); + cx.notify(); + } + pub(crate) fn split_mouse_move( &mut self, event: &MouseMoveEvent,