Skip to content

fix(tree): stop focus snapping to open file (#17); never strand focus on hidden tree (#16)#20

Merged
leboiko merged 2 commits into
masterfrom
fix/file-tree-focus-stability
Jun 15, 2026
Merged

fix(tree): stop focus snapping to open file (#17); never strand focus on hidden tree (#16)#20
leboiko merged 2 commits into
masterfrom
fix/file-tree-focus-stability

Conversation

@leboiko

@leboiko leboiko commented Jun 15, 2026

Copy link
Copy Markdown
Owner

Summary

Two related file-tree focus-correctness bugs.

#17 — Tree cursor snaps back to the open file (user-reported)

A user on Chrostini reported that with a file open, the left tree always shifts back to the opened file's index after ~1 second, making it impossible to navigate to or open a different file.

Root cause: the filesystem watcher fires Action::TreeDiscovered on every change — and on noisy filesystems (Chrostini inotify emits spurious IN_ACCESS) that's roughly once a second. The handler then unconditionally called reveal_path(active_file), yanking the user's tree selection back to the open file on every refresh.

Fix:

  • FileTreeState::rebuild now preserves the selection by path across a refresh (robust to sibling rows being added/removed by whatever change triggered the watcher), clamping to the old index when the path is gone.
  • TreeDiscovered only aligns the tree to the open file on the first discovery. Intentional reveals (open file, search jump, link pick) still call reveal_path at their own call sites.

#16 — Focus could be stranded on a hidden file tree

Several handlers set Focus::Tree without checking tree_hidden, landing focus on a pane that isn't rendered. Added a focus_tree_or_viewer() helper and routed the 8 affected sites through it: Tab, last-tab close (key + mouse), search-Esc, copy-menu Enter/Esc, FocusLeft, ExitSearch. The tree-click mouse site is intentionally left alone — it's only reachable when the tree is rendered.

Testing

  • [Bug] #17: a failing reproduction test written first (confirmed it snapped back to /fake/test.md), now asserts the user's selection survives a second TreeDiscovered while the first still aligns to the open file — guarding both directions so a no-op can't pass.
  • Focus can be stranded on a hidden file tree (missing tree_hidden guards) #16: per-entry-point redirect tests, each asserting both visible→Tree and hidden→Viewer.
  • Full workspace suite green (0 failures); cargo fmt --check, cargo clippy -D warnings, cargo deny check all clean.

Closes #17
Closes #16

🤖 Generated with Claude Code

…ocus on hidden tree

Two related file-tree focus-correctness bugs.

#17 — Tree cursor snaps back to the open file (~1×/sec on noisy
filesystems). The watcher fires `TreeDiscovered` on every filesystem
change, and the handler unconditionally re-revealed the active tab's
file, yanking the user's selection back and making it impossible to
navigate to or open a different file (reported on Chrostini, whose
inotify emits spurious IN_ACCESS events).
  - `FileTreeState::rebuild` now preserves the selection by path across
    a refresh (robust to siblings being added/removed), clamping to the
    old index when the path is gone.
  - `TreeDiscovered` only aligns the tree to the open file on the first
    discovery. Intentional reveals (open, search jump, link pick) keep
    calling `reveal_path` at their own sites.

#16 — Focus could land on a hidden file tree. Several handlers set
`Focus::Tree` without checking `tree_hidden`, stranding focus on an
unrendered pane. Added `focus_tree_or_viewer()` and routed the 8
affected sites (Tab, last-tab close, search-Esc, copy-menu Enter/Esc,
FocusLeft, ExitSearch, mouse tab-close) through it. The tree-click site
is left alone — it is only reachable when the tree is rendered.

Tests: failing reproduction for #17 (asserts the user's selection
survives a second TreeDiscovered while the first still aligns to the
open file); per-entry-point redirect tests for #16 asserting both
directions (visible→Tree, hidden→Viewer). Full workspace suite green;
fmt/clippy/deny clean.

Closes #17
Closes #16

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@leboiko

leboiko commented Jun 15, 2026

Copy link
Copy Markdown
Owner Author

🔍 Senior-engineer review (3 parallel agents: architecture / quality / testing)

