1use fuzzy::{StringMatch, StringMatchCandidate};
2use gpui::{BackgroundExecutor, HighlightStyle};
3use std::ops::Range;
4
5/// An outline of all the symbols contained in a buffer.
6#[derive(Debug)]
7pub struct Outline<T> {
8 pub items: Vec<OutlineItem<T>>,
9 candidates: Vec<StringMatchCandidate>,
10 path_candidates: Vec<StringMatchCandidate>,
11 path_candidate_prefixes: Vec<usize>,
12}
13
14#[derive(Clone, Debug, PartialEq, Eq)]
15pub struct OutlineItem<T> {
16 pub depth: usize,
17 pub range: Range<T>,
18 pub text: String,
19 pub highlight_ranges: Vec<(Range<usize>, HighlightStyle)>,
20 pub name_ranges: Vec<Range<usize>>,
21}
22
23impl<T> Outline<T> {
24 pub fn new(items: Vec<OutlineItem<T>>) -> Self {
25 let mut candidates = Vec::new();
26 let mut path_candidates = Vec::new();
27 let mut path_candidate_prefixes = Vec::new();
28 let mut path_text = String::new();
29 let mut path_stack = Vec::new();
30
31 for (id, item) in items.iter().enumerate() {
32 if item.depth < path_stack.len() {
33 path_stack.truncate(item.depth);
34 path_text.truncate(path_stack.last().copied().unwrap_or(0));
35 }
36 if !path_text.is_empty() {
37 path_text.push(' ');
38 }
39 path_candidate_prefixes.push(path_text.len());
40 path_text.push_str(&item.text);
41 path_stack.push(path_text.len());
42
43 let candidate_text = item
44 .name_ranges
45 .iter()
46 .map(|range| &item.text[range.start..range.end])
47 .collect::<String>();
48
49 path_candidates.push(StringMatchCandidate::new(id, path_text.clone()));
50 candidates.push(StringMatchCandidate::new(id, candidate_text));
51 }
52
53 Self {
54 candidates,
55 path_candidates,
56 path_candidate_prefixes,
57 items,
58 }
59 }
60
61 pub async fn search(&self, query: &str, executor: BackgroundExecutor) -> Vec<StringMatch> {
62 let query = query.trim_start();
63 let is_path_query = query.contains(' ');
64 let smart_case = query.chars().any(|c| c.is_uppercase());
65 let mut matches = fuzzy::match_strings(
66 if is_path_query {
67 &self.path_candidates
68 } else {
69 &self.candidates
70 },
71 query,
72 smart_case,
73 100,
74 &Default::default(),
75 executor.clone(),
76 )
77 .await;
78 matches.sort_unstable_by_key(|m| m.candidate_id);
79
80 let mut tree_matches = Vec::new();
81
82 let mut prev_item_ix = 0;
83 for mut string_match in matches {
84 let outline_match = &self.items[string_match.candidate_id];
85 string_match.string.clone_from(&outline_match.text);
86
87 if is_path_query {
88 let prefix_len = self.path_candidate_prefixes[string_match.candidate_id];
89 string_match
90 .positions
91 .retain(|position| *position >= prefix_len);
92 for position in &mut string_match.positions {
93 *position -= prefix_len;
94 }
95 } else {
96 let mut name_ranges = outline_match.name_ranges.iter();
97 let mut name_range = name_ranges.next().unwrap();
98 let mut preceding_ranges_len = 0;
99 for position in &mut string_match.positions {
100 while *position >= preceding_ranges_len + name_range.len() {
101 preceding_ranges_len += name_range.len();
102 name_range = name_ranges.next().unwrap();
103 }
104 *position = name_range.start + (*position - preceding_ranges_len);
105 }
106 }
107
108 let insertion_ix = tree_matches.len();
109 let mut cur_depth = outline_match.depth;
110 for (ix, item) in self.items[prev_item_ix..string_match.candidate_id]
111 .iter()
112 .enumerate()
113 .rev()
114 {
115 if cur_depth == 0 {
116 break;
117 }
118
119 let candidate_index = ix + prev_item_ix;
120 if item.depth == cur_depth - 1 {
121 tree_matches.insert(
122 insertion_ix,
123 StringMatch {
124 candidate_id: candidate_index,
125 score: Default::default(),
126 positions: Default::default(),
127 string: Default::default(),
128 },
129 );
130 cur_depth -= 1;
131 }
132 }
133
134 prev_item_ix = string_match.candidate_id + 1;
135 tree_matches.push(string_match);
136 }
137
138 tree_matches
139 }
140}