From 3fde4e285ea4d40ac7d6fcb75bb0357c4179b4e0 Mon Sep 17 00:00:00 2001 From: Xin Zhao Date: Mon, 1 Jun 2026 14:34:37 +0800 Subject: [PATCH] project: Treat replacement literally when dealing non-ASCII text search (#56123) Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Closes #55503 In #28752, to support case-insensitive search for non-ASCII queries, Zed internally falls back to a regex search. However, this also affected the replacement behavior, as the replacement code implemented a simple match logic: https://github.com/zed-industries/zed/blob/dccea211edfed189db0704ef1247e446aca81150/crates/project/src/search.rs#L452-L457 Since the regex fallback is an internal implementation detail (the user never enabled regex mode), the replacement should behave the same as a normal text replacement. This PR fixes that. Release Notes: - Fixed replacement text being treated as a regex pattern when performing case-insensitive text search with non-ASCII characters. --- crates/project/src/search.rs | 50 +++++++++++++++++++----------- crates/search/src/buffer_search.rs | 28 +++++++++++++++++ 2 files changed, 60 insertions(+), 18 deletions(-) diff --git a/crates/project/src/search.rs b/crates/project/src/search.rs index 83b4c585f14..27f7e18ecc7 100644 --- a/crates/project/src/search.rs +++ b/crates/project/src/search.rs @@ -78,6 +78,7 @@ pub enum SearchQuery { include_ignored: bool, one_match_per_line: bool, inner: SearchInputs, + escaped: bool, }, } @@ -169,6 +170,7 @@ impl SearchQuery { include_ignored, one_match_per_line, inner, + false, ) } @@ -202,6 +204,7 @@ impl SearchQuery { include_ignored, false, inner, + true, ) } @@ -212,6 +215,7 @@ impl SearchQuery { include_ignored: bool, one_match_per_line: bool, inner: SearchInputs, + escaped: bool, ) -> Result { if let Some((case_sensitive_from_pattern, new_pattern)) = Self::case_sensitive_from_pattern(&pattern) @@ -253,6 +257,7 @@ impl SearchQuery { include_ignored, inner, one_match_per_line, + escaped, }) } @@ -450,27 +455,36 @@ impl SearchQuery { /// Replaces search hits if replacement is set. `text` is assumed to be a string that matches this `SearchQuery` exactly, without any leftovers on either side. pub fn replacement_for<'a>(&self, text: &'a str) -> Option> { match self { - SearchQuery::Text { replacement, .. } => replacement.clone().map(Cow::from), + SearchQuery::Text { replacement, .. } + | SearchQuery::Regex { + replacement, + escaped: true, + .. + } => replacement.clone().map(Cow::from), + SearchQuery::Regex { - regex, replacement, .. + regex, + replacement: Some(replacement), + escaped: false, + .. } => { - if let Some(replacement) = replacement { - static TEXT_REPLACEMENT_SPECIAL_CHARACTERS_REGEX: LazyLock = - LazyLock::new(|| Regex::new(r"\\\\|\\n|\\t").unwrap()); - let replacement = TEXT_REPLACEMENT_SPECIAL_CHARACTERS_REGEX.replace_all( - replacement, - |c: &Captures| match c.get(0).unwrap().as_str() { - r"\\" => "\\", - r"\n" => "\n", - r"\t" => "\t", - x => unreachable!("Unexpected escape sequence: {}", x), - }, - ); - Some(regex.replace(text, replacement)) - } else { - None - } + static TEXT_REPLACEMENT_SPECIAL_CHARACTERS_REGEX: LazyLock = + LazyLock::new(|| Regex::new(r"\\\\|\\n|\\t").unwrap()); + let replacement = TEXT_REPLACEMENT_SPECIAL_CHARACTERS_REGEX.replace_all( + replacement, + |c: &Captures| match c.get(0).unwrap().as_str() { + r"\\" => "\\", + r"\n" => "\n", + r"\t" => "\t", + x => unreachable!("Unexpected escape sequence: {}", x), + }, + ); + Some(regex.replace(text, replacement)) } + + SearchQuery::Regex { + replacement: None, .. + } => None, } } diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index ea03a1a7f9f..07c91d7659d 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -4036,6 +4036,34 @@ mod tests { }); } + #[gpui::test] + async fn test_replace_with_non_ascii_characters(cx: &mut TestAppContext) { + let (editor, search_bar, cx) = init_test(cx); + + editor.update_in(cx, |editor, window, cx| { + editor.set_text("¥100 ¥200 ¥100", window, cx) + }); + + search_bar + .update_in(cx, |search_bar, window, cx| { + search_bar.search("¥", None, true, window, cx) + }) + .await + .unwrap(); + + search_bar.update_in(cx, |search_bar, window, cx| { + search_bar.replacement_editor.update(cx, |editor, cx| { + editor.set_text("\\n", window, cx); + }); + search_bar.replace_all(&ReplaceAll, window, cx) + }); + + assert_eq!( + editor.read_with(cx, |this, cx| this.text(cx)), + "\\n100 \\n200 \\n100" + ); + } + fn update_search_settings(search_settings: SearchSettings, cx: &mut TestAppContext) { cx.update(|cx| { SettingsStore::update_global(cx, |store, cx| {