diff --git a/Cargo.lock b/Cargo.lock index 0b5a802c520978a51b542f63373e51c0d5974ba9..ed7c23bab51b76dc8d8787854c6d89bdc825ab30 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 913d182aa5f60bdde01afcfd62c7ee6be17363eb..38bcdeda307fb9f6a541fdd5667124336296e0fb 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 de22c0eda027a4a3ec1e0a6c0f0bc4024fb58747..e199b700f6860db74ce2da5c09b7d0474cb84c5c 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 c14f7bea33b70089f3988766e64ea38fe8f1d11e..2c048c4b7e0c6c66c63966712433c6e7e752c14a 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,57 +119,111 @@ 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"); + fn on_event( + workspace: &mut Workspace, + _: ViewHandle, + event: &Event, + cx: &mut ViewContext, + ) { + match event { + Event::Dismissed => workspace.dismiss_modal(cx), + Event::Selected(symbol) => { + let buffer = workspace + .project() + .update(cx, |project, cx| project.open_buffer_for_symbol(symbol, cx)); + + let symbol = symbol.clone(); + cx.spawn(|workspace, mut cx| async move { + let buffer = buffer.await?; + workspace.update(&mut cx, |workspace, cx| { + let position = buffer + .read(cx) + .clip_point_utf16(symbol.range.start, Bias::Left); + + let editor = workspace.open_project_item::(buffer, cx); + editor.update(cx, |editor, cx| { + editor.select_ranges( + [position..position], + Some(Autoscroll::Center), + cx, + ); + }); + }); + Ok::<_, anyhow::Error>(()) + }) + .detach_and_log_err(cx); + workspace.dismiss_modal(cx); + } } + } +} - 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()); +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); + } - 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) - })); - }, - ); + fn match_count(&self) -> usize { + self.matches.len() + } - Container::new(list.boxed()) - .with_margin_top(6.0) - .named("matches") + fn selected_index(&self) -> usize { + self.selected_match_index } - fn render_match( - &self, - string_match: &StringMatch, - index: usize, - show_worktree_root_name: bool, - cx: &AppContext, - ) -> ElementBox { + 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 index == self.selected_match_index { + let style = if selected { &settings.theme.selector.active_item } else { &settings.theme.selector.item @@ -284,7 +232,7 @@ impl ProjectSymbolsView { 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 { + 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!( @@ -317,55 +265,4 @@ impl ProjectSymbolsView { .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, - event: &Event, - cx: &mut ViewContext, - ) { - match event { - Event::Dismissed => workspace.dismiss_modal(cx), - Event::Selected(symbol) => { - let buffer = workspace - .project() - .update(cx, |project, cx| project.open_buffer_for_symbol(symbol, cx)); - - let symbol = symbol.clone(); - cx.spawn(|workspace, mut cx| async move { - let buffer = buffer.await?; - workspace.update(&mut cx, |workspace, cx| { - let position = buffer - .read(cx) - .clip_point_utf16(symbol.range.start, Bias::Left); - - let editor = workspace.open_project_item::(buffer, cx); - editor.update(cx, |editor, cx| { - editor.select_ranges( - [position..position], - Some(Autoscroll::Center), - cx, - ); - }); - }); - Ok::<_, anyhow::Error>(()) - }) - .detach_and_log_err(cx); - workspace.dismiss_modal(cx); - } - } - } }