outline.rs

  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}