From 6ca46fcf2cc1a3fe7e19a467fa45266bb7b7385f Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Fri, 5 Jun 2026 12:44:29 +0200 Subject: [PATCH] project: Normalize line endings in textual project search (#58628) Release Notes: - Fixed an issue with project search on windows where multiline string searches would not work correctly due to newline differences --- crates/project/src/search.rs | 8 ++- .../tests/integration/project_tests.rs | 68 +++++++++++++++++++ 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/crates/project/src/search.rs b/crates/project/src/search.rs index 27f7e18ecc..316bb673a9 100644 --- a/crates/project/src/search.rs +++ b/crates/project/src/search.rs @@ -103,7 +103,8 @@ impl SearchQuery { match_full_paths: bool, buffers: Option>>, ) -> Result { - let query = query.to_string(); + let mut query = query.to_string(); + text::LineEnding::normalize(&mut query); if !case_sensitive && !query.is_ascii() { // AhoCorasickBuilder doesn't support case-insensitive search with unicode characters // Fallback to regex search as recommended by @@ -189,7 +190,8 @@ impl SearchQuery { match_full_paths: bool, buffers: Option>>, ) -> Result { - let query = query.to_string(); + let mut query = query.to_string(); + text::LineEnding::normalize(&mut query); let inner = SearchInputs { query: Arc::from(query.as_str()), files_to_include, @@ -402,6 +404,7 @@ impl SearchQuery { let mut text = String::new(); if query_str.contains('\n') { reader.read_to_string(&mut text)?; + text::LineEnding::normalize(&mut text); Ok(search.is_match(&text)) } else { let mut bytes_read = 0; @@ -425,6 +428,7 @@ impl SearchQuery { let mut text = String::new(); if *multiline { reader.read_to_string(&mut text)?; + text::LineEnding::normalize(&mut text); Ok(regex.is_match(&text)?) } else { let mut bytes_read = 0; diff --git a/crates/project/tests/integration/project_tests.rs b/crates/project/tests/integration/project_tests.rs index 9d4e88c69c..af276a0aa4 100644 --- a/crates/project/tests/integration/project_tests.rs +++ b/crates/project/tests/integration/project_tests.rs @@ -7443,6 +7443,74 @@ async fn test_search(cx: &mut gpui::TestAppContext) { ); } +#[gpui::test] +async fn test_search_multiline_crlf(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + path!("/dir"), + json!({ + "crlf.rs": "alpha\r\nbeta\r\ngamma", + "lf.rs": "alpha\nbeta\ngamma", + }), + ) + .await; + let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; + + // A query with CRLF line endings should match buffers regardless of their + // on-disk line endings (buffers always store normalized `\n`). + assert_eq!( + search( + &project, + SearchQuery::text( + "alpha\r\nbeta", + false, + true, + false, + Default::default(), + Default::default(), + false, + None, + ) + .unwrap(), + cx + ) + .await + .unwrap(), + HashMap::from_iter([ + (path!("dir/crlf.rs").to_string(), vec![0..10]), + (path!("dir/lf.rs").to_string(), vec![0..10]), + ]) + ); + + // The reverse: a query with LF line endings should match a file that is + // stored with CRLF line endings on disk. + assert_eq!( + search( + &project, + SearchQuery::text( + "alpha\nbeta", + false, + true, + false, + Default::default(), + Default::default(), + false, + None, + ) + .unwrap(), + cx + ) + .await + .unwrap(), + HashMap::from_iter([ + (path!("dir/crlf.rs").to_string(), vec![0..10]), + (path!("dir/lf.rs").to_string(), vec![0..10]), + ]) + ); +} + #[gpui::test] async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) { init_test(cx);