1use fuzzy::{StringMatch, StringMatchCandidate};
2use gpui::{
3 relative, AppContext, BackgroundExecutor, FontStyle, HighlightStyle, StyledText, TextStyle,
4 WhiteSpace,
5};
6use settings::Settings;
7use std::ops::Range;
8use theme::{color_alpha, ActiveTheme, ThemeSettings};
9
10/// An outline of all the symbols contained in a buffer.
11#[derive(Debug)]
12pub struct Outline<T> {
13 pub items: Vec<OutlineItem<T>>,
14 candidates: Vec<StringMatchCandidate>,
15 pub path_candidates: Vec<StringMatchCandidate>,
16 path_candidate_prefixes: Vec<usize>,
17}
18
19#[derive(Clone, Debug, PartialEq, Eq, Hash)]
20pub struct OutlineItem<T> {
21 pub depth: usize,
22 pub range: Range<T>,
23 pub text: String,
24 pub highlight_ranges: Vec<(Range<usize>, HighlightStyle)>,
25 pub name_ranges: Vec<Range<usize>>,
26 pub body_range: Option<Range<T>>,
27}
28
29impl<T> Outline<T> {
30 pub fn new(items: Vec<OutlineItem<T>>) -> Self {
31 let mut candidates = Vec::new();
32 let mut path_candidates = Vec::new();
33 let mut path_candidate_prefixes = Vec::new();
34 let mut path_text = String::new();
35 let mut path_stack = Vec::new();
36
37 for (id, item) in items.iter().enumerate() {
38 if item.depth < path_stack.len() {
39 path_stack.truncate(item.depth);
40 path_text.truncate(path_stack.last().copied().unwrap_or(0));
41 }
42 if !path_text.is_empty() {
43 path_text.push(' ');
44 }
45 path_candidate_prefixes.push(path_text.len());
46 path_text.push_str(&item.text);
47 path_stack.push(path_text.len());
48
49 let candidate_text = item
50 .name_ranges
51 .iter()
52 .map(|range| &item.text[range.start..range.end])
53 .collect::<String>();
54
55 path_candidates.push(StringMatchCandidate::new(id, path_text.clone()));
56 candidates.push(StringMatchCandidate::new(id, candidate_text));
57 }
58
59 Self {
60 candidates,
61 path_candidates,
62 path_candidate_prefixes,
63 items,
64 }
65 }
66
67 pub async fn search(&self, query: &str, executor: BackgroundExecutor) -> Vec<StringMatch> {
68 let query = query.trim_start();
69 let is_path_query = query.contains(' ');
70 let smart_case = query.chars().any(|c| c.is_uppercase());
71 let mut matches = fuzzy::match_strings(
72 if is_path_query {
73 &self.path_candidates
74 } else {
75 &self.candidates
76 },
77 query,
78 smart_case,
79 100,
80 &Default::default(),
81 executor.clone(),
82 )
83 .await;
84 matches.sort_unstable_by_key(|m| m.candidate_id);
85
86 let mut tree_matches = Vec::new();
87
88 let mut prev_item_ix = 0;
89 for mut string_match in matches {
90 let outline_match = &self.items[string_match.candidate_id];
91 string_match.string.clone_from(&outline_match.text);
92
93 if is_path_query {
94 let prefix_len = self.path_candidate_prefixes[string_match.candidate_id];
95 string_match
96 .positions
97 .retain(|position| *position >= prefix_len);
98 for position in &mut string_match.positions {
99 *position -= prefix_len;
100 }
101 } else {
102 let mut name_ranges = outline_match.name_ranges.iter();
103 let mut name_range = name_ranges.next().unwrap();
104 let mut preceding_ranges_len = 0;
105 for position in &mut string_match.positions {
106 while *position >= preceding_ranges_len + name_range.len() {
107 preceding_ranges_len += name_range.len();
108 name_range = name_ranges.next().unwrap();
109 }
110 *position = name_range.start + (*position - preceding_ranges_len);
111 }
112 }
113
114 let insertion_ix = tree_matches.len();
115 let mut cur_depth = outline_match.depth;
116 for (ix, item) in self.items[prev_item_ix..string_match.candidate_id]
117 .iter()
118 .enumerate()
119 .rev()
120 {
121 if cur_depth == 0 {
122 break;
123 }
124
125 let candidate_index = ix + prev_item_ix;
126 if item.depth == cur_depth - 1 {
127 tree_matches.insert(
128 insertion_ix,
129 StringMatch {
130 candidate_id: candidate_index,
131 score: Default::default(),
132 positions: Default::default(),
133 string: Default::default(),
134 },
135 );
136 cur_depth -= 1;
137 }
138 }
139
140 prev_item_ix = string_match.candidate_id + 1;
141 tree_matches.push(string_match);
142 }
143
144 tree_matches
145 }
146}
147
148pub fn render_item<T>(
149 outline_item: &OutlineItem<T>,
150 match_ranges: impl IntoIterator<Item = Range<usize>>,
151 cx: &AppContext,
152) -> StyledText {
153 let mut highlight_style = HighlightStyle::default();
154 highlight_style.background_color = Some(color_alpha(cx.theme().colors().text_accent, 0.3));
155 let custom_highlights = match_ranges
156 .into_iter()
157 .map(|range| (range, highlight_style));
158
159 let settings = ThemeSettings::get_global(cx);
160
161 // TODO: We probably shouldn't need to build a whole new text style here
162 // but I'm not sure how to get the current one and modify it.
163 // Before this change TextStyle::default() was used here, which was giving us the wrong font and text color.
164 let text_style = TextStyle {
165 color: cx.theme().colors().text,
166 font_family: settings.buffer_font.family.clone(),
167 font_features: settings.buffer_font.features.clone(),
168 font_size: settings.buffer_font_size(cx).into(),
169 font_weight: settings.buffer_font.weight,
170 font_style: FontStyle::Normal,
171 line_height: relative(1.),
172 background_color: None,
173 underline: None,
174 strikethrough: None,
175 white_space: WhiteSpace::Normal,
176 };
177 let highlights = gpui::combine_highlights(
178 custom_highlights,
179 outline_item.highlight_ranges.iter().cloned(),
180 );
181
182 StyledText::new(outline_item.text.clone()).with_highlights(&text_style, highlights)
183}