Add Buffer::outline method

Max Brunsfeld and Nathan Sobo created

Co-Authored-By: Nathan Sobo <nathan@zed.dev>

Change summary

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(-)

Detailed changes

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",

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<Outline> {
+        self.as_singleton().and_then(move |buffer| buffer.outline())
+    }
+
     fn buffer_snapshot_for_excerpt<'a>(
         &'a self,
         excerpt_id: &'a ExcerptId,

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<QueryCursor>);
+pub(crate) struct QueryCursorHandle(Option<QueryCursor>);
 
 #[derive(Clone)]
 struct SyntaxTree {
@@ -1264,6 +1265,13 @@ impl Buffer {
         self.edit_internal(ranges_iter, new_text, true, cx)
     }
 
+    /*
+    impl Buffer
+        pub fn edit
+        pub fn edit_internal
+        pub fn edit_with_autoindent
+    */
+
     pub fn edit_internal<I, S, T>(
         &mut self,
         ranges_iter: I,
@@ -1827,6 +1835,82 @@ impl BufferSnapshot {
         }
     }
 
+    pub fn outline(&self) -> Option<Outline> {
+        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::<Vec<_>>();
+
+        Some(Outline(items))
+    }
+
     pub fn enclosing_bracket_ranges<T: ToOffset>(
         &self,
         range: Range<T>,
@@ -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<Anchor>) -> impl Iterator<Item = (ReplicaId, impl Iterator<Item = &Selection<Anchor>>)>
+      pub fn remote_selections_in_range(&self, Range<Anchor>) -> impl Iterator<Item = (ReplicaId, i
+    */
+
     pub fn remote_selections_in_range<'a>(
         &'a self,
         range: Range<Anchor>,
@@ -2108,7 +2198,7 @@ impl<'a> Iterator for BufferChunks<'a> {
 }
 
 impl QueryCursorHandle {
-    fn new() -> Self {
+    pub(crate) fn new() -> Self {
         QueryCursorHandle(Some(
             QUERY_CURSORS
                 .lock()

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<HighlightMap>,
 }
 
@@ -127,6 +130,7 @@ impl Language {
                     brackets_query: Query::new(ts_language, "").unwrap(),
                     highlights_query: Query::new(ts_language, "").unwrap(),
                     indents_query: Query::new(ts_language, "").unwrap(),
+                    outline_query: Query::new(ts_language, "").unwrap(),
                     ts_language,
                     highlight_map: Default::default(),
                 })
@@ -164,6 +168,16 @@ impl Language {
         Ok(self)
     }
 
+    pub fn with_outline_query(mut self, source: &str) -> Result<Self> {
+        let grammar = self
+            .grammar
+            .as_mut()
+            .and_then(Arc::get_mut)
+            .ok_or_else(|| anyhow!("grammar does not exist or is already being used"))?;
+        grammar.outline_query = Query::new(grammar.ts_language, source)?;
+        Ok(self)
+    }
+
     pub fn name(&self) -> &str {
         self.config.name.as_str()
     }

crates/language/src/outline.rs 🔗

@@ -0,0 +1,12 @@
+use std::ops::Range;
+
+#[derive(Debug)]
+pub struct Outline(pub Vec<OutlineItem>);
+
+#[derive(Debug)]
+pub struct OutlineItem {
+    pub id: usize,
+    pub range: Range<usize>,
+    pub text: String,
+    pub name_range_in_text: Range<usize>,
+}

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

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<Workspace>) {
+        let editor = workspace
+            .active_item(cx)
+            .unwrap()
+            .to_any()
+            .downcast::<Editor>()
+            .unwrap();
+        let buffer = editor.read(cx).buffer().read(cx);
+        dbg!(buffer.read(cx).outline());
+    }
+
+    fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {}
+}

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

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

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 {

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({