Render query text red in project search if no results are found

This commit is contained in:
Lukas Wirth 2025-08-13 11:00:01 +02:00
parent e354159f77
commit bce501c696
3 changed files with 147 additions and 172 deletions

View file

@ -4,20 +4,20 @@ use crate::{
FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions, FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions,
SelectAllMatches, SelectNextMatch, SelectPreviousMatch, ToggleCaseSensitive, ToggleRegex, SelectAllMatches, SelectNextMatch, SelectPreviousMatch, ToggleCaseSensitive, ToggleRegex,
ToggleReplace, ToggleSelection, ToggleWholeWord, 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 any_vec::AnyVec;
use anyhow::Context as _; use anyhow::Context as _;
use collections::HashMap; use collections::HashMap;
use editor::{ use editor::{
DisplayPoint, Editor, EditorElement, EditorSettings, EditorStyle, DisplayPoint, Editor, EditorSettings,
actions::{Backtab, Tab}, actions::{Backtab, Tab},
}; };
use futures::channel::oneshot; use futures::channel::oneshot;
use gpui::{ use gpui::{
Action, App, ClickEvent, Context, Entity, EventEmitter, FocusHandle, Focusable, Action, App, ClickEvent, Context, Entity, EventEmitter, FocusHandle, Focusable,
InteractiveElement as _, IntoElement, KeyContext, ParentElement as _, Render, ScrollHandle, 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 language::{Language, LanguageRegistry};
use project::{ use project::{
@ -28,7 +28,6 @@ use schemars::JsonSchema;
use serde::Deserialize; use serde::Deserialize;
use settings::Settings; use settings::Settings;
use std::sync::Arc; use std::sync::Arc;
use theme::ThemeSettings;
use zed_actions::outline::ToggleOutline; use zed_actions::outline::ToggleOutline;
use ui::{ use ui::{
@ -126,46 +125,6 @@ pub struct BufferSearchBar {
} }
impl 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 { pub fn query_editor_focused(&self) -> bool {
self.query_editor_focused self.query_editor_focused
} }
@ -251,13 +210,13 @@ impl Render for BufferSearchBar {
input_base_styles(query_border) input_base_styles(query_border)
.id("editor-scroll") .id("editor-scroll")
.track_scroll(&self.editor_scroll_handle) .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| { .when(!hide_inline_icons, |div| {
div.child( div.child(
h_flex() h_flex()
.gap_1() .gap_1()
.children(supported_options.case.then(|| { .when(supported_options.case, |div| {
self.render_search_option_button( div.child(self.render_search_option_button(
SearchOptions::CASE_SENSITIVE, SearchOptions::CASE_SENSITIVE,
focus_handle.clone(), focus_handle.clone(),
cx.listener(|this, _, window, cx| { cx.listener(|this, _, window, cx| {
@ -267,26 +226,26 @@ impl Render for BufferSearchBar {
cx, cx,
) )
}), }),
) ))
})) })
.children(supported_options.word.then(|| { .when(supported_options.word, |div| {
self.render_search_option_button( div.child(self.render_search_option_button(
SearchOptions::WHOLE_WORD, SearchOptions::WHOLE_WORD,
focus_handle.clone(), focus_handle.clone(),
cx.listener(|this, _, window, cx| { cx.listener(|this, _, window, cx| {
this.toggle_whole_word(&ToggleWholeWord, window, cx) this.toggle_whole_word(&ToggleWholeWord, window, cx)
}), }),
) ))
})) })
.children(supported_options.regex.then(|| { .when(supported_options.regex, |div| {
self.render_search_option_button( div.child(self.render_search_option_button(
SearchOptions::REGEX, SearchOptions::REGEX,
focus_handle.clone(), focus_handle.clone(),
cx.listener(|this, _, window, cx| { cx.listener(|this, _, window, cx| {
this.toggle_regex(&ToggleRegex, window, cx) this.toggle_regex(&ToggleRegex, window, cx)
}), }),
) ))
})), }),
) )
}), }),
) )
@ -404,7 +363,7 @@ impl Render for BufferSearchBar {
h_flex() h_flex()
.gap_2() .gap_2()
.child( .child(
input_base_styles(replacement_border).child(self.render_text_input( input_base_styles(replacement_border).child(render_text_input(
&self.replacement_editor, &self.replacement_editor,
None, None,
cx, cx,

View file

@ -3,20 +3,20 @@ use crate::{
SearchOptions, SelectNextMatch, SelectPreviousMatch, ToggleCaseSensitive, ToggleIncludeIgnored, SearchOptions, SelectNextMatch, SelectPreviousMatch, ToggleCaseSensitive, ToggleIncludeIgnored,
ToggleRegex, ToggleReplace, ToggleWholeWord, ToggleRegex, ToggleReplace, ToggleWholeWord,
buffer_search::Deploy, 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 anyhow::Context as _;
use collections::{HashMap, HashSet}; use collections::{HashMap, HashSet};
use editor::{ use editor::{
Anchor, Editor, EditorElement, EditorEvent, EditorSettings, EditorStyle, MAX_TAB_TITLE_LEN, Anchor, Editor, EditorEvent, EditorSettings, MAX_TAB_TITLE_LEN, MultiBuffer, SelectionEffects,
MultiBuffer, SelectionEffects, actions::SelectAll, items::active_match_index, actions::SelectAll, items::active_match_index,
}; };
use futures::{StreamExt, stream::FuturesOrdered}; use futures::{StreamExt, stream::FuturesOrdered};
use gpui::{ use gpui::{
Action, AnyElement, AnyView, App, Axis, Context, Entity, EntityId, EventEmitter, FocusHandle, Action, AnyElement, AnyView, App, Axis, Context, Entity, EntityId, EventEmitter, FocusHandle,
Focusable, Global, Hsla, InteractiveElement, IntoElement, KeyContext, ParentElement, Point, Focusable, Global, Hsla, InteractiveElement, IntoElement, KeyContext, ParentElement, Point,
Render, SharedString, Styled, Subscription, Task, TextStyle, UpdateGlobal, WeakEntity, Window, Render, SharedString, Styled, Subscription, Task, UpdateGlobal, WeakEntity, Window, actions,
actions, div, div,
}; };
use language::{Buffer, Language}; use language::{Buffer, Language};
use menu::Confirm; use menu::Confirm;
@ -34,7 +34,6 @@ use std::{
pin::pin, pin::pin,
sync::Arc, sync::Arc,
}; };
use theme::ThemeSettings;
use ui::{ use ui::{
Icon, IconButton, IconButtonShape, IconName, KeyBinding, Label, LabelCommon, LabelSize, Icon, IconButton, IconButtonShape, IconName, KeyBinding, Label, LabelCommon, LabelSize,
Toggleable, Tooltip, h_flex, prelude::*, utils::SearchInputWidth, v_flex, Toggleable, Tooltip, h_flex, prelude::*, utils::SearchInputWidth, v_flex,
@ -1917,37 +1916,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 { 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) 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| this.confirm(action, window, cx)))
.on_action(cx.listener(|this, action, window, cx| { .on_action(cx.listener(|this, action, window, cx| {
@ -1981,7 +1978,7 @@ impl Render for ProjectSearchBar {
.on_action( .on_action(
cx.listener(|this, action, window, cx| this.next_history_query(action, window, cx)), 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( .child(
h_flex() h_flex()
.gap_1() .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() let matches_column = h_flex()
.pl_2() .pl_2()
.ml_2() .ml_2()
@ -2149,62 +2126,59 @@ impl Render for ProjectSearchBar {
let replace_line = search.replace_enabled.then(|| { let replace_line = search.replace_enabled.then(|| {
let replace_column = input_base_styles(BaseStyle::SingleInput, InputPanel::Replacement) 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 focus_handle = search.replacement_editor.read(cx).focus_handle(cx);
let replace_actions = let replace_actions = h_flex()
h_flex() .min_w_64()
.min_w_64() .gap_1()
.gap_1() .child(
.when(search.replace_enabled, |this| { IconButton::new("project-search-replace-next", IconName::ReplaceNext)
this.child( .shape(IconButtonShape::Square)
IconButton::new("project-search-replace-next", IconName::ReplaceNext) .on_click(cx.listener(|this, _, window, cx| {
.shape(IconButtonShape::Square) if let Some(search) = this.active_project_search.as_ref() {
.on_click(cx.listener(|this, _, window, cx| { search.update(cx, |this, cx| {
if let Some(search) = this.active_project_search.as_ref() { this.replace_next(&ReplaceNext, window, cx);
search.update(cx, |this, cx| { })
this.replace_next(&ReplaceNext, window, cx); }
}) }))
} .tooltip({
})) let focus_handle = focus_handle.clone();
.tooltip({ move |window, cx| {
let focus_handle = focus_handle.clone(); Tooltip::for_action_in(
move |window, cx| { "Replace Next Match",
Tooltip::for_action_in( &ReplaceNext,
"Replace Next Match", &focus_handle,
&ReplaceNext, window,
&focus_handle, cx,
window, )
cx, }
) }),
} )
}), .child(
) IconButton::new("project-search-replace-all", IconName::ReplaceAll)
.child( .shape(IconButtonShape::Square)
IconButton::new("project-search-replace-all", IconName::ReplaceAll) .on_click(cx.listener(|this, _, window, cx| {
.shape(IconButtonShape::Square) if let Some(search) = this.active_project_search.as_ref() {
.on_click(cx.listener(|this, _, window, cx| { search.update(cx, |this, cx| {
if let Some(search) = this.active_project_search.as_ref() { this.replace_all(&ReplaceAll, window, cx);
search.update(cx, |this, cx| { })
this.replace_all(&ReplaceAll, window, cx); }
}) }))
} .tooltip({
})) let focus_handle = focus_handle.clone();
.tooltip({ move |window, cx| {
let focus_handle = focus_handle.clone(); Tooltip::for_action_in(
move |window, cx| { "Replace All Matches",
Tooltip::for_action_in( &ReplaceAll,
"Replace All Matches", &focus_handle,
&ReplaceAll, window,
&focus_handle, cx,
window, )
cx, }
) }),
} );
}),
)
});
h_flex() h_flex()
.w_full() .w_full()
@ -2229,7 +2203,7 @@ impl Render for ProjectSearchBar {
.on_action(cx.listener(|this, action, window, cx| { .on_action(cx.listener(|this, action, window, cx| {
this.next_history_query(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( .child(
input_base_styles(BaseStyle::MultipleInputs, InputPanel::Exclude) input_base_styles(BaseStyle::MultipleInputs, InputPanel::Exclude)
@ -2239,7 +2213,7 @@ impl Render for ProjectSearchBar {
.on_action(cx.listener(|this, action, window, cx| { .on_action(cx.listener(|this, action, window, cx| {
this.next_history_query(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( .child(

View file

@ -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::{IconButton, IconButtonShape};
use ui::{Tooltip, prelude::*}; use ui::{Tooltip, prelude::*};
@ -60,3 +63,42 @@ pub(crate) fn toggle_replace_button(
} }
}) })
} }
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)
}