search: Fix some inconsistencies between project and buffer search bars (#36103)
- project search query string now turns red when no results are found matching buffer search behavior - General code deduplication as well as more consistent layout between the two bars, as some minor details have drifted apart - Tab cycling in buffer search now ends up in editor focus when cycling backwards, matching forward cycling - Report parse errors in filter include and exclude editors Release Notes: - N/A
This commit is contained in:
parent
23d0433158
commit
8d6982e78f
4 changed files with 545 additions and 689 deletions
|
@ -3,20 +3,23 @@ mod registrar;
|
|||
use crate::{
|
||||
FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions,
|
||||
SelectAllMatches, SelectNextMatch, SelectPreviousMatch, ToggleCaseSensitive, ToggleRegex,
|
||||
ToggleReplace, ToggleSelection, ToggleWholeWord, search_bar::render_nav_button,
|
||||
ToggleReplace, ToggleSelection, ToggleWholeWord,
|
||||
search_bar::{
|
||||
input_base_styles, render_action_button, render_text_input, toggle_replace_button,
|
||||
},
|
||||
};
|
||||
use any_vec::AnyVec;
|
||||
use anyhow::Context as _;
|
||||
use collections::HashMap;
|
||||
use editor::{
|
||||
DisplayPoint, Editor, EditorElement, EditorSettings, EditorStyle,
|
||||
DisplayPoint, Editor, EditorSettings,
|
||||
actions::{Backtab, Tab},
|
||||
};
|
||||
use futures::channel::oneshot;
|
||||
use gpui::{
|
||||
Action, App, ClickEvent, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
InteractiveElement as _, IntoElement, KeyContext, ParentElement as _, Render, ScrollHandle,
|
||||
Styled, Subscription, Task, TextStyle, Window, actions, div,
|
||||
Action, App, ClickEvent, Context, Entity, EventEmitter, Focusable, InteractiveElement as _,
|
||||
IntoElement, KeyContext, ParentElement as _, Render, ScrollHandle, Styled, Subscription, Task,
|
||||
Window, actions, div,
|
||||
};
|
||||
use language::{Language, LanguageRegistry};
|
||||
use project::{
|
||||
|
@ -27,7 +30,6 @@ use schemars::JsonSchema;
|
|||
use serde::Deserialize;
|
||||
use settings::Settings;
|
||||
use std::sync::Arc;
|
||||
use theme::ThemeSettings;
|
||||
use zed_actions::outline::ToggleOutline;
|
||||
|
||||
use ui::{
|
||||
|
@ -125,46 +127,6 @@ pub struct BufferSearchBar {
|
|||
}
|
||||
|
||||
impl BufferSearchBar {
|
||||
fn render_text_input(
|
||||
&self,
|
||||
editor: &Entity<Editor>,
|
||||
color_override: Option<Color>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let (color, use_syntax) = if editor.read(cx).read_only(cx) {
|
||||
(cx.theme().colors().text_disabled, false)
|
||||
} else {
|
||||
match color_override {
|
||||
Some(color_override) => (color_override.color(cx), false),
|
||||
None => (cx.theme().colors().text, true),
|
||||
}
|
||||
};
|
||||
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let text_style = TextStyle {
|
||||
color,
|
||||
font_family: settings.buffer_font.family.clone(),
|
||||
font_features: settings.buffer_font.features.clone(),
|
||||
font_fallbacks: settings.buffer_font.fallbacks.clone(),
|
||||
font_size: rems(0.875).into(),
|
||||
font_weight: settings.buffer_font.weight,
|
||||
line_height: relative(1.3),
|
||||
..TextStyle::default()
|
||||
};
|
||||
|
||||
let mut editor_style = EditorStyle {
|
||||
background: cx.theme().colors().toolbar_background,
|
||||
local_player: cx.theme().players().local(),
|
||||
text: text_style,
|
||||
..EditorStyle::default()
|
||||
};
|
||||
if use_syntax {
|
||||
editor_style.syntax = cx.theme().syntax().clone();
|
||||
}
|
||||
|
||||
EditorElement::new(editor, editor_style)
|
||||
}
|
||||
|
||||
pub fn query_editor_focused(&self) -> bool {
|
||||
self.query_editor_focused
|
||||
}
|
||||
|
@ -185,7 +147,14 @@ impl Render for BufferSearchBar {
|
|||
let hide_inline_icons = self.editor_needed_width
|
||||
> self.editor_scroll_handle.bounds().size.width - window.rem_size() * 6.;
|
||||
|
||||
let supported_options = self.supported_options(cx);
|
||||
let workspace::searchable::SearchOptions {
|
||||
case,
|
||||
word,
|
||||
regex,
|
||||
replacement,
|
||||
selection,
|
||||
find_in_results,
|
||||
} = self.supported_options(cx);
|
||||
|
||||
if self.query_editor.update(cx, |query_editor, _cx| {
|
||||
query_editor.placeholder_text().is_none()
|
||||
|
@ -220,268 +189,205 @@ impl Render for BufferSearchBar {
|
|||
}
|
||||
})
|
||||
.unwrap_or_else(|| "0/0".to_string());
|
||||
let should_show_replace_input = self.replace_enabled && supported_options.replacement;
|
||||
let should_show_replace_input = self.replace_enabled && replacement;
|
||||
let in_replace = self.replacement_editor.focus_handle(cx).is_focused(window);
|
||||
|
||||
let theme_colors = cx.theme().colors();
|
||||
let query_border = if self.query_error.is_some() {
|
||||
Color::Error.color(cx)
|
||||
} else {
|
||||
theme_colors.border
|
||||
};
|
||||
let replacement_border = theme_colors.border;
|
||||
|
||||
let container_width = window.viewport_size().width;
|
||||
let input_width = SearchInputWidth::calc_width(container_width);
|
||||
|
||||
let input_base_styles =
|
||||
|border_color| input_base_styles(border_color, |div| div.w(input_width));
|
||||
|
||||
let query_column = input_base_styles(query_border)
|
||||
.id("editor-scroll")
|
||||
.track_scroll(&self.editor_scroll_handle)
|
||||
.child(render_text_input(&self.query_editor, color_override, cx))
|
||||
.when(!hide_inline_icons, |div| {
|
||||
div.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.when(case, |div| {
|
||||
div.child(SearchOptions::CASE_SENSITIVE.as_button(
|
||||
self.search_options.contains(SearchOptions::CASE_SENSITIVE),
|
||||
focus_handle.clone(),
|
||||
cx.listener(|this, _, window, cx| {
|
||||
this.toggle_case_sensitive(&ToggleCaseSensitive, window, cx)
|
||||
}),
|
||||
))
|
||||
})
|
||||
.when(word, |div| {
|
||||
div.child(SearchOptions::WHOLE_WORD.as_button(
|
||||
self.search_options.contains(SearchOptions::WHOLE_WORD),
|
||||
focus_handle.clone(),
|
||||
cx.listener(|this, _, window, cx| {
|
||||
this.toggle_whole_word(&ToggleWholeWord, window, cx)
|
||||
}),
|
||||
))
|
||||
})
|
||||
.when(regex, |div| {
|
||||
div.child(SearchOptions::REGEX.as_button(
|
||||
self.search_options.contains(SearchOptions::REGEX),
|
||||
focus_handle.clone(),
|
||||
cx.listener(|this, _, window, cx| {
|
||||
this.toggle_regex(&ToggleRegex, window, cx)
|
||||
}),
|
||||
))
|
||||
}),
|
||||
)
|
||||
});
|
||||
|
||||
let mode_column = h_flex()
|
||||
.gap_1()
|
||||
.min_w_64()
|
||||
.when(replacement, |this| {
|
||||
this.child(toggle_replace_button(
|
||||
"buffer-search-bar-toggle-replace-button",
|
||||
focus_handle.clone(),
|
||||
self.replace_enabled,
|
||||
cx.listener(|this, _: &ClickEvent, window, cx| {
|
||||
this.toggle_replace(&ToggleReplace, window, cx);
|
||||
}),
|
||||
))
|
||||
})
|
||||
.when(selection, |this| {
|
||||
this.child(
|
||||
IconButton::new(
|
||||
"buffer-search-bar-toggle-search-selection-button",
|
||||
IconName::Quote,
|
||||
)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.shape(IconButtonShape::Square)
|
||||
.when(self.selection_search_enabled, |button| {
|
||||
button.style(ButtonStyle::Filled)
|
||||
})
|
||||
.on_click(cx.listener(|this, _: &ClickEvent, window, cx| {
|
||||
this.toggle_selection(&ToggleSelection, window, cx);
|
||||
}))
|
||||
.toggle_state(self.selection_search_enabled)
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Toggle Search Selection",
|
||||
&ToggleSelection,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
}),
|
||||
)
|
||||
})
|
||||
.when(!find_in_results, |el| {
|
||||
let query_focus = self.query_editor.focus_handle(cx);
|
||||
let matches_column = h_flex()
|
||||
.pl_2()
|
||||
.ml_2()
|
||||
.border_l_1()
|
||||
.border_color(theme_colors.border_variant)
|
||||
.child(render_action_button(
|
||||
"buffer-search-nav-button",
|
||||
ui::IconName::ChevronLeft,
|
||||
self.active_match_index.is_some(),
|
||||
"Select Previous Match",
|
||||
&SelectPreviousMatch,
|
||||
query_focus.clone(),
|
||||
))
|
||||
.child(render_action_button(
|
||||
"buffer-search-nav-button",
|
||||
ui::IconName::ChevronRight,
|
||||
self.active_match_index.is_some(),
|
||||
"Select Next Match",
|
||||
&SelectNextMatch,
|
||||
query_focus.clone(),
|
||||
))
|
||||
.when(!narrow_mode, |this| {
|
||||
this.child(div().ml_2().min_w(rems_from_px(40.)).child(
|
||||
Label::new(match_text).size(LabelSize::Small).color(
|
||||
if self.active_match_index.is_some() {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Disabled
|
||||
},
|
||||
),
|
||||
))
|
||||
});
|
||||
|
||||
el.child(render_action_button(
|
||||
"buffer-search-nav-button",
|
||||
IconName::SelectAll,
|
||||
true,
|
||||
"Select All Matches",
|
||||
&SelectAllMatches,
|
||||
query_focus,
|
||||
))
|
||||
.child(matches_column)
|
||||
})
|
||||
.when(find_in_results, |el| {
|
||||
el.child(render_action_button(
|
||||
"buffer-search",
|
||||
IconName::Close,
|
||||
true,
|
||||
"Close Search Bar",
|
||||
&Dismiss,
|
||||
focus_handle.clone(),
|
||||
))
|
||||
});
|
||||
|
||||
let search_line = h_flex()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.when(find_in_results, |el| {
|
||||
el.child(Label::new("Find in results").color(Color::Hint))
|
||||
})
|
||||
.child(query_column)
|
||||
.child(mode_column);
|
||||
|
||||
let replace_line =
|
||||
should_show_replace_input.then(|| {
|
||||
let replace_column = input_base_styles(replacement_border)
|
||||
.child(render_text_input(&self.replacement_editor, None, cx));
|
||||
let focus_handle = self.replacement_editor.read(cx).focus_handle(cx);
|
||||
|
||||
let replace_actions = h_flex()
|
||||
.min_w_64()
|
||||
.gap_1()
|
||||
.child(render_action_button(
|
||||
"buffer-search-replace-button",
|
||||
IconName::ReplaceNext,
|
||||
true,
|
||||
"Replace Next Match",
|
||||
&ReplaceNext,
|
||||
focus_handle.clone(),
|
||||
))
|
||||
.child(render_action_button(
|
||||
"buffer-search-replace-button",
|
||||
IconName::ReplaceAll,
|
||||
true,
|
||||
"Replace All Matches",
|
||||
&ReplaceAll,
|
||||
focus_handle,
|
||||
));
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.child(replace_column)
|
||||
.child(replace_actions)
|
||||
});
|
||||
|
||||
let mut key_context = KeyContext::new_with_defaults();
|
||||
key_context.add("BufferSearchBar");
|
||||
if in_replace {
|
||||
key_context.add("in_replace");
|
||||
}
|
||||
let query_border = if self.query_error.is_some() {
|
||||
Color::Error.color(cx)
|
||||
} else {
|
||||
cx.theme().colors().border
|
||||
};
|
||||
let replacement_border = cx.theme().colors().border;
|
||||
|
||||
let container_width = window.viewport_size().width;
|
||||
let input_width = SearchInputWidth::calc_width(container_width);
|
||||
|
||||
let input_base_styles = |border_color| {
|
||||
h_flex()
|
||||
.min_w_32()
|
||||
.w(input_width)
|
||||
.h_8()
|
||||
.pl_2()
|
||||
.pr_1()
|
||||
.py_1()
|
||||
.border_1()
|
||||
.border_color(border_color)
|
||||
.rounded_lg()
|
||||
};
|
||||
|
||||
let search_line = h_flex()
|
||||
.gap_2()
|
||||
.when(supported_options.find_in_results, |el| {
|
||||
el.child(Label::new("Find in results").color(Color::Hint))
|
||||
})
|
||||
.child(
|
||||
input_base_styles(query_border)
|
||||
.id("editor-scroll")
|
||||
.track_scroll(&self.editor_scroll_handle)
|
||||
.child(self.render_text_input(&self.query_editor, color_override, cx))
|
||||
.when(!hide_inline_icons, |div| {
|
||||
div.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.children(supported_options.case.then(|| {
|
||||
self.render_search_option_button(
|
||||
SearchOptions::CASE_SENSITIVE,
|
||||
focus_handle.clone(),
|
||||
cx.listener(|this, _, window, cx| {
|
||||
this.toggle_case_sensitive(
|
||||
&ToggleCaseSensitive,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
)
|
||||
}))
|
||||
.children(supported_options.word.then(|| {
|
||||
self.render_search_option_button(
|
||||
SearchOptions::WHOLE_WORD,
|
||||
focus_handle.clone(),
|
||||
cx.listener(|this, _, window, cx| {
|
||||
this.toggle_whole_word(&ToggleWholeWord, window, cx)
|
||||
}),
|
||||
)
|
||||
}))
|
||||
.children(supported_options.regex.then(|| {
|
||||
self.render_search_option_button(
|
||||
SearchOptions::REGEX,
|
||||
focus_handle.clone(),
|
||||
cx.listener(|this, _, window, cx| {
|
||||
this.toggle_regex(&ToggleRegex, window, cx)
|
||||
}),
|
||||
)
|
||||
})),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.min_w_64()
|
||||
.when(supported_options.replacement, |this| {
|
||||
this.child(
|
||||
IconButton::new(
|
||||
"buffer-search-bar-toggle-replace-button",
|
||||
IconName::Replace,
|
||||
)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.shape(IconButtonShape::Square)
|
||||
.when(self.replace_enabled, |button| {
|
||||
button.style(ButtonStyle::Filled)
|
||||
})
|
||||
.on_click(cx.listener(|this, _: &ClickEvent, window, cx| {
|
||||
this.toggle_replace(&ToggleReplace, window, cx);
|
||||
}))
|
||||
.toggle_state(self.replace_enabled)
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Toggle Replace",
|
||||
&ToggleReplace,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
}),
|
||||
)
|
||||
})
|
||||
.when(supported_options.selection, |this| {
|
||||
this.child(
|
||||
IconButton::new(
|
||||
"buffer-search-bar-toggle-search-selection-button",
|
||||
IconName::Quote,
|
||||
)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.shape(IconButtonShape::Square)
|
||||
.when(self.selection_search_enabled, |button| {
|
||||
button.style(ButtonStyle::Filled)
|
||||
})
|
||||
.on_click(cx.listener(|this, _: &ClickEvent, window, cx| {
|
||||
this.toggle_selection(&ToggleSelection, window, cx);
|
||||
}))
|
||||
.toggle_state(self.selection_search_enabled)
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Toggle Search Selection",
|
||||
&ToggleSelection,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
}),
|
||||
)
|
||||
})
|
||||
.when(!supported_options.find_in_results, |el| {
|
||||
el.child(
|
||||
IconButton::new("select-all", ui::IconName::SelectAll)
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(SelectAllMatches.boxed_clone(), cx)
|
||||
})
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Select All Matches",
|
||||
&SelectAllMatches,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.pl_2()
|
||||
.ml_1()
|
||||
.border_l_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.child(render_nav_button(
|
||||
ui::IconName::ChevronLeft,
|
||||
self.active_match_index.is_some(),
|
||||
"Select Previous Match",
|
||||
&SelectPreviousMatch,
|
||||
focus_handle.clone(),
|
||||
))
|
||||
.child(render_nav_button(
|
||||
ui::IconName::ChevronRight,
|
||||
self.active_match_index.is_some(),
|
||||
"Select Next Match",
|
||||
&SelectNextMatch,
|
||||
focus_handle.clone(),
|
||||
)),
|
||||
)
|
||||
.when(!narrow_mode, |this| {
|
||||
this.child(h_flex().ml_2().min_w(rems_from_px(40.)).child(
|
||||
Label::new(match_text).size(LabelSize::Small).color(
|
||||
if self.active_match_index.is_some() {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Disabled
|
||||
},
|
||||
),
|
||||
))
|
||||
})
|
||||
})
|
||||
.when(supported_options.find_in_results, |el| {
|
||||
el.child(
|
||||
IconButton::new(SharedString::from("Close"), IconName::Close)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::for_action("Close Search Bar", &Dismiss, window, cx)
|
||||
})
|
||||
.on_click(cx.listener(|this, _: &ClickEvent, window, cx| {
|
||||
this.dismiss(&Dismiss, window, cx)
|
||||
})),
|
||||
)
|
||||
}),
|
||||
);
|
||||
|
||||
let replace_line = should_show_replace_input.then(|| {
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
input_base_styles(replacement_border).child(self.render_text_input(
|
||||
&self.replacement_editor,
|
||||
None,
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.min_w_64()
|
||||
.gap_1()
|
||||
.child(
|
||||
IconButton::new("search-replace-next", ui::IconName::ReplaceNext)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Replace Next Match",
|
||||
&ReplaceNext,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.replace_next(&ReplaceNext, window, cx)
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("search-replace-all", ui::IconName::ReplaceAll)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Replace All Matches",
|
||||
&ReplaceAll,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.replace_all(&ReplaceAll, window, cx)
|
||||
})),
|
||||
),
|
||||
)
|
||||
});
|
||||
|
||||
let query_error_line = self.query_error.as_ref().map(|error| {
|
||||
Label::new(error)
|
||||
|
@ -491,10 +397,26 @@ impl Render for BufferSearchBar {
|
|||
.ml_2()
|
||||
});
|
||||
|
||||
let search_line =
|
||||
h_flex()
|
||||
.relative()
|
||||
.child(search_line)
|
||||
.when(!narrow_mode && !find_in_results, |div| {
|
||||
div.child(h_flex().absolute().right_0().child(render_action_button(
|
||||
"buffer-search",
|
||||
IconName::Close,
|
||||
true,
|
||||
"Close Search Bar",
|
||||
&Dismiss,
|
||||
focus_handle.clone(),
|
||||
)))
|
||||
.w_full()
|
||||
});
|
||||
v_flex()
|
||||
.id("buffer_search")
|
||||
.gap_2()
|
||||
.py(px(1.0))
|
||||
.w_full()
|
||||
.track_scroll(&self.scroll_handle)
|
||||
.key_context(key_context)
|
||||
.capture_action(cx.listener(Self::tab))
|
||||
|
@ -509,43 +431,26 @@ impl Render for BufferSearchBar {
|
|||
active_searchable_item.relay_action(Box::new(ToggleOutline), window, cx);
|
||||
}
|
||||
}))
|
||||
.when(self.supported_options(cx).replacement, |this| {
|
||||
.when(replacement, |this| {
|
||||
this.on_action(cx.listener(Self::toggle_replace))
|
||||
.when(in_replace, |this| {
|
||||
this.on_action(cx.listener(Self::replace_next))
|
||||
.on_action(cx.listener(Self::replace_all))
|
||||
})
|
||||
})
|
||||
.when(self.supported_options(cx).case, |this| {
|
||||
.when(case, |this| {
|
||||
this.on_action(cx.listener(Self::toggle_case_sensitive))
|
||||
})
|
||||
.when(self.supported_options(cx).word, |this| {
|
||||
.when(word, |this| {
|
||||
this.on_action(cx.listener(Self::toggle_whole_word))
|
||||
})
|
||||
.when(self.supported_options(cx).regex, |this| {
|
||||
.when(regex, |this| {
|
||||
this.on_action(cx.listener(Self::toggle_regex))
|
||||
})
|
||||
.when(self.supported_options(cx).selection, |this| {
|
||||
.when(selection, |this| {
|
||||
this.on_action(cx.listener(Self::toggle_selection))
|
||||
})
|
||||
.child(h_flex().relative().child(search_line.w_full()).when(
|
||||
!narrow_mode && !supported_options.find_in_results,
|
||||
|div| {
|
||||
div.child(
|
||||
h_flex().absolute().right_0().child(
|
||||
IconButton::new(SharedString::from("Close"), IconName::Close)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::for_action("Close Search Bar", &Dismiss, window, cx)
|
||||
})
|
||||
.on_click(cx.listener(|this, _: &ClickEvent, window, cx| {
|
||||
this.dismiss(&Dismiss, window, cx)
|
||||
})),
|
||||
),
|
||||
)
|
||||
.w_full()
|
||||
},
|
||||
))
|
||||
.child(search_line)
|
||||
.children(query_error_line)
|
||||
.children(replace_line)
|
||||
}
|
||||
|
@ -792,7 +697,7 @@ impl BufferSearchBar {
|
|||
active_editor.search_bar_visibility_changed(false, window, cx);
|
||||
active_editor.toggle_filtered_search_ranges(false, window, cx);
|
||||
let handle = active_editor.item_focus_handle(cx);
|
||||
self.focus(&handle, window, cx);
|
||||
self.focus(&handle, window);
|
||||
}
|
||||
cx.emit(Event::UpdateLocation);
|
||||
cx.emit(ToolbarItemEvent::ChangeLocation(
|
||||
|
@ -948,7 +853,7 @@ impl BufferSearchBar {
|
|||
}
|
||||
|
||||
pub fn focus_replace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.focus(&self.replacement_editor.focus_handle(cx), window, cx);
|
||||
self.focus(&self.replacement_editor.focus_handle(cx), window);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
|
@ -975,16 +880,6 @@ impl BufferSearchBar {
|
|||
self.update_matches(!updated, window, cx)
|
||||
}
|
||||
|
||||
fn render_search_option_button<Action: Fn(&ClickEvent, &mut Window, &mut App) + 'static>(
|
||||
&self,
|
||||
option: SearchOptions,
|
||||
focus_handle: FocusHandle,
|
||||
action: Action,
|
||||
) -> impl IntoElement + use<Action> {
|
||||
let is_active = self.search_options.contains(option);
|
||||
option.as_button(is_active, focus_handle, action)
|
||||
}
|
||||
|
||||
pub fn focus_editor(&mut self, _: &FocusEditor, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some(active_editor) = self.active_searchable_item.as_ref() {
|
||||
let handle = active_editor.item_focus_handle(cx);
|
||||
|
@ -1400,28 +1295,32 @@ impl BufferSearchBar {
|
|||
}
|
||||
|
||||
fn tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context<Self>) {
|
||||
// Search -> Replace -> Editor
|
||||
let focus_handle = if self.replace_enabled && self.query_editor_focused {
|
||||
self.replacement_editor.focus_handle(cx)
|
||||
} else if let Some(item) = self.active_searchable_item.as_ref() {
|
||||
item.item_focus_handle(cx)
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
self.focus(&focus_handle, window, cx);
|
||||
cx.stop_propagation();
|
||||
self.cycle_field(Direction::Next, window, cx);
|
||||
}
|
||||
|
||||
fn backtab(&mut self, _: &Backtab, window: &mut Window, cx: &mut Context<Self>) {
|
||||
// Search -> Replace -> Search
|
||||
let focus_handle = if self.replace_enabled && self.query_editor_focused {
|
||||
self.replacement_editor.focus_handle(cx)
|
||||
} else if self.replacement_editor_focused {
|
||||
self.query_editor.focus_handle(cx)
|
||||
} else {
|
||||
return;
|
||||
self.cycle_field(Direction::Prev, window, cx);
|
||||
}
|
||||
fn cycle_field(&mut self, direction: Direction, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let mut handles = vec![self.query_editor.focus_handle(cx)];
|
||||
if self.replace_enabled {
|
||||
handles.push(self.replacement_editor.focus_handle(cx));
|
||||
}
|
||||
if let Some(item) = self.active_searchable_item.as_ref() {
|
||||
handles.push(item.item_focus_handle(cx));
|
||||
}
|
||||
let current_index = match handles.iter().position(|focus| focus.is_focused(window)) {
|
||||
Some(index) => index,
|
||||
None => return,
|
||||
};
|
||||
self.focus(&focus_handle, window, cx);
|
||||
|
||||
let new_index = match direction {
|
||||
Direction::Next => (current_index + 1) % handles.len(),
|
||||
Direction::Prev if current_index == 0 => handles.len() - 1,
|
||||
Direction::Prev => (current_index - 1) % handles.len(),
|
||||
};
|
||||
let next_focus_handle = &handles[new_index];
|
||||
self.focus(next_focus_handle, window);
|
||||
cx.stop_propagation();
|
||||
}
|
||||
|
||||
|
@ -1469,10 +1368,8 @@ impl BufferSearchBar {
|
|||
}
|
||||
}
|
||||
|
||||
fn focus(&self, handle: &gpui::FocusHandle, window: &mut Window, cx: &mut Context<Self>) {
|
||||
cx.on_next_frame(window, |_, window, _| {
|
||||
window.invalidate_character_coordinates();
|
||||
});
|
||||
fn focus(&self, handle: &gpui::FocusHandle, window: &mut Window) {
|
||||
window.invalidate_character_coordinates();
|
||||
window.focus(handle);
|
||||
}
|
||||
|
||||
|
@ -1484,7 +1381,7 @@ impl BufferSearchBar {
|
|||
} else {
|
||||
self.query_editor.focus_handle(cx)
|
||||
};
|
||||
self.focus(&handle, window, cx);
|
||||
self.focus(&handle, window);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,20 +1,25 @@
|
|||
use crate::{
|
||||
BufferSearchBar, FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext,
|
||||
SearchOptions, SelectNextMatch, SelectPreviousMatch, ToggleCaseSensitive, ToggleIncludeIgnored,
|
||||
ToggleRegex, ToggleReplace, ToggleWholeWord, buffer_search::Deploy,
|
||||
ToggleRegex, ToggleReplace, ToggleWholeWord,
|
||||
buffer_search::Deploy,
|
||||
search_bar::{
|
||||
input_base_styles, render_action_button, render_text_input, toggle_replace_button,
|
||||
},
|
||||
};
|
||||
use anyhow::Context as _;
|
||||
use collections::{HashMap, HashSet};
|
||||
use collections::HashMap;
|
||||
use editor::{
|
||||
Anchor, Editor, EditorElement, EditorEvent, EditorSettings, EditorStyle, MAX_TAB_TITLE_LEN,
|
||||
MultiBuffer, SelectionEffects, actions::SelectAll, items::active_match_index,
|
||||
Anchor, Editor, EditorEvent, EditorSettings, MAX_TAB_TITLE_LEN, MultiBuffer, SelectionEffects,
|
||||
actions::{Backtab, SelectAll, Tab},
|
||||
items::active_match_index,
|
||||
};
|
||||
use futures::{StreamExt, stream::FuturesOrdered};
|
||||
use gpui::{
|
||||
Action, AnyElement, AnyView, App, Axis, Context, Entity, EntityId, EventEmitter, FocusHandle,
|
||||
Focusable, Global, Hsla, InteractiveElement, IntoElement, KeyContext, ParentElement, Point,
|
||||
Render, SharedString, Styled, Subscription, Task, TextStyle, UpdateGlobal, WeakEntity, Window,
|
||||
actions, div,
|
||||
Render, SharedString, Styled, Subscription, Task, UpdateGlobal, WeakEntity, Window, actions,
|
||||
div,
|
||||
};
|
||||
use language::{Buffer, Language};
|
||||
use menu::Confirm;
|
||||
|
@ -32,7 +37,6 @@ use std::{
|
|||
pin::pin,
|
||||
sync::Arc,
|
||||
};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
Icon, IconButton, IconButtonShape, IconName, KeyBinding, Label, LabelCommon, LabelSize,
|
||||
Toggleable, Tooltip, h_flex, prelude::*, utils::SearchInputWidth, v_flex,
|
||||
|
@ -208,7 +212,7 @@ pub struct ProjectSearchView {
|
|||
replacement_editor: Entity<Editor>,
|
||||
results_editor: Entity<Editor>,
|
||||
search_options: SearchOptions,
|
||||
panels_with_errors: HashSet<InputPanel>,
|
||||
panels_with_errors: HashMap<InputPanel, String>,
|
||||
active_match_index: Option<usize>,
|
||||
search_id: usize,
|
||||
included_files_editor: Entity<Editor>,
|
||||
|
@ -218,7 +222,6 @@ pub struct ProjectSearchView {
|
|||
included_opened_only: bool,
|
||||
regex_language: Option<Arc<Language>>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
query_error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
@ -879,7 +882,7 @@ impl ProjectSearchView {
|
|||
query_editor,
|
||||
results_editor,
|
||||
search_options: options,
|
||||
panels_with_errors: HashSet::default(),
|
||||
panels_with_errors: HashMap::default(),
|
||||
active_match_index: None,
|
||||
included_files_editor,
|
||||
excluded_files_editor,
|
||||
|
@ -888,7 +891,6 @@ impl ProjectSearchView {
|
|||
included_opened_only: false,
|
||||
regex_language: None,
|
||||
_subscriptions: subscriptions,
|
||||
query_error: None,
|
||||
};
|
||||
this.entity_changed(window, cx);
|
||||
this
|
||||
|
@ -1152,14 +1154,16 @@ impl ProjectSearchView {
|
|||
Ok(included_files) => {
|
||||
let should_unmark_error =
|
||||
self.panels_with_errors.remove(&InputPanel::Include);
|
||||
if should_unmark_error {
|
||||
if should_unmark_error.is_some() {
|
||||
cx.notify();
|
||||
}
|
||||
included_files
|
||||
}
|
||||
Err(_e) => {
|
||||
let should_mark_error = self.panels_with_errors.insert(InputPanel::Include);
|
||||
if should_mark_error {
|
||||
Err(e) => {
|
||||
let should_mark_error = self
|
||||
.panels_with_errors
|
||||
.insert(InputPanel::Include, e.to_string());
|
||||
if should_mark_error.is_none() {
|
||||
cx.notify();
|
||||
}
|
||||
PathMatcher::default()
|
||||
|
@ -1174,15 +1178,17 @@ impl ProjectSearchView {
|
|||
Ok(excluded_files) => {
|
||||
let should_unmark_error =
|
||||
self.panels_with_errors.remove(&InputPanel::Exclude);
|
||||
if should_unmark_error {
|
||||
if should_unmark_error.is_some() {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
excluded_files
|
||||
}
|
||||
Err(_e) => {
|
||||
let should_mark_error = self.panels_with_errors.insert(InputPanel::Exclude);
|
||||
if should_mark_error {
|
||||
Err(e) => {
|
||||
let should_mark_error = self
|
||||
.panels_with_errors
|
||||
.insert(InputPanel::Exclude, e.to_string());
|
||||
if should_mark_error.is_none() {
|
||||
cx.notify();
|
||||
}
|
||||
PathMatcher::default()
|
||||
|
@ -1219,19 +1225,19 @@ impl ProjectSearchView {
|
|||
) {
|
||||
Ok(query) => {
|
||||
let should_unmark_error = self.panels_with_errors.remove(&InputPanel::Query);
|
||||
if should_unmark_error {
|
||||
if should_unmark_error.is_some() {
|
||||
cx.notify();
|
||||
}
|
||||
self.query_error = None;
|
||||
|
||||
Some(query)
|
||||
}
|
||||
Err(e) => {
|
||||
let should_mark_error = self.panels_with_errors.insert(InputPanel::Query);
|
||||
if should_mark_error {
|
||||
let should_mark_error = self
|
||||
.panels_with_errors
|
||||
.insert(InputPanel::Query, e.to_string());
|
||||
if should_mark_error.is_none() {
|
||||
cx.notify();
|
||||
}
|
||||
self.query_error = Some(e.to_string());
|
||||
|
||||
None
|
||||
}
|
||||
|
@ -1249,15 +1255,17 @@ impl ProjectSearchView {
|
|||
) {
|
||||
Ok(query) => {
|
||||
let should_unmark_error = self.panels_with_errors.remove(&InputPanel::Query);
|
||||
if should_unmark_error {
|
||||
if should_unmark_error.is_some() {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
Some(query)
|
||||
}
|
||||
Err(_e) => {
|
||||
let should_mark_error = self.panels_with_errors.insert(InputPanel::Query);
|
||||
if should_mark_error {
|
||||
Err(e) => {
|
||||
let should_mark_error = self
|
||||
.panels_with_errors
|
||||
.insert(InputPanel::Query, e.to_string());
|
||||
if should_mark_error.is_none() {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
|
@ -1512,7 +1520,7 @@ impl ProjectSearchView {
|
|||
}
|
||||
|
||||
fn border_color_for(&self, panel: InputPanel, cx: &App) -> Hsla {
|
||||
if self.panels_with_errors.contains(&panel) {
|
||||
if self.panels_with_errors.contains_key(&panel) {
|
||||
Color::Error.color(cx)
|
||||
} else {
|
||||
cx.theme().colors().border
|
||||
|
@ -1610,16 +1618,11 @@ impl ProjectSearchBar {
|
|||
}
|
||||
}
|
||||
|
||||
fn tab(&mut self, _: &editor::actions::Tab, window: &mut Window, cx: &mut Context<Self>) {
|
||||
fn tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.cycle_field(Direction::Next, window, cx);
|
||||
}
|
||||
|
||||
fn backtab(
|
||||
&mut self,
|
||||
_: &editor::actions::Backtab,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
fn backtab(&mut self, _: &Backtab, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.cycle_field(Direction::Prev, window, cx);
|
||||
}
|
||||
|
||||
|
@ -1634,29 +1637,22 @@ impl ProjectSearchBar {
|
|||
fn cycle_field(&mut self, direction: Direction, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let active_project_search = match &self.active_project_search {
|
||||
Some(active_project_search) => active_project_search,
|
||||
|
||||
None => {
|
||||
return;
|
||||
}
|
||||
None => return,
|
||||
};
|
||||
|
||||
active_project_search.update(cx, |project_view, cx| {
|
||||
let mut views = vec![&project_view.query_editor];
|
||||
let mut views = vec![project_view.query_editor.focus_handle(cx)];
|
||||
if project_view.replace_enabled {
|
||||
views.push(&project_view.replacement_editor);
|
||||
views.push(project_view.replacement_editor.focus_handle(cx));
|
||||
}
|
||||
if project_view.filters_enabled {
|
||||
views.extend([
|
||||
&project_view.included_files_editor,
|
||||
&project_view.excluded_files_editor,
|
||||
project_view.included_files_editor.focus_handle(cx),
|
||||
project_view.excluded_files_editor.focus_handle(cx),
|
||||
]);
|
||||
}
|
||||
let current_index = match views
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, editor)| editor.focus_handle(cx).is_focused(window))
|
||||
{
|
||||
Some((index, _)) => index,
|
||||
let current_index = match views.iter().position(|focus| focus.is_focused(window)) {
|
||||
Some(index) => index,
|
||||
None => return,
|
||||
};
|
||||
|
||||
|
@ -1665,8 +1661,8 @@ impl ProjectSearchBar {
|
|||
Direction::Prev if current_index == 0 => views.len() - 1,
|
||||
Direction::Prev => (current_index - 1) % views.len(),
|
||||
};
|
||||
let next_focus_handle = views[new_index].focus_handle(cx);
|
||||
window.focus(&next_focus_handle);
|
||||
let next_focus_handle = &views[new_index];
|
||||
window.focus(next_focus_handle);
|
||||
cx.stop_propagation();
|
||||
});
|
||||
}
|
||||
|
@ -1915,37 +1911,6 @@ impl ProjectSearchBar {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn render_text_input(&self, editor: &Entity<Editor>, cx: &Context<Self>) -> impl IntoElement {
|
||||
let (color, use_syntax) = if editor.read(cx).read_only(cx) {
|
||||
(cx.theme().colors().text_disabled, false)
|
||||
} else {
|
||||
(cx.theme().colors().text, true)
|
||||
};
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let text_style = TextStyle {
|
||||
color,
|
||||
font_family: settings.buffer_font.family.clone(),
|
||||
font_features: settings.buffer_font.features.clone(),
|
||||
font_fallbacks: settings.buffer_font.fallbacks.clone(),
|
||||
font_size: rems(0.875).into(),
|
||||
font_weight: settings.buffer_font.weight,
|
||||
line_height: relative(1.3),
|
||||
..TextStyle::default()
|
||||
};
|
||||
|
||||
let mut editor_style = EditorStyle {
|
||||
background: cx.theme().colors().toolbar_background,
|
||||
local_player: cx.theme().players().local(),
|
||||
text: text_style,
|
||||
..EditorStyle::default()
|
||||
};
|
||||
if use_syntax {
|
||||
editor_style.syntax = cx.theme().syntax().clone();
|
||||
}
|
||||
|
||||
EditorElement::new(editor, editor_style)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ProjectSearchBar {
|
||||
|
@ -1959,28 +1924,43 @@ impl Render for ProjectSearchBar {
|
|||
let container_width = window.viewport_size().width;
|
||||
let input_width = SearchInputWidth::calc_width(container_width);
|
||||
|
||||
enum BaseStyle {
|
||||
SingleInput,
|
||||
MultipleInputs,
|
||||
}
|
||||
|
||||
let input_base_styles = |base_style: BaseStyle, panel: InputPanel| {
|
||||
h_flex()
|
||||
.min_w_32()
|
||||
.map(|div| match base_style {
|
||||
BaseStyle::SingleInput => div.w(input_width),
|
||||
BaseStyle::MultipleInputs => div.flex_grow(),
|
||||
})
|
||||
.h_8()
|
||||
.pl_2()
|
||||
.pr_1()
|
||||
.py_1()
|
||||
.border_1()
|
||||
.border_color(search.border_color_for(panel, cx))
|
||||
.rounded_lg()
|
||||
let input_base_styles = |panel: InputPanel| {
|
||||
input_base_styles(search.border_color_for(panel, cx), |div| match panel {
|
||||
InputPanel::Query | InputPanel::Replacement => div.w(input_width),
|
||||
InputPanel::Include | InputPanel::Exclude => div.flex_grow(),
|
||||
})
|
||||
};
|
||||
let theme_colors = cx.theme().colors();
|
||||
let project_search = search.entity.read(cx);
|
||||
let limit_reached = project_search.limit_reached;
|
||||
|
||||
let query_column = input_base_styles(BaseStyle::SingleInput, InputPanel::Query)
|
||||
let color_override = match (
|
||||
project_search.no_results,
|
||||
&project_search.active_query,
|
||||
&project_search.last_search_query_text,
|
||||
) {
|
||||
(Some(true), Some(q), Some(p)) if q.as_str() == p => Some(Color::Error),
|
||||
_ => None,
|
||||
};
|
||||
let match_text = search
|
||||
.active_match_index
|
||||
.and_then(|index| {
|
||||
let index = index + 1;
|
||||
let match_quantity = project_search.match_ranges.len();
|
||||
if match_quantity > 0 {
|
||||
debug_assert!(match_quantity >= index);
|
||||
if limit_reached {
|
||||
Some(format!("{index}/{match_quantity}+"))
|
||||
} else {
|
||||
Some(format!("{index}/{match_quantity}"))
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| "0/0".to_string());
|
||||
|
||||
let query_column = input_base_styles(InputPanel::Query)
|
||||
.on_action(cx.listener(|this, action, window, cx| this.confirm(action, window, cx)))
|
||||
.on_action(cx.listener(|this, action, window, cx| {
|
||||
this.previous_history_query(action, window, cx)
|
||||
|
@ -1988,7 +1968,7 @@ impl Render for ProjectSearchBar {
|
|||
.on_action(
|
||||
cx.listener(|this, action, window, cx| this.next_history_query(action, window, cx)),
|
||||
)
|
||||
.child(self.render_text_input(&search.query_editor, cx))
|
||||
.child(render_text_input(&search.query_editor, color_override, cx))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
|
@ -2017,6 +1997,7 @@ impl Render for ProjectSearchBar {
|
|||
|
||||
let mode_column = h_flex()
|
||||
.gap_1()
|
||||
.min_w_64()
|
||||
.child(
|
||||
IconButton::new("project-search-filter-button", IconName::Filter)
|
||||
.shape(IconButtonShape::Square)
|
||||
|
@ -2045,109 +2026,46 @@ impl Render for ProjectSearchBar {
|
|||
}
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("project-search-toggle-replace", IconName::Replace)
|
||||
.shape(IconButtonShape::Square)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.toggle_replace(&ToggleReplace, window, cx);
|
||||
}))
|
||||
.toggle_state(
|
||||
self.active_project_search
|
||||
.as_ref()
|
||||
.map(|search| search.read(cx).replace_enabled)
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Toggle Replace",
|
||||
&ToggleReplace,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
}),
|
||||
);
|
||||
.child(toggle_replace_button(
|
||||
"project-search-toggle-replace",
|
||||
focus_handle.clone(),
|
||||
self.active_project_search
|
||||
.as_ref()
|
||||
.map(|search| search.read(cx).replace_enabled)
|
||||
.unwrap_or_default(),
|
||||
cx.listener(|this, _, window, cx| {
|
||||
this.toggle_replace(&ToggleReplace, window, cx);
|
||||
}),
|
||||
));
|
||||
|
||||
let limit_reached = search.entity.read(cx).limit_reached;
|
||||
|
||||
let match_text = search
|
||||
.active_match_index
|
||||
.and_then(|index| {
|
||||
let index = index + 1;
|
||||
let match_quantity = search.entity.read(cx).match_ranges.len();
|
||||
if match_quantity > 0 {
|
||||
debug_assert!(match_quantity >= index);
|
||||
if limit_reached {
|
||||
Some(format!("{index}/{match_quantity}+"))
|
||||
} else {
|
||||
Some(format!("{index}/{match_quantity}"))
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| "0/0".to_string());
|
||||
let query_focus = search.query_editor.focus_handle(cx);
|
||||
|
||||
let matches_column = h_flex()
|
||||
.pl_2()
|
||||
.ml_2()
|
||||
.border_l_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.child(
|
||||
IconButton::new("project-search-prev-match", IconName::ChevronLeft)
|
||||
.shape(IconButtonShape::Square)
|
||||
.disabled(search.active_match_index.is_none())
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
if let Some(search) = this.active_project_search.as_ref() {
|
||||
search.update(cx, |this, cx| {
|
||||
this.select_match(Direction::Prev, window, cx);
|
||||
})
|
||||
}
|
||||
}))
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Go To Previous Match",
|
||||
&SelectPreviousMatch,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("project-search-next-match", IconName::ChevronRight)
|
||||
.shape(IconButtonShape::Square)
|
||||
.disabled(search.active_match_index.is_none())
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
if let Some(search) = this.active_project_search.as_ref() {
|
||||
search.update(cx, |this, cx| {
|
||||
this.select_match(Direction::Next, window, cx);
|
||||
})
|
||||
}
|
||||
}))
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Go To Next Match",
|
||||
&SelectNextMatch,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
}),
|
||||
)
|
||||
.border_color(theme_colors.border_variant)
|
||||
.child(render_action_button(
|
||||
"project-search-nav-button",
|
||||
IconName::ChevronLeft,
|
||||
search.active_match_index.is_some(),
|
||||
"Select Previous Match",
|
||||
&SelectPreviousMatch,
|
||||
query_focus.clone(),
|
||||
))
|
||||
.child(render_action_button(
|
||||
"project-search-nav-button",
|
||||
IconName::ChevronRight,
|
||||
search.active_match_index.is_some(),
|
||||
"Select Next Match",
|
||||
&SelectNextMatch,
|
||||
query_focus,
|
||||
))
|
||||
.child(
|
||||
div()
|
||||
.id("matches")
|
||||
.ml_1()
|
||||
.ml_2()
|
||||
.min_w(rems_from_px(40.))
|
||||
.child(Label::new(match_text).size(LabelSize::Small).color(
|
||||
if search.active_match_index.is_some() {
|
||||
Color::Default
|
||||
|
@ -2169,63 +2087,30 @@ impl Render for ProjectSearchBar {
|
|||
.child(h_flex().min_w_64().child(mode_column).child(matches_column));
|
||||
|
||||
let replace_line = search.replace_enabled.then(|| {
|
||||
let replace_column = input_base_styles(BaseStyle::SingleInput, InputPanel::Replacement)
|
||||
.child(self.render_text_input(&search.replacement_editor, cx));
|
||||
let replace_column = input_base_styles(InputPanel::Replacement)
|
||||
.child(render_text_input(&search.replacement_editor, None, cx));
|
||||
|
||||
let focus_handle = search.replacement_editor.read(cx).focus_handle(cx);
|
||||
|
||||
let replace_actions =
|
||||
h_flex()
|
||||
.min_w_64()
|
||||
.gap_1()
|
||||
.when(search.replace_enabled, |this| {
|
||||
this.child(
|
||||
IconButton::new("project-search-replace-next", IconName::ReplaceNext)
|
||||
.shape(IconButtonShape::Square)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
if let Some(search) = this.active_project_search.as_ref() {
|
||||
search.update(cx, |this, cx| {
|
||||
this.replace_next(&ReplaceNext, window, cx);
|
||||
})
|
||||
}
|
||||
}))
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Replace Next Match",
|
||||
&ReplaceNext,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("project-search-replace-all", IconName::ReplaceAll)
|
||||
.shape(IconButtonShape::Square)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
if let Some(search) = this.active_project_search.as_ref() {
|
||||
search.update(cx, |this, cx| {
|
||||
this.replace_all(&ReplaceAll, window, cx);
|
||||
})
|
||||
}
|
||||
}))
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Replace All Matches",
|
||||
&ReplaceAll,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
}),
|
||||
)
|
||||
});
|
||||
let replace_actions = h_flex()
|
||||
.min_w_64()
|
||||
.gap_1()
|
||||
.child(render_action_button(
|
||||
"project-search-replace-button",
|
||||
IconName::ReplaceNext,
|
||||
true,
|
||||
"Replace Next Match",
|
||||
&ReplaceNext,
|
||||
focus_handle.clone(),
|
||||
))
|
||||
.child(render_action_button(
|
||||
"project-search-replace-button",
|
||||
IconName::ReplaceAll,
|
||||
true,
|
||||
"Replace All Matches",
|
||||
&ReplaceAll,
|
||||
focus_handle,
|
||||
));
|
||||
|
||||
h_flex()
|
||||
.w_full()
|
||||
|
@ -2235,6 +2120,45 @@ impl Render for ProjectSearchBar {
|
|||
});
|
||||
|
||||
let filter_line = search.filters_enabled.then(|| {
|
||||
let include = input_base_styles(InputPanel::Include)
|
||||
.on_action(cx.listener(|this, action, window, cx| {
|
||||
this.previous_history_query(action, window, cx)
|
||||
}))
|
||||
.on_action(cx.listener(|this, action, window, cx| {
|
||||
this.next_history_query(action, window, cx)
|
||||
}))
|
||||
.child(render_text_input(&search.included_files_editor, None, cx));
|
||||
let exclude = input_base_styles(InputPanel::Exclude)
|
||||
.on_action(cx.listener(|this, action, window, cx| {
|
||||
this.previous_history_query(action, window, cx)
|
||||
}))
|
||||
.on_action(cx.listener(|this, action, window, cx| {
|
||||
this.next_history_query(action, window, cx)
|
||||
}))
|
||||
.child(render_text_input(&search.excluded_files_editor, None, cx));
|
||||
let mode_column = h_flex()
|
||||
.gap_1()
|
||||
.min_w_64()
|
||||
.child(
|
||||
IconButton::new("project-search-opened-only", IconName::FolderSearch)
|
||||
.shape(IconButtonShape::Square)
|
||||
.toggle_state(self.is_opened_only_enabled(cx))
|
||||
.tooltip(Tooltip::text("Only Search Open Files"))
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.toggle_opened_only(window, cx);
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
SearchOptions::INCLUDE_IGNORED.as_button(
|
||||
search
|
||||
.search_options
|
||||
.contains(SearchOptions::INCLUDE_IGNORED),
|
||||
focus_handle.clone(),
|
||||
cx.listener(|this, _, window, cx| {
|
||||
this.toggle_search_option(SearchOptions::INCLUDE_IGNORED, window, cx);
|
||||
}),
|
||||
),
|
||||
);
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
|
@ -2242,62 +2166,14 @@ impl Render for ProjectSearchBar {
|
|||
h_flex()
|
||||
.gap_2()
|
||||
.w(input_width)
|
||||
.child(
|
||||
input_base_styles(BaseStyle::MultipleInputs, InputPanel::Include)
|
||||
.on_action(cx.listener(|this, action, window, cx| {
|
||||
this.previous_history_query(action, window, cx)
|
||||
}))
|
||||
.on_action(cx.listener(|this, action, window, cx| {
|
||||
this.next_history_query(action, window, cx)
|
||||
}))
|
||||
.child(self.render_text_input(&search.included_files_editor, cx)),
|
||||
)
|
||||
.child(
|
||||
input_base_styles(BaseStyle::MultipleInputs, InputPanel::Exclude)
|
||||
.on_action(cx.listener(|this, action, window, cx| {
|
||||
this.previous_history_query(action, window, cx)
|
||||
}))
|
||||
.on_action(cx.listener(|this, action, window, cx| {
|
||||
this.next_history_query(action, window, cx)
|
||||
}))
|
||||
.child(self.render_text_input(&search.excluded_files_editor, cx)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.min_w_64()
|
||||
.gap_1()
|
||||
.child(
|
||||
IconButton::new("project-search-opened-only", IconName::FolderSearch)
|
||||
.shape(IconButtonShape::Square)
|
||||
.toggle_state(self.is_opened_only_enabled(cx))
|
||||
.tooltip(Tooltip::text("Only Search Open Files"))
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.toggle_opened_only(window, cx);
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
SearchOptions::INCLUDE_IGNORED.as_button(
|
||||
search
|
||||
.search_options
|
||||
.contains(SearchOptions::INCLUDE_IGNORED),
|
||||
focus_handle.clone(),
|
||||
cx.listener(|this, _, window, cx| {
|
||||
this.toggle_search_option(
|
||||
SearchOptions::INCLUDE_IGNORED,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
.child(include)
|
||||
.child(exclude),
|
||||
)
|
||||
.child(mode_column)
|
||||
});
|
||||
|
||||
let mut key_context = KeyContext::default();
|
||||
|
||||
key_context.add("ProjectSearchBar");
|
||||
|
||||
if search
|
||||
.replacement_editor
|
||||
.focus_handle(cx)
|
||||
|
@ -2306,16 +2182,33 @@ impl Render for ProjectSearchBar {
|
|||
key_context.add("in_replace");
|
||||
}
|
||||
|
||||
let query_error_line = search.query_error.as_ref().map(|error| {
|
||||
Label::new(error)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Error)
|
||||
.mt_neg_1()
|
||||
.ml_2()
|
||||
});
|
||||
let query_error_line = search
|
||||
.panels_with_errors
|
||||
.get(&InputPanel::Query)
|
||||
.map(|error| {
|
||||
Label::new(error)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Error)
|
||||
.mt_neg_1()
|
||||
.ml_2()
|
||||
});
|
||||
|
||||
let filter_error_line = search
|
||||
.panels_with_errors
|
||||
.get(&InputPanel::Include)
|
||||
.or_else(|| search.panels_with_errors.get(&InputPanel::Exclude))
|
||||
.map(|error| {
|
||||
Label::new(error)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Error)
|
||||
.mt_neg_1()
|
||||
.ml_2()
|
||||
});
|
||||
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.py(px(1.0))
|
||||
.w_full()
|
||||
.key_context(key_context)
|
||||
.on_action(cx.listener(|this, _: &ToggleFocus, window, cx| {
|
||||
this.move_focus_to_results(window, cx)
|
||||
|
@ -2323,14 +2216,8 @@ impl Render for ProjectSearchBar {
|
|||
.on_action(cx.listener(|this, _: &ToggleFilters, window, cx| {
|
||||
this.toggle_filters(window, cx);
|
||||
}))
|
||||
.capture_action(cx.listener(|this, action, window, cx| {
|
||||
this.tab(action, window, cx);
|
||||
cx.stop_propagation();
|
||||
}))
|
||||
.capture_action(cx.listener(|this, action, window, cx| {
|
||||
this.backtab(action, window, cx);
|
||||
cx.stop_propagation();
|
||||
}))
|
||||
.capture_action(cx.listener(Self::tab))
|
||||
.capture_action(cx.listener(Self::backtab))
|
||||
.on_action(cx.listener(|this, action, window, cx| this.confirm(action, window, cx)))
|
||||
.on_action(cx.listener(|this, action, window, cx| {
|
||||
this.toggle_replace(action, window, cx);
|
||||
|
@ -2362,12 +2249,11 @@ impl Render for ProjectSearchBar {
|
|||
})
|
||||
.on_action(cx.listener(Self::select_next_match))
|
||||
.on_action(cx.listener(Self::select_prev_match))
|
||||
.gap_2()
|
||||
.w_full()
|
||||
.child(search_line)
|
||||
.children(query_error_line)
|
||||
.children(replace_line)
|
||||
.children(filter_line)
|
||||
.children(filter_error_line)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,14 @@
|
|||
use gpui::{Action, FocusHandle, IntoElement};
|
||||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
use gpui::{Action, Entity, FocusHandle, Hsla, IntoElement, TextStyle};
|
||||
use settings::Settings;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{IconButton, IconButtonShape};
|
||||
use ui::{Tooltip, prelude::*};
|
||||
|
||||
pub(super) fn render_nav_button(
|
||||
use crate::ToggleReplace;
|
||||
|
||||
pub(super) fn render_action_button(
|
||||
id_prefix: &'static str,
|
||||
icon: ui::IconName,
|
||||
active: bool,
|
||||
tooltip: &'static str,
|
||||
|
@ -10,7 +16,7 @@ pub(super) fn render_nav_button(
|
|||
focus_handle: FocusHandle,
|
||||
) -> impl IntoElement {
|
||||
IconButton::new(
|
||||
SharedString::from(format!("search-nav-button-{}", action.name())),
|
||||
SharedString::from(format!("{id_prefix}-{}", action.name())),
|
||||
icon,
|
||||
)
|
||||
.shape(IconButtonShape::Square)
|
||||
|
@ -26,3 +32,74 @@ pub(super) fn render_nav_button(
|
|||
.tooltip(move |window, cx| Tooltip::for_action_in(tooltip, action, &focus_handle, window, cx))
|
||||
.disabled(!active)
|
||||
}
|
||||
|
||||
pub(crate) fn input_base_styles(border_color: Hsla, map: impl FnOnce(Div) -> Div) -> Div {
|
||||
h_flex()
|
||||
.min_w_32()
|
||||
.map(map)
|
||||
.h_8()
|
||||
.pl_2()
|
||||
.pr_1()
|
||||
.py_1()
|
||||
.border_1()
|
||||
.border_color(border_color)
|
||||
.rounded_lg()
|
||||
}
|
||||
|
||||
pub(crate) fn toggle_replace_button(
|
||||
id: &'static str,
|
||||
focus_handle: FocusHandle,
|
||||
replace_enabled: bool,
|
||||
on_click: impl Fn(&gpui::ClickEvent, &mut Window, &mut App) + 'static,
|
||||
) -> IconButton {
|
||||
IconButton::new(id, IconName::Replace)
|
||||
.shape(IconButtonShape::Square)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.when(replace_enabled, |button| button.style(ButtonStyle::Filled))
|
||||
.on_click(on_click)
|
||||
.toggle_state(replace_enabled)
|
||||
.tooltip({
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in("Toggle Replace", &ToggleReplace, &focus_handle, window, cx)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn render_text_input(
|
||||
editor: &Entity<Editor>,
|
||||
color_override: Option<Color>,
|
||||
app: &App,
|
||||
) -> impl IntoElement {
|
||||
let (color, use_syntax) = if editor.read(app).read_only(app) {
|
||||
(app.theme().colors().text_disabled, false)
|
||||
} else {
|
||||
match color_override {
|
||||
Some(color_override) => (color_override.color(app), false),
|
||||
None => (app.theme().colors().text, true),
|
||||
}
|
||||
};
|
||||
|
||||
let settings = ThemeSettings::get_global(app);
|
||||
let text_style = TextStyle {
|
||||
color,
|
||||
font_family: settings.buffer_font.family.clone(),
|
||||
font_features: settings.buffer_font.features.clone(),
|
||||
font_fallbacks: settings.buffer_font.fallbacks.clone(),
|
||||
font_size: rems(0.875).into(),
|
||||
font_weight: settings.buffer_font.weight,
|
||||
line_height: relative(1.3),
|
||||
..TextStyle::default()
|
||||
};
|
||||
|
||||
let mut editor_style = EditorStyle {
|
||||
background: app.theme().colors().toolbar_background,
|
||||
local_player: app.theme().players().local(),
|
||||
text: text_style,
|
||||
..EditorStyle::default()
|
||||
};
|
||||
if use_syntax {
|
||||
editor_style.syntax = app.theme().syntax().clone();
|
||||
}
|
||||
|
||||
EditorElement::new(editor, editor_style)
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue