Search & Replace
This area covers 27 features. Watch the walkthrough, then use the reference below — each feature links to the exact moment it appears (▶).
Who this is for: Translator / reviewer. Each feature below lists the role/permission it requires.
Features
Project-wide text search (main panel) ▶ 00:00
As a translator, I want to search across all files in a project for specific text, so that I can find and navigate to cells containing that term.
How it works. User opens ParallelPassagesPanel via Cmd+K (defaults to project scope) or Cmd+F with no Shift (file-scoped if active file exists). User types a query into the search input. The server runs FTS5 against all cells' source and target values, returning ranked results with snippet highlights (<mark> tags). Panel displays results grouped by file or in list form, user can click a result to jump to that cell. Query is debounced 250ms. Empty query shows idle prompt. Unmatched query shows 'No results for' message after index is ready.
Key files
src/components/ParallelPassagesPanel.tsx:1-718src/components/ProjectWorkspace.tsx:1848-1862src/hooks/useWorkspaceSearch.ts:79-183src/lib/sync/search-read.ts:51-67
File-scoped text search ▶ 00:06
As a translator with an open file, I want to search within just the current file without matching terms in other files, so that I can focus on local changes.
How it works. User presses Cmd+F (without Shift) with an active file open. ParallelPassagesPanel opens with 'File' scope pill pressed. Search results are filtered client-side to the active file's fileId after the project-wide server fetch. When no file is open, Cmd+F defaults to project scope. The 'File' pill is disabled (aria-disabled) when no file is active. File name appears in the scope toggle pill.
Key files
src/components/ProjectWorkspace.tsx:1852src/hooks/useWorkspaceSearch.ts:135-183src/components/ParallelPassagesPanel.tsx:599-610
Search results display & navigation ▶ 00:00
As a translator viewing search results, I want to see highlighted snippets with context labels and click any result to navigate directly to that cell, so that I can quickly review and jump to matches.
How it works. Results display filename, context label (e.g. section name), and snippet with matched terms highlighted in yellow (<mark> tags rendered without XSS). Results list shows up to 50 in dock panel, truncated with 'X more' message. Clicking a result fires onSelect callback, which typically jumps the editor to that cellId. In ParallelPassagesPanel, results show 'source' or 'target' label and paired-side snippet (when available in passages mode). Footer shows total count: '{N} results'.
Key files
src/components/ParallelPassagesPanel.tsx:218-262src/components/search/MarkedSnippet.tsxsrc/components/search/SearchResultsView.tsx:61-195src/components/SearchDockPanel.tsx:253-306
Expand search results (full-screen overlay) ▶ 00:00
As a translator using the dock search panel, I want to expand results to a full-screen overlay so I can view and navigate many results at once without the dock's height constraints.
How it works. User clicks 'Expand all' button in SearchDockPanel results header (visible when onExpandResults callback is wired). Results render in a full SearchResultsView overlay above the editor, showing all results grouped by file with sticky file headers. User can scroll, click any result to jump to that cell, and close the overlay with an X button or by selecting a result. Overlay covers the editor area.
Key files
src/components/SearchDockPanel.tsx:268-285src/components/search/SearchResultsView.tsxsrc/components/ProjectWorkspace.tsx:1915-1920
Dock search panel (quick-access) ▶ 00:00
As a translator, I want a quick inline search panel in the left dock so I can search without opening a full dialog, saving screen space and mouse travel.
How it works. User clicks Search tab (icon with magnifying glass) in the left dock rail. SearchDockPanel appears as an always-visible section in the dock. Panel shows three mode buttons (Search, Find & Replace, Bible resources), a scope toggle (File/Project), search input, live results (up to 50 rows), and an 'Open full search panel' button. Results update on each keystroke (debounced). Clearing the input empties results and refocuses the input. In Replace mode, a message says 'Find & Replace with diff preview lives in the full panel' with a button to open it.
Key files
src/components/SearchDockPanel.tsx:1-330src/components/LeftDock.tsxsrc/components/ProjectWorkspace.tsx
Keyboard shortcuts for search modes ▶ 00:00
As a translator, I want keyboard shortcuts to quickly open search in different modes without using menus, so that I can work faster.
How it works. Cmd+K (or Ctrl+K on Windows/Linux) opens ParallelPassagesPanel in 'search' mode, project scope. Cmd+F / Ctrl+F opens 'search' mode, file scope if active file exists (else project). Cmd+Shift+F / Ctrl+Shift+F opens 'search' mode, project scope. Cmd+Shift+R / Ctrl+Shift+R opens 'replace' mode, project scope. All shortcuts prevent default browser behavior and set the appropriate mode + scope before opening the panel.
Key files
src/components/ProjectWorkspace.tsx:1848-1862
Search scope toggle (File vs Project) ▶ 00:00
As a translator, I want to easily toggle between file-scoped and project-wide search so that I can switch focus without closing and reopening the panel.
How it works. ParallelPassagesPanel and SearchDockPanel both display a scope toggle with 'File' and 'Project' pills. 'File' pill is disabled (crossed out, tooltip 'Coming soon') when no file is active. Clicking a pill triggers an immediate search re-run with the new scope. Current scope pill shows aria-pressed='true'. In SearchDockPanel, scope also updates the placeholder text ('Search this file' vs 'Search project').
Key files
src/components/ParallelPassagesPanel.tsx:599-610, 534-537src/components/SearchDockPanel.tsx:175-218, 109-112
Search mode toggle (Search vs Passages vs Replace) ▶ 00:00
As a translator, I want to switch between standard search, parallel passages lookup, and find-and-replace modes without closing the panel, so that I can work in different modes fluidly.
How it works. ParallelPassagesPanel shows mode toggle with three pills: 'Search' (default, searches current project), 'Passages' (searches across all projects for parallel passages), 'Replace' (target-only search for find-and-replace). Clicking a pill updates aria-pressed and re-runs search under the new mode. In 'Replace' mode, the 'Content side' toggle is hidden and 'side' is forced to 'target'. Placeholder text updates per mode.
Key files
src/components/ParallelPassagesPanel.tsx:612-620, 540-547src/components/ParallelPassagesPanel.tsx:514, 550-552
Content side filter (Source / Target / Both) ▶ 00:00
As a translator, I want to search only source text, only target text, or both, so that I can find matches in the side I care about.
How it works. ParallelPassagesPanel shows content side toggle with pills 'Both', 'Source', 'Target' (hidden in Replace mode). Clicking a pill updates aria-pressed and re-runs the search with side='source' or side='target' parameter. 'Both' omits the side filter (default). SearchDockPanel also supports this toggle for project/file scopes.
Key files
src/components/ParallelPassagesPanel.tsx:622-633, 549-557src/hooks/useWorkspaceSearch.ts:162, 514-518
Find & Replace (full mode) ▶ 00:00
As a translator, I want to find text and replace it with new text across multiple cells, with a before/after preview, so that I can perform bulk edits safely.
How it works. User opens ParallelPassagesPanel in Replace mode (Cmd+Shift+R or via dock panel → Open full). Panel shows a 'Find' input and a 'Replace' section with a 'Replacement text' input. As user types in both inputs, diffs are computed in real-time showing before (strikethrough red) and after (green) text. Each diff row has a checkbox; user can select/deselect individual cells. 'Select all' / 'Select none' bulk controls appear. Replace button is disabled until at least one cell is selected. On click, Replace button calls onReplaceAll with the payload (diffs, counts, query, replaceQuery). After replace, a success message shows 'Replaced X cells'.
Key files
src/components/ParallelPassagesPanel.tsx:285-445src/lib/search/replace-action.ts:141-176
Replace diff preview (before/after display) ▶ 00:00
As a translator performing a replace operation, I want to see exactly what will change in each cell before confirming, so that I can avoid unintended edits.
How it works. For each cell affected by a replace, DiffPreviewRow shows: cell fileId + cellId label, before text in strikethrough red, after text in green, and match count ('X replacements in this cell' if >1). Rows are clickable/checkboxable to toggle selection. Scrollable list shows all affected cells. Replaced count in header: '{N} cells affected'.
Key files
src/components/ParallelPassagesPanel.tsx:173-214src/lib/search/replace-action.ts:28-39
HTML-aware replace (skip matches spanning tags) ▶ 00:00
As a system, I want to skip replacing text that spans HTML tag boundaries, so that I preserve rich-text structure and don't corrupt markup.
How it works. When computing replace diffs, matchSpansHtmlTag checks if any matched string contains '<' or '>'. If yes, that match is skipped (left as-is) but counted in totalSkipped. Warning message shown: '{N} matches skipped — spans HTML tag boundary'. These matches are not included in selected replacements.
Key files
src/lib/search/replace-action.ts:59-62, 98-133src/components/ParallelPassagesPanel.tsx:367-371
Case sensitivity toggle for find/replace ▶ 00:00
As a translator, I want to optionally make my search case-sensitive so that I can distinguish 'Hello' from 'hello' if needed.
How it works. Replace diff computation respects a caseSensitive flag (default false). When false, search is case-insensitive using RegExp 'i' flag. When true, uses case-sensitive match. In the UI, a checkbox or toggle could enable this (not currently visible in the panels, but the API supports it via SearchOptions.caseSensitive).
Key files
src/lib/search/replace-action.ts:72-87src/lib/search/workspace-index.ts:27-34
Parallel passages (multi-project search) ▶ 00:00
As a translator working on a related project, I want to search parallel passages across multiple projects for similar text, so that I can reuse or learn from translations in sister projects.
How it works. User opens ParallelPassagesPanel and clicks 'Passages' mode pill. Panel shows paired results where each cell displays its source-side snippet and target-side snippet (paired?) side by side. Search runs fetchParallelPassages across all projects the user has access to (fan-out via Promise.all). Results include projectId field to disambiguate cross-project hits. Unmatched query shows 'No resources found'.
Key files
src/hooks/useWorkspaceSearch.ts:185-241src/lib/sync/search-read.ts:76-92, 100-125src/components/ParallelPassagesPanel.tsx:510-512
Bible resources (Aquifer) reference lookup ▶ 00:00
As a Bible translator, I want to search Bible reference resources (people, places, terms, translation notes) directly within the editor, so that I can consult scholarly material without leaving the workspace.
How it works. User enables Bible resources via project settings (bibleResourcesEnabled flag). In SearchDockPanel, a third mode button 'Bible resources' appears. Clicking it opens BibleResourcesPanel. Panel shows a search input for Aquifer queries ('people, places, terms, and translation notes'). Results list up to 5 hits with kind badges (person, place, term, etc). Clicking a result opens its full page (BibleResourcesPanel switches to reader view) with title, truncated text, 'Open on bibletranslation.org' link. Quick-lookup button 'Notes for {canonicalRef}' pre-fetches the focused verse's resource. Back arrow returns to search.
Key files
src/components/SearchDockPanel.tsx:220-556src/lib/aquifer/client.ts:104-142src/components/ProjectWorkspace.tsx
Bible resource quick-lookup by verse reference ▶ 00:00
As a Bible translator viewing a specific verse, I want a one-click button to look up notes for that verse, so that I can get instant reference material.
How it works. When the user is focused on a cell with a canonical Bible reference (e.g., 'RUT 1:8' from the focused cell's metadata), BibleResourcesPanel shows a 'Notes for {ref}' button at the top. Clicking it auto-opens the aquifer page for that verse (parses ref, maps to /en/passages/{book}/{chapter}/{verse}/) without requiring a manual search.
Key files
src/components/SearchDockPanel.tsx:342-349, 474-486
Loading state & search readiness ▶ 00:00
As a user, I want clear feedback on whether the search index is ready and whether a query is in-flight, so that I know whether to wait for results or if results are stale.
How it works. ParallelPassagesPanel and SearchDockPanel show loading skeleton (4 shimmer rows) while search is in-flight (loading=true). Once at least one search has resolved, ready=true (tracks onReady callback). Idle state (no query) shows prompt: 'Type to search…' . Searching state (query typed but results not yet ready) shows 'Searching…'. Empty state (query typed, results=[], loading=false) shows 'No results for {query}'.
Key files
src/components/ParallelPassagesPanel.tsx:267-280, 573-690src/components/SearchDockPanel.tsx:254-265src/hooks/useWorkspaceSearch.ts:102-104, 135-183
Search input focus & dismiss ▶ 00:00
As a user, I want the search input to be auto-focused when I open the panel, and I want to dismiss the panel with a single keystroke, so that I can work without extra mouse interactions.
How it works. When ParallelPassagesPanel opens (open=true), a 50ms timeout ensures the search input is focused (inputRef.current?.focus()). User can press Escape key to close the panel (handled by Dialog component). SearchDockPanel's clear button (X) clears the input and refocuses it. When panel closes, results are cleared.
Key files
src/components/ParallelPassagesPanel.tsx:493-502src/components/SearchDockPanel.tsx:114-118, 240-250
Search results result limiting & pagination ▶ 00:00
As a user searching a large project, I want search to complete quickly and show me the most relevant results first, rather than hanging while fetching thousands of results.
How it works. Server-side search defaults to limit=50; client can request up to limit=500 via SearchOptions.limit. Server clamps to [1, 500]. SearchDockPanel shows 'X more — open full panel for all results' message when results.length > 50. ParallelPassagesPanel shows all server results in the scrollable list.
Key files
src/hooks/useWorkspaceSearch.ts:36-37, 164-168src/components/SearchDockPanel.tsx:299-303
Search error handling ▶ 00:00
As a user, I want to see error messages if the search service fails, so that I know something went wrong and can retry.
How it works. On fetch failure (network error, 4xx/5xx response), the hook catches and logs the error (console.warn). Results are cleared and loading is set to false. In BibleResourcesPanel, errors display as a red message at the top of the panel. User can retry by modifying the query or clicking search again.
Key files
src/hooks/useWorkspaceSearch.ts:177-182, 234-239src/components/SearchDockPanel.tsx:389-400, 519
Search result context labels ▶ 00:00
As a translator, I want to see context (e.g., section name or cell row identifier) for each search result, so that I can understand where the match is located without clicking.
How it works. Each search result displays a context label in small text above the snippet (e.g., 'Section 1.2' or file + cell ID). Label comes from result.context field (populated by server). In SearchDockPanel and SearchResultsView, context is shown in a smaller, muted font.
Key files
src/components/ParallelPassagesPanel.tsx:241-244src/components/search/SearchResultsView.tsx:182-184src/components/SearchDockPanel.tsx:293-295src/lib/search/workspace-index.ts:10-25
Replace validation & role-gating ▶ 00:00
As a project owner, I want to prevent read-only users from performing replace operations so that only authorized contributors can edit content.
How it works. Replace button is disabled and shows a tooltip 'Replace requires contributor access on these files' when isReadOnly=true. Replacement input field is also disabled. User with read-only access cannot perform any replace operation.
Key files
src/components/ParallelPassagesPanel.tsx:418-442src/components/ParallelPassagesPanel.tsx:330-331, 358
Replace clearing prior validations ▶ 00:00
As a system, I want replacing text to mark the cell as requiring re-validation, so that we don't keep stale validation marks on modified cells.
How it works. When replace is applied (onReplaceAll fires), each affected cell is committed with replaceString and searchQuery metadata. The replace advances the cell's event chain head, automatically dropping prior validations (per Q25 event-anchored validation). Help text in panel: 'Replacing text clears validation — it must be re-reviewed.'
Key files
src/components/ParallelPassagesPanel.tsx:361-364src/components/ParallelPassagesPanel.tsx:41-54
Search query sanitization ▶ 00:00
As a system, I want to safely handle user-provided search queries without breaking FTS5 or allowing injection, so that searches are robust.
How it works. Client sends raw user input to the server. Server sanitizes the query before passing to FTS5 — bare punctuation, reserved FTS words, etc. are stripped. Query that sanitizes to zero tokens returns {results: []} with HTTP 200 (not an error). No need for client-side escaping or validation.
Key files
src/lib/sync/search-read.ts:39-50
Debounced search input (250ms) ▶ 00:00
As a system, I want to avoid firing a search request on every keystroke so that I reduce server load when a user is typing.
How it works. When user types in search input, the query state updates immediately (for visual feedback), but the triggerSearch call is debounced 250ms. If user types another character before 250ms elapses, the previous timer is cancelled and a new 250ms countdown starts. Only the final query after the user stops typing (or presses Enter) triggers a search fetch.
Key files
src/components/ParallelPassagesPanel.tsx:450, 525-532, 559-563
Search documentation & placeholders ▶ 00:00
As a new user, I want helpful placeholder text and instructions in the search panels to understand what to search for and how the panels work.
How it works. Placeholder text adapts to mode and scope: 'Search project…', 'Search this file…', 'Find in project…', 'Search Bible resources…', 'Search parallel passages across all projects…', etc. Idle state (no query) shows mode-specific prompt. Replace mode shows disclaimer about clearing validations.
Key files
src/components/ParallelPassagesPanel.tsx:576-581, 675-681src/components/SearchDockPanel.tsx:237, 499, 527-529
Search result count display ▶ 00:00
As a user, I want to know how many results matched my query so that I can gauge whether to refine my search.
How it works. Panel footer or header shows '{N} results' and '{N} files' (when grouped by file). SearchDockPanel header shows result count and remaining count if truncated. ParallelPassagesPanel footer shows count when results.length > 0.
Key files
src/components/ParallelPassagesPanel.tsx:707-709src/components/SearchDockPanel.tsx:270-271src/components/search/SearchResultsView.tsx:93-95