diff --git a/Cargo.lock b/Cargo.lock index 0b5a802c52..ed7c23bab5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3794,6 +3794,7 @@ dependencies = [ "fuzzy", "gpui", "ordered-float", + "picker", "postage", "project", "settings", diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index 913d182aa5..38bcdeda30 100644 --- a/crates/picker/src/picker.rs +++ b/crates/picker/src/picker.rs @@ -15,6 +15,7 @@ pub struct Picker { delegate: WeakViewHandle, query_editor: ViewHandle, list_state: UniformListState, + update_task: Option>, } pub trait PickerDelegate: View { @@ -87,12 +88,14 @@ impl Picker { }); cx.subscribe(&query_editor, Self::on_query_editor_event) .detach(); - - Self { - delegate, + let mut this = Self { query_editor, list_state: Default::default(), - } + update_task: None, + delegate, + }; + this.update_matches(cx); + this } fn render_matches(&self, cx: &AppContext) -> ElementBox { @@ -137,22 +140,31 @@ impl Picker { event: &editor::Event, cx: &mut ViewContext, ) { - if let Some(delegate) = self.delegate.upgrade(cx) { - match event { - editor::Event::BufferEdited { .. } => { - let query = self.query_editor.read(cx).text(cx); - let update = delegate.update(cx, |d, cx| d.update_matches(query, cx)); - cx.spawn(|this, mut cx| async move { - update.await; - this.update(&mut cx, |_, cx| cx.notify()); + match event { + editor::Event::BufferEdited { .. } => self.update_matches(cx), + editor::Event::Blurred => { + if let Some(delegate) = self.delegate.upgrade(cx) { + delegate.update(cx, |delegate, cx| { + delegate.dismiss(cx); }) - .detach(); } - editor::Event::Blurred => delegate.update(cx, |delegate, cx| { - delegate.dismiss(cx); - }), - _ => {} } + _ => {} + } + } + + fn update_matches(&mut self, cx: &mut ViewContext) { + if let Some(delegate) = self.delegate.upgrade(cx) { + let query = self.query_editor.read(cx).text(cx); + let update = delegate.update(cx, |d, cx| d.update_matches(query, cx)); + cx.notify(); + self.update_task = Some(cx.spawn(|this, mut cx| async move { + update.await; + this.update(&mut cx, |this, cx| { + cx.notify(); + this.update_task.take(); + }); + })); } } diff --git a/crates/project_symbols/Cargo.toml b/crates/project_symbols/Cargo.toml index de22c0eda0..e199b700f6 100644 --- a/crates/project_symbols/Cargo.toml +++ b/crates/project_symbols/Cargo.toml @@ -11,6 +11,7 @@ doctest = false editor = { path = "../editor" } fuzzy = { path = "../fuzzy" } gpui = { path = "../gpui" } +picker = { path = "../picker" } project = { path = "../project" } text = { path = "../text" } settings = { path = "../settings" } diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index c14f7bea33..2c048c4b7e 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -3,43 +3,32 @@ use editor::{ }; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - actions, elements::*, keymap, AppContext, Axis, Entity, ModelHandle, MutableAppContext, - RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle, + actions, elements::*, AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Task, + View, ViewContext, ViewHandle, }; use ordered_float::OrderedFloat; +use picker::{Picker, PickerDelegate}; use project::{Project, Symbol}; use settings::Settings; -use std::{ - borrow::Cow, - cmp::{self, Reverse}, -}; +use std::{borrow::Cow, cmp::Reverse}; use util::ResultExt; -use workspace::{ - menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev}, - Workspace, -}; +use workspace::Workspace; actions!(project_symbols, [Toggle]); pub fn init(cx: &mut MutableAppContext) { 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); + Picker::::init(cx); } pub struct ProjectSymbolsView { - handle: WeakViewHandle, + picker: ViewHandle>, project: ModelHandle, selected_match_index: usize, - list_state: UniformListState, symbols: Vec, match_candidates: Vec, + show_worktree_root_name: bool, matches: Vec, - pending_symbols_task: Task>, - query_editor: ViewHandle, } pub enum Event { @@ -56,59 +45,29 @@ impl View for ProjectSymbolsView { "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, cx: &mut RenderContext) -> ElementBox { - let settings = cx.global::(); - 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( - FlexItem::new(self.render_matches(cx)) - .flex(1., false) - .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 render(&mut self, _: &mut RenderContext) -> ElementBox { + ChildView::new(self.picker.clone()).boxed() } fn on_focus(&mut self, cx: &mut ViewContext) { - cx.focus(&self.query_editor); + cx.focus(&self.picker); } } impl ProjectSymbolsView { fn new(project: ModelHandle, cx: &mut ViewContext) -> Self { - let query_editor = cx.add_view(|cx| { - Editor::single_line(Some(|theme| theme.selector.input_editor.clone()), cx) - }); - cx.subscribe(&query_editor, Self::on_query_editor_event) - .detach(); + let handle = cx.weak_handle(); + let picker = cx.add_view(|cx| Picker::new(handle, cx)); let mut this = Self { - handle: cx.weak_handle(), + picker, project, selected_match_index: 0, - list_state: Default::default(), symbols: Default::default(), match_candidates: Default::default(), matches: Default::default(), - pending_symbols_task: Task::ready(None), - query_editor, + show_worktree_root_name: false, }; - this.update_matches(cx); + this.update_matches(String::new(), cx).detach(); this } @@ -121,72 +80,7 @@ impl ProjectSymbolsView { }); } - fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext) { - if self.selected_match_index > 0 { - self.select(self.selected_match_index - 1, 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, cx); - } - } - - fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext) { - self.select(0, cx); - } - - fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext) { - self.select(self.matches.len().saturating_sub(1), cx); - } - - fn select(&mut self, index: usize, cx: &mut ViewContext) { - self.selected_match_index = index; - self.list_state.scroll_to(ScrollTarget::Show(index)); - cx.notify(); - } - - fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { - if let Some(symbol) = self - .matches - .get(self.selected_match_index) - .map(|mat| self.symbols[mat.candidate_id].clone()) - { - cx.emit(Event::Selected(symbol)); - } - } - - fn update_matches(&mut self, cx: &mut ViewContext) { - self.filter(cx); - let query = self.query_editor.read(cx).text(cx); - let symbols = self - .project - .update(cx, |project, cx| project.symbols(&query, cx)); - self.pending_symbols_task = cx.spawn_weak(|this, mut cx| async move { - let symbols = symbols.await.log_err()?; - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, cx| { - this.match_candidates = symbols - .iter() - .enumerate() - .map(|(id, symbol)| { - StringMatchCandidate::new( - id, - symbol.label.text[symbol.label.filter_range.clone()].to_string(), - ) - }) - .collect(); - this.symbols = symbols; - this.filter(cx); - }); - } - None - }); - } - - fn filter(&mut self, cx: &mut ViewContext) { - let query = self.query_editor.read(cx).text(cx); + fn filter(&mut self, query: &str, cx: &mut ViewContext) { let mut matches = if query.is_empty() { self.match_candidates .iter() @@ -201,7 +95,7 @@ impl ProjectSymbolsView { } else { smol::block_on(fuzzy::match_strings( &self.match_candidates, - &query, + query, false, 100, &Default::default(), @@ -225,112 +119,10 @@ impl ProjectSymbolsView { } self.matches = matches; - self.select_first(&SelectFirst, cx); + self.set_selected_index(0, cx); cx.notify(); } - fn render_matches(&self, cx: &AppContext) -> ElementBox { - if self.matches.is_empty() { - let settings = cx.global::(); - 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()); - - let show_worktree_root_name = - view.project.read(cx).visible_worktrees(cx).count() > 1; - items.extend(view.matches[range].iter().enumerate().map(move |(ix, m)| { - view.render_match(m, start + ix, show_worktree_root_name, cx) - })); - }, - ); - - Container::new(list.boxed()) - .with_margin_top(6.0) - .named("matches") - } - - fn render_match( - &self, - string_match: &StringMatch, - index: usize, - show_worktree_root_name: bool, - cx: &AppContext, - ) -> ElementBox { - let settings = cx.global::(); - 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]; - let syntax_runs = styled_runs_for_code_label(&symbol.label, &settings.theme.editor.syntax); - - let mut path = symbol.path.to_string_lossy(); - if show_worktree_root_name { - let project = self.project.read(cx); - if let Some(worktree) = project.worktree_for_id(symbol.worktree_id, cx) { - path = Cow::Owned(format!( - "{}{}{}", - worktree.read(cx).root_name(), - std::path::MAIN_SEPARATOR, - path.as_ref() - )); - } - } - - Flex::column() - .with_child( - Text::new(symbol.label.text.clone(), style.label.text.clone()) - .with_soft_wrap(false) - .with_highlights(combine_syntax_and_fuzzy_match_highlights( - &symbol.label.text, - style.label.text.clone().into(), - syntax_runs, - &string_match.positions, - )) - .boxed(), - ) - .with_child( - // Avoid styling the path differently when it is selected, since - // the symbol's syntax highlighting doesn't change when selected. - Label::new(path.to_string(), settings.theme.selector.item.label.clone()).boxed(), - ) - .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::BufferEdited { .. } => self.update_matches(cx), - _ => {} - } - } - fn on_event( workspace: &mut Workspace, _: ViewHandle, @@ -369,3 +161,108 @@ impl ProjectSymbolsView { } } } + +impl PickerDelegate for ProjectSymbolsView { + fn confirm(&mut self, cx: &mut ViewContext) { + if let Some(symbol) = self + .matches + .get(self.selected_match_index) + .map(|mat| self.symbols[mat.candidate_id].clone()) + { + cx.emit(Event::Selected(symbol)); + } + } + + fn dismiss(&mut self, cx: &mut ViewContext) { + cx.emit(Event::Dismissed); + } + + fn match_count(&self) -> usize { + self.matches.len() + } + + fn selected_index(&self) -> usize { + self.selected_match_index + } + + fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext) { + self.selected_match_index = ix; + cx.notify(); + } + + fn update_matches(&mut self, query: String, cx: &mut ViewContext) -> Task<()> { + self.filter(&query, cx); + self.show_worktree_root_name = self.project.read(cx).visible_worktrees(cx).count() > 1; + let symbols = self + .project + .update(cx, |project, cx| project.symbols(&query, cx)); + cx.spawn_weak(|this, mut cx| async move { + let symbols = symbols.await.log_err(); + if let Some(this) = this.upgrade(&cx) { + if let Some(symbols) = symbols { + this.update(&mut cx, |this, cx| { + this.match_candidates = symbols + .iter() + .enumerate() + .map(|(id, symbol)| { + StringMatchCandidate::new( + id, + symbol.label.text[symbol.label.filter_range.clone()] + .to_string(), + ) + }) + .collect(); + this.symbols = symbols; + this.filter(&query, cx); + }); + } + } + }) + } + + fn render_match(&self, ix: usize, selected: bool, cx: &AppContext) -> ElementBox { + let string_match = &self.matches[ix]; + let settings = cx.global::(); + let style = if selected { + &settings.theme.selector.active_item + } else { + &settings.theme.selector.item + }; + let symbol = &self.symbols[string_match.candidate_id]; + let syntax_runs = styled_runs_for_code_label(&symbol.label, &settings.theme.editor.syntax); + + let mut path = symbol.path.to_string_lossy(); + if self.show_worktree_root_name { + let project = self.project.read(cx); + if let Some(worktree) = project.worktree_for_id(symbol.worktree_id, cx) { + path = Cow::Owned(format!( + "{}{}{}", + worktree.read(cx).root_name(), + std::path::MAIN_SEPARATOR, + path.as_ref() + )); + } + } + + Flex::column() + .with_child( + Text::new(symbol.label.text.clone(), style.label.text.clone()) + .with_soft_wrap(false) + .with_highlights(combine_syntax_and_fuzzy_match_highlights( + &symbol.label.text, + style.label.text.clone().into(), + syntax_runs, + &string_match.positions, + )) + .boxed(), + ) + .with_child( + // Avoid styling the path differently when it is selected, since + // the symbol's syntax highlighting doesn't change when selected. + Label::new(path.to_string(), settings.theme.selector.item.label.clone()).boxed(), + ) + .contained() + .with_style(style.container) + .boxed() + } +}