diff --git a/crates/edit/src/buffer/mod.rs b/crates/edit/src/buffer/mod.rs index 2019c9513a1..1ff899b095b 100644 --- a/crates/edit/src/buffer/mod.rs +++ b/crates/edit/src/buffer/mod.rs @@ -4190,4 +4190,45 @@ mod tests { assert_eq!(buffer_stored_bytes(&buf), b"ax\nbx\nx\n"); } + + // Replace All must not search the text again from the start after + // replacing a match that ends at the end of the text. + #[test] + fn test_replace_all_match_at_end_of_text() { + for (text, pattern, replacement, expected) in [ + ("x foo", "foo", "Foo", "x Foo"), + ("x foo", "foo", "foo", "x foo"), + ("foo foo", "foo", "Foo", "Foo Foo"), + ("x a", "a", "aa", "x aa"), + ] { + let mut buf = prep_buffer(text); + buf.find_and_replace_all(pattern, Default::default(), replacement.as_bytes()).unwrap(); + assert_eq!(buffer_stored_bytes(&buf), expected.as_bytes(), "{text:?}"); + } + } + + // Same as above, but with "insert final newline" adding a newline after the last match. + #[test] + fn test_replace_all_match_at_end_of_text_final_newline() { + let mut buf = prep_buffer("x a"); + buf.set_insert_final_newline(true); + + buf.find_and_replace_all("a", Default::default(), b"aa").unwrap(); + + assert_eq!(buffer_stored_bytes(&buf), b"x aa\n"); + } + + // After Replace hits a match that ends at the end of the text, + // the search must wrap around instead of finding the replaced text again. + #[test] + fn test_replace_match_at_end_of_text_wraps_around() { + let mut buf = prep_buffer("foo x foo"); + buf.find_and_select("foo", Default::default()).unwrap(); + buf.find_and_select("foo", Default::default()).unwrap(); + buf.find_and_replace("foo", Default::default(), b"Foo").unwrap(); + + assert_eq!(buffer_stored_bytes(&buf), b"foo x Foo"); + let (beg, end) = buf.selection_range().unwrap(); + assert_eq!((beg.offset, end.offset), (0, 3)); + } } diff --git a/crates/edit/src/icu.rs b/crates/edit/src/icu.rs index 5b34d7b06a8..c7aeb37b03b 100644 --- a/crates/edit/src/icu.rs +++ b/crates/edit/src/icu.rs @@ -401,6 +401,9 @@ extern "C" fn utext_native_length(ut: &mut icu_ffi::UText) -> i64 { } extern "C" fn utext_access(ut: &mut icu_ffi::UText, native_index: i64, forward: bool) -> bool { + let tb = text_buffer_from_utext(ut); + let native_index = native_index.clamp(0, tb.text_length() as i64); + if let Some(cache) = utext_access_impl(ut, native_index, forward) { let native_off = native_index as usize - cache.utf8_range.start; ut.chunk_contents = cache.utf16.as_ptr(); @@ -411,6 +414,21 @@ extern "C" fn utext_access(ut: &mut icu_ffi::UText, native_index: i64, forward: ut.native_indexing_limit = cache.native_indexing_limit as i32; true } else { + // There's no text in the requested direction: `native_index` is at the end of the text + // (forward) or at its start (backward). The `UTextAccess` contract says that the iteration + // position must then be left at that end of the string. If we leave the previous chunk + // in place, ICU keeps iterating from wherever that chunk was (for instance `uregex_reset` + // would resume the search at offset 0 instead of the end of the text). + // + // An empty chunk at `native_index` does that. Since `native_indexing_limit` is 0, ICU won't + // call `utext_map_offset_to_native` or `utext_map_native_index_to_utf16` for this chunk. + let double_cache = double_cache_from_utext(ut); + ut.chunk_contents = double_cache.cache[0].utf16.as_ptr(); + ut.chunk_length = 0; + ut.chunk_offset = 0; + ut.chunk_native_start = native_index; + ut.chunk_native_limit = native_index; + ut.native_indexing_limit = 0; false } }