diff --git a/Cargo.lock b/Cargo.lock index 862890a3387e3eeebc48ff99261470c3f116de46..9eda0d351934bd1cea9d6ed0ea125e30ef981b82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index eb412f3dcb5343c8548a7d7a6228756713959c91..898f1fce4256fb4589fb7b37ad20f3c17c27d625 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -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>, - highlighted_row: Option, + highlighted_rows: Option>, } 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) { + 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) { + 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) { - self.highlighted_row = row; + pub fn set_highlighted_rows(&mut self, rows: Option>) { + self.highlighted_rows = rows; } - pub fn highlighted_row(&mut self) -> Option { - self.highlighted_row + pub fn highlighted_rows(&self) -> Option> { + self.highlighted_rows.clone() } fn next_blink_epoch(&mut self) -> usize { diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 5180314c2a22b705dfda7429e0e299b8a9e10816..ff4b792338a9a9fd82a9fb6348d68e5d512d85ed 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -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, - highlighted_row: Option, + highlighted_rows: Option>, line_layouts: Vec, line_number_layouts: Vec>, blocks: Vec<(u32, ElementBox)>, diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index c7192cd622c51dbd43dbf58f5f561e045f64a2b9..30020a0d55603154c28408ff3a2a6f9ac6dbc8a0 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -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> { + 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, diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 707a3bfb20602820f3be4f86681bb1605b04e0b8..00f253bc75eb09634f33920c3f2d3f10d15befc5 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -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, @@ -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(); } } diff --git a/crates/fuzzy/src/char_bag.rs b/crates/fuzzy/src/char_bag.rs index e2f68a27d1d9a46d4c24ee37f4835fce8623fe76..c9aab0cd0bab0c39d5bc6da6873a9b377f00d259 100644 --- a/crates/fuzzy/src/char_bag.rs +++ b/crates/fuzzy/src/char_bag.rs @@ -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; diff --git a/crates/fuzzy/src/fuzzy.rs b/crates/fuzzy/src/fuzzy.rs index c9bcdb827fe0bee446c0b3c32f04c73498e32549..92084b9dc78b026f1e12f5043a6195efc41f0578 100644 --- a/crates/fuzzy/src/fuzzy.rs +++ b/crates/fuzzy/src/fuzzy.rs @@ -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, 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; diff --git a/crates/go_to_line/src/go_to_line.rs b/crates/go_to_line/src/go_to_line.rs index cf965ebf1b398816c87862e4de9db388998e5615..53669ea2c62151b5be363051068d0630d766a76c 100644 --- a/crates/go_to_line/src/go_to_line.rs +++ b/crates/go_to_line/src/go_to_line.rs @@ -26,7 +26,7 @@ pub struct GoToLine { line_editor: ViewHandle, active_editor: ViewHandle, restore_state: Option, - line_selection: Option>, + line_selection_id: Option, 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::(&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::(&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) { cx.focus(&self.line_editor); } - - fn on_blur(&mut self, _: &mut ViewContext) {} } diff --git a/crates/gpui/src/elements/container.rs b/crates/gpui/src/elements/container.rs index 026542989e45e25cdaad0acc8a01086cdbf966b2..73a4349ba025681a232ae0986532db0faa00ed6a 100644 --- a/crates/gpui/src/elements/container.rs +++ b/crates/gpui/src/elements/container.rs @@ -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 diff --git a/crates/gpui/src/elements/text.rs b/crates/gpui/src/elements/text.rs index 2f20b77d566a8b14ab303d787bfffa89a1e2d007..7c983f1e6fb7b99659e9053feffb4f21f4535ffc 100644 --- a/crates/gpui/src/elements/text.rs +++ b/crates/gpui/src/elements/text.rs @@ -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, HighlightStyle)>, } pub struct LayoutState { - lines: Vec<(Line, Vec)>, + shaped_lines: Vec, + wrap_boundaries: Vec>, 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, 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::>() + 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::>(); + 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)>, + style: &'a TextStyle, + text_layout_cache: &'a TextLayoutCache, + font_cache: &'a Arc, + max_line_len: usize, + max_line_count: usize, +) -> Vec { + 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 +} diff --git a/crates/gpui/src/elements/uniform_list.rs b/crates/gpui/src/elements/uniform_list.rs index f499801e6384380eed8875068c498a3ffa930ab3..945340e4c0539bddb4e8a47b2cf9b072b008677b 100644 --- a/crates/gpui/src/elements/uniform_list.rs +++ b/crates/gpui/src/elements/uniform_list.rs @@ -14,9 +14,15 @@ use std::{cmp, ops::Range, sync::Arc}; #[derive(Clone, Default)] pub struct UniformListState(Arc>); +#[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, + scroll_to: Option, } 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 { diff --git a/crates/gpui/src/fonts.rs b/crates/gpui/src/fonts.rs index 6509360a626a9e8342afa1adbb90c9cedee327ce..25e16b717065d0e701e588fd5c3ec2c8c8a5a9fe 100644 --- a/crates/gpui/src/fonts.rs +++ b/crates/gpui/src/fonts.rs @@ -30,7 +30,7 @@ pub struct TextStyle { pub underline: Option, } -#[derive(Copy, Clone, Debug, Default)] +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] pub struct HighlightStyle { pub color: Color, pub font_properties: Properties, diff --git a/crates/gpui/src/keymap.rs b/crates/gpui/src/keymap.rs index bff1efbd248c3bd579432fbfc582157fc8cac4b4..848cb8fe393344710612fe5f382507b196162e06 100644 --- a/crates/gpui/src/keymap.rs +++ b/crates/gpui/src/keymap.rs @@ -23,6 +23,7 @@ struct Pending { context: Option, } +#[derive(Default)] pub struct Keymap(Vec); 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(keystrokes: &str, action: A, context: Option<&str>) -> Self { let context = if let Some(context) = context { diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index f8d5c1e8362b497d1708311b4b65c3513b6fa99d..6c29708b5ee26228be1523801d67fd25e301cae1 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -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" } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 07d4017c09eb14f64a7b9463b29247f436154403..4cf0c8f0813eb26cd7309892274f41609fa9b9a8 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -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); +pub(crate) struct QueryCursorHandle(Option); #[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( &mut self, ranges_iter: I, @@ -1827,6 +1835,110 @@ impl BufferSnapshot { } } + pub fn outline(&self, theme: Option<&SyntaxTheme>) -> Option> { + 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::>::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::>(); + + if items.is_empty() { + None + } else { + Some(Outline::new(items)) + } + } + pub fn enclosing_bracket_ranges( &self, range: Range, @@ -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) -> impl Iterator>)> + pub fn remote_selections_in_range(&self, Range) -> impl Iterator( &'a self, range: Range, @@ -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() diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 769bcbe69c03de41a4e61a417707a7c63dff9f62..72b883df0cea0ffa02c3dfdce29da2892f1ed08b 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -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, } @@ -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 { + 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() } diff --git a/crates/language/src/outline.rs b/crates/language/src/outline.rs new file mode 100644 index 0000000000000000000000000000000000000000..07f0196c9f59725855f624b486a079947bf7e1d0 --- /dev/null +++ b/crates/language/src/outline.rs @@ -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 { + pub items: Vec>, + candidates: Vec, + path_candidates: Vec, + path_candidate_prefixes: Vec, +} + +#[derive(Clone, Debug)] +pub struct OutlineItem { + pub depth: usize, + pub range: Range, + pub text: String, + pub highlight_ranges: Vec<(Range, HighlightStyle)>, + pub name_ranges: Vec>, +} + +impl Outline { + pub fn new(items: Vec>) -> 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::(); + + 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) -> Vec { + 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 + } +} diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index cf73e8dd23218a33946b02169e6ec54cc86eb964..e2ee035c86ac4d36a4cbeeebebe265ab6023a2a6 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -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::>(), + &[ + ("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, + query: &str, + cx: &gpui::TestAppContext, + ) -> Vec<(&'a str, Vec)> { + 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::>() + } +} + #[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() } diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 6d975e8e9fa87fd06a6112ca5694937fcdb09bf5..ad4355e90293fcd6811768514213cadb63c8fa6c 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -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, stdin: smol::io::BufReader, stdout: smol::io::BufWriter, - pub started: Arc, + pub started: Arc, } #[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(); diff --git a/crates/outline/Cargo.toml b/crates/outline/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..51c35792284fc01a79402ad2ac811e0712db113a --- /dev/null +++ b/crates/outline/Cargo.toml @@ -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" diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs new file mode 100644 index 0000000000000000000000000000000000000000..774b765977fa0bb2e06274da8f865ea7a59bfff3 --- /dev/null +++ b/crates/outline/src/outline.rs @@ -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, + active_editor: ViewHandle, + outline: Outline, + selected_match_index: usize, + restore_state: Option, + symbol_selection_id: Option, + matches: Vec, + query_editor: ViewHandle, + list_state: UniformListState, + settings: watch::Receiver, +} + +struct RestoreState { + scroll_position: Vector2F, + selections: Vec>, +} + +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) -> 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) { + cx.focus(&self.query_editor); + } +} + +impl OutlineView { + fn new( + outline: Outline, + editor: ViewHandle, + settings: watch::Receiver, + cx: &mut ViewContext, + ) -> 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::(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) { + if let Some(editor) = workspace + .active_item(cx) + .and_then(|item| item.to_any().downcast::()) + { + 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) { + 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) { + 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.select(0, true, false, cx); + } + + fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext) { + 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.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::(&buffer_snapshot).id) + }); + cx.notify(); + } + } + + fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { + 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::(&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, + event: &Event, + cx: &mut ViewContext, + ) { + match event { + Event::Dismissed => workspace.dismiss_modal(cx), + } + } + + fn on_query_editor_event( + &mut self, + _: ViewHandle, + event: &editor::Event, + cx: &mut ViewContext, + ) { + match event { + editor::Event::Blurred => cx.emit(Event::Dismissed), + editor::Event::Edited => self.update_matches(cx), + _ => {} + } + } + + fn update_matches(&mut self, cx: &mut ViewContext) { + 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::(&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, HighlightStyle)], + match_indices: &[usize], +) -> Vec<(Range, 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 + }, + ), + ] + ); + } +} diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 8c73f1be861a902d129fc9a12900399ba8e4a0ed..382a94284991d6d49775ffc988b73d9ae081d705 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -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, @@ -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)); } } diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs index df7713ad1fa947427d55d356b00f7347f7ef4276..f359bd85ddbbe1f961df2b8b669d2a1ea3c1a9a1 100644 --- a/crates/theme_selector/src/theme_selector.rs +++ b/crates/theme_selector/src/theme_selector.rs @@ -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) { + fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext) { 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) { + fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { 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, diff --git a/crates/workspace/src/menu.rs b/crates/workspace/src/menu.rs new file mode 100644 index 0000000000000000000000000000000000000000..e4ce82276a5fd1d9fc74eb4573be3309f6cee872 --- /dev/null +++ b/crates/workspace/src/menu.rs @@ -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")), + ]); +} diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 7faef17c1bd8d37cda91d37428521bab8856536a..fe540186a9acd9877b8f0e3d192f8f033a415cd1 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -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 { diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 66f6f68c9bb470bacb2e9684efd7b83a69b02921..a4a7252a808c9dd62bb6ecfefbf6c8a6e2555d33 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -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" } diff --git a/crates/zed/assets/themes/_base.toml b/crates/zed/assets/themes/_base.toml index 95dc7eee606646aa8af18046214681b8ffad739e..c4c3cf512564792735661268dc6d6d59ae61a8a9 100644 --- a/crates/zed/assets/themes/_base.toml +++ b/crates/zed/assets/themes/_base.toml @@ -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" } diff --git a/crates/zed/languages/rust/outline.scm b/crates/zed/languages/rust/outline.scm new file mode 100644 index 0000000000000000000000000000000000000000..5c89087ac0db7b037dbb38688260dd7c16a6d9ee --- /dev/null +++ b/crates/zed/languages/rust/outline.scm @@ -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 diff --git a/crates/zed/src/language.rs b/crates/zed/src/language.rs index a84d2cbd40b7a9d16734056e29ce79c18a173bff..98f6ab93d27675f391ee9164649490a055d3066e 100644 --- a/crates/zed/src/language.rs +++ b/crates/zed/src/language.rs @@ -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 { diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index f34c700c54b3771f683013c9d01a9d49c5651cb1..59804cf87c1bc8e8a6a8f2ea12c44faabdc10a02 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -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({