#![allow(dead_code)]
use std::ops::Range;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LineConstructKind {
Blank,
Plain,
FenceMarker,
FenceContent,
IndentedCode,
ListMarker,
ListContinuation,
Blockquote(u8),
SetextUnderline,
HtmlBlock,
Heading,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WidenResult {
Widened(Range<usize>),
FullRebuild,
}
pub(super) const MAX_INCREMENTAL_FRACTION: f32 = 0.5;
pub(super) const MAX_INCREMENTAL_LINES: usize = 256;
pub(super) const CURSOR_HINT_WINDOW: usize = 4;
pub fn compute_damage_range(
old: &[String],
new: &[String],
cursor_row: usize,
) -> Option<Range<usize>> {
if old == new {
return None;
}
if old.len() == new.len() && cursor_row < old.len() && old[cursor_row] != new[cursor_row] {
let lo = cursor_row.saturating_sub(CURSOR_HINT_WINDOW);
let hi = (cursor_row + CURSOR_HINT_WINDOW + 1).min(old.len());
let other_diff_in_window = (lo..hi).any(|i| i != cursor_row && old[i] != new[i]);
if !other_diff_in_window {
return Some(cursor_row..cursor_row + 1);
}
}
let lcp = old
.iter()
.zip(new.iter())
.take_while(|(a, b)| a == b)
.count();
let lcs = old
.iter()
.rev()
.zip(new.iter().rev())
.take_while(|(a, b)| a == b)
.count();
let new_end = new.len().saturating_sub(lcs);
let old_end = old.len().saturating_sub(lcs);
let start = lcp.min(new_end).min(old_end);
let end = new_end.max(start);
Some(start..end)
}
fn is_safe_boundary(kind: LineConstructKind) -> bool {
matches!(kind, LineConstructKind::Blank | LineConstructKind::Plain)
}
fn widen_up(kinds: &[LineConstructKind], damaged_start: usize) -> usize {
let mut row = damaged_start;
while row > 0 {
let candidate = row - 1;
if is_safe_boundary(kinds[candidate]) {
return candidate;
}
row = candidate;
}
0
}
fn widen_down(kinds: &[LineConstructKind], damaged_end: usize) -> usize {
let mut row = damaged_end;
while row < kinds.len() {
if is_safe_boundary(kinds[row]) {
return row + 1;
}
row += 1;
}
kinds.len()
}
pub fn expand_to_reset_boundary(
boundaries: &[usize],
lines_len: usize,
damaged: Range<usize>,
) -> WidenResult {
if lines_len == 0 {
return WidenResult::FullRebuild;
}
debug_assert!(
damaged.start <= lines_len && damaged.end <= lines_len,
"expand_to_reset_boundary: damaged range {:?} out of bounds for lines_len = {}",
damaged,
lines_len,
);
let start = boundaries
.iter()
.rev()
.find(|&&b| b <= damaged.start)
.copied()
.unwrap_or(0);
let end = boundaries
.iter()
.find(|&&b| b >= damaged.end)
.copied()
.unwrap_or(lines_len);
let widened_len = end - start;
let cap_abs = MAX_INCREMENTAL_LINES;
let cap_frac = (((lines_len as f32) * MAX_INCREMENTAL_FRACTION) as usize).max(cap_abs);
if widened_len > cap_abs || widened_len > cap_frac {
return WidenResult::FullRebuild;
}
WidenResult::Widened(start..end)
}
pub fn widen_to_safe(kinds: &[LineConstructKind], damaged: Range<usize>) -> WidenResult {
if kinds.is_empty() {
return WidenResult::FullRebuild;
}
debug_assert!(
damaged.start <= kinds.len() && damaged.end <= kinds.len(),
"widen_to_safe: damaged range {:?} out of bounds for kinds.len() = {}",
damaged,
kinds.len(),
);
let mut start = widen_up(kinds, damaged.start);
let mut end = widen_down(kinds, damaged.end);
start = start.saturating_sub(1);
end = (end + 1).min(kinds.len());
let widened_len = end - start;
let cap_abs = MAX_INCREMENTAL_LINES;
let cap_frac = (((kinds.len() as f32) * MAX_INCREMENTAL_FRACTION) as usize).max(cap_abs);
if widened_len > cap_abs || widened_len > cap_frac {
return WidenResult::FullRebuild;
}
WidenResult::Widened(start..end)
}
pub fn fence_ranges_from_kinds(kinds: &[LineConstructKind]) -> Vec<Range<usize>> {
let mut ranges = Vec::new();
let mut i = 0;
while i < kinds.len() {
if kinds[i] == LineConstructKind::FenceMarker {
let start = i;
i += 1;
while i < kinds.len() && kinds[i] == LineConstructKind::FenceContent {
i += 1;
}
if i < kinds.len() && kinds[i] == LineConstructKind::FenceMarker {
ranges.push(start..i + 1);
i += 1;
} else {
ranges.push(start..kinds.len());
}
} else {
i += 1;
}
}
ranges
}
#[cfg(test)]
mod tests {
use super::*;
use crate::components::text_editor::markdown::ParsedBuffer;
fn kinds_of(lines: &[&str]) -> Vec<LineConstructKind> {
let owned: Vec<String> = lines.iter().map(|s| s.to_string()).collect();
ParsedBuffer::parse(&owned).kinds
}
#[test]
fn plain_paragraph() {
assert_eq!(kinds_of(&["hello world"]), vec![LineConstructKind::Plain]);
}
#[test]
fn blank_line() {
assert_eq!(kinds_of(&[""]), vec![LineConstructKind::Blank]);
}
#[test]
fn atx_heading() {
assert_eq!(kinds_of(&["# title"]), vec![LineConstructKind::Heading]);
}
#[test]
fn setext_underline_above_is_plain() {
let k = kinds_of(&["title", "====="]);
assert_eq!(
k,
vec![LineConstructKind::Plain, LineConstructKind::SetextUnderline]
);
}
#[test]
fn fence_pair() {
let k = kinds_of(&["```rust", "let x = 1;", "```"]);
assert_eq!(
k,
vec![
LineConstructKind::FenceMarker,
LineConstructKind::FenceContent,
LineConstructKind::FenceMarker,
]
);
}
#[test]
fn list_marker_and_continuation() {
let k = kinds_of(&["- item", " continuation"]);
assert_eq!(
k,
vec![
LineConstructKind::ListMarker,
LineConstructKind::ListContinuation
]
);
}
#[test]
fn blockquote_levels() {
let k = kinds_of(&[">> two"]);
assert_eq!(k, vec![LineConstructKind::Blockquote(2)]);
}
#[test]
fn indented_code() {
let k = kinds_of(&["", " let x = 1;"]);
assert_eq!(k[1], LineConstructKind::IndentedCode);
}
#[test]
fn html_block() {
let k = kinds_of(&["<div>", "body", "</div>"]);
assert!(matches!(k[0], LineConstructKind::HtmlBlock));
}
#[test]
fn inline_html_inside_paragraph_does_not_become_html_block() {
let k = kinds_of(&["hello <br> world"]);
assert_eq!(
k[0],
LineConstructKind::Plain,
"paragraph with inline HTML must stay Plain"
);
let k = kinds_of(&["see <span>x</span> end"]);
assert_eq!(k[0], LineConstructKind::Plain);
}
fn lines(strs: &[&str]) -> Vec<String> {
strs.iter().map(|s| s.to_string()).collect()
}
#[test]
fn damage_single_char_insert_uses_cursor_hint() {
let old = lines(&["hello", "world"]);
let new = lines(&["hello", "worldx"]);
assert_eq!(compute_damage_range(&old, &new, 1), Some(1..2));
}
#[test]
fn damage_no_change_returns_none() {
let old = lines(&["a", "b"]);
assert_eq!(compute_damage_range(&old, &old, 0), None);
}
#[test]
fn damage_enter_at_line_end_uses_lcp_lcs() {
let old = lines(&["alpha", "beta"]);
let new = lines(&["alpha", "be", "ta"]);
let dmg = compute_damage_range(&old, &new, 1).unwrap();
assert_eq!(dmg.start, 1);
assert_eq!(dmg.end, new.len()); }
#[test]
fn damage_backspace_merging_lines() {
let old = lines(&["alpha", "beta", "gamma"]);
let new = lines(&["alphabeta", "gamma"]);
let dmg = compute_damage_range(&old, &new, 0).unwrap();
assert_eq!(dmg.start, 0);
}
#[test]
fn damage_multi_diff_within_window_falls_through_to_slow_path() {
let old = lines(&["a", "b", "c", "d", "e"]);
let mut new = old.clone();
new[1] = "B".to_string();
new[2] = "C".to_string();
let dmg = compute_damage_range(&old, &new, 1).unwrap();
assert_eq!(dmg, 1..3);
}
fn kinds_str(s: &str) -> Vec<LineConstructKind> {
s.chars()
.map(|c| match c {
'P' => LineConstructKind::Plain,
'B' => LineConstructKind::Blank,
'F' => LineConstructKind::FenceMarker,
'C' => LineConstructKind::FenceContent,
'L' => LineConstructKind::ListMarker,
'l' => LineConstructKind::ListContinuation,
'Q' => LineConstructKind::Blockquote(1),
'S' => LineConstructKind::SetextUnderline,
'H' => LineConstructKind::Heading,
'I' => LineConstructKind::IndentedCode,
'X' => LineConstructKind::HtmlBlock,
_ => panic!("bad kind char {c}"),
})
.collect()
}
#[test]
fn widen_plain_paragraph_to_blank_boundaries() {
let k = kinds_str("PBPPPBP");
match widen_to_safe(&k, 3..4) {
WidenResult::Widened(r) => {
assert!(r.start <= 1, "widen.start <= 1, got {}", r.start);
assert!(r.end >= 6, "widen.end >= 6, got {}", r.end);
}
x => panic!("expected Widened, got {x:?}"),
}
}
#[test]
fn widen_fence_interior_includes_both_markers() {
let k = kinds_str("PBFCCCFBP");
match widen_to_safe(&k, 4..5) {
WidenResult::Widened(r) => {
assert!(
r.start <= 2,
"must include opening fence marker at row 2, got start {}",
r.start
);
assert!(
r.end >= 7,
"must include closing fence marker at row 6 (end >= 7), got end {}",
r.end
);
}
x => panic!("expected Widened, got {x:?}"),
}
}
#[test]
fn widen_list_continuation_reaches_outermost_marker() {
let k = kinds_str("LlLlllBP");
match widen_to_safe(&k, 4..5) {
WidenResult::Widened(r) => assert_eq!(r.start, 0, "must reach col-0 list marker"),
x => panic!("expected Widened, got {x:?}"),
}
}
#[test]
fn widen_setext_underline_includes_text_line_above() {
let k = kinds_str("PSP");
match widen_to_safe(&k, 1..2) {
WidenResult::Widened(r) => {
assert_eq!(r.start, 0, "must include row above setext underline")
}
x => panic!("expected Widened, got {x:?}"),
}
}
#[test]
fn widen_html_block_includes_whole_block() {
let k = kinds_str("PXXXBP");
match widen_to_safe(&k, 2..3) {
WidenResult::Widened(r) => {
assert!(
r.start <= 1,
"must include first HtmlBlock row, got start {}",
r.start
);
assert!(
r.end >= 4,
"must include last HtmlBlock row, got end {}",
r.end
);
}
x => panic!("expected Widened, got {x:?}"),
}
}
#[test]
fn widen_exceeds_cap_returns_full_rebuild() {
let k = vec![LineConstructKind::FenceContent; 300];
assert_eq!(widen_to_safe(&k, 150..151), WidenResult::FullRebuild);
}
#[test]
fn widen_trips_when_fractional_cap_exceeds_absolute() {
let k = vec![LineConstructKind::FenceContent; 600];
assert_eq!(widen_to_safe(&k, 300..301), WidenResult::FullRebuild);
}
#[test]
fn widen_at_buffer_start_clamps_to_zero() {
let k = kinds_str("PPPPP");
match widen_to_safe(&k, 0..1) {
WidenResult::Widened(r) => assert_eq!(r.start, 0),
x => panic!("expected Widened, got {x:?}"),
}
}
#[test]
fn widen_at_buffer_end_clamps_to_len() {
let k = kinds_str("PPPPP");
match widen_to_safe(&k, 4..5) {
WidenResult::Widened(r) => assert_eq!(r.end, 5),
x => panic!("expected Widened, got {x:?}"),
}
}
#[test]
fn parse_records_boundaries_for_blank_separated_paragraphs() {
use super::super::markdown::ParsedBuffer;
let mut lines: Vec<String> = Vec::with_capacity(8);
for i in 0..4 {
lines.push(format!("paragraph {i}"));
lines.push(String::new());
}
let pb = ParsedBuffer::parse(&lines);
assert!(pb.reset_boundaries.contains(&0), "sentinel 0 missing");
assert!(
pb.reset_boundaries.contains(&lines.len()),
"sentinel lines.len() missing"
);
assert!(
pb.reset_boundaries.contains(&1),
"blank after paragraph 0 should be a boundary, got {:?}",
pb.reset_boundaries
);
assert!(
pb.reset_boundaries.contains(&3),
"blank after paragraph 1 should be a boundary, got {:?}",
pb.reset_boundaries
);
}
#[test]
fn expand_to_reset_uses_nearest_sentinels() {
let boundaries = vec![0, 5];
match expand_to_reset_boundary(&boundaries, 5, 2..3) {
WidenResult::Widened(r) => assert_eq!(r, 0..5),
x => panic!("expected Widened, got {x:?}"),
}
}
#[test]
fn expand_to_reset_snaps_to_interior_boundaries() {
let boundaries = vec![0, 3, 6, 10];
match expand_to_reset_boundary(&boundaries, 10, 4..5) {
WidenResult::Widened(r) => assert_eq!(r, 3..6),
x => panic!("expected Widened, got {x:?}"),
}
}
#[test]
fn expand_to_reset_damage_at_exact_boundary_is_zero_span() {
let boundaries = vec![0, 3, 6, 10];
match expand_to_reset_boundary(&boundaries, 10, 6..6) {
WidenResult::Widened(r) => assert_eq!(r, 6..6),
x => panic!("expected Widened, got {x:?}"),
}
}
#[test]
fn expand_to_reset_empty_buffer_falls_back() {
let boundaries = vec![0];
assert_eq!(
expand_to_reset_boundary(&boundaries, 0, 0..0),
WidenResult::FullRebuild
);
}
#[test]
fn expand_to_reset_caps_trip_fallback() {
let boundaries = vec![0, 600];
assert_eq!(
expand_to_reset_boundary(&boundaries, 600, 300..301),
WidenResult::FullRebuild
);
}
#[test]
fn widen_blockquote_includes_whole_block() {
let k = kinds_str("PQQQBP");
match widen_to_safe(&k, 2..3) {
WidenResult::Widened(r) => {
assert!(
r.start <= 1,
"must include first Blockquote row, got start {}",
r.start
);
assert!(
r.end >= 4,
"must include last Blockquote row, got end {}",
r.end
);
}
x => panic!("expected Widened, got {x:?}"),
}
}
#[test]
fn widen_multi_list_does_not_over_pull_across_blank() {
let k = kinds_str("LlBLll");
match widen_to_safe(&k, 4..5) {
WidenResult::Widened(r) => {
assert!(
r.start >= 1,
"widen.start must be >= 1 (D5 may pull past Blank by one row), got {}",
r.start
);
assert!(
r.start <= 2,
"widen.start must not pull in list A, got {}",
r.start
);
}
x => panic!("expected Widened, got {x:?}"),
}
}
#[test]
fn fence_ranges_single_fence() {
let k = kinds_str("PFCCFP");
let r = fence_ranges_from_kinds(&k);
assert_eq!(r, vec![1..5]);
}
#[test]
fn fence_ranges_two_fences() {
let k = kinds_str("FCFPFCF");
let r = fence_ranges_from_kinds(&k);
assert_eq!(r, vec![0..3, 4..7]);
}
#[test]
fn fence_ranges_unclosed_extends_to_end() {
let k = kinds_str("PFCCC");
let r = fence_ranges_from_kinds(&k);
assert_eq!(r, vec![1..5]);
}
#[test]
fn fence_ranges_empty() {
assert!(fence_ranges_from_kinds(&[]).is_empty());
}
#[test]
fn investigate_list_fence_indented_code_interaction() {
let initial: Vec<String> = vec![
"".to_string(), "- a".to_string(), "".to_string(), "".to_string(), "".to_string(), "".to_string(), "".to_string(), " a".to_string(), "```".to_string(), "".to_string(), "".to_string(), "".to_string(), "".to_string(), "".to_string(), "".to_string(), "".to_string(), "".to_string(), "> a".to_string(), "".to_string(), "> ".to_string(), "".to_string(), "".to_string(), "".to_string(), ];
let initial_pb = ParsedBuffer::parse(&initial);
eprintln!("initial kinds: {:?}", &initial_pb.kinds);
let mut edited = initial.clone();
edited[9].push(' ');
let edited_pb = ParsedBuffer::parse(&edited);
eprintln!("edited kinds: {:?}", &edited_pb.kinds);
for i in 0..23 {
if initial_pb.kinds[i] != edited_pb.kinds[i] {
eprintln!(
"Row {} differs: initial={:?}, edited={:?}",
i, initial_pb.kinds[i], edited_pb.kinds[i]
);
}
}
}
}