Reviewed 02b1a0e. No 🔴 critical issues. Findings below, with disposition — all actionable items fixed in the follow-up commit.

Verified NOT a problem (false alarm)

  • Two agents flagged the first_discovery = selected_path().is_none() heuristic as breaking startup alignment. Traced the actual flow: with a visible tree, App::new populates synchronously and TreeDiscovered never fires at startup — alignment to the open file comes from run()expand_and_select()reveal_path() (mod.rs:838), not from TreeDiscovered. So no startup regression. ✅

🟠 Major — fixed

  • Heuristic was non-obvious and misfired on a real edge. Replaced selected_path().is_none() with an explicit FileTreeState.aligned latch that reveal_path sets. This is clearer and closes the "all tree files deleted then recreated" edge, where the transient empty selection would have re-triggered a forced realign (a mini-[Bug] #17).
  • Mouse tab-close path (close_hit) was changed but untested. Added mouse_last_tab_close_respects_hidden_tree.

🟡 Minor — fixed

  • Stale-tree_area_rect mouse path: the tree_hidden render branch never clears tree_area_rect, so a click at stale coords could strand focus on the hidden tree (Focus can be stranded on a hidden file tree (missing tree_hidden guards) #16 via mouse). Added a !tree_hidden guard to the tree-click handler and routed it through focus_tree_or_viewer(). Now all literal Focus::Tree assignments route through the helper, making its doc-comment invariant true.
  • Missing branch coverage in rebuild: added tests for the index-clamp branch (selected path deleted → clamp to last row), the empty-tree branch (→ clears selection), selection-follows-path-when-siblings-shift, expanded-state preserved across rebuild, and reveal_path latching aligned only on a real match.
  • Doc-comment clarifications on rebuild (first-populate behavior) + clamp-underflow-safety comment; |p| p.to_path_buf() closure form.

Out of scope (intentionally not changed)

  • key_handlers.rs:34 config-popup focus restore uses pre_config_focus (restore-previous semantics, may be Editor/Search) — correctly not routed through focus_tree_or_viewer().

Test delta: 6 → 13 new tests. Full workspace suite green (477 lib tests), fmt/clippy -D warnings/deny all clean.

…ouse hardening, branch coverage

Follow-up to the self-review on PR #20.

- Replace the `first_discovery = selected_path().is_none()` heuristic with an
  explicit `FileTreeState.aligned` latch set by `reveal_path`. Clearer intent
  and closes the "all files deleted then recreated" edge, where a transient
  empty selection would re-trigger a forced realign to the open file (#17).
- Harden the mouse tree-click handler: the `tree_hidden` render branch never
  clears `tree_area_rect`, so a click at stale coordinates could strand focus
  on the hidden tree (#16 via mouse). Add a `!tree_hidden` guard and route the
  assignment through `focus_tree_or_viewer()`. All literal `Focus::Tree`
  assignments now go through the helper.
- Tests: mouse last-tab-close focus redirect; rebuild index-clamp branch
  (selected path deleted), empty-tree branch, selection-follows-path on sibling
  shift, expanded-state preserved across rebuild, `reveal_path` latches only on
  a real match, lazy-discovery aligns a previously-hidden tree, and the
  empty-then-refill no-realign case.
- Doc clarifications on `rebuild` (first-populate behavior, clamp underflow
  safety); closure form for `to_path_buf`.

Full workspace suite green (477 lib tests); fmt/clippy/deny clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@leboiko leboiko merged commit e1d0fa1 into master Jun 15, 2026
12 checks passed
@leboiko leboiko deleted the fix/file-tree-focus-stability branch June 15, 2026 20:26
leboiko added a commit that referenced this pull request Jun 17, 2026
Ships the file-tree focus-correctness fixes from #20:

- #17: tree cursor no longer snaps back to the open file on every
  filesystem change (selection preserved by path across rebuilds;
  TreeDiscovered only aligns on first discovery).
- #16: focus can no longer be stranded on a hidden file tree
  (8 entry points routed through focus_tree_or_viewer()).

Pinned by 10 regression tests. fmt/clippy/test/deny all green locally.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Focus can be stranded on a hidden file tree (missing tree_hidden guards)

1 participant