revue/widget/textarea/
find_replace.rs

1//! Find and replace functionality for TextArea
2
3use super::cursor::CursorPos;
4
5/// Find/Replace mode
6#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
7pub enum FindReplaceMode {
8    /// Find mode - search for text
9    #[default]
10    Find,
11    /// Replace mode - find and replace text
12    Replace,
13}
14
15/// Options for find/replace operations
16#[derive(Clone, Debug, Default)]
17pub struct FindOptions {
18    /// Case-sensitive search
19    pub case_sensitive: bool,
20    /// Match whole words only
21    pub whole_word: bool,
22    /// Use regex pattern
23    pub use_regex: bool,
24}
25
26/// A match found in the text
27#[derive(Clone, Debug, PartialEq, Eq)]
28pub struct FindMatch {
29    /// Start position
30    pub start: CursorPos,
31    /// End position
32    pub end: CursorPos,
33}
34
35impl FindMatch {
36    /// Create a new find match
37    pub fn new(start: CursorPos, end: CursorPos) -> Self {
38        Self { start, end }
39    }
40}
41
42/// Find/Replace state
43#[derive(Clone, Debug, Default)]
44pub struct FindReplaceState {
45    /// Search query
46    pub query: String,
47    /// Replacement text
48    pub replace_with: String,
49    /// Search options
50    pub options: FindOptions,
51    /// All matches in document
52    pub matches: Vec<FindMatch>,
53    /// Currently focused match index
54    pub current_match: Option<usize>,
55    /// UI mode (Find or Replace)
56    pub mode: FindReplaceMode,
57    /// Input focus: true = query input, false = replace input
58    pub query_focused: bool,
59}
60
61impl FindReplaceState {
62    /// Create a new find/replace state
63    pub fn new(mode: FindReplaceMode) -> Self {
64        Self {
65            mode,
66            query_focused: true,
67            ..Default::default()
68        }
69    }
70
71    /// Get match count
72    pub fn match_count(&self) -> usize {
73        self.matches.len()
74    }
75
76    /// Get current match (1-indexed for display)
77    pub fn current_match_display(&self) -> usize {
78        self.current_match.map(|i| i + 1).unwrap_or(0)
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85
86    // =========================================================================
87    // FindReplaceMode Tests
88    // =========================================================================
89
90    #[test]
91    fn test_find_replace_mode_default() {
92        assert_eq!(FindReplaceMode::default(), FindReplaceMode::Find);
93    }
94
95    #[test]
96    fn test_find_replace_mode_equality() {
97        assert_eq!(FindReplaceMode::Find, FindReplaceMode::Find);
98        assert_eq!(FindReplaceMode::Replace, FindReplaceMode::Replace);
99        assert_ne!(FindReplaceMode::Find, FindReplaceMode::Replace);
100    }
101
102    // =========================================================================
103    // FindOptions Tests
104    // =========================================================================
105
106    #[test]
107    fn test_find_options_default() {
108        let opts = FindOptions::default();
109        assert!(!opts.case_sensitive);
110        assert!(!opts.whole_word);
111        assert!(!opts.use_regex);
112    }
113
114    #[test]
115    fn test_find_options_custom() {
116        let opts = FindOptions {
117            case_sensitive: true,
118            whole_word: true,
119            use_regex: false,
120        };
121        assert!(opts.case_sensitive);
122        assert!(opts.whole_word);
123        assert!(!opts.use_regex);
124    }
125
126    // =========================================================================
127    // FindMatch Tests
128    // =========================================================================
129
130    #[test]
131    fn test_find_match_new() {
132        let start = CursorPos { line: 0, col: 5 };
133        let end = CursorPos { line: 0, col: 10 };
134        let match_result = FindMatch::new(start, end);
135
136        assert_eq!(match_result.start.line, 0);
137        assert_eq!(match_result.start.col, 5);
138        assert_eq!(match_result.end.line, 0);
139        assert_eq!(match_result.end.col, 10);
140    }
141
142    #[test]
143    fn test_find_match_equality() {
144        let m1 = FindMatch::new(CursorPos { line: 0, col: 0 }, CursorPos { line: 0, col: 5 });
145        let m2 = FindMatch::new(CursorPos { line: 0, col: 0 }, CursorPos { line: 0, col: 5 });
146        let m3 = FindMatch::new(CursorPos { line: 1, col: 0 }, CursorPos { line: 1, col: 5 });
147
148        assert_eq!(m1, m2);
149        assert_ne!(m1, m3);
150    }
151
152    #[test]
153    fn test_find_match_clone() {
154        let m = FindMatch::new(CursorPos { line: 2, col: 3 }, CursorPos { line: 2, col: 8 });
155        let cloned = m.clone();
156        assert_eq!(m, cloned);
157    }
158
159    // =========================================================================
160    // FindReplaceState Tests
161    // =========================================================================
162
163    #[test]
164    fn test_find_replace_state_default() {
165        let state = FindReplaceState::default();
166        assert!(state.query.is_empty());
167        assert!(state.replace_with.is_empty());
168        assert!(state.matches.is_empty());
169        assert!(state.current_match.is_none());
170        assert_eq!(state.mode, FindReplaceMode::Find);
171    }
172
173    #[test]
174    fn test_find_replace_state_new_find() {
175        let state = FindReplaceState::new(FindReplaceMode::Find);
176        assert_eq!(state.mode, FindReplaceMode::Find);
177        assert!(state.query_focused);
178    }
179
180    #[test]
181    fn test_find_replace_state_new_replace() {
182        let state = FindReplaceState::new(FindReplaceMode::Replace);
183        assert_eq!(state.mode, FindReplaceMode::Replace);
184        assert!(state.query_focused);
185    }
186
187    #[test]
188    fn test_find_replace_state_match_count_empty() {
189        let state = FindReplaceState::default();
190        assert_eq!(state.match_count(), 0);
191    }
192
193    #[test]
194    fn test_find_replace_state_match_count() {
195        let mut state = FindReplaceState::default();
196        state.matches = vec![
197            FindMatch::new(CursorPos { line: 0, col: 0 }, CursorPos { line: 0, col: 5 }),
198            FindMatch::new(CursorPos { line: 1, col: 0 }, CursorPos { line: 1, col: 5 }),
199            FindMatch::new(CursorPos { line: 2, col: 0 }, CursorPos { line: 2, col: 5 }),
200        ];
201        assert_eq!(state.match_count(), 3);
202    }
203
204    #[test]
205    fn test_find_replace_state_current_match_display_none() {
206        let state = FindReplaceState::default();
207        assert_eq!(state.current_match_display(), 0);
208    }
209
210    #[test]
211    fn test_find_replace_state_current_match_display() {
212        let mut state = FindReplaceState::default();
213        state.matches = vec![
214            FindMatch::new(CursorPos { line: 0, col: 0 }, CursorPos { line: 0, col: 5 }),
215            FindMatch::new(CursorPos { line: 1, col: 0 }, CursorPos { line: 1, col: 5 }),
216        ];
217        state.current_match = Some(0);
218        assert_eq!(state.current_match_display(), 1);
219
220        state.current_match = Some(1);
221        assert_eq!(state.current_match_display(), 2);
222    }
223
224    #[test]
225    fn test_find_replace_state_query_focused() {
226        let state = FindReplaceState::new(FindReplaceMode::Find);
227        assert!(state.query_focused);
228    }
229
230    #[test]
231    fn test_find_replace_state_with_data() {
232        let mut state = FindReplaceState::new(FindReplaceMode::Replace);
233        state.query = "search".to_string();
234        state.replace_with = "replace".to_string();
235        state.options = FindOptions {
236            case_sensitive: true,
237            whole_word: false,
238            use_regex: false,
239        };
240
241        assert_eq!(state.query, "search");
242        assert_eq!(state.replace_with, "replace");
243        assert!(state.options.case_sensitive);
244    }
245}