1use std::collections::hash_map::DefaultHasher;
32use std::hash::{Hash, Hasher};
33
34use crate::wrap::{ParagraphObjective, display_width, wrap_optimal};
35
36#[derive(Debug, Clone)]
42struct BreakSolution {
43 lines: Vec<String>,
45 total_cost: u64,
47 #[allow(dead_code)]
49 line_badness: Vec<u64>,
50 width: usize,
52 text_hash: u64,
54}
55
56impl BreakSolution {
57 fn is_valid(&self, text_hash: u64, width: usize) -> bool {
59 self.text_hash == text_hash && self.width == width
60 }
61}
62
63fn hash_paragraph(text: &str) -> u64 {
65 let mut hasher = DefaultHasher::new();
66 text.hash(&mut hasher);
67 hasher.finish()
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub struct EditEvent {
79 pub offset: usize,
81 pub deleted: usize,
83 pub inserted: usize,
85}
86
87#[derive(Debug, Clone)]
93pub struct ReflowResult {
94 pub lines: Vec<String>,
96 pub recomputed: Vec<usize>,
98 pub total_cost: u64,
100 pub paragraph_count: usize,
102}
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub struct BreakerSnapshot {
111 pub width: usize,
113 pub cached_paragraphs: usize,
115 pub dirty_paragraphs: usize,
117 pub generation: u64,
119 pub total_reflows: u64,
121 pub cache_hits: u64,
123 pub cache_misses: u64,
125}
126
127#[derive(Debug, Clone)]
136pub struct IncrementalBreaker {
137 width: usize,
139 objective: ParagraphObjective,
141 solutions: Vec<Option<BreakSolution>>,
143 generation: u64,
145 total_reflows: u64,
147 cache_hits: u64,
149 cache_misses: u64,
151}
152
153impl IncrementalBreaker {
154 #[must_use]
156 pub fn new(width: usize, objective: ParagraphObjective) -> Self {
157 Self {
158 width,
159 objective,
160 solutions: Vec::new(),
161 generation: 0,
162 total_reflows: 0,
163 cache_hits: 0,
164 cache_misses: 0,
165 }
166 }
167
168 #[must_use]
170 pub fn terminal(width: usize) -> Self {
171 Self::new(width, ParagraphObjective::terminal())
172 }
173
174 #[must_use]
176 pub fn width(&self) -> usize {
177 self.width
178 }
179
180 #[must_use]
182 pub fn generation(&self) -> u64 {
183 self.generation
184 }
185
186 #[must_use]
188 pub fn objective(&self) -> &ParagraphObjective {
189 &self.objective
190 }
191
192 pub fn set_width(&mut self, width: usize) {
196 if self.width != width {
197 self.width = width;
198 self.generation += 1;
199 }
201 }
202
203 pub fn set_objective(&mut self, objective: ParagraphObjective) {
207 self.objective = objective;
208 self.generation += 1;
209 self.solutions.clear();
210 }
211
212 pub fn invalidate_all(&mut self) {
214 self.generation += 1;
215 self.solutions.clear();
216 }
217
218 pub fn invalidate_paragraph(&mut self, paragraph_idx: usize) {
222 if paragraph_idx < self.solutions.len() {
223 self.solutions[paragraph_idx] = None;
224 self.generation += 1;
225 }
226 }
227
228 pub fn notify_edit(&mut self, old_text: &str, event: &EditEvent) {
234 let paragraphs: Vec<&str> = old_text.split('\n').collect();
235
236 let mut byte_offset = 0;
238 for (idx, para) in paragraphs.iter().enumerate() {
239 let para_end = byte_offset + para.len();
240 let edit_end = event.offset + event.deleted;
242 if event.offset <= para_end && edit_end >= byte_offset {
243 self.invalidate_paragraph(idx);
244 }
245 byte_offset = para_end + 1; }
247
248 if event.deleted != event.inserted {
251 let mut byte_offset = 0;
252 for (idx, para) in paragraphs.iter().enumerate() {
253 let para_end = byte_offset + para.len();
254 if para_end >= event.offset {
255 self.invalidate_paragraph(idx);
256 }
257 byte_offset = para_end + 1;
258 }
259 }
260 }
261
262 pub fn reflow(&mut self, text: &str) -> ReflowResult {
267 self.total_reflows += 1;
268
269 let paragraphs: Vec<&str> = text.split('\n').collect();
270 let para_count = paragraphs.len();
271
272 self.solutions.resize_with(para_count, || None);
274 self.solutions.truncate(para_count);
276
277 let mut all_lines = Vec::new();
278 let mut recomputed = Vec::new();
279 let mut total_cost = 0u64;
280
281 for (idx, paragraph) in paragraphs.iter().enumerate() {
282 let text_hash = hash_paragraph(paragraph);
283
284 let cached_valid = self.solutions[idx]
286 .as_ref()
287 .is_some_and(|sol| sol.is_valid(text_hash, self.width));
288
289 if cached_valid {
290 let sol = self.solutions[idx].as_ref().unwrap();
291 all_lines.extend(sol.lines.iter().cloned());
292 total_cost = total_cost.saturating_add(sol.total_cost);
293 self.cache_hits += 1;
294 } else {
295 let sol = self.break_paragraph(paragraph, text_hash);
297 all_lines.extend(sol.lines.iter().cloned());
298 total_cost = total_cost.saturating_add(sol.total_cost);
299 self.solutions[idx] = Some(sol);
300 recomputed.push(idx);
301 self.cache_misses += 1;
302 }
303 }
304
305 ReflowResult {
306 lines: all_lines,
307 recomputed,
308 total_cost,
309 paragraph_count: para_count,
310 }
311 }
312
313 pub fn reflow_full(&mut self, text: &str) -> ReflowResult {
315 self.invalidate_all();
316 self.reflow(text)
317 }
318
319 fn break_paragraph(&self, paragraph: &str, text_hash: u64) -> BreakSolution {
321 if paragraph.is_empty() {
322 return BreakSolution {
323 lines: vec![String::new()],
324 total_cost: 0,
325 line_badness: vec![0],
326 width: self.width,
327 text_hash,
328 };
329 }
330
331 let result = wrap_optimal(paragraph, self.width);
332
333 let mut adjusted_cost = result.total_cost;
335 if result.lines.len() > 1 {
336 if let Some(last) = result.lines.last() {
337 let last_chars = display_width(last);
338 adjusted_cost =
339 adjusted_cost.saturating_add(self.objective.widow_demerits(last_chars));
340 }
341 if let Some(first) = result.lines.first() {
342 let first_chars = display_width(first);
343 adjusted_cost =
344 adjusted_cost.saturating_add(self.objective.orphan_demerits(first_chars));
345 }
346 }
347
348 BreakSolution {
349 lines: result.lines,
350 total_cost: adjusted_cost,
351 line_badness: result.line_badness,
352 width: self.width,
353 text_hash,
354 }
355 }
356
357 #[must_use]
359 pub fn snapshot(&self) -> BreakerSnapshot {
360 let cached = self.solutions.iter().filter(|s| s.is_some()).count();
361 let dirty = self.solutions.iter().filter(|s| s.is_none()).count();
362 BreakerSnapshot {
363 width: self.width,
364 cached_paragraphs: cached,
365 dirty_paragraphs: dirty,
366 generation: self.generation,
367 total_reflows: self.total_reflows,
368 cache_hits: self.cache_hits,
369 cache_misses: self.cache_misses,
370 }
371 }
372}
373
374#[cfg(test)]
379mod tests {
380 use super::*;
381
382 fn breaker(width: usize) -> IncrementalBreaker {
383 IncrementalBreaker::terminal(width)
384 }
385
386 #[test]
389 fn reflow_empty_text() {
390 let mut b = breaker(80);
391 let r = b.reflow("");
392 assert_eq!(r.lines, vec![""]);
393 assert_eq!(r.paragraph_count, 1);
394 }
395
396 #[test]
397 fn reflow_single_word() {
398 let mut b = breaker(80);
399 let r = b.reflow("hello");
400 assert_eq!(r.lines, vec!["hello"]);
401 assert_eq!(r.total_cost, 0); }
403
404 #[test]
405 fn reflow_fits_in_width() {
406 let mut b = breaker(80);
407 let r = b.reflow("The quick brown fox jumps over the lazy dog.");
408 assert_eq!(r.lines.len(), 1);
409 assert_eq!(r.recomputed, vec![0]);
410 }
411
412 #[test]
413 fn reflow_wraps_long_text() {
414 let mut b = breaker(20);
415 let text = "The quick brown fox jumps over the lazy dog.";
416 let r = b.reflow(text);
417 assert!(r.lines.len() > 1);
418 for line in &r.lines[..r.lines.len() - 1] {
420 assert!(
421 display_width(line) <= 20,
422 "line too wide: {:?} (width {})",
423 line,
424 display_width(line)
425 );
426 }
427 }
428
429 #[test]
430 fn reflow_preserves_paragraphs() {
431 let mut b = breaker(80);
432 let r = b.reflow("First paragraph.\nSecond paragraph.\nThird.");
433 assert_eq!(r.paragraph_count, 3);
434 assert_eq!(r.lines.len(), 3);
435 }
436
437 #[test]
440 fn second_reflow_uses_cache() {
441 let mut b = breaker(80);
442 let text = "Hello world.";
443 b.reflow(text);
444 let r2 = b.reflow(text);
445 assert!(r2.recomputed.is_empty(), "second reflow should be cached");
446 }
447
448 #[test]
449 fn cache_hit_increments_counter() {
450 let mut b = breaker(80);
451 let text = "Hello.";
452 b.reflow(text);
453 b.reflow(text);
454 let snap = b.snapshot();
455 assert_eq!(snap.cache_hits, 1);
456 assert_eq!(snap.cache_misses, 1);
457 }
458
459 #[test]
460 fn width_change_invalidates_cache() {
461 let mut b = breaker(80);
462 let text = "Hello world.";
463 b.reflow(text);
464 b.set_width(40);
465 let r = b.reflow(text);
466 assert_eq!(r.recomputed, vec![0]);
467 }
468
469 #[test]
470 fn text_change_invalidates_paragraph() {
471 let mut b = breaker(80);
472 b.reflow("Hello.\nWorld.");
473 let r2 = b.reflow("Hello.\nChanged.");
474 assert_eq!(r2.recomputed, vec![1]);
476 }
477
478 #[test]
479 fn invalidate_all_forces_recomputation() {
480 let mut b = breaker(80);
481 b.reflow("Hello.\nWorld.");
482 b.invalidate_all();
483 let r = b.reflow("Hello.\nWorld.");
484 assert_eq!(r.recomputed, vec![0, 1]);
485 }
486
487 #[test]
488 fn invalidate_paragraph_selective() {
489 let mut b = breaker(80);
490 b.reflow("A.\nB.\nC.");
491 b.invalidate_paragraph(1);
492 let r = b.reflow("A.\nB.\nC.");
493 assert_eq!(r.recomputed, vec![1]);
495 }
496
497 #[test]
498 fn invalidate_out_of_bounds_is_noop() {
499 let mut b = breaker(80);
500 b.reflow("Hello.");
501 b.invalidate_paragraph(999); }
503
504 #[test]
507 fn notify_edit_invalidates_affected_paragraph() {
508 let mut b = breaker(80);
509 let text = "First.\nSecond.\nThird.";
510 b.reflow(text);
511 b.notify_edit(
513 text,
514 &EditEvent {
515 offset: 7,
516 deleted: 7,
517 inserted: 8,
518 },
519 );
520 let r = b.reflow("First.\nChanged!.\nThird.");
521 assert!(r.recomputed.contains(&1));
523 }
524
525 #[test]
526 fn notify_edit_at_start() {
527 let mut b = breaker(80);
528 let text = "Hello.\nWorld.";
529 b.reflow(text);
530 b.notify_edit(
531 text,
532 &EditEvent {
533 offset: 0,
534 deleted: 5,
535 inserted: 3,
536 },
537 );
538 let r = b.reflow("Hi.\nWorld.");
539 assert!(r.recomputed.contains(&0));
540 }
541
542 #[test]
545 fn set_width_same_is_noop() {
546 let mut b = breaker(80);
547 b.reflow("Test.");
548 let prev_gen = b.generation();
549 b.set_width(80);
550 assert_eq!(b.generation(), prev_gen);
551 }
552
553 #[test]
554 fn set_width_different_bumps_generation() {
555 let mut b = breaker(80);
556 let prev_gen = b.generation();
557 b.set_width(40);
558 assert!(b.generation() > prev_gen);
559 }
560
561 #[test]
564 fn set_objective_clears_cache() {
565 let mut b = breaker(80);
566 b.reflow("Hello world.");
567 b.set_objective(ParagraphObjective::typographic());
568 let snap = b.snapshot();
569 assert_eq!(snap.cached_paragraphs, 0);
570 }
571
572 #[test]
575 fn reflow_full_recomputes_everything() {
576 let mut b = breaker(80);
577 b.reflow("A.\nB.");
578 let r = b.reflow_full("A.\nB.");
579 assert_eq!(r.recomputed, vec![0, 1]);
580 }
581
582 #[test]
585 fn snapshot_initial_state() {
586 let b = breaker(80);
587 let snap = b.snapshot();
588 assert_eq!(snap.width, 80);
589 assert_eq!(snap.cached_paragraphs, 0);
590 assert_eq!(snap.dirty_paragraphs, 0);
591 assert_eq!(snap.generation, 0);
592 assert_eq!(snap.total_reflows, 0);
593 }
594
595 #[test]
596 fn snapshot_after_reflow() {
597 let mut b = breaker(80);
598 b.reflow("A.\nB.\nC.");
599 let snap = b.snapshot();
600 assert_eq!(snap.cached_paragraphs, 3);
601 assert_eq!(snap.total_reflows, 1);
602 assert_eq!(snap.cache_misses, 3);
603 }
604
605 #[test]
608 fn reflow_deterministic() {
609 let mut b1 = breaker(30);
610 let mut b2 = breaker(30);
611 let text = "The quick brown fox jumps over the lazy dog near the river bank.";
612 let r1 = b1.reflow(text);
613 let r2 = b2.reflow(text);
614 assert_eq!(r1.lines, r2.lines);
615 assert_eq!(r1.total_cost, r2.total_cost);
616 }
617
618 #[test]
619 fn reflow_idempotent() {
620 let mut b = breaker(30);
621 let text = "The quick brown fox jumps over the lazy dog.";
622 let r1 = b.reflow(text);
623 let r2 = b.reflow_full(text);
624 assert_eq!(r1.lines, r2.lines);
625 assert_eq!(r1.total_cost, r2.total_cost);
626 }
627
628 #[test]
631 fn reflow_zero_width() {
632 let mut b = breaker(0);
633 let r = b.reflow("Hello");
634 assert!(!r.lines.is_empty());
635 }
636
637 #[test]
638 fn reflow_very_narrow() {
639 let mut b = breaker(1);
640 let r = b.reflow("abc");
641 assert!(!r.lines.is_empty());
643 }
644
645 #[test]
646 fn reflow_only_newlines() {
647 let mut b = breaker(80);
648 let r = b.reflow("\n\n\n");
649 assert_eq!(r.paragraph_count, 4); }
651
652 #[test]
653 fn reflow_paragraph_count_changes() {
654 let mut b = breaker(80);
655 b.reflow("A.\nB.\nC.");
656 let r = b.reflow("A.\nB.");
658 assert_eq!(r.paragraph_count, 2);
659 }
660
661 #[test]
662 fn reflow_paragraph_count_grows() {
663 let mut b = breaker(80);
664 b.reflow("A.\nB.");
665 let r = b.reflow("A.\nB.\nC.\nD.");
666 assert_eq!(r.paragraph_count, 4);
667 assert!(r.recomputed.contains(&2));
669 assert!(r.recomputed.contains(&3));
670 }
671
672 #[test]
673 fn multiple_edits_accumulate() {
674 let mut b = breaker(80);
675 b.reflow("One.\nTwo.\nThree.");
676 b.invalidate_paragraph(0);
677 b.invalidate_paragraph(2);
678 let r = b.reflow("One.\nTwo.\nThree.");
679 assert_eq!(r.recomputed, vec![0, 2]);
680 }
681
682 #[test]
683 fn long_paragraph_performance() {
684 let mut b = breaker(60);
685 let text = "word ".repeat(500);
686 let r = b.reflow(&text);
687 assert!(r.lines.len() > 1);
688 let r2 = b.reflow(&text);
690 assert!(r2.recomputed.is_empty());
691 }
692
693 #[test]
696 fn terminal_constructor() {
697 let b = IncrementalBreaker::terminal(120);
698 assert_eq!(b.width(), 120);
699 assert_eq!(b.objective().line_penalty, 20); }
701
702 #[test]
703 fn new_with_default_objective() {
704 let b = IncrementalBreaker::new(80, ParagraphObjective::default());
705 assert_eq!(b.objective().line_penalty, 10); }
707}