Add regex highlights in the query input (#25005)

Kirill Bulatov created

Closes https://github.com/zed-industries/zed/discussions/24994

Release Notes:

- Added regex highlights in the query input

Change summary

crates/assistant/src/assistant_panel.rs    |  12 ++
crates/search/src/buffer_search.rs         | 123 ++++++++++++++++++-----
crates/search/src/project_search.rs        |   3 
crates/terminal_view/src/terminal_panel.rs |   4 
crates/vim/src/test/vim_test_context.rs    |   2 
crates/zed/src/zed.rs                      |   8 +
6 files changed, 117 insertions(+), 35 deletions(-)

Detailed changes

crates/assistant/src/assistant_panel.rs πŸ”—

@@ -279,7 +279,17 @@ impl AssistantPanel {
             });
             pane.toolbar().update(cx, |toolbar, cx| {
                 toolbar.add_item(context_editor_toolbar.clone(), window, cx);
-                toolbar.add_item(cx.new(|cx| BufferSearchBar::new(window, cx)), window, cx)
+                toolbar.add_item(
+                    cx.new(|cx| {
+                        BufferSearchBar::new(
+                            Some(workspace.project().read(cx).languages().clone()),
+                            window,
+                            cx,
+                        )
+                    }),
+                    window,
+                    cx,
+                )
             });
             pane
         });

crates/search/src/buffer_search.rs πŸ”—

@@ -6,6 +6,7 @@ use crate::{
     ToggleCaseSensitive, ToggleRegex, ToggleReplace, ToggleSelection, ToggleWholeWord,
 };
 use any_vec::AnyVec;
