From bce501c6960ec86e1ac7e445c051c106424f7a40 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Wed, 13 Aug 2025 11:00:01 +0200 Subject: [PATCH] Render query text red in project search if no results are found --- crates/search/src/buffer_search.rs | 75 +++-------- crates/search/src/project_search.rs | 200 ++++++++++++---------------- crates/search/src/search_bar.rs | 44 +++++- 3 files changed, 147 insertions(+), 172 deletions(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 60e88e9d27b0d9fd29fcb08c77cf933522353b28..341cbf8d2bf383149dde464e92887504595ac544 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -4,20 +4,20 @@ use crate::{ FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions, SelectAllMatches, SelectNextMatch, SelectPreviousMatch, ToggleCaseSensitive, ToggleRegex, ToggleReplace, ToggleSelection, ToggleWholeWord, - search_bar::{input_base_styles, render_nav_button, toggle_replace_button}, + search_bar::{input_base_styles, render_nav_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, + Styled, Subscription, Task, Window, actions, div, }; use language::{Language, LanguageRegistry}; use project::{ @@ -28,7 +28,6 @@ use schemars::JsonSchema; use serde::Deserialize; use settings::Settings; use std::sync::Arc; -use theme::ThemeSettings; use zed_actions::outline::ToggleOutline; use ui::{ @@ -126,46 +125,6 @@ pub struct BufferSearchBar { } impl BufferSearchBar { - fn render_text_input( - &self, - editor: &Entity, - color_override: Option, - cx: &mut Context, - ) -> 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 } @@ -251,13 +210,13 @@ impl Render for BufferSearchBar { 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)) + .child(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( + .when(supported_options.case, |div| { + div.child(self.render_search_option_button( SearchOptions::CASE_SENSITIVE, focus_handle.clone(), cx.listener(|this, _, window, cx| { @@ -267,26 +226,26 @@ impl Render for BufferSearchBar { cx, ) }), - ) - })) - .children(supported_options.word.then(|| { - self.render_search_option_button( + )) + }) + .when(supported_options.word, |div| { + div.child(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( + )) + }) + .when(supported_options.regex, |div| { + div.child(self.render_search_option_button( SearchOptions::REGEX, focus_handle.clone(), cx.listener(|this, _, window, cx| { this.toggle_regex(&ToggleRegex, window, cx) }), - ) - })), + )) + }), ) }), ) @@ -404,7 +363,7 @@ impl Render for BufferSearchBar { h_flex() .gap_2() .child( - input_base_styles(replacement_border).child(self.render_text_input( + input_base_styles(replacement_border).child(render_text_input( &self.replacement_editor, None, cx, diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index a1fa2931bbbb8deb0ca267ebd020f8712d3b0e0f..7e036358ed35e6f8b64defaaa888895c0e4d02c4 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -3,20 +3,20 @@ use crate::{ SearchOptions, SelectNextMatch, SelectPreviousMatch, ToggleCaseSensitive, ToggleIncludeIgnored, ToggleRegex, ToggleReplace, ToggleWholeWord, buffer_search::Deploy, - search_bar::{input_base_styles, toggle_replace_button}, + search_bar::{input_base_styles, render_text_input, toggle_replace_button}, }; use anyhow::Context as _; use collections::{HashMap, HashSet}; 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::SelectAll, 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; @@ -34,7 +34,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, @@ -1917,37 +1916,6 @@ impl ProjectSearchBar { }) } } - - fn render_text_input(&self, editor: &Entity, cx: &Context) -> 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 { @@ -1973,6 +1941,35 @@ impl Render for ProjectSearchBar { }) }; + let project_search = search.entity.read(cx); + let limit_reached = project_search.limit_reached; + + 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(BaseStyle::SingleInput, InputPanel::Query) .on_action(cx.listener(|this, action, window, cx| this.confirm(action, window, cx))) .on_action(cx.listener(|this, action, window, cx| { @@ -1981,7 +1978,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() @@ -2050,26 +2047,6 @@ impl Render for ProjectSearchBar { }), )); - 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 matches_column = h_flex() .pl_2() .ml_2() @@ -2149,62 +2126,59 @@ impl Render for ProjectSearchBar { 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)); + .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( + 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, + ) + } + }), + ); h_flex() .w_full() @@ -2229,7 +2203,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.included_files_editor, cx)), + .child(render_text_input(&search.included_files_editor, None, cx)), ) .child( input_base_styles(BaseStyle::MultipleInputs, InputPanel::Exclude) @@ -2239,7 +2213,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.excluded_files_editor, cx)), + .child(render_text_input(&search.excluded_files_editor, None, cx)), ), ) .child( diff --git a/crates/search/src/search_bar.rs b/crates/search/src/search_bar.rs index 60d98a3a9ff78b4734ff948bc918c55b666b680f..ba779beeb3bed15b224dccdfcbb3dc3756228025 100644 --- a/crates/search/src/search_bar.rs +++ b/crates/search/src/search_bar.rs @@ -1,4 +1,7 @@ -use gpui::{Action, FocusHandle, Hsla, 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::*}; @@ -60,3 +63,42 @@ pub(crate) fn toggle_replace_button( } }) } + +pub(crate) fn render_text_input( + editor: &Entity, + color_override: Option, + 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) +}