Explore registrar-based API for search bar.

This commit adds a Registrar trait for use by search crate. Registrar can register actions on some target and search can utilize that trait to opaquely add actions on that target.
Notably, search is now opt-in (it always was in zed2 actually). Having editor doesn't make it searchable straight out of the gate. You might have to call BufferSearchBar::new a bunch more.
This commit is contained in:
Piotr Osiewicz 2024-01-04 14:56:35 +01:00
parent cd0b15e23d
commit f70eddc988
2 changed files with 125 additions and 69 deletions

View file

@ -422,89 +422,115 @@ impl ToolbarItemView for BufferSearchBar {
}
}
impl BufferSearchBar {
fn register(workspace: &mut Workspace) {
workspace.register_action(move |workspace, deploy: &Deploy, cx| {
let pane = workspace.active_pane();
/// Registrar inverts the dependency between search and it's downstream user, allowing said downstream user to register search action without knowing exactly what those actions are.
pub trait SearchActionsRegistrar {
fn register_handler<A: Action>(
&mut self,
callback: fn(&mut BufferSearchBar, &A, &mut ViewContext<BufferSearchBar>),
);
}
pane.update(cx, |this, cx| {
this.toolbar().update(cx, |this, cx| {
if let Some(search_bar) = this.item_of_type::<BufferSearchBar>() {
search_bar.update(cx, |this, cx| {
this.deploy(deploy, cx);
});
return;
}
let view = cx.new_view(|cx| BufferSearchBar::new(cx));
this.add_item(view.clone(), cx);
view.update(cx, |this, cx| this.deploy(deploy, cx));
cx.notify();
})
impl BufferSearchBar {
pub fn register_inner(
registrar: &mut impl SearchActionsRegistrar,
supported_options: &workspace::searchable::SearchOptions,
) {
// supported_options controls whether the action is registered in the first place,
// but we still perform dynamic checks as e.g. if a view (like Workspace) uses SearchableItemHandle, they cannot know in advance
// whether a given option is supported.
if supported_options.case {
registrar.register_handler(|this, action: &ToggleCaseSensitive, cx| {
if this.supported_options().case {
this.toggle_case_sensitive(action, cx);
}
});
});
fn register_action<A: Action>(
workspace: &mut Workspace,
update: fn(&mut BufferSearchBar, &A, &mut ViewContext<BufferSearchBar>),
) {
workspace.register_action(move |workspace, action: &A, cx| {
let pane = workspace.active_pane();
pane.update(cx, move |this, cx| {
this.toolbar().update(cx, move |this, cx| {
if let Some(search_bar) = this.item_of_type::<BufferSearchBar>() {
search_bar.update(cx, move |this, cx| update(this, action, cx));
cx.notify();
}
})
});
}
if supported_options.word {
registrar.register_handler(|this, action: &ToggleWholeWord, cx| {
if this.supported_options().word {
this.toggle_whole_word(action, cx);
}
});
}
register_action(workspace, |this, action: &ToggleCaseSensitive, cx| {
if this.supported_options().case {
this.toggle_case_sensitive(action, cx);
}
});
register_action(workspace, |this, action: &ToggleWholeWord, cx| {
if this.supported_options().word {
this.toggle_whole_word(action, cx);
}
});
register_action(workspace, |this, action: &ToggleReplace, cx| {
if this.supported_options().replacement {
this.toggle_replace(action, cx);
}
});
register_action(workspace, |this, _: &ActivateRegexMode, cx| {
if this.supported_options().regex {
this.activate_search_mode(SearchMode::Regex, cx);
}
});
register_action(workspace, |this, _: &ActivateTextMode, cx| {
if supported_options.replacement {
registrar.register_handler(|this, action: &ToggleReplace, cx| {
if this.supported_options().replacement {
this.toggle_replace(action, cx);
}
});
}
if supported_options.regex {
registrar.register_handler(|this, _: &ActivateRegexMode, cx| {
if this.supported_options().regex {
this.activate_search_mode(SearchMode::Regex, cx);
}
});
}
registrar.register_handler(|this, _: &ActivateTextMode, cx| {
this.activate_search_mode(SearchMode::Text, cx);
});
register_action(workspace, |this, action: &CycleMode, cx| {
if this.supported_options().regex {
// If regex is not supported then search has just one mode (text) - in that case there's no point in supporting
// cycling.
this.cycle_mode(action, cx)
}
});
register_action(workspace, |this, action: &SelectNextMatch, cx| {
if supported_options.regex {
registrar.register_handler(|this, action: &CycleMode, cx| {
if this.supported_options().regex {
// If regex is not supported then search has just one mode (text) - in that case there's no point in supporting
// cycling.
this.cycle_mode(action, cx)
}
});
}
registrar.register_handler(|this, action: &SelectNextMatch, cx| {
this.select_next_match(action, cx);
});
register_action(workspace, |this, action: &SelectPrevMatch, cx| {
registrar.register_handler(|this, action: &SelectPrevMatch, cx| {
this.select_prev_match(action, cx);
});
register_action(workspace, |this, action: &SelectAllMatches, cx| {
registrar.register_handler(|this, action: &SelectAllMatches, cx| {
this.select_all_matches(action, cx);
});
register_action(workspace, |this, _: &editor::Cancel, cx| {
registrar.register_handler(|this, _: &editor::Cancel, cx| {
if !this.dismissed {
this.dismiss(&Dismiss, cx);
return;
}
cx.propagate();
});
registrar.register_handler(|this, deploy, cx| {
this.deploy(deploy, cx);
})
}
fn register(workspace: &mut Workspace) {
impl SearchActionsRegistrar for Workspace {
fn register_handler<A: Action>(
&mut self,
callback: fn(&mut BufferSearchBar, &A, &mut ViewContext<BufferSearchBar>),
) {
self.register_action(move |workspace, action: &A, cx| {
let pane = workspace.active_pane();
pane.update(cx, move |this, cx| {
this.toolbar().update(cx, move |this, cx| {
if let Some(search_bar) = this.item_of_type::<BufferSearchBar>() {
search_bar.update(cx, move |this, cx| callback(this, action, cx));
cx.notify();
}
})
});
});
}
}
Self::register_inner(
workspace,
&workspace::searchable::SearchOptions {
case: true,
word: true,
regex: true,
replacement: true,
},
);
}
pub fn new(cx: &mut ViewContext<Self>) -> Self {
let query_editor = cx.new_view(|cx| Editor::single_line(cx));

View file

@ -3,11 +3,13 @@ use std::{path::PathBuf, sync::Arc};
use crate::TerminalView;
use db::kvp::KEY_VALUE_STORE;
use gpui::{
actions, div, serde_json, AppContext, AsyncWindowContext, Entity, EventEmitter, ExternalPaths,
FocusHandle, FocusableView, IntoElement, ParentElement, Pixels, Render, Styled, Subscription,
Task, View, ViewContext, VisualContext, WeakView, WindowContext,
actions, div, serde_json, AppContext, AsyncWindowContext, Div, Entity, EventEmitter,
ExternalPaths, FocusHandle, FocusableView, InteractiveElement, IntoElement, ParentElement,
Pixels, Render, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView,
WindowContext,
};
use project::Fs;
use search::{buffer_search::SearchActionsRegistrar, BufferSearchBar};
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore};
use terminal::terminal_settings::{TerminalDockPosition, TerminalSettings};
@ -17,6 +19,7 @@ use workspace::{
dock::{DockPosition, Panel, PanelEvent},
item::Item,
pane,
searchable::SearchableItem,
ui::Icon,
Pane, Workspace,
};
@ -328,9 +331,36 @@ impl TerminalPanel {
impl EventEmitter<PanelEvent> for TerminalPanel {}
struct ActionsRegistrar<'a, 'b>
where
'b: 'a,
{
div: Option<Div>,
cx: &'a mut ViewContext<'b, TerminalPanel>,
}
impl SearchActionsRegistrar for ActionsRegistrar<'_, '_> {
fn register_handler<A: gpui::Action>(
&mut self,
callback: fn(&mut BufferSearchBar, &A, &mut ViewContext<BufferSearchBar>),
) {
self.div = self.div.take().map(|div| {
div.on_action(self.cx.listener(move |this, action, cx| {
this.pane
.read(cx)
.toolbar()
.read(cx)
.item_of_type::<BufferSearchBar>()
.map(|search_bar| search_bar.update(cx, |this, cx| callback(this, action, cx)));
}))
});
}
}
impl Render for TerminalPanel {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
div().size_full().child(self.pane.clone())
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let div = div();
let mut registrar = ActionsRegistrar { div: Some(div), cx };
BufferSearchBar::register_inner(&mut registrar, &TerminalView::supported_options());
registrar.div.unwrap().size_full().child(self.pane.clone())
}
}