Repository navigation
How Find and Replace Works
There are two tools: the find and replace bar inside every CodeMirror editor, and Replace in Files, which writes many files on disk from the Text tab of Search Everywhere. The user side is in Find and Replace.
CodeMirror's own search panel has no counter, no Exclude and no Select All Occurrences, and does not match the app. People expect Cmd+R, Option+C, W, X and a "3/12" counter. Replace in Files renames a word across a project without leaving the app.
findBar() in src/lib/editor/findPanel.svelte.ts is part of baseExtensions, so every editor, diff side and merge pane has it. It keeps @codemirror/search for matching, highlighting, Next, Previous and Replace, and swaps only the panel: search({ createPanel }) builds a FindPanel, which mounts FindBar.svelte into the panel element with Svelte's mount. It maps the panel's state onto the UI library's FindBar.
flowchart LR
Keys["Cmd+F, Cmd+R, Edit menu"] --> Open["openFind(view, replace)"]
Open --> Panel["FindPanel (CodeMirror panel)"]
Panel --> Bar["FindBar.svelte"]
Bar -->|"typing, toggles"| Apply["apply: buildQuery, setSearchQuery"]
Apply --> CM["@codemirror/search: highlight, next, replace"]
CM -->|"view update"| Count["countMatches, counterView"]
Count --> Bar
FindBarState holds the fields with runes; the panel writes it and the component reads it, and the panel's update follows the editor's query. Its onkeydown runs the panel's keys instead of the library's.
-
Opening.
openFindseeds the field with the selection when it is one line of at most 500 characters (selectionQuery, escaped when Regex is on). Cmd+F while the bar has focus only selects the field. -
Typing dispatches
setSearchQueryand selects the first match afteranchor, where the caret was before the search moved it (the jump's user eventselect.search.typingleaves the anchor alone). -
The query comes from
buildQueryinfindModel.ts. Without Regex it isliteral, so a typed\nstays two characters. With Regex the replacement understands$1,$&and\n. -
Counting.
countMatcheswalks the matches up toCOUNT_CAP(10,000) and finds the selected one;counterViewturns that into "3/12", "12 results", "0 results" or "Invalid regex". Documents over 200,000 characters count 120 ms after the last change instead of on every key. -
Exclude adds the selected match to
excludedField, a StateField of ranges mapped through edits. Every query gets the same test function,notExcluded, because CodeMirror compares a query's test by identity. Changing the search or closing the bar clears the exclusions. -
Select All Occurrences uses
selectMatcheswhile the bar is open, otherwiseoccurrenceRanges(the selection, or the whole word at the caret, case-sensitive), up toSELECT_CAP(1,000). -
Keys.
findKeymapreplaces CodeMirror'ssearchKeymapwith the same keys plus Cmd+R and Ctrl+Cmd+G. Keys pressed in the bar's fields that the bar does not handle go throughrunScopeHandlers(view, event, "search-panel"), so Cmd+G and the merge tool's Cmd+Enter work from the fields.
Read-only editors get the find row only. In a diff, both sides share one scroller, so each side gets panels({ topContainer }) with a host above the diff (the library's DiffFrame); a bar inside the editor would scroll away and push its side out of line.
The Edit menu's find items take their keys from EDITOR_SHORTCUTS with source: "find". The editor sees a key before the menu, so a key never runs twice. See How editing code works.
The Replace field is part of the Text tab in FileSearch.svelte. A replace is always two calls: a preview that only counts, then the write.
sequenceDiagram
participant P as FileSearch.svelte
participant D as dialogs.confirm
participant R as replace_in_files
participant F as Files on disk
P->>R: preview true, skipPaths = unsaved tabs
R-->>P: replacements, files, skipped
P->>D: "Replace 12 matches in 4 files?" (danger)
D-->>P: confirmed
P->>R: preview false, a new replace id
R->>F: per file: temp file, then rename over it
R-->>P: files, skipped, failed, cancelled
P->>P: toast, repoStore.filesWritten, search again
src-tauri/src/text_search/replace.rs reuses build_matcher from the text search, so a replace changes exactly what the Text tab showed. The replacement compiles once into a Template ($1 groups, $&, $$ and the escapes of unquote, the editor's rules). For each file it searches, builds the new bytes, and writes them with write_atomic: a temporary file next to the original with the same permissions, synced, then renamed over it. If the file's size or modified time changed since it was read, the write is refused and the file is reported as failed.
Files in skipPaths (open tabs with unsaved edits), symbolic links and read-only files are never written; they are reported with their match counts. Binary files and files over 5 MB are skipped like the search skips them. A newer replace_id or replace_in_files_cancel stops between files, so every file is either fully replaced or untouched. The texts of the dialog and the toast come from the pure replaceModel.ts.
After a write, repoStore.filesWritten refreshes the status of the touched repositories, and clean open tabs reload from disk like after any outside change.
| File | What it does |
|---|---|
src/lib/editor/findPanel.svelte.ts |
The panel, its commands (openFind, closeFind, selectAllOccurrences) and findKeymap
|
src/lib/editor/FindBar.svelte, FindBar (UI library) |
The bar's fields and buttons |
src/lib/editor/findModel.ts |
buildQuery, selectionQuery, countMatches, counterView, occurrenceRanges
|
src/lib/ui/SearchToggles.svelte |
The Text tab's Cc, W and .* toggles |
src/lib/diff/diffEditors.svelte.ts, DiffFrame (UI library) |
Find bar hosts above each diff side |
src/lib/search/FileSearch.svelte, searchReplace.svelte.ts
|
The Replace field, the two-step replace |
src/lib/search/replaceModel.ts |
Confirmation and summary texts |
src-tauri/src/text_search/replace.rs |
Templates, the replace run, write_atomic
|
src-tauri/src/commands/search.rs |
replace_in_files, replace_in_files_cancel
|
Keep CodeMirror's search, replace its panel. Matching and replace are well tested in @codemirror/search; createPanel swaps only the UI, without a fork.
Literal by default. The field is plain text unless Regex is on, so \n and $1 mean nothing until you ask for them.
The editor's Replace All does not ask, Replace in Files does. Replace All only changes the editor buffer, and Cmd+Z undoes it. Replace in Files writes to disk and has no undo, so it confirms with danger: true, as the project rules ask for destructive actions.
Count first, then write. The preview gives the dialog exact numbers and the skipped files before anything changes, and the write gets its own id so it can be stopped.
Skip unsaved files instead of editing their buffers. Writing a file with unsaved edits would lose one of the two versions.
Atomic writes. Other programs, the watcher and git see the old file or the new one, never half of it.
-
src/lib/editor/findModel.test.ts: query building, regex errors, selection seeding, counting, the counter text, occurrences, the first match after the anchor (the typing jump) and excluded spans. -
src/lib/editor/editorShortcuts.test.ts: every Edit and Code menu key is bound by the editor to the same command. -
src/lib/search/replaceModel.test.ts: the confirmation, skipped lists and summaries. -
src-tauri/src/text_search/replace/tests.rs: literal and regex replaces, group expansion and escapes, invalid regex, preview, skipping unsaved, binary, big, linked and read-only files, keeping the file mode, refusing a changed file, cancelling and the newest id.
The bar itself is Svelte glue: check it by hand in an editor, a diff (both sides) and the merge tool.
- A new find key:
findKeymap,EDITOR_SHORTCUTS, the Edit menu, the usage page and Keyboard Shortcuts. - Changing the replacement rules: change
TemplateandbuildQuerytogether, so one file and many files replace alike. - Retake
find-replace-bar.pngandreplace-in-files.pngwhen the bar or the Replace row changes.
A replace test failed under load.
-
The issue:
skips_links_and_read_only_filesfailed once in a busy run. - Why it happened: its link pointed at a replaced file. Parallel workers sometimes rewrote the target first, leaving the link no matches.
- The fix and why we chose it: the target now sits outside the searched folder, so timing cannot matter.
These pages are generated from docs/wiki. To correct something, open a pull request against the source file; edits made here are overwritten.
Using Nibtree
- Getting Started
- Welcome Screen
- Workspaces
- Files Panel
- File Operations
- File Icons
- Rounded Panels
- Editor and Tabs
- Pin Reorder and Wrap Tabs
- Single Tab Title
- Split Editor
- New File and Unsaved Changes
- Unload Hidden Tabs
- Editing Code
- Code Appearance
- Markdown Editor
- Image and PDF Preview
- Search Everywhere
- Recent Files
- Navigation Bar
- Find and Replace
- Changes and Commits
- Sign In Prompts
- Commit Box Layout
- Repository Actions
- Commit Options
- Ignoring Files
- Diffs
- Inline Diffs
- Staging View
- Revert a Change
- Resolving Conflicts
- Merge Tool
- Blame
- History and Log
- Merge Commits
- Branches and Tags
- Branches Popup
- Remotes
- Stashes
- Shelf
- Git Menu
- Git Dialogs
- Interactive Rebase
- Worktrees
- AI Agents
- AI Tasks
- AI Usage
- Keep Computer Awake
- Submodules
- Git LFS
- GitHub
- Git Console
- Terminal
- Terminal Features
- Scripts
- Navigation
- Menus
- Settings
- Settings Search
- Color Themes
- Settings Terminal and Automation
- Settings Files
- Updates
- Status Bar and Help
- Notifications
- Memory Use
- Clear Cache
- MCP and CLI
- Git Mergetool
- Keyboard Shortcuts
- Keyboard Shortcuts Editor
- Troubleshooting
- Troubleshooting Git
Developers
- Developer Guide
- Architecture
- Project Layout
- Backend
- How Folder Watching Works
- Folder Watching Bugs We Fixed
- How Workspace Files Work
- How the Preview Scheme Works
- Backend Services
- How Terminal Features Work
- Terminal Bugs We Fixed
- Frontend
- How the Rich Markdown Editor Works
- Rich Markdown Bugs We Fixed
- Git Menu Links
- Menu Keys and Routing
- Frontend Modules
- Blame Bugs We Fixed
- Project Color And Title Bar
- One Diff View
- Diff Bugs We Fixed
- Merge Tool Layouts
- Merge Tool Bugs We Fixed
- Color Theme Bugs We Fixed
- How the Connected Diff Works
- Rounded Panels Bugs We Fixed
- Update Bugs We Fixed
- Status Bar Bugs We Fixed
- MCP and CLI Bugs We Fixed
- Repository Actions Bugs We Fixed
- How the Changes Tab Works
- CodeMirror Patterns
- How the Cursor Works
- How Editor Features Work
- How the Path Bar Works
- Commands and Events
- Settings Reference
- Commands Workspace and Changes
- Commands Branches and Remotes
- Commands History and Extras
- Commands App and Tools
- Testing
- Test Suites
- Debugging
- Debugging with MCP
- Measuring Setting Memory
- How Terminal Memory Is Measured
- How Markdown Diagrams Work
- Releases and CI
- Publishing the Wiki
- Versioning and Changelog
- Browser Version Packages
- Browser Release Checks
- Docker Image
- Platforms and Signing
- Windows Support
- Windows Bugs We Fixed
- Windows MCP and CLI
- Docs and Screenshots
- UI Library
- UI Library MCP
- UI Library Bugs
- UI Library Migration
- Contributing
How features work
- Welcome screen
- Workspaces and folders
- Files panel
- File operations
- File icons
- Editor and tabs
- Pin, reorder and wrap tabs
- Single tab title
- Split editor
- New File and unsaved changes
- Unload hidden tabs
- Editing code
- Code appearance
- Markdown editor
- Image and PDF preview
- Search Everywhere
- Recent Files
- Navigation Bar
- Find and replace
- Changes and commits
- Sign-in prompts
- Commit box layout
- Repository actions
- Commit options
- Ignoring files
- Diffs
- Inline diffs
- Staging view
- Revert a change
- Resolving conflicts
- Merge tool
- Blame
- History and log
- Merge commits
- Commit tabs
- Branches and tags
- Branches popup
- Remotes
- Stashes
- Shelf
- Git menu
- Git dialogs
- Interactive rebase
- Worktrees
- AI agents
- AI usage
- Keep computer awake
- AI tasks
- Submodules
- Git LFS
- GitHub account
- Git Console
- Back and Forward
- Integrated terminal
- Scripts tool window
- Menu bar
- Settings
- Search in Settings
- Color themes
- Rounded panels
- Updates and release channels
- Status bar and help links
- Notifications
- Memory readout and memory log
- Clear Cache
- MCP server and command line tool
- git mergetool mode