mirror of
https://github.com/zellij-org/zellij.git
synced 2026-10-12 02:27:24 +00:00
fix(stack): scrollback truncation when adding a stacked pane in list mode (#5403)
When `stacked_pane_list` is on and the user adds a stacked pane, the classic in-grid stack path first collapses the pre-existing pane to `Fixed(1)` and then `focus_pane` fires `reapply_pane_frames`, which sends `resize_pty!` to the collapsed pane at 1 row. The stack-list groupify pass (invoked later during `render`) then extracts the pane to `suppressed_panes` and resizes it back to the visible size, issuing a second `resize_pty!`. The two rapid WINCH events cause the shell (nushell, pwsh, bash, …) inside the pane to redraw its prompt twice, overwriting previously-visible rows of output. The grid data is preserved in `lines_above`, but the visible viewport ends up mostly blank — the user sees only the pre-existing rows that had already scrolled off, giving the impression of a truncated buffer. Sync stacked-pane-list mode right after the classic mutation, before `focus_pane`. `groupify_all` moves the collapsed member into `suppressed_panes` first, so the subsequent `reapply_pane_frames` (which only iterates `tiled_panes`) never sends a 1-row `resize_pty!` to that shell. The pane is then resized once to the visible size via `resize_stack_list_hidden_members`. Regression test asserts that after adding a stacked pane in this mode, the pre-existing pane is in `suppressed_panes` and the new visible member has non-fixed row dimensions — proving groupify happened before any classic `reapply_pane_frames` step. Co-authored-by: divens <[email protected]> Co-authored-by: Aram Drevekenin <[email protected]>
This commit is contained in:
co-authored by
divens
Aram Drevekenin
parent
30c9d2fcb4
commit
6343be42c6
@@ -36,6 +36,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
||||
* fix: properly unbundle plugin wasm executables when stripping them (to assist distro packagers) (https://github.com/zellij-org/zellij/pull/5433)
|
||||
* feat: support OSC133, triple-mouse-click to mark whole command, configurable double-click word boundaries (https://github.com/zellij-org/zellij/pull/5460)
|
||||
|
||||
|
||||
## [0.44.3] - 2026-05-13
|
||||
* fix(windows): bump windows-sys to 0.59 to align manifest with code, fixing source builds via `cargo install`/`cargo binstall` (https://github.com/zellij-org/zellij/pull/5139)
|
||||
* fix: deadlock with --blocking panes (https://github.com/zellij-org/zellij/pull/5152)
|
||||
|
||||
@@ -7258,6 +7258,13 @@ impl Tab {
|
||||
self.tiled_panes
|
||||
.add_pane_to_stack_of_pane_id(pane_id, pane, root_pane_id);
|
||||
self.set_should_clear_display_before_rendering();
|
||||
// Groupify BEFORE the focus/frame re-application so the just-collapsed
|
||||
// in-grid stack member is moved to `suppressed_panes` before
|
||||
// `reapply_pane_frames` sends a `resize_pty!` at the 1-row collapsed
|
||||
// size. Without this, the shell inside that pane sees two rapid
|
||||
// WINCH events (1 row → visible-size rows) and redraws its prompt
|
||||
// twice, blanking out the visible rows of previous output.
|
||||
self.sync_stacked_pane_list_mode();
|
||||
if should_focus {
|
||||
self.tiled_panes.expand_pane_in_stack(pane_id);
|
||||
}
|
||||
@@ -7277,6 +7284,10 @@ impl Tab {
|
||||
self.dissolve_stack_lists_for_classic_mutation();
|
||||
self.tiled_panes
|
||||
.add_pane_to_stack_of_active_pane(pane_id, pane, client_id);
|
||||
// See comment in `add_stacked_pane_to_pane_id` — groupify before
|
||||
// focusing so the collapsed member is suppressed before
|
||||
// `reapply_pane_frames` fires its intermediate `resize_pty!`.
|
||||
self.sync_stacked_pane_list_mode();
|
||||
if should_focus {
|
||||
self.tiled_panes.focus_pane(pane_id, client_id);
|
||||
}
|
||||
|
||||
@@ -13134,6 +13134,96 @@ fn test_ctrl_scroll_up_merging_stacks_preserves_all_panes() {
|
||||
assert_snapshot!(snapshot);
|
||||
}
|
||||
|
||||
// Regression: when stacked_pane_list mode is active, adding a stacked pane
|
||||
// used to send the pre-existing pane an intermediate `resize_pty!` at 1 row
|
||||
// (from the classic in-grid collapse) before groupify_all moved it into
|
||||
// suppressed_panes and resized it back to the visible size. Shells (nushell,
|
||||
// pwsh, bash) redraw their prompt on each WINCH, which blanks out
|
||||
// previously-visible rows of output — visible to the user as "truncated
|
||||
// scrollback" when they focus back to that pane.
|
||||
//
|
||||
// The fix syncs stacked_pane_list mode immediately after the classic in-grid
|
||||
// mutation (before focus/frame reapplication), so the collapsed pane is
|
||||
// promoted to a stack-list suppressed pane before any resize_pty at the
|
||||
// 1-row size can leak out to its shell. This test pins that end-state:
|
||||
// after adding a stacked pane, the pre-existing pane must have been moved
|
||||
// into suppressed_panes (indicating groupify_all ran), rather than left in
|
||||
// tiled_panes at rows=1 waiting for the next render.
|
||||
#[test]
|
||||
fn adding_stacked_pane_in_stack_list_mode_moves_existing_pane_to_suppressed_immediately() {
|
||||
let size = Size {
|
||||
cols: 121,
|
||||
rows: 40,
|
||||
};
|
||||
let client_id = 1;
|
||||
let mut tab = create_new_tab_with_stacked_pane_list(size, ModeInfo::default(), true, None);
|
||||
|
||||
// Add a second tiled pane so we have a real stack candidate.
|
||||
let existing_pane_id = PaneId::Terminal(2);
|
||||
tab.new_pane(
|
||||
existing_pane_id,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
true,
|
||||
NewPanePlacement::default(),
|
||||
Some(client_id),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Sanity: pane 2 is a tiled pane and no stack lists exist yet.
|
||||
assert!(tab.tiled_panes.get_pane(existing_pane_id).is_some());
|
||||
assert!(!tab.has_stack_lists());
|
||||
assert!(!tab.suppressed_panes.contains_key(&existing_pane_id));
|
||||
|
||||
// Now add a stacked pane on top of the active pane (pane 2).
|
||||
let stacked_pane_id = PaneId::Terminal(3);
|
||||
tab.new_pane(
|
||||
stacked_pane_id,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
true,
|
||||
NewPanePlacement::Stacked {
|
||||
pane_id_to_stack_under: None,
|
||||
borderless: None,
|
||||
},
|
||||
Some(client_id),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// After the operation, stacked_pane_list mode should have picked up the
|
||||
// new stack: pane 2 (the pre-existing member) must be in suppressed_panes,
|
||||
// and the visible member (pane 3) must be in tiled_panes with a non-fixed
|
||||
// row dimension (i.e., not the collapsed 1-row tab-strip form).
|
||||
assert!(
|
||||
tab.has_stack_lists(),
|
||||
"stacked_pane_list mode should have groupified the new stack immediately"
|
||||
);
|
||||
assert!(
|
||||
tab.suppressed_panes.contains_key(&existing_pane_id),
|
||||
"pane 2 (pre-existing pane) should be in suppressed_panes after the \
|
||||
stacked pane is added; if it is still in tiled_panes at rows=1 the \
|
||||
next `reapply_pane_frames` will send a resize_pty to its shell at \
|
||||
the collapsed size, blanking its viewport"
|
||||
);
|
||||
assert!(
|
||||
tab.tiled_panes.get_pane(stacked_pane_id).is_some(),
|
||||
"the newly-added stacked pane should be the visible tiled pane"
|
||||
);
|
||||
let visible_geom = tab
|
||||
.tiled_panes
|
||||
.get_pane(stacked_pane_id)
|
||||
.unwrap()
|
||||
.position_and_size();
|
||||
assert!(
|
||||
!visible_geom.rows.is_fixed(),
|
||||
"the visible member of the stack must not be at fixed(1) rows"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ctrl_scroll_down_in_stack_dissolves_stack_lists_before_mutation() {
|
||||
let size = Size {
|
||||
|
||||
Reference in New Issue
Block a user