ADR-0052: One search panel: two engines, three scopes
- Status: Accepted
- Date: 2026-08-19
- Deciders: jka
- Decision: D52
Context
With both an inline find bar (ADR-0036) and a find-in-files panel (ADR-0051), the question was whether to merge them into one "all things search" dialog, as Notepad++ does.
Decision
The two remain separate surfaces, connected by a handoff. The find bar stays the quick in-buffer tool. The find-in-files panel becomes a general Search panel with three scopes:
| Scope | Operation | Reads |
|---|---|---|
| Project | project.search | files on disk |
| Open docs | buffers.search | every open buffer's text |
| Current doc | buffers.search with only | the focused buffer |
The buffer scopes read live text, so they include unsaved edits. Both engines share
one line matcher (rio::doc::grep_lines). Pressing Ctrl+Shift+F in the find bar
carries its needle and options into the panel and widens the scope to Project. The
panel's default scope is Current doc.
Replace works in every scope. Buffer scopes use buffer.replace_all and leave
changes unsaved. Project scope uses project.replace, after a confirmation, and
routes each file by whether it is open: open files are changed through their buffer
(undoable, unsaved), closed files are rewritten on disk with their encoding and line
endings preserved. Disk is never rewritten under an open buffer.
Regular expressions are available on every search and replace operation and on
both surfaces. Patterns are Tcl ARE, matched line by line (^ and $ anchor at line
boundaries). Whole word is disabled while regex is on, since a pattern states its own
boundaries. Replacement text may use back-references. An invalid pattern matches
nothing rather than raising an error, so a half-typed pattern shows no hits. Because
regex matches vary in length, each match row carries a lens array beside cols.
Consequences
- The quick bar and the panel cannot disagree, because they use the same engine.
- The panel lives in the bottom dock site as an ordinary tool window (ADR-0035).
project.replacemust call model functions directly instead of nested dispatch, so its events are not duplicated.