WIP: Make PickerDelegate a fully owned object instead of a view

This avoids issues with the parent view being on the stack when we want to
interact with the delegate from the picker. Still have several picker usages
to convert.
This commit is contained in:
Nathan Sobo 2023-04-19 22:05:29 -06:00
parent 5514349b6b
commit d70644618a
12 changed files with 309 additions and 393 deletions

View File

@ -1,49 +1,41 @@
use client::{ContactRequestStatus, User, UserStore};
use gpui::{
elements::*, AnyViewHandle, AppContext, Entity, ModelHandle, MouseState, Task, View,
ViewContext, ViewHandle,
};
use picker::{Picker, PickerDelegate};
use gpui::{elements::*, AppContext, ModelHandle, MouseState, Task, ViewContext};
use picker::{Picker, PickerDelegate, PickerEvent};
use settings::Settings;
use std::sync::Arc;
use util::TryFutureExt;
pub fn init(cx: &mut AppContext) {
Picker::<ContactFinder>::init(cx);
Picker::<ContactFinderDelegate>::init(cx);
}
pub struct ContactFinder {
picker: ViewHandle<Picker<Self>>,
pub type ContactFinder = Picker<ContactFinderDelegate>;
pub fn build_contact_finder(
user_store: ModelHandle<UserStore>,
cx: &mut ViewContext<ContactFinder>,
) -> ContactFinder {
Picker::new(
ContactFinderDelegate {
user_store,
potential_contacts: Arc::from([]),
selected_index: 0,
},
cx,
)
}
pub struct ContactFinderDelegate {
potential_contacts: Arc<[Arc<User>]>,
user_store: ModelHandle<UserStore>,
selected_index: usize,
}
pub enum Event {
Dismissed,
}
impl Entity for ContactFinder {
type Event = Event;
}
impl View for ContactFinder {
fn ui_name() -> &'static str {
"ContactFinder"
impl PickerDelegate for ContactFinderDelegate {
fn placeholder_text(&self) -> Arc<str> {
"Search collaborator by username...".into()
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> Element<Self> {
ChildView::new(&self.picker, cx).boxed()
}
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
if cx.is_self_focused() {
cx.focus(&self.picker);
}
}
}
impl PickerDelegate for ContactFinder {
fn match_count(&self) -> usize {
self.potential_contacts.len()
}
@ -52,20 +44,20 @@ impl PickerDelegate for ContactFinder {
self.selected_index
}
fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Self>) {
fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
self.selected_index = ix;
}
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) -> Task<()> {
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
let search_users = self
.user_store
.update(cx, |store, cx| store.fuzzy_search_users(query, cx));
cx.spawn(|this, mut cx| async move {
cx.spawn(|picker, mut cx| async move {
async {
let potential_contacts = search_users.await?;
this.update(&mut cx, |this, cx| {
this.potential_contacts = potential_contacts.into();
picker.update(&mut cx, |picker, cx| {
picker.delegate_mut().potential_contacts = potential_contacts.into();
cx.notify();
})?;
anyhow::Ok(())
@ -75,7 +67,7 @@ impl PickerDelegate for ContactFinder {
})
}
fn confirm(&mut self, cx: &mut ViewContext<Self>) {
fn confirm(&mut self, cx: &mut ViewContext<Picker<Self>>) {
if let Some(user) = self.potential_contacts.get(self.selected_index) {
let user_store = self.user_store.read(cx);
match user_store.contact_request_status(user) {
@ -94,8 +86,8 @@ impl PickerDelegate for ContactFinder {
}
}
fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
cx.emit(Event::Dismissed);
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
cx.emit(PickerEvent::Dismiss);
}
fn render_match(
@ -164,28 +156,3 @@ impl PickerDelegate for ContactFinder {
.boxed()
}
}
impl ContactFinder {
pub fn new(user_store: ModelHandle<UserStore>, cx: &mut ViewContext<Self>) -> Self {
let this = cx.weak_handle();
Self {
picker: cx.add_view(|cx| {
Picker::new("Search collaborator by username...", this, cx)
.with_theme(|theme| theme.contact_finder.picker.clone())
}),
potential_contacts: Arc::from([]),
user_store,
selected_index: 0,
}
}
pub fn editor_text(&self, cx: &AppContext) -> String {
self.picker.read(cx).query(cx)
}
pub fn with_editor_text(self, editor_text: String, cx: &mut ViewContext<Self>) -> Self {
self.picker
.update(cx, |picker, cx| picker.set_query(editor_text, cx));
self
}
}

View File

@ -1,9 +1,14 @@
use crate::{contact_finder::ContactFinder, contact_list::ContactList, ToggleContactsMenu};
use crate::{
contact_finder::{build_contact_finder, ContactFinder},
contact_list::ContactList,
ToggleContactsMenu,
};
use client::UserStore;
use gpui::{
actions, elements::*, platform::MouseButton, AppContext, Entity, ModelHandle, View,
ViewContext, ViewHandle,
};
use picker::PickerEvent;
use project::Project;
use settings::Settings;
@ -50,19 +55,19 @@ impl ContactsPopover {
fn toggle_contact_finder(&mut self, _: &ToggleContactFinder, cx: &mut ViewContext<Self>) {
match &self.child {
Child::ContactList(list) => self.show_contact_finder(list.read(cx).editor_text(cx), cx),
Child::ContactFinder(finder) => {
self.show_contact_list(finder.read(cx).editor_text(cx), cx)
}
Child::ContactFinder(finder) => self.show_contact_list(finder.read(cx).query(cx), cx),
}
}
fn show_contact_finder(&mut self, editor_text: String, cx: &mut ViewContext<ContactsPopover>) {
let child = cx.add_view(|cx| {
ContactFinder::new(self.user_store.clone(), cx).with_editor_text(editor_text, cx)
let finder = build_contact_finder(self.user_store.clone(), cx);
finder.set_query(editor_text, cx);
finder
});
cx.focus(&child);
self._subscription = Some(cx.subscribe(&child, |_, _, event, cx| match event {
crate::contact_finder::Event::Dismissed => cx.emit(Event::Dismissed),
PickerEvent::Dismiss => cx.emit(Event::Dismissed),
}));
self.child = Child::ContactFinder(child);
cx.notify();

View File

@ -1,24 +1,25 @@
use collections::CommandPaletteFilter;
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
actions, elements::*, keymap_matcher::Keystroke, Action, AnyViewHandle, AppContext, Drawable,
Entity, MouseState, View, ViewContext, ViewHandle,
actions, elements::*, keymap_matcher::Keystroke, Action, AppContext, Drawable, MouseState,
ViewContext,
};
use picker::{Picker, PickerDelegate};
use picker::{Picker, PickerDelegate, PickerEvent};
use settings::Settings;
use std::cmp;
use util::ResultExt;
use workspace::Workspace;
pub fn init(cx: &mut AppContext) {
cx.add_action(CommandPalette::toggle);
Picker::<CommandPalette>::init(cx);
cx.add_action(toggle_command_palette);
Picker::<CommandPaletteDelegate>::init(cx);
}
actions!(command_palette, [Toggle]);
pub struct CommandPalette {
picker: ViewHandle<Picker<Self>>,
pub type CommandPalette = Picker<CommandPaletteDelegate>;
pub struct CommandPaletteDelegate {
actions: Vec<Command>,
matches: Vec<StringMatch>,
selected_ix: usize,
@ -40,9 +41,19 @@ struct Command {
keystrokes: Vec<Keystroke>,
}
impl CommandPalette {
pub fn new(focused_view_id: usize, cx: &mut ViewContext<Self>) -> Self {
let this = cx.weak_handle();
fn toggle_command_palette(_: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
let workspace = cx.handle();
let focused_view_id = cx.focused_view_id().unwrap_or_else(|| workspace.id());
cx.defer(move |workspace, cx| {
workspace.toggle_modal(cx, |_, cx| {
cx.add_view(|cx| Picker::new(CommandPaletteDelegate::new(focused_view_id, cx), cx))
});
});
}
impl CommandPaletteDelegate {
pub fn new(focused_view_id: usize, cx: &mut ViewContext<Picker<Self>>) -> Self {
let actions = cx
.available_actions(focused_view_id)
.filter_map(|(name, action, bindings)| {
@ -65,73 +76,20 @@ impl CommandPalette {
})
.collect();
let picker = cx.add_view(|cx| Picker::new("Execute a command...", this, cx));
Self {
picker,
actions,
matches: vec![],
selected_ix: 0,
focused_view_id,
}
}
fn toggle(_: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
let workspace = cx.handle();
let focused_view_id = cx.focused_view_id().unwrap_or_else(|| workspace.id());
cx.defer(move |workspace, cx| {
let this = cx.add_view(|cx| Self::new(focused_view_id, cx));
workspace.toggle_modal(cx, |_, cx| {
cx.subscribe(&this, Self::on_event).detach();
this
});
});
}
fn on_event(
workspace: &mut Workspace,
_: ViewHandle<Self>,
event: &Event,
cx: &mut ViewContext<Workspace>,
) {
match event {
Event::Dismissed => workspace.dismiss_modal(cx),
Event::Confirmed {
window_id,
focused_view_id,
action,
} => {
let window_id = *window_id;
let focused_view_id = *focused_view_id;
let action = action.boxed_clone();
workspace.dismiss_modal(cx);
cx.defer(move |_, cx| cx.dispatch_any_action_at(window_id, focused_view_id, action))
}
}
}
}
impl Entity for CommandPalette {
type Event = Event;
}
impl View for CommandPalette {
fn ui_name() -> &'static str {
"CommandPalette"
impl PickerDelegate for CommandPaletteDelegate {
fn placeholder_text(&self) -> std::sync::Arc<str> {
"Execute a command...".into()
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> Element<Self> {
ChildView::new(&self.picker, cx).boxed()
}
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
if cx.is_self_focused() {
cx.focus(&self.picker);
}
}
}
impl PickerDelegate for CommandPalette {
fn match_count(&self) -> usize {
self.matches.len()
}
@ -140,14 +98,14 @@ impl PickerDelegate for CommandPalette {
self.selected_ix
}
fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Self>) {
fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
self.selected_ix = ix;
}
fn update_matches(
&mut self,
query: String,
cx: &mut gpui::ViewContext<Self>,
cx: &mut ViewContext<Picker<Self>>,
) -> gpui::Task<()> {
let candidates = self
.actions
@ -159,7 +117,7 @@ impl PickerDelegate for CommandPalette {
char_bag: command.name.chars().collect(),
})
.collect::<Vec<_>>();
cx.spawn(move |this, mut cx| async move {
cx.spawn(move |picker, mut cx| async move {
let matches = if query.is_empty() {
candidates
.into_iter()
@ -182,33 +140,36 @@ impl PickerDelegate for CommandPalette {
)
.await
};
this.update(&mut cx, |this, _| {
this.matches = matches;
if this.matches.is_empty() {
this.selected_ix = 0;
} else {
this.selected_ix = cmp::min(this.selected_ix, this.matches.len() - 1);
}
})
.log_err();
picker
.update(&mut cx, |picker, _| {
let delegate = picker.delegate_mut();
delegate.matches = matches;
if delegate.matches.is_empty() {
delegate.selected_ix = 0;
} else {
delegate.selected_ix =
cmp::min(delegate.selected_ix, delegate.matches.len() - 1);
}
})
.log_err();
})
}
fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
cx.emit(Event::Dismissed);
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
cx.emit(PickerEvent::Dismiss);
}
fn confirm(&mut self, cx: &mut ViewContext<Self>) {
fn confirm(&mut self, cx: &mut ViewContext<Picker<Self>>) {
if !self.matches.is_empty() {
let window_id = cx.window_id();
let focused_view_id = self.focused_view_id;
let action_ix = self.matches[self.selected_ix].candidate_id;
cx.emit(Event::Confirmed {
window_id: cx.window_id(),
focused_view_id: self.focused_view_id,
action: self.actions.remove(action_ix).action,
let action = self.actions.remove(action_ix).action;
cx.defer(move |_, cx| {
cx.dispatch_any_action_at(window_id, focused_view_id, action);
});
} else {
cx.emit(Event::Dismissed);
}
cx.emit(PickerEvent::Dismiss);
}
fn render_match(
@ -353,7 +314,7 @@ mod tests {
});
workspace.update(cx, |workspace, cx| {
CommandPalette::toggle(workspace, &Toggle, cx)
toggle_command_palette(workspace, &Toggle, cx);
});
let palette = workspace.read_with(cx, |workspace, _| {
@ -362,7 +323,9 @@ mod tests {
palette
.update(cx, |palette, cx| {
palette.update_matches("bcksp".to_string(), cx)
palette
.delegate_mut()
.update_matches("bcksp".to_string(), cx)
})
.await;
@ -383,12 +346,12 @@ mod tests {
});
workspace.update(cx, |workspace, cx| {
CommandPalette::toggle(workspace, &Toggle, cx);
CommandPaletteDelegate::toggle(workspace, &Toggle, cx);
});
// Assert editor command not present
let palette = workspace.read_with(cx, |workspace, _| {
workspace.modal::<CommandPalette>().unwrap()
workspace.modal::<CommandPaletteDelegate>().unwrap()
});
palette

View File

@ -1,7 +1,6 @@
use fuzzy::PathMatch;
use gpui::{
actions, elements::*, AnyViewHandle, AppContext, Entity, ModelHandle, MouseState, Task, View,
ViewContext, ViewHandle,
actions, elements::*, AppContext, ModelHandle, MouseState, Task, ViewContext, WeakViewHandle,
};
use picker::{Picker, PickerDelegate};
use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
@ -16,9 +15,11 @@ use std::{
use util::{post_inc, ResultExt};
use workspace::Workspace;
pub struct FileFinder {
pub type FileFinder = Picker<FileFinderDelegate>;
pub struct FileFinderDelegate {
workspace: WeakViewHandle<Workspace>,
project: ModelHandle<Project>,
picker: ViewHandle<Picker<Self>>,
search_count: usize,
latest_search_id: usize,
latest_search_did_cancel: bool,
@ -32,8 +33,26 @@ pub struct FileFinder {
actions!(file_finder, [Toggle]);
pub fn init(cx: &mut AppContext) {
cx.add_action(FileFinder::toggle);
Picker::<FileFinder>::init(cx);
cx.add_action(toggle_file_finder);
FileFinder::init(cx);
}
fn toggle_file_finder(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
workspace.toggle_modal(cx, |workspace, cx| {
let relative_to = workspace
.active_item(cx)
.and_then(|item| item.project_path(cx))
.map(|project_path| project_path.path.clone());
let project = workspace.project().clone();
let workspace = cx.handle().downgrade();
let finder = cx.add_view(|cx| {
Picker::new(
FileFinderDelegate::new(workspace, project, relative_to, cx),
cx,
)
});
finder
});
}
pub enum Event {
@ -41,27 +60,7 @@ pub enum Event {
Dismissed,
}
impl Entity for FileFinder {
type Event = Event;
}
impl View for FileFinder {
fn ui_name() -> &'static str {
"FileFinder"
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> Element<Self> {
ChildView::new(&self.picker, cx).boxed()
}
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
if cx.is_self_focused() {
cx.focus(&self.picker);
}
}
}
impl FileFinder {
impl FileFinderDelegate {
fn labels_for_match(&self, path_match: &PathMatch) -> (String, Vec<usize>, String, Vec<usize>) {
let path = &path_match.path;
let path_string = path.to_string_lossy();
@ -88,48 +87,20 @@ impl FileFinder {
(file_name, file_name_positions, full_path, path_positions)
}
fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
workspace.toggle_modal(cx, |workspace, cx| {
let project = workspace.project().clone();
let relative_to = workspace
.active_item(cx)
.and_then(|item| item.project_path(cx))
.map(|project_path| project_path.path.clone());
let finder = cx.add_view(|cx| Self::new(project, relative_to, cx));
cx.subscribe(&finder, Self::on_event).detach();
finder
});
}
fn on_event(
workspace: &mut Workspace,
_: ViewHandle<FileFinder>,
event: &Event,
cx: &mut ViewContext<Workspace>,
) {
match event {
Event::Selected(project_path) => {
workspace
.open_path(project_path.clone(), None, true, cx)
.detach_and_log_err(cx);
workspace.dismiss_modal(cx);
}
Event::Dismissed => {
workspace.dismiss_modal(cx);
}
}
}
pub fn new(
workspace: WeakViewHandle<Workspace>,
project: ModelHandle<Project>,
relative_to: Option<Arc<Path>>,
cx: &mut ViewContext<Self>,
cx: &mut ViewContext<FileFinder>,
) -> Self {
let handle = cx.weak_handle();
cx.observe(&project, Self::project_updated).detach();
cx.observe(&project, |picker, _, cx| {
let query = picker.query(cx);
picker.delegate_mut().spawn_search(query, cx).detach();
})
.detach();
Self {
workspace,
project,
picker: cx.add_view(|cx| Picker::new("Search project files...", handle, cx)),
search_count: 0,
latest_search_id: 0,
latest_search_did_cancel: false,
@ -141,12 +112,7 @@ impl FileFinder {
}
}
fn project_updated(&mut self, _: ModelHandle<Project>, cx: &mut ViewContext<Self>) {
self.spawn_search(self.picker.read(cx).query(cx), cx)
.detach();
}
fn spawn_search(&mut self, query: String, cx: &mut ViewContext<Self>) -> Task<()> {
fn spawn_search(&mut self, query: String, cx: &mut ViewContext<FileFinder>) -> Task<()> {
let relative_to = self.relative_to.clone();
let worktrees = self
.project
@ -172,7 +138,7 @@ impl FileFinder {
self.cancel_flag.store(true, atomic::Ordering::Relaxed);
self.cancel_flag = Arc::new(AtomicBool::new(false));
let cancel_flag = self.cancel_flag.clone();
cx.spawn(|this, mut cx| async move {
cx.spawn(|picker, mut cx| async move {
let matches = fuzzy::match_path_sets(
candidate_sets.as_slice(),
&query,
@ -184,10 +150,13 @@ impl FileFinder {
)
.await;
let did_cancel = cancel_flag.load(atomic::Ordering::Relaxed);
this.update(&mut cx, |this, cx| {
this.set_matches(search_id, did_cancel, query, matches, cx)
})
.log_err();
picker
.update(&mut cx, |picker, cx| {
picker
.delegate_mut()
.set_matches(search_id, did_cancel, query, matches, cx)
})
.log_err();
})
}
@ -197,7 +166,7 @@ impl FileFinder {
did_cancel: bool,
query: String,
matches: Vec<PathMatch>,
cx: &mut ViewContext<Self>,
cx: &mut ViewContext<FileFinder>,
) {
if search_id >= self.latest_search_id {
self.latest_search_id = search_id;
@ -209,12 +178,15 @@ impl FileFinder {
self.latest_search_query = query;
self.latest_search_did_cancel = did_cancel;
cx.notify();
self.picker.update(cx, |_, cx| cx.notify());
}
}
}
impl PickerDelegate for FileFinder {
impl PickerDelegate for FileFinderDelegate {
fn placeholder_text(&self) -> Arc<str> {
"Search project files...".into()
}
fn match_count(&self) -> usize {
self.matches.len()
}
@ -232,13 +204,13 @@ impl PickerDelegate for FileFinder {
0
}
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Self>) {
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<FileFinder>) {
let mat = &self.matches[ix];
self.selected = Some((mat.worktree_id, mat.path.clone()));
cx.notify();
}
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) -> Task<()> {
fn update_matches(&mut self, query: String, cx: &mut ViewContext<FileFinder>) -> Task<()> {
if query.is_empty() {
self.latest_search_id = post_inc(&mut self.search_count);
self.matches.clear();
@ -249,18 +221,25 @@ impl PickerDelegate for FileFinder {
}
}
fn confirm(&mut self, cx: &mut ViewContext<Self>) {
fn confirm(&mut self, cx: &mut ViewContext<FileFinder>) {
if let Some(m) = self.matches.get(self.selected_index()) {
cx.emit(Event::Selected(ProjectPath {
worktree_id: WorktreeId::from_usize(m.worktree_id),
path: m.path.clone(),
}));
if let Some(workspace) = self.workspace.upgrade(cx) {
let project_path = ProjectPath {
worktree_id: WorktreeId::from_usize(m.worktree_id),
path: m.path.clone(),
};
workspace.update(cx, |workspace, cx| {
workspace
.open_path(project_path.clone(), None, true, cx)
.detach_and_log_err(cx);
workspace.dismiss_modal(cx);
})
}
}
}
fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
cx.emit(Event::Dismissed);
}
fn dismissed(&mut self, _: &mut ViewContext<FileFinder>) {}
fn render_match(
&self,
@ -336,11 +315,11 @@ mod tests {
let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
finder
.update(cx, |finder, cx| {
finder.update_matches("bna".to_string(), cx)
finder.delegate_mut().update_matches("bna".to_string(), cx)
})
.await;
finder.read_with(cx, |finder, _| {
assert_eq!(finder.matches.len(), 2);
assert_eq!(finder.delegate().matches.len(), 2);
});
let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
@ -385,8 +364,12 @@ mod tests {
let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
let (_, finder) =
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
let (_, finder) = cx.add_window(|cx| {
Picker::new(
FileFinderDelegate::new(workspace.read(cx).project().clone(), None, cx),
cx,
)
});
let query = "hi".to_string();
finder
@ -395,13 +378,14 @@ mod tests {
finder.read_with(cx, |f, _| assert_eq!(f.matches.len(), 5));
finder.update(cx, |finder, cx| {
let matches = finder.matches.clone();
let delegate = finder.delegate_mut();
let matches = delegate.matches.clone();
// Simulate a search being cancelled after the time limit,
// returning only a subset of the matches that would have been found.
drop(finder.spawn_search(query.clone(), cx));
finder.set_matches(
finder.latest_search_id,
drop(delegate.spawn_search(query.clone(), cx));
delegate.set_matches(
finder.delegate().latest_search_id,
true, // did-cancel
query.clone(),
vec![matches[1].clone(), matches[3].clone()],
@ -409,16 +393,16 @@ mod tests {
);
// Simulate another cancellation.
drop(finder.spawn_search(query.clone(), cx));
finder.set_matches(
finder.latest_search_id,
drop(delegate.spawn_search(query.clone(), cx));
delegate.set_matches(
delegate.latest_search_id,
true, // did-cancel
query.clone(),
vec![matches[0].clone(), matches[2].clone(), matches[3].clone()],
cx,
);
assert_eq!(finder.matches, matches[0..4])
assert_eq!(delegate.matches, matches[0..4])
});
}
@ -459,8 +443,12 @@ mod tests {
)
.await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
let (_, finder) =
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
let (_, finder) = cx.add_window(|cx| {
Picker::new(
FileFinderDelegate::new(workspace.read(cx).project().clone(), None, cx),
cx,
)
});
finder
.update(cx, |f, cx| f.spawn_search("hi".into(), cx))
.await;
@ -483,8 +471,9 @@ mod tests {
)
.await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
let (_, finder) =
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
let (_, finder) = cx.add_window(|cx| {
FileFinderDelegate::new(workspace.read(cx).project().clone(), None, cx)
});
// Even though there is only one worktree, that worktree's filename
// is included in the matching, because the worktree is a single file.
@ -536,8 +525,9 @@ mod tests {
.await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
let (_, finder) =
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
let (_, finder) = cx.add_window(|cx| {
FileFinderDelegate::new(workspace.read(cx).project().clone(), None, cx)
});
// Run a search that matches two files with the same relative path.
finder
@ -582,8 +572,9 @@ mod tests {
// first when they have the same name. In this case, b.txt is closer to dir2's a.txt
// so that one should be sorted earlier
let b_path = Some(Arc::from(Path::new("/root/dir2/b.txt")));
let (_, finder) =
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), b_path, cx));
let (_, finder) = cx.add_window(|cx| {
FileFinderDelegate::new(workspace.read(cx).project().clone(), b_path, cx)
});
finder
.update(cx, |f, cx| f.spawn_search("a.txt".into(), cx))
@ -614,8 +605,9 @@ mod tests {
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
let (_, finder) =
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
let (_, finder) = cx.add_window(|cx| {
FileFinderDelegate::new(workspace.read(cx).project().clone(), None, cx)
});
finder
.update(cx, |f, cx| f.spawn_search("dir".into(), cx))
.await;

View File

@ -163,7 +163,7 @@ impl PickerDelegate for LanguageSelector {
cx.emit(Event::Dismissed);
}
fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
fn dismissed(&mut self, cx: &mut ViewContext<Self>) {
cx.emit(Event::Dismissed);
}

View File

@ -229,7 +229,7 @@ impl PickerDelegate for OutlineView {
cx.emit(Event::Dismissed);
}
fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
fn dismissed(&mut self, cx: &mut ViewContext<Self>) {
self.restore_active_editor(cx);
cx.emit(Event::Dismissed);
}

View File

@ -5,15 +5,20 @@ use gpui::{
keymap_matcher::KeymapContext,
platform::{CursorStyle, MouseButton},
AnyViewHandle, AppContext, Axis, Element, Entity, MouseState, Task, View, ViewContext,
ViewHandle, WeakViewHandle,
ViewHandle,
};
use menu::{Cancel, Confirm, SelectFirst, SelectIndex, SelectLast, SelectNext, SelectPrev};
use parking_lot::Mutex;
use std::{cmp, sync::Arc};
use util::ResultExt;
use workspace::Modal;
pub enum PickerEvent {
Dismiss,
}
pub struct Picker<D: PickerDelegate> {
delegate: WeakViewHandle<D>,
delegate: D,
query_editor: ViewHandle<Editor>,
list_state: UniformListState,
max_size: Vector2F,
@ -21,13 +26,14 @@ pub struct Picker<D: PickerDelegate> {
confirmed: bool,
}
pub trait PickerDelegate: View {
pub trait PickerDelegate: Sized + 'static {
fn placeholder_text(&self) -> Arc<str>;
fn match_count(&self) -> usize;
fn selected_index(&self) -> usize;
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Self>);
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) -> Task<()>;
fn confirm(&mut self, cx: &mut ViewContext<Self>);
fn dismiss(&mut self, cx: &mut ViewContext<Self>);
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>);
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()>;
fn confirm(&mut self, cx: &mut ViewContext<Picker<Self>>);
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>);
fn render_match(
&self,
ix: usize,
@ -41,7 +47,7 @@ pub trait PickerDelegate: View {
}
impl<D: PickerDelegate> Entity for Picker<D> {
type Event = ();
type Event = PickerEvent;
}
impl<D: PickerDelegate> View for Picker<D> {
@ -52,12 +58,7 @@ impl<D: PickerDelegate> View for Picker<D> {
fn render(&mut self, cx: &mut ViewContext<Self>) -> Element<Self> {
let theme = (self.theme.lock())(&cx.global::<settings::Settings>().theme);
let query = self.query(cx);
let delegate = self.delegate.clone();
let match_count = if let Some(delegate) = delegate.upgrade(cx) {
delegate.read(cx).match_count()
} else {
0
};
let match_count = self.delegate.match_count();
let container_style;
let editor_style;
@ -94,14 +95,11 @@ impl<D: PickerDelegate> View for Picker<D> {
match_count,
cx,
move |this, mut range, items, cx| {
let delegate = this.delegate.upgrade(cx).unwrap();
let selected_ix = delegate.read(cx).selected_index();
range.end = cmp::min(range.end, delegate.read(cx).match_count());
let selected_ix = this.delegate.selected_index();
range.end = cmp::min(range.end, this.delegate.match_count());
items.extend(range.map(move |ix| {
MouseEventHandler::<D, _>::new(ix, cx, |state, cx| {
delegate
.read(cx)
.render_match(ix, state, ix == selected_ix, cx)
this.delegate.render_match(ix, state, ix == selected_ix, cx)
})
// Capture mouse events
.on_down(MouseButton::Left, |_, _, _| {})
@ -141,6 +139,12 @@ impl<D: PickerDelegate> View for Picker<D> {
}
}
impl<D: PickerDelegate> Modal for Picker<D> {
fn dismiss_on_event(event: &Self::Event) -> bool {
matches!(event, PickerEvent::Dismiss)
}
}
impl<D: PickerDelegate> Picker<D> {
pub fn init(cx: &mut AppContext) {
cx.add_action(Self::select_first);
@ -152,14 +156,12 @@ impl<D: PickerDelegate> Picker<D> {
cx.add_action(Self::cancel);
}
pub fn new<P>(placeholder: P, delegate: WeakViewHandle<D>, cx: &mut ViewContext<Self>) -> Self
where
P: Into<Arc<str>>,
{
pub fn new(delegate: D, cx: &mut ViewContext<Self>) -> Self {
let theme = Arc::new(Mutex::new(
Box::new(|theme: &theme::Theme| theme.picker.clone())
as Box<dyn Fn(&theme::Theme) -> theme::Picker>,
));
let placeholder_text = delegate.placeholder_text();
let query_editor = cx.add_view({
let picker_theme = theme.clone();
|cx| {
@ -169,13 +171,13 @@ impl<D: PickerDelegate> Picker<D> {
})),
cx,
);
editor.set_placeholder_text(placeholder, cx);
editor.set_placeholder_text(placeholder_text, cx);
editor
}
});
cx.subscribe(&query_editor, Self::on_query_editor_event)
.detach();
let this = Self {
let mut this = Self {
query_editor,
list_state: Default::default(),
delegate,
@ -183,12 +185,9 @@ impl<D: PickerDelegate> Picker<D> {
theme,
confirmed: false,
};
cx.defer(|this, cx| {
if let Some(delegate) = this.delegate.upgrade(cx) {
cx.observe(&delegate, |_, _, cx| cx.notify()).detach();
this.update_matches(String::new(), cx)
}
});
// TODO! How can the delegate notify the picker to update?
// cx.observe(&delegate, |_, _, cx| cx.notify()).detach();
this.update_matches(String::new(), cx);
this
}
@ -205,6 +204,14 @@ impl<D: PickerDelegate> Picker<D> {
self
}
pub fn delegate(&self) -> &D {
&self.delegate
}
pub fn delegate_mut(&mut self) -> &mut D {
&mut self.delegate
}
pub fn query(&self, cx: &AppContext) -> String {
self.query_editor.read(cx).text(cx)
}
@ -223,121 +230,93 @@ impl<D: PickerDelegate> Picker<D> {
match event {
editor::Event::BufferEdited { .. } => self.update_matches(self.query(cx), cx),
editor::Event::Blurred if !self.confirmed => {
if let Some(delegate) = self.delegate.upgrade(cx) {
delegate.update(cx, |delegate, cx| {
delegate.dismiss(cx);
})
}
self.dismiss(cx);
}
_ => {}
}
}
pub fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) {
if let Some(delegate) = self.delegate.upgrade(cx) {
let update = delegate.update(cx, |d, cx| d.update_matches(query, cx));
cx.spawn_weak(|this, mut cx| async move {
update.await;
this.upgrade(&cx)?
.update(&mut cx, |this, cx| {
if let Some(delegate) = this.delegate.upgrade(cx) {
let delegate = delegate.read(cx);
let index = delegate.selected_index();
let target = if delegate.center_selection_after_match_updates() {
ScrollTarget::Center(index)
} else {
ScrollTarget::Show(index)
};
this.list_state.scroll_to(target);
cx.notify();
}
})
.log_err()
})
.detach()
}
let update = self.delegate.update_matches(query, cx);
cx.spawn_weak(|this, mut cx| async move {
update.await;
this.upgrade(&cx)?
.update(&mut cx, |this, cx| {
let index = this.delegate.selected_index();
let target = if this.delegate.center_selection_after_match_updates() {
ScrollTarget::Center(index)
} else {
ScrollTarget::Show(index)
};
this.list_state.scroll_to(target);
cx.notify();
})
.log_err()
})
.detach()
}
pub fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
if let Some(delegate) = self.delegate.upgrade(cx) {
delegate.update(cx, |delegate, cx| {
if delegate.match_count() > 0 {
delegate.set_selected_index(0, cx);
self.list_state.scroll_to(ScrollTarget::Show(0));
}
});
cx.notify();
if self.delegate.match_count() > 0 {
self.delegate.set_selected_index(0, cx);
self.list_state.scroll_to(ScrollTarget::Show(0));
}
cx.notify();
}
pub fn select_index(&mut self, action: &SelectIndex, cx: &mut ViewContext<Self>) {
if let Some(delegate) = self.delegate.upgrade(cx) {
let index = action.0;
delegate.update(cx, |delegate, cx| {
if delegate.match_count() > 0 {
self.confirmed = true;
delegate.set_selected_index(index, cx);
delegate.confirm(cx);
}
});
let index = action.0;
if self.delegate.match_count() > 0 {
self.confirmed = true;
self.delegate.set_selected_index(index, cx);
self.delegate.confirm(cx);
}
}
pub fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
if let Some(delegate) = self.delegate.upgrade(cx) {
delegate.update(cx, |delegate, cx| {
let match_count = delegate.match_count();
if match_count > 0 {
let index = match_count - 1;
delegate.set_selected_index(index, cx);
self.list_state.scroll_to(ScrollTarget::Show(index));
}
});
cx.notify();
let match_count = self.delegate.match_count();
if match_count > 0 {
let index = match_count - 1;
self.delegate.set_selected_index(index, cx);
self.list_state.scroll_to(ScrollTarget::Show(index));
}
cx.notify();
}
pub fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
if let Some(delegate) = self.delegate.upgrade(cx) {
delegate.update(cx, |delegate, cx| {
let next_index = delegate.selected_index() + 1;
if next_index < delegate.match_count() {
delegate.set_selected_index(next_index, cx);
self.list_state.scroll_to(ScrollTarget::Show(next_index));
}
});
cx.notify();
let next_index = self.delegate.selected_index() + 1;
if next_index < self.delegate.match_count() {
self.delegate.set_selected_index(next_index, cx);
self.list_state.scroll_to(ScrollTarget::Show(next_index));
}
cx.notify();
}
pub fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
if let Some(delegate) = self.delegate.upgrade(cx) {
delegate.update(cx, |delegate, cx| {
let mut selected_index = delegate.selected_index();
if selected_index > 0 {
selected_index -= 1;
delegate.set_selected_index(selected_index, cx);
self.list_state
.scroll_to(ScrollTarget::Show(selected_index));
}
});
cx.notify();
let mut selected_index = self.delegate.selected_index();
if selected_index > 0 {
selected_index -= 1;
self.delegate.set_selected_index(selected_index, cx);
self.list_state
.scroll_to(ScrollTarget::Show(selected_index));
}
cx.notify();
}
fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
if let Some(delegate) = self.delegate.upgrade(cx) {
self.confirmed = true;
delegate.update(cx, |delegate, cx| delegate.confirm(cx));
}
self.confirmed = true;
self.delegate.confirm(cx);
}
fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
if let Some(delegate) = self.delegate.upgrade(cx) {
delegate.update(cx, |delegate, cx| delegate.dismiss(cx));
}
self.dismiss(cx);
}
fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
cx.emit(PickerEvent::Dismiss);
self.delegate.dismissed(cx);
}
}

View File

@ -176,7 +176,7 @@ impl PickerDelegate for ProjectSymbolsView {
}
}
fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
fn dismissed(&mut self, cx: &mut ViewContext<Self>) {
cx.emit(Event::Dismissed);
}

View File

@ -176,7 +176,7 @@ impl PickerDelegate for RecentProjectsView {
}
}
fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
fn dismissed(&mut self, cx: &mut ViewContext<Self>) {
cx.emit(Event::Dismissed);
}

View File

@ -156,7 +156,7 @@ impl PickerDelegate for ThemeSelector {
cx.emit(Event::Dismissed);
}
fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
fn dismissed(&mut self, cx: &mut ViewContext<Self>) {
if !self.selection_completed {
Self::set_theme(self.original_theme.clone(), cx);
self.selection_completed = true;

View File

@ -155,7 +155,7 @@ impl PickerDelegate for BaseKeymapSelector {
cx.emit(Event::Dismissed);
}
fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
fn dismissed(&mut self, cx: &mut ViewContext<Self>) {
cx.emit(Event::Dismissed)
}

View File

@ -96,6 +96,10 @@ lazy_static! {
.and_then(parse_pixel_position_env_var);
}
pub trait Modal: View {
fn dismiss_on_event(event: &Self::Event) -> bool;
}
#[derive(Clone, PartialEq)]
pub struct RemoveWorktreeFromProject(pub WorktreeId);
@ -1335,7 +1339,7 @@ impl Workspace {
add_view: F,
) -> Option<ViewHandle<V>>
where
V: 'static + View,
V: 'static + Modal,
F: FnOnce(&mut Self, &mut ViewContext<Self>) -> ViewHandle<V>,
{
cx.notify();
@ -1347,6 +1351,12 @@ impl Workspace {
Some(already_open_modal)
} else {
let modal = add_view(self, cx);
cx.subscribe(&modal, |this, _, event, cx| {
if V::dismiss_on_event(event) {
this.dismiss_modal(cx);
}
})
.detach();
cx.focus(&modal);
self.modal = Some(modal.into_any());
None