1use crate::input::InputModeKind;
2use aho_corasick::AhoCorasick;
3use gpui::{Context, Window};
4use ropey::Rope;
5use std::{ops::Range, rc::Rc};
6
7use super::{InputBaseState, Replace, RopeExt as _, Search, movement::MoveDirection};
8
9#[derive(Debug, Clone)]
11pub struct SearchMatcher {
12 text: Rope,
13 pub query: Option<AhoCorasick>,
14 matched_ranges: Rc<Vec<Range<usize>>>,
15 current_match_ix: usize,
16 replacing: bool,
17}
18
19#[derive(Debug, Clone)]
20pub struct SearchSession {
21 pub open: bool,
22 pub replace_mode: bool,
23 pub case_insensitive: bool,
24 pub query: String,
25 pub replacement: String,
26 pub anchor_offset: Option<usize>,
27 pub matcher: SearchMatcher,
28}
29
30impl Default for SearchSession {
31 fn default() -> Self {
32 Self {
33 open: false,
34 replace_mode: false,
35 case_insensitive: true,
36 query: String::new(),
37 replacement: String::new(),
38 anchor_offset: None,
39 matcher: SearchMatcher::new(),
40 }
41 }
42}
43
44impl SearchSession {
45 pub(crate) fn open(&mut self, replace_mode: bool, replaceable: bool) {
46 self.open = true;
47 self.replace_mode = replace_mode && replaceable;
48 }
49
50 pub(crate) fn close(&mut self) {
51 self.open = false;
52 }
53
54 pub(crate) fn update_query(&mut self, query: impl Into<String>, case_insensitive: bool) {
55 self.query = query.into();
56 self.case_insensitive = case_insensitive;
57 self.matcher.update_query(&self.query, case_insensitive);
58 }
59}
60
61impl<M: InputModeKind> InputBaseState<M> {
62 pub fn open_search(&mut self, replace_mode: bool, cx: &mut Context<Self>) {
63 if !self.searchable {
64 return;
65 }
66 self.search_session
67 .open(replace_mode, self.is_replaceable());
68 let selected = self.selected_text().to_string();
69 if !selected.is_empty() {
70 self.search_session.query = selected;
71 }
72 self.search_session.anchor_offset = self
73 .last_layout
74 .as_ref()
75 .map(|layout| layout.visible_range_offset.start);
76 self.search_session.matcher.update_query(
77 &self.search_session.query,
78 self.search_session.case_insensitive,
79 );
80 self.search_session.matcher.update(&self.text);
81 if let Some(anchor) = self.search_session.anchor_offset {
82 self.search_session.matcher.update_cursor_by_offset(anchor);
83 }
84 cx.notify();
85 }
86
87 pub fn search_session(&self) -> &SearchSession {
88 &self.search_session
89 }
90
91 #[doc(hidden)]
92 pub fn set_search_replace_mode(&mut self, replace_mode: bool, cx: &mut Context<Self>) {
93 self.search_session.replace_mode = replace_mode && self.is_replaceable();
94 cx.notify();
95 }
96
97 pub fn is_replaceable(&self) -> bool {
102 self.replaceable && self.is_editable()
103 }
104
105 pub fn set_search_query(
106 &mut self,
107 query: impl Into<String>,
108 case_insensitive: bool,
109 cx: &mut Context<Self>,
110 ) {
111 self.search_session.update_query(query, case_insensitive);
112 self.search_session.matcher.update(&self.text);
113 cx.notify();
114 }
115
116 pub fn close_search(&mut self, cx: &mut Context<Self>) {
117 self.search_session.close();
118 cx.notify();
119 }
120
121 pub fn next_search_match(&mut self, cx: &mut Context<Self>) -> Option<Range<usize>> {
122 let previous = self.search_session.matcher.current_match_index();
123 let range = self.search_session.matcher.next()?;
124 let direction = (self.search_session.matcher.current_match_index() > previous)
125 .then_some(MoveDirection::Down);
126 self.scroll_to(range.end, direction, cx);
127 Some(range)
128 }
129
130 pub fn previous_search_match(&mut self, cx: &mut Context<Self>) -> Option<Range<usize>> {
131 let previous = self.search_session.matcher.current_match_index();
132 let range = self.search_session.matcher.next_back()?;
133 let direction = (self.search_session.matcher.current_match_index() < previous)
134 .then_some(MoveDirection::Up);
135 self.scroll_to(range.start, direction, cx);
136 Some(range)
137 }
138
139 pub fn replace_current_search_match(
140 &mut self,
141 replacement: &str,
142 window: &mut Window,
143 cx: &mut Context<Self>,
144 ) -> bool {
145 if !self.is_replaceable() {
146 return false;
147 }
148 let matcher = &mut self.search_session.matcher;
149 let Some(range) = matcher
150 .matched_ranges()
151 .get(matcher.current_match_index())
152 .cloned()
153 else {
154 return false;
155 };
156 let next = matcher.peek().unwrap_or_else(|| range.clone());
157 let direction = matcher
158 .has_next_without_wrap()
159 .then_some(MoveDirection::Down);
160 if direction.is_none() {
161 matcher.set_current_match_index(0);
162 }
163 matcher.begin_replacement();
164 let range_utf16 = self.range_to_utf16(&range);
165 self.scroll_to(next.end, direction, cx);
166 self.replace_text_in_range_silent(Some(range_utf16), replacement, window, cx);
167 true
168 }
169
170 pub fn replace_all_search_matches(
171 &mut self,
172 replacement: &str,
173 window: &mut Window,
174 cx: &mut Context<Self>,
175 ) -> usize {
176 if !self.is_replaceable() {
177 return 0;
178 }
179 let ranges = self.search_session.matcher.matched_ranges();
180 if ranges.is_empty() {
181 return 0;
182 }
183 let mut text = self.text.clone();
184 for range in ranges.iter().rev() {
185 text.replace(range.clone(), replacement);
186 }
187 self.search_session.matcher.begin_replacement();
188 let count = ranges.len();
189 self.replace_text_in_range_silent(Some(0..self.text.len()), &text.to_string(), window, cx);
190 self.scroll_to(0, Some(MoveDirection::Down), cx);
191 count
192 }
193
194 pub(super) fn update_search(&mut self, _cx: &mut gpui::App) {
195 self.search_session.matcher.update(&self.text);
196 }
197
198 pub(super) fn on_action_search(&mut self, _: &Search, _: &mut Window, cx: &mut Context<Self>) {
199 if !self.searchable {
200 return;
201 }
202 self.open_search(false, cx);
203 }
204
205 pub(super) fn on_action_replace(
206 &mut self,
207 _: &Replace,
208 _: &mut Window,
209 cx: &mut Context<Self>,
210 ) {
211 if !self.searchable {
212 return;
213 }
214 self.open_search(true, cx);
215 }
216}
217
218impl Default for SearchMatcher {
219 fn default() -> Self {
220 Self::new()
221 }
222}
223
224impl SearchMatcher {
225 pub fn new() -> Self {
226 Self {
227 text: "".into(),
228 query: None,
229 matched_ranges: Rc::new(Vec::new()),
230 current_match_ix: 0,
231 replacing: false,
232 }
233 }
234
235 pub fn update(&mut self, text: &Rope) {
237 if self.text.eq(text) {
238 self.replacing = false;
239 return;
240 }
241 self.text = text.clone();
242 self.update_matches();
243 }
244
245 pub fn update_query(&mut self, query: &str, case_insensitive: bool) {
246 self.query = (!query.is_empty()).then(|| {
247 AhoCorasick::builder()
248 .ascii_case_insensitive(case_insensitive)
249 .build([query])
250 .expect("failed to build input search query")
251 });
252 self.update_matches();
253 }
254
255 pub fn matched_ranges(&self) -> Rc<Vec<Range<usize>>> {
256 self.matched_ranges.clone()
257 }
258
259 pub fn current_match_index(&self) -> usize {
260 self.current_match_ix
261 }
262
263 pub fn len(&self) -> usize {
264 self.matched_ranges.len()
265 }
266
267 pub fn is_empty(&self) -> bool {
268 self.matched_ranges.is_empty()
269 }
270
271 pub fn label(&self) -> String {
272 if self.is_empty() {
273 "0/0".into()
274 } else {
275 format!("{}/{}", self.current_match_ix + 1, self.len())
276 }
277 }
278
279 fn peek(&self) -> Option<Range<usize>> {
280 self.next_index()
281 .and_then(|ix| self.matched_ranges.get(ix).cloned())
282 }
283
284 fn has_next_without_wrap(&self) -> bool {
285 self.current_match_ix < self.matched_ranges.len().saturating_sub(1)
286 }
287
288 pub fn update_cursor_by_offset(&mut self, offset: usize) {
289 for (ix, range) in self.matched_ranges.iter().enumerate() {
290 self.current_match_ix = ix;
291 if range.contains(&offset) || range.end >= offset {
292 return;
293 }
294 }
295 }
296
297 fn begin_replacement(&mut self) {
299 self.replacing = true;
300 }
301
302 fn set_current_match_index(&mut self, index: usize) {
303 self.current_match_ix = index.min(self.matched_ranges.len().saturating_sub(1));
304 }
305
306 fn next_index(&self) -> Option<usize> {
307 if self.is_empty() {
308 None
309 } else if self.has_next_without_wrap() {
310 Some(self.current_match_ix + 1)
311 } else {
312 Some(0)
313 }
314 }
315
316 fn update_matches(&mut self) {
317 let mut ranges = Vec::new();
318 if let Some(query) = &self.query {
319 let text = self.text.to_string();
320 ranges.extend(
321 query
322 .stream_find_iter(text.as_bytes())
323 .map(|result| result.expect("input search match").range()),
324 );
325 }
326 self.matched_ranges = Rc::new(ranges);
327 if !self.replacing || self.is_empty() {
328 self.current_match_ix = 0;
329 } else {
330 self.current_match_ix = self.current_match_ix.min(self.len() - 1);
331 }
332 self.replacing = false;
333 }
334}
335
336impl Iterator for SearchMatcher {
337 type Item = Range<usize>;
338
339 fn next(&mut self) -> Option<Self::Item> {
340 let ix = self.next_index()?;
341 self.current_match_ix = ix;
342 self.matched_ranges.get(ix).cloned()
343 }
344}
345
346impl DoubleEndedIterator for SearchMatcher {
347 fn next_back(&mut self) -> Option<Self::Item> {
348 if self.is_empty() {
349 return None;
350 }
351 if self.current_match_ix == 0 {
352 self.current_match_ix = self.len();
353 }
354 self.current_match_ix -= 1;
355 self.matched_ranges.get(self.current_match_ix).cloned()
356 }
357}
358
359#[cfg(test)]
360mod tests {
361 use super::*;
362
363 #[test]
364 fn finds_navigates_and_preserves_replacement_position() {
365 let mut matcher = SearchMatcher::new();
366 matcher.update(&Rope::from("foo FOO foo"));
367 matcher.update_query("foo", true);
368 assert_eq!(&*matcher.matched_ranges(), &[0..3, 4..7, 8..11]);
369 assert_eq!(matcher.next(), Some(4..7));
370 assert_eq!(matcher.next_back(), Some(0..3));
371
372 matcher.set_current_match_index(2);
373 matcher.begin_replacement();
374 matcher.update(&Rope::from("foo FOO bar"));
375 assert_eq!(matcher.current_match_index(), 1);
376 }
377
378 #[test]
379 fn next_wraps_to_start() {
380 let mut matcher = SearchMatcher::new();
381 matcher.update(&Rope::from(".....aaaaa.....aaaaa.....aaaaa"));
382 matcher.update_query("aaaaa", false);
383 matcher.set_current_match_index(2);
384 assert_eq!(matcher.next(), Some(5..10));
385 }
386
387 #[test]
388 fn replacement_keeps_current_match_index_on_next_match() {
389 let mut matcher = SearchMatcher::new();
390 matcher.update(&Rope::from("foo foo foo"));
391 matcher.update_query("foo", true);
392 assert_eq!(matcher.label(), "1/3");
393
394 assert!(matcher.has_next_without_wrap());
395 matcher.begin_replacement();
396 matcher.update(&Rope::from("bar foo foo"));
397 assert_eq!(matcher.current_match_index(), 0);
398 assert_eq!(matcher.matched_ranges()[0], 4..7);
399 assert_eq!(matcher.label(), "1/2");
400
401 matcher.set_current_match_index(1);
402 assert!(!matcher.has_next_without_wrap());
403 matcher.set_current_match_index(0);
404 matcher.begin_replacement();
405 matcher.update(&Rope::from("bar foo bar"));
406 assert_eq!(matcher.current_match_index(), 0);
407 assert_eq!(matcher.matched_ranges()[0], 4..7);
408 assert_eq!(matcher.label(), "1/1");
409 }
410
411 #[test]
412 fn update_matches_clamps_current_match_index_while_replacing() {
413 let mut matcher = SearchMatcher::new();
414 matcher.update(&Rope::from("foo foo foo"));
415 matcher.update_query("foo", true);
416 matcher.set_current_match_index(2);
417 matcher.begin_replacement();
418
419 matcher.update(&Rope::from("foo xoo foo"));
420
421 assert_eq!(matcher.len(), 2);
422 assert_eq!(matcher.current_match_index(), 1);
423 assert_eq!(matcher.label(), "2/2");
424 }
425}