Start on a new `project_symbols` crate

Antonio Scandurra created

Change summary

Cargo.lock                                    |  14 +
crates/project/src/project.rs                 |   9 
crates/project_symbols/Cargo.toml             |  16 +
crates/project_symbols/src/project_symbols.rs | 255 +++++++++++++++++++++
crates/zed/Cargo.toml                         |   1 
crates/zed/src/main.rs                        |   1 
6 files changed, 294 insertions(+), 2 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -3559,6 +3559,19 @@ dependencies = [
  "workspace",
 ]
 
+[[package]]
+name = "project_symbols"
+version = "0.1.0"
+dependencies = [
+ "editor",
+ "fuzzy",
+ "gpui",
+ "postage",
+ "project",
+ "text",
+ "workspace",
+]
+
 [[package]]
 name = "prost"
 version = "0.8.0"
@@ -5794,6 +5807,7 @@ dependencies = [
  "postage",
  "project",
  "project_panel",
+ "project_symbols",
  "rand 0.8.3",
  "regex",
  "rpc",

crates/project/src/project.rs 🔗

@@ -10,8 +10,8 @@ use collections::{hash_map, HashMap, HashSet};
 use futures::{future::Shared, Future, FutureExt};
 use fuzzy::{PathMatch, PathMatchCandidate, PathMatchCandidateSet};
 use gpui::{
-    AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task,
-    UpgradeModelHandle, WeakModelHandle,
+    fonts::HighlightStyle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
+    MutableAppContext, Task, UpgradeModelHandle, WeakModelHandle,
 };
 use language::{
     range_from_lsp, Anchor, AnchorRangeExt, Bias, Buffer, CodeAction, Completion, CompletionLabel,
@@ -118,6 +118,11 @@ pub struct Definition {
     pub target_range: Range<language::Anchor>,
 }
 
+pub struct ProjectSymbol {
+    pub text: String,
+    pub highlight_ranges: Vec<(Range<usize>, HighlightStyle)>,
+}
+
 #[derive(Default)]
 pub struct ProjectTransaction(pub HashMap<ModelHandle<Buffer>, language::Transaction>);
 

crates/project_symbols/Cargo.toml 🔗

@@ -0,0 +1,16 @@
+[package]
+name = "project_symbols"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+path = "src/project_symbols.rs"
+
+[dependencies]
+editor = { path = "../editor" }
+fuzzy = { path = "../fuzzy" }
+gpui = { path = "../gpui" }
+project = { path = "../project" }
+text = { path = "../text" }
+workspace = { path = "../workspace" }
+postage = { version = "0.4", features = ["futures-traits"] }

crates/project_symbols/src/project_symbols.rs 🔗

@@ -0,0 +1,255 @@
+use std::{cmp, sync::Arc};
+
+use editor::{combine_syntax_and_fuzzy_match_highlights, Editor, EditorSettings};
+use fuzzy::StringMatch;
+use gpui::{
+    action,
+    elements::*,
+    keymap::{self, Binding},
+    AppContext, Axis, Entity, ModelHandle, MutableAppContext, RenderContext, View, ViewContext,
+    ViewHandle, WeakViewHandle,
+};
+use postage::watch;
+use project::{Project, ProjectSymbol};
+use workspace::{
+    menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev},
+    Settings, Workspace,
+};
+
+action!(Toggle);
+
+pub fn init(cx: &mut MutableAppContext) {
+    cx.add_bindings([
+        Binding::new("cmd-t", Toggle, None),
+        Binding::new("escape", Toggle, Some("ProjectSymbolsView")),
+    ]);
+    cx.add_action(ProjectSymbolsView::toggle);
+    cx.add_action(ProjectSymbolsView::confirm);
+    cx.add_action(ProjectSymbolsView::select_prev);
+    cx.add_action(ProjectSymbolsView::select_next);
+    cx.add_action(ProjectSymbolsView::select_first);
+    cx.add_action(ProjectSymbolsView::select_last);
+}
+
+pub struct ProjectSymbolsView {
+    handle: WeakViewHandle<Self>,
+    project: ModelHandle<Project>,
+    settings: watch::Receiver<Settings>,
+    selected_match_index: usize,
+    list_state: UniformListState,
+    symbols: Vec<ProjectSymbol>,
+    matches: Vec<StringMatch>,
+    query_editor: ViewHandle<Editor>,
+}
+
+pub enum Event {
+    Dismissed,
+}
+
+impl Entity for ProjectSymbolsView {
+    type Event = Event;
+}
+
+impl View for ProjectSymbolsView {
+    fn ui_name() -> &'static str {
+        "ProjectSymbolsView"
+    }
+
+    fn keymap_context(&self, _: &AppContext) -> keymap::Context {
+        let mut cx = Self::default_keymap_context();
+        cx.set.insert("menu".into());
+        cx
+    }
+
+    fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
+        let settings = self.settings.borrow();
+
+        Flex::new(Axis::Vertical)
+            .with_child(
+                Container::new(ChildView::new(&self.query_editor).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(500.0)
+            .with_max_height(420.0)
+            .aligned()
+            .top()
+            .named("project symbols view")
+    }
+
+    fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
+        cx.focus(&self.query_editor);
+    }
+}
+
+impl ProjectSymbolsView {
+    fn new(
+        project: ModelHandle<Project>,
+        settings: watch::Receiver<Settings>,
+        cx: &mut ViewContext<Self>,
+    ) -> Self {
+        let query_editor = cx.add_view(|cx| {
+            Editor::single_line(
+                {
+                    let settings = settings.clone();
+                    Arc::new(move |_| {
+                        let settings = settings.borrow();
+                        EditorSettings {
+                            style: settings.theme.selector.input_editor.as_editor(),
+                            tab_size: settings.tab_size,
+                            soft_wrap: editor::SoftWrap::None,
+                        }
+                    })
+                },
+                cx,
+            )
+        });
+        cx.subscribe(&query_editor, Self::on_query_editor_event)
+            .detach();
+        let mut this = Self {
+            handle: cx.weak_handle(),
+            project,
+            settings,
+            selected_match_index: 0,
+            list_state: Default::default(),
+            symbols: Default::default(),
+            matches: Default::default(),
+            query_editor,
+        };
+        this.update_matches(cx);
+        this
+    }
+
+    fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
+        workspace.toggle_modal(cx, |cx, workspace| {
+            let project = workspace.project().clone();
+            let symbols = cx.add_view(|cx| Self::new(project, workspace.settings.clone(), cx));
+            cx.subscribe(&symbols, Self::on_event).detach();
+            symbols
+        });
+    }
+
+    fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
+        if self.selected_match_index > 0 {
+            self.select(self.selected_match_index - 1, false, cx);
+        }
+    }
+
+    fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
+        if self.selected_match_index + 1 < self.matches.len() {
+            self.select(self.selected_match_index + 1, false, cx);
+        }
+    }
+
+    fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
+        self.select(0, false, cx);
+    }
+
+    fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
+        self.select(self.matches.len().saturating_sub(1), false, cx);
+    }
+
+    fn select(&mut self, index: usize, center: bool, cx: &mut ViewContext<Self>) {
+        self.selected_match_index = index;
+        self.list_state.scroll_to(if center {
+            ScrollTarget::Center(index)
+        } else {
+            ScrollTarget::Show(index)
+        });
+        cx.notify();
+    }
+
+    fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
+        cx.emit(Event::Dismissed);
+    }
+
+    fn update_matches(&mut self, _: &mut ViewContext<Self>) {}
+
+    fn render_matches(&self) -> ElementBox {
+        if self.matches.is_empty() {
+            let settings = self.settings.borrow();
+            return Container::new(
+                Label::new(
+                    "No matches".into(),
+                    settings.theme.selector.empty.label.clone(),
+                )
+                .boxed(),
+            )
+            .with_style(settings.theme.selector.empty.container)
+            .named("empty matches");
+        }
+
+        let handle = self.handle.clone();
+        let list = UniformList::new(
+            self.list_state.clone(),
+            self.matches.len(),
+            move |mut range, items, cx| {
+                let cx = cx.as_ref();
+                let view = handle.upgrade(cx).unwrap();
+                let view = view.read(cx);
+                let start = range.start;
+                range.end = cmp::min(range.end, view.matches.len());
+                items.extend(
+                    view.matches[range]
+                        .iter()
+                        .enumerate()
+                        .map(move |(ix, m)| view.render_match(m, start + ix)),
+                );
+            },
+        );
+
+        Container::new(list.boxed())
+            .with_margin_top(6.0)
+            .named("matches")
+    }
+
+    fn render_match(&self, string_match: &StringMatch, index: usize) -> ElementBox {
+        let settings = self.settings.borrow();
+        let style = if index == self.selected_match_index {
+            &settings.theme.selector.active_item
+        } else {
+            &settings.theme.selector.item
+        };
+        let symbol = &self.symbols[string_match.candidate_id];
+
+        Text::new(symbol.text.clone(), style.label.text.clone())
+            .with_soft_wrap(false)
+            .with_highlights(combine_syntax_and_fuzzy_match_highlights(
+                &symbol.text,
+                style.label.text.clone().into(),
+                symbol.highlight_ranges.iter().cloned(),
+                &string_match.positions,
+            ))
+            .contained()
+            .with_style(style.container)
+            .boxed()
+    }
+
+    fn on_query_editor_event(
+        &mut self,
+        _: ViewHandle<Editor>,
+        event: &editor::Event,
+        cx: &mut ViewContext<Self>,
+    ) {
+        match event {
+            editor::Event::Blurred => cx.emit(Event::Dismissed),
+            editor::Event::Edited => self.update_matches(cx),
+            _ => {}
+        }
+    }
+
+    fn on_event(
+        workspace: &mut Workspace,
+        _: ViewHandle<Self>,
+        event: &Event,
+        cx: &mut ViewContext<Workspace>,
+    ) {
+        match event {
+            Event::Dismissed => workspace.dismiss_modal(cx),
+        }
+    }
+}

crates/zed/Cargo.toml 🔗

@@ -47,6 +47,7 @@ lsp = { path = "../lsp" }
 outline = { path = "../outline" }
 project = { path = "../project" }
 project_panel = { path = "../project_panel" }
+project_symbols = { path = "../project_symbols" }
 rpc = { path = "../rpc" }
 sum_tree = { path = "../sum_tree" }
 text = { path = "../text" }

crates/zed/src/main.rs 🔗

@@ -57,6 +57,7 @@ fn main() {
         file_finder::init(cx);
         chat_panel::init(cx);
         outline::init(cx);
+        project_symbols::init(cx);
         project_panel::init(cx);
         diagnostics::init(cx);
         find::init(cx);