use devup_editor_core::model::inline::{utf16_len, utf16_to_byte};
use devup_editor_core::{TextSpan, normalize_spans};
#[must_use]
pub fn slice_content(content: &[TextSpan], start: usize, end: usize) -> Vec<TextSpan> {
if start >= end {
return Vec::new();
}
let mut out: Vec<TextSpan> = Vec::new();
let mut cursor = 0usize;
for span in content {
let span_len = utf16_len(&span.text);
let span_start = cursor;
let span_end = cursor + span_len;
cursor = span_end;
if span_end <= start {
continue;
}
if span_start >= end {
break;
}
let from = start.saturating_sub(span_start);
let to = (end - span_start).min(span_len);
if from >= to {
continue;
}
let sliced = slice_utf16(&span.text, from, to);
if sliced.is_empty() {
continue;
}
out.push(TextSpan {
text: sliced,
marks: span.marks.clone(),
});
}
normalize_spans(&mut out);
out
}
fn slice_utf16(s: &str, from: usize, to: usize) -> String {
let byte_from = utf16_to_byte(s, from);
let byte_to = utf16_to_byte(s, to);
if byte_from >= byte_to {
return String::new();
}
s[byte_from..byte_to].to_string()
}
#[cfg(test)]
mod tests {
use super::*;
use devup_editor_core::Mark;
fn s(text: &str) -> TextSpan {
TextSpan::plain(text)
}
fn sm(text: &str, mark: Mark) -> TextSpan {
TextSpan::with_marks(text, vec![mark])
}
#[test]
fn slice_empty_range_returns_empty() {
let spans = vec![s("hello")];
assert!(slice_content(&spans, 2, 2).is_empty());
assert!(slice_content(&spans, 5, 2).is_empty());
}
#[test]
fn slice_single_span_middle() {
let spans = vec![s("hello world")];
let out = slice_content(&spans, 6, 11);
assert_eq!(out.len(), 1);
assert_eq!(out[0].text, "world");
}
#[test]
fn slice_across_multiple_spans() {
let spans = vec![s("hello "), sm("bold ", Mark::bold()), s("world")];
let out = slice_content(&spans, 3, 13);
assert_eq!(out.len(), 3);
assert_eq!(out[0].text, "lo ");
assert!(out[0].marks.is_empty());
assert_eq!(out[1].text, "bold ");
assert!(out[1].has_mark("bold"));
assert_eq!(out[2].text, "wo");
assert!(out[2].marks.is_empty());
}
#[test]
fn slice_clamps_beyond_end() {
let spans = vec![s("hi")];
let out = slice_content(&spans, 0, 100);
assert_eq!(out.len(), 1);
assert_eq!(out[0].text, "hi");
}
#[test]
fn slice_preserves_utf16_offsets_with_surrogates() {
let spans = vec![s("aπb")];
let out = slice_content(&spans, 0, 3);
assert_eq!(out.len(), 1);
assert_eq!(out[0].text, "aπ");
let out = slice_content(&spans, 1, 3);
assert_eq!(out.len(), 1);
assert_eq!(out[0].text, "π");
}
#[test]
fn slice_drops_empty_result_spans() {
let spans = vec![s("hi "), sm("", Mark::bold()), s("there")];
let out = slice_content(&spans, 0, 8);
assert_eq!(out.len(), 1);
assert_eq!(out[0].text, "hi there");
}
#[test]
fn slice_at_surrogate_boundary_clamps_forward() {
let spans = vec![s("aπb")];
let out = slice_content(&spans, 0, 2);
assert_eq!(out.len(), 1);
assert_eq!(out[0].text, "aπ");
}
#[test]
fn slice_handles_korean_hangul() {
let spans = vec![s("νκΈν
μ€νΈ")];
let out = slice_content(&spans, 2, 5);
assert_eq!(out.len(), 1);
assert_eq!(out[0].text, "ν
μ€νΈ");
}
#[test]
fn slice_handles_zwj_emoji_sequence() {
let spans = vec![s("π¨βπ©βπ§βπ¦X")];
let total = slice_content(&spans, 0, 12);
assert_eq!(total[0].text, "π¨βπ©βπ§βπ¦X");
let head = slice_content(&spans, 0, 11);
assert_eq!(head[0].text, "π¨βπ©βπ§βπ¦");
}
#[test]
fn slice_flag_emoji() {
let spans = vec![s("π°π·hello")];
let out = slice_content(&spans, 4, 9);
assert_eq!(out[0].text, "hello");
let flag = slice_content(&spans, 0, 4);
assert_eq!(flag[0].text, "π°π·");
}
}