
Adds a new panel: `OutlinePanel` which looks very close to project panel: <img width="256" alt="Screenshot 2024-06-10 at 23 19 05" src="https://github.com/zed-industries/zed/assets/2690773/c66e6e78-44ec-4de8-8d60-43238bb09ae9"> has similar settings and keymap (actions work in the `OutlinePanel` context and are under `outline_panel::` namespace), with two notable differences: * no "edit" actions such as cut/copy/paste/delete/etc. * directory auto folding is enabled by default Empty view: <img width="841" alt="Screenshot 2024-06-10 at 23 19 11" src="https://github.com/zed-industries/zed/assets/2690773/dc8bf37c-5a70-4fd5-9b57-76271eb7a40c"> When editor gets active, the panel displays all related files in a tree (similar to what the project panel does) and all related excerpts' outlines under each file. Same as in the project panel, directories can be expanded or collapsed, unfolded or folded; clicking file entries or outlines scrolls the buffer to the corresponding excerpt; changing editor's selection reveals the corresponding outline in the panel. The panel is applicable to any singleton buffer: <img width="1215" alt="Screenshot 2024-06-10 at 23 19 35" src="https://github.com/zed-industries/zed/assets/2690773/a087631f-5c2d-4d4d-ae25-30ab9731d528"> <img width="1728" alt="image" src="https://github.com/zed-industries/zed/assets/2690773/e4f8082c-d12d-4473-8500-e8fd1051285b"> or any multi buffer: (search multi buffer) <img width="1728" alt="Screenshot 2024-06-10 at 23 19 41" src="https://github.com/zed-industries/zed/assets/2690773/60f768a3-6716-4520-9b13-42da8fd15f50"> (diagnostics multi buffer) <img width="1728" alt="image" src="https://github.com/zed-industries/zed/assets/2690773/64e285bd-9530-4bf2-8f1f-10ee5596067c"> Release Notes: - Added an outline panel to show a "map" of the active editor
176 lines
6.1 KiB
Rust
176 lines
6.1 KiB
Rust
use fuzzy::{StringMatch, StringMatchCandidate};
|
|
use gpui::{
|
|
relative, AppContext, BackgroundExecutor, FontStyle, FontWeight, HighlightStyle, StyledText,
|
|
TextStyle, WhiteSpace,
|
|
};
|
|
use settings::Settings;
|
|
use std::ops::Range;
|
|
use theme::{ActiveTheme, ThemeSettings};
|
|
|
|
/// An outline of all the symbols contained in a buffer.
|
|
#[derive(Debug)]
|
|
pub struct Outline<T> {
|
|
pub items: Vec<OutlineItem<T>>,
|
|
candidates: Vec<StringMatchCandidate>,
|
|
path_candidates: Vec<StringMatchCandidate>,
|
|
path_candidate_prefixes: Vec<usize>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
|
pub struct OutlineItem<T> {
|
|
pub depth: usize,
|
|
pub range: Range<T>,
|
|
pub text: String,
|
|
pub highlight_ranges: Vec<(Range<usize>, HighlightStyle)>,
|
|
pub name_ranges: Vec<Range<usize>>,
|
|
}
|
|
|
|
impl<T> Outline<T> {
|
|
pub fn new(items: Vec<OutlineItem<T>>) -> Self {
|
|
let mut candidates = Vec::new();
|
|
let mut path_candidates = Vec::new();
|
|
let mut path_candidate_prefixes = Vec::new();
|
|
let mut path_text = String::new();
|
|
let mut path_stack = Vec::new();
|
|
|
|
for (id, item) in items.iter().enumerate() {
|
|
if item.depth < path_stack.len() {
|
|
path_stack.truncate(item.depth);
|
|
path_text.truncate(path_stack.last().copied().unwrap_or(0));
|
|
}
|
|
if !path_text.is_empty() {
|
|
path_text.push(' ');
|
|
}
|
|
path_candidate_prefixes.push(path_text.len());
|
|
path_text.push_str(&item.text);
|
|
path_stack.push(path_text.len());
|
|
|
|
let candidate_text = item
|
|
.name_ranges
|
|
.iter()
|
|
.map(|range| &item.text[range.start..range.end])
|
|
.collect::<String>();
|
|
|
|
path_candidates.push(StringMatchCandidate::new(id, path_text.clone()));
|
|
candidates.push(StringMatchCandidate::new(id, candidate_text));
|
|
}
|
|
|
|
Self {
|
|
candidates,
|
|
path_candidates,
|
|
path_candidate_prefixes,
|
|
items,
|
|
}
|
|
}
|
|
|
|
pub async fn search(&self, query: &str, executor: BackgroundExecutor) -> Vec<StringMatch> {
|
|
let query = query.trim_start();
|
|
let is_path_query = query.contains(' ');
|
|
let smart_case = query.chars().any(|c| c.is_uppercase());
|
|
let mut matches = fuzzy::match_strings(
|
|
if is_path_query {
|
|
&self.path_candidates
|
|
} else {
|
|
&self.candidates
|
|
},
|
|
query,
|
|
smart_case,
|
|
100,
|
|
&Default::default(),
|
|
executor.clone(),
|
|
)
|
|
.await;
|
|
matches.sort_unstable_by_key(|m| m.candidate_id);
|
|
|
|
let mut tree_matches = Vec::new();
|
|
|
|
let mut prev_item_ix = 0;
|
|
for mut string_match in matches {
|
|
let outline_match = &self.items[string_match.candidate_id];
|
|
string_match.string.clone_from(&outline_match.text);
|
|
|
|
if is_path_query {
|
|
let prefix_len = self.path_candidate_prefixes[string_match.candidate_id];
|
|
string_match
|
|
.positions
|
|
.retain(|position| *position >= prefix_len);
|
|
for position in &mut string_match.positions {
|
|
*position -= prefix_len;
|
|
}
|
|
} else {
|
|
let mut name_ranges = outline_match.name_ranges.iter();
|
|
let mut name_range = name_ranges.next().unwrap();
|
|
let mut preceding_ranges_len = 0;
|
|
for position in &mut string_match.positions {
|
|
while *position >= preceding_ranges_len + name_range.len() {
|
|
preceding_ranges_len += name_range.len();
|
|
name_range = name_ranges.next().unwrap();
|
|
}
|
|
*position = name_range.start + (*position - preceding_ranges_len);
|
|
}
|
|
}
|
|
|
|
let insertion_ix = tree_matches.len();
|
|
let mut cur_depth = outline_match.depth;
|
|
for (ix, item) in self.items[prev_item_ix..string_match.candidate_id]
|
|
.iter()
|
|
.enumerate()
|
|
.rev()
|
|
{
|
|
if cur_depth == 0 {
|
|
break;
|
|
}
|
|
|
|
let candidate_index = ix + prev_item_ix;
|
|
if item.depth == cur_depth - 1 {
|
|
tree_matches.insert(
|
|
insertion_ix,
|
|
StringMatch {
|
|
candidate_id: candidate_index,
|
|
score: Default::default(),
|
|
positions: Default::default(),
|
|
string: Default::default(),
|
|
},
|
|
);
|
|
cur_depth -= 1;
|
|
}
|
|
}
|
|
|
|
prev_item_ix = string_match.candidate_id + 1;
|
|
tree_matches.push(string_match);
|
|
}
|
|
|
|
tree_matches
|
|
}
|
|
}
|
|
|
|
pub fn render_item<T>(
|
|
outline_item: &OutlineItem<T>,
|
|
custom_highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
|
|
cx: &AppContext,
|
|
) -> StyledText {
|
|
let settings = ThemeSettings::get_global(cx);
|
|
|
|
// TODO: We probably shouldn't need to build a whole new text style here
|
|
// but I'm not sure how to get the current one and modify it.
|
|
// Before this change TextStyle::default() was used here, which was giving us the wrong font and text color.
|
|
let text_style = TextStyle {
|
|
color: cx.theme().colors().text,
|
|
font_family: settings.buffer_font.family.clone(),
|
|
font_features: settings.buffer_font.features.clone(),
|
|
font_size: settings.buffer_font_size(cx).into(),
|
|
font_weight: FontWeight::NORMAL,
|
|
font_style: FontStyle::Normal,
|
|
line_height: relative(1.),
|
|
background_color: None,
|
|
underline: None,
|
|
strikethrough: None,
|
|
white_space: WhiteSpace::Normal,
|
|
};
|
|
let highlights = gpui::combine_highlights(
|
|
custom_highlights,
|
|
outline_item.highlight_ranges.iter().cloned(),
|
|
);
|
|
|
|
StyledText::new(outline_item.text.clone()).with_highlights(&text_style, highlights)
|
|
}
|