Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions crates/edit/src/buffer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
18 changes: 18 additions & 0 deletions crates/edit/src/icu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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
}
}
Expand Down