{"record":{"id":"67b68d8d48f2532d","repo":"sinelaw/fresh","slug":"active-window-must-have-a-populated-split-layout-buffer","errorCode":null,"errorMessage":"active window must have a populated split layout","messagePattern":"active window must have a populated split layout","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/fresh-editor/src/app/buffer_close.rs","lineNumber":132,"sourceCode":"\n        // If closing a terminal buffer, tear down its terminal-side state.\n        // Removing the entry drops the buffer's remembered mode with it.\n        if let Some(tb) = self.active_window_mut().terminal_buffers.remove(&id) {\n            self.cleanup_closed_terminal(id, tb.terminal_id);\n        }\n\n        // Capture before resolving the replacement: the last-resort\n        // `new_buffer()` path calls `set_active_buffer`, which would change\n        // `active_buffer()` out from under this check.\n        let closing_active = self.active_buffer() == id;\n\n        // The split the replacement lands in.\n        let active_split = self\n            .windows\n            .get(&self.active_window)\n            .and_then(|w| w.buffers.splits())\n            .map(|(mgr, _)| mgr)\n            .expect(\"active window must have a populated split layout\")\n            .active_split();\n\n        let CloseReplacement {\n            buffer: replacement_buffer,\n            created_empty: created_empty_buffer,\n            return_to_group,\n        } = self.resolve_close_replacement(id, active_split, force_empty_placeholder);\n\n        // Switch to replacement buffer BEFORE updating splits.\n        // Only needed when the closing buffer is the one the user is\n        // looking at — otherwise the current active buffer stays.\n        if closing_active {\n            self.set_active_buffer(replacement_buffer);\n\n            // If we landed on a hidden panel buffer to fill the Group-case\n            // housekeeping slot, scrub the *visible* side effects\n            // (`open_buffers`, `focus_history`) so the panel buffer doesn't\n            // appear as a tab. The `keyed_states` entry `switch_buffer`","sourceCodeStart":114,"sourceCodeEnd":150,"githubUrl":"https://github.com/sinelaw/fresh/blob/67894ca5463dbd7a89bb31add4627c27d6b79d83/crates/fresh-editor/src/app/buffer_close.rs#L114-L150","documentation":"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.","triggerScenarios":"`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).","commonSituations":"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.","solutions":["Handle the None case explicitly: if there is no active split layout, fall back to a plain buffer close without replacement bookkeeping.","Guarantee every window always has a populated split layout (initialize splits at window creation; only dismantle after close completes).","Defer the replacement/split-update logic until after confirming the window and its splits are alive.","Add an assertion/test that `buffers.splits()` is Some whenever a buffer close is processed."],"exampleFix":"// before\n.and_then(|w| w.buffers.splits())\n.map(|(mgr, _)| mgr)\n.expect(\"active window must have a populated split layout\")\n.active_split()\n// after\nlet Some(active_split) = self\n    .windows\n    .get(&self.active_window)\n    .and_then(|w| w.buffers.splits())\n    .map(|(mgr, _)| mgr)\n    .map(|mgr| mgr.active_split())\nelse {\n    tracing::debug!(\"no active split layout; skipping replacement targeting\");\n    return self.close_without_replacement(id);\n};","handlingStrategy":"validation","validationCode":"if app.windows.get(&app.active_window).and_then(|w| w.buffers.splits()).is_none() {\n    // no split layout: fall back to a simple close without replacement logic\n    return app.close_without_replacement(id);\n}","typeGuard":"fn active_split_manager(app: &App) -> Option<&SplitManager> {\n    app.windows.get(&app.active_window)\n        .and_then(|w| w.buffers.splits())\n        .map(|(mgr, _)| mgr)\n}","tryCatchPattern":"if std::panic::catch_unwind(AssertUnwindSafe(|| app.close_buffer_internal(id))).is_err() {\n    tracing::error!(\"close_buffer_internal panicked: missing split layout\");\n}","preventionTips":["Initialize the split layout eagerly at window creation so splits() is never None.","Tear down splits only after all close flows complete.","Handle Option from splits() explicitly with a fallback instead of expect.","Assert split-layout presence in tests covering force_close_buffer and quit-time closes."],"tags":["rust","panic","split-layout","internal-invariant"],"backgroundTag":"internal-invariant-violation","analyzedSha":"67894ca5463dbd7a89bb31add4627c27d6b79d83","analyzedAt":"2026-09-13T15:04:03.701Z","contentChangedAt":"2026-09-13T15:04:03.701Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}