From 63a401ac5da18001a2dbbbbe18a5dd74f9c4bfd3 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 12 Jan 2022 18:17:19 -0800 Subject: [PATCH 01/34] Add Buffer::outline method Co-Authored-By: Nathan Sobo --- Cargo.lock | 12 ++++ crates/editor/src/multi_buffer.rs | 6 +- crates/language/src/buffer.rs | 96 ++++++++++++++++++++++++++- crates/language/src/language.rs | 14 ++++ crates/language/src/outline.rs | 12 ++++ crates/outline/Cargo.toml | 14 ++++ crates/outline/src/outline.rs | 53 +++++++++++++++ crates/zed/Cargo.toml | 1 + crates/zed/languages/rust/outline.scm | 17 +++++ crates/zed/src/language.rs | 2 + crates/zed/src/main.rs | 1 + 11 files changed, 224 insertions(+), 4 deletions(-) create mode 100644 crates/language/src/outline.rs create mode 100644 crates/outline/Cargo.toml create mode 100644 crates/outline/src/outline.rs create mode 100644 crates/zed/languages/rust/outline.scm diff --git a/Cargo.lock b/Cargo.lock index 8c3174d68d40df5c364bee8327318476b8dd0fa7..f1761a5daaa2eb4d9f09dfac36f6a63a5ee91bb1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3121,6 +3121,17 @@ 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", + "gpui", + "postage", + "text", + "workspace", +] + [[package]] name = "p256" version = "0.9.0" @@ -5724,6 +5735,7 @@ dependencies = [ "log-panics", "lsp", "num_cpus", + "outline", "parking_lot", "postage", "project", diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index c7192cd622c51dbd43dbf58f5f561e045f64a2b9..54a44d2aa77555251458c731cdf266c97869d1d8 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -7,7 +7,7 @@ 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, + ToOffset as _, ToPoint as _, TransactionId, Outline, }; use std::{ cell::{Ref, RefCell}, @@ -1698,6 +1698,10 @@ impl MultiBufferSnapshot { }) } + pub fn outline(&self) -> Option { + self.as_singleton().and_then(move |buffer| buffer.outline()) + } + fn buffer_snapshot_for_excerpt<'a>( &'a self, excerpt_id: &'a ExcerptId, diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 07d4017c09eb14f64a7b9463b29247f436154403..515def357f8aa089f9eae1d816ffd1c089e4b69f 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,82 @@ impl BufferSnapshot { } } + pub fn outline(&self) -> 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 item_capture_ix = grammar.outline_query.capture_index_for_name("item")?; + let context_capture_ix = grammar.outline_query.capture_index_for_name("context")?; + let name_capture_ix = grammar.outline_query.capture_index_for_name("name")?; + + let mut id = 0; + let mut items = matches + .filter_map(|mat| { + let item_node = mat.nodes_for_capture_index(item_capture_ix).next()?; + let mut name_node = Some(mat.nodes_for_capture_index(name_capture_ix).next()?); + let mut context_nodes = mat.nodes_for_capture_index(context_capture_ix).peekable(); + + let id = post_inc(&mut id); + let range = item_node.start_byte()..item_node.end_byte(); + + let mut text = String::new(); + let mut name_range_in_text = 0..0; + loop { + let node; + let node_is_name; + match (context_nodes.peek(), name_node.as_ref()) { + (None, None) => break, + (None, Some(_)) => { + node = name_node.take().unwrap(); + node_is_name = true; + } + (Some(_), None) => { + node = context_nodes.next().unwrap(); + node_is_name = false; + } + (Some(context_node), Some(name)) => { + if context_node.start_byte() < name.start_byte() { + node = context_nodes.next().unwrap(); + node_is_name = false; + } else { + node = name_node.take().unwrap(); + node_is_name = true; + } + } + } + + if !text.is_empty() { + text.push(' '); + } + let range = node.start_byte()..node.end_byte(); + if node_is_name { + name_range_in_text = text.len()..(text.len() + range.len()) + } + text.extend(self.text_for_range(range)); + } + + Some(OutlineItem { + id, + range, + text, + name_range_in_text, + }) + }) + .collect::>(); + + Some(Outline(items)) + } + pub fn enclosing_bracket_ranges( &self, range: Range, @@ -1854,6 +1938,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 +2198,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..56bcfbc67c6137fb71a612952845bdf022301e53 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; 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..427b91d46419fd9fab0bac1f31592853021fcae4 --- /dev/null +++ b/crates/language/src/outline.rs @@ -0,0 +1,12 @@ +use std::ops::Range; + +#[derive(Debug)] +pub struct Outline(pub Vec); + +#[derive(Debug)] +pub struct OutlineItem { + pub id: usize, + pub range: Range, + pub text: String, + pub name_range_in_text: Range, +} diff --git a/crates/outline/Cargo.toml b/crates/outline/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..9ee0c76f7d3da8e2acf9b4e8d61f60d3dff8564e --- /dev/null +++ b/crates/outline/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "outline" +version = "0.1.0" +edition = "2021" + +[lib] +path = "src/outline.rs" + +[dependencies] +text = { path = "../text" } +editor = { path = "../editor" } +gpui = { path = "../gpui" } +workspace = { path = "../workspace" } +postage = { version = "0.4", features = ["futures-traits"] } diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs new file mode 100644 index 0000000000000000000000000000000000000000..e9507296e78f0df289ccb314dd13d4b8e058c650 --- /dev/null +++ b/crates/outline/src/outline.rs @@ -0,0 +1,53 @@ +use editor::{display_map::ToDisplayPoint, Autoscroll, Editor, EditorSettings}; +use gpui::{ + action, elements::*, geometry::vector::Vector2F, keymap::Binding, Axis, Entity, + MutableAppContext, RenderContext, View, ViewContext, ViewHandle, +}; +use postage::watch; +use std::sync::Arc; +use text::{Bias, Point, Selection}; +use workspace::{Settings, Workspace}; + +action!(Toggle); +action!(Confirm); + +pub fn init(cx: &mut MutableAppContext) { + cx.add_bindings([ + Binding::new("cmd-shift-O", Toggle, Some("Editor")), + Binding::new("escape", Toggle, Some("GoToLine")), + Binding::new("enter", Confirm, Some("GoToLine")), + ]); + cx.add_action(OutlineView::toggle); + cx.add_action(OutlineView::confirm); +} + +struct OutlineView {} + +impl Entity for OutlineView { + type Event = (); +} + +impl View for OutlineView { + fn ui_name() -> &'static str { + "OutlineView" + } + + fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox { + todo!() + } +} + +impl OutlineView { + fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext) { + let editor = workspace + .active_item(cx) + .unwrap() + .to_any() + .downcast::() + .unwrap(); + let buffer = editor.read(cx).buffer().read(cx); + dbg!(buffer.read(cx).outline()); + } + + fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) {} +} 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/languages/rust/outline.scm b/crates/zed/languages/rust/outline.scm new file mode 100644 index 0000000000000000000000000000000000000000..3f3cf62fc09352231508dc5ae7f29b5a48a09105 --- /dev/null +++ b/crates/zed/languages/rust/outline.scm @@ -0,0 +1,17 @@ +(impl_item + "impl" @context + type: (_) @name) @item + +(function_item + (visibility_modifier)? @context + "fn" @context + name: (identifier) @name) @item + +(struct_item + (visibility_modifier)? @context + "struct" @context + name: (type_identifier) @name) @item + +(field_declaration + (visibility_modifier)? @context + name: (field_identifier) @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({ From ef596c64f8e38fc184ca7da781488bf1be80611d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 13 Jan 2022 11:35:43 +0100 Subject: [PATCH 02/34] Add OutlineItem::depth so that we can render a tree in the outline view --- crates/language/src/buffer.rs | 11 ++++++++++- crates/language/src/outline.rs | 1 + 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 515def357f8aa089f9eae1d816ffd1c089e4b69f..a0c9e38349141c58be02dc51974ab70bf55fc7ac 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1853,8 +1853,9 @@ impl BufferSnapshot { let context_capture_ix = grammar.outline_query.capture_index_for_name("context")?; let name_capture_ix = grammar.outline_query.capture_index_for_name("name")?; + let mut stack: Vec> = Default::default(); let mut id = 0; - let mut items = matches + let items = matches .filter_map(|mat| { let item_node = mat.nodes_for_capture_index(item_capture_ix).next()?; let mut name_node = Some(mat.nodes_for_capture_index(name_capture_ix).next()?); @@ -1899,8 +1900,16 @@ impl BufferSnapshot { text.extend(self.text_for_range(range)); } + 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 { id, + depth: stack.len() - 1, range, text, name_range_in_text, diff --git a/crates/language/src/outline.rs b/crates/language/src/outline.rs index 427b91d46419fd9fab0bac1f31592853021fcae4..c824c6cd87a11612eb7b42e126c1984f37742d52 100644 --- a/crates/language/src/outline.rs +++ b/crates/language/src/outline.rs @@ -6,6 +6,7 @@ pub struct Outline(pub Vec); #[derive(Debug)] pub struct OutlineItem { pub id: usize, + pub depth: usize, pub range: Range, pub text: String, pub name_range_in_text: Range, From d6ed2ba6425e3f128dd536f4d1cceb3430f3a627 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 13 Jan 2022 12:01:11 +0100 Subject: [PATCH 03/34] Start on rendering the outline view --- Cargo.lock | 1 + crates/language/src/language.rs | 2 +- crates/language/src/outline.rs | 2 +- crates/outline/Cargo.toml | 3 +- crates/outline/src/outline.rs | 155 ++++++++++++++++++++++++++++++-- 5 files changed, 152 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f1761a5daaa2eb4d9f09dfac36f6a63a5ee91bb1..289ed1957ebc494ca7a8770b08ba31c64d29538e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3127,6 +3127,7 @@ version = "0.1.0" dependencies = [ "editor", "gpui", + "language", "postage", "text", "workspace", diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 56bcfbc67c6137fb71a612952845bdf022301e53..72b883df0cea0ffa02c3dfdce29da2892f1ed08b 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -14,7 +14,7 @@ pub use diagnostic_set::DiagnosticEntry; use gpui::AppContext; use highlight_map::HighlightMap; use lazy_static::lazy_static; -pub use outline::Outline; +pub use outline::{Outline, OutlineItem}; use parking_lot::Mutex; use serde::Deserialize; use std::{ops::Range, path::Path, str, sync::Arc}; diff --git a/crates/language/src/outline.rs b/crates/language/src/outline.rs index c824c6cd87a11612eb7b42e126c1984f37742d52..6aa559f9639f95fc9ddbbe9400d77f2f018364d0 100644 --- a/crates/language/src/outline.rs +++ b/crates/language/src/outline.rs @@ -3,7 +3,7 @@ use std::ops::Range; #[derive(Debug)] pub struct Outline(pub Vec); -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct OutlineItem { pub id: usize, pub depth: usize, diff --git a/crates/outline/Cargo.toml b/crates/outline/Cargo.toml index 9ee0c76f7d3da8e2acf9b4e8d61f60d3dff8564e..4a6d4630888bac6a7171d0574da3de10c6873ea0 100644 --- a/crates/outline/Cargo.toml +++ b/crates/outline/Cargo.toml @@ -7,8 +7,9 @@ edition = "2021" path = "src/outline.rs" [dependencies] -text = { path = "../text" } editor = { path = "../editor" } gpui = { path = "../gpui" } +language = { path = "../language" } +text = { path = "../text" } workspace = { path = "../workspace" } postage = { version = "0.4", features = ["futures-traits"] } diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index e9507296e78f0df289ccb314dd13d4b8e058c650..7ac63f1382d4b095d5a5d4dd52976ddba2553d00 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -1,10 +1,11 @@ use editor::{display_map::ToDisplayPoint, Autoscroll, Editor, EditorSettings}; use gpui::{ action, elements::*, geometry::vector::Vector2F, keymap::Binding, Axis, Entity, - MutableAppContext, RenderContext, View, ViewContext, ViewHandle, + MutableAppContext, RenderContext, View, ViewContext, ViewHandle, WeakViewHandle, }; +use language::{Outline, OutlineItem}; use postage::watch; -use std::sync::Arc; +use std::{cmp, sync::Arc}; use text::{Bias, Point, Selection}; use workspace::{Settings, Workspace}; @@ -14,14 +15,21 @@ action!(Confirm); pub fn init(cx: &mut MutableAppContext) { cx.add_bindings([ Binding::new("cmd-shift-O", Toggle, Some("Editor")), - Binding::new("escape", Toggle, Some("GoToLine")), - Binding::new("enter", Confirm, Some("GoToLine")), + Binding::new("escape", Toggle, Some("OutlineView")), + Binding::new("enter", Confirm, Some("OutlineView")), ]); cx.add_action(OutlineView::toggle); cx.add_action(OutlineView::confirm); } -struct OutlineView {} +struct OutlineView { + handle: WeakViewHandle, + outline: Outline, + matches: Vec, + query_editor: ViewHandle, + list_state: UniformListState, + settings: watch::Receiver, +} impl Entity for OutlineView { type Event = (); @@ -33,11 +41,70 @@ impl View for OutlineView { } fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox { - todo!() + let settings = self.settings.borrow(); + + Align::new( + ConstrainedBox::new( + Container::new( + 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()) + .boxed(), + ) + .with_style(settings.theme.selector.container) + .boxed(), + ) + .with_max_width(500.0) + .with_max_height(420.0) + .boxed(), + ) + .top() + .named("outline view") + } + + fn on_focus(&mut self, cx: &mut ViewContext) { + cx.focus(&self.query_editor); } } impl OutlineView { + fn new( + outline: Outline, + 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(); + Self { + handle: cx.weak_handle(), + matches: outline.0.clone(), + outline, + query_editor, + list_state: Default::default(), + settings, + } + } + fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext) { let editor = workspace .active_item(cx) @@ -45,9 +112,81 @@ impl OutlineView { .to_any() .downcast::() .unwrap(); - let buffer = editor.read(cx).buffer().read(cx); - dbg!(buffer.read(cx).outline()); + let buffer = editor.read(cx).buffer().read(cx).read(cx).outline(); + if let Some(outline) = buffer { + workspace.toggle_modal(cx, |cx, workspace| { + cx.add_view(|cx| OutlineView::new(outline, workspace.settings(), cx)) + }) + } } fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) {} + + fn on_query_editor_event( + &mut self, + _: ViewHandle, + event: &editor::Event, + cx: &mut ViewContext, + ) { + match event { + editor::Event::Edited => { + let query = self.query_editor.update(cx, |buffer, cx| buffer.text(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 |(i, outline_match)| view.render_match(outline_match, start + i), + )); + }, + ); + + Container::new(list.boxed()) + .with_margin_top(6.0) + .named("matches") + } + + fn render_match(&self, outline_match: &OutlineItem, index: usize) -> ElementBox { + // TODO: maintain selected index. + let selected_index = 0; + let settings = self.settings.borrow(); + let style = if index == selected_index { + &settings.theme.selector.active_item + } else { + &settings.theme.selector.item + }; + + Label::new(outline_match.text.clone(), style.label.clone()) + .contained() + .with_padding_left(20. * outline_match.depth as f32) + .contained() + .with_style(style.container) + .boxed() + } } From 5f2ac61401b9c443e3404a2e2d0beb321b341783 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 13 Jan 2022 15:07:48 +0100 Subject: [PATCH 04/34] Use only lowercase characters to determine if query matches a candidate --- crates/fuzzy/src/char_bag.rs | 1 + crates/fuzzy/src/fuzzy.rs | 18 +++++++++++------- 2 files changed, 12 insertions(+), 7 deletions(-) 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..5f4dbea684e7fe266bd74740eb39f8c69fe8f2fd 100644 --- a/crates/fuzzy/src/fuzzy.rs +++ b/crates/fuzzy/src/fuzzy.rs @@ -433,13 +433,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; From 5e64f1aca827eeda1501bd621a6060bc4a96c557 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 13 Jan 2022 15:09:27 +0100 Subject: [PATCH 05/34] Report the candidate's index when matching strings --- crates/fuzzy/src/fuzzy.rs | 21 ++++++++++++++------- crates/theme_selector/src/theme_selector.rs | 4 +++- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/crates/fuzzy/src/fuzzy.rs b/crates/fuzzy/src/fuzzy.rs index 5f4dbea684e7fe266bd74740eb39f8c69fe8f2fd..c3624b403770fd09b2ea53e7e243e91c077e5b8f 100644 --- a/crates/fuzzy/src/fuzzy.rs +++ b/crates/fuzzy/src/fuzzy.rs @@ -109,6 +109,7 @@ impl<'a> MatchCandidate for &'a StringMatchCandidate { #[derive(Clone, Debug)] pub struct StringMatch { + pub candidate_index: usize, pub score: f64, pub positions: Vec, pub string: String, @@ -187,8 +188,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, @@ -197,6 +198,7 @@ pub async fn match_strings( max_results, ); matcher.match_strings( + segment_start, &candidates[segment_start..segment_end], results, cancel_flag, @@ -319,6 +321,7 @@ impl<'a> Matcher<'a> { pub fn match_strings( &mut self, + start_index: usize, candidates: &[StringMatchCandidate], results: &mut Vec, cancel_flag: &AtomicBool, @@ -326,10 +329,12 @@ impl<'a> Matcher<'a> { self.match_internal( &[], &[], + start_index, candidates.iter(), results, cancel_flag, - |candidate, score| StringMatch { + |candidate_index, candidate, score| StringMatch { + candidate_index, score, positions: Vec::new(), string: candidate.string.to_string(), @@ -353,10 +358,11 @@ impl<'a> Matcher<'a> { self.match_internal( &prefix, &lowercase_prefix, + 0, path_entries, results, cancel_flag, - |candidate, score| PathMatch { + |_, candidate, score| PathMatch { score, worktree_id: tree_id, positions: Vec::new(), @@ -370,18 +376,19 @@ impl<'a> Matcher<'a> { &mut self, prefix: &[char], lowercase_prefix: &[char], + start_index: usize, candidates: impl Iterator, results: &mut Vec, cancel_flag: &AtomicBool, build_match: F, ) where R: Match, - F: Fn(&C, f64) -> R, + F: Fn(usize, &C, f64) -> R, { let mut candidate_chars = Vec::new(); let mut lowercase_candidate_chars = Vec::new(); - for candidate in candidates { + for (candidate_ix, candidate) in candidates.enumerate() { if !candidate.has_chars(self.query_char_bag) { continue; } @@ -415,7 +422,7 @@ impl<'a> Matcher<'a> { ); if score > 0.0 { - let mut mat = build_match(&candidate, score); + let mut mat = build_match(start_index + candidate_ix, &candidate, score); if let Err(i) = results.binary_search_by(|m| mat.cmp(&m)) { if results.len() < self.max_results { mat.set_positions(self.match_positions.clone()); diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs index df7713ad1fa947427d55d356b00f7347f7ef4276..7d65087b0b755eeec8ebca258abe7b43c5827bd9 100644 --- a/crates/theme_selector/src/theme_selector.rs +++ b/crates/theme_selector/src/theme_selector.rs @@ -167,7 +167,9 @@ impl ThemeSelector { self.matches = if query.is_empty() { candidates .into_iter() - .map(|candidate| StringMatch { + .enumerate() + .map(|(index, candidate)| StringMatch { + candidate_index: index, string: candidate.string, positions: Vec::new(), score: 0.0, From 06ba1c64cf2ffd1e296c9a9899efd7ff67bf282d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 13 Jan 2022 15:10:29 +0100 Subject: [PATCH 06/34] Implement `Outline::search` --- crates/language/Cargo.toml | 1 + crates/language/src/buffer.rs | 2 +- crates/language/src/outline.rs | 70 +++++++++++++++++++++++++++++++++- 3 files changed, 71 insertions(+), 2 deletions(-) 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 a0c9e38349141c58be02dc51974ab70bf55fc7ac..03f7552e69e56451b7e26b27c837e5253583c1c4 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1917,7 +1917,7 @@ impl BufferSnapshot { }) .collect::>(); - Some(Outline(items)) + Some(Outline::new(items)) } pub fn enclosing_bracket_ranges( diff --git a/crates/language/src/outline.rs b/crates/language/src/outline.rs index 6aa559f9639f95fc9ddbbe9400d77f2f018364d0..c8ed6cf5758654d07691cc6f1eb72f6381e5b115 100644 --- a/crates/language/src/outline.rs +++ b/crates/language/src/outline.rs @@ -1,7 +1,13 @@ use std::ops::Range; +use fuzzy::{StringMatch, StringMatchCandidate}; +use gpui::AppContext; + #[derive(Debug)] -pub struct Outline(pub Vec); +pub struct Outline { + pub items: Vec, + candidates: Vec, +} #[derive(Clone, Debug)] pub struct OutlineItem { @@ -11,3 +17,65 @@ pub struct OutlineItem { pub text: String, pub name_range_in_text: Range, } + +impl Outline { + pub fn new(items: Vec) -> Self { + Self { + candidates: items + .iter() + .map(|item| { + let text = &item.text[item.name_range_in_text.clone()]; + StringMatchCandidate { + string: text.to_string(), + char_bag: text.into(), + } + }) + .collect(), + items, + } + } + + pub fn search(&self, query: &str, cx: &AppContext) -> Vec { + let mut matches = smol::block_on(fuzzy::match_strings( + &self.candidates, + query, + true, + 100, + &Default::default(), + cx.background().clone(), + )); + matches.sort_unstable_by_key(|m| m.candidate_index); + + 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_index]; + for position in &mut string_match.positions { + *position += outline_match.name_range_in_text.start; + } + + for (ix, item) in self.items[prev_item_ix..string_match.candidate_index] + .iter() + .enumerate() + { + let candidate_index = ix + prev_item_ix; + if item.range.contains(&outline_match.range.start) + && item.range.contains(&outline_match.range.end) + { + tree_matches.push(StringMatch { + candidate_index, + score: Default::default(), + positions: Default::default(), + string: Default::default(), + }); + } + } + + prev_item_ix = string_match.candidate_index; + tree_matches.push(string_match); + } + + tree_matches + } +} From d74658fdb5369a979df45682f341e1850b6ce867 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 13 Jan 2022 15:10:50 +0100 Subject: [PATCH 07/34] Allow searching of outline items --- Cargo.lock | 2 ++ crates/outline/Cargo.toml | 1 + crates/outline/src/outline.rs | 51 +++++++++++++++++++++++++---------- 3 files changed, 40 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 289ed1957ebc494ca7a8770b08ba31c64d29538e..ef2be46b9d3883a6732452901b70b3c713f72a05 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2602,6 +2602,7 @@ dependencies = [ "ctor", "env_logger", "futures", + "fuzzy", "gpui", "lazy_static", "log", @@ -3126,6 +3127,7 @@ name = "outline" version = "0.1.0" dependencies = [ "editor", + "fuzzy", "gpui", "language", "postage", diff --git a/crates/outline/Cargo.toml b/crates/outline/Cargo.toml index 4a6d4630888bac6a7171d0574da3de10c6873ea0..5383e676610b47d7d5dffcfbe197cc0590fdd422 100644 --- a/crates/outline/Cargo.toml +++ b/crates/outline/Cargo.toml @@ -8,6 +8,7 @@ path = "src/outline.rs" [dependencies] editor = { path = "../editor" } +fuzzy = { path = "../fuzzy" } gpui = { path = "../gpui" } language = { path = "../language" } text = { path = "../text" } diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index 7ac63f1382d4b095d5a5d4dd52976ddba2553d00..d692358faeffbe4e70b7ad58f7bd1659b0b97d61 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -1,12 +1,12 @@ -use editor::{display_map::ToDisplayPoint, Autoscroll, Editor, EditorSettings}; +use editor::{Editor, EditorSettings}; +use fuzzy::StringMatch; use gpui::{ - action, elements::*, geometry::vector::Vector2F, keymap::Binding, Axis, Entity, - MutableAppContext, RenderContext, View, ViewContext, ViewHandle, WeakViewHandle, + action, elements::*, keymap::Binding, Axis, Entity, MutableAppContext, RenderContext, View, + ViewContext, ViewHandle, WeakViewHandle, }; -use language::{Outline, OutlineItem}; +use language::Outline; use postage::watch; use std::{cmp, sync::Arc}; -use text::{Bias, Point, Selection}; use workspace::{Settings, Workspace}; action!(Toggle); @@ -25,7 +25,7 @@ pub fn init(cx: &mut MutableAppContext) { struct OutlineView { handle: WeakViewHandle, outline: Outline, - matches: Vec, + matches: Vec, query_editor: ViewHandle, list_state: UniformListState, settings: watch::Receiver, @@ -40,7 +40,7 @@ impl View for OutlineView { "OutlineView" } - fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox { + fn render(&mut self, _: &mut RenderContext) -> ElementBox { let settings = self.settings.borrow(); Align::new( @@ -95,14 +95,16 @@ impl OutlineView { }); cx.subscribe(&query_editor, Self::on_query_editor_event) .detach(); - Self { + let mut this = Self { handle: cx.weak_handle(), - matches: outline.0.clone(), + matches: Default::default(), outline, query_editor, list_state: Default::default(), settings, - } + }; + this.update_matches(cx); + this } fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext) { @@ -129,13 +131,32 @@ impl OutlineView { cx: &mut ViewContext, ) { match event { - editor::Event::Edited => { - let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx)); - } + editor::Event::Edited => self.update_matches(cx), _ => {} } } + fn update_matches(&mut self, cx: &mut ViewContext) { + let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx)); + if query.is_empty() { + self.matches = self + .outline + .items + .iter() + .enumerate() + .map(|(index, _)| StringMatch { + candidate_index: index, + score: Default::default(), + positions: Default::default(), + string: Default::default(), + }) + .collect(); + } else { + self.matches = self.outline.search(&query, cx); + } + cx.notify(); + } + fn render_matches(&self) -> ElementBox { if self.matches.is_empty() { let settings = self.settings.borrow(); @@ -172,7 +193,7 @@ impl OutlineView { .named("matches") } - fn render_match(&self, outline_match: &OutlineItem, index: usize) -> ElementBox { + fn render_match(&self, string_match: &StringMatch, index: usize) -> ElementBox { // TODO: maintain selected index. let selected_index = 0; let settings = self.settings.borrow(); @@ -181,8 +202,10 @@ impl OutlineView { } else { &settings.theme.selector.item }; + let outline_match = &self.outline.items[string_match.candidate_index]; Label::new(outline_match.text.clone(), style.label.clone()) + .with_highlights(string_match.positions.clone()) .contained() .with_padding_left(20. * outline_match.depth as f32) .contained() From aee3bb98f24197190ba59cbc755fb50b110919ad Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 13 Jan 2022 16:44:06 +0100 Subject: [PATCH 08/34] Implement selecting prev and next in outline view Co-Authored-By: Nathan Sobo --- Cargo.lock | 1 + crates/outline/Cargo.toml | 1 + crates/outline/src/outline.rs | 86 ++++++++++++++++++++++++++--------- 3 files changed, 67 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ef2be46b9d3883a6732452901b70b3c713f72a05..c02c7943ab70feb51076898ea7174ee7ffbf6eec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3130,6 +3130,7 @@ dependencies = [ "fuzzy", "gpui", "language", + "ordered-float", "postage", "text", "workspace", diff --git a/crates/outline/Cargo.toml b/crates/outline/Cargo.toml index 5383e676610b47d7d5dffcfbe197cc0590fdd422..80e612bf3b3d417ec06551c0b73ce7b99b24b006 100644 --- a/crates/outline/Cargo.toml +++ b/crates/outline/Cargo.toml @@ -13,4 +13,5 @@ gpui = { path = "../gpui" } language = { path = "../language" } text = { path = "../text" } workspace = { path = "../workspace" } +ordered-float = "2.1.1" postage = { version = "0.4", features = ["futures-traits"] } diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index d692358faeffbe4e70b7ad58f7bd1659b0b97d61..7a47c1a01786c5fcc42ca1f15d581f8651cb59c0 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -1,10 +1,18 @@ use editor::{Editor, EditorSettings}; use fuzzy::StringMatch; use gpui::{ - action, elements::*, keymap::Binding, Axis, Entity, MutableAppContext, RenderContext, View, - ViewContext, ViewHandle, WeakViewHandle, + action, + elements::*, + keymap::{ + self, + menu::{SelectNext, SelectPrev}, + Binding, + }, + AppContext, Axis, Entity, MutableAppContext, RenderContext, View, ViewContext, ViewHandle, + WeakViewHandle, }; use language::Outline; +use ordered_float::OrderedFloat; use postage::watch; use std::{cmp, sync::Arc}; use workspace::{Settings, Workspace}; @@ -20,11 +28,14 @@ pub fn init(cx: &mut MutableAppContext) { ]); cx.add_action(OutlineView::toggle); cx.add_action(OutlineView::confirm); + cx.add_action(OutlineView::select_prev); + cx.add_action(OutlineView::select_next); } struct OutlineView { handle: WeakViewHandle, outline: Outline, + selected_match_index: usize, matches: Vec, query_editor: ViewHandle, list_state: UniformListState, @@ -40,6 +51,12 @@ impl View for OutlineView { "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(); @@ -98,6 +115,7 @@ impl OutlineView { let mut this = Self { handle: cx.weak_handle(), matches: Default::default(), + selected_match_index: 0, outline, query_editor, list_state: Default::default(), @@ -122,7 +140,23 @@ impl OutlineView { } } - fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) {} + fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext) { + if self.selected_match_index > 0 { + self.selected_match_index -= 1; + self.list_state.scroll_to(self.selected_match_index); + cx.notify(); + } + } + + fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { + if self.selected_match_index + 1 < self.matches.len() { + self.selected_match_index += 1; + self.list_state.scroll_to(self.selected_match_index); + cx.notify(); + } + } + + fn confirm(&mut self, _: &Confirm, _: &mut ViewContext) {} fn on_query_editor_event( &mut self, @@ -151,9 +185,19 @@ impl OutlineView { string: Default::default(), }) .collect(); + self.selected_match_index = 0; } else { self.matches = self.outline.search(&query, cx); + self.selected_match_index = self + .matches + .iter() + .enumerate() + .max_by_key(|(_, m)| OrderedFloat(m.score)) + .map(|(ix, _)| ix) + .unwrap_or(0); } + + self.list_state.scroll_to(self.selected_match_index); cx.notify(); } @@ -172,21 +216,23 @@ impl OutlineView { } 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 |(i, outline_match)| view.render_match(outline_match, start + i), - )); - }, - ); + 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) @@ -194,10 +240,8 @@ impl OutlineView { } fn render_match(&self, string_match: &StringMatch, index: usize) -> ElementBox { - // TODO: maintain selected index. - let selected_index = 0; let settings = self.settings.borrow(); - let style = if index == selected_index { + let style = if index == self.selected_match_index { &settings.theme.selector.active_item } else { &settings.theme.selector.item From e165f1e16c27d3008dc76a126c93ee9be4fd1010 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 13 Jan 2022 16:59:52 +0100 Subject: [PATCH 09/34] Use `OutlineItem::depth` to include ancestors of matching candidates Co-Authored-By: Nathan Sobo --- crates/language/src/outline.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/crates/language/src/outline.rs b/crates/language/src/outline.rs index c8ed6cf5758654d07691cc6f1eb72f6381e5b115..55472f9a1c6ab7f3bd4226e55644ebdfcf77fe4e 100644 --- a/crates/language/src/outline.rs +++ b/crates/language/src/outline.rs @@ -55,24 +55,29 @@ impl Outline { *position += outline_match.name_range_in_text.start; } + let mut cur_depth = outline_match.depth; for (ix, item) in self.items[prev_item_ix..string_match.candidate_index] .iter() .enumerate() + .rev() { + if cur_depth == 0 { + break; + } + let candidate_index = ix + prev_item_ix; - if item.range.contains(&outline_match.range.start) - && item.range.contains(&outline_match.range.end) - { + if item.depth == cur_depth - 1 { tree_matches.push(StringMatch { candidate_index, score: Default::default(), positions: Default::default(), string: Default::default(), }); + cur_depth -= 1; } } - prev_item_ix = string_match.candidate_index; + prev_item_ix = string_match.candidate_index + 1; tree_matches.push(string_match); } From 2660d37ad81e17989eb7bb678c0837a6ab710541 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 13 Jan 2022 18:24:00 +0100 Subject: [PATCH 10/34] Return `Outline` from `MultiBuffer::outline` Co-Authored-By: Max Brunsfeld --- crates/editor/src/multi_buffer.rs | 24 ++++++++++++++++++++---- crates/language/src/buffer.rs | 4 ++-- crates/language/src/outline.rs | 12 ++++++------ crates/outline/src/outline.rs | 6 +++--- 4 files changed, 31 insertions(+), 15 deletions(-) diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 54a44d2aa77555251458c731cdf266c97869d1d8..128cefd49e9a76cf6cee9fef0876ca93397caed3 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, Outline, + Buffer, BufferChunks, BufferSnapshot, Chunk, DiagnosticEntry, Event, File, Language, Outline, + OutlineItem, Selection, ToOffset as _, ToPoint as _, TransactionId, }; use std::{ cell::{Ref, RefCell}, @@ -1698,8 +1698,24 @@ impl MultiBufferSnapshot { }) } - pub fn outline(&self) -> Option { - self.as_singleton().and_then(move |buffer| buffer.outline()) + pub fn outline(&self) -> Option> { + let buffer = self.as_singleton()?; + let outline = buffer.outline()?; + let excerpt_id = &self.excerpts.iter().next().unwrap().id; + Some(Outline::new( + outline + .items + .into_iter() + .map(|item| OutlineItem { + id: item.id, + 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, + name_range_in_text: item.name_range_in_text, + }) + .collect(), + )) } fn buffer_snapshot_for_excerpt<'a>( diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 03f7552e69e56451b7e26b27c837e5253583c1c4..40f83e9ba0d8175f7536a111adf3806cb22a5461 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1835,7 +1835,7 @@ impl BufferSnapshot { } } - pub fn outline(&self) -> Option { + pub fn outline(&self) -> Option> { let tree = self.tree.as_ref()?; let grammar = self .language @@ -1910,7 +1910,7 @@ impl BufferSnapshot { Some(OutlineItem { id, depth: stack.len() - 1, - range, + range: self.anchor_after(range.start)..self.anchor_before(range.end), text, name_range_in_text, }) diff --git a/crates/language/src/outline.rs b/crates/language/src/outline.rs index 55472f9a1c6ab7f3bd4226e55644ebdfcf77fe4e..7d2e47d964cae10b3f5b98b81a96b5d6826df8b1 100644 --- a/crates/language/src/outline.rs +++ b/crates/language/src/outline.rs @@ -4,22 +4,22 @@ use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::AppContext; #[derive(Debug)] -pub struct Outline { - pub items: Vec, +pub struct Outline { + pub items: Vec>, candidates: Vec, } #[derive(Clone, Debug)] -pub struct OutlineItem { +pub struct OutlineItem { pub id: usize, pub depth: usize, - pub range: Range, + pub range: Range, pub text: String, pub name_range_in_text: Range, } -impl Outline { - pub fn new(items: Vec) -> Self { +impl Outline { + pub fn new(items: Vec>) -> Self { Self { candidates: items .iter() diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index 7a47c1a01786c5fcc42ca1f15d581f8651cb59c0..9b182f0caeb8d93275969f40bc55c300001488f7 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -1,4 +1,4 @@ -use editor::{Editor, EditorSettings}; +use editor::{Anchor, Editor, EditorSettings}; use fuzzy::StringMatch; use gpui::{ action, @@ -34,7 +34,7 @@ pub fn init(cx: &mut MutableAppContext) { struct OutlineView { handle: WeakViewHandle, - outline: Outline, + outline: Outline, selected_match_index: usize, matches: Vec, query_editor: ViewHandle, @@ -90,7 +90,7 @@ impl View for OutlineView { impl OutlineView { fn new( - outline: Outline, + outline: Outline, settings: watch::Receiver, cx: &mut ViewContext, ) -> Self { From 055d48cfb23100573251f5d86c51d72f0a342d3e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 13 Jan 2022 18:43:49 +0100 Subject: [PATCH 11/34] Select the closest outline item when the outline view's query is empty Co-Authored-By: Max Brunsfeld --- crates/editor/src/editor.rs | 6 ++++-- crates/language/src/buffer.rs | 6 +++++- crates/outline/src/outline.rs | 38 +++++++++++++++++++++++++++++++---- 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index eb412f3dcb5343c8548a7d7a6228756713959c91..0c861a889fe22c9be874d1568b8b76b2b18f1b34 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; diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 40f83e9ba0d8175f7536a111adf3806cb22a5461..317cff84b608987919b6d3d9c27cce9be7e3cd22 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1917,7 +1917,11 @@ impl BufferSnapshot { }) .collect::>(); - Some(Outline::new(items)) + if items.is_empty() { + None + } else { + Some(Outline::new(items)) + } } pub fn enclosing_bracket_ranges( diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index 9b182f0caeb8d93275969f40bc55c300001488f7..778b1be555a9703b5283174aaf32c6307fb69168 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -1,4 +1,4 @@ -use editor::{Anchor, Editor, EditorSettings}; +use editor::{Anchor, AnchorRangeExt, Editor, EditorSettings}; use fuzzy::StringMatch; use gpui::{ action, @@ -14,7 +14,10 @@ use gpui::{ use language::Outline; use ordered_float::OrderedFloat; use postage::watch; -use std::{cmp, sync::Arc}; +use std::{ + cmp::{self, Reverse}, + sync::Arc, +}; use workspace::{Settings, Workspace}; action!(Toggle); @@ -34,6 +37,7 @@ pub fn init(cx: &mut MutableAppContext) { struct OutlineView { handle: WeakViewHandle, + editor: ViewHandle, outline: Outline, selected_match_index: usize, matches: Vec, @@ -91,6 +95,7 @@ impl View for OutlineView { impl OutlineView { fn new( outline: Outline, + editor: ViewHandle, settings: watch::Receiver, cx: &mut ViewContext, ) -> Self { @@ -114,6 +119,7 @@ impl OutlineView { .detach(); let mut this = Self { handle: cx.weak_handle(), + editor, matches: Default::default(), selected_match_index: 0, outline, @@ -135,7 +141,7 @@ impl OutlineView { let buffer = editor.read(cx).buffer().read(cx).read(cx).outline(); if let Some(outline) = buffer { workspace.toggle_modal(cx, |cx, workspace| { - cx.add_view(|cx| OutlineView::new(outline, workspace.settings(), cx)) + cx.add_view(|cx| OutlineView::new(outline, editor, workspace.settings(), cx)) }) } } @@ -185,7 +191,31 @@ impl OutlineView { string: Default::default(), }) .collect(); - self.selected_match_index = 0; + + let editor = self.editor.read(cx); + let buffer = editor.buffer().read(cx).read(cx); + let cursor_offset = editor.newest_selection::(&buffer).head(); + self.selected_match_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; } else { self.matches = self.outline.search(&query, cx); self.selected_match_index = self From 373fe6fadf87bbeec3137d314f6d17ccf241cb3d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 13 Jan 2022 09:49:46 -0800 Subject: [PATCH 12/34] Change Editor::set_highlighted_row to take a row range Co-Authored-By: Antonio Scandurra --- crates/editor/src/editor.rs | 12 ++++----- crates/editor/src/element.rs | 39 ++++++++++++++++++----------- crates/go_to_line/src/go_to_line.rs | 5 ++-- 3 files changed, 33 insertions(+), 23 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 0c861a889fe22c9be874d1568b8b76b2b18f1b34..574ff3822b168c80395cdc21caf410aaa125ab17 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -376,7 +376,7 @@ pub struct Editor { blinking_paused: bool, mode: EditorMode, placeholder_text: Option>, - highlighted_row: Option, + highlighted_rows: Option>, } pub struct EditorSnapshot { @@ -505,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), @@ -3546,12 +3546,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..a7b082dbdf0e483cf058339a79a2886a05e51c7b 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -263,12 +263,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), @@ -640,15 +644,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 +759,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 +840,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 +971,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/go_to_line/src/go_to_line.rs b/crates/go_to_line/src/go_to_line.rs index cf965ebf1b398816c87862e4de9db388998e5615..6a90eb785b413d7a3f50c443b44a0eedb626a22e 100644 --- a/crates/go_to_line/src/go_to_line.rs +++ b/crates/go_to_line/src/go_to_line.rs @@ -143,8 +143,9 @@ impl GoToLine { 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())); + active_editor.set_highlighted_rows(Some(row..row + 1)); Some(active_editor.newest_selection(&snapshot.buffer_snapshot)) }); cx.notify(); @@ -162,7 +163,7 @@ impl Entity for GoToLine { let line_selection = self.line_selection.take(); let restore_state = self.restore_state.take(); self.active_editor.update(cx, |editor, cx| { - editor.set_highlighted_row(None); + editor.set_highlighted_rows(None); if let Some((line_selection, restore_state)) = line_selection.zip(restore_state) { let newest_selection = editor.newest_selection::(&editor.buffer().read(cx).read(cx)); From f2cef0b795e7fe1ad3efb3a444ab0de657d33434 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 13 Jan 2022 11:48:44 -0800 Subject: [PATCH 13/34] Implement navigation via outline modal Co-Authored-By: Antonio Scandurra --- crates/go_to_line/src/go_to_line.rs | 18 ++-- crates/outline/src/outline.rs | 123 +++++++++++++++++++++++----- 2 files changed, 114 insertions(+), 27 deletions(-) diff --git a/crates/go_to_line/src/go_to_line.rs b/crates/go_to_line/src/go_to_line.rs index 6a90eb785b413d7a3f50c443b44a0eedb626a22e..301f37a9fbc5713114ce45121a917c0f17f8121c 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,14 +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_rows(Some(row..row + 1)); - Some(active_editor.newest_selection(&snapshot.buffer_snapshot)) + Some( + active_editor + .newest_selection::(&snapshot.buffer_snapshot) + .id, + ) }); cx.notify(); } @@ -160,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_rows(None); - if let Some((line_selection, restore_state)) = line_selection.zip(restore_state) { + 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); } diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index 778b1be555a9703b5283174aaf32c6307fb69168..ad5760818334ba20e316f5f5e02ff284aa242c2e 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -1,8 +1,12 @@ -use editor::{Anchor, AnchorRangeExt, Editor, EditorSettings}; +use editor::{ + display_map::ToDisplayPoint, Anchor, AnchorRangeExt, Autoscroll, Editor, EditorSettings, + ToPoint, +}; use fuzzy::StringMatch; use gpui::{ action, elements::*, + geometry::vector::Vector2F, keymap::{ self, menu::{SelectNext, SelectPrev}, @@ -11,7 +15,7 @@ use gpui::{ AppContext, Axis, Entity, MutableAppContext, RenderContext, View, ViewContext, ViewHandle, WeakViewHandle, }; -use language::Outline; +use language::{Outline, Selection}; use ordered_float::OrderedFloat; use postage::watch; use std::{ @@ -37,17 +41,32 @@ pub fn init(cx: &mut MutableAppContext) { struct OutlineView { handle: WeakViewHandle, - editor: ViewHandle, + 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 = (); + type Event = Event; + + fn release(&mut self, cx: &mut MutableAppContext) { + self.restore_active_editor(cx); + } } impl View for OutlineView { @@ -79,8 +98,8 @@ impl View for OutlineView { .with_style(settings.theme.selector.container) .boxed(), ) - .with_max_width(500.0) - .with_max_height(420.0) + .with_max_width(800.0) + .with_max_height(1200.0) .boxed(), ) .top() @@ -117,11 +136,21 @@ impl OutlineView { }); 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(), - editor, + active_editor: editor, matches: Default::default(), selected_match_index: 0, + restore_state, + symbol_selection_id: None, outline, query_editor, list_state: Default::default(), @@ -141,28 +170,79 @@ impl OutlineView { let buffer = editor.read(cx).buffer().read(cx).read(cx).outline(); if let Some(outline) = buffer { workspace.toggle_modal(cx, |cx, workspace| { - cx.add_view(|cx| OutlineView::new(outline, editor, workspace.settings(), cx)) + let view = + cx.add_view(|cx| OutlineView::new(outline, editor, workspace.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.selected_match_index -= 1; - self.list_state.scroll_to(self.selected_match_index); - cx.notify(); + self.select(self.selected_match_index - 1, true, cx); } } fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { if self.selected_match_index + 1 < self.matches.len() { - self.selected_match_index += 1; - self.list_state.scroll_to(self.selected_match_index); + self.select(self.selected_match_index + 1, true, cx); + } + } + + fn select(&mut self, index: usize, navigate: bool, cx: &mut ViewContext) { + self.selected_match_index = index; + self.list_state.scroll_to(self.selected_match_index); + if navigate { + let selected_match = &self.matches[self.selected_match_index]; + let outline_item = &self.outline.items[selected_match.candidate_index]; + 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, _: &mut ViewContext) {} + 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, @@ -177,8 +257,11 @@ impl OutlineView { } 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 @@ -192,10 +275,10 @@ impl OutlineView { }) .collect(); - let editor = self.editor.read(cx); + let editor = self.active_editor.read(cx); let buffer = editor.buffer().read(cx).read(cx); let cursor_offset = editor.newest_selection::(&buffer).head(); - self.selected_match_index = self + selected_index = self .outline .items .iter() @@ -216,19 +299,19 @@ impl OutlineView { .max_by_key(|(_, depth, distance)| (*depth, Reverse(*distance))) .unwrap() .0; + navigate_to_selected_index = false; } else { self.matches = self.outline.search(&query, cx); - self.selected_match_index = self + selected_index = self .matches .iter() .enumerate() .max_by_key(|(_, m)| OrderedFloat(m.score)) .map(|(ix, _)| ix) .unwrap_or(0); + navigate_to_selected_index = true; } - - self.list_state.scroll_to(self.selected_match_index); - cx.notify(); + self.select(selected_index, navigate_to_selected_index, cx); } fn render_matches(&self) -> ElementBox { From 950b06674ffafe5e234711e7023f5bd0d1a7c47f Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 13 Jan 2022 11:49:00 -0800 Subject: [PATCH 14/34] Add more items to rust outline query --- crates/zed/languages/rust/outline.scm | 56 ++++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 5 deletions(-) diff --git a/crates/zed/languages/rust/outline.scm b/crates/zed/languages/rust/outline.scm index 3f3cf62fc09352231508dc5ae7f29b5a48a09105..bf92b3fdfa9e49542544ad0dc55c6a1e2ab5a4a5 100644 --- a/crates/zed/languages/rust/outline.scm +++ b/crates/zed/languages/rust/outline.scm @@ -1,17 +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: (_)? @context + "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: (identifier) @name) @item + name: (_) @name) @item -(struct_item +(function_signature_item (visibility_modifier)? @context - "struct" @context - name: (type_identifier) @name) @item + (function_modifiers)? @context + "fn" @context + name: (_) @name) @item + +(macro_definition + . "macro_rules!" @context + name: (_) @name) @item + +(mod_item + (visibility_modifier)? @context + "mod" + 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: (field_identifier) @name) @item + name: (_) @name) @item From 3e1c559b2d6943760730b35896f2879214154587 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 13 Jan 2022 14:04:25 -0800 Subject: [PATCH 15/34] Allow multiple disjoint nodes to be captured as matcheable in the outline query --- Cargo.lock | 1 + crates/editor/src/multi_buffer.rs | 3 +- crates/language/src/buffer.rs | 60 +++++------- crates/language/src/outline.rs | 37 +++++--- crates/language/src/tests.rs | 131 ++++++++++++++++++++++++-- crates/outline/Cargo.toml | 1 + crates/outline/src/outline.rs | 4 +- crates/zed/languages/rust/outline.scm | 2 +- 8 files changed, 181 insertions(+), 58 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c02c7943ab70feb51076898ea7174ee7ffbf6eec..3604004d0f514a2ac68ffaef1ba1ebc660885d29 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3132,6 +3132,7 @@ dependencies = [ "language", "ordered-float", "postage", + "smol", "text", "workspace", ] diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 128cefd49e9a76cf6cee9fef0876ca93397caed3..05294c141948cee1eaefb693bb117ccfd18fce43 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -1707,12 +1707,11 @@ impl MultiBufferSnapshot { .items .into_iter() .map(|item| OutlineItem { - id: item.id, 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, - name_range_in_text: item.name_range_in_text, + name_ranges: item.name_ranges, }) .collect(), )) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 317cff84b608987919b6d3d9c27cce9be7e3cd22..e699023d842dd452fa28071a54a442c659b61ea0 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1850,52 +1850,45 @@ impl BufferSnapshot { ); let item_capture_ix = grammar.outline_query.capture_index_for_name("item")?; - let context_capture_ix = grammar.outline_query.capture_index_for_name("context")?; 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> = Default::default(); - let mut id = 0; + let mut stack = Vec::>::new(); let items = matches .filter_map(|mat| { let item_node = mat.nodes_for_capture_index(item_capture_ix).next()?; - let mut name_node = Some(mat.nodes_for_capture_index(name_capture_ix).next()?); - let mut context_nodes = mat.nodes_for_capture_index(context_capture_ix).peekable(); - - let id = post_inc(&mut id); let range = item_node.start_byte()..item_node.end_byte(); - let mut text = String::new(); - let mut name_range_in_text = 0..0; - loop { - let node; + let mut name_ranges = Vec::new(); + + for capture in mat.captures { let node_is_name; - match (context_nodes.peek(), name_node.as_ref()) { - (None, None) => break, - (None, Some(_)) => { - node = name_node.take().unwrap(); - node_is_name = true; - } - (Some(_), None) => { - node = context_nodes.next().unwrap(); - node_is_name = false; - } - (Some(context_node), Some(name)) => { - if context_node.start_byte() < name.start_byte() { - node = context_nodes.next().unwrap(); - node_is_name = false; - } else { - node = name_node.take().unwrap(); - node_is_name = true; - } - } + 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(' '); } - let range = node.start_byte()..node.end_byte(); if node_is_name { - name_range_in_text = text.len()..(text.len() + range.len()) + let mut start = text.len() as u32; + let end = start + range.len() as u32; + + // 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); } text.extend(self.text_for_range(range)); } @@ -1908,11 +1901,10 @@ impl BufferSnapshot { stack.push(range.clone()); Some(OutlineItem { - id, depth: stack.len() - 1, range: self.anchor_after(range.start)..self.anchor_before(range.end), text, - name_range_in_text, + name_ranges: name_ranges.into_boxed_slice(), }) }) .collect::>(); diff --git a/crates/language/src/outline.rs b/crates/language/src/outline.rs index 7d2e47d964cae10b3f5b98b81a96b5d6826df8b1..76b7462a972646a796f6032aa53c861b568db7c7 100644 --- a/crates/language/src/outline.rs +++ b/crates/language/src/outline.rs @@ -1,7 +1,6 @@ -use std::ops::Range; - use fuzzy::{StringMatch, StringMatchCandidate}; -use gpui::AppContext; +use gpui::executor::Background; +use std::{ops::Range, sync::Arc}; #[derive(Debug)] pub struct Outline { @@ -11,11 +10,10 @@ pub struct Outline { #[derive(Clone, Debug)] pub struct OutlineItem { - pub id: usize, pub depth: usize, pub range: Range, pub text: String, - pub name_range_in_text: Range, + pub name_ranges: Box<[Range]>, } impl Outline { @@ -24,10 +22,14 @@ impl Outline { candidates: items .iter() .map(|item| { - let text = &item.text[item.name_range_in_text.clone()]; + let text = item + .name_ranges + .iter() + .map(|range| &item.text[range.start as usize..range.end as usize]) + .collect::(); StringMatchCandidate { - string: text.to_string(), - char_bag: text.into(), + char_bag: text.as_str().into(), + string: text, } }) .collect(), @@ -35,15 +37,16 @@ impl Outline { } } - pub fn search(&self, query: &str, cx: &AppContext) -> Vec { - let mut matches = smol::block_on(fuzzy::match_strings( + pub async fn search(&self, query: &str, executor: Arc) -> Vec { + let mut matches = fuzzy::match_strings( &self.candidates, query, true, 100, &Default::default(), - cx.background().clone(), - )); + executor, + ) + .await; matches.sort_unstable_by_key(|m| m.candidate_index); let mut tree_matches = Vec::new(); @@ -51,8 +54,16 @@ impl Outline { let mut prev_item_ix = 0; for mut string_match in matches { let outline_match = &self.items[string_match.candidate_index]; + + 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 { - *position += outline_match.name_range_in_text.start; + 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 mut cur_depth = outline_match.depth; diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index cf73e8dd23218a33946b02169e6ec54cc86eb964..1622ca92a93701acbbcca89dc14c5ef1b56c0a31 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -278,6 +278,121 @@ 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 + "#, + ) + .unwrap(), + )); + + let text = r#" + struct Person { + name: String, + age: usize, + } + + enum LoginState { + LoggedOut, + LoggingOn, + LoggedIn { + person: Person, + time: Instant, + } + } + + 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()) + .unwrap(); + + assert_eq!( + outline + .items + .iter() + .map(|item| (item.text.as_str(), item.name_ranges.as_ref(), item.depth)) + .collect::>(), + &[ + ("struct Person", [7..13].as_slice(), 0), + ("name", &[0..4], 1), + ("age", &[0..3], 1), + ("enum LoginState", &[5..15], 0), + ("LoggedOut", &[0..9], 1), + ("LoggingOn", &[0..9], 1), + ("LoggedIn", &[0..8], 1), + ("person", &[0..6], 2), + ("time", &[0..4], 2), + ("impl Drop for Person", &[5..9, 13..20], 0), + ("fn drop", &[3..7], 1), + ] + ); + + assert_eq!( + search(&outline, "oon", &cx).await, + &[ + ("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, 13, 14])] + ); + assert_eq!( + search(&outline, "dpn", &cx).await, + &[("impl Drop for Person", vec![5, 8, 19])] + ); + + 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_index].text.as_str(), + mat.positions, + ) + }) + .collect::>() + } +} + #[gpui::test] fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) { let buffer = cx.add_model(|cx| { @@ -1017,14 +1132,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/outline/Cargo.toml b/crates/outline/Cargo.toml index 80e612bf3b3d417ec06551c0b73ce7b99b24b006..51c35792284fc01a79402ad2ac811e0712db113a 100644 --- a/crates/outline/Cargo.toml +++ b/crates/outline/Cargo.toml @@ -15,3 +15,4 @@ 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 index ad5760818334ba20e316f5f5e02ff284aa242c2e..3eb8374a777e53995869b4eb07875b3a68719615 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -301,7 +301,7 @@ impl OutlineView { .0; navigate_to_selected_index = false; } else { - self.matches = self.outline.search(&query, cx); + self.matches = smol::block_on(self.outline.search(&query, cx.background().clone())); selected_index = self .matches .iter() @@ -309,7 +309,7 @@ impl OutlineView { .max_by_key(|(_, m)| OrderedFloat(m.score)) .map(|(ix, _)| ix) .unwrap_or(0); - navigate_to_selected_index = true; + navigate_to_selected_index = !self.matches.is_empty(); } self.select(selected_index, navigate_to_selected_index, cx); } diff --git a/crates/zed/languages/rust/outline.scm b/crates/zed/languages/rust/outline.scm index bf92b3fdfa9e49542544ad0dc55c6a1e2ab5a4a5..c954acf15264dddd0299a2498a6e402188bf7b19 100644 --- a/crates/zed/languages/rust/outline.scm +++ b/crates/zed/languages/rust/outline.scm @@ -14,7 +14,7 @@ (impl_item "impl" @context - trait: (_)? @context + trait: (_)? @name "for"? @context type: (_) @name) @item From 7913a1ea22a31b1a58a20cad6d58be539f23d213 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 13 Jan 2022 14:46:15 -0800 Subject: [PATCH 16/34] Include highlighting runs in Outline --- crates/editor/src/multi_buffer.rs | 5 +++-- crates/language/src/buffer.rs | 23 +++++++++++++++++++++-- crates/language/src/outline.rs | 3 ++- crates/language/src/tests.rs | 2 +- crates/outline/src/outline.rs | 13 +++++++++---- 5 files changed, 36 insertions(+), 10 deletions(-) diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 05294c141948cee1eaefb693bb117ccfd18fce43..70a3ad82f4a4c263990a604250902d8c13f7edf4 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -1698,9 +1698,9 @@ impl MultiBufferSnapshot { }) } - pub fn outline(&self) -> Option> { + pub fn outline(&self, theme: Option<&SyntaxTheme>) -> Option> { let buffer = self.as_singleton()?; - let outline = buffer.outline()?; + let outline = buffer.outline(theme)?; let excerpt_id = &self.excerpts.iter().next().unwrap().id; Some(Outline::new( outline @@ -1711,6 +1711,7 @@ impl MultiBufferSnapshot { range: self.anchor_in_excerpt(excerpt_id.clone(), item.range.start) ..self.anchor_in_excerpt(excerpt_id.clone(), item.range.end), text: item.text, + text_runs: item.text_runs, name_ranges: item.name_ranges, }) .collect(), diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index e699023d842dd452fa28071a54a442c659b61ea0..6a4f3bebd8f4b89b740a11574c0e7ea98ac84106 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1835,7 +1835,7 @@ impl BufferSnapshot { } } - pub fn outline(&self) -> Option> { + pub fn outline(&self, theme: Option<&SyntaxTheme>) -> Option> { let tree = self.tree.as_ref()?; let grammar = self .language @@ -1849,6 +1849,8 @@ impl BufferSnapshot { 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 @@ -1863,6 +1865,7 @@ impl BufferSnapshot { let range = item_node.start_byte()..item_node.end_byte(); let mut text = String::new(); let mut name_ranges = Vec::new(); + let mut text_runs = Vec::new(); for capture in mat.captures { let node_is_name; @@ -1890,7 +1893,22 @@ impl BufferSnapshot { name_ranges.push(start..end); } - text.extend(self.text_for_range(range)); + + 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(); + } + text_runs.push((chunk.text.len(), chunk.highlight_style)); + text.push_str(chunk.text); + if offset >= range.end { + break; + } + } } while stack.last().map_or(false, |prev_range| { @@ -1905,6 +1923,7 @@ impl BufferSnapshot { range: self.anchor_after(range.start)..self.anchor_before(range.end), text, name_ranges: name_ranges.into_boxed_slice(), + text_runs, }) }) .collect::>(); diff --git a/crates/language/src/outline.rs b/crates/language/src/outline.rs index 76b7462a972646a796f6032aa53c861b568db7c7..c0b12b12100461df87fdb7f8cf1b35929e124f51 100644 --- a/crates/language/src/outline.rs +++ b/crates/language/src/outline.rs @@ -1,5 +1,5 @@ use fuzzy::{StringMatch, StringMatchCandidate}; -use gpui::executor::Background; +use gpui::{executor::Background, fonts::HighlightStyle}; use std::{ops::Range, sync::Arc}; #[derive(Debug)] @@ -14,6 +14,7 @@ pub struct OutlineItem { pub range: Range, pub text: String, pub name_ranges: Box<[Range]>, + pub text_runs: Vec<(usize, Option)>, } impl Outline { diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index 1622ca92a93701acbbcca89dc14c5ef1b56c0a31..2c3b0e62c8e4b0dab3875aa56113310ad63da011 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -332,7 +332,7 @@ async fn test_outline(mut cx: gpui::TestAppContext) { 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()) + .read_with(&cx, |buffer, _| buffer.snapshot().outline(None)) .unwrap(); assert_eq!( diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index 3eb8374a777e53995869b4eb07875b3a68719615..36fe071b4553f008b254426272f4243d7565f5c3 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -167,11 +167,16 @@ impl OutlineView { .to_any() .downcast::() .unwrap(); - let buffer = editor.read(cx).buffer().read(cx).read(cx).outline(); + 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, workspace| { - let view = - cx.add_view(|cx| OutlineView::new(outline, editor, workspace.settings(), cx)); + 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 }) From adeb7e686403ecca464b1d5184b71732f83f9a43 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 13 Jan 2022 18:09:54 -0800 Subject: [PATCH 17/34] Incorporate syntax highlighting into symbol outline view Still need to figure out how to style the fuzzy match characters now that there's syntax highlighting. Right now, they are underlined in red. --- crates/editor/src/element.rs | 107 +++++------------ crates/editor/src/multi_buffer.rs | 2 +- crates/gpui/src/elements/text.rs | 189 ++++++++++++++++++++++++------ crates/gpui/src/fonts.rs | 2 +- crates/language/src/buffer.rs | 12 +- crates/language/src/outline.rs | 4 +- crates/outline/src/outline.rs | 177 +++++++++++++++++++++++++++- 7 files changed, 369 insertions(+), 124 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index a7b082dbdf0e483cf058339a79a2886a05e51c7b..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}, @@ -541,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( diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 70a3ad82f4a4c263990a604250902d8c13f7edf4..30020a0d55603154c28408ff3a2a6f9ac6dbc8a0 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -1711,7 +1711,7 @@ impl MultiBufferSnapshot { range: self.anchor_in_excerpt(excerpt_id.clone(), item.range.start) ..self.anchor_in_excerpt(excerpt_id.clone(), item.range.end), text: item.text, - text_runs: item.text_runs, + highlight_ranges: item.highlight_ranges, name_ranges: item.name_ranges, }) .collect(), 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/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/language/src/buffer.rs b/crates/language/src/buffer.rs index 6a4f3bebd8f4b89b740a11574c0e7ea98ac84106..2b24a055244804cdbb91a6f8922488dc92aaccc5 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1865,7 +1865,7 @@ impl BufferSnapshot { let range = item_node.start_byte()..item_node.end_byte(); let mut text = String::new(); let mut name_ranges = Vec::new(); - let mut text_runs = Vec::new(); + let mut highlight_ranges = Vec::new(); for capture in mat.captures { let node_is_name; @@ -1903,7 +1903,11 @@ impl BufferSnapshot { } else { offset += chunk.text.len(); } - text_runs.push((chunk.text.len(), chunk.highlight_style)); + 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; @@ -1922,8 +1926,8 @@ impl BufferSnapshot { depth: stack.len() - 1, range: self.anchor_after(range.start)..self.anchor_before(range.end), text, - name_ranges: name_ranges.into_boxed_slice(), - text_runs, + name_ranges, + highlight_ranges, }) }) .collect::>(); diff --git a/crates/language/src/outline.rs b/crates/language/src/outline.rs index c0b12b12100461df87fdb7f8cf1b35929e124f51..e6a5258f9c7bff9bcb7261fa3d52a3d6091f7e59 100644 --- a/crates/language/src/outline.rs +++ b/crates/language/src/outline.rs @@ -13,8 +13,8 @@ pub struct OutlineItem { pub depth: usize, pub range: Range, pub text: String, - pub name_ranges: Box<[Range]>, - pub text_runs: Vec<(usize, Option)>, + pub name_ranges: Vec>, + pub highlight_ranges: Vec<(Range, HighlightStyle)>, } impl Outline { diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index 36fe071b4553f008b254426272f4243d7565f5c3..96daebf1fad902bd9a7e1689ddbb4ca4c3b0ed1a 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -5,7 +5,9 @@ use editor::{ use fuzzy::StringMatch; use gpui::{ action, + color::Color, elements::*, + fonts::HighlightStyle, geometry::vector::Vector2F, keymap::{ self, @@ -20,6 +22,7 @@ use ordered_float::OrderedFloat; use postage::watch; use std::{ cmp::{self, Reverse}, + ops::Range, sync::Arc, }; use workspace::{Settings, Workspace}; @@ -364,14 +367,180 @@ impl OutlineView { } else { &settings.theme.selector.item }; - let outline_match = &self.outline.items[string_match.candidate_index]; + let outline_item = &self.outline.items[string_match.candidate_index]; - Label::new(outline_match.text.clone(), style.label.clone()) - .with_highlights(string_match.positions.clone()) + 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, + Color::red(), + )) .contained() - .with_padding_left(20. * outline_match.depth as f32) + .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], + match_underline: Color, +) -> Vec<(Range, HighlightStyle)> { + let mut result = Vec::new(); + let mut match_indices = match_indices.iter().copied().peekable(); + + for (range, syntax_highlight) in syntax_ranges + .iter() + .cloned() + .chain([(usize::MAX..0, 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); + result.push(( + match_index..end_index, + HighlightStyle { + underline: Some(match_underline), + ..default_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 { + end_index = char_ix_after(next_match_index, text); + match_indices.next(); + } else { + break; + } + } + + result.push(( + match_index..end_index, + HighlightStyle { + underline: Some(match_underline), + ..syntax_highlight + }, + )); + 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::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..10, + HighlightStyle { + color: Color::green(), + ..default + }, + ), + ]; + let match_indices = [4, 6, 7]; + let match_underline = Color::white(); + assert_eq!( + combine_syntax_and_fuzzy_match_highlights( + &string, + default, + &syntax_ranges, + &match_indices, + match_underline + ), + &[ + ( + 0..3, + HighlightStyle { + color: Color::red(), + ..default + }, + ), + ( + 4..5, + HighlightStyle { + color: Color::green(), + underline: Some(match_underline), + ..default + }, + ), + ( + 5..6, + HighlightStyle { + color: Color::green(), + ..default + }, + ), + ( + 6..8, + HighlightStyle { + color: Color::green(), + underline: Some(match_underline), + ..default + }, + ), + ( + 8..10, + HighlightStyle { + color: Color::green(), + ..default + }, + ), + ] + ); + } +} From 9c1f58ee89b5a171100aebdbab6a7736848aed85 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 14 Jan 2022 09:12:30 +0100 Subject: [PATCH 18/34] Maintain order of outline items when filling out tree's missing parts --- crates/language/src/outline.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/crates/language/src/outline.rs b/crates/language/src/outline.rs index e6a5258f9c7bff9bcb7261fa3d52a3d6091f7e59..dc1a8e3aefc5dd855dd2890117a263b7a11c2317 100644 --- a/crates/language/src/outline.rs +++ b/crates/language/src/outline.rs @@ -67,6 +67,7 @@ impl Outline { *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_index] .iter() @@ -79,12 +80,15 @@ impl Outline { let candidate_index = ix + prev_item_ix; if item.depth == cur_depth - 1 { - tree_matches.push(StringMatch { - candidate_index, - score: Default::default(), - positions: Default::default(), - string: Default::default(), - }); + tree_matches.insert( + insertion_ix, + StringMatch { + candidate_index, + score: Default::default(), + positions: Default::default(), + string: Default::default(), + }, + ); cur_depth -= 1; } } From deb679b8f58854f366d4666997361a2f1a88392d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 14 Jan 2022 09:16:39 +0100 Subject: [PATCH 19/34] Report all matching strings in fuzzy matcher even if they're duplicates --- crates/fuzzy/src/fuzzy.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/fuzzy/src/fuzzy.rs b/crates/fuzzy/src/fuzzy.rs index c3624b403770fd09b2ea53e7e243e91c077e5b8f..62624b033d3dcf3414ee0c5d6a3c671e76d01d0b 100644 --- a/crates/fuzzy/src/fuzzy.rs +++ b/crates/fuzzy/src/fuzzy.rs @@ -117,7 +117,7 @@ pub struct StringMatch { impl PartialEq for StringMatch { fn eq(&self, other: &Self) -> bool { - self.score.eq(&other.score) + self.cmp(other).is_eq() } } @@ -134,13 +134,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_index.cmp(&other.candidate_index)) } } impl PartialEq for PathMatch { fn eq(&self, other: &Self) -> bool { - self.score.eq(&other.score) + self.cmp(other).is_eq() } } From ecba761e18dbe22d717bdfda97476900ae5ae9e8 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 14 Jan 2022 09:22:20 +0100 Subject: [PATCH 20/34] Make `mod` a @context --- crates/zed/languages/rust/outline.scm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/zed/languages/rust/outline.scm b/crates/zed/languages/rust/outline.scm index c954acf15264dddd0299a2498a6e402188bf7b19..5c89087ac0db7b037dbb38688260dd7c16a6d9ee 100644 --- a/crates/zed/languages/rust/outline.scm +++ b/crates/zed/languages/rust/outline.scm @@ -41,7 +41,7 @@ (mod_item (visibility_modifier)? @context - "mod" + "mod" @context name: (_) @name) @item (type_item From b0033bb6d49809c6c6190f976df74303231a1500 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 14 Jan 2022 10:08:08 +0100 Subject: [PATCH 21/34] Don't emit duplicate text when mixing syntax highlighting and match indices --- crates/outline/src/outline.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index 96daebf1fad902bd9a7e1689ddbb4ca4c3b0ed1a..047044a36ad908216f50d95219c0b1d77dd7a362 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -437,7 +437,7 @@ fn combine_syntax_and_fuzzy_match_highlights( 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 { + if next_match_index == end_index && next_match_index < range.end { end_index = char_ix_after(next_match_index, text); match_indices.next(); } else { @@ -485,14 +485,14 @@ mod tests { }, ), ( - 4..10, + 4..8, HighlightStyle { color: Color::green(), ..default }, ), ]; - let match_indices = [4, 6, 7]; + let match_indices = [4, 6, 7, 8]; let match_underline = Color::white(); assert_eq!( combine_syntax_and_fuzzy_match_highlights( @@ -534,9 +534,9 @@ mod tests { }, ), ( - 8..10, + 8..9, HighlightStyle { - color: Color::green(), + underline: Some(match_underline), ..default }, ), From e7f1398f3a622f48e699164fc1a22aa527d69758 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 14 Jan 2022 10:20:04 +0100 Subject: [PATCH 22/34] :lipstick: --- crates/lsp/src/lsp.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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(); From a64ba8b687e74e9924e6fcc9bf0d9b6c7170e90a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 14 Jan 2022 11:09:02 +0100 Subject: [PATCH 23/34] Allow matching of context items in outline view --- crates/editor/src/multi_buffer.rs | 1 - crates/language/src/buffer.rs | 17 --------- crates/language/src/outline.rs | 28 ++------------ crates/language/src/tests.rs | 62 ++++++++++++++++++++----------- 4 files changed, 44 insertions(+), 64 deletions(-) diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 30020a0d55603154c28408ff3a2a6f9ac6dbc8a0..daecc56650455d17526e4361e3bd63d280fff478 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -1712,7 +1712,6 @@ impl MultiBufferSnapshot { ..self.anchor_in_excerpt(excerpt_id.clone(), item.range.end), text: item.text, highlight_ranges: item.highlight_ranges, - name_ranges: item.name_ranges, }) .collect(), )) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 2b24a055244804cdbb91a6f8922488dc92aaccc5..13e80bdd936f3d7833e445436e4db4de56f444ab 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1864,15 +1864,11 @@ impl BufferSnapshot { 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; } @@ -1881,18 +1877,6 @@ impl BufferSnapshot { if !text.is_empty() { text.push(' '); } - if node_is_name { - let mut start = text.len() as u32; - let end = start + range.len() as u32; - - // 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); @@ -1926,7 +1910,6 @@ impl BufferSnapshot { depth: stack.len() - 1, range: self.anchor_after(range.start)..self.anchor_before(range.end), text, - name_ranges, highlight_ranges, }) }) diff --git a/crates/language/src/outline.rs b/crates/language/src/outline.rs index dc1a8e3aefc5dd855dd2890117a263b7a11c2317..c70d2c40837807b2c6719f8201c03e83917927f8 100644 --- a/crates/language/src/outline.rs +++ b/crates/language/src/outline.rs @@ -13,7 +13,6 @@ pub struct OutlineItem { pub depth: usize, pub range: Range, pub text: String, - pub name_ranges: Vec>, pub highlight_ranges: Vec<(Range, HighlightStyle)>, } @@ -22,16 +21,9 @@ impl Outline { Self { candidates: items .iter() - .map(|item| { - let text = item - .name_ranges - .iter() - .map(|range| &item.text[range.start as usize..range.end as usize]) - .collect::(); - StringMatchCandidate { - char_bag: text.as_str().into(), - string: text, - } + .map(|item| StringMatchCandidate { + char_bag: item.text.as_str().into(), + string: item.text.clone(), }) .collect(), items, @@ -53,20 +45,8 @@ impl Outline { let mut tree_matches = Vec::new(); let mut prev_item_ix = 0; - for mut string_match in matches { + for string_match in matches { let outline_match = &self.items[string_match.candidate_index]; - - 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_index] diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index 2c3b0e62c8e4b0dab3875aa56113310ad63da011..cd5dd0315b1d429c3184a2a5cf9d467a8679b533 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -302,6 +302,9 @@ async fn test_outline(mut cx: gpui::TestAppContext) { (function_item "fn" @context name: (_) @name) @item + (mod_item + "mod" @context + name: (_) @name) @item "#, ) .unwrap(), @@ -313,15 +316,19 @@ async fn test_outline(mut cx: gpui::TestAppContext) { age: usize, } - enum LoginState { - LoggedOut, - LoggingOn, - LoggedIn { - person: Person, - time: Instant, + 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"); @@ -339,39 +346,50 @@ async fn test_outline(mut cx: gpui::TestAppContext) { outline .items .iter() - .map(|item| (item.text.as_str(), item.name_ranges.as_ref(), item.depth)) + .map(|item| (item.text.as_str(), item.depth)) .collect::>(), &[ - ("struct Person", [7..13].as_slice(), 0), - ("name", &[0..4], 1), - ("age", &[0..3], 1), - ("enum LoginState", &[5..15], 0), - ("LoggedOut", &[0..9], 1), - ("LoggingOn", &[0..9], 1), - ("LoggedIn", &[0..8], 1), - ("person", &[0..6], 2), - ("time", &[0..4], 2), - ("impl Drop for Person", &[5..9, 13..20], 0), - ("fn drop", &[3..7], 1), + ("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), ] ); assert_eq!( search(&outline, "oon", &cx).await, &[ - ("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 + ("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 Eq for Person", vec![9, 16, 17]), // matches part of the context + ("impl Drop for Person", vec![11, 18, 19]), // matches in two disjoint names ] ); assert_eq!( search(&outline, "dp p", &cx).await, - &[("impl Drop for Person", vec![5, 8, 13, 14])] + &[("impl Drop for Person", vec![5, 8, 9, 14])] ); assert_eq!( search(&outline, "dpn", &cx).await, &[("impl Drop for Person", vec![5, 8, 19])] ); + assert_eq!( + search(&outline, "impl", &cx).await, + &[ + ("impl Eq for Person", vec![0, 1, 2, 3]), + ("impl Drop for Person", vec![0, 1, 2, 3]) + ] + ); async fn search<'a>( outline: &'a Outline, From e538beb920ab7d8379a2e6fac471d6a1293a6d57 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 14 Jan 2022 14:38:19 +0100 Subject: [PATCH 24/34] Highlight matches by increasing the font weight Co-Authored-By: Nathan Sobo --- crates/outline/src/outline.rs | 39 +++++++++++++---------------------- 1 file changed, 14 insertions(+), 25 deletions(-) diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index 047044a36ad908216f50d95219c0b1d77dd7a362..de37a63da5bcedcdb239a0f3dd5fbd543a0f43d8 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -5,9 +5,8 @@ use editor::{ use fuzzy::StringMatch; use gpui::{ action, - color::Color, elements::*, - fonts::HighlightStyle, + fonts::{self, HighlightStyle}, geometry::vector::Vector2F, keymap::{ self, @@ -376,7 +375,6 @@ impl OutlineView { style.label.text.clone().into(), &outline_item.highlight_ranges, &string_match.positions, - Color::red(), )) .contained() .with_padding_left(20. * outline_item.depth as f32) @@ -391,16 +389,17 @@ fn combine_syntax_and_fuzzy_match_highlights( default_style: HighlightStyle, syntax_ranges: &[(Range, HighlightStyle)], match_indices: &[usize], - match_underline: Color, ) -> Vec<(Range, HighlightStyle)> { let mut result = Vec::new(); let mut match_indices = match_indices.iter().copied().peekable(); - for (range, syntax_highlight) in syntax_ranges + 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() { @@ -409,13 +408,9 @@ fn combine_syntax_and_fuzzy_match_highlights( } match_indices.next(); let end_index = char_ix_after(match_index, text); - result.push(( - match_index..end_index, - HighlightStyle { - underline: Some(match_underline), - ..default_style - }, - )); + 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 { @@ -445,13 +440,9 @@ fn combine_syntax_and_fuzzy_match_highlights( } } - result.push(( - match_index..end_index, - HighlightStyle { - underline: Some(match_underline), - ..syntax_highlight - }, - )); + 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; } @@ -470,7 +461,7 @@ fn char_ix_after(ix: usize, text: &str) -> usize { #[cfg(test)] mod tests { use super::*; - use gpui::fonts::HighlightStyle; + use gpui::{color::Color, fonts::HighlightStyle}; #[test] fn test_combine_syntax_and_fuzzy_match_highlights() { @@ -493,14 +484,12 @@ mod tests { ), ]; let match_indices = [4, 6, 7, 8]; - let match_underline = Color::white(); assert_eq!( combine_syntax_and_fuzzy_match_highlights( &string, default, &syntax_ranges, &match_indices, - match_underline ), &[ ( @@ -514,7 +503,7 @@ mod tests { 4..5, HighlightStyle { color: Color::green(), - underline: Some(match_underline), + font_properties: *fonts::Properties::default().weight(fonts::Weight::BOLD), ..default }, ), @@ -529,14 +518,14 @@ mod tests { 6..8, HighlightStyle { color: Color::green(), - underline: Some(match_underline), + font_properties: *fonts::Properties::default().weight(fonts::Weight::BOLD), ..default }, ), ( 8..9, HighlightStyle { - underline: Some(match_underline), + font_properties: *fonts::Properties::default().weight(fonts::Weight::BOLD), ..default }, ), From be24e589266e3373d852579ea6cbf8144b8f5708 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 14 Jan 2022 14:55:03 +0100 Subject: [PATCH 25/34] Associate `StringMatchCandidate` with an id Co-Authored-By: Nathan Sobo --- crates/fuzzy/src/fuzzy.rs | 22 +++++++++------------ crates/language/src/outline.rs | 16 ++++++++------- crates/language/src/tests.rs | 7 +------ crates/outline/src/outline.rs | 6 +++--- crates/theme_selector/src/theme_selector.rs | 6 ++++-- 5 files changed, 26 insertions(+), 31 deletions(-) diff --git a/crates/fuzzy/src/fuzzy.rs b/crates/fuzzy/src/fuzzy.rs index 62624b033d3dcf3414ee0c5d6a3c671e76d01d0b..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,7 +110,7 @@ impl<'a> MatchCandidate for &'a StringMatchCandidate { #[derive(Clone, Debug)] pub struct StringMatch { - pub candidate_index: usize, + pub candidate_id: usize, pub score: f64, pub positions: Vec, pub string: String, @@ -134,7 +135,7 @@ impl Ord for StringMatch { self.score .partial_cmp(&other.score) .unwrap_or(Ordering::Equal) - .then_with(|| self.candidate_index.cmp(&other.candidate_index)) + .then_with(|| self.candidate_id.cmp(&other.candidate_id)) } } @@ -198,7 +199,6 @@ pub async fn match_strings( max_results, ); matcher.match_strings( - segment_start, &candidates[segment_start..segment_end], results, cancel_flag, @@ -321,7 +321,6 @@ impl<'a> Matcher<'a> { pub fn match_strings( &mut self, - start_index: usize, candidates: &[StringMatchCandidate], results: &mut Vec, cancel_flag: &AtomicBool, @@ -329,12 +328,11 @@ impl<'a> Matcher<'a> { self.match_internal( &[], &[], - start_index, candidates.iter(), results, cancel_flag, - |candidate_index, candidate, score| StringMatch { - candidate_index, + |candidate, score| StringMatch { + candidate_id: candidate.id, score, positions: Vec::new(), string: candidate.string.to_string(), @@ -358,11 +356,10 @@ impl<'a> Matcher<'a> { self.match_internal( &prefix, &lowercase_prefix, - 0, path_entries, results, cancel_flag, - |_, candidate, score| PathMatch { + |candidate, score| PathMatch { score, worktree_id: tree_id, positions: Vec::new(), @@ -376,19 +373,18 @@ impl<'a> Matcher<'a> { &mut self, prefix: &[char], lowercase_prefix: &[char], - start_index: usize, candidates: impl Iterator, results: &mut Vec, cancel_flag: &AtomicBool, build_match: F, ) where R: Match, - F: Fn(usize, &C, f64) -> R, + F: Fn(&C, f64) -> R, { let mut candidate_chars = Vec::new(); let mut lowercase_candidate_chars = Vec::new(); - for (candidate_ix, candidate) in candidates.enumerate() { + for candidate in candidates { if !candidate.has_chars(self.query_char_bag) { continue; } @@ -422,7 +418,7 @@ impl<'a> Matcher<'a> { ); if score > 0.0 { - let mut mat = build_match(start_index + candidate_ix, &candidate, score); + let mut mat = build_match(&candidate, score); if let Err(i) = results.binary_search_by(|m| mat.cmp(&m)) { if results.len() < self.max_results { mat.set_positions(self.match_positions.clone()); diff --git a/crates/language/src/outline.rs b/crates/language/src/outline.rs index c70d2c40837807b2c6719f8201c03e83917927f8..ea0c5a1f6b9b5a6e372c9d28cea9c153407401a0 100644 --- a/crates/language/src/outline.rs +++ b/crates/language/src/outline.rs @@ -21,7 +21,9 @@ impl Outline { Self { candidates: items .iter() - .map(|item| StringMatchCandidate { + .enumerate() + .map(|(id, item)| StringMatchCandidate { + id, char_bag: item.text.as_str().into(), string: item.text.clone(), }) @@ -37,19 +39,19 @@ impl Outline { true, 100, &Default::default(), - executor, + executor.clone(), ) .await; - matches.sort_unstable_by_key(|m| m.candidate_index); + matches.sort_unstable_by_key(|m| m.candidate_id); let mut tree_matches = Vec::new(); let mut prev_item_ix = 0; for string_match in matches { - let outline_match = &self.items[string_match.candidate_index]; + let outline_match = &self.items[string_match.candidate_id]; 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_index] + for (ix, item) in self.items[prev_item_ix..string_match.candidate_id] .iter() .enumerate() .rev() @@ -63,7 +65,7 @@ impl Outline { tree_matches.insert( insertion_ix, StringMatch { - candidate_index, + candidate_id: candidate_index, score: Default::default(), positions: Default::default(), string: Default::default(), @@ -73,7 +75,7 @@ impl Outline { } } - prev_item_ix = string_match.candidate_index + 1; + prev_item_ix = string_match.candidate_id + 1; tree_matches.push(string_match); } diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index cd5dd0315b1d429c3184a2a5cf9d467a8679b533..849c60ca5ffc451c8d2092f13113460efc70dc06 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -401,12 +401,7 @@ async fn test_outline(mut cx: gpui::TestAppContext) { .await; matches .into_iter() - .map(|mat| { - ( - outline.items[mat.candidate_index].text.as_str(), - mat.positions, - ) - }) + .map(|mat| (outline.items[mat.candidate_id].text.as_str(), mat.positions)) .collect::>() } } diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index de37a63da5bcedcdb239a0f3dd5fbd543a0f43d8..88c380801172a9b4147d10e632c8b5e3d5355152 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -202,7 +202,7 @@ impl OutlineView { self.list_state.scroll_to(self.selected_match_index); if navigate { let selected_match = &self.matches[self.selected_match_index]; - let outline_item = &self.outline.items[selected_match.candidate_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; @@ -275,7 +275,7 @@ impl OutlineView { .iter() .enumerate() .map(|(index, _)| StringMatch { - candidate_index: index, + candidate_id: index, score: Default::default(), positions: Default::default(), string: Default::default(), @@ -366,7 +366,7 @@ impl OutlineView { } else { &settings.theme.selector.item }; - let outline_item = &self.outline.items[string_match.candidate_index]; + let outline_item = &self.outline.items[string_match.candidate_id]; Text::new(outline_item.text.clone(), style.label.text.clone()) .with_soft_wrap(false) diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs index 7d65087b0b755eeec8ebca258abe7b43c5827bd9..d1cbafe7dcb262b39ab023949b8d61fd4c53e296 100644 --- a/crates/theme_selector/src/theme_selector.rs +++ b/crates/theme_selector/src/theme_selector.rs @@ -157,7 +157,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, }) @@ -169,7 +171,7 @@ impl ThemeSelector { .into_iter() .enumerate() .map(|(index, candidate)| StringMatch { - candidate_index: index, + candidate_id: index, string: candidate.string, positions: Vec::new(), score: 0.0, From f934370e7f16e478637b2e7275493992a9627b8f Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 14 Jan 2022 09:02:04 -0700 Subject: [PATCH 26/34] Match full path when query contains spaces Co-Authored-By: Antonio Scandurra --- crates/language/src/outline.rs | 49 ++++++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/crates/language/src/outline.rs b/crates/language/src/outline.rs index ea0c5a1f6b9b5a6e372c9d28cea9c153407401a0..1b4fad56fd056dcca3e907b6efab72ba3e4637bd 100644 --- a/crates/language/src/outline.rs +++ b/crates/language/src/outline.rs @@ -6,6 +6,8 @@ use std::{ops::Range, sync::Arc}; pub struct Outline { pub items: Vec>, candidates: Vec, + path_candidates: Vec, + path_candidate_prefixes: Vec, } #[derive(Clone, Debug)] @@ -18,6 +20,30 @@ pub struct OutlineItem { impl Outline { pub fn new(items: Vec>) -> Self { + let mut path_candidates = Vec::new(); + let mut path_candidate_prefixes = Vec::new(); + let mut item_text = String::new(); + let mut stack = Vec::new(); + + for (id, item) in items.iter().enumerate() { + if item.depth < stack.len() { + stack.truncate(item.depth); + item_text.truncate(stack.last().copied().unwrap_or(0)); + } + if !item_text.is_empty() { + item_text.push(' '); + } + path_candidate_prefixes.push(item_text.len()); + item_text.push_str(&item.text); + stack.push(item_text.len()); + + path_candidates.push(StringMatchCandidate { + id, + string: item_text.clone(), + char_bag: item_text.as_str().into(), + }); + } + Self { candidates: items .iter() @@ -28,13 +54,21 @@ impl Outline { string: item.text.clone(), }) .collect(), + 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 mut matches = fuzzy::match_strings( - &self.candidates, + if is_path_query { + &self.path_candidates + } else { + &self.candidates + }, query, true, 100, @@ -47,8 +81,19 @@ impl Outline { let mut tree_matches = Vec::new(); let mut prev_item_ix = 0; - for string_match in matches { + 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; + } + } + 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] From b52db225440125d8b17b9b180767bdacf01a202d Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 14 Jan 2022 09:16:09 -0700 Subject: [PATCH 27/34] Only enable smart case if the query contains an uppercase character Co-Authored-By: Antonio Scandurra --- crates/language/src/outline.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/language/src/outline.rs b/crates/language/src/outline.rs index 1b4fad56fd056dcca3e907b6efab72ba3e4637bd..0da6e725476c0abc746da71033814e97dabb335c 100644 --- a/crates/language/src/outline.rs +++ b/crates/language/src/outline.rs @@ -63,6 +63,7 @@ impl Outline { 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 @@ -70,7 +71,7 @@ impl Outline { &self.candidates }, query, - true, + smart_case, 100, &Default::default(), executor.clone(), From e4c0fc6ad5419faf38335c0d3a1b2b26f97f6e59 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 14 Jan 2022 17:25:24 +0100 Subject: [PATCH 28/34] Dismiss outline view when the query editor is blurred Co-Authored-By: Nathan Sobo --- crates/go_to_line/src/go_to_line.rs | 2 -- crates/outline/src/outline.rs | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/go_to_line/src/go_to_line.rs b/crates/go_to_line/src/go_to_line.rs index 301f37a9fbc5713114ce45121a917c0f17f8121c..53669ea2c62151b5be363051068d0630d766a76c 100644 --- a/crates/go_to_line/src/go_to_line.rs +++ b/crates/go_to_line/src/go_to_line.rs @@ -224,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/outline/src/outline.rs b/crates/outline/src/outline.rs index 88c380801172a9b4147d10e632c8b5e3d5355152..1ddd3321e3aca3f783a0f1b386726eba93eff538 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -258,6 +258,7 @@ impl OutlineView { cx: &mut ViewContext, ) { match event { + editor::Event::Blurred => cx.emit(Event::Dismissed), editor::Event::Edited => self.update_matches(cx), _ => {} } From ce51196eaba0279f83604716a1d6e6927c11447b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 14 Jan 2022 17:53:06 +0100 Subject: [PATCH 29/34] Center the selected item when updating outline query Co-Authored-By: Max Brunsfeld Co-Authored-By: Nathan Sobo --- crates/file_finder/src/file_finder.rs | 9 +++-- crates/gpui/src/elements/uniform_list.rs | 41 ++++++++++++++++----- crates/outline/src/outline.rs | 14 ++++--- crates/project_panel/src/project_panel.rs | 6 +-- crates/theme_selector/src/theme_selector.rs | 6 ++- 5 files changed, 54 insertions(+), 22 deletions(-) diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 707a3bfb20602820f3be4f86681bb1605b04e0b8..48fb562e7da33f47a247c355f7775b11e2942777 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -353,7 +353,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 +365,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 +417,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/gpui/src/elements/uniform_list.rs b/crates/gpui/src/elements/uniform_list.rs index f499801e6384380eed8875068c498a3ffa930ab3..889494c1bbc5111dd16214bd79798fa412709d77 100644 --- a/crates/gpui/src/elements/uniform_list.rs +++ b/crates/gpui/src/elements/uniform_list.rs @@ -14,9 +14,14 @@ use std::{cmp, ops::Range, sync::Arc}; #[derive(Clone, Default)] pub struct UniformListState(Arc>); +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 +32,7 @@ impl UniformListState { #[derive(Default)] struct StateInner { scroll_top: f32, - scroll_to: Option, + scroll_to: Option, } pub struct LayoutState { @@ -97,14 +102,32 @@ where state.scroll_top = scroll_max; } - if let Some(item_ix) = state.scroll_to.take() { + 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; + } + } + 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; + } } } } diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index 1ddd3321e3aca3f783a0f1b386726eba93eff538..7e480aca8742aa9c5c5e90a8dc4d5b266b14eea5 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -187,19 +187,23 @@ impl OutlineView { fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext) { if self.selected_match_index > 0 { - self.select(self.selected_match_index - 1, true, cx); + 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, cx); + self.select(self.selected_match_index + 1, true, false, cx); } } - fn select(&mut self, index: usize, navigate: bool, cx: &mut ViewContext) { + fn select(&mut self, index: usize, navigate: bool, center: bool, cx: &mut ViewContext) { self.selected_match_index = index; - self.list_state.scroll_to(self.selected_match_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]; @@ -319,7 +323,7 @@ impl OutlineView { .unwrap_or(0); navigate_to_selected_index = !self.matches.is_empty(); } - self.select(selected_index, navigate_to_selected_index, cx); + self.select(selected_index, navigate_to_selected_index, true, cx); } fn render_matches(&self) -> ElementBox { diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 8c73f1be861a902d129fc9a12900399ba8e4a0ed..b28c526564dcb049d6d27345594319c3ab9d8255 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1,8 +1,8 @@ use gpui::{ action, elements::{ - Align, ConstrainedBox, Empty, Flex, Label, MouseEventHandler, ParentElement, Svg, - UniformList, UniformListState, + Align, ConstrainedBox, Empty, Flex, Label, MouseEventHandler, ParentElement, ScrollTarget, + Svg, UniformList, UniformListState, }, keymap::{ self, @@ -278,7 +278,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 d1cbafe7dcb262b39ab023949b8d61fd4c53e296..af7e8527f0603556335dc5cbe9d250a6a318e027 100644 --- a/crates/theme_selector/src/theme_selector.rs +++ b/crates/theme_selector/src/theme_selector.rs @@ -140,7 +140,8 @@ impl ThemeSelector { 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(); } @@ -148,7 +149,8 @@ impl ThemeSelector { 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(); } From ea69dcd42a4ccc1632b4ea12a726c2f59cf912cf Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 14 Jan 2022 09:16:29 -0800 Subject: [PATCH 30/34] Match on names only when outline query has no spaces Co-Authored-By: Antonio Scandurra Co-Authored-By: Nathan Sobo --- crates/editor/src/multi_buffer.rs | 1 + crates/language/src/buffer.rs | 17 +++++++++ crates/language/src/outline.rs | 58 ++++++++++++++++++++----------- crates/language/src/tests.rs | 25 +++++++------ 4 files changed, 70 insertions(+), 31 deletions(-) diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index daecc56650455d17526e4361e3bd63d280fff478..30020a0d55603154c28408ff3a2a6f9ac6dbc8a0 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -1712,6 +1712,7 @@ impl MultiBufferSnapshot { ..self.anchor_in_excerpt(excerpt_id.clone(), item.range.end), text: item.text, highlight_ranges: item.highlight_ranges, + name_ranges: item.name_ranges, }) .collect(), )) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 13e80bdd936f3d7833e445436e4db4de56f444ab..4cf0c8f0813eb26cd7309892274f41609fa9b9a8 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1864,11 +1864,15 @@ impl BufferSnapshot { 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; } @@ -1877,6 +1881,18 @@ impl BufferSnapshot { 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); @@ -1911,6 +1927,7 @@ impl BufferSnapshot { range: self.anchor_after(range.start)..self.anchor_before(range.end), text, highlight_ranges, + name_ranges, }) }) .collect::>(); diff --git a/crates/language/src/outline.rs b/crates/language/src/outline.rs index 0da6e725476c0abc746da71033814e97dabb335c..07f0196c9f59725855f624b486a079947bf7e1d0 100644 --- a/crates/language/src/outline.rs +++ b/crates/language/src/outline.rs @@ -16,44 +16,49 @@ pub struct OutlineItem { 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 item_text = String::new(); - let mut stack = Vec::new(); + let mut path_text = String::new(); + let mut path_stack = Vec::new(); for (id, item) in items.iter().enumerate() { - if item.depth < stack.len() { - stack.truncate(item.depth); - item_text.truncate(stack.last().copied().unwrap_or(0)); + if item.depth < path_stack.len() { + path_stack.truncate(item.depth); + path_text.truncate(path_stack.last().copied().unwrap_or(0)); } - if !item_text.is_empty() { - item_text.push(' '); + if !path_text.is_empty() { + path_text.push(' '); } - path_candidate_prefixes.push(item_text.len()); - item_text.push_str(&item.text); - stack.push(item_text.len()); + 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, - string: item_text.clone(), - char_bag: item_text.as_str().into(), + 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: items - .iter() - .enumerate() - .map(|(id, item)| StringMatchCandidate { - id, - char_bag: item.text.as_str().into(), - string: item.text.clone(), - }) - .collect(), + candidates, path_candidates, path_candidate_prefixes, items, @@ -93,6 +98,17 @@ impl Outline { 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(); diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index 849c60ca5ffc451c8d2092f13113460efc70dc06..e2ee035c86ac4d36a4cbeeebebe265ab6023a2a6 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -365,29 +365,34 @@ async fn test_outline(mut cx: gpui::TestAppContext) { ] ); + // 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 Eq for Person", vec![9, 16, 17]), // matches part of the context - ("impl Drop for Person", vec![11, 18, 19]), // matches in two disjoint names + ("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])] + &[ + ("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, 8, 19])] + &[("impl Drop for Person", vec![5, 14, 19])] ); assert_eq!( - search(&outline, "impl", &cx).await, + search(&outline, "impl ", &cx).await, &[ - ("impl Eq for Person", vec![0, 1, 2, 3]), - ("impl Drop for Person", vec![0, 1, 2, 3]) + ("impl Eq for Person", vec![0, 1, 2, 3, 4]), + ("impl Drop for Person", vec![0, 1, 2, 3, 4]), + ("fn drop", vec![]), ] ); From b7561c6cef73922c5d6ca8c10834a4b9a0dc6bdc Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 14 Jan 2022 10:45:37 -0800 Subject: [PATCH 31/34] Add select_first and select_last bindings to outline view Co-Authored-By: Antonio Scandurra Co-Authored-By: Nathan Sobo --- crates/editor/src/editor.rs | 10 +++++++++ crates/file_finder/src/file_finder.rs | 13 +++++------- crates/gpui/src/keymap.rs | 19 +---------------- crates/outline/src/outline.rs | 23 ++++++++++++++------- crates/project_panel/src/project_panel.rs | 11 +++++----- crates/theme_selector/src/theme_selector.rs | 13 ++++++------ crates/workspace/src/menu.rs | 19 +++++++++++++++++ crates/workspace/src/workspace.rs | 5 ++++- 8 files changed, 66 insertions(+), 47 deletions(-) create mode 100644 crates/workspace/src/menu.rs diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 574ff3822b168c80395cdc21caf410aaa125ab17..898f1fce4256fb4589fb7b37ad20f3c17c27d625 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2390,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, @@ -2407,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), diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 48fb562e7da33f47a247c355f7775b11e2942777..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")), ]); } 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/outline/src/outline.rs b/crates/outline/src/outline.rs index 7e480aca8742aa9c5c5e90a8dc4d5b266b14eea5..dbcfbef9734a379113b04e32abcffcfe72cd478a 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -8,11 +8,7 @@ use gpui::{ elements::*, fonts::{self, HighlightStyle}, geometry::vector::Vector2F, - keymap::{ - self, - menu::{SelectNext, SelectPrev}, - Binding, - }, + keymap::{self, Binding}, AppContext, Axis, Entity, MutableAppContext, RenderContext, View, ViewContext, ViewHandle, WeakViewHandle, }; @@ -24,21 +20,24 @@ use std::{ ops::Range, sync::Arc, }; -use workspace::{Settings, Workspace}; +use workspace::{ + menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev}, + Settings, Workspace, +}; action!(Toggle); -action!(Confirm); pub fn init(cx: &mut MutableAppContext) { cx.add_bindings([ Binding::new("cmd-shift-O", Toggle, Some("Editor")), Binding::new("escape", Toggle, Some("OutlineView")), - Binding::new("enter", Confirm, 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 { @@ -197,6 +196,14 @@ impl OutlineView { } } + 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 { diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index b28c526564dcb049d6d27345594319c3ab9d8255..382a94284991d6d49775ffc988b73d9ae081d705 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -4,11 +4,7 @@ use gpui::{ Align, ConstrainedBox, Empty, Flex, Label, MouseEventHandler, ParentElement, ScrollTarget, Svg, UniformList, UniformListState, }, - keymap::{ - self, - menu::{SelectNext, SelectPrev}, - Binding, - }, + 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, diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs index af7e8527f0603556335dc5cbe9d250a6a318e027..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,7 +137,7 @@ 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; } @@ -145,7 +146,7 @@ impl ThemeSelector { 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; } 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 { From 5de5e4b6f2e8029a466aa89accab16737cd5917e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 14 Jan 2022 10:51:26 -0800 Subject: [PATCH 32/34] Avoid panic in OutlineView when active item isn't an editor --- crates/outline/src/outline.rs | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index dbcfbef9734a379113b04e32abcffcfe72cd478a..c4285a4552668b2b1b239c3cee2df8d68616a7e5 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -162,25 +162,24 @@ impl OutlineView { } fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext) { - let editor = workspace + if let Some(editor) = workspace .active_item(cx) - .unwrap() - .to_any() - .downcast::() - .unwrap(); - 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 - }) + .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 + }) + } } } From dd8e5ee54319c1ca9a328e455641e90656310f09 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 14 Jan 2022 11:01:20 -0800 Subject: [PATCH 33/34] Add bottom margin to the outline view --- crates/gpui/src/elements/container.rs | 5 +++++ crates/outline/src/outline.rs | 30 +++++++++++---------------- crates/zed/assets/themes/_base.toml | 2 +- 3 files changed, 18 insertions(+), 19 deletions(-) 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/outline/src/outline.rs b/crates/outline/src/outline.rs index c4285a4552668b2b1b239c3cee2df8d68616a7e5..774b765977fa0bb2e06274da8f865ea7a59bfff3 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -84,27 +84,21 @@ impl View for OutlineView { fn render(&mut self, _: &mut RenderContext) -> ElementBox { let settings = self.settings.borrow(); - Align::new( - ConstrainedBox::new( - Container::new( - 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()) - .boxed(), - ) - .with_style(settings.theme.selector.container) - .boxed(), + 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) - .boxed(), - ) - .top() - .named("outline view") + .aligned() + .top() + .named("outline view") } fn on_focus(&mut self, cx: &mut ViewContext) { 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" } From f3239fe1d5429e8e83ce22301b24b999e905d4fd Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 14 Jan 2022 11:56:28 -0800 Subject: [PATCH 34/34] Apply scroll_max after uniform list autoscrolls --- crates/gpui/src/elements/uniform_list.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/gpui/src/elements/uniform_list.rs b/crates/gpui/src/elements/uniform_list.rs index 889494c1bbc5111dd16214bd79798fa412709d77..945340e4c0539bddb4e8a47b2cf9b072b008677b 100644 --- a/crates/gpui/src/elements/uniform_list.rs +++ b/crates/gpui/src/elements/uniform_list.rs @@ -14,6 +14,7 @@ use std::{cmp, ops::Range, sync::Arc}; #[derive(Clone, Default)] pub struct UniformListState(Arc>); +#[derive(Debug)] pub enum ScrollTarget { Show(usize), Center(usize), @@ -98,10 +99,6 @@ 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; @@ -130,6 +127,10 @@ where } } } + + if state.scroll_top > scroll_max { + state.scroll_top = scroll_max; + } } fn scroll_top(&self) -> f32 {