The Find and Replace feature provides comprehensive search and replace functionality for Ferrite. It includes real-time incremental search, match highlighting, keyboard navigation, and integration with the undo/redo system.
src/editor/find_replace.rs
├── FindState - Search state management
├── FindReplacePanel - Floating UI panel
└── FindReplacePanelOutput - Panel action output
User Input → FindReplacePanel → FindReplacePanelOutput → app.rs handlers
↓
FindState.find_matches(content)
↓
Vec<(start, end)> matches
↓
Navigation / Replace operations
FindState manages all search state:
pub struct FindState {
pub search_term: String, // Current search term
pub replace_term: String, // Replacement text
pub case_sensitive: bool, // Case-sensitive matching
pub whole_word: bool, // Match whole words only
pub use_regex: bool, // Use regex matching
pub current_match: usize, // Current match index
pub matches: Vec<(usize, usize)>, // Match positions (start, end)
pub is_replace_mode: bool, // Find-only vs Replace mode
}| Method | Description |
|---|---|
find_matches(&mut self, text: &str) -> usize |
Find all matches, returns count |
next_match() -> Option<usize> |
Move to next match (wraps) |
prev_match() -> Option<usize> |
Move to previous match (wraps) |
current_match_position() -> Option<(usize, usize)> |
Get current match bounds |
replace_current(text: &str) -> Option<String> |
Replace current match |
replace_all(text: &str) -> String |
Replace all matches |
- Standard substring matching
- Case-insensitive by default
- Supports whole-word matching with word boundaries
- Full regex pattern support via the
regexcrate - Case-insensitive flag (
(?i)) applied automatically when needed - Word boundaries (
\b) applied for whole-word regex matching - Invalid patterns gracefully return 0 matches
| Option | Icon | Description |
|---|---|---|
| Case Sensitive | Aa |
Match exact case |
| Whole Word | W |
Match complete words only |
| Use Regex | .* |
Enable regex pattern matching |
A floating UI panel that provides the search interface:
- Search input: Real-time search with incremental results
- Replace input: Visible only in replace mode
- Match counter: Shows "{current} of {total}"
- Toggle buttons: Case, Whole Word, Regex
- Navigation buttons: Previous (◀) / Next (▶)
- Replace buttons: Replace / Replace All
- Find Mode (Ctrl+F): Search-only functionality
- Replace Mode (Ctrl+H): Search with replace options
| Shortcut | Action | Context |
|---|---|---|
Ctrl+F |
Open find panel | Global |
Ctrl+H |
Open find/replace panel | Global |
F3 |
Find next match | When panel open |
Shift+F3 |
Find previous match | When panel open |
Enter |
Find next match | In search input |
Escape |
Close panel | When panel open |
Handles keyboard shortcuts and panel output:
// Opening the panel
fn handle_open_find(&mut self, replace_mode: bool)
// Navigation
fn handle_find_next(&mut self)
fn handle_find_prev(&mut self)
// Replace operations (with undo support)
fn handle_replace_current(&mut self)
fn handle_replace_all(&mut self)FindState is stored in UiState:
pub struct UiState {
pub show_find_replace: bool,
pub find_state: FindState,
// ...
}The Tools section 🔍 button opens the find panel:
RibbonAction::FindReplace => {
self.handle_open_find(false);
}Replace operations integrate with the existing undo system:
fn handle_replace_current(&mut self) {
if let Some(new_content) = find_state.replace_current(&content) {
// tab.set_content() automatically:
// - Pushes current state to undo stack
// - Clears redo stack
tab.set_content(new_content);
}
}Uses word boundary detection:
let is_start_boundary = match_start == 0
|| !text[..match_start]
.chars()
.last()
.map(|c| c.is_alphanumeric() || c == '_')
.unwrap_or(false);- Literal: Both text and search term lowercased
- Regex:
(?i)flag prepended to pattern
After re-searching (e.g., options changed), current_match is clamped:
if !self.matches.is_empty() && self.current_match >= self.matches.len() {
self.current_match = 0;
}33 tests cover:
- Basic matching (literal, case, whole word, regex)
- Navigation (next/prev, wrapping)
- Replace operations (current, all)
- Edge cases (empty, unicode, multiline, invalid regex)
- Panel state management
Run tests:
cargo test find_replaceWhen the find panel is open and matches are found, the editor displays:
- Current match: Bright yellow highlight (more opaque)
- Other matches: Pale yellow highlight (semi-transparent)
When navigating between matches (F3/Shift+F3), the editor automatically scrolls to show the current match, positioning it roughly 1/3 from the top of the viewport.
The SearchHighlights struct in widget.rs carries:
pub struct SearchHighlights {
pub matches: Vec<(usize, usize)>, // All match positions
pub current_match: usize, // Index of current match
pub scroll_to_match: bool, // Whether to auto-scroll
}Highlights are drawn using the galley's cursor positioning:
- Convert byte positions to
CCursor - Get
RCursorwith row and column information - Calculate screen rectangles using
row.x_offset() - Draw filled rectangles behind text
Potential improvements for future versions:
- Selection-based search - search for selected text
- Search history - recent search terms dropdown
- Find in files - search across multiple open tabs
- Preserve case replacement option
- Count-only mode for large documents
- Match highlighting in Rendered mode - currently only Raw mode is supported