Merge branch 'main' into project_search2
This commit is contained in:
@@ -10,15 +10,15 @@ use collections::HashMap;
|
||||
use editor::{Editor, EditorMode};
|
||||
use futures::channel::oneshot;
|
||||
use gpui::{
|
||||
actions, div, red, Action, AppContext, Div, EventEmitter, FocusableView,
|
||||
InteractiveElement as _, IntoElement, ParentElement as _, Render, Styled, Subscription, Task,
|
||||
View, ViewContext, VisualContext as _, WeakView, WindowContext,
|
||||
actions, div, impl_actions, red, Action, AppContext, Div, EventEmitter, FocusableView,
|
||||
InteractiveElement as _, IntoElement, KeyContext, ParentElement as _, Render, Styled,
|
||||
Subscription, Task, View, ViewContext, VisualContext as _, WeakView, WindowContext,
|
||||
};
|
||||
use project::search::SearchQuery;
|
||||
use serde::Deserialize;
|
||||
use std::{any::Any, sync::Arc};
|
||||
|
||||
use ui::{h_stack, Clickable, Icon, IconButton, IconElement};
|
||||
use ui::{h_stack, ButtonCommon, Clickable, Icon, IconButton, IconElement, Tooltip};
|
||||
use util::ResultExt;
|
||||
use workspace::{
|
||||
item::ItemHandle,
|
||||
@@ -26,12 +26,14 @@ use workspace::{
|
||||
ToolbarItemLocation, ToolbarItemView,
|
||||
};
|
||||
|
||||
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
|
||||
#[derive(PartialEq, Clone, Deserialize)]
|
||||
pub struct Deploy {
|
||||
pub focus: bool,
|
||||
}
|
||||
|
||||
actions!(Dismiss, FocusEditor);
|
||||
impl_actions!(buffer_search, [Deploy]);
|
||||
|
||||
actions!(buffer_search, [Dismiss, FocusEditor]);
|
||||
|
||||
pub enum Event {
|
||||
UpdateLocation,
|
||||
@@ -131,13 +133,7 @@ impl Render for BufferSearchBar {
|
||||
let search_button_for_mode = |mode| {
|
||||
let is_active = self.current_mode == mode;
|
||||
|
||||
render_search_mode_button(
|
||||
mode,
|
||||
is_active,
|
||||
cx.listener(move |this, _, cx| {
|
||||
this.activate_search_mode(mode, cx);
|
||||
}),
|
||||
)
|
||||
render_search_mode_button(mode, is_active)
|
||||
};
|
||||
let search_option_button = |option| {
|
||||
let is_active = self.search_options.contains(option);
|
||||
@@ -163,23 +159,35 @@ impl Render for BufferSearchBar {
|
||||
});
|
||||
let should_show_replace_input = self.replace_enabled && supported_options.replacement;
|
||||
let replace_all = should_show_replace_input
|
||||
.then(|| super::render_replace_button(ReplaceAll, ui::Icon::ReplaceAll));
|
||||
let replace_next = should_show_replace_input
|
||||
.then(|| super::render_replace_button(ReplaceNext, ui::Icon::ReplaceNext));
|
||||
.then(|| super::render_replace_button(ReplaceAll, ui::Icon::ReplaceAll, "Replace all"));
|
||||
let replace_next = should_show_replace_input.then(|| {
|
||||
super::render_replace_button(ReplaceNext, ui::Icon::ReplaceNext, "Replace next")
|
||||
});
|
||||
let in_replace = self.replacement_editor.focus_handle(cx).is_focused(cx);
|
||||
|
||||
let mut key_context = KeyContext::default();
|
||||
key_context.add("BufferSearchBar");
|
||||
if in_replace {
|
||||
key_context.add("in_replace");
|
||||
}
|
||||
|
||||
h_stack()
|
||||
.key_context("BufferSearchBar")
|
||||
.key_context(key_context)
|
||||
.on_action(cx.listener(Self::previous_history_query))
|
||||
.on_action(cx.listener(Self::next_history_query))
|
||||
.on_action(cx.listener(Self::dismiss))
|
||||
.on_action(cx.listener(Self::select_next_match))
|
||||
.on_action(cx.listener(Self::select_prev_match))
|
||||
.on_action(cx.listener(|this, _: &ActivateRegexMode, cx| {
|
||||
this.activate_search_mode(SearchMode::Regex, cx);
|
||||
}))
|
||||
.on_action(cx.listener(|this, _: &ActivateTextMode, cx| {
|
||||
this.activate_search_mode(SearchMode::Text, cx);
|
||||
}))
|
||||
.when(self.supported_options().replacement, |this| {
|
||||
this.on_action(cx.listener(Self::toggle_replace))
|
||||
.when(in_replace, |this| {
|
||||
this.key_context("in_replace")
|
||||
.on_action(cx.listener(Self::replace_next))
|
||||
this.on_action(cx.listener(Self::replace_next))
|
||||
.on_action(cx.listener(Self::replace_all))
|
||||
})
|
||||
})
|
||||
@@ -238,21 +246,19 @@ impl Render for BufferSearchBar {
|
||||
h_stack()
|
||||
.gap_0p5()
|
||||
.flex_none()
|
||||
.child(self.render_action_button(cx))
|
||||
.child(self.render_action_button())
|
||||
.children(match_count)
|
||||
.child(render_nav_button(
|
||||
ui::Icon::ChevronLeft,
|
||||
self.active_match_index.is_some(),
|
||||
cx.listener(move |this, _, cx| {
|
||||
this.select_prev_match(&Default::default(), cx);
|
||||
}),
|
||||
"Select previous match",
|
||||
&SelectPrevMatch,
|
||||
))
|
||||
.child(render_nav_button(
|
||||
ui::Icon::ChevronRight,
|
||||
self.active_match_index.is_some(),
|
||||
cx.listener(move |this, _, cx| {
|
||||
this.select_next_match(&Default::default(), cx);
|
||||
}),
|
||||
"Select next match",
|
||||
&SelectNextMatch,
|
||||
)),
|
||||
)
|
||||
}
|
||||
@@ -597,14 +603,10 @@ impl BufferSearchBar {
|
||||
self.update_matches(cx)
|
||||
}
|
||||
|
||||
fn render_action_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
// let tooltip_style = theme.tooltip.clone();
|
||||
|
||||
// let style = theme.search.action_button.clone();
|
||||
|
||||
IconButton::new("select-all", ui::Icon::SelectAll).on_click(cx.listener(|this, _, cx| {
|
||||
this.select_all_matches(&SelectAllMatches, cx);
|
||||
}))
|
||||
fn render_action_button(&self) -> impl IntoElement {
|
||||
IconButton::new("select-all", ui::Icon::SelectAll)
|
||||
.on_click(|_, cx| cx.dispatch_action(SelectAllMatches.boxed_clone()))
|
||||
.tooltip(|cx| Tooltip::for_action("Select all matches", &SelectAllMatches, cx))
|
||||
}
|
||||
|
||||
pub fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext<Self>) {
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
use gpui::{Action, SharedString};
|
||||
|
||||
use crate::{ActivateRegexMode, ActivateSemanticMode, ActivateTextMode};
|
||||
|
||||
// TODO: Update the default search mode to get from config
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq)]
|
||||
pub enum SearchMode {
|
||||
@@ -15,6 +19,16 @@ impl SearchMode {
|
||||
SearchMode::Regex => "Regex",
|
||||
}
|
||||
}
|
||||
pub(crate) fn tooltip(&self) -> SharedString {
|
||||
format!("Activate {} Mode", self.label()).into()
|
||||
}
|
||||
pub(crate) fn action(&self) -> Box<dyn Action> {
|
||||
match self {
|
||||
SearchMode::Text => ActivateTextMode.boxed_clone(),
|
||||
SearchMode::Semantic => ActivateSemanticMode.boxed_clone(),
|
||||
SearchMode::Regex => ActivateRegexMode.boxed_clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn next_mode(mode: &SearchMode, semantic_enabled: bool) -> SearchMode {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
use crate::{
|
||||
history::SearchHistory, mode::SearchMode, ActivateRegexMode, ActivateSemanticMode,
|
||||
ActivateTextMode, CycleMode, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext,
|
||||
SearchOptions, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleReplace,
|
||||
ToggleWholeWord,
|
||||
history::SearchHistory, mode::SearchMode, ActivateRegexMode, ActivateTextMode, CycleMode,
|
||||
NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions,
|
||||
SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleReplace, ToggleWholeWord,
|
||||
};
|
||||
use anyhow::{Context as _, Result};
|
||||
use collections::HashMap;
|
||||
@@ -45,7 +44,10 @@ use workspace::{
|
||||
WorkspaceId,
|
||||
};
|
||||
|
||||
actions!(SearchInNew, ToggleFocus, NextField, ToggleFilters,);
|
||||
actions!(
|
||||
project_search,
|
||||
[SearchInNew, ToggleFocus, NextField, ToggleFilters]
|
||||
);
|
||||
|
||||
#[derive(Default)]
|
||||
struct ActiveSearches(HashMap<WeakModel<Project>, WeakView<ProjectSearchView>>);
|
||||
@@ -58,7 +60,9 @@ pub fn init(cx: &mut AppContext) {
|
||||
cx.set_global(ActiveSearches::default());
|
||||
cx.set_global(ActiveSettings::default());
|
||||
cx.observe_new_views(|workspace: &mut Workspace, _cx| {
|
||||
workspace.register_action(ProjectSearchView::deploy);
|
||||
workspace
|
||||
.register_action(ProjectSearchView::deploy)
|
||||
.register_action(ProjectSearchBar::search_in_new);
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
@@ -1007,8 +1011,7 @@ 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.
|
||||
// Add another search tab to the workspace.
|
||||
fn deploy(
|
||||
workspace: &mut Workspace,
|
||||
_: &workspace::NewSearch,
|
||||
@@ -1019,20 +1022,6 @@ impl ProjectSearchView {
|
||||
state.0.retain(|project, _| project.is_upgradable())
|
||||
});
|
||||
|
||||
let active_search = cx
|
||||
.global::<ActiveSearches>()
|
||||
.0
|
||||
.get(&workspace.project().downgrade())
|
||||
.and_then(WeakView::upgrade);
|
||||
|
||||
let existing = active_search
|
||||
.and_then(|active_search| {
|
||||
workspace
|
||||
.items_of_type::<ProjectSearchView>(cx)
|
||||
.find(|search| search == &active_search)
|
||||
})
|
||||
.or_else(|| workspace.item_of_type::<ProjectSearchView>(cx));
|
||||
|
||||
let query = workspace.active_item(cx).and_then(|item| {
|
||||
let editor = item.act_as::<Editor>(cx)?;
|
||||
let query = editor.query_suggestion(cx);
|
||||
@@ -1043,28 +1032,22 @@ impl ProjectSearchView {
|
||||
}
|
||||
});
|
||||
|
||||
let search = if let Some(existing) = existing {
|
||||
workspace.activate_item(&existing, cx);
|
||||
existing
|
||||
let settings = cx
|
||||
.global::<ActiveSettings>()
|
||||
.0
|
||||
.get(&workspace.project().downgrade());
|
||||
|
||||
let settings = if let Some(settings) = settings {
|
||||
Some(settings.clone())
|
||||
} else {
|
||||
let settings = cx
|
||||
.global::<ActiveSettings>()
|
||||
.0
|
||||
.get(&workspace.project().downgrade());
|
||||
|
||||
let settings = if let Some(settings) = settings {
|
||||
Some(settings.clone())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let model = cx.build_model(|cx| ProjectSearch::new(workspace.project().clone(), cx));
|
||||
let view = cx.build_view(|cx| ProjectSearchView::new(model, cx, settings));
|
||||
|
||||
workspace.add_item(Box::new(view.clone()), cx);
|
||||
view
|
||||
None
|
||||
};
|
||||
|
||||
let model = cx.build_model(|cx| ProjectSearch::new(workspace.project().clone(), cx));
|
||||
let search = cx.build_view(|cx| ProjectSearchView::new(model, cx, settings));
|
||||
|
||||
workspace.add_item(Box::new(search.clone()), cx);
|
||||
|
||||
search.update(cx, |search, cx| {
|
||||
if let Some(query) = query {
|
||||
search.set_query(&query, cx);
|
||||
@@ -2436,29 +2419,86 @@ pub mod tests {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx)
|
||||
});
|
||||
deterministic.run_until_parked();
|
||||
let Some(search_view_2) = cx.read(|cx| {
|
||||
workspace
|
||||
.read(cx)
|
||||
.active_pane()
|
||||
.read(cx)
|
||||
.active_item()
|
||||
.and_then(|item| item.downcast::<ProjectSearchView>())
|
||||
}) else {
|
||||
panic!("Search view expected to appear after new search event trigger")
|
||||
};
|
||||
let search_view_id_2 = search_view_2.id();
|
||||
assert_ne!(
|
||||
search_view_2, search_view,
|
||||
"New search view should be open after `workspace::NewSearch` event"
|
||||
);
|
||||
|
||||
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.query_editor.read(cx).text(cx), "TWO", "First search view should not have an updated query");
|
||||
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"
|
||||
"Results of the first search view should not update too"
|
||||
);
|
||||
assert!(
|
||||
search_view.query_editor.is_focused(cx),
|
||||
"Focus should be moved into query editor again after search view 2nd open in a row"
|
||||
!search_view.query_editor.is_focused(cx),
|
||||
"Focus should be moved away from the first search view"
|
||||
);
|
||||
});
|
||||
|
||||
search_view_2.update(cx, |search_view_2, cx| {
|
||||
assert_eq!(
|
||||
search_view_2.query_editor.read(cx).text(cx),
|
||||
"two",
|
||||
"New search view should get the query from the text cursor was at during the event spawn (first search view's first result)"
|
||||
);
|
||||
assert_eq!(
|
||||
search_view_2
|
||||
.results_editor
|
||||
.update(cx, |editor, cx| editor.display_text(cx)),
|
||||
"",
|
||||
"No search results should be in the 2nd view yet, as we did not spawn a search for it"
|
||||
);
|
||||
assert!(
|
||||
search_view_2.query_editor.is_focused(cx),
|
||||
"Focus should be moved into query editor fo the new window"
|
||||
);
|
||||
});
|
||||
|
||||
search_view_2.update(cx, |search_view_2, cx| {
|
||||
search_view_2
|
||||
.query_editor
|
||||
.update(cx, |query_editor, cx| query_editor.set_text("FOUR", cx));
|
||||
search_view_2.search(cx);
|
||||
});
|
||||
deterministic.run_until_parked();
|
||||
search_view_2.update(cx, |search_view_2, cx| {
|
||||
assert_eq!(
|
||||
search_view_2
|
||||
.results_editor
|
||||
.update(cx, |editor, cx| editor.display_text(cx)),
|
||||
"\n\nconst FOUR: usize = one::ONE + three::THREE;",
|
||||
"New search view with the updated query should have new search results"
|
||||
);
|
||||
assert!(
|
||||
search_view_2.results_editor.is_focused(cx),
|
||||
"Search view with mismatching query should be focused after search results are available",
|
||||
);
|
||||
});
|
||||
|
||||
cx.spawn(|mut cx| async move {
|
||||
window.dispatch_action(search_view_id, &ToggleFocus, &mut cx);
|
||||
window.dispatch_action(search_view_id_2, &ToggleFocus, &mut cx);
|
||||
})
|
||||
.detach();
|
||||
deterministic.run_until_parked();
|
||||
search_view.update(cx, |search_view, cx| {
|
||||
search_view_id_2.update(cx, |search_view_2, cx| {
|
||||
assert!(
|
||||
search_view.results_editor.is_focused(cx),
|
||||
search_view_2.results_editor.is_focused(cx),
|
||||
"Search view with matching query should switch focus to the results editor after the toggle focus event",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ pub use buffer_search::BufferSearchBar;
|
||||
use gpui::{actions, Action, AppContext, IntoElement};
|
||||
pub use mode::SearchMode;
|
||||
use project::search::SearchQuery;
|
||||
use ui::prelude::*;
|
||||
use ui::{prelude::*, Tooltip};
|
||||
use ui::{ButtonStyle, Icon, IconButton};
|
||||
//pub use project_search::{ProjectSearchBar, ProjectSearchView};
|
||||
// use theme::components::{
|
||||
@@ -22,20 +22,23 @@ pub fn init(cx: &mut AppContext) {
|
||||
}
|
||||
|
||||
actions!(
|
||||
CycleMode,
|
||||
ToggleWholeWord,
|
||||
ToggleCaseSensitive,
|
||||
ToggleReplace,
|
||||
SelectNextMatch,
|
||||
SelectPrevMatch,
|
||||
SelectAllMatches,
|
||||
NextHistoryQuery,
|
||||
PreviousHistoryQuery,
|
||||
ActivateTextMode,
|
||||
ActivateSemanticMode,
|
||||
ActivateRegexMode,
|
||||
ReplaceAll,
|
||||
ReplaceNext,
|
||||
search,
|
||||
[
|
||||
CycleMode,
|
||||
ToggleWholeWord,
|
||||
ToggleCaseSensitive,
|
||||
ToggleReplace,
|
||||
SelectNextMatch,
|
||||
SelectPrevMatch,
|
||||
SelectAllMatches,
|
||||
NextHistoryQuery,
|
||||
PreviousHistoryQuery,
|
||||
ActivateTextMode,
|
||||
ActivateSemanticMode,
|
||||
ActivateRegexMode,
|
||||
ReplaceAll,
|
||||
ReplaceNext,
|
||||
]
|
||||
);
|
||||
|
||||
bitflags! {
|
||||
@@ -85,7 +88,7 @@ impl SearchOptions {
|
||||
}
|
||||
|
||||
pub fn as_button(&self, active: bool) -> impl IntoElement {
|
||||
IconButton::new(0, self.icon())
|
||||
IconButton::new(self.label(), self.icon())
|
||||
.on_click({
|
||||
let action = self.to_toggle_action();
|
||||
move |_, cx| {
|
||||
@@ -94,26 +97,38 @@ impl SearchOptions {
|
||||
})
|
||||
.style(ButtonStyle::Subtle)
|
||||
.when(active, |button| button.style(ButtonStyle::Filled))
|
||||
.tooltip({
|
||||
let action = self.to_toggle_action();
|
||||
let label: SharedString = format!("Toggle {}", self.label()).into();
|
||||
move |cx| Tooltip::for_action(label.clone(), &*action, cx)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_replace_button(active: bool) -> impl IntoElement {
|
||||
// todo: add toggle_replace button
|
||||
IconButton::new(0, Icon::Replace)
|
||||
IconButton::new("buffer-search-bar-toggle-replace-button", Icon::Replace)
|
||||
.on_click(|_, cx| {
|
||||
cx.dispatch_action(Box::new(ToggleReplace));
|
||||
cx.notify();
|
||||
})
|
||||
.style(ButtonStyle::Subtle)
|
||||
.when(active, |button| button.style(ButtonStyle::Filled))
|
||||
.tooltip(|cx| Tooltip::for_action("Toggle replace", &ToggleReplace, cx))
|
||||
}
|
||||
|
||||
fn render_replace_button(
|
||||
action: impl Action + 'static + Send + Sync,
|
||||
icon: Icon,
|
||||
tooltip: &'static str,
|
||||
) -> impl IntoElement {
|
||||
// todo: add tooltip
|
||||
IconButton::new(0, icon).on_click(move |_, cx| {
|
||||
cx.dispatch_action(action.boxed_clone());
|
||||
})
|
||||
let id: SharedString = format!("search-replace-{}", action.name()).into();
|
||||
IconButton::new(id, icon)
|
||||
.tooltip({
|
||||
let action = action.boxed_clone();
|
||||
move |cx| Tooltip::for_action(tooltip, &*action, cx)
|
||||
})
|
||||
.on_click(move |_, cx| {
|
||||
cx.dispatch_action(action.boxed_clone());
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,30 +1,36 @@
|
||||
use gpui::{ClickEvent, IntoElement, WindowContext};
|
||||
use ui::prelude::*;
|
||||
use gpui::{Action, IntoElement};
|
||||
use ui::{prelude::*, Tooltip};
|
||||
use ui::{Button, IconButton};
|
||||
|
||||
use crate::mode::SearchMode;
|
||||
|
||||
pub(super) fn render_nav_button(
|
||||
icon: ui::Icon,
|
||||
_active: bool,
|
||||
on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
|
||||
active: bool,
|
||||
tooltip: &'static str,
|
||||
action: &'static dyn Action,
|
||||
) -> impl IntoElement {
|
||||
// let tooltip_style = cx.theme().tooltip.clone();
|
||||
// let cursor_style = if active {
|
||||
// CursorStyle::PointingHand
|
||||
// } else {
|
||||
// CursorStyle::default()
|
||||
// };
|
||||
// enum NavButton {}
|
||||
IconButton::new("search-nav-button", icon).on_click(on_click)
|
||||
IconButton::new(
|
||||
SharedString::from(format!("search-nav-button-{}", action.name())),
|
||||
icon,
|
||||
)
|
||||
.on_click(|_, cx| cx.dispatch_action(action.boxed_clone()))
|
||||
.tooltip(move |cx| Tooltip::for_action(tooltip, action, cx))
|
||||
.disabled(!active)
|
||||
}
|
||||
|
||||
pub(crate) fn render_search_mode_button(
|
||||
mode: SearchMode,
|
||||
is_active: bool,
|
||||
on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
|
||||
) -> Button {
|
||||
pub(crate) fn render_search_mode_button(mode: SearchMode, is_active: bool) -> Button {
|
||||
Button::new(mode.label(), mode.label())
|
||||
.selected(is_active)
|
||||
.on_click(on_click)
|
||||
.on_click({
|
||||
let action = mode.action();
|
||||
move |_, cx| {
|
||||
cx.dispatch_action(action.boxed_clone());
|
||||
}
|
||||
})
|
||||
.tooltip({
|
||||
let action = mode.action();
|
||||
let tooltip_text = mode.tooltip();
|
||||
move |cx| Tooltip::for_action(tooltip_text.clone(), &*action, cx)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user