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    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}