Docs

Browser Find-in-Page — Delivery Notes

⌘F did nothing useful on a browser tab: Pulsar’s find-and-replace searches a TextEditor buffer, and a live web page has no buffer. This shipped a real browser find driven by Electron’s native <webview>.findInPage(), surfaced as an independent draggable pane item and styled from the business design tokens. The decision record is ADR-0009; this note is the delivery write-up — what shipped, the false starts, and the gotchas.

Status

Implemented and verified live. Branch feat/browser-find (in both tranquil-browser and tranquil-client); uncommitted at time of writing.

PieceState
BrowserFindItem — draggable pane item, native <webview> find
⌘F routing (workspace keymap + abortKeyBinding, webview keyHandler)
⌘F menu-accelerator leak suppressed (interceptor + eager activation)
⌘G / ⇧⌘G next/previous; clickable search icon
Design-token styling (both business themes) + terminal find palette themed
Palette entries: find, toggle-url-bar, go-back, go-forward (focus-independent)
Stale-highlight clearing on tab switch (flicker-free on click)

The behavior wanted

“⌘F on a browser tab → find text on the page, like any browser.” Pulsar can’t do it: find-and-replace only searches a TextEditor, its extension hooks require a real editor, and it’s third-party core we never modify. Electron’s <webview> element, though, exposes a native find on the host side — findInPage(text, opts), stopFindInPage(action), and the found-in-page event — which highlights in the live page with no guest injection or IPC. That’s the engine.

What shipped

A single owned pane item, tranquil-browser/lib/browser-find-item.js:

  • An independent, draggable workspace item (BrowserFindItem) — plain DOM via getElement(), getDefaultLocation() → "right", getAllowedLocations() = all docks + center, a search icon, and input + counter + previous/next/close controls. Registered with an opener + singleton; showFind() opens it in the right dock (or focuses the existing one). Drag it anywhere.
  • Targets the active browser’s <webview>getCenter().getActivePaneItem() (with a lastBrowser fallback), calling wv.findInPage(...) and listening for found-in-page to update the “N of M” counter.
  • Styled from the design tokens — geometry in tranquil-browser/styles/tranquil-browser.less, colors in the business themes’ tranquil-browser.less (@app-bg, @border-strong, @accent, @text-*). See the token gotcha below.

The false starts (and why they were wrong)

  1. A fixed addBottomPanel strip. First cut. It didn’t fit the theme, sat below the cmd+j dock rather than in it, and its icons didn’t render. Replaced by the pane item.
  2. Opening Find as a sibling tab in the browser’s own pane. A pane shows one tab at a time, so Find hid the browser — and hiding a <webview> detaches it, which both blanked the page you were searching and threw The WebView must be attached to the DOM…. Find must live in a separate pane (the dock), so the browser stays visible.
  3. Reusing find-and-replace via getEmbeddedTextEditor. It needs a real TextEditor buffer, so it can’t search or highlight a live DOM. Dead end.

Gotchas worth remembering

URI scheme collision. The browser’s own URL opener claims any URI starting with tranquil-browser:, so atom.workspace.open("tranquil-browser://find") was turned into a blank browser tab (a webview loading a bogus URL — the source of an empty “find” tab and a webview crash). Fixed by using a distinct scheme: tranquil-find://find.

The ⌘F menu-accelerator leak. Electron menu accelerators fire even when a <webview> has focus, so the “Find in Buffer” menu item still dispatched find-and-replace:show onto browser tabs — showing the (inert) editor find bar on top of ours. Fixes:

  • a workspace-level tranquil-browser:find keymap that beats find-and-replace’s .platform-darwin binding and calls abortKeyBinding() for non-browser items (editor find still works);
  • the webview keyHandler handles the guest-focused case (a focused page swallows ⌘F, forwarded over the existing key relay);
  • an onDidDispatch interceptor that hides find-and-replace’s panel on browser tabs; and
  • eagerly activating find-and-replace at startup (it’s lazily loaded via activationCommands), so the first ⌘F’s show + our hide land in the same tick — no flash.

