From 8f375a5026c1c54f8c9e2e6f57ee7288b48ff99e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 22 Feb 2022 08:42:12 +0100 Subject: [PATCH] Start on a new `project_symbols` crate --- 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(-) create mode 100644 crates/project_symbols/Cargo.toml create mode 100644 crates/project_symbols/src/project_symbols.rs diff --git a/Cargo.lock b/Cargo.lock index 31bd5f2993..09c517559c 100644 --- a/Cargo.lock +++ b/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", diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index c1ef8fdfcd..1aeefc5c8e 100644 --- a/crates/project/src/project.rs +++ b/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, } +pub struct ProjectSymbol { + pub text: String, + pub highlight_ranges: Vec<(Range, HighlightStyle)>, +} + #[derive(Default)] pub struct ProjectTransaction(pub HashMap, language::Transaction>); diff --git a/crates/project_symbols/Cargo.toml b/crates/project_symbols/Cargo.toml new file mode 100644 index 0000000000..ec697ea541 --- /dev/null +++ b/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"] } diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs new file mode 100644 index 0000000000..40aeca87bd --- /dev/null +++ b/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, + project: ModelHandle, + settings: watch::Receiver, + selected_match_index: usize, + list_state: UniformListState, + symbols: Vec, + matches: Vec, + query_editor: ViewHandle, +} + +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) -> 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) { + cx.focus(&self.query_editor); + } +} + +impl ProjectSymbolsView { + fn new( + project: ModelHandle, + settings: watch::Receiver, + cx: &mut ViewContext, + ) -> Self { + let query_editor = cx.add_view(|cx| { + Editor::single_line( + { + let settings = settings.clone(); + Arc::new(move |_| { + let settings = settings.borrow(); + EditorSettings { + style: settings.theme.selector.input_editor.as_editor(), + tab_size: settings.tab_size, + soft_wrap: editor::SoftWrap::None, + } + }) + }, + cx, + ) + }); + cx.subscribe(&query_editor, Self::on_query_editor_event) + .detach(); + let 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.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) { + if self.selected_match_index > 0 { + self.select(self.selected_match_index - 1, 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, false, cx); + } + } + + fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext) { + self.select(0, false, cx); + } + + fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext) { + self.select(self.matches.len().saturating_sub(1), false, cx); + } + + fn select(&mut self, index: usize, center: bool, cx: &mut ViewContext) { + 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) { + cx.emit(Event::Dismissed); + } + + fn update_matches(&mut self, _: &mut ViewContext) {} + + 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, + event: &editor::Event, + cx: &mut ViewContext, + ) { + match event { + editor::Event::Blurred => cx.emit(Event::Dismissed), + editor::Event::Edited => self.update_matches(cx), + _ => {} + } + } + + fn on_event( + workspace: &mut Workspace, + _: ViewHandle, + event: &Event, + cx: &mut ViewContext, + ) { + match event { + Event::Dismissed => workspace.dismiss_modal(cx), + } + } +} diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 3b3b0393f6..02e0b70f88 100644 --- a/crates/zed/Cargo.toml +++ b/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" } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index b154241343..a6dda2e27b 100644 --- a/crates/zed/src/main.rs +++ b/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);