1use crate::change::{Change, ChangeKind, ChangeSpan};
4use crate::diff::DiffResult;
5use rustc_hash::FxHashSet;
6use serde::{Deserialize, Serialize};
7use std::sync::Arc;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
11pub enum StepDirection {
12 #[default]
13 None,
14 Forward,
15 Backward,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
20pub enum AnimationFrame {
21 #[default]
22 Idle,
23 FadeOut,
24 FadeIn,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct StepState {
30 pub current_step: usize,
32 pub total_steps: usize,
34 pub applied_changes: Vec<usize>,
36 #[serde(skip, default)]
38 applied_changes_set: FxHashSet<usize>,
39 pub active_change: Option<usize>,
41 #[serde(default, skip_serializing_if = "Option::is_none")]
43 pub cursor_change: Option<usize>,
44 #[serde(default, skip_serializing_if = "Option::is_none")]
46 pub animating_hunk: Option<usize>,
47 pub step_direction: StepDirection,
49 pub current_hunk: usize,
51 pub total_hunks: usize,
53 #[serde(default)]
55 pub last_nav_was_hunk: bool,
56 #[serde(default)]
58 pub hunk_preview_mode: bool,
59 #[serde(default)]
61 pub preview_from_backward: bool,
62 #[serde(default)]
64 pub show_hunk_extent_while_stepping: bool,
65}
66
67impl StepState {
68 pub fn new(total_changes: usize, total_hunks: usize) -> Self {
69 Self {
70 current_step: 0,
71 total_steps: total_changes + 1, applied_changes: Vec::new(),
73 applied_changes_set: FxHashSet::default(),
74 active_change: None,
75 cursor_change: None,
76 animating_hunk: None,
77 step_direction: StepDirection::None,
78 current_hunk: 0,
79 total_hunks,
80 last_nav_was_hunk: false,
81 hunk_preview_mode: false,
82 preview_from_backward: false,
83 show_hunk_extent_while_stepping: false,
84 }
85 }
86
87 pub fn is_at_start(&self) -> bool {
89 self.current_step == 0
90 }
91
92 pub fn is_at_end(&self) -> bool {
94 self.current_step >= self.total_steps - 1
95 }
96
97 pub fn progress(&self) -> f64 {
99 if self.total_steps <= 1 {
100 return 100.0;
101 }
102 (self.current_step as f64 / (self.total_steps - 1) as f64) * 100.0
103 }
104
105 fn rebuild_applied_set(&mut self) {
106 self.applied_changes_set = self.applied_changes.iter().copied().collect();
107 }
108
109 pub fn is_applied(&self, change_id: usize) -> bool {
110 self.applied_changes_set.contains(&change_id)
111 }
112
113 fn push_applied(&mut self, change_id: usize) {
114 if self.applied_changes_set.insert(change_id) {
115 self.applied_changes.push(change_id);
116 }
117 }
118
119 fn pop_applied(&mut self) -> Option<usize> {
120 let change_id = self.applied_changes.pop()?;
121 self.applied_changes_set.remove(&change_id);
122 Some(change_id)
123 }
124
125 fn truncate_applied_to(&mut self, new_len: usize) -> usize {
126 let old_len = self.applied_changes.len();
127 if new_len >= old_len {
128 return 0;
129 }
130 for change_id in &self.applied_changes[new_len..] {
131 self.applied_changes_set.remove(change_id);
132 }
133 self.applied_changes.truncate(new_len);
134 old_len - new_len
135 }
136
137 fn clear_applied(&mut self) {
138 self.applied_changes.clear();
139 self.applied_changes_set.clear();
140 }
141}
142
143pub struct DiffNavigator {
145 diff: DiffResult,
147 state: StepState,
149 old_content: Arc<str>,
151 new_content: Arc<str>,
153 change_to_hunk: Vec<Option<usize>>,
155 change_id_to_hunk_exact: Vec<Option<usize>>,
157 change_to_index: Vec<Option<usize>>,
159 change_to_step_index: Vec<Option<usize>>,
161 lazy_maps: bool,
163 hunk_step_ranges: Vec<Option<HunkStepRange>>,
165 hunk_change_ranges: Vec<Option<(usize, usize)>>,
167 hunk_change_ranges_exact: Vec<Option<(usize, usize)>>,
169 evo_visible_index: Option<Vec<Option<usize>>>,
171 evo_visible_len: Option<usize>,
173 evo_display_to_change: Option<Vec<usize>>,
175 evo_nearest_visible: Option<Vec<Option<usize>>>,
177}
178
179const LARGE_CONTEXT_PAD: usize = 3;
180
181#[derive(Debug, Clone, Copy)]
182struct HunkStepRange {
183 start: usize,
184 len: usize,
185}
186
187impl DiffNavigator {
188 pub fn new(
189 diff: DiffResult,
190 old_content: Arc<str>,
191 new_content: Arc<str>,
192 lazy_maps: bool,
193 ) -> Self {
194 let total_changes = diff.significant_changes.len();
195 let total_hunks = diff.hunks.len();
196
197 let mut change_to_hunk = vec![None; diff.changes.len()];
199 let mut max_change_id = 0usize;
200 for change in diff.changes.iter() {
201 max_change_id = max_change_id.max(change.id);
202 }
203 let mut change_to_index = vec![None; max_change_id.saturating_add(1)];
204 for (idx, change) in diff.changes.iter().enumerate() {
205 if let Some(slot) = change_to_index.get_mut(change.id) {
206 *slot = Some(idx);
207 }
208 }
209 let mut change_to_step_index = vec![None; max_change_id.saturating_add(1)];
210 for (idx, change_id) in diff.significant_changes.iter().enumerate() {
211 if let Some(slot) = change_to_step_index.get_mut(*change_id) {
212 *slot = Some(idx);
213 }
214 }
215
216 let mut change_id_to_hunk_exact = vec![None; max_change_id.saturating_add(1)];
217 let mut hunk_change_ranges = vec![None; diff.hunks.len()];
218 let mut hunk_change_ranges_exact = vec![None; diff.hunks.len()];
219 for (hunk_idx, hunk) in diff.hunks.iter().enumerate() {
220 let mut min_idx = usize::MAX;
221 let mut max_idx = 0usize;
222 for &change_id in &hunk.change_ids {
223 if let Some(slot) = change_id_to_hunk_exact.get_mut(change_id) {
224 *slot = Some(hunk.id);
225 }
226 if let Some(Some(idx)) = change_to_index.get(change_id) {
227 min_idx = min_idx.min(*idx);
228 max_idx = max_idx.max(*idx);
229 }
230 }
231 if min_idx == usize::MAX {
232 continue;
233 }
234 hunk_change_ranges_exact[hunk_idx] = Some((min_idx, max_idx));
235 let start = if lazy_maps {
236 min_idx.saturating_sub(LARGE_CONTEXT_PAD)
237 } else {
238 min_idx
239 };
240 let end = if lazy_maps {
241 (max_idx + LARGE_CONTEXT_PAD).min(diff.changes.len().saturating_sub(1))
242 } else {
243 max_idx
244 };
245 hunk_change_ranges[hunk_idx] = Some((start, end));
246 for idx in start..=end {
247 if let Some(slot) = change_to_hunk.get_mut(idx) {
248 if slot.is_none() {
249 *slot = Some(hunk_idx);
250 }
251 }
252 }
253 }
254
255 let mut hunk_step_ranges = vec![None; diff.hunks.len()];
256 for (hunk_idx, hunk) in diff.hunks.iter().enumerate() {
257 if hunk.change_ids.is_empty() {
258 continue;
259 }
260 let mut min = usize::MAX;
261 let mut count = 0usize;
262 for change_id in &hunk.change_ids {
263 if let Some(Some(step_idx)) = change_to_step_index.get(*change_id) {
264 if *step_idx < min {
265 min = *step_idx;
266 }
267 count += 1;
268 }
269 }
270 if count > 0 && min != usize::MAX {
271 hunk_step_ranges[hunk_idx] = Some(HunkStepRange {
272 start: min,
273 len: count,
274 });
275 }
276 }
277
278 Self {
279 diff,
280 state: StepState::new(total_changes, total_hunks),
281 old_content,
282 new_content,
283 change_to_hunk,
284 change_id_to_hunk_exact,
285 change_to_index,
286 change_to_step_index,
287 lazy_maps,
288 hunk_step_ranges,
289 hunk_change_ranges,
290 hunk_change_ranges_exact,
291 evo_visible_index: None,
292 evo_visible_len: None,
293 evo_display_to_change: None,
294 evo_nearest_visible: None,
295 }
296 }
297
298 pub fn state(&self) -> &StepState {
300 &self.state
301 }
302
303 #[cfg(test)]
305 pub fn state_mut(&mut self) -> &mut StepState {
306 &mut self.state
307 }
308
309 pub fn set_state(&mut self, state: StepState) -> bool {
311 if state.total_steps != self.state.total_steps
312 || state.total_hunks != self.state.total_hunks
313 {
314 return false;
315 }
316 self.state = state;
317 self.state.rebuild_applied_set();
318 true
319 }
320
321 pub fn diff(&self) -> &DiffResult {
323 &self.diff
324 }
325
326 fn change_visible_in_evolution(change: &Change) -> bool {
327 let mut has_old = false;
328 let mut has_new = false;
329 for span in &change.spans {
330 match span.kind {
331 ChangeKind::Insert => has_new = true,
332 ChangeKind::Delete => has_old = true,
333 ChangeKind::Replace => {
334 has_old = true;
335 has_new = true;
336 }
337 ChangeKind::Equal => {}
338 }
339 }
340 !has_old || has_new
341 }
342
343 fn change_visible_in_evolution_state(&self, change: &Change) -> bool {
344 let applied = self.state.is_applied(change.id);
345 let mut has_old = false;
346 let mut has_new = false;
347 for span in &change.spans {
348 match span.kind {
349 ChangeKind::Insert => has_new = true,
350 ChangeKind::Delete => has_old = true,
351 ChangeKind::Replace => {
352 has_old = true;
353 has_new = true;
354 }
355 ChangeKind::Equal => {}
356 }
357 }
358 if has_old && !has_new {
359 return !applied;
360 }
361 if has_new && !has_old {
362 return applied;
363 }
364 true
365 }
366
367 fn ensure_evo_visible_index(&mut self) {
368 if self.evo_visible_index.is_some() {
369 return;
370 }
371 let mut mapping = Vec::with_capacity(self.diff.changes.len());
372 let mut display_to_change = Vec::new();
373 let mut display_idx = 0usize;
374 for (idx, change) in self.diff.changes.iter().enumerate() {
375 if Self::change_visible_in_evolution(change) {
376 mapping.push(Some(display_idx));
377 display_to_change.push(idx);
378 display_idx += 1;
379 } else {
380 mapping.push(None);
381 }
382 }
383 self.evo_visible_len = Some(display_idx);
384 self.evo_visible_index = Some(mapping);
385 self.evo_display_to_change = Some(display_to_change);
386
387 let mut prev_visible = vec![None; self.diff.changes.len()];
388 let mut last_visible = None;
389 for (idx, change) in self.diff.changes.iter().enumerate() {
390 if Self::change_visible_in_evolution(change) {
391 last_visible = Some(idx);
392 }
393 prev_visible[idx] = last_visible;
394 }
395 let mut next_visible = vec![None; self.diff.changes.len()];
396 let mut next = None;
397 for (idx, change) in self.diff.changes.iter().enumerate().rev() {
398 if Self::change_visible_in_evolution(change) {
399 next = Some(idx);
400 }
401 next_visible[idx] = next;
402 }
403 let mut nearest = vec![None; self.diff.changes.len()];
404 for idx in 0..self.diff.changes.len() {
405 match (prev_visible[idx], next_visible[idx]) {
406 (Some(prev), Some(next)) => {
407 let prev_dist = idx.saturating_sub(prev);
408 let next_dist = next.saturating_sub(idx);
409 nearest[idx] = if next_dist < prev_dist {
410 Some(next)
411 } else {
412 Some(prev)
413 };
414 }
415 (Some(prev), None) => nearest[idx] = Some(prev),
416 (None, Some(next)) => nearest[idx] = Some(next),
417 (None, None) => nearest[idx] = None,
418 }
419 }
420 self.evo_nearest_visible = Some(nearest);
421 }
422
423 pub fn evolution_display_index_for_change(&mut self, change_id: usize) -> Option<usize> {
424 self.ensure_evo_visible_index();
425 let idx = self.change_index_for(change_id)?;
426 self.evo_visible_index
427 .as_ref()
428 .and_then(|mapping| mapping.get(idx).copied().flatten())
429 }
430
431 pub fn evolution_display_index_for_change_index(&mut self, change_idx: usize) -> Option<usize> {
432 self.ensure_evo_visible_index();
433 self.evo_visible_index
434 .as_ref()
435 .and_then(|mapping| mapping.get(change_idx).copied().flatten())
436 }
437
438 pub fn evolution_display_index_or_nearest(&mut self, change_id: usize) -> Option<usize> {
439 if let Some(idx) = self.evolution_display_index_for_change(change_id) {
440 return Some(idx);
441 }
442 self.ensure_evo_visible_index();
443 let change_idx = self.change_index_for(change_id)?;
444 let nearest = self
445 .evo_nearest_visible
446 .as_ref()
447 .and_then(|mapping| mapping.get(change_idx).copied().flatten())?;
448 self.evolution_display_index_for_change_index(nearest)
449 }
450
451 pub fn evolution_nearest_visible_change_id(&mut self, change_id: usize) -> Option<usize> {
452 self.ensure_evo_visible_index();
453 let change_idx = self.change_index_for(change_id)?;
454 let nearest = self
455 .evo_nearest_visible
456 .as_ref()
457 .and_then(|mapping| mapping.get(change_idx).copied().flatten())?;
458 self.diff.changes.get(nearest).map(|change| change.id)
459 }
460
461 pub fn evolution_nearest_visible_change_id_dynamic(
462 &self,
463 change_id: usize,
464 max_scan: usize,
465 ) -> Option<usize> {
466 let idx = self.change_index_for(change_id)?;
467 if self.diff.changes.is_empty() {
468 return None;
469 }
470 let mut offset = 0usize;
471 while offset <= max_scan {
472 if let Some(left) = idx.checked_sub(offset) {
473 let change = &self.diff.changes[left];
474 if self.change_visible_in_evolution_state(change) {
475 return Some(change.id);
476 }
477 }
478 let right = idx + offset;
479 if right < self.diff.changes.len() {
480 let change = &self.diff.changes[right];
481 if self.change_visible_in_evolution_state(change) {
482 return Some(change.id);
483 }
484 }
485 offset += 1;
486 }
487 None
488 }
489
490 pub fn evolution_visible_len(&mut self) -> usize {
491 self.ensure_evo_visible_index();
492 self.evo_visible_len.unwrap_or(0)
493 }
494
495 pub fn evolution_change_range_for_display(
496 &mut self,
497 display_idx: usize,
498 radius: usize,
499 ) -> Option<(usize, usize)> {
500 self.ensure_evo_visible_index();
501 let visible_len = self.evo_visible_len?;
502 if visible_len == 0 {
503 return None;
504 }
505 let display_idx = display_idx.min(visible_len.saturating_sub(1));
506 let start_display = display_idx.saturating_sub(radius);
507 let end_display = (display_idx + radius).min(visible_len.saturating_sub(1));
508 let display_to_change = self.evo_display_to_change.as_ref()?;
509 let start_change = *display_to_change.get(start_display)?;
510 let end_change = *display_to_change.get(end_display)?;
511 Some((start_change, end_change))
512 }
513
514 pub fn set_cursor_hunk(&mut self, hunk_idx: usize, change_id: Option<usize>) {
516 if self.state.total_hunks > 0 {
517 self.state.current_hunk = hunk_idx.min(self.state.total_hunks - 1);
518 }
519 self.state.cursor_change = change_id;
520 }
521
522 pub fn set_cursor_override(&mut self, change_id: Option<usize>) {
524 self.state.cursor_change = change_id;
525 self.state.active_change = None;
526 self.state.step_direction = StepDirection::None;
527 self.state.animating_hunk = None;
528 }
529
530 pub fn set_cursor_change(&mut self, change_id: Option<usize>) {
532 self.state.cursor_change = change_id;
533 }
534
535 pub fn clear_cursor_change(&mut self) {
537 self.state.cursor_change = None;
538 }
539
540 pub fn set_hunk_scope(&mut self, enabled: bool) {
542 self.state.last_nav_was_hunk = enabled;
543 }
544
545 #[allow(clippy::should_implement_trait)]
547 pub fn next(&mut self) -> bool {
548 if self.state.hunk_preview_mode {
550 return self.dissolve_preview_for_step_down();
551 }
552
553 if self.state.is_at_end() {
554 return false;
555 }
556
557 let prev_hunk = self.state.current_hunk;
558
559 self.state.step_direction = StepDirection::Forward;
560 self.state.animating_hunk = None; let change_idx = self.state.current_step;
564 if change_idx < self.diff.significant_changes.len() {
565 let change_id = self.diff.significant_changes[change_idx];
566 self.state.push_applied(change_id);
567 self.state.active_change = Some(change_id);
568
569 if let Some(hunk) = self.diff.hunk_for_change(change_id) {
571 self.state.current_hunk = hunk.id;
572 }
573 }
574
575 self.state.current_step += 1;
576
577 if self.state.current_hunk != prev_hunk {
579 self.state.last_nav_was_hunk = false;
580 }
581
582 true
583 }
584
585 fn dissolve_preview_for_step_down(&mut self) -> bool {
587 if self.state.preview_from_backward {
588 self.state.hunk_preview_mode = false;
589 self.state.preview_from_backward = false;
590 return self.next();
591 }
592
593 let current_hunk_idx = self.state.current_hunk;
594 let hunk_len = self
595 .diff
596 .hunks
597 .get(current_hunk_idx)
598 .map(|hunk| hunk.change_ids.len())
599 .unwrap_or(0);
600
601 if hunk_len <= 1 {
603 self.state.hunk_preview_mode = false;
604 return self.next();
606 }
607
608 let (first_change, second_change) = {
610 let hunk = &self.diff.hunks[current_hunk_idx];
611 (hunk.change_ids[0], hunk.change_ids[1])
612 };
613
614 self.remove_applied_bulk_for_hunk(current_hunk_idx, Some(first_change));
616
617 self.state.push_applied(second_change);
619
620 self.state.current_step = self.state.applied_changes.len();
622
623 self.state.active_change = Some(second_change);
624 self.state.step_direction = StepDirection::Forward;
625 self.state.hunk_preview_mode = false;
626 self.state.preview_from_backward = false;
627 self.state.animating_hunk = None;
628 self.state.last_nav_was_hunk = true;
630
631 true
632 }
633
634 pub fn prev(&mut self) -> bool {
636 if self.state.hunk_preview_mode {
638 return self.dissolve_preview_for_step_up();
639 }
640
641 if self.state.is_at_start() {
642 return false;
643 }
644
645 let prev_hunk = self.state.current_hunk;
646
647 self.state.step_direction = StepDirection::Backward;
648 self.state.animating_hunk = None; self.state.current_step -= 1;
650
651 if let Some(unapplied_change_id) = self.state.pop_applied() {
653 self.state.active_change = Some(unapplied_change_id);
654
655 if let Some(&last_applied) = self.state.applied_changes.last() {
657 if let Some(hunk) = self.diff.hunk_for_change(last_applied) {
658 self.state.current_hunk = hunk.id;
659 }
660 } else {
661 self.state.current_hunk = 0;
662 }
663 } else {
664 self.state.active_change = None;
665 }
666
667 if self.state.is_at_start() {
669 self.state.current_hunk = 0;
670 }
671
672 if self.state.is_at_start() || self.state.current_hunk != prev_hunk {
674 self.state.last_nav_was_hunk = false;
675 }
676
677 true
678 }
679
680 fn dissolve_preview_for_step_up(&mut self) -> bool {
682 if self.state.preview_from_backward {
683 self.state.hunk_preview_mode = false;
684 self.state.preview_from_backward = false;
685 return self.prev();
686 }
687
688 let current_hunk_idx = self.state.current_hunk;
689
690 self.state.animating_hunk = Some(current_hunk_idx);
692
693 self.remove_applied_bulk_for_hunk(current_hunk_idx, None);
695
696 self.state.current_step = self.state.applied_changes.len();
698
699 if current_hunk_idx > 0 {
701 self.state.current_hunk = current_hunk_idx - 1;
702 }
703
704 self.state.step_direction = StepDirection::Backward;
705 self.state.active_change = self.state.applied_changes.last().copied();
707 self.state.hunk_preview_mode = false;
708 self.state.preview_from_backward = false;
709 self.state.last_nav_was_hunk = false; true
712 }
713
714 pub fn clear_active_change(&mut self) {
717 if self.state.step_direction == StepDirection::Backward {
718 self.state.active_change = self.state.applied_changes.last().copied();
719 } else {
720 self.state.active_change = None;
721 }
722 self.state.animating_hunk = None;
723 self.state.step_direction = StepDirection::None;
724 }
725
726 fn remove_applied_bulk_for_hunk(&mut self, hunk_idx: usize, keep: Option<usize>) -> usize {
727 let (diff, change_to_step_index, state) =
728 (&self.diff, &self.change_to_step_index, &mut self.state);
729 let Some(hunk) = diff.hunks.get(hunk_idx) else {
730 return 0;
731 };
732
733 let mut min_index: Option<usize> = None;
734 let mut keep_index: Option<usize> = None;
735
736 for &change_id in &hunk.change_ids {
737 if Some(change_id) == keep {
738 keep_index = change_to_step_index.get(change_id).copied().flatten();
739 continue;
740 }
741 if !state.is_applied(change_id) {
742 continue;
743 }
744 if let Some(step_idx) = change_to_step_index.get(change_id).copied().flatten() {
745 min_index = Some(min_index.map_or(step_idx, |min| min.min(step_idx)));
746 }
747 }
748
749 let new_len = if let Some(keep_id) = keep {
750 let Some(step_idx) = keep_index else {
751 return 0;
752 };
753 if !state.is_applied(keep_id) {
754 return 0;
755 }
756 step_idx + 1
757 } else {
758 let Some(step_idx) = min_index else {
759 return 0;
760 };
761 step_idx
762 };
763
764 state.truncate_applied_to(new_len)
765 }
766
767 pub fn goto(&mut self, step: usize) {
769 let target_step = step.min(self.state.total_steps - 1);
770
771 self.state.current_step = 0;
773 self.state.clear_applied();
774 self.state.active_change = None;
775 self.state.cursor_change = None;
776 self.state.animating_hunk = None;
777 self.state.current_hunk = 0;
778 self.state.last_nav_was_hunk = false; self.state.hunk_preview_mode = false; self.state.preview_from_backward = false;
781
782 if self.lazy_maps {
783 if target_step > 0 {
784 let end = target_step.min(self.diff.significant_changes.len());
785 self.state.applied_changes = self.diff.significant_changes[..end].to_vec();
786 self.state.rebuild_applied_set();
787 self.state.current_step = end;
788 self.state.active_change = self.state.applied_changes.last().copied();
789 self.state.step_direction = StepDirection::Forward;
790 } else {
791 self.state.step_direction = StepDirection::None;
792 }
793 } else {
794 for _ in 0..target_step {
796 self.next();
797 }
798 }
799
800 self.update_current_hunk();
802 }
803
804 pub fn goto_start(&mut self) {
806 self.goto(0);
807 }
808
809 pub fn goto_end(&mut self) {
811 self.goto(self.state.total_steps - 1);
812 }
813
814 pub fn next_hunk(&mut self) -> bool {
821 if self.diff.hunks.is_empty() {
822 return false;
823 }
824
825 let was_in_preview = self.state.hunk_preview_mode;
827
828 self.state.step_direction = StepDirection::Forward;
829 self.state.hunk_preview_mode = false; self.state.preview_from_backward = false;
831
832 let current_hunk = &self.diff.hunks[self.state.current_hunk];
833 let has_applied_in_current = current_hunk
834 .change_ids
835 .iter()
836 .any(|id| self.state.is_applied(*id));
837
838 if !has_applied_in_current {
840 let mut moved = false;
841 for &change_id in ¤t_hunk.change_ids {
842 if !self.state.is_applied(change_id) {
843 self.state.push_applied(change_id);
844 self.state.current_step += 1;
845 moved = true;
846 }
847 }
848
849 self.state.animating_hunk = Some(self.state.current_hunk);
850 self.state.active_change = current_hunk.change_ids.first().copied();
851
852 if moved {
853 self.state.last_nav_was_hunk = true;
854 self.state.hunk_preview_mode = true;
855 self.state.preview_from_backward = false;
856 }
857
858 return moved;
859 }
860
861 self.state.hunk_preview_mode = false;
863 self.state.preview_from_backward = false;
864 let mut completed_any = false;
865 for &change_id in ¤t_hunk.change_ids {
866 if !self.state.is_applied(change_id) {
867 self.state.push_applied(change_id);
868 self.state.current_step += 1;
869 completed_any = true;
870 }
871 }
872
873 let next_hunk_idx = self.state.current_hunk + 1;
875 if next_hunk_idx >= self.diff.hunks.len() {
876 if completed_any {
878 self.state.animating_hunk = Some(self.state.current_hunk);
879 self.state.active_change = current_hunk.change_ids.last().copied();
880 self.state.last_nav_was_hunk = true;
881 return true;
882 }
883 self.state.hunk_preview_mode = was_in_preview;
885 return false;
886 }
887
888 let hunk = &self.diff.hunks[next_hunk_idx];
889
890 let mut moved = false;
892 for &change_id in &hunk.change_ids {
893 if !self.state.is_applied(change_id) {
894 self.state.push_applied(change_id);
895 self.state.current_step += 1;
896 moved = true;
897 }
898 }
899
900 self.state.animating_hunk = Some(next_hunk_idx);
901 self.state.active_change = hunk.change_ids.first().copied();
902 self.state.current_hunk = next_hunk_idx;
903
904 if moved {
905 self.state.last_nav_was_hunk = true;
906 self.state.hunk_preview_mode = true;
907 self.state.preview_from_backward = false;
908 }
909
910 moved
911 }
912
913 pub fn prev_hunk(&mut self) -> bool {
916 if self.diff.hunks.is_empty() {
917 return false;
918 }
919
920 let was_in_preview = self.state.hunk_preview_mode;
922
923 self.state.hunk_preview_mode = false;
925 self.state.preview_from_backward = false;
926
927 if self.state.current_hunk == 0 {
929 let hunk = &self.diff.hunks[0];
930 let has_applied = hunk.change_ids.iter().any(|id| self.state.is_applied(*id));
931 if !has_applied {
932 self.state.hunk_preview_mode = was_in_preview;
934 return false;
935 }
936 }
937
938 self.state.step_direction = StepDirection::Backward;
939
940 let current_hunk_idx = self.state.current_hunk;
942 let mut moved = false;
943
944 let removed = self.remove_applied_bulk_for_hunk(current_hunk_idx, None);
946 if removed > 0 {
947 self.state.current_step = self.state.applied_changes.len();
948 moved = true;
949 }
950
951 self.state.animating_hunk = Some(current_hunk_idx);
954 self.state.active_change = self
955 .diff
956 .hunks
957 .get(current_hunk_idx)
958 .and_then(|hunk| hunk.change_ids.first().copied());
959
960 if moved {
963 let still_has_applied = self
965 .diff
966 .hunks
967 .get(current_hunk_idx)
968 .map(|hunk| hunk.change_ids.iter().any(|id| self.state.is_applied(*id)))
969 .unwrap_or(false);
970 if !still_has_applied && self.state.current_hunk > 0 {
971 self.state.current_hunk -= 1;
972 }
973 } else if self.state.current_hunk > 0 {
974 self.state.current_hunk -= 1;
976 return self.prev_hunk();
977 }
978
979 if moved {
986 let entered_prev_hunk = self.state.current_hunk != current_hunk_idx;
987 if entered_prev_hunk {
988 self.state.hunk_preview_mode = true;
989 self.state.preview_from_backward = true;
990 self.state.last_nav_was_hunk = true;
991 } else {
992 self.state.last_nav_was_hunk = !self.state.is_at_start();
994 }
995 }
996
997 moved
998 }
999
1000 pub fn goto_hunk(&mut self, hunk_idx: usize) {
1004 if hunk_idx >= self.diff.hunks.len() {
1005 return;
1006 }
1007
1008 self.goto_start();
1010
1011 for idx in 0..hunk_idx {
1013 let hunk = &self.diff.hunks[idx];
1014 for &change_id in &hunk.change_ids {
1015 self.state.push_applied(change_id);
1016 self.state.current_step += 1;
1017 }
1018 }
1019
1020 let hunk = &self.diff.hunks[hunk_idx];
1022 for &change_id in &hunk.change_ids {
1023 self.state.push_applied(change_id);
1024 self.state.current_step += 1;
1025 }
1026
1027 self.state.current_hunk = hunk_idx;
1028 self.state.animating_hunk = Some(hunk_idx);
1029 self.state.active_change = hunk.change_ids.first().copied();
1030 self.state.step_direction = StepDirection::Forward;
1031 self.state.last_nav_was_hunk = true;
1032 self.state.hunk_preview_mode = true;
1033 self.state.preview_from_backward = false;
1034 }
1035
1036 pub fn goto_hunk_start(&mut self) -> bool {
1039 if self.diff.hunks.is_empty() {
1040 return false;
1041 }
1042
1043 let current_hunk_idx = self.state.current_hunk;
1044 let first_change = match self
1045 .diff
1046 .hunks
1047 .get(current_hunk_idx)
1048 .and_then(|hunk| hunk.change_ids.first().copied())
1049 {
1050 Some(id) => id,
1051 None => return false,
1052 };
1053
1054 if !self.state.is_applied(first_change) {
1056 return false;
1057 }
1058
1059 let removed = self.remove_applied_bulk_for_hunk(current_hunk_idx, Some(first_change));
1061 let unapplied_any = removed > 0;
1062 if removed > 0 {
1063 self.state.current_step = self.state.applied_changes.len();
1064 }
1065
1066 if !unapplied_any && self.state.active_change == Some(first_change) {
1068 return false;
1069 }
1070
1071 self.state.active_change = Some(first_change);
1072 self.state.hunk_preview_mode = false;
1073 self.state.preview_from_backward = false;
1074 self.state.last_nav_was_hunk = true;
1075 true
1076 }
1077
1078 pub fn goto_hunk_end(&mut self) -> bool {
1081 if self.diff.hunks.is_empty() {
1082 return false;
1083 }
1084
1085 let hunk = &self.diff.hunks[self.state.current_hunk];
1086 let has_applied = hunk.change_ids.iter().any(|id| self.state.is_applied(*id));
1087 if !has_applied {
1088 return false;
1089 }
1090
1091 let last_change = hunk.change_ids.last().copied();
1092
1093 for &change_id in &hunk.change_ids {
1095 if !self.state.is_applied(change_id) {
1096 self.state.push_applied(change_id);
1097 self.state.current_step += 1;
1098 }
1099 }
1100
1101 if self.state.active_change == last_change {
1103 return false;
1104 }
1105
1106 self.state.active_change = last_change;
1107 self.state.hunk_preview_mode = false;
1108 self.state.preview_from_backward = false;
1109 self.state.last_nav_was_hunk = true;
1110 true
1111 }
1112
1113 pub fn update_current_hunk(&mut self) {
1115 if self.diff.hunks.is_empty() {
1116 return;
1117 }
1118
1119 if let Some(&last_applied) = self.state.applied_changes.last() {
1121 for (idx, hunk) in self.diff.hunks.iter().enumerate() {
1122 if hunk.change_ids.contains(&last_applied) {
1123 self.state.current_hunk = idx;
1124 return;
1125 }
1126 }
1127 }
1128
1129 self.state.current_hunk = 0;
1131 }
1132
1133 pub fn current_hunk(&self) -> Option<&crate::diff::Hunk> {
1135 self.diff.hunks.get(self.state.current_hunk)
1136 }
1137
1138 pub fn hunks(&self) -> &[crate::diff::Hunk] {
1140 &self.diff.hunks
1141 }
1142
1143 pub fn set_show_hunk_extent_while_stepping(&mut self, enabled: bool) {
1144 self.state.show_hunk_extent_while_stepping = enabled;
1145 }
1146
1147 fn is_change_in_animating_hunk(&self, change_id: usize) -> bool {
1151 self.state
1152 .animating_hunk
1153 .and_then(|hunk_idx| {
1154 self.hunk_index_for_change(change_id)
1155 .map(|id| id == hunk_idx)
1156 })
1157 .unwrap_or(false)
1158 }
1159
1160 fn hunk_index_for_change(&self, change_id: usize) -> Option<usize> {
1161 let idx = self.change_to_index.get(change_id).copied().flatten()?;
1162 self.change_to_hunk.get(idx).copied().flatten()
1163 }
1164
1165 fn hunk_index_for_change_exact(&self, change_id: usize) -> Option<usize> {
1166 self.change_id_to_hunk_exact
1167 .get(change_id)
1168 .copied()
1169 .flatten()
1170 }
1171
1172 fn hunk_change_index_range(&self, hunk_idx: usize) -> Option<(usize, usize)> {
1173 self.hunk_change_ranges.get(hunk_idx).copied().flatten()
1174 }
1175
1176 fn hunk_change_index_range_exact(&self, hunk_idx: usize) -> Option<(usize, usize)> {
1177 self.hunk_change_ranges_exact
1178 .get(hunk_idx)
1179 .copied()
1180 .flatten()
1181 }
1182
1183 fn change_index(&self, change_id: usize) -> Option<usize> {
1184 self.change_to_index.get(change_id).copied().flatten()
1185 }
1186
1187 pub fn hunk_index_for_change_id(&self, change_id: usize) -> Option<usize> {
1188 self.hunk_index_for_change(change_id)
1189 }
1190
1191 pub fn hunk_index_for_change_id_exact(&self, change_id: usize) -> Option<usize> {
1192 self.hunk_index_for_change_exact(change_id)
1193 }
1194
1195 pub fn change_index_for(&self, change_id: usize) -> Option<usize> {
1196 self.change_index(change_id)
1197 }
1198
1199 pub fn hunk_step_range(&self, hunk_idx: usize) -> Option<(usize, usize)> {
1200 self.hunk_step_ranges
1201 .get(hunk_idx)
1202 .copied()
1203 .flatten()
1204 .map(|range| (range.start, range.len))
1205 }
1206
1207 pub fn active_change(&self) -> Option<&Change> {
1209 self.state
1210 .active_change
1211 .and_then(|id| self.diff.changes.iter().find(|c| c.id == id))
1212 }
1213
1214 pub fn changes_with_status(&self) -> Vec<(&Change, bool, bool)> {
1216 self.diff
1217 .changes
1218 .iter()
1219 .filter(|c| c.has_changes())
1220 .map(|c| {
1221 let applied = self.state.is_applied(c.id);
1222 let active = self.state.active_change == Some(c.id);
1223 (c, applied, active)
1224 })
1225 .collect()
1226 }
1227
1228 pub fn current_view(&self) -> Vec<ViewLine> {
1231 self.current_view_with_frame(AnimationFrame::Idle)
1232 }
1233
1234 pub fn current_view_with_frame(&self, frame: AnimationFrame) -> Vec<ViewLine> {
1237 self.view_for_changes(self.diff.changes.iter(), frame)
1238 }
1239
1240 pub fn view_line_for_change(
1241 &self,
1242 frame: AnimationFrame,
1243 change_id: usize,
1244 ) -> Option<ViewLine> {
1245 let change = self.diff.changes.iter().find(|c| c.id == change_id)?;
1246 let is_applied = self.state.is_applied(change_id);
1247 let is_in_hunk = self.is_change_in_animating_hunk(change_id);
1248 let is_active_change = self.state.active_change == Some(change_id);
1249 let is_active = is_active_change || is_in_hunk;
1250 let has_changes = change.has_changes();
1251 let scope_hunk = if self.state.last_nav_was_hunk {
1252 self.state
1253 .cursor_change
1254 .and_then(|id| self.hunk_index_for_change_exact(id))
1255 .unwrap_or(self.state.current_hunk)
1256 } else {
1257 self.state.current_hunk
1258 };
1259 let in_scope = if self.state.last_nav_was_hunk {
1260 let scope_range = if has_changes {
1261 self.hunk_change_index_range_exact(scope_hunk)
1262 } else {
1263 self.hunk_change_index_range(scope_hunk)
1264 };
1265 let idx = self.change_to_index.get(change_id).copied().flatten();
1266 match (scope_range, idx) {
1267 (Some((start, end)), Some(idx)) => idx >= start && idx <= end,
1268 _ => {
1269 if has_changes {
1270 self.hunk_index_for_change_exact(change_id) == Some(scope_hunk)
1271 } else {
1272 self.hunk_index_for_change(change_id) == Some(scope_hunk)
1273 }
1274 }
1275 }
1276 } else {
1277 self.hunk_index_for_change(change_id) == Some(scope_hunk)
1278 };
1279 let show_hunk_extent = is_in_hunk
1280 || (in_scope
1281 && (self.state.last_nav_was_hunk || self.state.show_hunk_extent_while_stepping));
1282
1283 let primary_change_id = if self.state.cursor_change.is_some()
1284 && self.state.active_change.is_none()
1285 && self.state.step_direction == StepDirection::None
1286 {
1287 self.state.cursor_change
1288 } else if self.state.step_direction == StepDirection::Backward {
1289 self.state
1290 .applied_changes
1291 .last()
1292 .copied()
1293 .or(self.state.active_change)
1294 } else {
1295 self.state.active_change
1296 };
1297
1298 let is_primary_active =
1299 primary_change_id == Some(change_id) || (primary_change_id.is_none() && is_in_hunk);
1300
1301 if change.spans.len() > 1 {
1302 self.build_word_level_line(
1303 change,
1304 is_applied,
1305 is_active,
1306 is_active_change,
1307 is_primary_active,
1308 show_hunk_extent,
1309 frame,
1310 )
1311 } else {
1312 let span = change.spans.first()?;
1313 self.build_single_span_line(
1314 span,
1315 change_id,
1316 is_applied,
1317 is_active,
1318 is_active_change,
1319 is_primary_active,
1320 show_hunk_extent,
1321 frame,
1322 )
1323 }
1324 }
1325
1326 pub fn current_view_for_hunk(
1327 &self,
1328 frame: AnimationFrame,
1329 hunk_idx: usize,
1330 context_lines: usize,
1331 ) -> Vec<ViewLine> {
1332 if self.diff.changes.is_empty() {
1333 return Vec::new();
1334 }
1335 let Some(hunk) = self.diff.hunks.get(hunk_idx) else {
1336 return self.current_view_with_frame(frame);
1337 };
1338 let mut min_idx = None;
1339 let mut max_idx = None;
1340 for change_id in &hunk.change_ids {
1341 if let Some(idx) = self.change_index(*change_id) {
1342 min_idx = Some(min_idx.map_or(idx, |v: usize| v.min(idx)));
1343 max_idx = Some(max_idx.map_or(idx, |v: usize| v.max(idx)));
1344 }
1345 }
1346 let Some(min_idx) = min_idx else {
1347 return self.current_view_with_frame(frame);
1348 };
1349 let Some(max_idx) = max_idx else {
1350 return self.current_view_with_frame(frame);
1351 };
1352 let start = min_idx.saturating_sub(context_lines);
1353 let end = (max_idx + context_lines).min(self.diff.changes.len().saturating_sub(1));
1354 self.view_for_changes(self.diff.changes[start..=end].iter(), frame)
1355 }
1356
1357 pub fn current_view_for_change_window(
1358 &self,
1359 frame: AnimationFrame,
1360 change_id: usize,
1361 radius: usize,
1362 ) -> Vec<ViewLine> {
1363 let Some(idx) = self.change_index(change_id) else {
1364 return self.current_view_with_frame(frame);
1365 };
1366 if self.diff.changes.is_empty() {
1367 return Vec::new();
1368 }
1369 let start = idx.saturating_sub(radius);
1370 let end = (idx + radius).min(self.diff.changes.len().saturating_sub(1));
1371 self.view_for_changes(self.diff.changes[start..=end].iter(), frame)
1372 }
1373
1374 pub fn current_view_for_change_range(
1375 &self,
1376 frame: AnimationFrame,
1377 start: usize,
1378 end: usize,
1379 ) -> Vec<ViewLine> {
1380 if self.diff.changes.is_empty() {
1381 return Vec::new();
1382 }
1383 let start = start.min(self.diff.changes.len().saturating_sub(1));
1384 let end = end.min(self.diff.changes.len().saturating_sub(1));
1385 if start > end {
1386 return Vec::new();
1387 }
1388 self.view_for_changes(self.diff.changes[start..=end].iter(), frame)
1389 }
1390
1391 fn view_for_changes<'a, I>(&self, changes: I, frame: AnimationFrame) -> Vec<ViewLine>
1392 where
1393 I: IntoIterator<Item = &'a Change>,
1394 {
1395 let mut lines = Vec::new();
1396
1397 let primary_change_id = if self.state.cursor_change.is_some()
1400 && self.state.active_change.is_none()
1401 && self.state.step_direction == StepDirection::None
1402 {
1403 self.state.cursor_change
1404 } else if self.state.step_direction == StepDirection::Backward {
1405 self.state
1406 .applied_changes
1407 .last()
1408 .copied()
1409 .or(self.state.active_change)
1410 } else {
1411 self.state.active_change
1412 };
1413
1414 let mut primary_assigned = false;
1416 let scope_hunk = if self.state.last_nav_was_hunk {
1417 self.state
1418 .cursor_change
1419 .and_then(|id| self.hunk_index_for_change_exact(id))
1420 .unwrap_or(self.state.current_hunk)
1421 } else {
1422 self.state.current_hunk
1423 };
1424 let scope_range_exact = if self.state.last_nav_was_hunk {
1425 self.hunk_change_index_range_exact(scope_hunk)
1426 } else {
1427 None
1428 };
1429 let scope_range_padded = if self.state.last_nav_was_hunk {
1430 self.hunk_change_index_range(scope_hunk)
1431 } else {
1432 None
1433 };
1434
1435 for change in changes {
1436 let is_applied = self.state.is_applied(change.id);
1437 let has_changes = change.has_changes();
1438 let use_exact = self.state.last_nav_was_hunk && has_changes;
1439
1440 let is_primary_active = primary_change_id == Some(change.id);
1442
1443 let is_in_hunk = self.is_change_in_animating_hunk(change.id);
1445 let is_active_change = self.state.active_change == Some(change.id);
1446 let is_active = is_active_change || is_in_hunk;
1448 let scope_range = if use_exact {
1450 scope_range_exact
1451 } else {
1452 scope_range_padded
1453 };
1454 let change_idx =
1455 scope_range.and_then(|_| self.change_to_index.get(change.id).copied().flatten());
1456 let in_scope = if let (Some((start, end)), Some(idx)) = (scope_range, change_idx) {
1457 idx >= start && idx <= end
1458 } else if use_exact {
1459 self.hunk_index_for_change_exact(change.id) == Some(scope_hunk)
1460 } else {
1461 self.hunk_index_for_change(change.id) == Some(scope_hunk)
1462 };
1463 let show_hunk_extent = is_in_hunk
1464 || (in_scope
1465 && (self.state.last_nav_was_hunk
1466 || self.state.show_hunk_extent_while_stepping));
1467
1468 let is_primary_active = is_primary_active
1471 || (!primary_assigned && is_in_hunk && primary_change_id.is_none());
1472
1473 if is_primary_active {
1474 primary_assigned = true;
1475 }
1476
1477 let is_word_level = change.spans.len() > 1;
1479
1480 if is_word_level {
1481 let line = self.build_word_level_line(
1483 change,
1484 is_applied,
1485 is_active,
1486 is_active_change,
1487 is_primary_active,
1488 show_hunk_extent,
1489 frame,
1490 );
1491 if let Some(l) = line {
1492 lines.push(l);
1493 }
1494 } else {
1495 if let Some(span) = change.spans.first() {
1497 if let Some(line) = self.build_single_span_line(
1498 span,
1499 change.id,
1500 is_applied,
1501 is_active,
1502 is_active_change,
1503 is_primary_active,
1504 show_hunk_extent,
1505 frame,
1506 ) {
1507 lines.push(line);
1508 }
1509 }
1510 }
1511 }
1512
1513 lines
1514 }
1515
1516 fn compute_show_new(&self, is_applied: bool, frame: AnimationFrame) -> bool {
1519 if self.state.step_direction == StepDirection::None {
1521 return is_applied;
1522 }
1523 match frame {
1524 AnimationFrame::Idle => is_applied,
1525 AnimationFrame::FadeOut => self.state.step_direction == StepDirection::Backward,
1527 AnimationFrame::FadeIn => self.state.step_direction != StepDirection::Backward,
1529 }
1530 }
1531
1532 #[allow(clippy::too_many_arguments)]
1533 fn build_word_level_line(
1534 &self,
1535 change: &Change,
1536 is_applied: bool,
1537 is_active: bool,
1538 is_active_change: bool,
1539 is_primary_active: bool,
1540 show_hunk_extent: bool,
1541 frame: AnimationFrame,
1542 ) -> Option<ViewLine> {
1543 let first_span = change.spans.first()?;
1544 let old_line = first_span.old_line;
1545 let new_line = first_span.new_line;
1546
1547 let has_old = change
1550 .spans
1551 .iter()
1552 .any(|s| matches!(s.kind, ChangeKind::Delete | ChangeKind::Replace));
1553 let has_new = change
1554 .spans
1555 .iter()
1556 .any(|s| matches!(s.kind, ChangeKind::Insert | ChangeKind::Replace));
1557
1558 let mut view_spans = Vec::new();
1560 let mut content = String::new();
1561
1562 for span in &change.spans {
1563 let (span_kind, text) = if is_active {
1565 let show_new = match frame {
1572 AnimationFrame::Idle => self.compute_show_new(is_applied, frame),
1573 _ => {
1574 if has_old && has_new {
1575 self.compute_show_new(is_applied, frame)
1576 } else if has_new {
1577 true } else {
1579 false }
1581 }
1582 };
1583
1584 match span.kind {
1585 ChangeKind::Equal => (ViewSpanKind::Equal, span.text.clone()),
1586 ChangeKind::Delete => {
1587 if show_new {
1588 continue; } else {
1590 (ViewSpanKind::PendingDelete, span.text.clone())
1591 }
1592 }
1593 ChangeKind::Insert => {
1594 if show_new {
1595 (ViewSpanKind::PendingInsert, span.text.clone())
1596 } else {
1597 continue; }
1599 }
1600 ChangeKind::Replace => {
1601 if show_new {
1602 (
1603 ViewSpanKind::PendingInsert,
1604 span.new_text.clone().unwrap_or_else(|| span.text.clone()),
1605 )
1606 } else {
1607 (ViewSpanKind::PendingDelete, span.text.clone())
1608 }
1609 }
1610 }
1611 } else if is_applied {
1612 let kind = match span.kind {
1614 ChangeKind::Equal => ViewSpanKind::Equal,
1615 ChangeKind::Delete => ViewSpanKind::Deleted,
1616 ChangeKind::Insert => ViewSpanKind::Inserted,
1617 ChangeKind::Replace => ViewSpanKind::Inserted,
1618 };
1619 let text = match span.kind {
1620 ChangeKind::Delete => {
1621 continue; }
1623 ChangeKind::Replace => {
1624 span.new_text.clone().unwrap_or_else(|| span.text.clone())
1625 }
1626 _ => span.text.clone(),
1627 };
1628 (kind, text)
1629 } else {
1630 match span.kind {
1632 ChangeKind::Insert => {
1633 continue; }
1635 _ => (ViewSpanKind::Equal, span.text.clone()),
1636 }
1637 };
1638
1639 content.push_str(&text);
1640 view_spans.push(ViewSpan {
1641 text,
1642 kind: span_kind,
1643 });
1644 }
1645
1646 if view_spans.is_empty() {
1648 return None;
1649 }
1650
1651 let line_kind = if is_active {
1654 LineKind::PendingModify
1655 } else if is_applied {
1656 LineKind::Modified
1657 } else {
1658 LineKind::Context
1659 };
1660
1661 let hunk_index = self.hunk_index_for_change(change.id);
1663 let has_changes = change.has_changes();
1664
1665 Some(ViewLine {
1666 content,
1667 spans: view_spans,
1668 kind: line_kind,
1669 old_line,
1670 new_line,
1671 is_active,
1672 is_active_change,
1673 is_primary_active,
1674 show_hunk_extent,
1675 change_id: change.id,
1676 hunk_index,
1677 has_changes,
1678 })
1679 }
1680
1681 #[allow(clippy::too_many_arguments)]
1682 fn build_single_span_line(
1683 &self,
1684 span: &ChangeSpan,
1685 change_id: usize,
1686 is_applied: bool,
1687 is_active: bool,
1688 is_active_change: bool,
1689 is_primary_active: bool,
1690 show_hunk_extent: bool,
1691 frame: AnimationFrame,
1692 ) -> Option<ViewLine> {
1693 let view_span_kind;
1694 let line_kind;
1695 let content;
1696
1697 match span.kind {
1698 ChangeKind::Equal => {
1699 view_span_kind = ViewSpanKind::Equal;
1700 line_kind = LineKind::Context;
1701 content = span.text.clone();
1702 }
1703 ChangeKind::Delete => {
1704 if is_active {
1706 view_span_kind = ViewSpanKind::PendingDelete;
1707 line_kind = LineKind::PendingDelete;
1708 content = span.text.clone();
1709 } else if is_applied {
1710 view_span_kind = ViewSpanKind::Deleted;
1711 line_kind = LineKind::Deleted;
1712 content = span.text.clone();
1713 } else {
1714 view_span_kind = ViewSpanKind::Equal;
1715 line_kind = LineKind::Context;
1716 content = span.text.clone();
1717 }
1718 }
1719 ChangeKind::Insert => {
1720 if is_active {
1722 view_span_kind = ViewSpanKind::PendingInsert;
1723 line_kind = LineKind::PendingInsert;
1724 content = span.text.clone();
1725 } else if is_applied {
1726 view_span_kind = ViewSpanKind::Inserted;
1727 line_kind = LineKind::Inserted;
1728 content = span.text.clone();
1729 } else {
1730 return None; }
1732 }
1733 ChangeKind::Replace => {
1734 if is_active {
1736 let show_new = self.compute_show_new(is_applied, frame);
1737 if show_new {
1738 view_span_kind = ViewSpanKind::PendingInsert;
1739 content = span.new_text.clone().unwrap_or_else(|| span.text.clone());
1740 } else {
1741 view_span_kind = ViewSpanKind::PendingDelete;
1742 content = span.text.clone();
1743 }
1744 line_kind = LineKind::PendingModify;
1745 } else if is_applied {
1746 view_span_kind = ViewSpanKind::Inserted;
1747 line_kind = LineKind::Modified;
1748 content = span.new_text.clone().unwrap_or_else(|| span.text.clone());
1749 } else {
1750 view_span_kind = ViewSpanKind::Equal;
1751 line_kind = LineKind::Context;
1752 content = span.text.clone();
1753 }
1754 }
1755 }
1756
1757 let hunk_index = self.hunk_index_for_change(change_id);
1759 let has_changes = !matches!(span.kind, ChangeKind::Equal);
1760
1761 Some(ViewLine {
1762 content: content.clone(),
1763 spans: vec![ViewSpan {
1764 text: content,
1765 kind: view_span_kind,
1766 }],
1767 kind: line_kind,
1768 old_line: span.old_line,
1769 new_line: span.new_line,
1770 is_active,
1771 is_active_change,
1772 is_primary_active,
1773 show_hunk_extent,
1774 change_id,
1775 hunk_index,
1776 has_changes,
1777 })
1778 }
1779
1780 pub fn old_content(&self) -> &str {
1782 self.old_content.as_ref()
1783 }
1784
1785 pub fn new_content(&self) -> &str {
1787 self.new_content.as_ref()
1788 }
1789}
1790
1791#[derive(Debug, Clone)]
1793pub struct ViewSpan {
1794 pub text: String,
1795 pub kind: ViewSpanKind,
1796}
1797
1798#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1800pub enum ViewSpanKind {
1801 Equal,
1802 Inserted,
1803 Deleted,
1804 PendingInsert,
1805 PendingDelete,
1806}
1807
1808#[derive(Debug, Clone)]
1810pub struct ViewLine {
1811 pub content: String,
1813 pub spans: Vec<ViewSpan>,
1815 pub kind: LineKind,
1817 pub old_line: Option<usize>,
1818 pub new_line: Option<usize>,
1819 pub is_active: bool,
1821 pub is_active_change: bool,
1823 pub is_primary_active: bool,
1825 pub show_hunk_extent: bool,
1827 pub change_id: usize,
1829 pub hunk_index: Option<usize>,
1831 pub has_changes: bool,
1833}
1834
1835#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1837pub enum LineKind {
1838 Context,
1840 Inserted,
1842 Deleted,
1844 Modified,
1846 PendingDelete,
1848 PendingInsert,
1850 PendingModify,
1852}
1853
1854#[cfg(test)]
1855mod tests {
1856 use super::*;
1857 use crate::change::{Change, ChangeKind, ChangeSpan};
1858 use crate::diff::DiffEngine;
1859 use crate::diff::{DiffResult, Hunk};
1860 use std::sync::Arc;
1861
1862 fn build_manual_diff(
1863 changes: Vec<Change>,
1864 significant_changes: Vec<usize>,
1865 hunks: Vec<Hunk>,
1866 ) -> DiffResult {
1867 let mut insertions = 0usize;
1868 let mut deletions = 0usize;
1869 for change in &changes {
1870 for span in &change.spans {
1871 match span.kind {
1872 ChangeKind::Insert => insertions += 1,
1873 ChangeKind::Delete => deletions += 1,
1874 ChangeKind::Replace => {
1875 insertions += 1;
1876 deletions += 1;
1877 }
1878 ChangeKind::Equal => {}
1879 }
1880 }
1881 }
1882 DiffResult {
1883 changes,
1884 significant_changes,
1885 hunks,
1886 insertions,
1887 deletions,
1888 }
1889 }
1890
1891 fn make_equal_change(id: usize) -> Change {
1892 let line = id + 1;
1893 Change::single(
1894 id,
1895 ChangeSpan::equal(format!("line{}", id)).with_lines(Some(line), Some(line)),
1896 )
1897 }
1898
1899 fn make_insert_change(id: usize) -> Change {
1900 let line = id + 1;
1901 Change::single(
1902 id,
1903 ChangeSpan::insert(format!("ins{}", id)).with_lines(None, Some(line)),
1904 )
1905 }
1906
1907 fn assert_applied_is_prefix(nav: &DiffNavigator) {
1908 let applied = &nav.state().applied_changes;
1909 let sig = &nav.diff.significant_changes;
1910 assert!(
1911 applied.len() <= sig.len(),
1912 "applied changes should not exceed significant changes"
1913 );
1914 assert_eq!(
1915 &sig[..applied.len()],
1916 applied.as_slice(),
1917 "applied changes should remain a prefix of significant_changes"
1918 );
1919 }
1920
1921 #[test]
1922 fn test_navigation() {
1923 let old = "foo\nbar\nbaz";
1924 let new = "foo\nqux\nbaz";
1925
1926 let engine = DiffEngine::new();
1927 let diff = engine.diff_strings(old, new);
1928 let mut nav = DiffNavigator::new(diff, Arc::from(old), Arc::from(new), false);
1929
1930 assert!(nav.state().is_at_start());
1931 assert!(!nav.state().is_at_end());
1932
1933 nav.next();
1934 assert!(!nav.state().is_at_start());
1935
1936 nav.goto_end();
1937 assert!(nav.state().is_at_end());
1938
1939 nav.prev();
1940 assert!(!nav.state().is_at_end());
1941
1942 nav.goto_start();
1943 assert!(nav.state().is_at_start());
1944 }
1945
1946 #[test]
1947 fn test_progress() {
1948 let old = "a\nb\nc\nd";
1949 let new = "a\nB\nC\nd";
1950
1951 let engine = DiffEngine::new();
1952 let diff = engine.diff_strings(old, new);
1953 let mut nav = DiffNavigator::new(diff, Arc::from(old), Arc::from(new), false);
1954
1955 assert_eq!(nav.state().progress(), 0.0);
1956
1957 nav.goto_end();
1958 assert_eq!(nav.state().progress(), 100.0);
1959 }
1960
1961 #[test]
1962 fn test_word_level_view() {
1963 let old = "const foo = 4";
1964 let new = "const bar = 5";
1965
1966 let engine = DiffEngine::new().with_word_level(true);
1967 let diff = engine.diff_strings(old, new);
1968 let mut nav = DiffNavigator::new(diff, Arc::from(old), Arc::from(new), false);
1969
1970 let view = nav.current_view();
1972 assert_eq!(view.len(), 1);
1973 assert_eq!(view[0].content, "const foo = 4");
1974
1975 nav.next();
1977 let view = nav.current_view();
1978 assert_eq!(view.len(), 1);
1979 assert_eq!(view[0].content, "const bar = 5");
1980 }
1981
1982 #[test]
1983 fn test_prev_hunk_animation_state() {
1984 let old = "a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nk\nl";
1987 let new = "a\nB\nc\nd\ne\nf\ng\nh\ni\nj\nK\nl";
1988 let engine = DiffEngine::new();
1991 let diff = engine.diff_strings(old, new);
1992
1993 assert!(
1995 diff.hunks.len() >= 2,
1996 "Expected at least 2 hunks, got {}. Adjust fixture gap.",
1997 diff.hunks.len()
1998 );
1999
2000 let mut nav = DiffNavigator::new(diff, Arc::from(old), Arc::from(new), false);
2001
2002 nav.next_hunk();
2004 nav.next_hunk();
2005 assert_eq!(nav.state().current_hunk, 1);
2006
2007 nav.prev_hunk();
2009
2010 assert_eq!(
2012 nav.state().animating_hunk,
2013 Some(1),
2014 "animating_hunk should stay on the hunk being removed for fade animation"
2015 );
2016 assert_eq!(
2018 nav.state().current_hunk,
2019 0,
2020 "current_hunk should move to destination for status display"
2021 );
2022
2023 let view = nav.current_view();
2025 let active_lines: Vec<_> = view.iter().filter(|l| l.is_active).collect();
2026 assert!(
2027 !active_lines.is_empty(),
2028 "Hunk changes should be marked active during fade"
2029 );
2030
2031 nav.clear_active_change();
2033 assert_eq!(
2034 nav.state().animating_hunk,
2035 None,
2036 "animating_hunk should be cleared after animation completes"
2037 );
2038 }
2039
2040 #[test]
2041 fn test_prev_to_start_preserves_animation_state() {
2042 let old = "a\nb\nc";
2045 let new = "a\nB\nc";
2046
2047 let engine = DiffEngine::new();
2048 let diff = engine.diff_strings(old, new);
2049 let mut nav = DiffNavigator::new(diff, Arc::from(old), Arc::from(new), false);
2050
2051 nav.next();
2053 assert_eq!(nav.state().current_step, 1);
2054
2055 nav.prev();
2057 assert!(nav.state().is_at_start());
2058
2059 assert!(
2061 nav.state().active_change.is_some(),
2062 "active_change preserved for fade-out"
2063 );
2064 assert_eq!(
2065 nav.state().step_direction,
2066 StepDirection::Backward,
2067 "step_direction should be Backward"
2068 );
2069
2070 nav.clear_active_change();
2072 assert_eq!(nav.state().active_change, None);
2073 assert_eq!(nav.state().animating_hunk, None);
2074 assert_eq!(nav.state().step_direction, StepDirection::None);
2075 }
2076
2077 #[test]
2078 fn test_prev_hunk_from_hunk_0_unapplies_changes() {
2079 let old = "a\nb\nc";
2081 let new = "a\nB\nc";
2082
2083 let engine = DiffEngine::new();
2084 let diff = engine.diff_strings(old, new);
2085 let mut nav = DiffNavigator::new(diff, Arc::from(old), Arc::from(new), false);
2086
2087 nav.next_hunk();
2089 assert_eq!(nav.state().current_step, 1);
2090 assert_eq!(nav.state().current_hunk, 0);
2091
2092 let moved = nav.prev_hunk();
2094 assert!(
2095 moved,
2096 "prev_hunk should succeed when hunk 0 has applied changes"
2097 );
2098 assert!(nav.state().is_at_start());
2099 assert_eq!(nav.state().current_step, 0);
2100
2101 assert_eq!(
2103 nav.state().animating_hunk,
2104 Some(0),
2105 "animating_hunk should point to hunk 0 for fade animation"
2106 );
2107 assert_eq!(nav.state().step_direction, StepDirection::Backward);
2108
2109 let moved_again = nav.prev_hunk();
2111 assert!(
2112 !moved_again,
2113 "prev_hunk should fail when hunk 0 has no applied changes"
2114 );
2115 }
2116
2117 #[test]
2118 fn test_backward_primary_marker_on_destination() {
2119 let old = "line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8";
2122 let new = "line1\nLINE2\nline3\nline4\nline5\nline6\nLINE7\nline8";
2123
2124 let engine = DiffEngine::new();
2125 let diff = engine.diff_strings(old, new);
2126 assert!(diff.hunks.len() >= 2, "Fixture must produce 2 hunks");
2127
2128 let mut nav = DiffNavigator::new(diff, Arc::from(old), Arc::from(new), false);
2129
2130 nav.next_hunk(); nav.next_hunk(); assert_eq!(nav.state().current_step, 2);
2134
2135 nav.prev_hunk();
2137
2138 let view = nav.current_view_with_frame(AnimationFrame::FadeOut);
2139
2140 let primary_lines: Vec<_> = view.iter().filter(|l| l.is_primary_active).collect();
2142 let active_lines: Vec<_> = view.iter().filter(|l| l.is_active).collect();
2143
2144 assert_eq!(primary_lines.len(), 1, "exactly one primary line");
2146
2147 assert!(!active_lines.is_empty(), "fading line should be active");
2149
2150 let primary = primary_lines[0];
2152 assert!(
2153 primary.content.contains("LINE2"),
2154 "primary marker should be on destination (hunk 0)"
2155 );
2156 assert!(
2157 !primary.content.contains("LINE7"),
2158 "primary marker should not be on fading line (hunk 1)"
2159 );
2160 }
2161
2162 #[test]
2163 fn test_forward_primary_marker_on_first_change() {
2164 let old = "line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8";
2167 let new = "line1\nLINE2\nLINE3\nline4\nline5\nline6\nline7\nline8";
2168 let engine = DiffEngine::new();
2171 let diff = engine.diff_strings(old, new);
2172 assert!(
2173 !diff.hunks.is_empty(),
2174 "Fixture must produce at least 1 hunk"
2175 );
2176 assert!(
2177 diff.hunks[0].change_ids.len() >= 2,
2178 "Hunk 0 must have at least 2 changes for this test"
2179 );
2180
2181 let mut nav = DiffNavigator::new(diff, Arc::from(old), Arc::from(new), false);
2182
2183 nav.next_hunk();
2185
2186 let view = nav.current_view_with_frame(AnimationFrame::FadeIn);
2188
2189 let primary_lines: Vec<_> = view.iter().filter(|l| l.is_primary_active).collect();
2190 assert_eq!(primary_lines.len(), 1, "exactly one primary line");
2191
2192 let primary = primary_lines[0];
2194 assert!(
2195 primary.content.contains("LINE2"),
2196 "primary marker should be on first change (LINE2), got: {}",
2197 primary.content
2198 );
2199 }
2200
2201 #[test]
2202 fn test_step_down_after_next_hunk() {
2203 let old = "line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8";
2205 let new = "line1\nLINE2\nLINE3\nline4\nline5\nline6\nline7\nline8";
2206
2207 let engine = DiffEngine::new();
2208 let diff = engine.diff_strings(old, new);
2209 assert!(
2210 diff.hunks[0].change_ids.len() >= 2,
2211 "Hunk 0 must have at least 2 changes"
2212 );
2213
2214 let mut nav = DiffNavigator::new(diff, Arc::from(old), Arc::from(new), false);
2215
2216 nav.next_hunk();
2218 assert_eq!(
2219 nav.state().current_step,
2220 2,
2221 "Should apply all 2 changes after next_hunk"
2222 );
2223 assert!(nav.state().hunk_preview_mode, "Should be in preview mode");
2224
2225 let moved = nav.next();
2227 assert!(moved, "next() should succeed");
2228 assert_eq!(
2229 nav.state().current_step,
2230 2,
2231 "Should still be at step 2 after dissolve"
2232 );
2233 assert!(!nav.state().hunk_preview_mode, "Should exit preview mode");
2234
2235 let view = nav.current_view_with_frame(AnimationFrame::FadeIn);
2237 let primary = view.iter().find(|l| l.is_primary_active);
2238 assert!(primary.is_some(), "Should have primary line");
2239 assert!(
2240 primary.unwrap().content.contains("LINE3"),
2241 "Primary should be on LINE3 after stepping"
2242 );
2243 }
2244
2245 #[test]
2246 fn test_next_hunk_completes_current_then_lands_on_next() {
2247 let old = "line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8";
2250 let new = "line1\nLINE2\nLINE3\nline4\nline5\nline6\nLINE7\nLINE8";
2251 let engine = DiffEngine::new();
2254 let diff = engine.diff_strings(old, new);
2255 assert!(diff.hunks.len() >= 2, "Must have 2 hunks");
2256
2257 let mut nav = DiffNavigator::new(diff, Arc::from(old), Arc::from(new), false);
2258
2259 nav.next_hunk();
2261 assert_eq!(nav.state().current_hunk, 0);
2262 assert_eq!(
2263 nav.state().current_step,
2264 2,
2265 "Should apply all 2 changes in hunk 0"
2266 );
2267 assert!(nav.state().hunk_preview_mode);
2268
2269 nav.next_hunk();
2271 assert_eq!(nav.state().current_hunk, 1, "Should be in hunk 1");
2272
2273 assert_eq!(nav.state().current_step, 4, "Should have applied 4 changes");
2275 assert!(nav.state().hunk_preview_mode);
2276
2277 let view = nav.current_view_with_frame(AnimationFrame::FadeIn);
2279 let primary = view.iter().find(|l| l.is_primary_active);
2280 assert!(primary.is_some());
2281 assert!(
2282 primary.unwrap().content.contains("LINE7"),
2283 "Cursor should be on LINE7"
2284 );
2285 }
2286
2287 #[test]
2288 fn test_next_hunk_on_last_hunk_stays_at_end() {
2289 let old = "line1\nline2\nline3";
2292 let new = "line1\nLINE2\nLINE3";
2293
2294 let engine = DiffEngine::new();
2295 let diff = engine.diff_strings(old, new);
2296 assert_eq!(diff.hunks.len(), 1, "Must have exactly 1 hunk");
2297 assert_eq!(
2298 diff.hunks[0].change_ids.len(),
2299 2,
2300 "Hunk must have 2 changes"
2301 );
2302
2303 let mut nav = DiffNavigator::new(diff, Arc::from(old), Arc::from(new), false);
2304
2305 let moved1 = nav.next_hunk();
2307 assert!(moved1);
2308 assert_eq!(nav.state().current_step, 2, "Should apply all 2 changes");
2309 assert!(nav.state().hunk_preview_mode);
2310
2311 let moved2 = nav.next_hunk();
2313 assert!(!moved2, "Should return false when no next hunk");
2314 assert_eq!(nav.state().current_step, 2, "Should still be at step 2");
2315 }
2316
2317 #[test]
2318 fn test_hunk_change_range_cached_for_large_hunk() {
2319 let old = "a\nb\nc\nd\ne";
2320 let new = "A\nB\nC\nD\nE";
2321
2322 let engine = DiffEngine::new();
2323 let diff = engine.diff_strings(old, new);
2324 assert_eq!(diff.hunks.len(), 1, "single hunk for full replace");
2325
2326 let nav = DiffNavigator::new(diff, Arc::from(old), Arc::from(new), true);
2327 let range = nav.hunk_change_index_range(0);
2328 assert!(range.is_some(), "expected cached range for hunk 0");
2329 let (start, end) = range.unwrap();
2330 assert!(start <= end, "range should be valid");
2331 }
2332
2333 #[test]
2334 fn test_no_step_scope_prefers_exact_mapping_for_changes() {
2335 let changes = (0..8)
2336 .map(|id| {
2337 if id == 2 || id == 5 {
2338 make_insert_change(id)
2339 } else {
2340 make_equal_change(id)
2341 }
2342 })
2343 .collect::<Vec<_>>();
2344 let hunks = vec![
2345 Hunk {
2346 id: 0,
2347 change_ids: vec![2],
2348 old_start: None,
2349 new_start: Some(3),
2350 insertions: 1,
2351 deletions: 0,
2352 },
2353 Hunk {
2354 id: 1,
2355 change_ids: vec![5],
2356 old_start: None,
2357 new_start: Some(6),
2358 insertions: 1,
2359 deletions: 0,
2360 },
2361 ];
2362 let diff = build_manual_diff(changes, vec![2, 5], hunks);
2363 let mut nav = DiffNavigator::new(diff, Arc::from(""), Arc::from(""), true);
2364 nav.goto_end();
2365
2366 assert_eq!(
2367 nav.hunk_index_for_change_id(5),
2368 Some(0),
2369 "fixture should overlap padded range"
2370 );
2371 assert_eq!(
2372 nav.hunk_index_for_change_id_exact(5),
2373 Some(1),
2374 "exact mapping should point to hunk 1"
2375 );
2376
2377 nav.set_cursor_hunk(1, Some(5));
2378 nav.set_hunk_scope(true);
2379
2380 let view = nav.current_view_with_frame(AnimationFrame::Idle);
2381 let line_hunk_1 = view.iter().find(|l| l.change_id == 5).unwrap();
2382 let line_hunk_0 = view.iter().find(|l| l.change_id == 2).unwrap();
2383
2384 assert!(
2385 line_hunk_1.show_hunk_extent,
2386 "change line in scope hunk should show extent"
2387 );
2388 assert!(
2389 !line_hunk_0.show_hunk_extent,
2390 "change line outside scope hunk should not show extent"
2391 );
2392 }
2393
2394 #[test]
2395 fn test_no_step_scope_includes_context_lines() {
2396 let changes = (0..10)
2397 .map(|id| {
2398 if id == 4 {
2399 make_insert_change(id)
2400 } else {
2401 make_equal_change(id)
2402 }
2403 })
2404 .collect::<Vec<_>>();
2405 let hunks = vec![Hunk {
2406 id: 0,
2407 change_ids: vec![4],
2408 old_start: None,
2409 new_start: Some(5),
2410 insertions: 1,
2411 deletions: 0,
2412 }];
2413 let diff = build_manual_diff(changes, vec![4], hunks);
2414 let mut nav = DiffNavigator::new(diff, Arc::from(""), Arc::from(""), true);
2415 nav.goto_end();
2416
2417 nav.set_cursor_hunk(0, Some(4));
2418 nav.set_hunk_scope(true);
2419
2420 let view = nav.current_view_with_frame(AnimationFrame::Idle);
2421 let scope_range = nav
2422 .hunk_change_index_range(0)
2423 .expect("expected cached padded range");
2424
2425 for line in view.iter().filter(|l| !l.has_changes) {
2426 let idx = nav.change_index_for(line.change_id).unwrap();
2427 let in_range = idx >= scope_range.0 && idx <= scope_range.1;
2428 assert_eq!(
2429 line.show_hunk_extent, in_range,
2430 "context line {} scope mismatch",
2431 line.change_id
2432 );
2433 }
2434 }
2435
2436 #[test]
2437 fn test_markers_persist_within_hunk() {
2438 let old = "line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8";
2440 let new = "line1\nLINE2\nLINE3\nline4\nline5\nline6\nline7\nline8";
2441
2442 let engine = DiffEngine::new();
2443 let diff = engine.diff_strings(old, new);
2444
2445 let mut nav = DiffNavigator::new(diff, Arc::from(old), Arc::from(new), false);
2446
2447 nav.next_hunk();
2448 assert!(
2449 nav.state().last_nav_was_hunk,
2450 "last_nav_was_hunk should be true after next_hunk"
2451 );
2452
2453 nav.next();
2455 assert!(
2456 nav.state().last_nav_was_hunk,
2457 "last_nav_was_hunk should persist within hunk"
2458 );
2459 }
2460
2461 #[test]
2462 fn test_markers_clear_on_hunk_exit() {
2463 let old = "line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8";
2465 let new = "line1\nLINE2\nline3\nline4\nline5\nline6\nLINE7\nline8";
2466 let engine = DiffEngine::new();
2469 let diff = engine.diff_strings(old, new);
2470 assert!(diff.hunks.len() >= 2, "Must have 2 hunks");
2471
2472 let mut nav = DiffNavigator::new(diff, Arc::from(old), Arc::from(new), false);
2473
2474 nav.next_hunk();
2476 assert!(nav.state().last_nav_was_hunk);
2477
2478 nav.next();
2480 assert!(
2481 !nav.state().last_nav_was_hunk,
2482 "Markers should clear when stepping into different hunk"
2483 );
2484 }
2485
2486 #[test]
2487 fn test_active_change_flag_in_hunk_preview() {
2488 let old = "line1\nline2\nline3\n";
2491 let new = "LINE1\nLINE2\nline3\n";
2492
2493 let engine = DiffEngine::new();
2494 let diff = engine.diff_strings(old, new);
2495 assert_eq!(diff.hunks.len(), 1, "Expected a single hunk");
2496 assert!(
2497 diff.hunks[0].change_ids.len() >= 2,
2498 "Expected 2 changes in the hunk"
2499 );
2500
2501 let mut nav = DiffNavigator::new(diff, Arc::from(old), Arc::from(new), false);
2502 nav.next_hunk();
2503
2504 let view = nav.current_view_with_frame(AnimationFrame::FadeIn);
2505 let active_changes: Vec<_> = view.iter().filter(|l| l.is_active_change).collect();
2506 let active_lines: Vec<_> = view.iter().filter(|l| l.is_active).collect();
2507
2508 assert_eq!(active_changes.len(), 1, "Only one active change expected");
2509 assert!(
2510 active_lines.len() >= 2,
2511 "All hunk lines should be active during preview"
2512 );
2513 assert!(
2514 active_changes[0].is_primary_active,
2515 "Active change should be primary"
2516 );
2517 assert!(
2518 active_changes[0].content.contains("LINE1"),
2519 "Active change should be the first change in the hunk"
2520 );
2521 }
2522
2523 #[test]
2524 fn test_word_level_phase_aware_mixed_change() {
2525 let old = "const foo = 4";
2528 let new = "const bar = 5";
2529
2530 let engine = DiffEngine::new().with_word_level(true);
2531 let diff = engine.diff_strings(old, new);
2532 let mut nav = DiffNavigator::new(diff, Arc::from(old), Arc::from(new), false);
2533
2534 nav.next();
2536 assert!(nav.state().active_change.is_some());
2537
2538 let view = nav.current_view_with_frame(AnimationFrame::FadeOut);
2540 assert_eq!(view.len(), 1);
2541 assert_eq!(
2542 view[0].content, "const foo = 4",
2543 "FadeOut should show old content for mixed word-level change"
2544 );
2545
2546 let view = nav.current_view_with_frame(AnimationFrame::FadeIn);
2548 assert_eq!(view.len(), 1);
2549 assert_eq!(
2550 view[0].content, "const bar = 5",
2551 "FadeIn should show new content for mixed word-level change"
2552 );
2553
2554 let view = nav.current_view_with_frame(AnimationFrame::Idle);
2556 assert_eq!(view.len(), 1);
2557 assert_eq!(
2558 view[0].content, "const bar = 5",
2559 "Idle should show applied (new) content"
2560 );
2561 }
2562
2563 #[test]
2564 fn test_word_level_insert_only_visible_both_phases() {
2565 let old = "hello";
2567 let new = "hello world";
2568
2569 let engine = DiffEngine::new().with_word_level(true);
2570 let diff = engine.diff_strings(old, new);
2571 let mut nav = DiffNavigator::new(diff, Arc::from(old), Arc::from(new), false);
2572
2573 nav.next();
2575
2576 let view = nav.current_view_with_frame(AnimationFrame::FadeOut);
2578 assert_eq!(view.len(), 1);
2579 assert!(
2580 view[0].content.contains("world"),
2581 "Insert-only change should be visible during FadeOut, got: {}",
2582 view[0].content
2583 );
2584
2585 let view = nav.current_view_with_frame(AnimationFrame::FadeIn);
2587 assert_eq!(view.len(), 1);
2588 assert!(
2589 view[0].content.contains("world"),
2590 "Insert-only change should be visible during FadeIn, got: {}",
2591 view[0].content
2592 );
2593 }
2594
2595 #[test]
2596 fn test_word_level_delete_only_visible_both_phases() {
2597 let old = "hello world";
2599 let new = "hello";
2600
2601 let engine = DiffEngine::new().with_word_level(true);
2602 let diff = engine.diff_strings(old, new);
2603 let mut nav = DiffNavigator::new(diff, Arc::from(old), Arc::from(new), false);
2604
2605 nav.next();
2607
2608 let view = nav.current_view_with_frame(AnimationFrame::FadeOut);
2610 assert_eq!(view.len(), 1);
2611 assert!(
2612 view[0].content.contains("world"),
2613 "Delete-only change should show deleted content during FadeOut, got: {}",
2614 view[0].content
2615 );
2616
2617 let view = nav.current_view_with_frame(AnimationFrame::FadeIn);
2619 assert_eq!(view.len(), 1);
2620 assert!(
2621 view[0].content.contains("world"),
2622 "Delete-only change should show deleted content during FadeIn, got: {}",
2623 view[0].content
2624 );
2625 }
2626
2627 #[test]
2628 fn test_word_level_insert_only_idle_respects_applied_state() {
2629 let old = "hello";
2632 let new = "hello world";
2633
2634 let engine = DiffEngine::new().with_word_level(true);
2635 let diff = engine.diff_strings(old, new);
2636 let mut nav = DiffNavigator::new(diff, Arc::from(old), Arc::from(new), false);
2637
2638 nav.next();
2640 nav.prev();
2641
2642 let view = nav.current_view_with_frame(AnimationFrame::Idle);
2644 assert_eq!(view.len(), 1);
2645 assert!(
2646 !view[0].content.contains("world"),
2647 "Idle should not show unapplied insert, got: {}",
2648 view[0].content
2649 );
2650 }
2651
2652 #[test]
2653 fn test_word_level_phase_aware_backward_with_multiple_changes() {
2654 let old = "aaa\nconst foo = 4\nccc\nddd\neee\nfff\nggg\nconst bar = 8\niii\njjj";
2657 let new = "aaa\nconst bbb = 5\nccc\nddd\neee\nfff\nggg\nconst qux = 9\niii\njjj";
2658 let engine = DiffEngine::new().with_word_level(true);
2661 let diff = engine.diff_strings(old, new);
2662
2663 assert!(
2665 diff.significant_changes.len() >= 2,
2666 "Expected at least 2 changes, got {}",
2667 diff.significant_changes.len()
2668 );
2669
2670 let mut nav = DiffNavigator::new(diff, Arc::from(old), Arc::from(new), false);
2671
2672 nav.next(); nav.next(); assert_eq!(nav.state().current_step, 2);
2676
2677 nav.prev();
2679 assert_eq!(nav.state().current_step, 1);
2680 assert_eq!(nav.state().step_direction, StepDirection::Backward);
2681 assert!(nav.state().active_change.is_some());
2682
2683 let view = nav.current_view_with_frame(AnimationFrame::FadeOut);
2685 let active_line = view.iter().find(|l| l.is_active);
2686 assert!(active_line.is_some(), "Should have an active line");
2687
2688 let active = active_line.unwrap();
2691 assert_eq!(
2692 active.content, "const qux = 9",
2693 "Backward FadeOut should show new content (being removed)"
2694 );
2695
2696 let view = nav.current_view_with_frame(AnimationFrame::FadeIn);
2699 let active_line = view.iter().find(|l| l.is_active);
2700 assert!(active_line.is_some(), "Should have an active line");
2701 let active = active_line.unwrap();
2702 assert_eq!(
2703 active.content, "const bar = 8",
2704 "Backward FadeIn should show old content (being restored)"
2705 );
2706 }
2707
2708 #[test]
2709 fn test_word_level_insert_only_backward_visible_both_phases() {
2710 let old = "aaa\nhello\nccc\nddd\neee\nfff\nggg\nfoo\niii\njjj";
2714 let new = "aaa\nhello world\nccc\nddd\neee\nfff\nggg\nfoo bar\niii\njjj";
2715 let engine = DiffEngine::new().with_word_level(true);
2718 let diff = engine.diff_strings(old, new);
2719
2720 assert!(
2721 diff.significant_changes.len() >= 2,
2722 "Need 2+ changes to avoid landing on step 0, got {}",
2723 diff.significant_changes.len()
2724 );
2725
2726 let mut nav = DiffNavigator::new(diff, Arc::from(old), Arc::from(new), false);
2727
2728 nav.next();
2730 nav.next();
2731 nav.prev();
2732
2733 assert_eq!(nav.state().step_direction, StepDirection::Backward);
2734 assert!(nav.state().active_change.is_some());
2735
2736 let view = nav.current_view_with_frame(AnimationFrame::FadeOut);
2738 let active_line = view.iter().find(|l| l.is_active);
2739 assert!(
2740 active_line.is_some(),
2741 "Should have an active line during FadeOut"
2742 );
2743 let active = active_line.unwrap();
2744 assert!(
2745 active.content.contains("bar"),
2746 "Backward FadeOut should show insert-only content, got: {}",
2747 active.content
2748 );
2749
2750 let view = nav.current_view_with_frame(AnimationFrame::FadeIn);
2752 let active_line = view.iter().find(|l| l.is_active);
2753 assert!(
2754 active_line.is_some(),
2755 "Should have an active line during FadeIn"
2756 );
2757 let active = active_line.unwrap();
2758 assert!(
2759 active.content.contains("bar"),
2760 "Backward FadeIn should show insert-only content, got: {}",
2761 active.content
2762 );
2763 }
2764
2765 #[test]
2766 fn test_word_level_active_line_kind_pending_modify() {
2767 let old = "const foo = 4";
2770 let new = "const bar = 5";
2771
2772 let engine = DiffEngine::new().with_word_level(true);
2773 let diff = engine.diff_strings(old, new);
2774 let mut nav = DiffNavigator::new(diff, Arc::from(old), Arc::from(new), false);
2775
2776 nav.next(); let view = nav.current_view_with_frame(AnimationFrame::FadeOut);
2779 assert_eq!(view.len(), 1);
2780 assert_eq!(
2781 view[0].kind,
2782 LineKind::PendingModify,
2783 "Active word-level line should have PendingModify kind during FadeOut"
2784 );
2785
2786 let view = nav.current_view_with_frame(AnimationFrame::FadeIn);
2787 assert_eq!(view.len(), 1);
2788 assert_eq!(
2789 view[0].kind,
2790 LineKind::PendingModify,
2791 "Active word-level line should have PendingModify kind during FadeIn"
2792 );
2793 }
2794
2795 #[test]
2796 fn test_primary_active_unique_when_active_change_set() {
2797 let old = "a\nb\nc\n";
2800 let new = "a\nB\nc\n";
2801 let diff = DiffEngine::new().diff_strings(old, new);
2802 let mut nav = DiffNavigator::new(diff, Arc::from(old), Arc::from(new), false);
2803
2804 nav.next();
2805 let view = nav.current_view_with_frame(AnimationFrame::FadeIn);
2806
2807 let primary: Vec<_> = view.iter().filter(|l| l.is_primary_active).collect();
2808 assert_eq!(
2809 primary.len(),
2810 1,
2811 "Exactly one line should be primary active"
2812 );
2813 assert!(
2814 primary[0].is_active,
2815 "Primary active line must also be is_active"
2816 );
2817 }
2818
2819 #[test]
2820 fn test_hunk_extent_not_primary() {
2821 let old = "a\nb\nc\nd\n";
2824 let new = "A\nb\nC\nd\n"; let diff = DiffEngine::new().diff_strings(old, new);
2826
2827 assert_eq!(
2828 diff.hunks.len(),
2829 1,
2830 "Fixture should produce a single hunk; adjust the unchanged gap if this fails"
2831 );
2832
2833 let mut nav = DiffNavigator::new(diff, Arc::from(old), Arc::from(new), false);
2834
2835 nav.next_hunk();
2836 let view = nav.current_view_with_frame(AnimationFrame::FadeOut);
2837
2838 let active = view.iter().filter(|l| l.is_active).count();
2840 let extent = view.iter().filter(|l| l.show_hunk_extent).count();
2841 let primary = view.iter().filter(|l| l.is_primary_active).count();
2842
2843 assert!(
2844 active > 1,
2845 "Multiple lines should be active during animation, got {}",
2846 active
2847 );
2848 assert!(
2849 extent > 1,
2850 "Multiple lines should show extent markers, got {}",
2851 extent
2852 );
2853 assert_eq!(primary, 1, "Exactly one line should be primary active");
2854 }
2855
2856 #[test]
2857 fn test_hunk_extent_while_stepping() {
2858 let old = "one\ntwo\nthree\nfour\n";
2861 let new = "ONE\nTWO\nthree\nfour\n";
2862 let diff = DiffEngine::new().diff_strings(old, new);
2863 let mut nav = DiffNavigator::new(diff, Arc::from(old), Arc::from(new), false);
2864
2865 nav.next();
2866 nav.set_show_hunk_extent_while_stepping(true);
2867 let view = nav.current_view_with_frame(AnimationFrame::Idle);
2868
2869 let extent = view.iter().filter(|l| l.show_hunk_extent).count();
2870 assert!(
2871 extent > 0,
2872 "Extent markers should show while stepping when enabled"
2873 );
2874 }
2875
2876 #[test]
2877 fn test_applied_changes_stay_prefix_during_hunk_ops() {
2878 let old = "l1\nl2\nl3\nl4\nl5\nl6\nl7\nl8\nl9\nl10\nl11\nl12\n";
2879 let new = "l1\nL2\nL3\nl4\nl5\nl6\nl7\nl8\nL9\nl10\nl11\nl12\n";
2880 let diff = DiffEngine::new().diff_strings(old, new);
2881 let mut nav = DiffNavigator::new(diff, Arc::from(old), Arc::from(new), false);
2882
2883 assert_applied_is_prefix(&nav);
2884
2885 nav.next_hunk();
2886 assert_applied_is_prefix(&nav);
2887
2888 nav.next();
2889 assert_applied_is_prefix(&nav);
2890
2891 nav.next_hunk();
2892 assert_applied_is_prefix(&nav);
2893
2894 nav.prev_hunk();
2895 assert_applied_is_prefix(&nav);
2896 }
2897
2898 #[test]
2899 fn test_applied_prefix_after_preview_step_down() {
2900 let old = "a\nb\nc\n";
2902 let new = "A\nB\nc\n";
2903 let diff = DiffEngine::new().diff_strings(old, new);
2904 let mut nav = DiffNavigator::new(diff, Arc::from(old), Arc::from(new), false);
2905
2906 nav.next_hunk();
2907 assert!(nav.state().hunk_preview_mode);
2908 assert_applied_is_prefix(&nav);
2909
2910 nav.next(); assert!(!nav.state().hunk_preview_mode);
2912 assert_applied_is_prefix(&nav);
2913 }
2914
2915 #[test]
2916 fn test_applied_prefix_after_preview_step_up() {
2917 let old = "a\nb\nc\nd\ne\nf\ng\nh\n";
2919 let new = "A\nb\nc\nd\ne\nf\nG\nh\n";
2920 let diff = DiffEngine::new().diff_strings(old, new);
2921 let mut nav = DiffNavigator::new(diff, Arc::from(old), Arc::from(new), false);
2922
2923 nav.next_hunk();
2924 assert!(nav.state().hunk_preview_mode);
2925 assert_applied_is_prefix(&nav);
2926
2927 nav.prev(); assert!(!nav.state().hunk_preview_mode);
2929 assert_applied_is_prefix(&nav);
2930 }
2931
2932 #[test]
2933 fn test_primary_active_fallback_when_active_change_none() {
2934 let old = "a\nb\nc\n";
2937 let new = "A\nb\nC\n"; let diff = DiffEngine::new().diff_strings(old, new);
2939 let mut nav = DiffNavigator::new(diff, Arc::from(old), Arc::from(new), false);
2940
2941 nav.state_mut().animating_hunk = Some(0);
2943 nav.state_mut().active_change = None;
2944 nav.state_mut().step_direction = StepDirection::Forward;
2945
2946 let view = nav.current_view_with_frame(AnimationFrame::FadeOut);
2947
2948 let primary: Vec<_> = view.iter().filter(|l| l.is_primary_active).collect();
2949 assert_eq!(
2950 primary.len(),
2951 1,
2952 "Exactly one line should be primary active"
2953 );
2954 assert!(
2955 primary[0].is_active,
2956 "Primary active line must also be is_active"
2957 );
2958
2959 let first_active_idx = view.iter().position(|l| l.is_active).unwrap();
2961 let first_primary_idx = view.iter().position(|l| l.is_primary_active).unwrap();
2962 assert_eq!(
2963 first_active_idx, first_primary_idx,
2964 "First active line should be the primary line"
2965 );
2966 }
2967}