sinelaw/fresh · error

has_other_tab

Error message

has_other_tab

What it means

Panic from `.expect("has_other_tab")` in `close_tab_in_split` (crates/fresh-editor/src/app/buffer_close.rs:765). After removing the closing target from `targets`, the code assumes at least one other tab remains in the split to activate; when the closed tab was the only distinct target, `find` returns None and the assert fires.

Solutions

  1. Check `targets.len() > 1` / existence of a non-closing target before entering the replacement-selection branch and route single-tab splits to the split-close path
  2. Guard with `if let Some(next) = ... else { return }` and close the split instead of panicking
  3. Ensure the earlier `handle_close_split` branch catches all single-target splits so this line is only reached with leftovers
  4. Add a unit test: close the sole tab of a split

Example fix

// before
*targets.iter().find(|t| **t != closing).expect("has_other_tab")
// after
match targets.iter().find(|t| **t != closing) {
    Some(t) => *t,
    None => return self.handle_close_split(split_id.into()),
}
Defensive patterns

Strategy: type-guard

Validate before calling

let other = targets.iter().find(|t| *t != closing);
if other.is_none() { /* route to handle_close_split instead of continuing */ }

Type guard

fn replacement_target<'a>(targets: &'a [TabTarget], closing: &TabTarget) -> Option<&'a TabTarget> {
    targets.iter().find(|t| *t != closing)
}

Try / catch

match targets.iter().find(|t| *t != closing) { Some(t) => activate(t), None => close_split(split_id) }

Prevention

When it happens

Trigger: Closing a tab whose split contains only that one target while the earlier `closing_idx == 0 && targets.len() == 1` branch is reached (i.e. `targets` filtered to `!= closing` is empty).

Common situations: Closing the last remaining tab in a split where the tab-closing logic already removed the split (ordering mismatch with the `split_tabs.len() <= 1` branch); duplicated TabTarget entries that make `position`/dedupe logic select an unexpected index; calling close on an already-closing buffer.

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/400f68024aff37db. Report an issue: GitHub.

Appendix: source

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

            if !has_other_tab {
                // This is genuinely the only tab in this split — close it.
                self.handle_close_split(split_id.into());
                self.sync_terminal_mode_to_active_buffer();
                return true;
            }

            // Pick the tab to activate after removal: the one before the
            // closed tab (or the next one if we closed the first). This
            // mirrors the previous buffer-only behaviour but can also land
            // on a remaining group tab.
            let replacement = if closing_idx > 0 {
                targets[closing_idx - 1]
            } else {
                // First remaining target after the closed one.
                *targets
                    .iter()
                    .find(|t| **t != closing)
                    .expect("has_other_tab")
            };

            // Activate the replacement tab and drop the closed one. The buffer
            // case must move the split tree AND the `SplitViewState.active_buffer`
            // together: routing it through `set_pane_buffer` (not the tree-only
            // `set_split_buffer`) is the fix for the cursor desync — updating
            // only the tree stranded the view-state on the just-closed buffer,
            // so the cursor and render read its zeroed view-state while edits
            // applied to the tree's (different) buffer.
            match replacement {
                TabTarget::Buffer(replacement_buffer) => {
                    self.active_window_mut()
                        .set_pane_buffer(split_id, replacement_buffer);
                    // The replacement is active now, so removing the closed
                    // buffer also frees its keyed view-state (`remove_buffer`
                    // refuses to drop the state of whatever is still active).
                    if let Some(view_state) = self
                        .windows

View on GitHub (pinned to 67894ca546)