Merge branch 'main' into channel-guests

This commit is contained in:
Conrad Irwin 2024-01-05 10:02:22 -07:00
commit 3c0052850c
79 changed files with 3501 additions and 3265 deletions

View file

@ -223,6 +223,7 @@ impl Render for BufferSearchBar {
.gap_2()
.border_1()
.border_color(editor_border)
.min_w(rems(384. / 16.))
.rounded_lg()
.child(IconElement::new(Icon::MagnifyingGlass))
.child(self.render_text_input(&self.query_editor, cx))
@ -422,89 +423,134 @@ 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| {
type GetSearchBar<T> =
for<'a, 'b> fn(&'a T, &'a mut ViewContext<'b, T>) -> Option<View<BufferSearchBar>>;
/// Registers search actions on a div that can be taken out.
pub struct DivRegistrar<'a, 'b, T: 'static> {
div: Option<Div>,
cx: &'a mut ViewContext<'b, T>,
search_getter: GetSearchBar<T>,
}
impl<'a, 'b, T: 'static> DivRegistrar<'a, 'b, T> {
pub fn new(search_getter: GetSearchBar<T>, cx: &'a mut ViewContext<'b, T>) -> Self {
Self {
div: Some(div()),
cx,
search_getter,
}
}
pub fn into_div(self) -> Div {
// This option is always Some; it's an option in the first place because we want to call methods
// on div that require ownership.
self.div.unwrap()
}
}
impl<T: 'static> SearchActionsRegistrar for DivRegistrar<'_, '_, T> {
fn register_handler<A: gpui::Action>(
&mut self,
callback: fn(&mut BufferSearchBar, &A, &mut ViewContext<BufferSearchBar>),
) {
let getter = self.search_getter;
self.div = self.div.take().map(|div| {
div.on_action(self.cx.listener(move |this, action, cx| {
(getter)(this, cx)
.clone()
.map(|search_bar| search_bar.update(cx, |this, cx| callback(this, action, cx)));
}))
});
}
}
/// Register actions for an active pane.
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, |this, cx| {
this.deploy(deploy, cx);
});
return;
search_bar.update(cx, move |this, cx| callback(this, action, cx));
cx.notify();
}
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();
})
});
});
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();
}
})
});
});
}
register_action(workspace, |this, action: &ToggleCaseSensitive, cx| {
}
}
impl BufferSearchBar {
pub fn register_inner(registrar: &mut impl SearchActionsRegistrar) {
registrar.register_handler(|this, action: &ToggleCaseSensitive, cx| {
if this.supported_options().case {
this.toggle_case_sensitive(action, cx);
}
});
register_action(workspace, |this, action: &ToggleWholeWord, cx| {
registrar.register_handler(|this, action: &ToggleWholeWord, cx| {
if this.supported_options().word {
this.toggle_whole_word(action, cx);
}
});
register_action(workspace, |this, action: &ToggleReplace, cx| {
registrar.register_handler(|this, action: &ToggleReplace, cx| {
if this.supported_options().replacement {
this.toggle_replace(action, cx);
}
});
register_action(workspace, |this, _: &ActivateRegexMode, cx| {
registrar.register_handler(|this, _: &ActivateRegexMode, cx| {
if this.supported_options().regex {
this.activate_search_mode(SearchMode::Regex, cx);
}
});
register_action(workspace, |this, _: &ActivateTextMode, cx| {
registrar.register_handler(|this, _: &ActivateTextMode, cx| {
this.activate_search_mode(SearchMode::Text, cx);
});
register_action(workspace, |this, action: &CycleMode, cx| {
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)
}
});
register_action(workspace, |this, action: &SelectNextMatch, 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) {
Self::register_inner(workspace);
}
pub fn new(cx: &mut ViewContext<Self>) -> Self {
let query_editor = cx.new_view(|cx| Editor::single_line(cx));

View file

@ -38,8 +38,8 @@ use std::{
use theme::ThemeSettings;
use ui::{
h_stack, prelude::*, v_stack, Button, Icon, IconButton, IconElement, Label, LabelCommon,
LabelSize, Selectable, Tooltip,
h_stack, prelude::*, v_stack, Icon, IconButton, IconElement, Label, LabelCommon, LabelSize,
Selectable, ToggleButton, Tooltip,
};
use util::{paths::PathMatcher, ResultExt as _};
use workspace::{
@ -61,12 +61,12 @@ struct ActiveSearches(HashMap<WeakModel<Project>, WeakView<ProjectSearchView>>);
struct ActiveSettings(HashMap<WeakModel<Project>, ProjectSearchSettings>);
pub fn init(cx: &mut AppContext) {
// todo!() po
cx.set_global(ActiveSearches::default());
cx.set_global(ActiveSettings::default());
cx.observe_new_views(|workspace: &mut Workspace, _cx| {
workspace
.register_action(ProjectSearchView::deploy)
.register_action(ProjectSearchView::new_search)
.register_action(ProjectSearchView::deploy_search)
.register_action(ProjectSearchBar::search_in_new);
})
.detach();
@ -288,7 +288,6 @@ impl Render for ProjectSearchView {
.size_full()
.track_focus(&self.focus_handle)
.child(self.results_editor.clone())
.into_any()
} else {
let model = self.model.read(cx);
let has_no_results = model.no_results.unwrap_or(false);
@ -365,6 +364,7 @@ impl Render for ProjectSearchView {
.flex_1()
.size_full()
.justify_center()
.bg(cx.theme().colors().editor_background)
.track_focus(&self.focus_handle)
.child(
h_stack()
@ -374,7 +374,6 @@ impl Render for ProjectSearchView {
.child(v_stack().child(major_text).children(minor_text))
.child(h_stack().flex_1()),
)
.into_any()
}
}
}
@ -943,11 +942,41 @@ impl ProjectSearchView {
});
}
// Re-activate the most recently activated search or the most recent if it has been closed.
// If no search exists in the workspace, create a new one.
fn deploy_search(
workspace: &mut Workspace,
_: &workspace::DeploySearch,
cx: &mut ViewContext<Workspace>,
) {
let active_search = cx
.global::<ActiveSearches>()
.0
.get(&workspace.project().downgrade());
let existing = active_search
.and_then(|active_search| {
workspace
.items_of_type::<ProjectSearchView>(cx)
.filter(|search| &search.downgrade() == active_search)
.last()
})
.or_else(|| workspace.item_of_type::<ProjectSearchView>(cx));
Self::existing_or_new_search(workspace, existing, cx)
}
// Add another search tab to the workspace.
fn deploy(
fn new_search(
workspace: &mut Workspace,
_: &workspace::NewSearch,
cx: &mut ViewContext<Workspace>,
) {
Self::existing_or_new_search(workspace, None, cx)
}
fn existing_or_new_search(
workspace: &mut Workspace,
existing: Option<View<ProjectSearchView>>,
cx: &mut ViewContext<Workspace>,
) {
// Clean up entries for dropped projects
cx.update_global(|state: &mut ActiveSearches, _cx| {
@ -964,19 +993,27 @@ impl ProjectSearchView {
}
});
let settings = cx
.global::<ActiveSettings>()
.0
.get(&workspace.project().downgrade());
let settings = if let Some(settings) = settings {
Some(settings.clone())
let search = if let Some(existing) = existing {
workspace.activate_item(&existing, cx);
existing
} else {
None
};
let settings = cx
.global::<ActiveSettings>()
.0
.get(&workspace.project().downgrade());
let model = cx.new_model(|cx| ProjectSearch::new(workspace.project().clone(), cx));
let search = cx.new_view(|cx| ProjectSearchView::new(model, cx, settings));
let settings = if let Some(settings) = settings {
Some(settings.clone())
} else {
None
};
let model = cx.new_model(|cx| ProjectSearch::new(workspace.project().clone(), cx));
let view = cx.new_view(|cx| ProjectSearchView::new(model, cx, settings));
workspace.add_item(Box::new(view.clone()), cx);
view
};
workspace.add_item(Box::new(search.clone()), cx);
@ -1641,20 +1678,26 @@ impl Render for ProjectSearchBar {
let mode_column = v_stack().items_start().justify_start().child(
h_stack()
.gap_2()
.child(
h_stack()
.child(
Button::new("project-search-text-button", "Text")
ToggleButton::new("project-search-text-button", "Text")
.style(ButtonStyle::Filled)
.size(ButtonSize::Large)
.selected(search.current_mode == SearchMode::Text)
.on_click(cx.listener(|this, _, cx| {
this.activate_search_mode(SearchMode::Text, cx)
}))
.tooltip(|cx| {
Tooltip::for_action("Toggle text search", &ActivateTextMode, cx)
}),
})
.first(),
)
.child(
Button::new("project-search-regex-button", "Regex")
ToggleButton::new("project-search-regex-button", "Regex")
.style(ButtonStyle::Filled)
.size(ButtonSize::Large)
.selected(search.current_mode == SearchMode::Regex)
.on_click(cx.listener(|this, _, cx| {
this.activate_search_mode(SearchMode::Regex, cx)
@ -1665,11 +1708,20 @@ impl Render for ProjectSearchBar {
&ActivateRegexMode,
cx,
)
})
.map(|this| {
if semantic_is_available {
this.middle()
} else {
this.last()
}
}),
)
.when(semantic_is_available, |this| {
this.child(
Button::new("project-search-semantic-button", "Semantic")
ToggleButton::new("project-search-semantic-button", "Semantic")
.style(ButtonStyle::Filled)
.size(ButtonSize::Large)
.selected(search.current_mode == SearchMode::Semantic)
.on_click(cx.listener(|this, _, cx| {
this.activate_search_mode(SearchMode::Semantic, cx)
@ -1680,7 +1732,8 @@ impl Render for ProjectSearchBar {
&ActivateSemanticMode,
cx,
)
}),
})
.last(),
)
}),
)
@ -1831,6 +1884,7 @@ impl Render for ProjectSearchBar {
.child(
h_stack()
.justify_between()
.gap_2()
.child(query_column)
.child(mode_column)
.child(replace_column)
@ -2062,7 +2116,7 @@ pub mod tests {
}
#[gpui::test]
async fn test_project_search_focus(cx: &mut TestAppContext) {
async fn test_deploy_project_search_focus(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.background_executor.clone());
@ -2103,7 +2157,237 @@ pub mod tests {
.update(cx, |toolbar, cx| toolbar.add_item(search_bar, cx))
});
ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx)
ProjectSearchView::deploy_search(workspace, &workspace::DeploySearch, cx)
})
.unwrap();
let Some(search_view) = cx.read(|cx| {
workspace
.read(cx)
.unwrap()
.active_pane()
.read(cx)
.active_item()
.and_then(|item| item.downcast::<ProjectSearchView>())
}) else {
panic!("Search view expected to appear after new search event trigger")
};
cx.spawn(|mut cx| async move {
window
.update(&mut cx, |_, cx| {
cx.dispatch_action(ToggleFocus.boxed_clone())
})
.unwrap();
})
.detach();
cx.background_executor.run_until_parked();
window
.update(cx, |_, cx| {
search_view.update(cx, |search_view, cx| {
assert!(
search_view.query_editor.focus_handle(cx).is_focused(cx),
"Empty search view should be focused after the toggle focus event: no results panel to focus on",
);
});
}).unwrap();
window
.update(cx, |_, cx| {
search_view.update(cx, |search_view, cx| {
let query_editor = &search_view.query_editor;
assert!(
query_editor.focus_handle(cx).is_focused(cx),
"Search view should be focused after the new search view is activated",
);
let query_text = query_editor.read(cx).text(cx);
assert!(
query_text.is_empty(),
"New search query should be empty but got '{query_text}'",
);
let results_text = search_view
.results_editor
.update(cx, |editor, cx| editor.display_text(cx));
assert!(
results_text.is_empty(),
"Empty search view should have no results but got '{results_text}'"
);
});
})
.unwrap();
window
.update(cx, |_, cx| {
search_view.update(cx, |search_view, cx| {
search_view.query_editor.update(cx, |query_editor, cx| {
query_editor.set_text("sOMETHINGtHATsURELYdOESnOTeXIST", cx)
});
search_view.search(cx);
});
})
.unwrap();
cx.background_executor.run_until_parked();
window
.update(cx, |_, cx| {
search_view.update(cx, |search_view, cx| {
let results_text = search_view
.results_editor
.update(cx, |editor, cx| editor.display_text(cx));
assert!(
results_text.is_empty(),
"Search view for mismatching query should have no results but got '{results_text}'"
);
assert!(
search_view.query_editor.focus_handle(cx).is_focused(cx),
"Search view should be focused after mismatching query had been used in search",
);
});
}).unwrap();
cx.spawn(|mut cx| async move {
window.update(&mut cx, |_, cx| {
cx.dispatch_action(ToggleFocus.boxed_clone())
})
})
.detach();
cx.background_executor.run_until_parked();
window.update(cx, |_, cx| {
search_view.update(cx, |search_view, cx| {
assert!(
search_view.query_editor.focus_handle(cx).is_focused(cx),
"Search view with mismatching query should be focused after the toggle focus event: still no results panel to focus on",
);
});
}).unwrap();
window
.update(cx, |_, cx| {
search_view.update(cx, |search_view, cx| {
search_view
.query_editor
.update(cx, |query_editor, cx| query_editor.set_text("TWO", cx));
search_view.search(cx);
});
})
.unwrap();
cx.background_executor.run_until_parked();
window.update(cx, |_, cx| {
search_view.update(cx, |search_view, cx| {
assert_eq!(
search_view
.results_editor
.update(cx, |editor, cx| editor.display_text(cx)),
"\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;",
"Search view results should match the query"
);
assert!(
search_view.results_editor.focus_handle(cx).is_focused(cx),
"Search view with mismatching query should be focused after search results are available",
);
});
}).unwrap();
cx.spawn(|mut cx| async move {
window
.update(&mut cx, |_, cx| {
cx.dispatch_action(ToggleFocus.boxed_clone())
})
.unwrap();
})
.detach();
cx.background_executor.run_until_parked();
window.update(cx, |_, cx| {
search_view.update(cx, |search_view, cx| {
assert!(
search_view.results_editor.focus_handle(cx).is_focused(cx),
"Search view with matching query should still have its results editor focused after the toggle focus event",
);
});
}).unwrap();
workspace
.update(cx, |workspace, cx| {
ProjectSearchView::deploy_search(workspace, &workspace::DeploySearch, cx)
})
.unwrap();
window.update(cx, |_, cx| {
search_view.update(cx, |search_view, cx| {
assert_eq!(search_view.query_editor.read(cx).text(cx), "two", "Query should be updated to first search result after search view 2nd open in a row");
assert_eq!(
search_view
.results_editor
.update(cx, |editor, cx| editor.display_text(cx)),
"\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;",
"Results should be unchanged after search view 2nd open in a row"
);
assert!(
search_view.query_editor.focus_handle(cx).is_focused(cx),
"Focus should be moved into query editor again after search view 2nd open in a row"
);
});
}).unwrap();
cx.spawn(|mut cx| async move {
window
.update(&mut cx, |_, cx| {
cx.dispatch_action(ToggleFocus.boxed_clone())
})
.unwrap();
})
.detach();
cx.background_executor.run_until_parked();
window.update(cx, |_, cx| {
search_view.update(cx, |search_view, cx| {
assert!(
search_view.results_editor.focus_handle(cx).is_focused(cx),
"Search view with matching query should switch focus to the results editor after the toggle focus event",
);
});
}).unwrap();
}
#[gpui::test]
async fn test_new_project_search_focus(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.background_executor.clone());
fs.insert_tree(
"/dir",
json!({
"one.rs": "const ONE: usize = 1;",
"two.rs": "const TWO: usize = one::ONE + one::ONE;",
"three.rs": "const THREE: usize = one::ONE + two::TWO;",
"four.rs": "const FOUR: usize = one::ONE + three::THREE;",
}),
)
.await;
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
let window = cx.add_window(|cx| Workspace::test_new(project, cx));
let workspace = window.clone();
let search_bar = window.build_view(cx, |_| ProjectSearchBar::new());
let active_item = cx.read(|cx| {
workspace
.read(cx)
.unwrap()
.active_pane()
.read(cx)
.active_item()
.and_then(|item| item.downcast::<ProjectSearchView>())
});
assert!(
active_item.is_none(),
"Expected no search panel to be active"
);
window
.update(cx, move |workspace, cx| {
assert_eq!(workspace.panes().len(), 1);
workspace.panes()[0].update(cx, move |pane, cx| {
pane.toolbar()
.update(cx, |toolbar, cx| toolbar.add_item(search_bar, cx))
});
ProjectSearchView::new_search(workspace, &workspace::NewSearch, cx)
})
.unwrap();
@ -2252,7 +2536,7 @@ pub mod tests {
workspace
.update(cx, |workspace, cx| {
ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx)
ProjectSearchView::new_search(workspace, &workspace::NewSearch, cx)
})
.unwrap();
cx.background_executor.run_until_parked();
@ -2538,7 +2822,7 @@ pub mod tests {
.update(cx, |toolbar, cx| toolbar.add_item(search_bar, cx))
});
ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx)
ProjectSearchView::new_search(workspace, &workspace::NewSearch, cx)
}
})
.unwrap();