sinelaw/fresh · error

active window must have a populated split layout

Error message

active window must have a populated split layout

What it means

Panic from `.expect("active window must have a populated split layout")` in `close_buffer_internal` (buffer_close.rs:132). After resolving which buffer to close, the code fetches the active window's split manager to find the active split for the replacement buffer. The expect asserts two invariants: the active window exists AND its `buffers.splits()` layout is populated. It panics when either the window is gone or the split tree is empty/uninitialized at that point.

Solutions

  1. Handle the None case explicitly: if there is no active split layout, fall back to a plain buffer close without replacement bookkeeping.
  2. Guarantee every window always has a populated split layout (initialize splits at window creation; only dismantle after close completes).
  3. Defer the replacement/split-update logic until after confirming the window and its splits are alive.
  4. Add an assertion/test that `buffers.splits()` is Some whenever a buffer close is processed.

Example fix

// before
.and_then(|w| w.buffers.splits())
.map(|(mgr, _)| mgr)
.expect("active window must have a populated split layout")
.active_split()
// after
let Some(active_split) = self
    .windows
    .get(&self.active_window)
    .and_then(|w| w.buffers.splits())
    .map(|(mgr, _)| mgr)
    .map(|mgr| mgr.active_split())
else {
    tracing::debug!("no active split layout; skipping replacement targeting");
    return self.close_without_replacement(id);
};
Defensive patterns

Strategy: validation

Validate before calling

if app.windows.get(&app.active_window).and_then(|w| w.buffers.splits()).is_none() {
    // no split layout: fall back to a simple close without replacement logic
    return app.close_without_replacement(id);
}

Type guard

fn active_split_manager(app: &App) -> Option<&SplitManager> {
    app.windows.get(&app.active_window)
        .and_then(|w| w.buffers.splits())
        .map(|(mgr, _)| mgr)
}

Try / catch

if std::panic::catch_unwind(AssertUnwindSafe(|| app.close_buffer_internal(id))).is_err() {
    tracing::error!("close_buffer_internal panicked: missing split layout");
}

Prevention

When it happens

Trigger: `close_buffer`, `force_close_buffer`, or `close_tab_keeping_editor_leaf_empty` leads to `close_buffer_internal` while `self.windows.get(&self.active_window)` is None or `w.buffers.splits()` returns None (window with no split layout yet, e.g. a freshly created or torn-down window).

Common situations: Force-closing buffers during window close/quit flow before splits are built; closing the last tab in a window whose split tree was already dismantled; UI close events arriving after window teardown.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of sinelaw/fresh@67894ca546 (2026-09-13). Data as JSON: /api/errors/67b68d8d48f2532d. Report an issue: GitHub.

Appendix: source

Thrown at crates/fresh-editor/src/app/buffer_close.rs:132

        // If closing a terminal buffer, tear down its terminal-side state.
        // Removing the entry drops the buffer's remembered mode with it.
        if let Some(tb) = self.active_window_mut().terminal_buffers.remove(&id) {
            self.cleanup_closed_terminal(id, tb.terminal_id);
        }

        // Capture before resolving the replacement: the last-resort
        // `new_buffer()` path calls `set_active_buffer`, which would change
        // `active_buffer()` out from under this check.
        let closing_active = self.active_buffer() == id;

        // The split the replacement lands in.
        let active_split = self
            .windows
            .get(&self.active_window)
            .and_then(|w| w.buffers.splits())
            .map(|(mgr, _)| mgr)
            .expect("active window must have a populated split layout")
            .active_split();

        let CloseReplacement {
            buffer: replacement_buffer,
            created_empty: created_empty_buffer,
            return_to_group,
        } = self.resolve_close_replacement(id, active_split, force_empty_placeholder);

        // Switch to replacement buffer BEFORE updating splits.
        // Only needed when the closing buffer is the one the user is
        // looking at — otherwise the current active buffer stays.
        if closing_active {
            self.set_active_buffer(replacement_buffer);

            // If we landed on a hidden panel buffer to fill the Group-case
            // housekeeping slot, scrub the *visible* side effects
            // (`open_buffers`, `focus_history`) so the panel buffer doesn't
            // appear as a tab. The `keyed_states` entry `switch_buffer`

View on GitHub (pinned to 67894ca546)