From 285be5ee2bc3719ce0969b53fce833e5f9d93e9d Mon Sep 17 00:00:00 2001 From: sarankumar1325 <120080287+sarankumar1325@users.noreply.github.com> Date: Tue, 27 Oct 2026 01:09:44 +0530 Subject: [PATCH] Leave the UText position at the end when access runs out of text utext_access returned false without touching the chunk when there was no text in the requested direction. The UTextAccess contract says the iteration position must then be left at that end of the string, so ICU kept using the stale chunk instead. After Replace All replaced a match that ends at the end of the text, the search was reset to text_length() and resumed from offset 0, finding the match it had just replaced. If the replacement matched again, Replace All looped forever. The same stale chunk made Replace All replace the last match twice when "insert final newline" was on, and made Replace select the replaced text again instead of wrapping around. utext_access now clamps the index and, when there's no text, pins an empty chunk at that end. Fixes #994 --- crates/edit/src/buffer/mod.rs | 41 +++++++++++++++++++++++++++++++++++ crates/edit/src/icu.rs | 18 +++++++++++++++ 2 files changed, 59 insertions(+) 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 } }