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