+use anyhow::Context as _;
 use collections::HashMap;
 use editor::{
     actions::{Tab, TabPrev},
@@ -14,9 +15,10 @@ use editor::{
 use futures::channel::oneshot;
 use gpui::{
     actions, div, impl_actions, Action, App, ClickEvent, Context, Entity, EventEmitter,
-    FocusHandle, Focusable, Hsla, InteractiveElement as _, IntoElement, KeyContext,
-    ParentElement as _, Render, ScrollHandle, Styled, Subscription, Task, TextStyle, Window,
+    FocusHandle, Focusable, InteractiveElement as _, IntoElement, KeyContext, ParentElement as _,
+    Render, ScrollHandle, Styled, Subscription, Task, TextStyle, Window,
 };
+use language::{Language, LanguageRegistry};
 use project::{
     search::SearchQuery,
     search_history::{SearchHistory, SearchHistoryCursor},
@@ -108,41 +110,48 @@ pub struct BufferSearchBar {
     scroll_handle: ScrollHandle,
     editor_scroll_handle: ScrollHandle,
     editor_needed_width: Pixels,
+    regex_language: Option<Arc<Language>>,
 }
 
 impl BufferSearchBar {
     fn render_text_input(
         &self,
         editor: &Entity<Editor>,
-        color: Hsla,
-
+        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: if editor.read(cx).read_only(cx) {
-                cx.theme().colors().text_disabled
-            } else {
-                color
-            },
+            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),
-            ..Default::default()
+            ..TextStyle::default()
         };
 
-        EditorElement::new(
-            editor,
-            EditorStyle {
-                background: cx.theme().colors().editor_background,
-                local_player: cx.theme().players().local(),
-                text: text_style,
-                ..Default::default()
-            },
-        )
+        let mut editor_style = EditorStyle {
+            background: cx.theme().colors().editor_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 {
@@ -179,7 +188,7 @@ impl Render for BufferSearchBar {
             editor.set_placeholder_text("Replace with…", cx);
         });
 
-        let mut text_color = Color::Default;
+        let mut color_override = None;
         let match_text = self
             .active_searchable_item
             .as_ref()
@@ -195,7 +204,7 @@ impl Render for BufferSearchBar {
                 if let Some(match_ix) = self.active_match_index {
                     Some(format!("{}/{}", match_ix + 1, matches_count))
                 } else {
-                    text_color = Color::Error; // No matches found
+                    color_override = Some(Color::Error); // No matches found
                     None
                 }
             })
@@ -239,7 +248,7 @@ impl Render for BufferSearchBar {
                 input_base_styles()
                     .id("editor-scroll")
                     .track_scroll(&self.editor_scroll_handle)
-                    .child(self.render_text_input(&self.query_editor, text_color.color(cx), cx))
+                    .child(self.render_text_input(&self.query_editor, color_override, cx))
                     .when(!hide_inline_icons, |div| {
                         div.child(
                             h_flex()
@@ -412,7 +421,7 @@ impl Render for BufferSearchBar {
                 .gap_2()
                 .child(input_base_styles().child(self.render_text_input(
                     &self.replacement_editor,
-                    cx.theme().colors().text,
+                    None,
                     cx,
                 )))
                 .child(
@@ -652,7 +661,11 @@ impl BufferSearchBar {
         }))
     }
 
-    pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
+    pub fn new(
+        languages: Option<Arc<LanguageRegistry>>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> Self {
         let query_editor = cx.new(|cx| Editor::single_line(window, cx));
         cx.subscribe_in(&query_editor, window, Self::on_query_editor_event)
             .detach();
@@ -661,6 +674,32 @@ impl BufferSearchBar {
             .detach();
 
         let search_options = SearchOptions::from_settings(&EditorSettings::get_global(cx).search);
+        if let Some(languages) = languages {
+            let query_buffer = query_editor
+                .read(cx)
+                .buffer()
+                .read(cx)
+                .as_singleton()
+                .expect("query editor should be backed by a singleton buffer");
+            query_buffer.update(cx, |query_buffer, _| {
+                query_buffer.set_language_registry(languages.clone());
+            });
+
+            cx.spawn(|buffer_search_bar, mut cx| async move {
+                let regex_language = languages
+                    .language_for_name("regex")
+                    .await
+                    .context("loading regex language")?;
+                buffer_search_bar
+                    .update(&mut cx, |buffer_search_bar, cx| {
+                        buffer_search_bar.regex_language = Some(regex_language);
+                        buffer_search_bar.adjust_query_regex_language(cx);
+                    })
+                    .ok();
+                anyhow::Ok(())
+            })
+            .detach_and_log_err(cx);
+        }
 
         Self {
             query_editor,
@@ -688,6 +727,7 @@ impl BufferSearchBar {
             scroll_handle: ScrollHandle::new(),
             editor_scroll_handle: ScrollHandle::new(),
             editor_needed_width: px(0.),
+            regex_language: None,
         }
     }
 
@@ -910,6 +950,7 @@ impl BufferSearchBar {
         self.search_options.toggle(search_option);
         self.default_options = self.search_options;
         drop(self.update_matches(false, window, cx));
+        self.adjust_query_regex_language(cx);
         cx.notify();
     }
 
@@ -1429,6 +1470,28 @@ impl BufferSearchBar {
             }
         }
     }
+
+    fn adjust_query_regex_language(&self, cx: &mut App) {
+        let enable = self.search_options.contains(SearchOptions::REGEX);
+        let query_buffer = self
+            .query_editor
+            .read(cx)
+            .buffer()
+            .read(cx)
+            .as_singleton()
+            .expect("query editor should be backed by a singleton buffer");
+        if enable {
+            if let Some(regex_language) = self.regex_language.clone() {
+                query_buffer.update(cx, |query_buffer, cx| {
+                    query_buffer.set_language(Some(regex_language), cx);
+                })
+            }
+        } else {
+            query_buffer.update(cx, |query_buffer, cx| {
+                query_buffer.set_language(None, cx);
+            })
+        }
+    }
 }
 
 #[cfg(test)]
@@ -1482,7 +1545,7 @@ mod tests {
             cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
 
         let search_bar = cx.new_window_entity(|window, cx| {
-            let mut search_bar = BufferSearchBar::new(window, cx);
+            let mut search_bar = BufferSearchBar::new(None, window, cx);
             search_bar.set_active_pane_item(Some(&editor), window, cx);
             search_bar.show(window, cx);
             search_bar
@@ -1851,7 +1914,7 @@ mod tests {
         });
 
         let search_bar = window.build_entity(cx, |window, cx| {
-            let mut search_bar = BufferSearchBar::new(window, cx);
+            let mut search_bar = BufferSearchBar::new(None, window, cx);
             search_bar.set_active_pane_item(Some(&editor), window, cx);
             search_bar.show(window, cx);
             search_bar
@@ -2059,7 +2122,7 @@ mod tests {
             cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
 
         let search_bar = cx.new_window_entity(|window, cx| {
-            let mut search_bar = BufferSearchBar::new(window, cx);
+            let mut search_bar = BufferSearchBar::new(None, window, cx);
             search_bar.set_active_pane_item(Some(&editor), window, cx);
             search_bar.show(window, cx);
             search_bar
@@ -2133,7 +2196,7 @@ mod tests {
             cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
 
         let search_bar = cx.new_window_entity(|window, cx| {
-            let mut search_bar = BufferSearchBar::new(window, cx);
+            let mut search_bar = BufferSearchBar::new(None, window, cx);
             search_bar.set_active_pane_item(Some(&editor), window, cx);
             search_bar.show(window, cx);
             search_bar
@@ -2513,7 +2576,7 @@ mod tests {
             cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx));
 
         let search_bar = cx.new_window_entity(|window, cx| {
-            let mut search_bar = BufferSearchBar::new(window, cx);
+            let mut search_bar = BufferSearchBar::new(None, window, cx);
             search_bar.set_active_pane_item(Some(&editor), window, cx);
             search_bar.show(window, cx);
             search_bar
@@ -2596,7 +2659,7 @@ mod tests {
         });
 
         let search_bar = cx.new_window_entity(|window, cx| {
-            let mut search_bar = BufferSearchBar::new(window, cx);
+            let mut search_bar = BufferSearchBar::new(None, window, cx);
             search_bar.set_active_pane_item(Some(&editor), window, cx);
             search_bar.show(window, cx);
             search_bar

crates/search/src/project_search.rs πŸ”—

@@ -3804,7 +3804,8 @@ pub mod tests {
         cx.run_until_parked();
 
         let buffer_search_bar = cx.new_window_entity(|window, cx| {
-            let mut search_bar = BufferSearchBar::new(window, cx);
+            let mut search_bar =
+                BufferSearchBar::new(Some(project.read(cx).languages().clone()), window, cx);
             search_bar.set_active_pane_item(Some(&editor), window, cx);
             search_bar.show(window, cx);
             search_bar

crates/terminal_view/src/terminal_panel.rs πŸ”—

@@ -980,7 +980,9 @@ pub fn new_terminal_pane(
             false
         })));
 
-        let buffer_search_bar = cx.new(|cx| search::BufferSearchBar::new(window, cx));
+        let buffer_search_bar = cx.new(|cx| {
+            search::BufferSearchBar::new(Some(project.read(cx).languages().clone()), window, cx)
+        });
         let breadcrumbs = cx.new(|_| Breadcrumbs::new());
         pane.toolbar().update(cx, |toolbar, cx| {
             toolbar.add_item(buffer_search_bar, window, cx);

crates/vim/src/test/vim_test_context.rs πŸ”—

@@ -78,7 +78,7 @@ impl VimTestContext {
         cx.update_workspace(|workspace, window, cx| {
             workspace.active_pane().update(cx, |pane, cx| {
                 pane.toolbar().update(cx, |toolbar, cx| {
-                    let buffer_search_bar = cx.new(|cx| BufferSearchBar::new(window, cx));
+                    let buffer_search_bar = cx.new(|cx| BufferSearchBar::new(None, window, cx));
                     toolbar.add_item(buffer_search_bar, window, cx);
 
                     let project_search_bar = cx.new(|_| ProjectSearchBar::new());

crates/zed/src/zed.rs πŸ”—

@@ -885,7 +885,13 @@ fn initialize_pane(
             toolbar.add_item(multibuffer_hint, window, cx);
             let breadcrumbs = cx.new(|_| Breadcrumbs::new());
             toolbar.add_item(breadcrumbs, window, cx);
-            let buffer_search_bar = cx.new(|cx| search::BufferSearchBar::new(window, cx));
+            let buffer_search_bar = cx.new(|cx| {
+                search::BufferSearchBar::new(
+                    Some(workspace.project().read(cx).languages().clone()),
+                    window,
+                    cx,
+                )
+            });
             toolbar.add_item(buffer_search_bar.clone(), window, cx);
             let proposed_change_bar = cx.new(|_| ProposedChangesEditorToolbar::new());
             toolbar.add_item(proposed_change_bar, window, cx);