1use crate::delta::{Assoc, Delta, Op};
14use crate::model::{
15 line_kind_mismatch, Container, Island, Line, LineKind, LineKindMismatch, Mark, MarkKind,
16 Content, Usv, ISLAND_SLOT,
17};
18use crate::normalize::is_bidi_char;
19use crate::usv::char_to_byte;
20use std::borrow::Cow;
21
22#[derive(Debug, Clone, PartialEq)]
24pub enum MarkOp {
25 Add {
31 start: Usv,
32 end: Usv,
33 kind: MarkKind,
34 },
35 Remove {
42 start: Usv,
43 end: Usv,
44 kind: MarkKind,
45 },
46 RemoveAnchor { id: String },
48}
49
50#[derive(Debug, Clone, PartialEq)]
53pub enum LineOp {
54 Split { at: Usv },
56 Join { line: usize },
58 SetKind { line: usize, kind: LineKind },
60 SetContainers {
62 line: usize,
63 containers: Vec<Container>,
64 },
65 SetContinues { line: usize, continues: bool },
75}
76
77use crate::serial::{
88 container_from_value, container_to_value, line_kind_from_value, line_kind_to_value,
89 mark_from_value, mark_to_value, usv_from, ParseError,
90};
91use serde_json::{Map, Value};
92
93pub fn mark_op_to_value(op: &MarkOp) -> Value {
96 let mut m = Map::new();
97 match op {
98 MarkOp::Add { start, end, kind } => {
99 m.insert("op".into(), "add".into());
100 merge_mark(&mut m, *start, *end, kind);
101 }
102 MarkOp::Remove { start, end, kind } => {
103 m.insert("op".into(), "remove".into());
104 merge_mark(&mut m, *start, *end, kind);
105 }
106 MarkOp::RemoveAnchor { id } => {
107 m.insert("op".into(), "removeAnchor".into());
108 m.insert("id".into(), Value::String(id.clone()));
109 }
110 }
111 Value::Object(m)
112}
113
114fn merge_mark(m: &mut Map<String, Value>, start: Usv, end: Usv, kind: &MarkKind) {
117 let mark = Mark {
118 start,
119 end,
120 kind: kind.clone(),
121 };
122 if let Value::Object(fields) = mark_to_value(&mark) {
123 m.extend(fields);
124 }
125}
126
127pub fn mark_op_from_value(v: &Value) -> Result<MarkOp, ParseError> {
130 let o = v.as_object().ok_or(ParseError::Shape("mark op"))?;
131 match o.get("op").and_then(Value::as_str) {
132 Some("add") => {
133 let mark = mark_from_value(v)?;
134 Ok(MarkOp::Add {
135 start: mark.start,
136 end: mark.end,
137 kind: mark.kind,
138 })
139 }
140 Some("remove") => {
141 let mark = mark_from_value(v)?;
142 Ok(MarkOp::Remove {
143 start: mark.start,
144 end: mark.end,
145 kind: mark.kind,
146 })
147 }
148 Some("removeAnchor") => Ok(MarkOp::RemoveAnchor {
149 id: o
150 .get("id")
151 .and_then(Value::as_str)
152 .ok_or(ParseError::Shape("removeAnchor id"))?
153 .to_string(),
154 }),
155 _ => Err(ParseError::Shape("mark op kind")),
156 }
157}
158
159pub fn line_op_to_value(op: &LineOp) -> Value {
162 let mut m = Map::new();
163 match op {
164 LineOp::Split { at } => {
165 m.insert("op".into(), "split".into());
166 m.insert("at".into(), Value::from(*at));
167 }
168 LineOp::Join { line } => {
169 m.insert("op".into(), "join".into());
170 m.insert("line".into(), Value::from(*line));
171 }
172 LineOp::SetKind { line, kind } => {
173 m.insert("op".into(), "setKind".into());
174 m.insert("line".into(), Value::from(*line));
175 if let Value::Object(fields) = line_kind_to_value(kind) {
176 m.extend(fields);
177 }
178 }
179 LineOp::SetContainers { line, containers } => {
180 m.insert("op".into(), "setContainers".into());
181 m.insert("line".into(), Value::from(*line));
182 m.insert(
183 "containers".into(),
184 Value::Array(containers.iter().map(container_to_value).collect()),
185 );
186 }
187 LineOp::SetContinues { line, continues } => {
188 m.insert("op".into(), "setContinues".into());
189 m.insert("line".into(), Value::from(*line));
190 m.insert("continues".into(), Value::Bool(*continues));
191 }
192 }
193 Value::Object(m)
194}
195
196pub fn line_op_from_value(v: &Value) -> Result<LineOp, ParseError> {
198 let o = v.as_object().ok_or(ParseError::Shape("line op"))?;
199 let line = || usv_from(o.get("line"), "line op line");
200 match o.get("op").and_then(Value::as_str) {
201 Some("split") => Ok(LineOp::Split {
202 at: usv_from(o.get("at"), "split at")?,
203 }),
204 Some("join") => Ok(LineOp::Join { line: line()? }),
205 Some("setKind") => Ok(LineOp::SetKind {
206 line: line()?,
207 kind: line_kind_from_value(v)?,
208 }),
209 Some("setContainers") => Ok(LineOp::SetContainers {
210 line: line()?,
211 containers: o
212 .get("containers")
213 .and_then(Value::as_array)
214 .ok_or(ParseError::Shape("setContainers containers"))?
215 .iter()
216 .map(container_from_value)
217 .collect::<Result<_, _>>()?,
218 }),
219 Some("setContinues") => Ok(LineOp::SetContinues {
220 line: line()?,
221 continues: o
222 .get("continues")
223 .and_then(Value::as_bool)
224 .ok_or(ParseError::Shape("setContinues continues"))?,
225 }),
226 _ => Err(ParseError::Shape("line op kind")),
227 }
228}
229
230#[allow(clippy::type_complexity)]
239pub fn change_bundle_from_value(
240 v: &Value,
241) -> Result<(Delta, Vec<LineOp>, Vec<MarkOp>), String> {
242 let obj = v
243 .as_object()
244 .ok_or("bundle must be an object { delta?, lineOps?, markOps? }")?;
245 let get = |snake: &str, camel: &str| obj.get(snake).or_else(|| obj.get(camel));
246 let delta = match get("delta", "delta") {
247 Some(Value::Null) | None => Delta { ops: Vec::new() },
248 Some(d) => serde_json::from_value(d.clone()).map_err(|e| format!("invalid delta: {e}"))?,
249 };
250 let line_ops = op_array(get("line_ops", "lineOps"), line_op_from_value, "lineOps")?;
251 let mark_ops = op_array(get("mark_ops", "markOps"), mark_op_from_value, "markOps")?;
252 Ok((delta, line_ops, mark_ops))
253}
254
255fn op_array<T>(
259 value: Option<&Value>,
260 convert: impl Fn(&Value) -> Result<T, ParseError>,
261 what: &str,
262) -> Result<Vec<T>, String> {
263 let Some(value) = value else {
264 return Ok(Vec::new());
265 };
266 if value.is_null() {
267 return Ok(Vec::new());
268 }
269 let arr = value
270 .as_array()
271 .ok_or_else(|| format!("{what} must be an array"))?;
272 arr.iter()
273 .map(|v| convert(v).map_err(|e| format!("invalid {what}: {e}")))
274 .collect()
275}
276
277#[derive(Debug, Clone, PartialEq, Eq)]
280pub enum ApplyError {
281 MarkOutOfRange {
282 start: Usv,
283 end: Usv,
284 len: Usv,
285 },
286 LineOutOfRange {
287 line: usize,
288 lines: usize,
289 },
290 SplitPositionOutOfRange {
291 at: Usv,
292 len: Usv,
293 },
294 SplitAtNewline {
295 at: Usv,
296 },
297 LineCountMismatch {
298 lines: usize,
299 segments: usize,
300 },
301 FirstLineContinues,
306 DeltaBaseMismatch {
309 expected: usize,
310 actual: usize,
311 },
312 IslandSlotInInsert,
317 AnchorIdCollision { id: String },
324 EmptyAnchorId,
327 LineKindMismatch {
334 line: usize,
335 mismatch: LineKindMismatch,
336 },
337 NestingTooDeep {
341 line: usize,
342 depth: usize,
343 max: usize,
344 },
345}
346
347impl Content {
348 pub fn apply_text_delta(&mut self, delta: &Delta) -> Result<(), ApplyError> {
364 self.apply_text_delta_inner(delta)?;
365 self.normalize();
366 Ok(())
367 }
368
369 fn apply_text_delta_inner(&mut self, delta: &Delta) -> Result<(), ApplyError> {
374 for op in &delta.ops {
378 if let Op::Insert(s) = op {
379 if s.contains(ISLAND_SLOT) {
380 return Err(ApplyError::IslandSlotInInsert);
381 }
382 }
383 }
384
385 let sanitized = sanitize_inserts(delta);
393 let delta = sanitized.as_ref();
394
395 let old_chars: Vec<char> = self.text.chars().collect();
396 let old_lines = self.lines.clone();
397 let new_text = delta
402 .try_apply(&self.text)
403 .map_err(|e| ApplyError::DeltaBaseMismatch {
404 expected: e.expected,
405 actual: e.actual,
406 })?;
407
408 self.rebase_marks(delta);
409 let new_len = new_text.chars().count();
410 self.marks.retain(|m| {
411 m.start <= m.end
412 && m.end <= new_len
413 && (m.start < m.end || !m.kind.is_formatting())
414 });
415
416 self.text = new_text;
417 self.lines = sync_lines_for_delta(&old_chars, old_lines, delta);
418 let old_islands = std::mem::take(&mut self.islands);
419 self.islands = sync_islands_for_delta(&old_chars, old_islands, delta);
420 if self.lines.len() != self.segment_count() {
421 return Err(ApplyError::LineCountMismatch {
422 lines: self.lines.len(),
423 segments: self.segment_count(),
424 });
425 }
426 Ok(())
427 }
428
429 fn rebase_marks(&mut self, delta: &Delta) {
435 for m in &mut self.marks {
436 if m.start == m.end {
437 let p = delta.map_pos(m.start, Assoc::Before);
438 m.start = p;
439 m.end = p;
440 } else {
441 m.start = delta.map_pos(m.start, Assoc::After);
442 m.end = delta.map_pos(m.end, Assoc::Before);
443 }
444 }
445 }
446
447 pub fn apply_mark_ops(&mut self, ops: &[MarkOp]) -> Result<(), ApplyError> {
449 self.apply_mark_ops_inner(ops)?;
450 self.normalize();
451 Ok(())
452 }
453
454 fn apply_mark_ops_inner(&mut self, ops: &[MarkOp]) -> Result<(), ApplyError> {
458 let len = self.len_usv();
459 for op in ops {
460 match op {
461 MarkOp::Add { start, end, kind } => {
462 if *start > *end || *end > len {
463 return Err(ApplyError::MarkOutOfRange {
464 start: *start,
465 end: *end,
466 len,
467 });
468 }
469 if kind.is_formatting() && start == end {
470 return Err(ApplyError::MarkOutOfRange {
471 start: *start,
472 end: *end,
473 len,
474 });
475 }
476 if let MarkKind::Anchor { id } = kind {
483 if id.is_empty() {
484 return Err(ApplyError::EmptyAnchorId);
485 }
486 if self
487 .marks
488 .iter()
489 .any(|m| matches!(&m.kind, MarkKind::Anchor { id: aid } if aid == id))
490 {
491 return Err(ApplyError::AnchorIdCollision { id: id.clone() });
492 }
493 }
494 self.marks.push(Mark {
495 start: *start,
496 end: *end,
497 kind: kind.clone(),
498 });
499 }
500 MarkOp::Remove { start, end, kind } => {
501 if *start > *end || *end > len {
502 return Err(ApplyError::MarkOutOfRange {
503 start: *start,
504 end: *end,
505 len,
506 });
507 }
508 let mut next = Vec::with_capacity(self.marks.len());
509 for m in self.marks.drain(..) {
510 if m.kind != *kind || !ranges_overlap(m.start, m.end, *start, *end) {
513 next.push(m);
514 continue;
515 }
516 if !kind.is_formatting() {
519 continue;
520 }
521 if m.start < *start {
525 next.push(Mark {
526 start: m.start,
527 end: *start,
528 kind: m.kind.clone(),
529 });
530 }
531 if *end < m.end {
532 next.push(Mark {
533 start: *end,
534 end: m.end,
535 kind: m.kind.clone(),
536 });
537 }
538 }
539 self.marks = next;
540 }
541 MarkOp::RemoveAnchor { id } => {
542 self.marks
543 .retain(|m| !matches!(&m.kind, MarkKind::Anchor { id: aid } if aid == id));
544 }
545 }
546 }
547 Ok(())
548 }
549
550 pub fn apply_line_ops(&mut self, ops: &[LineOp]) -> Result<(), ApplyError> {
552 self.apply_line_ops_inner(ops)?;
553 self.normalize();
554 Ok(())
555 }
556
557 fn apply_line_ops_inner(&mut self, ops: &[LineOp]) -> Result<(), ApplyError> {
561 for op in ops {
562 match op {
563 LineOp::Split { at } => self.split_line(*at)?,
564 LineOp::Join { line } => self.join_line(*line)?,
565 LineOp::SetKind { line, kind } => {
566 let seg = self
572 .text
573 .split('\n')
574 .nth(*line)
575 .ok_or(ApplyError::LineOutOfRange {
576 line: *line,
577 lines: self.lines.len(),
578 })?;
579 if let Some(mismatch) = line_kind_mismatch(kind, seg) {
580 return Err(ApplyError::LineKindMismatch {
581 line: *line,
582 mismatch,
583 });
584 }
585 let line = self.line_mut(*line)?;
586 line.kind = kind.clone();
587 }
588 LineOp::SetContainers { line, containers } => {
589 if containers.len() > crate::MAX_NESTING_DEPTH {
593 return Err(ApplyError::NestingTooDeep {
594 line: *line,
595 depth: containers.len(),
596 max: crate::MAX_NESTING_DEPTH,
597 });
598 }
599 let line = self.line_mut(*line)?;
600 line.containers = containers.clone();
601 }
602 LineOp::SetContinues { line, continues } => {
603 if *line == 0 && *continues {
609 return Err(ApplyError::FirstLineContinues);
610 }
611 let l = self.line_mut(*line)?;
612 l.continues = *continues;
613 }
614 }
615 }
616 Ok(())
617 }
618
619 pub fn apply_field_change(
640 &mut self,
641 text_delta: &Delta,
642 line_ops: &[LineOp],
643 mark_ops: &[MarkOp],
644 ) -> Result<(), ApplyError> {
645 if line_ops.is_empty() && mark_ops.is_empty() {
646 return self.apply_text_delta(text_delta);
647 }
648 let mut scratch = self.clone();
649 scratch.apply_text_delta_inner(text_delta)?;
650 scratch.apply_line_ops_inner(line_ops)?;
651 scratch.apply_mark_ops_inner(mark_ops)?;
652 scratch.normalize();
653 *self = scratch;
654 Ok(())
655 }
656
657 fn line_mut(&mut self, line: usize) -> Result<&mut Line, ApplyError> {
658 let lines = self.lines.len();
659 self.lines
660 .get_mut(line)
661 .ok_or(ApplyError::LineOutOfRange { line, lines })
662 }
663
664 fn split_line(&mut self, at: Usv) -> Result<(), ApplyError> {
665 let char_indices: Vec<(usize, char)> = self.text.char_indices().collect();
666 let len = char_indices.len();
667 if at > len {
668 return Err(ApplyError::SplitPositionOutOfRange { at, len });
669 }
670 if at > 0 && char_indices[at - 1].1 == '\n' {
671 return Err(ApplyError::SplitAtNewline { at });
672 }
673 if at < len && char_indices[at].1 == '\n' {
674 return Err(ApplyError::SplitAtNewline { at });
675 }
676
677 let byte = char_indices.get(at).map_or(self.text.len(), |&(b, _)| b);
682 let line_idx = char_indices[..at].iter().filter(|&(_, c)| *c == '\n').count();
683 self.text.insert(byte, '\n');
684
685 self.rebase_marks(&Delta {
690 ops: vec![Op::Retain(at), Op::Insert("\n".to_string())],
691 });
692
693 let template = self
694 .lines
695 .get(line_idx)
696 .cloned()
697 .unwrap_or_else(default_para_line);
698 let mut new_line = template;
699 new_line.continues = false;
700 self.lines.insert(line_idx + 1, new_line);
701
702 if self.lines.len() != self.segment_count() {
703 return Err(ApplyError::LineCountMismatch {
704 lines: self.lines.len(),
705 segments: self.segment_count(),
706 });
707 }
708 Ok(())
709 }
710
711 fn join_line(&mut self, line: usize) -> Result<(), ApplyError> {
712 if line + 1 >= self.lines.len() {
713 return Err(ApplyError::LineOutOfRange {
714 line,
715 lines: self.lines.len(),
716 });
717 }
718 let nl = newline_at_line_boundary(&self.text, line)?;
719 let byte = char_to_byte(&self.text, nl);
720 self.text.remove(byte);
721
722 self.rebase_marks(&Delta {
727 ops: vec![Op::Retain(nl), Op::Delete(1)],
728 });
729
730 self.lines.remove(line + 1);
731
732 if self.lines.len() != self.segment_count() {
733 return Err(ApplyError::LineCountMismatch {
734 lines: self.lines.len(),
735 segments: self.segment_count(),
736 });
737 }
738 Ok(())
739 }
740}
741
742fn default_para_line() -> Line {
743 Line {
744 kind: LineKind::Para,
745 containers: Vec::new(),
746 continues: false,
747 }
748}
749
750fn ranges_overlap(a0: Usv, a1: Usv, b0: Usv, b1: Usv) -> bool {
751 a0 < b1 && b0 < a1
752}
753
754fn insert_forbidden(c: char) -> bool {
758 c == '\r' || is_bidi_char(c)
759}
760
761fn sanitize_inserts(delta: &Delta) -> Cow<'_, Delta> {
767 let needs_cleaning = delta
768 .ops
769 .iter()
770 .any(|op| matches!(op, Op::Insert(s) if s.chars().any(insert_forbidden)));
771 if !needs_cleaning {
772 return Cow::Borrowed(delta);
773 }
774 let ops = delta
775 .ops
776 .iter()
777 .map(|op| match op {
778 Op::Insert(s) => Op::Insert(s.chars().filter(|c| !insert_forbidden(*c)).collect()),
779 other => other.clone(),
780 })
781 .collect();
782 Cow::Owned(Delta { ops })
783}
784
785fn sync_lines_for_delta(old_chars: &[char], old_lines: Vec<Line>, delta: &Delta) -> Vec<Line> {
798 let cap = old_lines.len();
799 let mut rest = old_lines.into_iter();
800 let mut out: Vec<Line> = Vec::with_capacity(cap);
801 let mut cur: Option<Line> = rest.next();
802 let mut old = 0usize;
803
804 for op in &delta.ops {
805 match op {
806 Op::Retain(n) => {
807 for _ in 0..*n {
808 if old >= old_chars.len() {
809 break;
810 }
811 if old_chars[old] == '\n' {
812 out.extend(cur.take());
813 cur = rest.next();
814 }
815 old += 1;
816 }
817 }
818 Op::Delete(n) => {
819 for _ in 0..*n {
820 if old >= old_chars.len() {
821 break;
822 }
823 if old_chars[old] == '\n' {
826 rest.next();
827 }
828 old += 1;
829 }
830 }
831 Op::Insert(s) => {
832 for c in s.chars() {
833 if c == '\n' {
834 let mut new_line = match cur.take() {
835 Some(line) => {
836 let clone = line.clone();
837 out.push(line);
838 clone
839 }
840 None => default_para_line(),
841 };
842 new_line.continues = false;
843 cur = Some(new_line);
844 }
845 }
846 }
847 }
848 }
849
850 out.extend(cur);
851 out.extend(rest);
852 out
853}
854
855fn sync_islands_for_delta(
861 old_chars: &[char],
862 old_islands: Vec<Island>,
863 delta: &Delta,
864) -> Vec<Island> {
865 let mut keep = vec![true; old_islands.len()];
866 let mut old = 0usize;
867 let mut slot_idx = 0usize;
868
869 for op in &delta.ops {
870 match op {
871 Op::Retain(n) => {
872 for _ in 0..*n {
873 if old >= old_chars.len() {
874 break;
875 }
876 if old_chars[old] == ISLAND_SLOT {
877 slot_idx += 1;
878 }
879 old += 1;
880 }
881 }
882 Op::Delete(n) => {
883 for _ in 0..*n {
884 if old >= old_chars.len() {
885 break;
886 }
887 if old_chars[old] == ISLAND_SLOT {
888 if let Some(k) = keep.get_mut(slot_idx) {
889 *k = false;
890 }
891 slot_idx += 1;
892 }
893 old += 1;
894 }
895 }
896 Op::Insert(_) => {}
899 }
900 }
901
902 old_islands
903 .into_iter()
904 .zip(keep)
905 .filter_map(|(island, keep)| keep.then_some(island))
906 .collect()
907}
908
909fn newline_at_line_boundary(text: &str, line: usize) -> Result<Usv, ApplyError> {
910 let mut current = 0usize;
911 for (i, c) in text.chars().enumerate() {
912 if c == '\n' {
913 if current == line {
914 return Ok(i);
915 }
916 current += 1;
917 }
918 }
919 Err(ApplyError::LineOutOfRange {
920 line,
921 lines: text.chars().filter(|&c| c == '\n').count() + 1,
922 })
923}
924
925#[cfg(test)]
926mod tests {
927 use super::*;
928 use crate::delta::diff;
929 use crate::import::from_markdown;
930
931 #[test]
932 fn mark_op_wire_round_trips_each_variant() {
933 let ops = vec![
934 MarkOp::Add {
935 start: 0,
936 end: 3,
937 kind: MarkKind::Strong,
938 },
939 MarkOp::Add {
940 start: 1,
941 end: 2,
942 kind: MarkKind::Link {
943 url: "https://x".into(),
944 },
945 },
946 MarkOp::Remove {
947 start: 4,
948 end: 6,
949 kind: MarkKind::Anchor { id: "c1".into() },
950 },
951 MarkOp::RemoveAnchor { id: "c2".into() },
952 ];
953 for op in ops {
954 let v = mark_op_to_value(&op);
955 assert_eq!(mark_op_from_value(&v).unwrap(), op, "round-trip: {v}");
956 }
957 }
958
959 #[test]
960 fn line_op_wire_round_trips_each_variant() {
961 let ops = vec![
962 LineOp::Split { at: 5 },
963 LineOp::Join { line: 1 },
964 LineOp::SetKind {
965 line: 0,
966 kind: LineKind::Heading { level: 2 },
967 },
968 LineOp::SetContainers {
969 line: 2,
970 containers: vec![Container::Quote],
971 },
972 LineOp::SetKind {
975 line: 0,
976 kind: LineKind::Unknown {
977 tag: "callout".into(),
978 attrs: serde_json::json!({"variant": "warn"}),
979 },
980 },
981 LineOp::SetContainers {
982 line: 2,
983 containers: vec![Container::Unknown {
984 tag: "indent".into(),
985 attrs: serde_json::json!({"depth": 2}),
986 }],
987 },
988 LineOp::SetContinues {
989 line: 1,
990 continues: true,
991 },
992 LineOp::SetContinues {
993 line: 3,
994 continues: false,
995 },
996 ];
997 for op in ops {
998 let v = line_op_to_value(&op);
999 assert_eq!(line_op_from_value(&v).unwrap(), op, "round-trip: {v}");
1000 }
1001 }
1002
1003 #[test]
1004 fn delta_serde_shape() {
1005 let d = Delta {
1006 ops: vec![Op::Retain(2), Op::Insert("hi".into()), Op::Delete(1)],
1007 };
1008 let v = serde_json::to_value(&d).unwrap();
1009 assert_eq!(
1010 v,
1011 serde_json::json!({"ops": [{"retain": 2}, {"insert": "hi"}, {"delete": 1}]})
1012 );
1013 assert_eq!(serde_json::from_value::<Delta>(v).unwrap(), d);
1014 }
1015
1016 #[test]
1017 fn apply_text_delta_rebases_marks() {
1018 let mut rt = from_markdown("hello").unwrap();
1019 rt.marks.push(Mark {
1020 start: 1,
1021 end: 4,
1022 kind: MarkKind::Strong,
1023 });
1024 rt.normalize();
1025 let d = diff("hello", "hXello");
1026 rt.apply_text_delta(&d).unwrap();
1027 let strong = rt
1028 .marks
1029 .iter()
1030 .find(|m| matches!(m.kind, MarkKind::Strong))
1031 .unwrap();
1032 assert_eq!((strong.start, strong.end), (2, 5));
1033 assert_eq!(rt.text, "hXello");
1034 }
1035
1036 #[test]
1037 fn apply_text_delta_pads_short_prepend() {
1038 let mut rt = from_markdown("hello").unwrap();
1042 rt.apply_text_delta(&Delta {
1043 ops: vec![Op::Insert("NEW ".into())],
1044 })
1045 .unwrap();
1046 assert_eq!(rt.text, "NEW hello");
1047 }
1048
1049 #[test]
1050 fn apply_text_delta_rejects_over_long_delta() {
1051 let mut rt = from_markdown("hi").unwrap();
1054 assert!(matches!(
1055 rt.apply_text_delta(&Delta {
1056 ops: vec![Op::Retain(99)],
1057 }),
1058 Err(ApplyError::DeltaBaseMismatch { .. })
1059 ));
1060 assert_eq!(rt.text, "hi");
1061 }
1062
1063 #[test]
1064 fn apply_mark_ops_add_and_remove() {
1065 let mut rt = from_markdown("abcd").unwrap();
1066 rt.apply_mark_ops(&[MarkOp::Add {
1067 start: 0,
1068 end: 2,
1069 kind: MarkKind::Emph,
1070 }])
1071 .unwrap();
1072 assert!(rt.marks.iter().any(|m| matches!(m.kind, MarkKind::Emph)));
1073 rt.apply_mark_ops(&[MarkOp::Remove {
1074 start: 0,
1075 end: 4,
1076 kind: MarkKind::Emph,
1077 }])
1078 .unwrap();
1079 assert!(!rt.marks.iter().any(|m| matches!(m.kind, MarkKind::Emph)));
1080 }
1081
1082 #[test]
1083 fn apply_mark_ops_remove_punches_hole() {
1084 let mut rt = from_markdown("abcdef").unwrap();
1088 rt.apply_mark_ops(&[MarkOp::Add {
1089 start: 0,
1090 end: 6,
1091 kind: MarkKind::Strong,
1092 }])
1093 .unwrap();
1094 rt.apply_mark_ops(&[MarkOp::Remove {
1095 start: 2,
1096 end: 4,
1097 kind: MarkKind::Strong,
1098 }])
1099 .unwrap();
1100 let strong: Vec<_> = rt
1101 .marks
1102 .iter()
1103 .filter(|m| matches!(m.kind, MarkKind::Strong))
1104 .map(|m| (m.start, m.end))
1105 .collect();
1106 assert_eq!(strong, vec![(0, 2), (4, 6)]);
1107 }
1108
1109 #[test]
1110 fn apply_mark_ops_remove_at_edge_leaves_no_zero_width() {
1111 let mut rt = from_markdown("abcdef").unwrap();
1114 rt.apply_mark_ops(&[MarkOp::Add {
1115 start: 0,
1116 end: 6,
1117 kind: MarkKind::Strong,
1118 }])
1119 .unwrap();
1120 rt.apply_mark_ops(&[MarkOp::Remove {
1121 start: 0,
1122 end: 2,
1123 kind: MarkKind::Strong,
1124 }])
1125 .unwrap();
1126 let strong: Vec<_> = rt
1127 .marks
1128 .iter()
1129 .filter(|m| matches!(m.kind, MarkKind::Strong))
1130 .map(|m| (m.start, m.end))
1131 .collect();
1132 assert_eq!(strong, vec![(2, 6)]);
1133 }
1134
1135 #[test]
1136 fn apply_mark_ops_remove_covering_range_drops_mark() {
1137 let mut rt = from_markdown("abcdef").unwrap();
1140 rt.apply_mark_ops(&[MarkOp::Add {
1141 start: 2,
1142 end: 4,
1143 kind: MarkKind::Emph,
1144 }])
1145 .unwrap();
1146 rt.apply_mark_ops(&[MarkOp::Remove {
1147 start: 0,
1148 end: 6,
1149 kind: MarkKind::Emph,
1150 }])
1151 .unwrap();
1152 assert!(!rt.marks.iter().any(|m| matches!(m.kind, MarkKind::Emph)));
1153 }
1154
1155 #[test]
1156 fn apply_mark_ops_remove_non_formatting_drops_whole() {
1157 let mut rt = from_markdown("abcdef").unwrap();
1160 rt.marks.push(Mark {
1161 start: 0,
1162 end: 6,
1163 kind: MarkKind::Unknown {
1164 tag: "x".into(),
1165 attrs: serde_json::json!({}),
1166 },
1167 });
1168 rt.normalize();
1169 rt.apply_mark_ops(&[MarkOp::Remove {
1170 start: 2,
1171 end: 4,
1172 kind: MarkKind::Unknown {
1173 tag: "x".into(),
1174 attrs: serde_json::json!({}),
1175 },
1176 }])
1177 .unwrap();
1178 assert!(!rt
1179 .marks
1180 .iter()
1181 .any(|m| matches!(m.kind, MarkKind::Unknown { .. })));
1182 }
1183
1184 #[test]
1185 fn apply_text_delta_splits_lines_on_newline_insert() {
1186 let mut rt = from_markdown("one two").unwrap();
1187 let d = diff("one two", "one\ntwo");
1188 rt.apply_text_delta(&d).unwrap();
1189 assert_eq!(rt.lines.len(), 2);
1190 assert_eq!(rt.segment_count(), 2);
1191 assert_eq!(rt.validate(), Ok(()));
1192 }
1193
1194 #[test]
1195 fn line_op_split_and_join() {
1196 let mut rt = from_markdown("onetwo").unwrap();
1197 rt.apply_line_ops(&[LineOp::Split { at: 3 }]).unwrap();
1198 assert_eq!(rt.text, "one\ntwo");
1199 assert_eq!(rt.lines.len(), 2);
1200
1201 rt.apply_line_ops(&[LineOp::Join { line: 0 }]).unwrap();
1202 assert_eq!(rt.text, "onetwo");
1203 assert_eq!(rt.lines.len(), 1);
1204 assert_eq!(rt.validate(), Ok(()));
1205 }
1206
1207 #[test]
1208 fn line_op_set_kind() {
1209 let mut rt = from_markdown("title").unwrap();
1210 rt.apply_line_ops(&[LineOp::SetKind {
1211 line: 0,
1212 kind: LineKind::Heading { level: 2 },
1213 }])
1214 .unwrap();
1215 assert!(matches!(rt.lines[0].kind, LineKind::Heading { level: 2 }));
1216 }
1217
1218 #[test]
1223 fn line_op_set_kind_refuses_a_kind_the_text_contradicts() {
1224 let mut rt = from_markdown("hello world").unwrap();
1225 assert_eq!(
1226 rt.apply_line_ops(&[LineOp::SetKind {
1227 line: 0,
1228 kind: LineKind::Island,
1229 }]),
1230 Err(ApplyError::LineKindMismatch {
1231 line: 0,
1232 mismatch: LineKindMismatch::IslandNotOneSlot,
1233 })
1234 );
1235 assert_eq!(
1236 rt.apply_line_ops(&[LineOp::SetKind {
1237 line: 0,
1238 kind: LineKind::Rule,
1239 }]),
1240 Err(ApplyError::LineKindMismatch {
1241 line: 0,
1242 mismatch: LineKindMismatch::RuleNotEmpty,
1243 })
1244 );
1245 assert_eq!(rt.text, "hello world");
1246 assert_eq!(rt.lines[0].kind, LineKind::Para);
1247 assert_eq!(rt.validate(), Ok(()));
1248
1249 let mut tbl = from_markdown("| a | b |\n|---|---|\n| 1 | 2 |").unwrap();
1252 assert_eq!(
1253 tbl.apply_line_ops(&[LineOp::SetKind {
1254 line: 0,
1255 kind: LineKind::Code { lang: None },
1256 }]),
1257 Err(ApplyError::LineKindMismatch {
1258 line: 0,
1259 mismatch: LineKindMismatch::CodeHasSlot,
1260 })
1261 );
1262 assert_eq!(tbl.lines[0].kind, LineKind::Island);
1263 }
1264
1265 #[test]
1268 fn line_op_set_containers_is_depth_capped() {
1269 let mut rt = from_markdown("hi").unwrap();
1270 let deep = vec![Container::Quote; crate::MAX_NESTING_DEPTH + 1];
1271 assert_eq!(
1272 rt.apply_line_ops(&[LineOp::SetContainers {
1273 line: 0,
1274 containers: deep,
1275 }]),
1276 Err(ApplyError::NestingTooDeep {
1277 line: 0,
1278 depth: crate::MAX_NESTING_DEPTH + 1,
1279 max: crate::MAX_NESTING_DEPTH,
1280 })
1281 );
1282 assert!(rt.lines[0].containers.is_empty());
1283 }
1284
1285 #[test]
1286 fn line_op_set_continues_sets_and_clears() {
1287 let mut rt = from_markdown("one two").unwrap();
1291 rt.apply_text_delta(&diff("one two", "one\ntwo")).unwrap();
1292 assert!(!rt.lines[1].continues, "delta-split newline is a new block");
1293
1294 rt.apply_line_ops(&[LineOp::SetContinues {
1295 line: 1,
1296 continues: true,
1297 }])
1298 .unwrap();
1299 assert!(rt.lines[1].continues);
1300 assert_eq!(rt.validate(), Ok(()));
1301 assert_eq!(
1302 crate::export::to_markdown(&rt).matches("\n\n").count(),
1303 0,
1304 "a within-block hard break is not a paragraph boundary"
1305 );
1306
1307 rt.apply_line_ops(&[LineOp::SetContinues {
1309 line: 1,
1310 continues: false,
1311 }])
1312 .unwrap();
1313 assert!(!rt.lines[1].continues);
1314 assert_eq!(rt.validate(), Ok(()));
1315 }
1316
1317 #[test]
1318 fn line_op_set_continues_rejects_first_line() {
1319 let mut rt = from_markdown("one two").unwrap();
1320 rt.apply_text_delta(&diff("one two", "one\ntwo")).unwrap();
1321 let before = rt.clone();
1322 assert_eq!(
1325 rt.apply_line_ops(&[LineOp::SetContinues {
1326 line: 0,
1327 continues: true,
1328 }]),
1329 Err(ApplyError::FirstLineContinues)
1330 );
1331 assert_eq!(rt, before, "rejected op leaves the content untouched");
1332 rt.apply_line_ops(&[LineOp::SetContinues {
1334 line: 0,
1335 continues: false,
1336 }])
1337 .unwrap();
1338 assert_eq!(rt.validate(), Ok(()));
1339 }
1340
1341 fn island(id: &str) -> Island {
1342 Island {
1343 id: id.into(),
1344 island_type: "image".into(),
1345 props: serde_json::json!({}),
1346 loss: crate::model::Loss::Lossless,
1347 }
1348 }
1349
1350 fn content_with_island() -> Content {
1352 let mut rt = Content::empty();
1353 rt.text = format!("a{ISLAND_SLOT}b");
1354 rt.lines = vec![Line {
1355 kind: LineKind::Para,
1356 containers: vec![],
1357 continues: false,
1358 }];
1359 rt.islands = vec![island("i1")];
1360 assert_eq!(rt.validate(), Ok(()));
1361 rt
1362 }
1363
1364 #[test]
1365 fn delete_slot_cascades_island_removal() {
1366 let mut rt = content_with_island();
1367 let d = Delta {
1369 ops: vec![Op::Retain(1), Op::Delete(1), Op::Retain(1)],
1370 };
1371 rt.apply_text_delta(&d).unwrap();
1372 assert_eq!(rt.text, "ab");
1373 assert!(rt.islands.is_empty(), "island cascaded away with its slot");
1374 assert_eq!(rt.validate(), Ok(()));
1376 }
1377
1378 #[test]
1379 fn delete_one_of_two_slots_removes_the_matching_island() {
1380 let mut rt = Content::empty();
1381 rt.text = format!("{ISLAND_SLOT}x{ISLAND_SLOT}");
1382 rt.lines = vec![Line {
1383 kind: LineKind::Para,
1384 containers: vec![],
1385 continues: false,
1386 }];
1387 rt.islands = vec![island("first"), island("second")];
1388 assert_eq!(rt.validate(), Ok(()));
1389
1390 let d = Delta {
1392 ops: vec![Op::Delete(1), Op::Retain(2)],
1393 };
1394 rt.apply_text_delta(&d).unwrap();
1395 assert_eq!(rt.text, format!("x{ISLAND_SLOT}"));
1396 assert_eq!(rt.islands.len(), 1);
1399 assert_eq!(rt.islands[0].id, "second");
1400 assert_eq!(rt.validate(), Ok(()));
1401 }
1402
1403 #[test]
1404 fn insert_raw_slot_is_rejected() {
1405 let mut rt = from_markdown("ab").unwrap();
1406 let d = Delta {
1408 ops: vec![
1409 Op::Retain(1),
1410 Op::Insert(ISLAND_SLOT.to_string()),
1411 Op::Retain(1),
1412 ],
1413 };
1414 assert_eq!(rt.apply_text_delta(&d), Err(ApplyError::IslandSlotInInsert));
1415 assert_eq!(rt.text, "ab");
1417 assert!(rt.islands.is_empty());
1418 assert_eq!(rt.validate(), Ok(()));
1419 }
1420
1421 #[test]
1422 fn insert_carriage_return_is_stripped() {
1423 let mut rt = from_markdown("ab").unwrap();
1427 let d = Delta {
1428 ops: vec![Op::Retain(1), Op::Insert("\r".into()), Op::Retain(1)],
1429 };
1430 rt.apply_text_delta(&d).unwrap();
1431 assert_eq!(rt.text, "ab");
1432 assert_eq!(rt.validate(), Ok(()));
1433 }
1434
1435 #[test]
1436 fn insert_bidi_control_is_stripped() {
1437 let mut rt = from_markdown("ab").unwrap();
1440 let d = Delta {
1441 ops: vec![
1442 Op::Retain(1),
1443 Op::Insert("\u{202E}".into()),
1444 Op::Retain(1),
1445 ],
1446 };
1447 rt.apply_text_delta(&d).unwrap();
1448 assert_eq!(rt.text, "ab");
1449 assert_eq!(rt.validate(), Ok(()));
1450 }
1451
1452 #[test]
1453 fn insert_crlf_keeps_the_newline_and_splits() {
1454 let mut rt = from_markdown("ab").unwrap();
1457 let d = Delta {
1458 ops: vec![Op::Retain(1), Op::Insert("\r\n".into()), Op::Retain(1)],
1459 };
1460 rt.apply_text_delta(&d).unwrap();
1461 assert_eq!(rt.text, "a\nb");
1462 assert_eq!(rt.lines.len(), 2);
1463 assert_eq!(rt.validate(), Ok(()));
1464 }
1465
1466 #[test]
1467 fn insert_of_clean_text_is_not_reallocated() {
1468 let d = Delta {
1471 ops: vec![Op::Retain(1), Op::Insert("clean\n".into()), Op::Retain(1)],
1472 };
1473 assert!(matches!(sanitize_inserts(&d), Cow::Borrowed(_)));
1474 }
1475
1476 #[test]
1477 fn apply_field_change_bundle_order() {
1478 let mut rt = from_markdown("abc").unwrap();
1479 let d = diff("abc", "abXc");
1480 rt.apply_field_change(
1481 &d,
1482 &[],
1483 &[MarkOp::Add {
1484 start: 3,
1485 end: 4,
1486 kind: MarkKind::Strong,
1487 }],
1488 )
1489 .unwrap();
1490 let strong = rt
1491 .marks
1492 .iter()
1493 .find(|m| matches!(m.kind, MarkKind::Strong))
1494 .unwrap();
1495 assert_eq!((strong.start, strong.end), (3, 4));
1496 assert_eq!(rt.text, "abXc");
1497 }
1498
1499 #[test]
1500 fn apply_field_change_is_all_or_nothing() {
1501 let mut rt = from_markdown("abc").unwrap();
1505 let before = rt.clone();
1506 let d = diff("abc", "abXc");
1507 let err = rt.apply_field_change(
1508 &d,
1509 &[],
1510 &[
1511 MarkOp::Add {
1512 start: 0,
1513 end: 2,
1514 kind: MarkKind::Strong,
1515 },
1516 MarkOp::Add {
1517 start: 99,
1518 end: 100,
1519 kind: MarkKind::Emph,
1520 },
1521 ],
1522 );
1523 assert!(matches!(err, Err(ApplyError::MarkOutOfRange { .. })));
1524 assert_eq!(rt, before, "failed bundle must not mutate the content");
1525 }
1526
1527 #[test]
1531 fn add_anchor_id_uniqueness() {
1532 let anchor = |id: &str| MarkKind::Anchor { id: id.into() };
1533 let add = |start, end, id: &str| MarkOp::Add {
1534 start,
1535 end,
1536 kind: anchor(id),
1537 };
1538
1539 let mut rt = from_markdown("abcd").unwrap();
1541 rt.apply_field_change(&diff("abcd", "abcd"), &[], &[add(0, 2, "x")])
1542 .unwrap();
1543 assert_eq!(
1544 rt.apply_field_change(&diff("abcd", "abcd"), &[], &[add(2, 4, "x")]),
1545 Err(ApplyError::AnchorIdCollision { id: "x".into() })
1546 );
1547
1548 let mut rt = from_markdown("abcd").unwrap();
1550 assert_eq!(
1551 rt.apply_field_change(&diff("abcd", "abcd"), &[], &[add(0, 2, "")]),
1552 Err(ApplyError::EmptyAnchorId)
1553 );
1554
1555 let mut rt = from_markdown("abcd").unwrap();
1558 rt.apply_field_change(&diff("abcd", "abcd"), &[], &[add(0, 2, "x")])
1559 .unwrap();
1560 rt.apply_field_change(
1561 &diff("abcd", "abcd"),
1562 &[],
1563 &[MarkOp::RemoveAnchor { id: "x".into() }, add(2, 4, "x")],
1564 )
1565 .unwrap();
1566 let anchors: Vec<_> = rt
1567 .marks
1568 .iter()
1569 .filter(|m| matches!(m.kind, MarkKind::Anchor { .. }))
1570 .collect();
1571 assert_eq!(anchors.len(), 1);
1572 assert_eq!((anchors[0].start, anchors[0].end), (2, 4));
1573 }
1574
1575 fn tag_line(level: u8, continues: bool) -> Line {
1584 Line {
1585 kind: LineKind::Heading { level },
1586 containers: Vec::new(),
1587 continues,
1588 }
1589 }
1590
1591 fn tags(lines: &[Line]) -> Vec<(u8, bool)> {
1594 lines
1595 .iter()
1596 .map(|l| match l.kind {
1597 LineKind::Heading { level } => (level, l.continues),
1598 LineKind::Para => (0, l.continues),
1599 _ => (255, l.continues),
1600 })
1601 .collect()
1602 }
1603
1604 #[test]
1605 fn sync_lines_retain_only_is_identity() {
1606 let old_chars: Vec<char> = "a\nb\nc".chars().collect();
1607 let lines = vec![tag_line(1, false), tag_line(2, false), tag_line(3, false)];
1608 let d = Delta {
1609 ops: vec![Op::Retain(5)],
1610 };
1611 assert_eq!(sync_lines_for_delta(&old_chars, lines.clone(), &d), lines);
1612 }
1613
1614 #[test]
1615 fn sync_lines_insert_newline_clones_split_line_and_clears_continues() {
1616 let old_chars: Vec<char> = "a\nbc".chars().collect();
1620 let l1 = Line {
1621 kind: LineKind::Heading { level: 5 },
1622 containers: vec![Container::Quote],
1623 continues: true,
1624 };
1625 let lines = vec![tag_line(1, false), l1.clone()];
1626 let d = Delta {
1628 ops: vec![Op::Retain(3), Op::Insert("\n".into()), Op::Retain(1)],
1629 };
1630 let out = sync_lines_for_delta(&old_chars, lines, &d);
1631 assert_eq!(out.len(), 3);
1632 assert_eq!(out[1], l1, "first half is the untouched original line");
1633 assert_eq!(out[2].kind, LineKind::Heading { level: 5 });
1634 assert_eq!(out[2].containers, vec![Container::Quote]);
1635 assert!(!out[2].continues, "the split clone starts a new block");
1636 }
1637
1638 #[test]
1639 fn sync_lines_delete_newline_drops_following_line() {
1640 let old_chars: Vec<char> = "a\nb\nc".chars().collect();
1643 let lines = vec![tag_line(1, false), tag_line(2, false), tag_line(3, false)];
1644 let d = Delta {
1645 ops: vec![Op::Retain(1), Op::Delete(1), Op::Retain(3)],
1646 };
1647 let out = sync_lines_for_delta(&old_chars, lines, &d);
1648 assert_eq!(tags(&out), vec![(1, false), (3, false)]);
1649 }
1650
1651 #[test]
1652 fn sync_lines_delete_trailing_newline_without_following_line_is_guarded() {
1653 let old_chars: Vec<char> = "a\n".chars().collect();
1657 let lines = vec![tag_line(1, false)];
1658 let d = Delta {
1659 ops: vec![Op::Retain(1), Op::Delete(1)],
1660 };
1661 let out = sync_lines_for_delta(&old_chars, lines, &d);
1662 assert_eq!(tags(&out), vec![(1, false)]);
1663 }
1664
1665 #[test]
1666 fn sync_lines_stops_at_end_of_old_chars() {
1667 let old_chars: Vec<char> = "a\nb".chars().collect();
1670 let lines = vec![tag_line(1, false), tag_line(2, false)];
1671 let d = Delta {
1672 ops: vec![Op::Retain(99)],
1673 };
1674 assert_eq!(sync_lines_for_delta(&old_chars, lines.clone(), &d), lines);
1675 }
1676
1677 #[test]
1678 fn sync_lines_insert_two_newlines_adds_two_clones() {
1679 let old_chars: Vec<char> = "abc".chars().collect();
1682 let src = Line {
1683 kind: LineKind::Heading { level: 7 },
1684 containers: vec![Container::Quote],
1685 continues: false,
1686 };
1687 let d = Delta {
1688 ops: vec![Op::Retain(1), Op::Insert("\n\n".into()), Op::Retain(2)],
1689 };
1690 let out = sync_lines_for_delta(&old_chars, vec![src], &d);
1691 assert_eq!(out.len(), 3);
1692 for l in &out {
1693 assert_eq!(l.kind, LineKind::Heading { level: 7 });
1694 assert_eq!(l.containers, vec![Container::Quote]);
1695 assert!(!l.continues);
1696 }
1697 }
1698
1699 #[test]
1702 fn split_line_rebases_mark_across_the_split_point() {
1703 let mut rt = from_markdown("abcd").unwrap();
1708 rt.apply_mark_ops(&[MarkOp::Add {
1709 start: 1,
1710 end: 3,
1711 kind: MarkKind::Strong,
1712 }])
1713 .unwrap();
1714 rt.apply_line_ops(&[LineOp::Split { at: 2 }]).unwrap();
1715 assert_eq!(rt.text, "ab\ncd");
1716 let strong: Vec<_> = rt
1717 .marks
1718 .iter()
1719 .filter(|m| matches!(m.kind, MarkKind::Strong))
1720 .map(|m| (m.start, m.end))
1721 .collect();
1722 assert_eq!(strong, vec![(1, 4)]);
1725 assert_eq!(rt.validate(), Ok(()));
1726 }
1727
1728 #[test]
1729 fn join_line_rebases_marks_to_final_text_coordinates() {
1730 let mut rt = from_markdown("ab").unwrap();
1735 rt.apply_text_delta(&diff("ab", "ab\ncd")).unwrap();
1736 rt.marks.push(Mark {
1737 start: 2,
1738 end: 4,
1739 kind: MarkKind::Strong,
1740 });
1741 rt.normalize();
1742 rt.apply_line_ops(&[LineOp::Join { line: 0 }]).unwrap();
1745 assert_eq!(rt.text, "abcd");
1746 let strong: Vec<_> = rt
1747 .marks
1748 .iter()
1749 .filter(|m| matches!(m.kind, MarkKind::Strong))
1750 .map(|m| (m.start, m.end))
1751 .collect();
1752 assert_eq!(strong, vec![(2, 3)], "strong lands on 'c', not 'd' or 'cd'");
1753 assert_eq!(rt.validate(), Ok(()));
1754 }
1755
1756 #[test]
1757 fn field_change_terminal_normalize_matches_per_stage_normalize() {
1758 let start = from_markdown("hello world").unwrap();
1763 let text_delta = diff("hello world", "hello brave world");
1764 let line_ops = vec![LineOp::Split { at: 5 }]; let mark_ops = vec![MarkOp::Add {
1766 start: 0,
1767 end: 5,
1768 kind: MarkKind::Strong,
1769 }];
1770
1771 let mut bundled = start.clone();
1772 bundled
1773 .apply_field_change(&text_delta, &line_ops, &mark_ops)
1774 .unwrap();
1775
1776 let mut staged = start;
1777 staged.apply_text_delta(&text_delta).unwrap();
1778 staged.apply_line_ops(&line_ops).unwrap();
1779 staged.apply_mark_ops(&mark_ops).unwrap();
1780
1781 assert_eq!(bundled, staged, "terminal normalize diverged from per-stage");
1782 assert_eq!(bundled.validate(), Ok(()));
1783 }
1784
1785 #[test]
1786 fn sync_lines_select_all_delete_collapses_to_first_line() {
1787 let text: String = (0..50).map(|i| format!("line{i}\n")).collect();
1791 let old_chars: Vec<char> = text.chars().collect();
1792 let lines: Vec<Line> = (0..=50).map(|i| tag_line((i % 200) as u8, false)).collect();
1793 assert_eq!(lines.len(), old_chars.iter().filter(|&&c| c == '\n').count() + 1);
1794 let d = Delta {
1795 ops: vec![Op::Delete(old_chars.len())],
1796 };
1797 let out = sync_lines_for_delta(&old_chars, lines, &d);
1798 assert_eq!(tags(&out), vec![(0, false)], "only the first line survives");
1799 }
1800
1801 #[test]
1802 fn sync_lines_insert_newline_past_end_appends_default() {
1803 let old_chars: Vec<char> = "a\n".chars().collect();
1807 let lines = vec![tag_line(1, false)];
1808 let d = Delta {
1810 ops: vec![Op::Retain(2), Op::Insert("\n".into())],
1811 };
1812 let out = sync_lines_for_delta(&old_chars, lines, &d);
1813 assert_eq!(out.len(), 2);
1814 assert_eq!(tags(&out)[0], (1, false));
1815 assert_eq!(out[1].kind, LineKind::Para);
1816 assert!(out[1].containers.is_empty());
1817 assert!(!out[1].continues);
1818 }
1819}