diff --git a/codex-rs/tui/src/wrapping.rs b/codex-rs/tui/src/wrapping.rs index 61bd8147ab..bc66b18ad5 100644 --- a/codex-rs/tui/src/wrapping.rs +++ b/codex-rs/tui/src/wrapping.rs @@ -49,8 +49,16 @@ where for (line_index, line) in textwrap::wrap(text, &opts).iter().enumerate() { match line { std::borrow::Cow::Borrowed(slice) => { - let start = unsafe { slice.as_ptr().offset_from(text.as_ptr()) as usize }; - let end = start + slice.len(); + let range = borrowed_slice_range(text, slice).unwrap_or_else(|| { + let synthetic_prefix = if line_index == 0 { + opts.initial_indent + } else { + opts.subsequent_indent + }; + map_owned_wrapped_line_to_range(text, cursor, slice, synthetic_prefix) + }); + let start = range.start; + let end = range.end; let trailing_spaces = text[end..].chars().take_while(|c| *c == ' ').count(); lines.push(start..end + trailing_spaces + 1); cursor = end + trailing_spaces; @@ -84,10 +92,16 @@ where for (line_index, line) in textwrap::wrap(text, &opts).iter().enumerate() { match line { std::borrow::Cow::Borrowed(slice) => { - let start = unsafe { slice.as_ptr().offset_from(text.as_ptr()) as usize }; - let end = start + slice.len(); - lines.push(start..end); - cursor = end; + let range = borrowed_slice_range(text, slice).unwrap_or_else(|| { + let synthetic_prefix = if line_index == 0 { + opts.initial_indent + } else { + opts.subsequent_indent + }; + map_owned_wrapped_line_to_range(text, cursor, slice, synthetic_prefix) + }); + cursor = range.end; + lines.push(range); } std::borrow::Cow::Owned(slice) => { let synthetic_prefix = if line_index == 0 { @@ -104,6 +118,19 @@ where lines } +fn borrowed_slice_range(text: &str, slice: &str) -> Option> { + let text_start = text.as_ptr() as usize; + let text_end = text_start.checked_add(text.len())?; + let slice_start = slice.as_ptr() as usize; + let slice_end = slice_start.checked_add(slice.len())?; + + if slice_start < text_start || slice_end > text_end { + return None; + } + + Some((slice_start - text_start)..(slice_end - text_start)) +} + /// Maps an owned (materialized) wrapped line back to a byte range in `text`. /// /// `textwrap` returns `Cow::Owned` when it inserts a hyphenation penalty @@ -1485,6 +1512,17 @@ them."# assert_eq!(range, 0..5); } + #[test] + fn borrowed_slice_range_rejects_slices_outside_source_text() { + let text = "test message"; + let external = String::from("test"); + + assert_eq!(borrowed_slice_range(text, &external), None); + + let fallback = map_owned_wrapped_line_to_range(text, /*cursor*/ 0, &external, ""); + assert_eq!(fallback, 0..4); + } + #[test] fn map_owned_wrapped_line_to_range_indent_coincides_with_source() { // When the synthetic indent prefix starts with a character that also