1use language::{Point, TextBufferSnapshot};
2use std::{cmp, ops::Range};
3
4const REPLACEMENT_COST: u32 = 1;
5const INSERTION_COST: u32 = 3;
6const DELETION_COST: u32 = 10;
7
8/// A streaming fuzzy matcher that can process text chunks incrementally
9/// and return the best match found so far at each step.
10pub struct StreamingFuzzyMatcher {
11 snapshot: TextBufferSnapshot,
12 query_lines: Vec<String>,
13 line_hint: Option<u32>,
14 incomplete_line: String,
15 matches: Vec<Range<usize>>,
16 matrix: SearchMatrix,
17}
18
19impl StreamingFuzzyMatcher {
20 pub fn new(snapshot: TextBufferSnapshot) -> Self {
21 let buffer_line_count = snapshot.max_point().row as usize + 1;
22 Self {
23 snapshot,
24 query_lines: Vec::new(),
25 line_hint: None,
26 incomplete_line: String::new(),
27 matches: Vec::new(),
28 matrix: SearchMatrix::new(buffer_line_count + 1),
29 }
30 }
31
32 /// Returns the query lines.
33 pub fn query_lines(&self) -> &[String] {
34 &self.query_lines
35 }
36
37 /// Push a new chunk of text and get the best match found so far.
38 ///
39 /// This method accumulates text chunks and processes complete lines.
40 /// Partial lines are buffered internally until a newline is received.
41 ///
42 /// # Returns
43 ///
44 /// Returns `Some(range)` if a match has been found with the accumulated
45 /// query so far, or `None` if no suitable match exists yet.
46 pub fn push(&mut self, chunk: &str, line_hint: Option<u32>) -> Option<Range<usize>> {
47 // Add the chunk to our incomplete line buffer
48 self.incomplete_line.push_str(chunk);
49 self.line_hint = line_hint;
50
51 if let Some((last_pos, _)) = self.incomplete_line.match_indices('\n').next_back() {
52 let complete_part = &self.incomplete_line[..=last_pos];
53
54 // Split into lines and add to query_lines
55 for line in complete_part.lines() {
56 self.query_lines.push(line.to_string());
57 }
58
59 self.incomplete_line.replace_range(..last_pos + 1, "");
60
61 self.matches = self.resolve_location_fuzzy();
62 }
63
64 let best_match = self.select_best_match();
65 best_match.or_else(|| self.matches.first().cloned())
66 }
67
68 /// Finish processing and return the final best match(es).
69 ///
70 /// This processes any remaining incomplete line before returning the final
71 /// match result.
72 pub fn finish(&mut self) -> Vec<Range<usize>> {
73 // Process any remaining incomplete line
74 if !self.incomplete_line.is_empty() {
75 self.query_lines.push(self.incomplete_line.clone());
76 self.incomplete_line.clear();
77 self.matches = self.resolve_location_fuzzy();
78 }
79 self.matches.clone()
80 }
81
82 fn resolve_location_fuzzy(&mut self) -> Vec<Range<usize>> {
83 let new_query_line_count = self.query_lines.len();
84 let old_query_line_count = self.matrix.rows.saturating_sub(1);
85 if new_query_line_count == old_query_line_count {
86 return Vec::new();
87 }
88
89 self.matrix.resize_rows(new_query_line_count + 1);
90
91 // Process only the new query lines
92 for row in old_query_line_count..new_query_line_count {
93 let query_line = self.query_lines[row].trim();
94 let leading_deletion_cost = (row + 1) as u32 * DELETION_COST;
95
96 self.matrix.set(
97 row + 1,
98 0,
99 SearchState::new(leading_deletion_cost, SearchDirection::Up),
100 );
101
102 let mut buffer_lines = self.snapshot.as_rope().chunks().lines();
103 let mut col = 0;
104 while let Some(buffer_line) = buffer_lines.next() {
105 let buffer_line = buffer_line.trim();
106 let up = SearchState::new(
107 self.matrix
108 .get(row, col + 1)
109 .cost
110 .saturating_add(DELETION_COST),
111 SearchDirection::Up,
112 );
113 let left = SearchState::new(
114 self.matrix
115 .get(row + 1, col)
116 .cost
117 .saturating_add(INSERTION_COST),
118 SearchDirection::Left,
119 );
120 let diagonal = SearchState::new(
121 if query_line == buffer_line {
122 self.matrix.get(row, col).cost
123 } else if fuzzy_eq(query_line, buffer_line) {
124 self.matrix.get(row, col).cost + REPLACEMENT_COST
125 } else {
126 self.matrix
127 .get(row, col)
128 .cost
129 .saturating_add(DELETION_COST + INSERTION_COST)
130 },
131 SearchDirection::Diagonal,
132 );
133 self.matrix
134 .set(row + 1, col + 1, up.min(left).min(diagonal));
135 col += 1;
136 }
137 }
138
139 // Find all matches with the best cost
140 let buffer_line_count = self.snapshot.max_point().row as usize + 1;
141 let mut best_cost = u32::MAX;
142 let mut matches_with_best_cost = Vec::new();
143
144 for col in 1..=buffer_line_count {
145 let cost = self.matrix.get(new_query_line_count, col).cost;
146 if cost < best_cost {
147 best_cost = cost;
148 matches_with_best_cost.clear();
149 matches_with_best_cost.push(col as u32);
150 } else if cost == best_cost {
151 matches_with_best_cost.push(col as u32);
152 }
153 }
154
155 // Find ranges for the matches
156 let mut valid_matches = Vec::new();
157 for &buffer_row_end in &matches_with_best_cost {
158 let mut matched_lines = 0;
159 let mut query_row = new_query_line_count;
160 let mut buffer_row_start = buffer_row_end;
161 while query_row > 0 && buffer_row_start > 0 {
162 let current = self.matrix.get(query_row, buffer_row_start as usize);
163 match current.direction {
164 SearchDirection::Diagonal => {
165 query_row -= 1;
166 buffer_row_start -= 1;
167 matched_lines += 1;
168 }
169 SearchDirection::Up => {
170 query_row -= 1;
171 }
172 SearchDirection::Left => {
173 buffer_row_start -= 1;
174 }
175 }
176 }
177
178 let matched_buffer_row_count = buffer_row_end - buffer_row_start;
179 let matched_ratio = matched_lines as f32
180 / (matched_buffer_row_count as f32).max(new_query_line_count as f32);
181 if matched_ratio >= 0.8 {
182 let buffer_start_ix = self
183 .snapshot
184 .point_to_offset(Point::new(buffer_row_start, 0));
185 let buffer_end_ix = self.snapshot.point_to_offset(Point::new(
186 buffer_row_end - 1,
187 self.snapshot.line_len(buffer_row_end - 1),
188 ));
189 valid_matches.push((buffer_row_start, buffer_start_ix..buffer_end_ix));
190 }
191 }
192
193 valid_matches.into_iter().map(|(_, range)| range).collect()
194 }
195
196 /// Return the best match with starting position close enough to line_hint.
197 pub fn select_best_match(&self) -> Option<Range<usize>> {
198 // Allow line hint to be off by that many lines.
199 // Higher values increase probability of applying edits to a wrong place,
200 // Lower values increase edits failures and overall conversation length.
201 const LINE_HINT_TOLERANCE: u32 = 200;
202
203 if self.matches.is_empty() {
204 return None;
205 }
206
207 if self.matches.len() == 1 {
208 return self.matches.first().cloned();
209 }
210
211 let Some(line_hint) = self.line_hint else {
212 // Multiple ambiguous matches
213 return None;
214 };
215
216 let mut best_match = None;
217 let mut best_distance = u32::MAX;
218
219 for range in &self.matches {
220 let start_point = self.snapshot.offset_to_point(range.start);
221 let start_line = start_point.row;
222 let distance = start_line.abs_diff(line_hint);
223
224 if distance <= LINE_HINT_TOLERANCE && distance < best_distance {
225 best_distance = distance;
226 best_match = Some(range.clone());
227 }
228 }
229
230 best_match
231 }
232}
233
234fn fuzzy_eq(left: &str, right: &str) -> bool {
235 const THRESHOLD: f64 = 0.8;
236
237 let min_levenshtein = left.len().abs_diff(right.len());
238 let min_normalized_levenshtein =
239 1. - (min_levenshtein as f64 / cmp::max(left.len(), right.len()) as f64);
240 if min_normalized_levenshtein < THRESHOLD {
241 return false;
242 }
243
244 strsim::normalized_levenshtein(left, right) >= THRESHOLD
245}
246
247#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
248enum SearchDirection {
249 Up,
250 Left,
251 Diagonal,
252}
253
254#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
255struct SearchState {
256 cost: u32,
257 direction: SearchDirection,
258}
259
260impl SearchState {
261 fn new(cost: u32, direction: SearchDirection) -> Self {
262 Self { cost, direction }
263 }
264}
265
266struct SearchMatrix {
267 cols: usize,
268 rows: usize,
269 data: Vec<SearchState>,
270}
271
272impl SearchMatrix {
273 fn new(cols: usize) -> Self {
274 SearchMatrix {
275 cols,
276 rows: 0,
277 data: Vec::new(),
278 }
279 }
280
281 fn resize_rows(&mut self, needed_rows: usize) {
282 debug_assert!(needed_rows > self.rows);
283 self.rows = needed_rows;
284 self.data.resize(
285 self.rows * self.cols,
286 SearchState::new(0, SearchDirection::Diagonal),
287 );
288 }
289
290 fn get(&self, row: usize, col: usize) -> SearchState {
291 debug_assert!(row < self.rows && col < self.cols);
292 self.data[row * self.cols + col]
293 }
294
295 fn set(&mut self, row: usize, col: usize, state: SearchState) {
296 debug_assert!(row < self.rows && col < self.cols);
297 self.data[row * self.cols + col] = state;
298 }
299}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304 use indoc::indoc;
305 use language::{BufferId, TextBuffer};
306 use rand::prelude::*;
307 use text::ReplicaId;
308 use util::test::{generate_marked_text, marked_text_ranges};
309
310 #[test]
311 fn test_empty_query() {
312 let buffer = TextBuffer::new(
313 ReplicaId::LOCAL,
314 BufferId::new(1).unwrap(),
315 "Hello world\nThis is a test\nFoo bar baz",
316 );
317 let snapshot = buffer.snapshot();
318
319 let mut finder = StreamingFuzzyMatcher::new(snapshot.clone());
320 assert_eq!(push(&mut finder, ""), None);
321 assert_eq!(finish(finder), None);
322 }
323
324 #[test]
325 fn test_streaming_exact_match() {
326 let buffer = TextBuffer::new(
327 ReplicaId::LOCAL,
328 BufferId::new(1).unwrap(),
329 "Hello world\nThis is a test\nFoo bar baz",
330 );
331 let snapshot = buffer.snapshot();
332
333 let mut finder = StreamingFuzzyMatcher::new(snapshot.clone());
334
335 // Push partial query
336 assert_eq!(push(&mut finder, "This"), None);
337
338 // Complete the line
339 assert_eq!(
340 push(&mut finder, " is a test\n"),
341 Some("This is a test".to_string())
342 );
343
344 // Finish should return the same result
345 assert_eq!(finish(finder), Some("This is a test".to_string()));
346 }
347
348 #[test]
349 fn test_streaming_fuzzy_match() {
350 let buffer = TextBuffer::new(
351 ReplicaId::LOCAL,
352 BufferId::new(1).unwrap(),
353 indoc! {"
354 function foo(a, b) {
355 return a + b;
356 }
357
358 function bar(x, y) {
359 return x * y;
360 }
361 "},
362 );
363 let snapshot = buffer.snapshot();
364
365 let mut finder = StreamingFuzzyMatcher::new(snapshot.clone());
366
367 // Push a fuzzy query that should match the first function
368 assert_eq!(
369 push(&mut finder, "function foo(a, c) {\n").as_deref(),
370 Some("function foo(a, b) {")
371 );
372 assert_eq!(
373 push(&mut finder, " return a + c;\n}\n").as_deref(),
374 Some(concat!(
375 "function foo(a, b) {\n",
376 " return a + b;\n",
377 "}"
378 ))
379 );
380 }
381
382 #[test]
383 fn test_incremental_improvement() {
384 let buffer = TextBuffer::new(
385 ReplicaId::LOCAL,
386 BufferId::new(1).unwrap(),
387 "Line 1\nLine 2\nLine 3\nLine 4\nLine 5",
388 );
389 let snapshot = buffer.snapshot();
390
391 let mut finder = StreamingFuzzyMatcher::new(snapshot.clone());
392
393 // No match initially
394 assert_eq!(push(&mut finder, "Lin"), None);
395
396 // Get a match when we complete a line
397 assert_eq!(push(&mut finder, "e 3\n"), Some("Line 3".to_string()));
398
399 // The match might change if we add more specific content
400 assert_eq!(
401 push(&mut finder, "Line 4\n"),
402 Some("Line 3\nLine 4".to_string())
403 );
404 assert_eq!(finish(finder), Some("Line 3\nLine 4".to_string()));
405 }
406
407 #[test]
408 fn test_incomplete_lines_buffering() {
409 let buffer = TextBuffer::new(
410 ReplicaId::LOCAL,
411 BufferId::new(1).unwrap(),
412 indoc! {"
413 The quick brown fox
414 jumps over the lazy dog
415 Pack my box with five dozen liquor jugs
416 "},
417 );
418 let snapshot = buffer.snapshot();
419
420 let mut finder = StreamingFuzzyMatcher::new(snapshot.clone());
421
422 // Push text in small chunks across line boundaries
423 assert_eq!(push(&mut finder, "jumps "), None); // No newline yet
424 assert_eq!(push(&mut finder, "over the"), None); // Still no newline
425 assert_eq!(push(&mut finder, " lazy"), None); // Still incomplete
426
427 // Complete the line
428 assert_eq!(
429 push(&mut finder, " dog\n"),
430 Some("jumps over the lazy dog".to_string())
431 );
432 }
433
434 #[test]
435 fn test_multiline_fuzzy_match() {
436 let buffer = TextBuffer::new(
437 ReplicaId::LOCAL,
438 BufferId::new(1).unwrap(),
439 indoc! {r#"
440 impl Display for User {
441 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
442 write!(f, "User: {} ({})", self.name, self.email)
443 }
444 }
445
446 impl Debug for User {
447 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
448 f.debug_struct("User")
449 .field("name", &self.name)
450 .field("email", &self.email)
451 .finish()
452 }
453 }
454 "#},
455 );
456 let snapshot = buffer.snapshot();
457
458 let mut finder = StreamingFuzzyMatcher::new(snapshot.clone());
459
460 assert_eq!(
461 push(&mut finder, "impl Debug for User {\n"),
462 Some("impl Debug for User {".to_string())
463 );
464 assert_eq!(
465 push(
466 &mut finder,
467 " fn fmt(&self, f: &mut Formatter) -> Result {\n"
468 )
469 .as_deref(),
470 Some(concat!(
471 "impl Debug for User {\n",
472 " fn fmt(&self, f: &mut Formatter) -> fmt::Result {"
473 ))
474 );
475 assert_eq!(
476 push(&mut finder, " f.debug_struct(\"User\")\n").as_deref(),
477 Some(concat!(
478 "impl Debug for User {\n",
479 " fn fmt(&self, f: &mut Formatter) -> fmt::Result {\n",
480 " f.debug_struct(\"User\")"
481 ))
482 );
483 assert_eq!(
484 push(
485 &mut finder,
486 " .field(\"name\", &self.username)\n"
487 )
488 .as_deref(),
489 Some(concat!(
490 "impl Debug for User {\n",
491 " fn fmt(&self, f: &mut Formatter) -> fmt::Result {\n",
492 " f.debug_struct(\"User\")\n",
493 " .field(\"name\", &self.name)"
494 ))
495 );
496 assert_eq!(
497 finish(finder).as_deref(),
498 Some(concat!(
499 "impl Debug for User {\n",
500 " fn fmt(&self, f: &mut Formatter) -> fmt::Result {\n",
501 " f.debug_struct(\"User\")\n",
502 " .field(\"name\", &self.name)"
503 ))
504 );
505 }
506
507 #[gpui::test(iterations = 100)]
508 fn test_resolve_location_single_line(mut rng: StdRng) {
509 assert_location_resolution(
510 concat!(
511 " Lorem\n",
512 "« ipsum»\n",
513 " dolor sit amet\n",
514 " consecteur",
515 ),
516 "ipsum",
517 &mut rng,
518 );
519 }
520
521 #[gpui::test(iterations = 100)]
522 fn test_resolve_location_multiline(mut rng: StdRng) {
523 assert_location_resolution(
524 concat!(
525 " Lorem\n",
526 "« ipsum\n",
527 " dolor sit amet»\n",
528 " consecteur",
529 ),
530 "ipsum\ndolor sit amet",
531 &mut rng,
532 );
533 }
534
535 #[gpui::test(iterations = 100)]
536 fn test_resolve_location_function_with_typo(mut rng: StdRng) {
537 assert_location_resolution(
538 indoc! {"
539 «fn foo1(a: usize) -> usize {
540 40
541 }»
542
543 fn foo2(b: usize) -> usize {
544 42
545 }
546 "},
547 "fn foo1(a: usize) -> u32 {\n40\n}",
548 &mut rng,
549 );
550 }
551
552 #[gpui::test(iterations = 100)]
553 fn test_resolve_location_class_methods(mut rng: StdRng) {
554 assert_location_resolution(
555 indoc! {"
556 class Something {
557 one() { return 1; }
558 « two() { return 2222; }
559 three() { return 333; }
560 four() { return 4444; }
561 five() { return 5555; }
562 six() { return 6666; }»
563 seven() { return 7; }
564 eight() { return 8; }
565 }
566 "},
567 indoc! {"
568 two() { return 2222; }
569 four() { return 4444; }
570 five() { return 5555; }
571 six() { return 6666; }
572 "},
573 &mut rng,
574 );
575 }
576
577 #[gpui::test(iterations = 100)]
578 fn test_resolve_location_imports_no_match(mut rng: StdRng) {
579 assert_location_resolution(
580 indoc! {"
581 use std::ops::Range;
582 use std::sync::Mutex;
583 use std::{
584 collections::HashMap,
585 env,
586 ffi::{OsStr, OsString},
587 fs,
588 io::{BufRead, BufReader},
589 mem,
590 path::{Path, PathBuf},
591 process::Command,
592 sync::LazyLock,
593 time::SystemTime,
594 };
595 "},
596 indoc! {"
597 use std::collections::{HashMap, HashSet};
598 use std::ffi::{OsStr, OsString};
599 use std::fmt::Write as _;
600 use std::fs;
601 use std::io::{BufReader, Read, Write};
602 use std::mem;
603 use std::path::{Path, PathBuf};
604 use std::process::Command;
605 use std::sync::Arc;
606 "},
607 &mut rng,
608 );
609 }
610
611 #[gpui::test(iterations = 100)]
612 fn test_resolve_location_nested_closure(mut rng: StdRng) {
613 assert_location_resolution(
614 indoc! {"
615 impl Foo {
616 fn new() -> Self {
617 Self {
618 subscriptions: vec![
619 cx.observe_window_activation(window, |editor, window, cx| {
620 let active = window.is_window_active();
621 editor.blink_manager.update(cx, |blink_manager, cx| {
622 if active {
623 blink_manager.enable(cx);
624 } else {
625 blink_manager.disable(cx);
626 }
627 });
628 }),
629 ];
630 }
631 }
632 }
633 "},
634 concat!(
635 " editor.blink_manager.update(cx, |blink_manager, cx| {\n",
636 " blink_manager.enable(cx);\n",
637 " });",
638 ),
639 &mut rng,
640 );
641 }
642
643 #[gpui::test(iterations = 100)]
644 fn test_resolve_location_tool_invocation(mut rng: StdRng) {
645 assert_location_resolution(
646 indoc! {r#"
647 let tool = cx
648 .update(|cx| working_set.tool(&tool_name, cx))
649 .map_err(|err| {
650 anyhow!("Failed to look up tool '{}': {}", tool_name, err)
651 })?;
652
653 let Some(tool) = tool else {
654 return Err(anyhow!("Tool '{}' not found", tool_name));
655 };
656
657 let project = project.clone();
658 let action_log = action_log.clone();
659 let messages = messages.clone();
660 let tool_result = cx
661 .update(|cx| tool.run(invocation.input, &messages, project, action_log, cx))
662 .map_err(|err| anyhow!("Failed to start tool '{}': {}", tool_name, err))?;
663
664 tasks.push(tool_result.output);
665 "#},
666 concat!(
667 "let tool_result = cx\n",
668 " .update(|cx| tool.run(invocation.input, &messages, project, action_log, cx))\n",
669 " .output;",
670 ),
671 &mut rng,
672 );
673 }
674
675 #[gpui::test]
676 fn test_line_hint_selection() {
677 let text = indoc! {r#"
678 fn first_function() {
679 return 42;
680 }
681
682 fn second_function() {
683 return 42;
684 }
685
686 fn third_function() {
687 return 42;
688 }
689 "#};
690
691 let buffer = TextBuffer::new(
692 ReplicaId::LOCAL,
693 BufferId::new(1).unwrap(),
694 text.to_string(),
695 );
696 let snapshot = buffer.snapshot();
697 let mut matcher = StreamingFuzzyMatcher::new(snapshot.clone());
698
699 // Given a query that matches all three functions
700 let query = "return 42;\n";
701
702 // Test with line hint pointing to second function (around line 5)
703 let best_match = matcher.push(query, Some(5)).expect("Failed to match query");
704
705 let matched_text = snapshot
706 .text_for_range(best_match.clone())
707 .collect::<String>();
708 assert!(matched_text.contains("return 42;"));
709 assert_eq!(
710 best_match,
711 63..77,
712 "Expected to match `second_function` based on the line hint"
713 );
714
715 let mut matcher = StreamingFuzzyMatcher::new(snapshot.clone());
716 matcher.push(query, None);
717 matcher.finish();
718 let best_match = matcher.select_best_match();
719 assert!(
720 best_match.is_none(),
721 "Best match should be None when query cannot be uniquely resolved"
722 );
723 }
724
725 #[track_caller]
726 fn assert_location_resolution(text_with_expected_range: &str, query: &str, rng: &mut StdRng) {
727 let (text, expected_ranges) = marked_text_ranges(text_with_expected_range, false);
728 let buffer = TextBuffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), text.clone());
729 let snapshot = buffer.snapshot();
730
731 let mut matcher = StreamingFuzzyMatcher::new(snapshot.clone());
732
733 // Split query into random chunks
734 let chunks = to_random_chunks(rng, query);
735
736 // Push chunks incrementally
737 for chunk in &chunks {
738 matcher.push(chunk, None);
739 }
740
741 let actual_ranges = matcher.finish();
742
743 // If no expected ranges, we expect no match
744 if expected_ranges.is_empty() {
745 assert!(
746 actual_ranges.is_empty(),
747 "Expected no match for query: {:?}, but found: {:?}",
748 query,
749 actual_ranges
750 );
751 } else {
752 let text_with_actual_range = generate_marked_text(&text, &actual_ranges, false);
753 pretty_assertions::assert_eq!(
754 text_with_actual_range,
755 text_with_expected_range,
756 indoc! {"
757 Query: {:?}
758 Chunks: {:?}
759 Expected marked text: {}
760 Actual marked text: {}
761 Expected ranges: {:?}
762 Actual ranges: {:?}"
763 },
764 query,
765 chunks,
766 text_with_expected_range,
767 text_with_actual_range,
768 expected_ranges,
769 actual_ranges
770 );
771 }
772 }
773
774 fn to_random_chunks(rng: &mut StdRng, input: &str) -> Vec<String> {
775 let chunk_count = rng.random_range(1..=cmp::min(input.len(), 50));
776 let mut chunk_indices = (0..input.len()).choose_multiple(rng, chunk_count);
777 chunk_indices.sort();
778 chunk_indices.push(input.len());
779
780 let mut chunks = Vec::new();
781 let mut last_ix = 0;
782 for chunk_ix in chunk_indices {
783 chunks.push(input[last_ix..chunk_ix].to_string());
784 last_ix = chunk_ix;
785 }
786 chunks
787 }
788
789 fn push(finder: &mut StreamingFuzzyMatcher, chunk: &str) -> Option<String> {
790 finder
791 .push(chunk, None)
792 .map(|range| finder.snapshot.text_for_range(range).collect::<String>())
793 }
794
795 fn finish(mut finder: StreamingFuzzyMatcher) -> Option<String> {
796 let snapshot = finder.snapshot.clone();
797 let matches = finder.finish();
798 matches
799 .first()
800 .map(|range| snapshot.text_for_range(range.clone()).collect::<String>())
801 }
802}