Skip to content

fix(search): stop lingering highlights on document switch and 1→0 clear - #17

Merged
nprudhomme merged 1 commit into
ekino:mainfrom
padupuy:fix/search-lingering-highlights
Jul 2, 2026
Merged

nprudhomme merged 1 commit into
ekino:mainfrom
padupuy:fix/search-lingering-highlights

Conversation

@padupuy

@padupuy padupuy commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Problem

Two distinct causes of stale search highlights that survived #13:

  1. Ghost highlights on document switch. reset() (called by loadFile after re-rendering the new document) never resynced its internal query with the search field. Switching documents re-ran the previous term on the freshly rendered content — even when the field had been emptied — leaving yellow highlights with an empty search box.

  2. One glyph lingers on 1 char → 0. On the CSS Custom Highlight path (WKWebView), removing the last highlight from CSS.highlights does not always repaint the affected glyph. Most visible when the query goes from a single character to empty. The JS state and registry are correctly cleared (proven by a test through the real input-event + debounce path) — this is a WebKit repaint quirk.

Fix

  • reset() now reads ui.input.value (the input is the source of truth in the app; the debounce reads it too), so a new document never re-highlights a stale query.
  • forceHighlightRepaint() nudges an imperceptible container opacity toggle + reflow after clearing highlights on the CSS-highlights path, invalidating the stale WebKit paint.

Tests

  • reset() drops a stale query once the field is empty (document switch)
  • clears highlights when a 1-char query is deleted to empty via input events (real event path, CSS-highlight environment)
  • Existing reset test aligned to real usage (input as source of truth)

Full suite: 295 passing, tsc --noEmit clean.

🤖 Generated with Claude Code

Two distinct causes of stale search highlights:

- reset() (called by loadFile after re-rendering) never resynced its
  internal query with the input field, so switching documents re-ran the
  previous term on the freshly rendered content — even when the field was
  empty. It now reads ui.input.value, the source of truth.

- On the CSS Custom Highlight path (WKWebView), removing the last
  highlight from the registry does not always repaint the glyph, most
  visibly when the query goes from 1 char to 0. forceHighlightRepaint()
  nudges an imperceptible container repaint after clearing.

Adds regression tests for both, including one through the real
input-event + debounce path.
@nprudhomme
nprudhomme merged commit d17b397 into ekino:main Jul 2, 2026
4 checks passed
@nprudhomme nprudhomme mentioned this pull request Jul 3, 2026
7 tasks
nprudhomme added a commit that referenced this pull request Aug 13, 2026
Bump version to 0.11.0 and document the user-facing changes since 0.10.0:
native File/View menus with Open Recent (#19), fast document open with
in-memory cache, cloud-sync UX and a debug HUD (#18), and a search
highlight fix on document switch / 1->0 clear (#17).
nprudhomme added a commit that referenced this pull request Aug 13, 2026
* chore(release): v0.11.0

Bump version to 0.11.0 and document the user-facing changes since 0.10.0:
native File/View menus with Open Recent (#19), fast document open with
in-memory cache, cloud-sync UX and a debug HUD (#18), and a search
highlight fix on document switch / 1->0 clear (#17).

* fix(ui): stop transparent modal backdrops from locking the window

A modal backdrop could be left in `display: flex` while fully transparent,
covering the whole window at z-index 2000. It swallowed every click and
scroll while staying invisible, and because the close path had already
detached its listeners, the user had no way to dismiss it — the app was
unusable until restart. Reported on 0.10.0: no clicks on Open Folder, no
file selection in the sidebar, no scrolling, while the native menu bar
still worked.

The backdrops were shown and hidden through deferred callbacks. WebKit
suspends both animation frames and timers while a window is occluded, so
neither the reveal nor the hide is guaranteed to run — leaving the layer
displayed but never marked `visible`.

Make interactivity follow opacity in CSS: an overlay without `.visible`
is now click-through and hidden from the tab order and the accessibility
tree. That is the load-bearing guarantee — it holds whatever the reason
the layer got stuck, including a keyboard user reaching a destructive
button inside an invisible dialog.

Defence in depth on top of it:
- reveal synchronously (forced reflow, then class) instead of from a
  frame callback, so a dialog opened on an occluded window is still
  visible and therefore dismissable;
- track an open generation so a pending hide never acts on a backdrop a
  newer open has claimed, in both confirm-dialog and Preferences;
- focus the trap immediately, since callers now reveal before trapping.

Tests cover the two suspension modes that produce the field state:
frames never delivered, and timers never delivered.

Claude-Session: https://claude.ai/code/session_01NcJuB2iR4WEfuQVLpYqWi4

* docs(changelog): record multi-window support in 0.11.0

PR #22 landed without a changelog entry. Covers the new windows, the
native Window menu, the arrangement commands, and the switch from
broadcast to frontmost-window menu delivery.

Claude-Session: https://claude.ai/code/session_01NcJuB2iR4WEfuQVLpYqWi4

* docs(changelog): set the 0.11.0 release date

The heading still carried the date the release branch was prepared.

Claude-Session: https://claude.ai/code/session_01NcJuB2iR4WEfuQVLpYqWi4
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.

2 participants