outline.rs

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