use crate::types::StyledSpan;
pub fn snap_to_char_boundary(s: &str, byte_idx: usize) -> usize {
let byte_idx = byte_idx.min(s.len());
let mut i = byte_idx;
while i > 0 && !s.is_char_boundary(i) {
i -= 1;
}
i
}
pub fn prev_char_boundary(s: &str, byte_idx: usize) -> usize {
let byte_idx = byte_idx.min(s.len());
if byte_idx == 0 {
return 0;
}
let mut i = byte_idx - 1;
while i > 0 && !s.is_char_boundary(i) {
i -= 1;
}
i
}
pub fn next_char_boundary(s: &str, byte_idx: usize) -> usize {
let byte_idx = byte_idx.min(s.len());
if byte_idx >= s.len() {
return s.len();
}
let mut i = byte_idx + 1;
while i < s.len() && !s.is_char_boundary(i) {
i += 1;
}
i
}
pub fn safe_prefix(s: &str, byte_idx: usize) -> &str {
&s[..snap_to_char_boundary(s, byte_idx)]
}
pub fn safe_slice(s: &str, lo: usize, hi: usize) -> &str {
let (lo, hi) = if lo <= hi { (lo, hi) } else { (hi, lo) };
let lo = snap_to_char_boundary(s, lo);
let hi = snap_to_char_boundary(s, hi);
&s[lo..hi]
}
pub fn char_to_byte_idx(text: &str, char_idx: usize) -> usize {
text.char_indices()
.nth(char_idx)
.map(|(i, _)| i)
.unwrap_or(text.len())
}
pub fn char_col_to_visual(raw_text: &str, char_col: usize, tabstop: usize) -> usize {
let tabstop = tabstop.max(1);
let mut vis = 0usize;
for (i, ch) in raw_text.chars().enumerate() {
if ch == '\n' || ch == '\r' {
break;
}
if i >= char_col {
break;
}
if ch == '\t' {
vis = ((vis / tabstop) + 1) * tabstop;
} else {
vis += 1;
}
}
vis
}
pub fn strip_json_comments(input: &str) -> String {
let mut out = String::with_capacity(input.len());
let bytes = input.as_bytes();
let len = bytes.len();
let mut i = 0;
let char_len_at =
|at: usize| -> usize { input[at..].chars().next().map_or(1, |c| c.len_utf8()) };
while i < len {
if bytes[i] == b'"' {
out.push('"');
i += 1;
while i < len {
if bytes[i] == b'\\' && i + 1 < len {
let esc_len = char_len_at(i + 1);
out.push_str(&input[i..i + 1 + esc_len]);
i += 1 + esc_len;
} else if bytes[i] == b'"' {
out.push('"');
i += 1;
break;
} else {
let n = char_len_at(i);
out.push_str(&input[i..i + n]);
i += n;
}
}
} else if i + 1 < len && bytes[i] == b'/' && bytes[i + 1] == b'/' {
i += 2;
while i < len && bytes[i] != b'\n' {
i += 1;
}
} else if i + 1 < len && bytes[i] == b'/' && bytes[i + 1] == b'*' {
i += 2;
while i + 1 < len && !(bytes[i] == b'*' && bytes[i + 1] == b'/') {
if bytes[i] == b'\n' {
out.push('\n');
}
i += 1;
}
i += 2; } else {
let n = char_len_at(i);
out.push_str(&input[i..i + n]);
i += n;
}
}
out
}
pub fn char_cell_width(c: char) -> u16 {
unicode_width::UnicodeWidthChar::width(c).unwrap_or(1) as u16
}
pub fn display_width(s: &str) -> usize {
s.chars().map(|c| char_cell_width(c) as usize).sum()
}
pub fn is_wide_char(c: char) -> bool {
char_cell_width(c) == 2
}
pub fn fuzzy_score(haystack: &str, query: &str) -> Option<(i32, Vec<usize>)> {
if query.is_empty() {
return Some((0, Vec::new()));
}
let query_chars: Vec<char> = query.chars().collect();
let mut qi = 0usize;
let mut score = 100i32;
let mut positions = Vec::with_capacity(query_chars.len());
let mut prev_matched_char_idx: Option<usize> = None;
let mut prev_char: Option<char> = None;
for (char_idx, (byte_idx, ch)) in haystack.char_indices().enumerate() {
if qi < query_chars.len() && ch == query_chars[qi] {
if let Some(prev_idx) = prev_matched_char_idx {
let gap = char_idx - prev_idx - 1;
if gap == 0 {
score += 15; } else {
score -= gap as i32;
}
}
let at_boundary = prev_char.is_none_or(|c| c.is_whitespace())
|| matches!(prev_char, Some('/' | '\\' | '_' | '-' | '.'));
if at_boundary {
score += 10;
}
positions.push(byte_idx);
prev_matched_char_idx = Some(char_idx);
qi += 1;
}
prev_char = Some(ch);
}
if qi == query_chars.len() {
Some((score, positions))
} else {
None
}
}
pub fn word_wrap(text: &str, col_budget: usize) -> Vec<String> {
let spans = [StyledSpan::plain(text)];
wrap_spans(&spans, col_budget, WrapPolicy::Word)
.into_iter()
.map(|group| group.iter().map(|s| s.text.as_str()).collect())
.collect()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WrapPolicy {
Word,
Char,
}
pub fn wrap_spans(
spans: &[StyledSpan],
col_budget: usize,
policy: WrapPolicy,
) -> Vec<Vec<StyledSpan>> {
let chars: Vec<(char, usize)> = spans
.iter()
.enumerate()
.flat_map(|(idx, span)| span.text.chars().map(move |c| (c, idx)))
.collect();
if spans.is_empty() || chars.is_empty() {
return vec![vec![]];
}
let total_width: usize = chars
.iter()
.map(|&(c, _)| char_cell_width(c) as usize)
.sum();
if col_budget == 0 || total_width <= col_budget {
return vec![spans.to_vec()];
}
let rows: Vec<Vec<(char, usize)>> = match policy {
WrapPolicy::Char => wrap_chars_by_width(&chars, col_budget),
WrapPolicy::Word => wrap_chars_by_word(&chars, col_budget),
};
rows.iter()
.map(|row| group_into_spans(row, spans))
.collect()
}
fn wrap_chars_by_width(chars: &[(char, usize)], budget: usize) -> Vec<Vec<(char, usize)>> {
let mut rows = Vec::new();
let mut row: Vec<(char, usize)> = Vec::new();
let mut row_w = 0usize;
for &(c, idx) in chars {
let w = char_cell_width(c) as usize;
if !row.is_empty() && row_w + w > budget {
rows.push(std::mem::take(&mut row));
row_w = 0;
}
row.push((c, idx));
row_w += w;
}
if !row.is_empty() {
rows.push(row);
}
rows
}
fn wrap_chars_by_word(chars: &[(char, usize)], budget: usize) -> Vec<Vec<(char, usize)>> {
enum TokenKind {
Word,
Space,
}
let mut tokens: Vec<(TokenKind, Vec<(char, usize)>)> = Vec::new();
for &(c, idx) in chars {
let kind_is_space = c == ' ';
match tokens.last_mut() {
Some((TokenKind::Space, run)) if kind_is_space => run.push((c, idx)),
Some((TokenKind::Word, run)) if !kind_is_space => run.push((c, idx)),
_ => tokens.push((
if kind_is_space {
TokenKind::Space
} else {
TokenKind::Word
},
vec![(c, idx)],
)),
}
}
let mut rows: Vec<Vec<(char, usize)>> = Vec::new();
let mut row: Vec<(char, usize)> = Vec::new();
let mut row_w = 0usize;
let mut pending_space: Option<(char, usize)> = None;
for (kind, content) in tokens {
match kind {
TokenKind::Space => {
pending_space = content.first().copied();
}
TokenKind::Word => {
let word_w: usize = content
.iter()
.map(|&(c, _)| char_cell_width(c) as usize)
.sum();
if word_w > budget {
if !row.is_empty() {
rows.push(std::mem::take(&mut row));
}
pending_space = None;
let mut chunks = wrap_chars_by_width(&content, budget);
row = chunks.pop().unwrap_or_default();
row_w = row.iter().map(|&(c, _)| char_cell_width(c) as usize).sum();
rows.extend(chunks);
} else {
let extra = if row.is_empty() || pending_space.is_none() {
0
} else {
1
};
if row_w + extra + word_w <= budget {
if extra == 1 {
row.push(pending_space.expect("extra == 1 implies Some"));
row_w += 1;
}
row.extend_from_slice(&content);
row_w += word_w;
} else {
if !row.is_empty() {
rows.push(std::mem::take(&mut row));
}
row = content;
row_w = word_w;
}
pending_space = None;
}
}
}
}
if !row.is_empty() {
rows.push(row);
}
if rows.is_empty() {
rows.push(Vec::new());
}
rows
}
fn group_into_spans(row: &[(char, usize)], spans: &[StyledSpan]) -> Vec<StyledSpan> {
let mut result = Vec::new();
let mut i = 0usize;
while i < row.len() {
let idx = row[i].1;
let mut text = String::new();
while i < row.len() && row[i].1 == idx {
text.push(row[i].0);
i += 1;
}
result.push(StyledSpan {
text,
..spans[idx].clone()
});
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn snap_to_char_boundary_already_on_boundary_is_noop() {
let s = "héllo";
assert_eq!(snap_to_char_boundary(s, 0), 0);
assert_eq!(snap_to_char_boundary(s, 1), 1);
assert_eq!(snap_to_char_boundary(s, 3), 3);
}
#[test]
fn snap_to_char_boundary_mid_char_walks_back() {
let s = "héllo";
assert_eq!(snap_to_char_boundary(s, 2), 1);
}
#[test]
fn snap_to_char_boundary_clamps_past_end() {
let s = "abc";
assert_eq!(snap_to_char_boundary(s, 100), 3);
}
#[test]
fn snap_to_char_boundary_cjk_and_emoji() {
let s = "中文🎉end";
for i in 0..=s.len() {
let snapped = snap_to_char_boundary(s, i);
assert!(s.is_char_boundary(snapped));
let _ = &s[..snapped]; }
}
#[test]
fn prev_char_boundary_at_zero_stays_zero() {
assert_eq!(prev_char_boundary("abc", 0), 0);
}
#[test]
fn prev_char_boundary_steps_back_one_multibyte_char() {
let s = "héllo";
assert_eq!(prev_char_boundary(s, 3), 1);
}
#[test]
fn prev_char_boundary_from_mid_char_lands_before_that_char() {
let s = "héllo";
assert_eq!(prev_char_boundary(s, 2), 1);
}
#[test]
fn prev_char_boundary_clamps_past_end() {
let s = "héllo";
assert_eq!(prev_char_boundary(s, 999), prev_char_boundary(s, s.len()));
}
#[test]
fn next_char_boundary_at_end_stays_at_end() {
let s = "abc";
assert_eq!(next_char_boundary(s, 3), 3);
assert_eq!(next_char_boundary(s, 100), 3);
}
#[test]
fn next_char_boundary_steps_forward_one_multibyte_char() {
let s = "héllo";
assert_eq!(next_char_boundary(s, 1), 3);
}
#[test]
fn next_char_boundary_from_mid_char_lands_after_that_char() {
let s = "héllo";
assert_eq!(next_char_boundary(s, 2), 3);
}
#[test]
fn safe_prefix_on_boundary_matches_manual_slice() {
let s = "héllo";
assert_eq!(safe_prefix(s, 3), &s[..3]);
}
#[test]
fn safe_prefix_mid_char_does_not_panic() {
let s = "héllo";
assert_eq!(safe_prefix(s, 2), "h");
}
#[test]
fn safe_prefix_past_end_returns_whole_string() {
let s = "héllo";
assert_eq!(safe_prefix(s, 999), s);
}
#[test]
fn safe_slice_mid_char_bounds_do_not_panic() {
let s = "中文🎉end";
for lo in 0..=s.len() {
for hi in 0..=s.len() {
let slice = safe_slice(s, lo, hi);
let _ = slice; }
}
}
#[test]
fn safe_slice_swaps_reversed_bounds() {
let s = "abcdef";
assert_eq!(safe_slice(s, 4, 1), &s[1..4]);
}
#[test]
fn safe_slice_on_boundaries_matches_manual_slice() {
let s = "héllo world";
assert_eq!(safe_slice(s, 0, 3), &s[0..3]);
}
#[test]
fn char_to_byte_idx_ascii_matches_index() {
assert_eq!(char_to_byte_idx("abcde", 0), 0);
assert_eq!(char_to_byte_idx("abcde", 3), 3);
}
#[test]
fn char_to_byte_idx_multibyte_input() {
let s = "héllo"; assert_eq!(char_to_byte_idx(s, 0), 0);
assert_eq!(char_to_byte_idx(s, 1), 1);
assert_eq!(char_to_byte_idx(s, 2), 3);
}
#[test]
fn char_to_byte_idx_cjk_and_emoji() {
let s = "中文🎉end";
let byte_offsets: Vec<usize> = s.char_indices().map(|(b, _)| b).collect();
for (char_idx, &expected_byte) in byte_offsets.iter().enumerate() {
assert_eq!(char_to_byte_idx(s, char_idx), expected_byte);
}
}
#[test]
fn char_to_byte_idx_past_end_clamps_to_len() {
let s = "héllo";
assert_eq!(char_to_byte_idx(s, 999), s.len());
}
#[test]
fn char_col_to_visual_no_tabs_is_identity() {
assert_eq!(char_col_to_visual("hello", 3, 4), 3);
assert_eq!(char_col_to_visual("hello", 0, 4), 0);
}
#[test]
fn char_col_to_visual_expands_tabs_to_tabstop() {
assert_eq!(char_col_to_visual("\tx", 1, 4), 4);
assert_eq!(char_col_to_visual("\tx", 2, 4), 5);
}
#[test]
fn char_col_to_visual_stops_at_newline() {
assert_eq!(char_col_to_visual("ab\ncd", 5, 4), 2);
}
#[test]
fn char_col_to_visual_multibyte_chars_count_as_one_cell_each() {
let s = "中x";
assert_eq!(char_col_to_visual(s, 1, 4), 1);
assert_eq!(char_col_to_visual(s, 2, 4), 2);
}
#[test]
fn char_col_to_visual_zero_tabstop_treated_as_one() {
assert_eq!(char_col_to_visual("\tx", 1, 0), 1);
}
#[test]
fn strip_json_comments_removes_line_and_block_comments() {
let input = "{\n // a line comment\n \"a\": 1, /* inline block */\n \"b\": \"has // not a comment\"\n}";
let stripped = strip_json_comments(input);
let val: serde_json::Value = serde_json::from_str(&stripped).unwrap();
assert_eq!(val["a"], 1);
assert_eq!(val["b"], "has // not a comment");
}
#[test]
fn strip_json_comments_preserves_newlines_in_block_comments() {
let input = "{\n/* line1\nline2\nline3 */\n\"a\": 1\n}";
let stripped = strip_json_comments(input);
assert_eq!(stripped.lines().count(), input.lines().count());
let val: serde_json::Value = serde_json::from_str(&stripped).unwrap();
assert_eq!(val["a"], 1);
}
#[test]
fn strip_json_comments_multibyte_content_round_trips() {
let input = r#"{"a": "日本語 🎉", "b": "héllo"}"#;
let stripped = strip_json_comments(input);
assert_eq!(
stripped, input,
"no comments present, must round-trip exactly"
);
let val: serde_json::Value = serde_json::from_str(&stripped).unwrap();
assert_eq!(val["a"], "日本語 🎉");
assert_eq!(val["b"], "héllo");
}
#[test]
fn strip_json_comments_multibyte_around_comments() {
let input = "{\n // 日本語 comment\n \"a\": \"héllo 🎉\" /* 中文 block */\n}";
let stripped = strip_json_comments(input);
let val: serde_json::Value = serde_json::from_str(&stripped).unwrap();
assert_eq!(val["a"], "héllo 🎉");
}
#[test]
fn char_cell_width_ascii_is_one() {
assert_eq!(char_cell_width('a'), 1);
assert_eq!(char_cell_width(' '), 1);
}
#[test]
fn char_cell_width_cjk_is_two() {
assert_eq!(char_cell_width('日'), 2);
assert_eq!(char_cell_width('中'), 2);
}
#[test]
fn char_cell_width_combining_mark_is_zero() {
assert_eq!(char_cell_width('\u{0301}'), 0);
}
#[test]
fn display_width_sums_mixed_ascii_and_cjk() {
assert_eq!(display_width("ab日本"), 2 + 2 + 2);
assert_eq!(display_width(""), 0);
assert_eq!(display_width("hello"), 5);
}
#[test]
fn is_wide_char_classifies_cjk_and_ascii() {
assert!(is_wide_char('日'));
assert!(is_wide_char('中'));
assert!(!is_wide_char('a'));
assert!(!is_wide_char(' '));
assert!(!is_wide_char('\u{0301}'));
}
#[test]
fn fuzzy_score_exact_match_scores_high() {
let s = fuzzy_score("src/main.rs", "src/main.rs");
assert!(s.is_some());
assert!(s.unwrap().0 > 0);
}
#[test]
fn fuzzy_score_subsequence_matches() {
let s = fuzzy_score("src/main", "sm");
assert!(s.is_some());
}
#[test]
fn fuzzy_score_non_subsequence_is_none() {
assert!(fuzzy_score("src/main", "xyz").is_none());
}
#[test]
fn fuzzy_score_boundary_bonus() {
let at_boundary = fuzzy_score("src/main", "m").unwrap().0;
let mid_word = fuzzy_score("abcmain", "m").unwrap().0;
assert!(at_boundary > mid_word);
}
#[test]
fn fuzzy_score_consecutive_run_beats_scattered() {
let contiguous = fuzzy_score("xxabxx", "ab").unwrap().0;
let scattered = fuzzy_score("axbxxx", "ab").unwrap().0;
assert!(contiguous > scattered);
}
#[test]
fn fuzzy_score_returns_match_positions() {
let (_, positions) = fuzzy_score("abcdef", "ace").unwrap();
assert_eq!(positions, vec![0, 2, 4]);
}
#[test]
fn fuzzy_score_empty_query_matches_trivially() {
let s = fuzzy_score("anything", "");
assert_eq!(s, Some((0, Vec::new())));
}
#[test]
fn fuzzy_score_empty_haystack_non_empty_query_is_none() {
assert!(fuzzy_score("", "x").is_none());
}
#[test]
fn fuzzy_score_treats_backslash_as_boundary() {
let posix = fuzzy_score("src/main", "m").unwrap().0;
let windows = fuzzy_score("src\\main", "m").unwrap().0;
let mid = fuzzy_score("abcmain", "m").unwrap().0;
assert_eq!(posix, windows);
assert!(posix > mid);
}
#[test]
fn word_wrap_short_line_is_not_split() {
assert_eq!(word_wrap("hello", 80), vec!["hello".to_string()]);
}
#[test]
fn word_wrap_empty_returns_one_empty_row() {
assert_eq!(word_wrap("", 80), vec![String::new()]);
}
#[test]
fn word_wrap_zero_budget_returns_text_unmodified() {
assert_eq!(word_wrap("hello world", 0), vec!["hello world".to_string()]);
}
#[test]
fn word_wrap_breaks_at_word_boundary() {
let wrapped = word_wrap("hello world", 7);
assert_eq!(wrapped, vec!["hello".to_string(), "world".to_string()]);
}
#[test]
fn word_wrap_packs_multiple_words_per_row() {
let wrapped = word_wrap("the quick brown fox", 10);
assert_eq!(
wrapped,
vec!["the quick".to_string(), "brown fox".to_string()]
);
}
#[test]
fn word_wrap_hard_breaks_a_word_longer_than_budget() {
let wrapped = word_wrap("abcde", 3);
assert_eq!(wrapped, vec!["abc".to_string(), "de".to_string()]);
}
#[test]
fn word_wrap_hard_breaks_overlong_word_within_wrapped_text() {
let wrapped = word_wrap("a implementation b", 10);
assert_eq!(
wrapped,
vec![
"a".to_string(),
"implementa".to_string(),
"tion b".to_string(),
]
);
}
#[test]
fn word_wrap_never_exceeds_budget() {
let text = "the quick brown fox jumps over the lazy dog and then some more words follow";
for budget in 1..12 {
for row in word_wrap(text, budget) {
assert!(
row.chars().count() <= budget,
"row {row:?} exceeds budget {budget}"
);
}
}
}
#[test]
fn word_wrap_no_budget_loses_no_non_space_characters() {
let text = "the quick brown fox jumps over the lazy dog";
let wrapped = word_wrap(text, 6);
let rejoined: String = wrapped.join(" ");
let original_non_space: String = text.chars().filter(|c| !c.is_whitespace()).collect();
let rejoined_non_space: String = rejoined.chars().filter(|c| !c.is_whitespace()).collect();
assert_eq!(original_non_space, rejoined_non_space);
}
#[test]
fn word_wrap_cjk_wraps_at_display_width_not_char_count() {
let text = "中文中文中文"; let wrapped = word_wrap(text, 6);
assert_eq!(
wrapped,
vec!["中文中".to_string(), "文中文".to_string()],
"must wrap by display width (2 cells/char), not char count"
);
for row in &wrapped {
assert!(
display_width(row) <= 6,
"row {row:?} exceeds the 6-cell budget"
);
}
}
#[test]
fn word_wrap_emoji_wraps_at_display_width() {
let text = "🎉🎉🎉🎉🎉";
let wrapped = word_wrap(text, 4);
for row in &wrapped {
assert!(
display_width(row) <= 4,
"row {row:?} exceeds the 4-cell budget"
);
}
assert!(wrapped.len() >= 3, "expected wrapping, got {wrapped:?}");
}
#[test]
fn wrap_spans_char_policy_short_line_not_split() {
let spans = vec![StyledSpan::plain("hello")];
let groups = wrap_spans(&spans, 80, WrapPolicy::Char);
assert_eq!(groups.len(), 1);
assert_eq!(groups[0].len(), 1);
assert_eq!(groups[0][0].text, "hello");
}
#[test]
fn wrap_spans_char_policy_splits_single_span_at_budget() {
let spans = vec![StyledSpan::plain("abcde")];
let groups = wrap_spans(&spans, 3, WrapPolicy::Char);
assert_eq!(groups.len(), 2);
assert_eq!(groups[0][0].text, "abc");
assert_eq!(groups[1][0].text, "de");
}
#[test]
fn wrap_spans_char_policy_preserves_style_on_split_chunks() {
use crate::types::Color;
let bold_span = StyledSpan {
text: "abcdef".to_string(),
fg: Some(Color::rgb(255, 0, 0)),
bg: None,
bold: true,
italic: false,
underline: false,
};
let groups = wrap_spans(&[bold_span], 4, WrapPolicy::Char);
assert_eq!(groups.len(), 2);
assert!(groups[0][0].bold, "first chunk must retain bold");
assert!(groups[1][0].bold, "second chunk must retain bold");
assert_eq!(groups[0][0].fg, Some(Color::rgb(255, 0, 0)));
assert_eq!(groups[0][0].text, "abcd");
assert_eq!(groups[1][0].text, "ef");
}
#[test]
fn wrap_spans_char_policy_multi_span_boundary() {
let a = StyledSpan::plain("abc");
let b = StyledSpan::plain("def");
let groups = wrap_spans(&[a, b], 4, WrapPolicy::Char);
assert_eq!(groups.len(), 2);
let row0_text: String = groups[0].iter().map(|s| s.text.as_str()).collect();
let row1_text: String = groups[1].iter().map(|s| s.text.as_str()).collect();
assert_eq!(row0_text, "abcd");
assert_eq!(row1_text, "ef");
}
#[test]
fn wrap_spans_empty_returns_one_empty_group() {
let groups = wrap_spans(&[], 80, WrapPolicy::Char);
assert_eq!(groups.len(), 1);
assert!(groups[0].is_empty());
let groups = wrap_spans(&[], 80, WrapPolicy::Word);
assert_eq!(groups.len(), 1);
assert!(groups[0].is_empty());
}
#[test]
fn wrap_spans_char_policy_is_display_width_aware() {
let spans = vec![StyledSpan::plain("中文中文")]; let groups = wrap_spans(&spans, 6, WrapPolicy::Char);
assert_eq!(groups.len(), 2);
assert_eq!(groups[0][0].text, "中文中");
assert_eq!(groups[1][0].text, "文");
}
#[test]
fn wrap_spans_word_policy_preserves_style_across_word_boundary() {
let a = StyledSpan::plain("aaa ");
let bold = StyledSpan {
text: "bold text".to_string(),
fg: None,
bg: None,
bold: true,
italic: false,
underline: false,
};
let c = StyledSpan::plain(" bbb");
let groups = wrap_spans(&[a, bold, c], 8, WrapPolicy::Word);
let row_texts: Vec<String> = groups
.iter()
.map(|g| g.iter().map(|s| s.text.as_str()).collect())
.collect();
assert_eq!(
row_texts,
vec!["aaa bold".to_string(), "text bbb".to_string()]
);
assert!(
groups[0].iter().any(|s| s.bold && s.text == "bold"),
"bold must survive onto row 0: {:?}",
groups[0]
);
assert!(
groups[1].iter().any(|s| s.bold && s.text == "text"),
"bold must survive onto row 1: {:?}",
groups[1]
);
assert!(
groups[1].iter().any(|s| !s.bold && s.text.trim() == "bbb"),
"trailing plain text must stay non-bold: {:?}",
groups[1]
);
}
}
#[cfg(test)]
mod proptests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn strip_json_comments_never_panics(s in ".{0,2000}") {
let _ = strip_json_comments(&s);
}
#[test]
fn strip_json_comments_never_panics_on_lossy_bytes(bytes in prop::collection::vec(any::<u8>(), 0..2000)) {
let s = String::from_utf8_lossy(&bytes);
let _ = strip_json_comments(&s);
}
#[test]
fn strip_json_comments_is_identity_when_nothing_to_strip(
s in "[^/\"]{0,200}"
) {
prop_assert_eq!(strip_json_comments(&s), s);
}
#[test]
fn strip_json_comments_removes_line_comments(
value in "[a-zA-Z0-9_]{1,20}",
comment in "[^\\n\"/]{0,40}",
) {
let input = format!("{value} // {comment}\n");
let stripped = strip_json_comments(&input);
prop_assert_eq!(stripped, format!("{value} \n"));
}
}
}