feat: find in page — match count, highlight-all, next/prev#16
Conversation
Replace native WKWebView.find (no count, current-match-only highlight) with a bridge.js find engine over its own filtered text walker (excludes heading anchors, copy buttons, Mermaid SVG, KaTeX). Reports count/index via a new findResult message into AppState.findCount/findIndex. clearFind is sequenced around PDF export so highlights don't bake in.
Show "N of M" / "No results" between the field and the chevrons; disable next/prev when there are zero matches instead of on empty query.
magnifyingglass button before the reload divider, disabled when no file is open, opening the find bar.
Swap the two find shortcuts so in-page find takes the conventional Cmd-F. Update the shortcut cheatsheet and changelog.
findFocus is module-level and survived a document switch, so opening a new file with find active left refind() clamped to the previous file's match index -- and because refind() deliberately does not scroll, the current match landed off-screen. render() already distinguishes the two cases: loadMarkdown passes keepScroll=false, reloadMarkdown and setTheme pass true.
The new topbar find button is gated on selectedFile, but the menu command and its Cmd-F shortcut were not, so Cmd-F with no document open raised an empty find bar over an empty content pane.
Review pass — no correctness blockerEvery high-risk claim in this PR was checked against the DOM spec and survived. That's worth stating plainly, because the two sibling PRs each turned up real defects. Reverse-order wrapping is correct. The reviewer traced One precision note, not a defect: the comment's phrase "leaves the original node as the prefix" is imprecise when
The
Fixed in
|
Integration defect between footnotes and find. The footnote extension emits a visually-hidden <h2 class="sr-only">Footnotes</h2>; it is invisible to the reader but still a text node, so searching "footnotes" matched it, inflating the count and wrapping a <mark> the user cannot see. Neither PR could catch this alone -- the two features only meet after both land.
Implements the find-in-page spec. Covers original request item 1.
Why native find had to go
WKFindConfiguration/WKFindResultexpose onlymatchFound: Bool— no count — and native find highlights just the current match. Any solution keeping it would compute the count in JS anyway, leaving two sources of truth for "which match is current". So find moves intobridge.jsentirely.The crux: find needs its own text base
contentEl.textContentis not what the reader sees. Afterrender(), it also contains:addHeadingAnchors()#, first child of every h1–h4addCodeCopyButtons()Copy, inside every<pre>renderMath()<annotation>MathMLrenderMermaid()<text>nodes inside an inline<svg>Searching that would inflate the count with invisible text, and wrapping a match inside the Mermaid subtree would insert an HTML
<mark>into an<svg>. So find gets its own filtered walker (FIND_EXCLUDE) and a dedicatedwrapFindMatch().The marks feature is deliberately left alone. It is tempting to call
resolveAnchorequally buggy and unify the two walkers. It isn't: a mark's quote/offsets are captured on mouseup, after the same injections have run, so the pollution is symmetric across write and read and cancels out. Marks and find have different correct text bases. Refactoring the anchoring would risk re-anchoring every saved highlight for zero gain.git diffconfirmsresolveAnchor,offsetsFromRange,rangeFromOffsets, andwrapRangeare byte-unchanged;bridge.jsis purely additive (138 insertions, 0 deletions).Two subtleties worth reviewing
surroundContentssplits the text node and leaves the original as the prefix, so wrapping an early match first invalidates the offsets of a later one sharing that node.find()vsrefind(). Re-application after a DOM rebuild (FSEvents re-render, marks re-wrap, PDF export) must keep the current match and not scroll —render()preservesscrollY, and a naive re-find()would fight it, yanking the viewport to match 1 mid-read.refind()preserves focus, clamps the index, and doesn't scroll. Converselyrender()resetsfindFocuswhenkeepScrollis false, so opening a new file restarts at match 1 rather than inheriting the old index.Also:
clearFind()runs beforecreatePDF(otherwise highlights bake into the exported PDF), and ⌘F/⌘⇧F swap — Find in Page is now ⌘F, Filter Files is ⌘⇧F. That's a muscle-memory change; noted inSHORTCUTS.mdandCHANGELOG.md.Verification status
Verified mechanically:
swift buildpasses;swift testgreen;WKFindConfigurationgone from the tree; marks plumbing untouched; both re-apply sites callrefind(), and the onlyfind(call is the user-initiated entry point.Not verified — requires a human at the GUI. All 13 spec verification items are visual. The two that guard the trickiest bugs:
Copywith a code block present → the copy-button label is not counted#with headings present → heading anchors not counted1 of 5/ wrap-around /No results/ disabled chevrons