diff options
| author | Jon Kinney <jonkinney@gmail.com> | 2026-08-04 16:35:15 -0500 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-08-05 00:35:15 +0300 |
| commit | 09cdd88d0aca35617eb05468c2421b0467656e4f (patch) | |
| tree | 46ab6bec955c322d7c49148bee2762c502dd8662 /src/workspace | |
| parent | 93e0086f383f000ada02be382230507d5bdf4d7e (diff) | |
fix(layout): return focus to the pane a split was opened from (#2266)
Closing a focused pane handed focus to the next pane in tree order.
For a pane opened beside another one -- a plugin split, a file viewer,
any transient tool pane -- that is rarely where the user was: it lands
on some unrelated neighbour rather than the pane that opened it.
Track the pane focus came from in TileLayout and prefer it when the
focused pane closes, falling back to tree order when there is no
history, when it points at the pane being closed, or when it points at
a pane that has since gone away. The history lives in the layout, so it
can only ever name a pane in the same tab.
A one-slot history is only sound if internal focus excursions never
write it, so the tree edits that used to bounce focus around now go
through target-taking primitives instead. close_pane removes a
background pane directly, so detach_pane and take_pane_for_move stop
focus-close-refocusing. split_pane splits a target without moving
focus: the runtime split path only focuses the new pane once the spawn
succeeds, which makes a failed split a pure rollback, and the targeted
and unfocused workspace split paths stop fabricating history.
insert_pane_near now takes the focus intent, so an unfocused pane move
leaves the target tab's history alone. The layout-level focused-split
helpers become test-only; production splits all flow through the
target-taking path.
Co-authored-by: Can Celik <ogulcancelik@gmail.com>
Diffstat (limited to 'src/workspace')
| -rw-r--r-- | src/workspace/tab.rs | 118 |
1 files changed, 53 insertions, 65 deletions
diff --git a/src/workspace/tab.rs b/src/workspace/tab.rs index 5cc35b68..6f0fa75e 100644 --- a/src/workspace/tab.rs +++ b/src/workspace/tab.rs @@ -205,6 +205,7 @@ impl Tab { self.custom_name = Some(name); } + #[cfg(test)] pub fn split_focused( &mut self, direction: Direction, @@ -217,7 +218,9 @@ impl Tab { shell_config: crate::pane::PaneShellConfig<'_>, launch_env: &PaneLaunchEnv, ) -> std::io::Result<NewPane> { - self.split_focused_with_runtime( + self.split_pane_with_runtime( + self.layout.focused(), + true, direction, None, rows, @@ -232,34 +235,6 @@ impl Tab { ) } - pub fn split_focused_with_ratio( - &mut self, - direction: Direction, - ratio: f32, - rows: u16, - cols: u16, - cwd: Option<PathBuf>, - scrollback_limit_bytes: usize, - host_terminal_theme: crate::terminal_theme::TerminalTheme, - host_terminal_appearance: Option<crate::terminal_theme::HostAppearance>, - shell_config: crate::pane::PaneShellConfig<'_>, - launch_env: &PaneLaunchEnv, - ) -> std::io::Result<NewPane> { - self.split_focused_with_runtime( - direction, - Some(ratio), - rows, - cols, - cwd, - scrollback_limit_bytes, - host_terminal_theme, - host_terminal_appearance, - shell_config, - launch_env, - None, - ) - } - pub fn split_focused_command( &mut self, direction: Direction, @@ -272,7 +247,9 @@ impl Tab { host_terminal_theme: crate::terminal_theme::TerminalTheme, host_terminal_appearance: Option<crate::terminal_theme::HostAppearance>, ) -> std::io::Result<NewPane> { - self.split_focused_with_runtime( + self.split_pane_with_runtime( + self.layout.focused(), + true, direction, None, rows, @@ -290,37 +267,51 @@ impl Tab { ) } - pub fn split_focused_argv_command( + /// Split `target` with a shell pane. Focus moves to the new pane only when + /// `focus_new_pane` is set; a spawn failure rolls the layout back without + /// touching focus or its history. + #[allow(clippy::too_many_arguments)] + pub(crate) fn split_pane_shell( &mut self, + target: PaneId, + focus_new_pane: bool, direction: Direction, + ratio: Option<f32>, rows: u16, cols: u16, cwd: Option<PathBuf>, - argv: &[String], - launch_env: &PaneLaunchEnv, scrollback_limit_bytes: usize, host_terminal_theme: crate::terminal_theme::TerminalTheme, host_terminal_appearance: Option<crate::terminal_theme::HostAppearance>, + shell_config: crate::pane::PaneShellConfig<'_>, + launch_env: &PaneLaunchEnv, ) -> std::io::Result<NewPane> { - self.split_focused_with_runtime( + self.split_pane_with_runtime( + target, + focus_new_pane, direction, - None, + ratio, rows, cols, cwd, scrollback_limit_bytes, host_terminal_theme, host_terminal_appearance, - crate::pane::PaneShellConfig::new("", crate::config::ShellModeConfig::NonLogin), + shell_config, launch_env, - Some(SplitCommand::Argv { argv, launch_env }), + None, ) } - pub fn split_focused_argv_command_with_ratio( + /// Split `target` with an argv-command pane. Same focus contract as + /// `split_pane_shell`. + #[allow(clippy::too_many_arguments)] + pub(crate) fn split_pane_argv( &mut self, + target: PaneId, + focus_new_pane: bool, direction: Direction, - ratio: f32, + ratio: Option<f32>, rows: u16, cols: u16, cwd: Option<PathBuf>, @@ -330,9 +321,11 @@ impl Tab { host_terminal_theme: crate::terminal_theme::TerminalTheme, host_terminal_appearance: Option<crate::terminal_theme::HostAppearance>, ) -> std::io::Result<NewPane> { - self.split_focused_with_runtime( + self.split_pane_with_runtime( + target, + focus_new_pane, direction, - Some(ratio), + ratio, rows, cols, cwd, @@ -347,8 +340,10 @@ impl Tab { // Split construction threads geometry, host context, launch policy, and command state. #[allow(clippy::too_many_arguments)] - fn split_focused_with_runtime( + fn split_pane_with_runtime( &mut self, + target: PaneId, + focus_new_pane: bool, direction: Direction, ratio: Option<f32>, rows: u16, @@ -361,10 +356,14 @@ impl Tab { launch_env: &PaneLaunchEnv, command: Option<SplitCommand<'_>>, ) -> std::io::Result<NewPane> { - let previous_focus = self.layout.focused(); - let new_id = match ratio { - Some(ratio) => self.layout.split_focused_with_ratio(direction, ratio), - None => self.layout.split_focused(direction), + let Some(new_id) = self + .layout + .split_pane(target, direction, ratio.unwrap_or(0.5)) + else { + return Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + "split target pane is not in the layout", + )); }; let actual_cwd = cwd.unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| "/".into())); @@ -425,8 +424,7 @@ impl Tab { let runtime = match runtime { Ok(runtime) => runtime, Err(err) => { - self.layout.close_focused(); - self.layout.focus_pane(previous_focus); + self.layout.close_pane(new_id); return Err(err); } }; @@ -437,6 +435,9 @@ impl Tab { } None => TerminalState::new(terminal_id.clone(), actual_cwd), }; + if focus_new_pane { + self.layout.focus_pane(new_id); + } self.panes.insert(new_id, PaneState::new(terminal_id)); self.zoomed = false; Ok(NewPane { @@ -493,14 +494,7 @@ impl Tab { if self.layout.pane_count() > 1 { let next_root = self.promoted_root_if_needed(pane_id); - if self.layout.focused() == pane_id { - self.layout.close_focused(); - } else { - let prev_focus = self.layout.focused(); - self.layout.focus_pane(pane_id); - self.layout.close_focused(); - self.layout.focus_pane(prev_focus); - } + self.layout.close_pane(pane_id); if let Some(next_root) = next_root { self.root_pane = next_root; } @@ -520,10 +514,11 @@ impl Tab { moved: MovedPane, direction: Direction, ratio: f32, + focus: bool, ) -> Result<PaneId, MovedPane> { if !self .layout - .insert_pane_near(target_pane_id, moved.pane_id, direction, ratio) + .insert_pane_near(target_pane_id, moved.pane_id, direction, ratio, focus) { return Err(moved); } @@ -540,14 +535,7 @@ impl Tab { let next_root = self.promoted_root_if_needed(pane_id); - if self.layout.focused() == pane_id { - self.layout.close_focused(); - } else { - let prev_focus = self.layout.focused(); - self.layout.focus_pane(pane_id); - self.layout.close_focused(); - self.layout.focus_pane(prev_focus); - } + self.layout.close_pane(pane_id); let pane = self.panes.remove(&pane_id)?; let terminal_id = pane.attached_terminal_id; |
