From 4785520d991a459517858dda47075fc3379f5e98 Mon Sep 17 00:00:00 2001 From: Mayfield Date: Mon, 25 Mar 2024 07:21:04 -0400 Subject: [PATCH] Support newline and tab literals in regex search-and-replace operations (#9609) Closes #7645 Release Notes: - Added support for inserting newlines (`\n`) and tabs (`\t`) in editor Regex search replacements ([#7645](https://github.com/zed-industries/zed/issues/7645)). --- crates/project/src/search.rs | 16 ++++- crates/search/src/buffer_search.rs | 108 +++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+), 2 deletions(-) diff --git a/crates/project/src/search.rs b/crates/project/src/search.rs index 4b872aa5558f82e5425f016e107f29a6e8630b00..5df9748f97aadc325db7ac5c2644600907848ace 100644 --- a/crates/project/src/search.rs +++ b/crates/project/src/search.rs @@ -3,17 +3,19 @@ use anyhow::{Context, Result}; use client::proto; use itertools::Itertools; use language::{char_kind, BufferSnapshot}; -use regex::{Regex, RegexBuilder}; +use regex::{Captures, Regex, RegexBuilder}; use smol::future::yield_now; use std::{ borrow::Cow, io::{BufRead, BufReader, Read}, ops::Range, path::Path, - sync::Arc, + sync::{Arc, OnceLock}, }; use util::paths::PathMatcher; +static TEXT_REPLACEMENT_SPECIAL_CHARACTERS_REGEX: OnceLock = OnceLock::new(); + #[derive(Clone, Debug)] pub struct SearchInputs { query: Arc, @@ -231,6 +233,16 @@ impl SearchQuery { regex, replacement, .. } => { if let Some(replacement) = replacement { + let replacement = TEXT_REPLACEMENT_SPECIAL_CHARACTERS_REGEX + .get_or_init(|| Regex::new(r"\\\\|\\n|\\t").unwrap()) + .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 diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index f14ad4100e47e29520207c8933257ec2bf0b5694..d1497cf49ff2008445c76226d396a42d3ca4ee3d 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -1918,6 +1918,114 @@ mod tests { ); } + struct ReplacementTestParams<'a> { + editor: &'a View, + search_bar: &'a View, + cx: &'a mut VisualTestContext, + search_mode: SearchMode, + search_text: &'static str, + search_options: Option, + replacement_text: &'static str, + replace_all: bool, + expected_text: String, + } + + async fn run_replacement_test(options: ReplacementTestParams<'_>) { + options + .search_bar + .update(options.cx, |search_bar, cx| { + search_bar.activate_search_mode(options.search_mode, cx); + search_bar.search(options.search_text, options.search_options, cx) + }) + .await + .unwrap(); + + options.search_bar.update(options.cx, |search_bar, cx| { + search_bar.replacement_editor.update(cx, |editor, cx| { + editor.set_text(options.replacement_text, cx); + }); + + if options.replace_all { + search_bar.replace_all(&ReplaceAll, cx) + } else { + search_bar.replace_next(&ReplaceNext, cx) + } + }); + + assert_eq!( + options + .editor + .update(options.cx, |this, cx| { this.text(cx) }), + options.expected_text + ); + } + + #[gpui::test] + async fn test_replace_special_characters(cx: &mut TestAppContext) { + let (editor, search_bar, cx) = init_test(cx); + + run_replacement_test(ReplacementTestParams { + editor: &editor, + search_bar: &search_bar, + cx, + search_mode: SearchMode::Text, + search_text: "expression", + search_options: None, + replacement_text: r"\n", + replace_all: true, + expected_text: r#" + A regular \n (shortened as regex or regexp;[1] also referred to as + rational \n[2][3]) is a sequence of characters that specifies a search + pattern in text. Usually such patterns are used by string-searching algorithms + for "find" or "find and replace" operations on strings, or for input validation. + "# + .unindent(), + }) + .await; + + run_replacement_test(ReplacementTestParams { + editor: &editor, + search_bar: &search_bar, + cx, + search_mode: SearchMode::Regex, + search_text: "or", + search_options: Some(SearchOptions::WHOLE_WORD), + replacement_text: r"\\\n\\\\", + replace_all: false, + expected_text: r#" + A regular \n (shortened as regex \ + \\ regexp;[1] also referred to as + rational \n[2][3]) is a sequence of characters that specifies a search + pattern in text. Usually such patterns are used by string-searching algorithms + for "find" or "find and replace" operations on strings, or for input validation. + "# + .unindent(), + }) + .await; + + run_replacement_test(ReplacementTestParams { + editor: &editor, + search_bar: &search_bar, + cx, + search_mode: SearchMode::Regex, + search_text: r"(that|used) ", + search_options: None, + replacement_text: r"$1\n", + replace_all: true, + expected_text: r#" + A regular \n (shortened as regex \ + \\ regexp;[1] also referred to as + rational \n[2][3]) is a sequence of characters that + specifies a search + pattern in text. Usually such patterns are used + by string-searching algorithms + for "find" or "find and replace" operations on strings, or for input validation. + "# + .unindent(), + }) + .await; + } + #[gpui::test] async fn test_invalid_regexp_search_after_valid(cx: &mut TestAppContext) { let (editor, search_bar, cx) = init_test(cx);