@@ -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<Regex> = OnceLock::new();
+
#[derive(Clone, Debug)]
pub struct SearchInputs {
query: Arc<str>,
@@ -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
@@ -1918,6 +1918,114 @@ mod tests {
);
}
+ struct ReplacementTestParams<'a> {
+ editor: &'a View<Editor>,
+ search_bar: &'a View<BufferSearchBar>,
+ cx: &'a mut VisualTestContext,
+ search_mode: SearchMode,
+ search_text: &'static str,
+ search_options: Option<SearchOptions>,
+ 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);