Electron’s findNext contract is counter-intuitive. findInPage(text, { findNext: true }) selects/advances the active match and reports the count; findNext: false only highlights matches without selecting one, so found-in-page never reports an ordinal and the counter stayed empty. We pass findNext: true always (as the reference electron-find does) — Electron treats a changed query as a new search and an unchanged query as “advance”, so one flag covers the first find and stepping (⌘G / ⇧⌘G).

@ui-variables are default-Atom grays. The active UI theme (tranquil-business-*) doesn’t remap Atom’s @ui-variables; it styles components directly with the design tokens in colors.less. So styling the find pane (or the terminal find palette) with @ui-variables looked wrong. Colors live in the business themes using the real tokens; the geometry stays in tranquil-browser.

Clearing highlights across display:none tabs. Pulsar hides an inactive pane item with display:none (pane-element.js hideItemView), and Electron refuses findInPage/stopFindInPage on a hidden webview — so a tab keeps its find highlight when you switch away, and there’s no will-change-active-pane-item event to clear it before it hides. The only moment the outgoing tab is still visible/clearable is the tab’s mousedown (fires before the pane switches). So:

  • a capture-phase mousedown on the tab bar clears the current tab’s highlight pre-switch → tab-click switches are flicker-free;
  • an on-arrival clear (in observeActivePaneItem) is the fallback for non-click switches (Ctrl+Tab) — those still show a one-frame flash, since there’s no pre-visible signal.

Also swept up

  • ⌘G / ⇧⌘G bound (scoped to the find pane) to tranquil-browser:find-next / :find-previous; the search icon is clickable and runs the find like Enter.
  • Command-palette visibility fix. toggle-url-bar, go-back, go-forward were registered on narrow selectors (.tranquil-browser [webview]), so they only appeared in the palette while a browser view was focused. Moved to atom-workspace scope (targeting the center’s active browser) so they’re always listed.
  • Terminal find palette themed. The terminal package paints its find bar with @tool-panel-background-color (default-Atom light gray → unreadable on the dark UI); repainted from the business palette in the themes’ terminal.less (theme stylesheets outrank package ones, no !important needed for the background).

Files changed

Repo / fileChange
tranquil-browser/lib/browser-find-item.jsNewBrowserFindItem pane item + native find engine
tranquil-browser/lib/tranquil-browser.jsOpener + singleton, showFind, ⌘F command + abortKeyBinding, onDidDispatch interceptor, eager-activate find-and-replace, workspace-scoped toggle-url-bar/go-back/go-forward
tranquil-browser/lib/tranquil-browser-view.jskeyHandler case "f"; removed dead #find input and the per-view nav-command registrations
tranquil-browser/keymaps/tranquil-browser.json⌘F/Ctrl-F → find; ⌘G/⇧⌘G → next/previous (scoped to the find pane)
tranquil-browser/styles/tranquil-browser.lessFind-pane layout/geometry; removed the old bottom-bar + dead .find-bc styles
tranquil-browser/lib/browser-find-bar.js, resources/electron-find/, on-find IPCDeleted — the old bottom-panel bar and the dormant/broken guest find widget
`tranquil-business-darklight/styles/tranquil-browser.less`
`tranquil-business-darklight/styles/terminal.less`

How it was verified

Live in a running window: ⌘F opens the Find pane in the right dock (opening the dock if closed); type + Enter highlights all matches in the page and shows “1 of N”; ⌘G / ⇧⌘G step through; the search icon runs the find; the pane drags to other panes/docks; the palette lists Tranquil Browser: Find (and the nav commands regardless of focus); the first ⌘F after a reload no longer flashes the editor find bar; the terminal find palette reads correctly in tranquil-business-dark and -light; and switching browser tabs by click clears the previous tab’s highlight with no flicker.