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