@@ -90,7 +90,7 @@ pub struct BufferSearchBar {
dismissed: bool,
search_history: SearchHistory,
current_mode: SearchMode,
- replace_is_active: bool,
+ replace_enabled: bool,
}
impl Entity for BufferSearchBar {
@@ -266,7 +266,7 @@ impl View for BufferSearchBar {
.with_max_width(theme.search.editor.max_width)
.with_height(theme.search.search_bar_row_height)
.flex(1., false);
- let should_show_replace_input = self.replace_is_active && supported_options.replacement;
+ let should_show_replace_input = self.replace_enabled && supported_options.replacement;
let replacement = should_show_replace_input.then(|| {
Flex::row()
@@ -308,7 +308,7 @@ impl View for BufferSearchBar {
Flex::row()
.align_children_center()
.with_child(super::toggle_replace_button(
- self.replace_is_active,
+ self.replace_enabled,
theme.tooltip.clone(),
theme.search.option_button_component.clone(),
))
@@ -447,7 +447,7 @@ impl BufferSearchBar {
search_history: SearchHistory::default(),
current_mode: SearchMode::default(),
active_search: None,
- replace_is_active: false,
+ replace_enabled: false,
}
}
@@ -891,7 +891,10 @@ impl BufferSearchBar {
}
fn toggle_replace(&mut self, _: &ToggleReplace, cx: &mut ViewContext<Self>) {
if let Some(_) = &self.active_searchable_item {
- self.replace_is_active = !self.replace_is_active;
+ self.replace_enabled = !self.replace_enabled;
+ if !self.replace_enabled {
+ cx.focus(&self.query_editor);
+ }
cx.notify();
}
}
@@ -901,10 +904,13 @@ impl BufferSearchBar {
search_bar.update(cx, |bar, cx| {
if let Some(_) = &bar.active_searchable_item {
should_propagate = false;
- bar.replace_is_active = !bar.replace_is_active;
+ bar.replace_enabled = !bar.replace_enabled;
if bar.dismissed {
bar.show(cx);
}
+ if !bar.replace_enabled {
+ cx.focus(&bar.query_editor);
+ }
cx.notify();
}
});
@@ -914,6 +920,7 @@ impl BufferSearchBar {
}
}
fn replace_next(&mut self, _: &ReplaceNext, cx: &mut ViewContext<Self>) {
+ let mut should_propagate = true;
if !self.dismissed && self.active_search.is_some() {
if let Some(searchable_item) = self.active_searchable_item.as_ref() {
if let Some(query) = self.active_search.as_ref() {
@@ -929,10 +936,15 @@ impl BufferSearchBar {
searchable_item.replace(&matches[active_index], &query, cx);
self.select_next_match(&SelectNextMatch, cx);
}
+ should_propagate = false;
+ self.focus_editor(&FocusEditor, cx);
}
}
}
}
+ if should_propagate {
+ cx.propagate_action();
+ }
}
fn replace_all(&mut self, _: &ReplaceAll, cx: &mut ViewContext<Self>) {
if !self.dismissed && self.active_search.is_some() {
@@ -3,8 +3,8 @@ use crate::{
mode::{SearchMode, Side},
search_bar::{render_nav_button, render_option_button_icon, render_search_mode_button},
ActivateRegexMode, ActivateSemanticMode, ActivateTextMode, CycleMode, NextHistoryQuery,
- PreviousHistoryQuery, SearchOptions, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive,
- ToggleWholeWord,
+ PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions, SelectNextMatch, SelectPrevMatch,
+ ToggleCaseSensitive, ToggleReplace, ToggleWholeWord,
};
use anyhow::{Context, Result};
use collections::HashMap;
@@ -64,10 +64,14 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(ProjectSearchBar::search_in_new);
cx.add_action(ProjectSearchBar::select_next_match);
cx.add_action(ProjectSearchBar::select_prev_match);
+ cx.add_action(ProjectSearchBar::replace_next);
+ cx.add_action(ProjectSearchBar::replace_all);
cx.add_action(ProjectSearchBar::cycle_mode);
cx.add_action(ProjectSearchBar::next_history_query);
cx.add_action(ProjectSearchBar::previous_history_query);
cx.add_action(ProjectSearchBar::activate_regex_mode);
+ cx.add_action(ProjectSearchBar::toggle_replace);
+ cx.add_action(ProjectSearchBar::toggle_replace_on_a_pane);
cx.add_action(ProjectSearchBar::activate_text_mode);
// This action should only be registered if the semantic index is enabled
@@ -77,6 +81,8 @@ pub fn init(cx: &mut AppContext) {
cx.capture_action(ProjectSearchBar::tab);
cx.capture_action(ProjectSearchBar::tab_previous);
+ cx.capture_action(ProjectSearchView::replace_all);
+ cx.capture_action(ProjectSearchView::replace_next);
add_toggle_option_action::<ToggleCaseSensitive>(SearchOptions::CASE_SENSITIVE, cx);
add_toggle_option_action::<ToggleWholeWord>(SearchOptions::WHOLE_WORD, cx);
add_toggle_filters_action::<ToggleFilters>(cx);
@@ -127,6 +133,7 @@ enum InputPanel {
pub struct ProjectSearchView {
model: ModelHandle<ProjectSearch>,
query_editor: ViewHandle<Editor>,
+ replacement_editor: ViewHandle<Editor>,
results_editor: ViewHandle<Editor>,
semantic_state: Option<SemanticState>,
semantic_permissioned: Option<bool>,
@@ -138,6 +145,7 @@ pub struct ProjectSearchView {
included_files_editor: ViewHandle<Editor>,
excluded_files_editor: ViewHandle<Editor>,
filters_enabled: bool,
+ replace_enabled: bool,
current_mode: SearchMode,
}
@@ -844,6 +852,45 @@ impl ProjectSearchView {
cx.notify();
}
+ fn replace_next(&mut self, _: &ReplaceNext, cx: &mut ViewContext<Self>) {
+ let model = self.model.read(cx);
+ if let Some(query) = model.active_query.as_ref() {
+ if model.match_ranges.is_empty() {
+ return;
+ }
+ if let Some(active_index) = self.active_match_index {
+ let query = query.clone().with_replacement(self.replacement(cx));
+ self.results_editor.replace(
+ &(Box::new(model.match_ranges[active_index].clone()) as _),
+ &query,
+ cx,
+ );
+ self.select_match(Direction::Next, cx)
+ }
+ }
+ }
+ pub fn replacement(&self, cx: &AppContext) -> String {
+ self.replacement_editor.read(cx).text(cx)
+ }
+ fn replace_all(&mut self, _: &ReplaceAll, cx: &mut ViewContext<Self>) {
+ let model = self.model.read(cx);
+ if let Some(query) = model.active_query.as_ref() {
+ if model.match_ranges.is_empty() {
+ return;
+ }
+ if self.active_match_index.is_some() {
+ let query = query.clone().with_replacement(self.replacement(cx));
+ let matches = model
+ .match_ranges
+ .iter()
+ .map(|item| Box::new(item.clone()) as _)
+ .collect::<Vec<_>>();
+ for item in matches {
+ self.results_editor.replace(&item, &query, cx);
+ }
+ }
+ }
+ }
fn new(
model: ModelHandle<ProjectSearch>,
@@ -852,6 +899,7 @@ impl ProjectSearchView {
) -> Self {
let project;
let excerpts;
+ let mut replacement_text = None;
let mut query_text = String::new();
// Read in settings if available
@@ -871,6 +919,7 @@ impl ProjectSearchView {
excerpts = model.excerpts.clone();
if let Some(active_query) = model.active_query.as_ref() {
query_text = active_query.as_str().to_string();
+ replacement_text = active_query.replacement().map(ToOwned::to_owned);
options = SearchOptions::from_query(active_query);
}
}
@@ -891,7 +940,17 @@ impl ProjectSearchView {
cx.emit(ViewEvent::EditorEvent(event.clone()))
})
.detach();
-
+ let replacement_editor = cx.add_view(|cx| {
+ let mut editor = Editor::single_line(
+ Some(Arc::new(|theme| theme.search.editor.input.clone())),
+ cx,
+ );
+ editor.set_placeholder_text("Replace in project..", cx);
+ if let Some(text) = replacement_text {
+ editor.set_text(text, cx);
+ }
+ editor
+ });
let results_editor = cx.add_view(|cx| {
let mut editor = Editor::for_multibuffer(excerpts, Some(project.clone()), cx);
editor.set_searchable(false);
@@ -945,6 +1004,7 @@ impl ProjectSearchView {
// Check if Worktrees have all been previously indexed
let mut this = ProjectSearchView {
+ replacement_editor,
search_id: model.read(cx).search_id,
model,
query_editor,
@@ -959,6 +1019,7 @@ impl ProjectSearchView {
excluded_files_editor,
filters_enabled,
current_mode,
+ replace_enabled: false,
};
this.model_changed(cx);
this
@@ -1346,6 +1407,26 @@ impl ProjectSearchBar {
}
}
+ fn replace_next(pane: &mut Pane, _: &ReplaceNext, cx: &mut ViewContext<Pane>) {
+ if let Some(search_view) = pane
+ .active_item()
+ .and_then(|item| item.downcast::<ProjectSearchView>())
+ {
+ search_view.update(cx, |view, cx| view.replace_next(&ReplaceNext, cx));
+ } else {
+ cx.propagate_action();
+ }
+ }
+ fn replace_all(pane: &mut Pane, _: &ReplaceAll, cx: &mut ViewContext<Pane>) {
+ if let Some(search_view) = pane
+ .active_item()
+ .and_then(|item| item.downcast::<ProjectSearchView>())
+ {
+ search_view.update(cx, |view, cx| view.replace_all(&ReplaceAll, cx));
+ } else {
+ cx.propagate_action();
+ }
+ }
fn select_prev_match(pane: &mut Pane, _: &SelectPrevMatch, cx: &mut ViewContext<Pane>) {
if let Some(search_view) = pane
.active_item()
@@ -1376,12 +1457,16 @@ impl ProjectSearchBar {
};
active_project_search.update(cx, |project_view, cx| {
- let views = &[
- &project_view.query_editor,
- &project_view.included_files_editor,
- &project_view.excluded_files_editor,
- ];
-
+ let mut views = vec![&project_view.query_editor];
+ if project_view.filters_enabled {
+ views.extend([
+ &project_view.included_files_editor,
+ &project_view.excluded_files_editor,
+ ]);
+ }
+ if project_view.replace_enabled {
+ views.push(&project_view.replacement_editor);
+ }
let current_index = match views
.iter()
.enumerate()
@@ -1417,7 +1502,36 @@ impl ProjectSearchBar {
false
}
}
-
+ fn toggle_replace(&mut self, _: &ToggleReplace, cx: &mut ViewContext<Self>) {
+ if let Some(search) = &self.active_project_search {
+ search.update(cx, |this, cx| {
+ this.replace_enabled = !this.replace_enabled;
+ if !this.replace_enabled {
+ cx.focus(&this.query_editor);
+ }
+ cx.notify();
+ });
+ }
+ }
+ fn toggle_replace_on_a_pane(pane: &mut Pane, _: &ToggleReplace, cx: &mut ViewContext<Pane>) {
+ let mut should_propagate = true;
+ if let Some(search_view) = pane
+ .active_item()
+ .and_then(|item| item.downcast::<ProjectSearchView>())
+ {
+ search_view.update(cx, |this, cx| {
+ should_propagate = false;
+ this.replace_enabled = !this.replace_enabled;
+ if !this.replace_enabled {
+ cx.focus(&this.query_editor);
+ }
+ cx.notify();
+ });
+ }
+ if should_propagate {
+ cx.propagate_action();
+ }
+ }
fn activate_text_mode(pane: &mut Pane, _: &ActivateTextMode, cx: &mut ViewContext<Pane>) {
if let Some(search_view) = pane
.active_item()
@@ -1653,7 +1767,43 @@ impl View for ProjectSearchBar {
.with_style(theme.search.match_index.container)
.aligned()
});
-
+ let should_show_replace_input = search.replace_enabled;
+ let replacement = should_show_replace_input.then(|| {
+ Flex::row()
+ .with_child(
+ Svg::for_style(theme.search.replace_icon.clone().icon)
+ .contained()
+ .with_style(theme.search.replace_icon.clone().container),
+ )
+ .with_child(ChildView::new(&search.replacement_editor, cx).flex(1., true))
+ .align_children_center()
+ .flex(1., true)
+ .contained()
+ .with_style(query_container_style)
+ .constrained()
+ .with_min_width(theme.search.editor.min_width)
+ .with_max_width(theme.search.editor.max_width)
+ .with_height(theme.search.search_bar_row_height)
+ .flex(1., false)
+ });
+ let replace_all = should_show_replace_input.then(|| {
+ super::replace_action(
+ ReplaceAll,
+ "Replace all",
+ "icons/replace_all.svg",
+ theme.tooltip.clone(),
+ theme.search.action_button.clone(),
+ )
+ });
+ let replace_next = should_show_replace_input.then(|| {
+ super::replace_action(
+ ReplaceNext,
+ "Replace next",
+ "icons/replace_next.svg",
+ theme.tooltip.clone(),
+ theme.search.action_button.clone(),
+ )
+ });
let query_column = Flex::column()
.with_spacing(theme.search.search_row_spacing)
.with_child(
@@ -1706,7 +1856,17 @@ impl View for ProjectSearchBar {
.flex(1., false)
}))
.flex(1., false);
-
+ let switches_column = Flex::row()
+ .align_children_center()
+ .with_child(super::toggle_replace_button(
+ search.replace_enabled,
+ theme.tooltip.clone(),
+ theme.search.option_button_component.clone(),
+ ))
+ .constrained()
+ .with_height(theme.search.search_bar_row_height)
+ .contained()
+ .with_style(theme.search.option_button_group);
let mode_column =
Flex::row()
.with_child(search_button_for_mode(
@@ -1744,6 +1904,8 @@ impl View for ProjectSearchBar {
};
let nav_column = Flex::row()
+ .with_children(replace_next)
+ .with_children(replace_all)
.with_child(Flex::row().with_children(matches))
.with_child(nav_button_for_direction("<", Direction::Prev, cx))
.with_child(nav_button_for_direction(">", Direction::Next, cx))
@@ -1753,6 +1915,8 @@ impl View for ProjectSearchBar {
Flex::row()
.with_child(query_column)
+ .with_child(switches_column)
+ .with_children(replacement)
.with_child(mode_column)
.with_child(nav_column)
.contained()