Detailed changes
@@ -2602,6 +2602,7 @@ dependencies = [
"ctor",
"env_logger",
"futures",
+ "fuzzy",
"gpui",
"lazy_static",
"log",
@@ -3121,6 +3122,21 @@ version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afb2e1c3ee07430c2cf76151675e583e0f19985fa6efae47d6848a3e2c824f85"
+[[package]]
+name = "outline"
+version = "0.1.0"
+dependencies = [
+ "editor",
+ "fuzzy",
+ "gpui",
+ "language",
+ "ordered-float",
+ "postage",
+ "smol",
+ "text",
+ "workspace",
+]
+
[[package]]
name = "p256"
version = "0.9.0"
@@ -5724,6 +5740,7 @@ dependencies = [
"log-panics",
"lsp",
"num_cpus",
+ "outline",
"parking_lot",
"postage",
"project",
@@ -28,8 +28,10 @@ use language::{
BracketPair, Buffer, Diagnostic, DiagnosticSeverity, Language, Point, Selection, SelectionGoal,
TransactionId,
};
-pub use multi_buffer::{Anchor, ExcerptId, ExcerptProperties, MultiBuffer, ToOffset, ToPoint};
-use multi_buffer::{AnchorRangeExt, MultiBufferChunks, MultiBufferSnapshot};
+pub use multi_buffer::{
+ Anchor, AnchorRangeExt, ExcerptId, ExcerptProperties, MultiBuffer, ToOffset, ToPoint,
+};
+use multi_buffer::{MultiBufferChunks, MultiBufferSnapshot};
use postage::watch;
use serde::{Deserialize, Serialize};
use smallvec::SmallVec;
@@ -374,7 +376,7 @@ pub struct Editor {
blinking_paused: bool,
mode: EditorMode,
placeholder_text: Option<Arc<str>>,
- highlighted_row: Option<u32>,
+ highlighted_rows: Option<Range<u32>>,
}
pub struct EditorSnapshot {
@@ -503,7 +505,7 @@ impl Editor {
blinking_paused: false,
mode: EditorMode::Full,
placeholder_text: None,
- highlighted_row: None,
+ highlighted_rows: None,
};
let selection = Selection {
id: post_inc(&mut this.next_selection_id),
@@ -2388,6 +2390,11 @@ impl Editor {
}
pub fn move_to_beginning(&mut self, _: &MoveToBeginning, cx: &mut ViewContext<Self>) {
+ if matches!(self.mode, EditorMode::SingleLine) {
+ cx.propagate_action();
+ return;
+ }
+
let selection = Selection {
id: post_inc(&mut self.next_selection_id),
start: 0,
@@ -2405,6 +2412,11 @@ impl Editor {
}
pub fn move_to_end(&mut self, _: &MoveToEnd, cx: &mut ViewContext<Self>) {
+ if matches!(self.mode, EditorMode::SingleLine) {
+ cx.propagate_action();
+ return;
+ }
+
let cursor = self.buffer.read(cx).read(cx).len();
let selection = Selection {
id: post_inc(&mut self.next_selection_id),
@@ -3544,12 +3556,12 @@ impl Editor {
.update(cx, |map, cx| map.set_wrap_width(width, cx))
}
- pub fn set_highlighted_row(&mut self, row: Option<u32>) {
- self.highlighted_row = row;
+ pub fn set_highlighted_rows(&mut self, rows: Option<Range<u32>>) {
+ self.highlighted_rows = rows;
}
- pub fn highlighted_row(&mut self) -> Option<u32> {
- self.highlighted_row
+ pub fn highlighted_rows(&self) -> Option<Range<u32>> {
+ self.highlighted_rows.clone()
}
fn next_blink_epoch(&mut self) -> usize {
@@ -7,6 +7,8 @@ use clock::ReplicaId;
use collections::{BTreeMap, HashMap};
use gpui::{
color::Color,
+ elements::layout_highlighted_chunks,
+ fonts::HighlightStyle,
geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
@@ -19,7 +21,7 @@ use gpui::{
MutableAppContext, PaintContext, Quad, Scene, SizeConstraint, ViewContext, WeakViewHandle,
};
use json::json;
-use language::{Bias, Chunk};
+use language::Bias;
use smallvec::SmallVec;
use std::{
cmp::{self, Ordering},
@@ -263,12 +265,16 @@ impl EditorElement {
}
}
- if let Some(highlighted_row) = layout.highlighted_row {
+ if let Some(highlighted_rows) = &layout.highlighted_rows {
let origin = vec2f(
bounds.origin_x(),
- bounds.origin_y() + (layout.line_height * highlighted_row as f32) - scroll_top,
+ bounds.origin_y() + (layout.line_height * highlighted_rows.start as f32)
+ - scroll_top,
+ );
+ let size = vec2f(
+ bounds.width(),
+ layout.line_height * highlighted_rows.len() as f32,
);
- let size = vec2f(bounds.width(), layout.line_height);
cx.scene.push_quad(Quad {
bounds: RectF::new(origin, size),
background: Some(style.highlighted_line_background),
@@ -537,86 +543,37 @@ impl EditorElement {
)
})
.collect();
- }
-
- let style = &self.settings.style;
- let mut prev_font_properties = style.text.font_properties.clone();
- let mut prev_font_id = style.text.font_id;
-
- let mut layouts = Vec::with_capacity(rows.len());
- let mut line = String::new();
- let mut styles = Vec::new();
- let mut row = rows.start;
- let mut line_exceeded_max_len = false;
- let chunks = snapshot.chunks(rows.clone(), Some(&style.syntax));
-
- let newline_chunk = Chunk {
- text: "\n",
- ..Default::default()
- };
- 'outer: for chunk in chunks.chain([newline_chunk]) {
- for (ix, mut line_chunk) in chunk.text.split('\n').enumerate() {
- if ix > 0 {
- layouts.push(cx.text_layout_cache.layout_str(
- &line,
- style.text.font_size,
- &styles,
- ));
- line.clear();
- styles.clear();
- row += 1;
- line_exceeded_max_len = false;
- if row == rows.end {
- break 'outer;
- }
- }
-
- if !line_chunk.is_empty() && !line_exceeded_max_len {
- let highlight_style =
- chunk.highlight_style.unwrap_or(style.text.clone().into());
- // Avoid a lookup if the font properties match the previous ones.
- let font_id = if highlight_style.font_properties == prev_font_properties {
- prev_font_id
- } else {
- cx.font_cache
- .select_font(
- style.text.font_family_id,
- &highlight_style.font_properties,
- )
- .unwrap_or(style.text.font_id)
- };
-
- if line.len() + line_chunk.len() > MAX_LINE_LEN {
- let mut chunk_len = MAX_LINE_LEN - line.len();
- while !line_chunk.is_char_boundary(chunk_len) {
- chunk_len -= 1;
+ } else {
+ let style = &self.settings.style;
+ let chunks = snapshot
+ .chunks(rows.clone(), Some(&style.syntax))
+ .map(|chunk| {
+ let highlight = if let Some(severity) = chunk.diagnostic {
+ let underline = Some(super::diagnostic_style(severity, true, style).text);
+ if let Some(mut highlight) = chunk.highlight_style {
+ highlight.underline = underline;
+ Some(highlight)
+ } else {
+ Some(HighlightStyle {
+ underline,
+ color: style.text.color,
+ font_properties: style.text.font_properties,
+ })
}
- line_chunk = &line_chunk[..chunk_len];
- line_exceeded_max_len = true;
- }
-
- let underline = if let Some(severity) = chunk.diagnostic {
- Some(super::diagnostic_style(severity, true, style).text)
} else {
- highlight_style.underline
+ chunk.highlight_style
};
-
- line.push_str(line_chunk);
- styles.push((
- line_chunk.len(),
- RunStyle {
- font_id,
- color: highlight_style.color,
- underline,
- },
- ));
- prev_font_id = font_id;
- prev_font_properties = highlight_style.font_properties;
- }
- }
+ (chunk.text, highlight)
+ });
+ layout_highlighted_chunks(
+ chunks,
+ &style.text,
+ &cx.text_layout_cache,
+ &cx.font_cache,
+ MAX_LINE_LEN,
+ rows.len() as usize,
+ )
}
-
- layouts
}
fn layout_blocks(
@@ -640,15 +597,20 @@ impl EditorElement {
.to_display_point(snapshot)
.row();
- let anchor_x = text_x + if rows.contains(&anchor_row) {
- line_layouts[(anchor_row - rows.start) as usize]
- .x_for_index(block.column() as usize)
- } else {
- layout_line(anchor_row, snapshot, style, cx.text_layout_cache)
- .x_for_index(block.column() as usize)
- };
+ let anchor_x = text_x
+ + if rows.contains(&anchor_row) {
+ line_layouts[(anchor_row - rows.start) as usize]
+ .x_for_index(block.column() as usize)
+ } else {
+ layout_line(anchor_row, snapshot, style, cx.text_layout_cache)
+ .x_for_index(block.column() as usize)
+ };
- let mut element = block.render(&BlockContext { cx, anchor_x, line_number_x, });
+ let mut element = block.render(&BlockContext {
+ cx,
+ anchor_x,
+ line_number_x,
+ });
element.layout(
SizeConstraint {
min: Vector2F::zero(),
@@ -750,9 +712,9 @@ impl Element for EditorElement {
let mut selections = HashMap::default();
let mut active_rows = BTreeMap::new();
- let mut highlighted_row = None;
+ let mut highlighted_rows = None;
self.update_view(cx.app, |view, cx| {
- highlighted_row = view.highlighted_row();
+ highlighted_rows = view.highlighted_rows();
let display_map = view.display_map.update(cx, |map, cx| map.snapshot(cx));
let local_selections = view
@@ -831,7 +793,7 @@ impl Element for EditorElement {
snapshot,
style: self.settings.style.clone(),
active_rows,
- highlighted_row,
+ highlighted_rows,
line_layouts,
line_number_layouts,
blocks,
@@ -962,7 +924,7 @@ pub struct LayoutState {
style: EditorStyle,
snapshot: EditorSnapshot,
active_rows: BTreeMap<u32, bool>,
- highlighted_row: Option<u32>,
+ highlighted_rows: Option<Range<u32>>,
line_layouts: Vec<text_layout::Line>,
line_number_layouts: Vec<Option<text_layout::Line>>,
blocks: Vec<(u32, ElementBox)>,
@@ -6,8 +6,8 @@ use clock::ReplicaId;
use collections::{HashMap, HashSet};
use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task};
use language::{
- Buffer, BufferChunks, BufferSnapshot, Chunk, DiagnosticEntry, Event, File, Language, Selection,
- ToOffset as _, ToPoint as _, TransactionId,
+ Buffer, BufferChunks, BufferSnapshot, Chunk, DiagnosticEntry, Event, File, Language, Outline,
+ OutlineItem, Selection, ToOffset as _, ToPoint as _, TransactionId,
};
use std::{
cell::{Ref, RefCell},
@@ -1698,6 +1698,26 @@ impl MultiBufferSnapshot {
})
}
+ pub fn outline(&self, theme: Option<&SyntaxTheme>) -> Option<Outline<Anchor>> {
+ let buffer = self.as_singleton()?;
+ let outline = buffer.outline(theme)?;
+ let excerpt_id = &self.excerpts.iter().next().unwrap().id;
+ Some(Outline::new(
+ outline
+ .items
+ .into_iter()
+ .map(|item| OutlineItem {
+ depth: item.depth,
+ range: self.anchor_in_excerpt(excerpt_id.clone(), item.range.start)
+ ..self.anchor_in_excerpt(excerpt_id.clone(), item.range.end),
+ text: item.text,
+ highlight_ranges: item.highlight_ranges,
+ name_ranges: item.name_ranges,
+ })
+ .collect(),
+ ))
+ }
+
fn buffer_snapshot_for_excerpt<'a>(
&'a self,
excerpt_id: &'a ExcerptId,
@@ -3,11 +3,7 @@ use fuzzy::PathMatch;
use gpui::{
action,
elements::*,
- keymap::{
- self,
- menu::{SelectNext, SelectPrev},
- Binding,
- },
+ keymap::{self, Binding},
AppContext, Axis, Entity, ModelHandle, MutableAppContext, RenderContext, Task, View,
ViewContext, ViewHandle, WeakViewHandle,
};
@@ -22,7 +18,10 @@ use std::{
},
};
use util::post_inc;
-use workspace::{Settings, Workspace};
+use workspace::{
+ menu::{Confirm, SelectNext, SelectPrev},
+ Settings, Workspace,
+};
pub struct FileFinder {
handle: WeakViewHandle<Self>,
@@ -40,7 +39,6 @@ pub struct FileFinder {
}
action!(Toggle);
-action!(Confirm);
action!(Select, ProjectPath);
pub fn init(cx: &mut MutableAppContext) {
@@ -53,7 +51,6 @@ pub fn init(cx: &mut MutableAppContext) {
cx.add_bindings(vec![
Binding::new("cmd-p", Toggle, None),
Binding::new("escape", Toggle, Some("FileFinder")),
- Binding::new("enter", Confirm, Some("FileFinder")),
]);
}
@@ -353,7 +350,8 @@ impl FileFinder {
let mat = &self.matches[selected_index];
self.selected = Some((mat.worktree_id, mat.path.clone()));
}
- self.list_state.scroll_to(selected_index);
+ self.list_state
+ .scroll_to(ScrollTarget::Show(selected_index));
cx.notify();
}
@@ -364,7 +362,8 @@ impl FileFinder {
let mat = &self.matches[selected_index];
self.selected = Some((mat.worktree_id, mat.path.clone()));
}
- self.list_state.scroll_to(selected_index);
+ self.list_state
+ .scroll_to(ScrollTarget::Show(selected_index));
cx.notify();
}
@@ -415,7 +414,8 @@ impl FileFinder {
}
self.latest_search_query = query;
self.latest_search_did_cancel = did_cancel;
- self.list_state.scroll_to(self.selected_index());
+ self.list_state
+ .scroll_to(ScrollTarget::Show(self.selected_index()));
cx.notify();
}
}
@@ -9,6 +9,7 @@ impl CharBag {
}
fn insert(&mut self, c: char) {
+ let c = c.to_ascii_lowercase();
if c >= 'a' && c <= 'z' {
let mut count = self.0;
let idx = c as u8 - 'a' as u8;
@@ -55,6 +55,7 @@ pub struct PathMatch {
#[derive(Clone, Debug)]
pub struct StringMatchCandidate {
+ pub id: usize,
pub string: String,
pub char_bag: CharBag,
}
@@ -109,6 +110,7 @@ impl<'a> MatchCandidate for &'a StringMatchCandidate {
#[derive(Clone, Debug)]
pub struct StringMatch {
+ pub candidate_id: usize,
pub score: f64,
pub positions: Vec<usize>,
pub string: String,
@@ -116,7 +118,7 @@ pub struct StringMatch {
impl PartialEq for StringMatch {
fn eq(&self, other: &Self) -> bool {
- self.score.eq(&other.score)
+ self.cmp(other).is_eq()
}
}
@@ -133,13 +135,13 @@ impl Ord for StringMatch {
self.score
.partial_cmp(&other.score)
.unwrap_or(Ordering::Equal)
- .then_with(|| self.string.cmp(&other.string))
+ .then_with(|| self.candidate_id.cmp(&other.candidate_id))
}
}
impl PartialEq for PathMatch {
fn eq(&self, other: &Self) -> bool {
- self.score.eq(&other.score)
+ self.cmp(other).is_eq()
}
}
@@ -187,8 +189,8 @@ pub async fn match_strings(
for (segment_idx, results) in segment_results.iter_mut().enumerate() {
let cancel_flag = &cancel_flag;
scope.spawn(async move {
- let segment_start = segment_idx * segment_size;
- let segment_end = segment_start + segment_size;
+ let segment_start = cmp::min(segment_idx * segment_size, candidates.len());
+ let segment_end = cmp::min(segment_start + segment_size, candidates.len());
let mut matcher = Matcher::new(
query,
lowercase_query,
@@ -330,6 +332,7 @@ impl<'a> Matcher<'a> {
results,
cancel_flag,
|candidate, score| StringMatch {
+ candidate_id: candidate.id,
score,
positions: Vec::new(),
string: candidate.string.to_string(),
@@ -433,13 +436,17 @@ impl<'a> Matcher<'a> {
}
}
- fn find_last_positions(&mut self, prefix: &[char], path: &[char]) -> bool {
- let mut path = path.iter();
- let mut prefix_iter = prefix.iter();
- for (i, char) in self.query.iter().enumerate().rev() {
- if let Some(j) = path.rposition(|c| c == char) {
- self.last_positions[i] = j + prefix.len();
- } else if let Some(j) = prefix_iter.rposition(|c| c == char) {
+ fn find_last_positions(
+ &mut self,
+ lowercase_prefix: &[char],
+ lowercase_candidate: &[char],
+ ) -> bool {
+ let mut lowercase_prefix = lowercase_prefix.iter();
+ let mut lowercase_candidate = lowercase_candidate.iter();
+ for (i, char) in self.lowercase_query.iter().enumerate().rev() {
+ if let Some(j) = lowercase_candidate.rposition(|c| c == char) {
+ self.last_positions[i] = j + lowercase_prefix.len();
+ } else if let Some(j) = lowercase_prefix.rposition(|c| c == char) {
self.last_positions[i] = j;
} else {
return false;
@@ -26,7 +26,7 @@ pub struct GoToLine {
line_editor: ViewHandle<Editor>,
active_editor: ViewHandle<Editor>,
restore_state: Option<RestoreState>,
- line_selection: Option<Selection<usize>>,
+ line_selection_id: Option<usize>,
cursor_point: Point,
max_point: Point,
}
@@ -84,7 +84,7 @@ impl GoToLine {
line_editor,
active_editor,
restore_state,
- line_selection: None,
+ line_selection_id: None,
cursor_point,
max_point,
}
@@ -139,13 +139,18 @@ impl GoToLine {
column.map(|column| column.saturating_sub(1)).unwrap_or(0),
)
}) {
- self.line_selection = self.active_editor.update(cx, |active_editor, cx| {
+ self.line_selection_id = self.active_editor.update(cx, |active_editor, cx| {
let snapshot = active_editor.snapshot(cx).display_snapshot;
let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left);
let display_point = point.to_display_point(&snapshot);
+ let row = display_point.row();
active_editor.select_ranges([point..point], Some(Autoscroll::Center), cx);
- active_editor.set_highlighted_row(Some(display_point.row()));
- Some(active_editor.newest_selection(&snapshot.buffer_snapshot))
+ active_editor.set_highlighted_rows(Some(row..row + 1));
+ Some(
+ active_editor
+ .newest_selection::<usize>(&snapshot.buffer_snapshot)
+ .id,
+ )
});
cx.notify();
}
@@ -159,14 +164,14 @@ impl Entity for GoToLine {
type Event = Event;
fn release(&mut self, cx: &mut MutableAppContext) {
- let line_selection = self.line_selection.take();
+ let line_selection_id = self.line_selection_id.take();
let restore_state = self.restore_state.take();
self.active_editor.update(cx, |editor, cx| {
- editor.set_highlighted_row(None);
- if let Some((line_selection, restore_state)) = line_selection.zip(restore_state) {
+ editor.set_highlighted_rows(None);
+ if let Some((line_selection_id, restore_state)) = line_selection_id.zip(restore_state) {
let newest_selection =
editor.newest_selection::<usize>(&editor.buffer().read(cx).read(cx));
- if line_selection.id == newest_selection.id {
+ if line_selection_id == newest_selection.id {
editor.set_scroll_position(restore_state.scroll_position, cx);
editor.update_selections(restore_state.selections, None, cx);
}
@@ -219,6 +224,4 @@ impl View for GoToLine {
fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
cx.focus(&self.line_editor);
}
-
- fn on_blur(&mut self, _: &mut ViewContext<Self>) {}
}
@@ -52,6 +52,11 @@ impl Container {
self
}
+ pub fn with_margin_bottom(mut self, margin: f32) -> Self {
+ self.style.margin.bottom = margin;
+ self
+ }
+
pub fn with_margin_left(mut self, margin: f32) -> Self {
self.style.margin.left = margin;
self
@@ -1,13 +1,16 @@
+use std::{ops::Range, sync::Arc};
+
use crate::{
color::Color,
- fonts::TextStyle,
+ fonts::{HighlightStyle, TextStyle},
geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
},
json::{ToJson, Value},
- text_layout::{Line, ShapedBoundary},
- DebugContext, Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint,
+ text_layout::{Line, RunStyle, ShapedBoundary},
+ DebugContext, Element, Event, EventContext, FontCache, LayoutContext, PaintContext,
+ SizeConstraint, TextLayoutCache,
};
use serde_json::json;
@@ -15,10 +18,12 @@ pub struct Text {
text: String,
style: TextStyle,
soft_wrap: bool,
+ highlights: Vec<(Range<usize>, HighlightStyle)>,
}
pub struct LayoutState {
- lines: Vec<(Line, Vec<ShapedBoundary>)>,
+ shaped_lines: Vec<Line>,
+ wrap_boundaries: Vec<Vec<ShapedBoundary>>,
line_height: f32,
}
@@ -28,6 +33,7 @@ impl Text {
text,
style,
soft_wrap: true,
+ highlights: Vec::new(),
}
}
@@ -36,6 +42,11 @@ impl Text {
self
}
+ pub fn with_highlights(mut self, runs: Vec<(Range<usize>, HighlightStyle)>) -> Self {
+ self.highlights = runs;
+ self
+ }
+
pub fn with_soft_wrap(mut self, soft_wrap: bool) -> Self {
self.soft_wrap = soft_wrap;
self
@@ -51,32 +62,59 @@ impl Element for Text {
constraint: SizeConstraint,
cx: &mut LayoutContext,
) -> (Vector2F, Self::LayoutState) {
- let font_id = self.style.font_id;
- let line_height = cx.font_cache.line_height(font_id, self.style.font_size);
+ // Convert the string and highlight ranges into an iterator of highlighted chunks.
+ let mut offset = 0;
+ let mut highlight_ranges = self.highlights.iter().peekable();
+ let chunks = std::iter::from_fn(|| {
+ let result;
+ if let Some((range, highlight)) = highlight_ranges.peek() {
+ if offset < range.start {
+ result = Some((&self.text[offset..range.start], None));
+ offset = range.start;
+ } else {
+ result = Some((&self.text[range.clone()], Some(*highlight)));
+ highlight_ranges.next();
+ offset = range.end;
+ }
+ } else if offset < self.text.len() {
+ result = Some((&self.text[offset..], None));
+ offset = self.text.len();
+ } else {
+ result = None;
+ }
+ result
+ });
- let mut wrapper = cx.font_cache.line_wrapper(font_id, self.style.font_size);
- let mut lines = Vec::new();
+ // Perform shaping on these highlighted chunks
+ let shaped_lines = layout_highlighted_chunks(
+ chunks,
+ &self.style,
+ cx.text_layout_cache,
+ &cx.font_cache,
+ usize::MAX,
+ self.text.matches('\n').count() + 1,
+ );
+
+ // If line wrapping is enabled, wrap each of the shaped lines.
+ let font_id = self.style.font_id;
let mut line_count = 0;
let mut max_line_width = 0_f32;
- for line in self.text.lines() {
- let shaped_line = cx.text_layout_cache.layout_str(
- line,
- self.style.font_size,
- &[(line.len(), self.style.to_run())],
- );
- let wrap_boundaries = if self.soft_wrap {
- wrapper
- .wrap_shaped_line(line, &shaped_line, constraint.max.x())
- .collect::<Vec<_>>()
+ let mut wrap_boundaries = Vec::new();
+ let mut wrapper = cx.font_cache.line_wrapper(font_id, self.style.font_size);
+ for (line, shaped_line) in self.text.lines().zip(&shaped_lines) {
+ if self.soft_wrap {
+ let boundaries = wrapper
+ .wrap_shaped_line(line, shaped_line, constraint.max.x())
+ .collect::<Vec<_>>();
+ line_count += boundaries.len() + 1;
+ wrap_boundaries.push(boundaries);
} else {
- Vec::new()
- };
-
+ line_count += 1;
+ }
max_line_width = max_line_width.max(shaped_line.width());
- line_count += wrap_boundaries.len() + 1;
- lines.push((shaped_line, wrap_boundaries));
}
+ let line_height = cx.font_cache.line_height(font_id, self.style.font_size);
let size = vec2f(
max_line_width
.ceil()
@@ -84,7 +122,14 @@ impl Element for Text {
.min(constraint.max.x()),
(line_height * line_count as f32).ceil(),
);
- (size, LayoutState { lines, line_height })
+ (
+ size,
+ LayoutState {
+ shaped_lines,
+ wrap_boundaries,
+ line_height,
+ },
+ )
}
fn paint(
@@ -95,8 +140,10 @@ impl Element for Text {
cx: &mut PaintContext,
) -> Self::PaintState {
let mut origin = bounds.origin();
- for (line, wrap_boundaries) in &layout.lines {
- let wrapped_line_boundaries = RectF::new(
+ let empty = Vec::new();
+ for (ix, line) in layout.shaped_lines.iter().enumerate() {
+ let wrap_boundaries = layout.wrap_boundaries.get(ix).unwrap_or(&empty);
+ let boundaries = RectF::new(
origin,
vec2f(
bounds.width(),
@@ -104,16 +151,20 @@ impl Element for Text {
),
);
- if wrapped_line_boundaries.intersects(visible_bounds) {
- line.paint_wrapped(
- origin,
- visible_bounds,
- layout.line_height,
- wrap_boundaries.iter().copied(),
- cx,
- );
+ if boundaries.intersects(visible_bounds) {
+ if self.soft_wrap {
+ line.paint_wrapped(
+ origin,
+ visible_bounds,
+ layout.line_height,
+ wrap_boundaries.iter().copied(),
+ cx,
+ );
+ } else {
+ line.paint(origin, visible_bounds, layout.line_height, cx);
+ }
}
- origin.set_y(wrapped_line_boundaries.max_y());
+ origin.set_y(boundaries.max_y());
}
}
@@ -143,3 +194,71 @@ impl Element for Text {
})
}
}
+
+/// Perform text layout on a series of highlighted chunks of text.
+pub fn layout_highlighted_chunks<'a>(
+ chunks: impl Iterator<Item = (&'a str, Option<HighlightStyle>)>,
+ style: &'a TextStyle,
+ text_layout_cache: &'a TextLayoutCache,
+ font_cache: &'a Arc<FontCache>,
+ max_line_len: usize,
+ max_line_count: usize,
+) -> Vec<Line> {
+ let mut layouts = Vec::with_capacity(max_line_count);
+ let mut prev_font_properties = style.font_properties.clone();
+ let mut prev_font_id = style.font_id;
+ let mut line = String::new();
+ let mut styles = Vec::new();
+ let mut row = 0;
+ let mut line_exceeded_max_len = false;
+ for (chunk, highlight_style) in chunks.chain([("\n", None)]) {
+ for (ix, mut line_chunk) in chunk.split('\n').enumerate() {
+ if ix > 0 {
+ layouts.push(text_layout_cache.layout_str(&line, style.font_size, &styles));
+ line.clear();
+ styles.clear();
+ row += 1;
+ line_exceeded_max_len = false;
+ if row == max_line_count {
+ return layouts;
+ }
+ }
+
+ if !line_chunk.is_empty() && !line_exceeded_max_len {
+ let highlight_style = highlight_style.unwrap_or(style.clone().into());
+
+ // Avoid a lookup if the font properties match the previous ones.
+ let font_id = if highlight_style.font_properties == prev_font_properties {
+ prev_font_id
+ } else {
+ font_cache
+ .select_font(style.font_family_id, &highlight_style.font_properties)
+ .unwrap_or(style.font_id)
+ };
+
+ if line.len() + line_chunk.len() > max_line_len {
+ let mut chunk_len = max_line_len - line.len();
+ while !line_chunk.is_char_boundary(chunk_len) {
+ chunk_len -= 1;
+ }
+ line_chunk = &line_chunk[..chunk_len];
+ line_exceeded_max_len = true;
+ }
+
+ line.push_str(line_chunk);
+ styles.push((
+ line_chunk.len(),
+ RunStyle {
+ font_id,
+ color: highlight_style.color,
+ underline: highlight_style.underline,
+ },
+ ));
+ prev_font_id = font_id;
+ prev_font_properties = highlight_style.font_properties;
+ }
+ }
+ }
+
+ layouts
+}
@@ -14,9 +14,15 @@ use std::{cmp, ops::Range, sync::Arc};
#[derive(Clone, Default)]
pub struct UniformListState(Arc<Mutex<StateInner>>);
+#[derive(Debug)]
+pub enum ScrollTarget {
+ Show(usize),
+ Center(usize),
+}
+
impl UniformListState {
- pub fn scroll_to(&self, item_ix: usize) {
- self.0.lock().scroll_to = Some(item_ix);
+ pub fn scroll_to(&self, scroll_to: ScrollTarget) {
+ self.0.lock().scroll_to = Some(scroll_to);
}
pub fn scroll_top(&self) -> f32 {
@@ -27,7 +33,7 @@ impl UniformListState {
#[derive(Default)]
struct StateInner {
scroll_top: f32,
- scroll_to: Option<usize>,
+ scroll_to: Option<ScrollTarget>,
}
pub struct LayoutState {
@@ -93,20 +99,38 @@ where
fn autoscroll(&mut self, scroll_max: f32, list_height: f32, item_height: f32) {
let mut state = self.state.0.lock();
- if state.scroll_top > scroll_max {
- state.scroll_top = scroll_max;
- }
+ if let Some(scroll_to) = state.scroll_to.take() {
+ let item_ix;
+ let center;
+ match scroll_to {
+ ScrollTarget::Show(ix) => {
+ item_ix = ix;
+ center = false;
+ }
+ ScrollTarget::Center(ix) => {
+ item_ix = ix;
+ center = true;
+ }
+ }
- if let Some(item_ix) = state.scroll_to.take() {
let item_top = self.padding_top + item_ix as f32 * item_height;
let item_bottom = item_top + item_height;
-
- if item_top < state.scroll_top {
- state.scroll_top = item_top;
- } else if item_bottom > (state.scroll_top + list_height) {
- state.scroll_top = item_bottom - list_height;
+ if center {
+ let item_center = item_top + item_height / 2.;
+ state.scroll_top = (item_center - list_height / 2.).max(0.);
+ } else {
+ let scroll_bottom = state.scroll_top + list_height;
+ if item_top < state.scroll_top {
+ state.scroll_top = item_top;
+ } else if item_bottom > scroll_bottom {
+ state.scroll_top = item_bottom - list_height;
+ }
}
}
+
+ if state.scroll_top > scroll_max {
+ state.scroll_top = scroll_max;
+ }
}
fn scroll_top(&self) -> f32 {
@@ -30,7 +30,7 @@ pub struct TextStyle {
pub underline: Option<Color>,
}
-#[derive(Copy, Clone, Debug, Default)]
+#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub struct HighlightStyle {
pub color: Color,
pub font_properties: Properties,
@@ -23,6 +23,7 @@ struct Pending {
context: Option<Context>,
}
+#[derive(Default)]
pub struct Keymap(Vec<Binding>);
pub struct Binding {
@@ -153,24 +154,6 @@ impl Keymap {
}
}
-pub mod menu {
- use crate::action;
-
- action!(SelectPrev);
- action!(SelectNext);
-}
-
-impl Default for Keymap {
- fn default() -> Self {
- Self(vec![
- Binding::new("up", menu::SelectPrev, Some("menu")),
- Binding::new("ctrl-p", menu::SelectPrev, Some("menu")),
- Binding::new("down", menu::SelectNext, Some("menu")),
- Binding::new("ctrl-n", menu::SelectNext, Some("menu")),
- ])
- }
-}
-
impl Binding {
pub fn new<A: Action>(keystrokes: &str, action: A, context: Option<&str>) -> Self {
let context = if let Some(context) = context {
@@ -19,6 +19,7 @@ test-support = [
[dependencies]
clock = { path = "../clock" }
collections = { path = "../collections" }
+fuzzy = { path = "../fuzzy" }
gpui = { path = "../gpui" }
lsp = { path = "../lsp" }
rpc = { path = "../rpc" }
@@ -6,7 +6,8 @@ pub use crate::{
};
use crate::{
diagnostic_set::{DiagnosticEntry, DiagnosticGroup},
- range_from_lsp,
+ outline::OutlineItem,
+ range_from_lsp, Outline,
};
use anyhow::{anyhow, Result};
use clock::ReplicaId;
@@ -193,7 +194,7 @@ pub trait File {
fn as_any(&self) -> &dyn Any;
}
-struct QueryCursorHandle(Option<QueryCursor>);
+pub(crate) struct QueryCursorHandle(Option<QueryCursor>);
#[derive(Clone)]
struct SyntaxTree {
@@ -1264,6 +1265,13 @@ impl Buffer {
self.edit_internal(ranges_iter, new_text, true, cx)
}
+ /*
+ impl Buffer
+ pub fn edit
+ pub fn edit_internal
+ pub fn edit_with_autoindent
+ */
+
pub fn edit_internal<I, S, T>(
&mut self,
ranges_iter: I,
@@ -1827,6 +1835,110 @@ impl BufferSnapshot {
}
}
+ pub fn outline(&self, theme: Option<&SyntaxTheme>) -> Option<Outline<Anchor>> {
+ let tree = self.tree.as_ref()?;
+ let grammar = self
+ .language
+ .as_ref()
+ .and_then(|language| language.grammar.as_ref())?;
+
+ let mut cursor = QueryCursorHandle::new();
+ let matches = cursor.matches(
+ &grammar.outline_query,
+ tree.root_node(),
+ TextProvider(self.as_rope()),
+ );
+
+ let mut chunks = self.chunks(0..self.len(), theme);
+
+ let item_capture_ix = grammar.outline_query.capture_index_for_name("item")?;
+ let name_capture_ix = grammar.outline_query.capture_index_for_name("name")?;
+ let context_capture_ix = grammar
+ .outline_query
+ .capture_index_for_name("context")
+ .unwrap_or(u32::MAX);
+
+ let mut stack = Vec::<Range<usize>>::new();
+ let items = matches
+ .filter_map(|mat| {
+ let item_node = mat.nodes_for_capture_index(item_capture_ix).next()?;
+ let range = item_node.start_byte()..item_node.end_byte();
+ let mut text = String::new();
+ let mut name_ranges = Vec::new();
+ let mut highlight_ranges = Vec::new();
+
+ for capture in mat.captures {
+ let node_is_name;
+ if capture.index == name_capture_ix {
+ node_is_name = true;
+ } else if capture.index == context_capture_ix {
+ node_is_name = false;
+ } else {
+ continue;
+ }
+
+ let range = capture.node.start_byte()..capture.node.end_byte();
+ if !text.is_empty() {
+ text.push(' ');
+ }
+ if node_is_name {
+ let mut start = text.len();
+ let end = start + range.len();
+
+ // When multiple names are captured, then the matcheable text
+ // includes the whitespace in between the names.
+ if !name_ranges.is_empty() {
+ start -= 1;
+ }
+
+ name_ranges.push(start..end);
+ }
+
+ let mut offset = range.start;
+ chunks.seek(offset);
+ while let Some(mut chunk) = chunks.next() {
+ if chunk.text.len() > range.end - offset {
+ chunk.text = &chunk.text[0..(range.end - offset)];
+ offset = range.end;
+ } else {
+ offset += chunk.text.len();
+ }
+ if let Some(style) = chunk.highlight_style {
+ let start = text.len();
+ let end = start + chunk.text.len();
+ highlight_ranges.push((start..end, style));
+ }
+ text.push_str(chunk.text);
+ if offset >= range.end {
+ break;
+ }
+ }
+ }
+
+ while stack.last().map_or(false, |prev_range| {
+ !prev_range.contains(&range.start) || !prev_range.contains(&range.end)
+ }) {
+ stack.pop();
+ }
+ stack.push(range.clone());
+
+ Some(OutlineItem {
+ depth: stack.len() - 1,
+ range: self.anchor_after(range.start)..self.anchor_before(range.end),
+ text,
+ highlight_ranges,
+ name_ranges,
+ })
+ })
+ .collect::<Vec<_>>();
+
+ if items.is_empty() {
+ None
+ } else {
+ Some(Outline::new(items))
+ }
+ }
+
pub fn enclosing_bracket_ranges<T: ToOffset>(
&self,
range: Range<T>,
@@ -1854,6 +1966,12 @@ impl BufferSnapshot {
.min_by_key(|(open_range, close_range)| close_range.end - open_range.start)
}
+ /*
+ impl BufferSnapshot
+ pub fn remote_selections_in_range(&self, Range<Anchor>) -> impl Iterator<Item = (ReplicaId, impl Iterator<Item = &Selection<Anchor>>)>
+ pub fn remote_selections_in_range(&self, Range<Anchor>) -> impl Iterator<Item = (ReplicaId, i
+ */
+
pub fn remote_selections_in_range<'a>(
&'a self,
range: Range<Anchor>,
@@ -2108,7 +2226,7 @@ impl<'a> Iterator for BufferChunks<'a> {
}
impl QueryCursorHandle {
- fn new() -> Self {
+ pub(crate) fn new() -> Self {
QueryCursorHandle(Some(
QUERY_CURSORS
.lock()
@@ -1,6 +1,7 @@
mod buffer;
mod diagnostic_set;
mod highlight_map;
+mod outline;
pub mod proto;
#[cfg(test)]
mod tests;
@@ -13,6 +14,7 @@ pub use diagnostic_set::DiagnosticEntry;
use gpui::AppContext;
use highlight_map::HighlightMap;
use lazy_static::lazy_static;
+pub use outline::{Outline, OutlineItem};
use parking_lot::Mutex;
use serde::Deserialize;
use std::{ops::Range, path::Path, str, sync::Arc};
@@ -74,6 +76,7 @@ pub struct Grammar {
pub(crate) highlights_query: Query,
pub(crate) brackets_query: Query,
pub(crate) indents_query: Query,
+ pub(crate) outline_query: Query,
pub(crate) highlight_map: Mutex<HighlightMap>,
}
@@ -127,6 +130,7 @@ impl Language {
brackets_query: Query::new(ts_language, "").unwrap(),
highlights_query: Query::new(ts_language, "").unwrap(),
indents_query: Query::new(ts_language, "").unwrap(),
+ outline_query: Query::new(ts_language, "").unwrap(),
ts_language,
highlight_map: Default::default(),
})
@@ -164,6 +168,16 @@ impl Language {
Ok(self)
}
+ pub fn with_outline_query(mut self, source: &str) -> Result<Self> {
+ let grammar = self
+ .grammar
+ .as_mut()
+ .and_then(Arc::get_mut)
+ .ok_or_else(|| anyhow!("grammar does not exist or is already being used"))?;
+ grammar.outline_query = Query::new(grammar.ts_language, source)?;
+ Ok(self)
+ }
+
pub fn name(&self) -> &str {
self.config.name.as_str()
}
@@ -0,0 +1,146 @@
+use fuzzy::{StringMatch, StringMatchCandidate};
+use gpui::{executor::Background, fonts::HighlightStyle};
+use std::{ops::Range, sync::Arc};
+
+#[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)]
+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 as usize..range.end as usize])
+ .collect::<String>();
+
+ path_candidates.push(StringMatchCandidate {
+ id,
+ char_bag: path_text.as_str().into(),
+ string: path_text.clone(),
+ });
+ candidates.push(StringMatchCandidate {
+ id,
+ char_bag: candidate_text.as_str().into(),
+ string: candidate_text,
+ });
+ }
+
+ Self {
+ candidates,
+ path_candidates,
+ path_candidate_prefixes,
+ items,
+ }
+ }
+
+ pub async fn search(&self, query: &str, executor: Arc<Background>) -> 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];
+
+ 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() as usize {
+ preceding_ranges_len += name_range.len();
+ name_range = name_ranges.next().unwrap();
+ }
+ *position = name_range.start as usize + (*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
+ }
+}
@@ -278,6 +278,139 @@ async fn test_reparse(mut cx: gpui::TestAppContext) {
}
}
+#[gpui::test]
+async fn test_outline(mut cx: gpui::TestAppContext) {
+ let language = Some(Arc::new(
+ rust_lang()
+ .with_outline_query(
+ r#"
+ (struct_item
+ "struct" @context
+ name: (_) @name) @item
+ (enum_item
+ "enum" @context
+ name: (_) @name) @item
+ (enum_variant
+ name: (_) @name) @item
+ (field_declaration
+ name: (_) @name) @item
+ (impl_item
+ "impl" @context
+ trait: (_) @name
+ "for" @context
+ type: (_) @name) @item
+ (function_item
+ "fn" @context
+ name: (_) @name) @item
+ (mod_item
+ "mod" @context
+ name: (_) @name) @item
+ "#,
+ )
+ .unwrap(),
+ ));
+
+ let text = r#"
+ struct Person {
+ name: String,
+ age: usize,
+ }
+
+ mod module {
+ enum LoginState {
+ LoggedOut,
+ LoggingOn,
+ LoggedIn {
+ person: Person,
+ time: Instant,
+ }
+ }
+ }
+
+ impl Eq for Person {}
+
+ impl Drop for Person {
+ fn drop(&mut self) {
+ println!("bye");
+ }
+ }
+ "#
+ .unindent();
+
+ let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, None, cx));
+ let outline = buffer
+ .read_with(&cx, |buffer, _| buffer.snapshot().outline(None))
+ .unwrap();
+
+ assert_eq!(
+ outline
+ .items
+ .iter()
+ .map(|item| (item.text.as_str(), item.depth))
+ .collect::<Vec<_>>(),
+ &[
+ ("struct Person", 0),
+ ("name", 1),
+ ("age", 1),
+ ("mod module", 0),
+ ("enum LoginState", 1),
+ ("LoggedOut", 2),
+ ("LoggingOn", 2),
+ ("LoggedIn", 2),
+ ("person", 3),
+ ("time", 3),
+ ("impl Eq for Person", 0),
+ ("impl Drop for Person", 0),
+ ("fn drop", 1),
+ ]
+ );
+
+ // Without space, we only match on names
+ assert_eq!(
+ search(&outline, "oon", &cx).await,
+ &[
+ ("mod module", vec![]), // included as the parent of a match
+ ("enum LoginState", vec![]), // included as the parent of a match
+ ("LoggingOn", vec![1, 7, 8]), // matches
+ ("impl Drop for Person", vec![7, 18, 19]), // matches in two disjoint names
+ ]
+ );
+
+ assert_eq!(
+ search(&outline, "dp p", &cx).await,
+ &[
+ ("impl Drop for Person", vec![5, 8, 9, 14]),
+ ("fn drop", vec![]),
+ ]
+ );
+ assert_eq!(
+ search(&outline, "dpn", &cx).await,
+ &[("impl Drop for Person", vec![5, 14, 19])]
+ );
+ assert_eq!(
+ search(&outline, "impl ", &cx).await,
+ &[
+ ("impl Eq for Person", vec![0, 1, 2, 3, 4]),
+ ("impl Drop for Person", vec![0, 1, 2, 3, 4]),
+ ("fn drop", vec![]),
+ ]
+ );
+
+ async fn search<'a>(
+ outline: &'a Outline<Anchor>,
+ query: &str,
+ cx: &gpui::TestAppContext,
+ ) -> Vec<(&'a str, Vec<usize>)> {
+ let matches = cx
+ .read(|cx| outline.search(query, cx.background().clone()))
+ .await;
+ matches
+ .into_iter()
+ .map(|mat| (outline.items[mat.candidate_id].text.as_str(), mat.positions))
+ .collect::<Vec<_>>()
+ }
+}
+
#[gpui::test]
fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) {
let buffer = cx.add_model(|cx| {
@@ -1017,14 +1150,18 @@ fn rust_lang() -> Language {
)
.with_indents_query(
r#"
- (call_expression) @indent
- (field_expression) @indent
- (_ "(" ")" @end) @indent
- (_ "{" "}" @end) @indent
- "#,
+ (call_expression) @indent
+ (field_expression) @indent
+ (_ "(" ")" @end) @indent
+ (_ "{" "}" @end) @indent
+ "#,
)
.unwrap()
- .with_brackets_query(r#" ("{" @open "}" @close) "#)
+ .with_brackets_query(
+ r#"
+ ("{" @open "}" @close)
+ "#,
+ )
.unwrap()
}
@@ -16,7 +16,7 @@ use std::{
io::Write,
str::FromStr,
sync::{
- atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
+ atomic::{AtomicUsize, Ordering::SeqCst},
Arc,
},
};
@@ -431,7 +431,7 @@ pub struct FakeLanguageServer {
buffer: Vec<u8>,
stdin: smol::io::BufReader<async_pipe::PipeReader>,
stdout: smol::io::BufWriter<async_pipe::PipeWriter>,
- pub started: Arc<AtomicBool>,
+ pub started: Arc<std::sync::atomic::AtomicBool>,
}
#[cfg(any(test, feature = "test-support"))]
@@ -449,7 +449,7 @@ impl LanguageServer {
stdin: smol::io::BufReader::new(stdin.1),
stdout: smol::io::BufWriter::new(stdout.0),
buffer: Vec::new(),
- started: Arc::new(AtomicBool::new(true)),
+ started: Arc::new(std::sync::atomic::AtomicBool::new(true)),
};
let server = Self::new_internal(stdin.0, stdout.1, Path::new("/"), executor).unwrap();
@@ -0,0 +1,18 @@
+[package]
+name = "outline"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+path = "src/outline.rs"
+
+[dependencies]
+editor = { path = "../editor" }
+fuzzy = { path = "../fuzzy" }
+gpui = { path = "../gpui" }
+language = { path = "../language" }
+text = { path = "../text" }
+workspace = { path = "../workspace" }
+ordered-float = "2.1.1"
+postage = { version = "0.4", features = ["futures-traits"] }
+smol = "1.2"
@@ -0,0 +1,540 @@
+use editor::{
+ display_map::ToDisplayPoint, Anchor, AnchorRangeExt, Autoscroll, Editor, EditorSettings,
+ ToPoint,
+};
+use fuzzy::StringMatch;
+use gpui::{
+ action,
+ elements::*,
+ fonts::{self, HighlightStyle},
+ geometry::vector::Vector2F,
+ keymap::{self, Binding},
+ AppContext, Axis, Entity, MutableAppContext, RenderContext, View, ViewContext, ViewHandle,
+ WeakViewHandle,
+};
+use language::{Outline, Selection};
+use ordered_float::OrderedFloat;
+use postage::watch;
+use std::{
+ cmp::{self, Reverse},
+ ops::Range,
+ sync::Arc,
+};
+use workspace::{
+ menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev},
+ Settings, Workspace,
+};
+
+action!(Toggle);
+
+pub fn init(cx: &mut MutableAppContext) {
+ cx.add_bindings([
+ Binding::new("cmd-shift-O", Toggle, Some("Editor")),
+ Binding::new("escape", Toggle, Some("OutlineView")),
+ ]);
+ cx.add_action(OutlineView::toggle);
+ cx.add_action(OutlineView::confirm);
+ cx.add_action(OutlineView::select_prev);
+ cx.add_action(OutlineView::select_next);
+ cx.add_action(OutlineView::select_first);
+ cx.add_action(OutlineView::select_last);
+}
+
+struct OutlineView {
+ handle: WeakViewHandle<Self>,
+ active_editor: ViewHandle<Editor>,
+ outline: Outline<Anchor>,
+ selected_match_index: usize,
+ restore_state: Option<RestoreState>,
+ symbol_selection_id: Option<usize>,
+ matches: Vec<StringMatch>,
+ query_editor: ViewHandle<Editor>,
+ list_state: UniformListState,
+ settings: watch::Receiver<Settings>,
+}
+
+struct RestoreState {
+ scroll_position: Vector2F,
+ selections: Vec<Selection<usize>>,
+}
+
+pub enum Event {
+ Dismissed,
+}
+
+impl Entity for OutlineView {
+ type Event = Event;
+
+ fn release(&mut self, cx: &mut MutableAppContext) {
+ self.restore_active_editor(cx);
+ }
+}
+
+impl View for OutlineView {
+ fn ui_name() -> &'static str {
+ "OutlineView"
+ }
+
+ fn keymap_context(&self, _: &AppContext) -> keymap::Context {
+ let mut cx = Self::default_keymap_context();
+ cx.set.insert("menu".into());
+ cx
+ }
+
+ fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
+ let settings = self.settings.borrow();
+
+ Flex::new(Axis::Vertical)
+ .with_child(
+ Container::new(ChildView::new(self.query_editor.id()).boxed())
+ .with_style(settings.theme.selector.input_editor.container)
+ .boxed(),
+ )
+ .with_child(Flexible::new(1.0, false, self.render_matches()).boxed())
+ .contained()
+ .with_style(settings.theme.selector.container)
+ .constrained()
+ .with_max_width(800.0)
+ .with_max_height(1200.0)
+ .aligned()
+ .top()
+ .named("outline view")
+ }
+
+ fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
+ cx.focus(&self.query_editor);
+ }
+}
+
+impl OutlineView {
+ fn new(
+ outline: Outline<Anchor>,
+ editor: ViewHandle<Editor>,
+ settings: watch::Receiver<Settings>,
+ cx: &mut ViewContext<Self>,
+ ) -> Self {
+ let query_editor = cx.add_view(|cx| {
+ Editor::single_line(
+ {
+ let settings = settings.clone();
+ Arc::new(move |_| {
+ let settings = settings.borrow();
+ EditorSettings {
+ style: settings.theme.selector.input_editor.as_editor(),
+ tab_size: settings.tab_size,
+ soft_wrap: editor::SoftWrap::None,
+ }
+ })
+ },
+ cx,
+ )
+ });
+ cx.subscribe(&query_editor, Self::on_query_editor_event)
+ .detach();
+
+ let restore_state = editor.update(cx, |editor, cx| {
+ Some(RestoreState {
+ scroll_position: editor.scroll_position(cx),
+ selections: editor.local_selections::<usize>(cx),
+ })
+ });
+
+ let mut this = Self {
+ handle: cx.weak_handle(),
+ active_editor: editor,
+ matches: Default::default(),
+ selected_match_index: 0,
+ restore_state,
+ symbol_selection_id: None,
+ outline,
+ query_editor,
+ list_state: Default::default(),
+ settings,
+ };
+ this.update_matches(cx);
+ this
+ }
+
+ fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
+ if let Some(editor) = workspace
+ .active_item(cx)
+ .and_then(|item| item.to_any().downcast::<Editor>())
+ {
+ let settings = workspace.settings();
+ let buffer = editor
+ .read(cx)
+ .buffer()
+ .read(cx)
+ .read(cx)
+ .outline(Some(settings.borrow().theme.editor.syntax.as_ref()));
+ if let Some(outline) = buffer {
+ workspace.toggle_modal(cx, |cx, _| {
+ let view = cx.add_view(|cx| OutlineView::new(outline, editor, settings, cx));
+ cx.subscribe(&view, Self::on_event).detach();
+ view
+ })
+ }
+ }
+ }
+
+ fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
+ if self.selected_match_index > 0 {
+ self.select(self.selected_match_index - 1, true, false, cx);
+ }
+ }
+
+ fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
+ if self.selected_match_index + 1 < self.matches.len() {
+ self.select(self.selected_match_index + 1, true, false, cx);
+ }
+ }
+
+ fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
+ self.select(0, true, false, cx);
+ }
+
+ fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
+ self.select(self.matches.len().saturating_sub(1), true, false, cx);
+ }
+
+ fn select(&mut self, index: usize, navigate: bool, center: bool, cx: &mut ViewContext<Self>) {
+ self.selected_match_index = index;
+ self.list_state.scroll_to(if center {
+ ScrollTarget::Center(index)
+ } else {
+ ScrollTarget::Show(index)
+ });
+ if navigate {
+ let selected_match = &self.matches[self.selected_match_index];
+ let outline_item = &self.outline.items[selected_match.candidate_id];
+ self.symbol_selection_id = self.active_editor.update(cx, |active_editor, cx| {
+ let snapshot = active_editor.snapshot(cx).display_snapshot;
+ let buffer_snapshot = &snapshot.buffer_snapshot;
+ let start = outline_item.range.start.to_point(&buffer_snapshot);
+ let end = outline_item.range.end.to_point(&buffer_snapshot);
+ let display_rows = start.to_display_point(&snapshot).row()
+ ..end.to_display_point(&snapshot).row() + 1;
+ active_editor.select_ranges([start..start], Some(Autoscroll::Center), cx);
+ active_editor.set_highlighted_rows(Some(display_rows));
+ Some(active_editor.newest_selection::<usize>(&buffer_snapshot).id)
+ });
+ cx.notify();
+ }
+ }
+
+ fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
+ self.restore_state.take();
+ cx.emit(Event::Dismissed);
+ }
+
+ fn restore_active_editor(&mut self, cx: &mut MutableAppContext) {
+ let symbol_selection_id = self.symbol_selection_id.take();
+ self.active_editor.update(cx, |editor, cx| {
+ editor.set_highlighted_rows(None);
+ if let Some((symbol_selection_id, restore_state)) =
+ symbol_selection_id.zip(self.restore_state.as_ref())
+ {
+ let newest_selection =
+ editor.newest_selection::<usize>(&editor.buffer().read(cx).read(cx));
+ if symbol_selection_id == newest_selection.id {
+ editor.set_scroll_position(restore_state.scroll_position, cx);
+ editor.update_selections(restore_state.selections.clone(), None, cx);
+ }
+ }
+ })
+ }
+
+ fn on_event(
+ workspace: &mut Workspace,
+ _: ViewHandle<Self>,
+ event: &Event,
+ cx: &mut ViewContext<Workspace>,
+ ) {
+ match event {
+ Event::Dismissed => workspace.dismiss_modal(cx),
+ }
+ }
+
+ fn on_query_editor_event(
+ &mut self,
+ _: ViewHandle<Editor>,
+ event: &editor::Event,
+ cx: &mut ViewContext<Self>,
+ ) {
+ match event {
+ editor::Event::Blurred => cx.emit(Event::Dismissed),
+ editor::Event::Edited => self.update_matches(cx),
+ _ => {}
+ }
+ }
+
+ fn update_matches(&mut self, cx: &mut ViewContext<Self>) {
+ let selected_index;
+ let navigate_to_selected_index;
+ let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx));
+ if query.is_empty() {
+ self.restore_active_editor(cx);
+ self.matches = self
+ .outline
+ .items
+ .iter()
+ .enumerate()
+ .map(|(index, _)| StringMatch {
+ candidate_id: index,
+ score: Default::default(),
+ positions: Default::default(),
+ string: Default::default(),
+ })
+ .collect();
+
+ let editor = self.active_editor.read(cx);
+ let buffer = editor.buffer().read(cx).read(cx);
+ let cursor_offset = editor.newest_selection::<usize>(&buffer).head();
+ selected_index = self
+ .outline
+ .items
+ .iter()
+ .enumerate()
+ .map(|(ix, item)| {
+ let range = item.range.to_offset(&buffer);
+ let distance_to_closest_endpoint = cmp::min(
+ (range.start as isize - cursor_offset as isize).abs() as usize,
+ (range.end as isize - cursor_offset as isize).abs() as usize,
+ );
+ let depth = if range.contains(&cursor_offset) {
+ Some(item.depth)
+ } else {
+ None
+ };
+ (ix, depth, distance_to_closest_endpoint)
+ })
+ .max_by_key(|(_, depth, distance)| (*depth, Reverse(*distance)))
+ .unwrap()
+ .0;
+ navigate_to_selected_index = false;
+ } else {
+ self.matches = smol::block_on(self.outline.search(&query, cx.background().clone()));
+ selected_index = self
+ .matches
+ .iter()
+ .enumerate()
+ .max_by_key(|(_, m)| OrderedFloat(m.score))
+ .map(|(ix, _)| ix)
+ .unwrap_or(0);
+ navigate_to_selected_index = !self.matches.is_empty();
+ }
+ self.select(selected_index, navigate_to_selected_index, true, cx);
+ }
+
+ fn render_matches(&self) -> ElementBox {
+ if self.matches.is_empty() {
+ let settings = self.settings.borrow();
+ return Container::new(
+ Label::new(
+ "No matches".into(),
+ settings.theme.selector.empty.label.clone(),
+ )
+ .boxed(),
+ )
+ .with_style(settings.theme.selector.empty.container)
+ .named("empty matches");
+ }
+
+ let handle = self.handle.clone();
+ let list = UniformList::new(
+ self.list_state.clone(),
+ self.matches.len(),
+ move |mut range, items, cx| {
+ let cx = cx.as_ref();
+ let view = handle.upgrade(cx).unwrap();
+ let view = view.read(cx);
+ let start = range.start;
+ range.end = cmp::min(range.end, view.matches.len());
+ items.extend(
+ view.matches[range]
+ .iter()
+ .enumerate()
+ .map(move |(ix, m)| view.render_match(m, start + ix)),
+ );
+ },
+ );
+
+ Container::new(list.boxed())
+ .with_margin_top(6.0)
+ .named("matches")
+ }
+
+ fn render_match(&self, string_match: &StringMatch, index: usize) -> ElementBox {
+ let settings = self.settings.borrow();
+ let style = if index == self.selected_match_index {
+ &settings.theme.selector.active_item
+ } else {
+ &settings.theme.selector.item
+ };
+ let outline_item = &self.outline.items[string_match.candidate_id];
+
+ Text::new(outline_item.text.clone(), style.label.text.clone())
+ .with_soft_wrap(false)
+ .with_highlights(combine_syntax_and_fuzzy_match_highlights(
+ &outline_item.text,
+ style.label.text.clone().into(),
+ &outline_item.highlight_ranges,
+ &string_match.positions,
+ ))
+ .contained()
+ .with_padding_left(20. * outline_item.depth as f32)
+ .contained()
+ .with_style(style.container)
+ .boxed()
+ }
+}
+
+fn combine_syntax_and_fuzzy_match_highlights(
+ text: &str,
+ default_style: HighlightStyle,
+ syntax_ranges: &[(Range<usize>, HighlightStyle)],
+ match_indices: &[usize],
+) -> Vec<(Range<usize>, HighlightStyle)> {
+ let mut result = Vec::new();
+ let mut match_indices = match_indices.iter().copied().peekable();
+
+ for (range, mut syntax_highlight) in syntax_ranges
+ .iter()
+ .cloned()
+ .chain([(usize::MAX..0, Default::default())])
+ {
+ syntax_highlight.font_properties.weight(Default::default());
+
+ // Add highlights for any fuzzy match characters before the next
+ // syntax highlight range.
+ while let Some(&match_index) = match_indices.peek() {
+ if match_index >= range.start {
+ break;
+ }
+ match_indices.next();
+ let end_index = char_ix_after(match_index, text);
+ let mut match_style = default_style;
+ match_style.font_properties.weight(fonts::Weight::BOLD);
+ result.push((match_index..end_index, match_style));
+ }
+
+ if range.start == usize::MAX {
+ break;
+ }
+
+ // Add highlights for any fuzzy match characters within the
+ // syntax highlight range.
+ let mut offset = range.start;
+ while let Some(&match_index) = match_indices.peek() {
+ if match_index >= range.end {
+ break;
+ }
+
+ match_indices.next();
+ if match_index > offset {
+ result.push((offset..match_index, syntax_highlight));
+ }
+
+ let mut end_index = char_ix_after(match_index, text);
+ while let Some(&next_match_index) = match_indices.peek() {
+ if next_match_index == end_index && next_match_index < range.end {
+ end_index = char_ix_after(next_match_index, text);
+ match_indices.next();
+ } else {
+ break;
+ }
+ }
+
+ let mut match_style = syntax_highlight;
+ match_style.font_properties.weight(fonts::Weight::BOLD);
+ result.push((match_index..end_index, match_style));
+ offset = end_index;
+ }
+
+ if offset < range.end {
+ result.push((offset..range.end, syntax_highlight));
+ }
+ }
+
+ result
+}
+
+fn char_ix_after(ix: usize, text: &str) -> usize {
+ ix + text[ix..].chars().next().unwrap().len_utf8()
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use gpui::{color::Color, fonts::HighlightStyle};
+
+ #[test]
+ fn test_combine_syntax_and_fuzzy_match_highlights() {
+ let string = "abcdefghijklmnop";
+ let default = HighlightStyle::default();
+ let syntax_ranges = [
+ (
+ 0..3,
+ HighlightStyle {
+ color: Color::red(),
+ ..default
+ },
+ ),
+ (
+ 4..8,
+ HighlightStyle {
+ color: Color::green(),
+ ..default
+ },
+ ),
+ ];
+ let match_indices = [4, 6, 7, 8];
+ assert_eq!(
+ combine_syntax_and_fuzzy_match_highlights(
+ &string,
+ default,
+ &syntax_ranges,
+ &match_indices,
+ ),
+ &[
+ (
+ 0..3,
+ HighlightStyle {
+ color: Color::red(),
+ ..default
+ },
+ ),
+ (
+ 4..5,
+ HighlightStyle {
+ color: Color::green(),
+ font_properties: *fonts::Properties::default().weight(fonts::Weight::BOLD),
+ ..default
+ },
+ ),
+ (
+ 5..6,
+ HighlightStyle {
+ color: Color::green(),
+ ..default
+ },
+ ),
+ (
+ 6..8,
+ HighlightStyle {
+ color: Color::green(),
+ font_properties: *fonts::Properties::default().weight(fonts::Weight::BOLD),
+ ..default
+ },
+ ),
+ (
+ 8..9,
+ HighlightStyle {
+ font_properties: *fonts::Properties::default().weight(fonts::Weight::BOLD),
+ ..default
+ },
+ ),
+ ]
+ );
+ }
+}
@@ -1,14 +1,10 @@
use gpui::{
action,
elements::{
- Align, ConstrainedBox, Empty, Flex, Label, MouseEventHandler, ParentElement, Svg,
- UniformList, UniformListState,
- },
- keymap::{
- self,
- menu::{SelectNext, SelectPrev},
- Binding,
+ Align, ConstrainedBox, Empty, Flex, Label, MouseEventHandler, ParentElement, ScrollTarget,
+ Svg, UniformList, UniformListState,
},
+ keymap::{self, Binding},
platform::CursorStyle,
AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, ReadModel, View,
ViewContext, ViewHandle, WeakViewHandle,
@@ -20,7 +16,10 @@ use std::{
ffi::OsStr,
ops::Range,
};
-use workspace::{Settings, Workspace};
+use workspace::{
+ menu::{SelectNext, SelectPrev},
+ Settings, Workspace,
+};
pub struct ProjectPanel {
project: ModelHandle<Project>,
@@ -278,7 +277,7 @@ impl ProjectPanel {
fn autoscroll(&mut self) {
if let Some(selection) = self.selection {
- self.list.scroll_to(selection.index);
+ self.list.scroll_to(ScrollTarget::Show(selection.index));
}
}
@@ -3,7 +3,7 @@ use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
use gpui::{
action,
elements::*,
- keymap::{self, menu, Binding},
+ keymap::{self, Binding},
AppContext, Axis, Element, ElementBox, Entity, MutableAppContext, RenderContext, View,
ViewContext, ViewHandle,
};
@@ -11,7 +11,10 @@ use parking_lot::Mutex;
use postage::watch;
use std::{cmp, sync::Arc};
use theme::ThemeRegistry;
-use workspace::{AppState, Settings, Workspace};
+use workspace::{
+ menu::{Confirm, SelectNext, SelectPrev},
+ AppState, Settings, Workspace,
+};
#[derive(Clone)]
pub struct ThemeSelectorParams {
@@ -30,7 +33,6 @@ pub struct ThemeSelector {
selected_index: usize,
}
-action!(Confirm);
action!(Toggle, ThemeSelectorParams);
action!(Reload, ThemeSelectorParams);
@@ -45,7 +47,6 @@ pub fn init(params: ThemeSelectorParams, cx: &mut MutableAppContext) {
Binding::new("cmd-k cmd-t", Toggle(params.clone()), None),
Binding::new("cmd-k t", Reload(params.clone()), None),
Binding::new("escape", Toggle(params.clone()), Some("ThemeSelector")),
- Binding::new("enter", Confirm, Some("ThemeSelector")),
]);
}
@@ -136,19 +137,21 @@ impl ThemeSelector {
}
}
- fn select_prev(&mut self, _: &menu::SelectPrev, cx: &mut ViewContext<Self>) {
+ fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
if self.selected_index > 0 {
self.selected_index -= 1;
}
- self.list_state.scroll_to(self.selected_index);
+ self.list_state
+ .scroll_to(ScrollTarget::Show(self.selected_index));
cx.notify();
}
- fn select_next(&mut self, _: &menu::SelectNext, cx: &mut ViewContext<Self>) {
+ fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
if self.selected_index + 1 < self.matches.len() {
self.selected_index += 1;
}
- self.list_state.scroll_to(self.selected_index);
+ self.list_state
+ .scroll_to(ScrollTarget::Show(self.selected_index));
cx.notify();
}
@@ -157,7 +160,9 @@ impl ThemeSelector {
let candidates = self
.themes
.list()
- .map(|name| StringMatchCandidate {
+ .enumerate()
+ .map(|(id, name)| StringMatchCandidate {
+ id,
char_bag: name.as_str().into(),
string: name,
})
@@ -167,7 +172,9 @@ impl ThemeSelector {
self.matches = if query.is_empty() {
candidates
.into_iter()
- .map(|candidate| StringMatch {
+ .enumerate()
+ .map(|(index, candidate)| StringMatch {
+ candidate_id: index,
string: candidate.string,
positions: Vec::new(),
score: 0.0,
@@ -0,0 +1,19 @@
+use gpui::{action, keymap::Binding, MutableAppContext};
+
+action!(Confirm);
+action!(SelectPrev);
+action!(SelectNext);
+action!(SelectFirst);
+action!(SelectLast);
+
+pub fn init(cx: &mut MutableAppContext) {
+ cx.add_bindings([
+ Binding::new("up", SelectPrev, Some("menu")),
+ Binding::new("ctrl-p", SelectPrev, Some("menu")),
+ Binding::new("down", SelectNext, Some("menu")),
+ Binding::new("ctrl-n", SelectNext, Some("menu")),
+ Binding::new("cmd-up", SelectFirst, Some("menu")),
+ Binding::new("cmd-down", SelectLast, Some("menu")),
+ Binding::new("enter", Confirm, Some("menu")),
+ ]);
+}
@@ -1,3 +1,4 @@
+pub mod menu;
pub mod pane;
pub mod pane_group;
pub mod settings;
@@ -48,6 +49,9 @@ action!(Save);
action!(DebugElements);
pub fn init(cx: &mut MutableAppContext) {
+ pane::init(cx);
+ menu::init(cx);
+
cx.add_global_action(open);
cx.add_global_action(move |action: &OpenPaths, cx: &mut MutableAppContext| {
open_paths(&action.0.paths, &action.0.app_state, cx).detach();
@@ -84,7 +88,6 @@ pub fn init(cx: &mut MutableAppContext) {
None,
),
]);
- pane::init(cx);
}
pub struct AppState {
@@ -43,6 +43,7 @@ gpui = { path = "../gpui" }
journal = { path = "../journal" }
language = { path = "../language" }
lsp = { path = "../lsp" }
+outline = { path = "../outline" }
project = { path = "../project" }
project_panel = { path = "../project_panel" }
rpc = { path = "../rpc" }
@@ -211,7 +211,7 @@ text = { extends = "$text.0" }
[selector]
background = "$surface.0"
padding = 8
-margin.top = 52
+margin = { top = 52, bottom = 52 }
corner_radius = 6
shadow = { offset = [0, 2], blur = 16, color = "$shadow.0" }
border = { width = 1, color = "$border.0" }
@@ -0,0 +1,63 @@
+(struct_item
+ (visibility_modifier)? @context
+ "struct" @context
+ name: (_) @name) @item
+
+(enum_item
+ (visibility_modifier)? @context
+ "enum" @context
+ name: (_) @name) @item
+
+(enum_variant
+ (visibility_modifier)? @context
+ name: (_) @name) @item
+
+(impl_item
+ "impl" @context
+ trait: (_)? @name
+ "for"? @context
+ type: (_) @name) @item
+
+(trait_item
+ (visibility_modifier)? @context
+ "trait" @context
+ name: (_) @name) @item
+
+(function_item
+ (visibility_modifier)? @context
+ (function_modifiers)? @context
+ "fn" @context
+ name: (_) @name) @item
+
+(function_signature_item
+ (visibility_modifier)? @context
+ (function_modifiers)? @context
+ "fn" @context
+ name: (_) @name) @item
+
+(macro_definition
+ . "macro_rules!" @context
+ name: (_) @name) @item
+
+(mod_item
+ (visibility_modifier)? @context
+ "mod" @context
+ name: (_) @name) @item
+
+(type_item
+ (visibility_modifier)? @context
+ "type" @context
+ name: (_) @name) @item
+
+(associated_type
+ "type" @context
+ name: (_) @name) @item
+
+(const_item
+ (visibility_modifier)? @context
+ "const" @context
+ name: (_) @name) @item
+
+(field_declaration
+ (visibility_modifier)? @context
+ name: (_) @name) @item
@@ -24,6 +24,8 @@ fn rust() -> Language {
.unwrap()
.with_indents_query(load_query("rust/indents.scm").as_ref())
.unwrap()
+ .with_outline_query(load_query("rust/outline.scm").as_ref())
+ .unwrap()
}
fn markdown() -> Language {
@@ -59,6 +59,7 @@ fn main() {
go_to_line::init(cx);
file_finder::init(cx);
chat_panel::init(cx);
+ outline::init(cx);
project_panel::init(cx);
diagnostics::init(cx);
cx.spawn({