aboutsummaryrefslogtreecommitdiff
path: root/src/workspace
diff options
context:
space:
mode:
authorJon Kinney <jonkinney@gmail.com>2026-08-04 16:35:15 -0500
committerGitHub <noreply@github.com>2026-08-05 00:35:15 +0300
commit09cdd88d0aca35617eb05468c2421b0467656e4f (patch)
tree46ab6bec955c322d7c49148bee2762c502dd8662 /src/workspace
parent93e0086f383f000ada02be382230507d5bdf4d7e (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.rs118
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;