1use std::{
2 cell::RefCell,
3 collections::{HashMap, HashSet},
4 ops::Range,
5 rc::Rc,
6 sync::Arc,
7};
8
9use gpui::{
10 App, AppContext as _, Context, ElementId, EventEmitter, FocusHandle, Focusable, ListAlignment,
11 ListOffset, ListState, Pixels, ScrollHandle, SharedString, Size, Subscription, Task, Window,
12 WindowId, point, px,
13};
14use gpui_base::{TextSelection, TextSelectionContentKey, TextSelectionEvent, TextSelectionHandle};
15
16use super::{
17 DiffAnnotation, DiffConflictResolution, DiffFile, DiffFileStatus, DiffInlineUnit,
18 DiffLinePosition, DiffLineRange, DiffMode, DiffSide,
19 conflict::ConflictPart,
20 presentation::{FilePresentation, PresentationOptions, SyntaxHighlighters},
21 selection::SelectionGeometry,
22};
23
24#[derive(Clone, Debug, PartialEq, Eq)]
26pub(crate) enum DisplayRow {
27 File(usize),
29 Notice(usize),
31 Hunk {
32 file: usize,
33 hunk: usize,
34 },
35 Fold {
36 file: usize,
37 pairs: Range<usize>,
38 },
39 Conflict {
42 file: usize,
43 conflict: usize,
44 part: ConflictPart,
45 },
46 Code {
47 file: usize,
48 original: Option<usize>,
49 modified: Option<usize>,
50 changed: bool,
51 },
52}
53
54impl DisplayRow {
55 pub(crate) fn file(&self) -> usize {
56 match self {
57 Self::File(file) | Self::Notice(file) => *file,
58 Self::Hunk { file, .. }
59 | Self::Fold { file, .. }
60 | Self::Conflict { file, .. }
61 | Self::Code { file, .. } => *file,
62 }
63 }
64}
65
66#[derive(Clone, Copy, Debug, PartialEq, Eq)]
68pub(crate) enum FoldExpansion {
69 Up,
71 Down,
73 All,
74}
75
76#[derive(Clone, Debug)]
78#[non_exhaustive]
79pub enum DiffEvent {
80 SelectionStarted(DiffLineRange),
82 SelectionChanged(Option<DiffLineRange>),
84 SelectionEnded(DiffLineRange),
86 FileExpanded(SharedString),
88 FileCollapsed(SharedString),
90 ConflictResolved(SharedString, usize),
93}
94
95#[derive(Clone, Debug, PartialEq)]
97pub(crate) struct LayoutKey {
98 rem: Pixels,
99 font_size: Pixels,
100 font_family: SharedString,
101 mode: DiffMode,
102 line_number: bool,
103 soft_wrap: bool,
104}
105
106impl LayoutKey {
107 pub(crate) fn new(
108 rem: Pixels,
109 font_size: Pixels,
110 font_family: SharedString,
111 mode: DiffMode,
112 line_number: bool,
113 soft_wrap: bool,
114 ) -> Self {
115 Self {
116 rem,
117 font_size,
118 font_family,
119 mode,
120 line_number,
121 soft_wrap,
122 }
123 }
124}
125
126#[derive(Default)]
128struct FileRows {
129 original: Vec<Option<usize>>,
130 modified: Vec<Option<usize>>,
131}
132
133#[derive(Clone, Debug, PartialEq, Eq)]
135pub(crate) enum SelectedLines {
136 Side {
138 file: usize,
139 side: DiffSide,
140 lines: Range<usize>,
141 },
142 Patch { file: usize, patch: Range<usize> },
144}
145
146impl SelectedLines {
147 pub(crate) fn contains(
148 &self,
149 files: &[DiffFile],
150 file: usize,
151 side: DiffSide,
152 ix: usize,
153 ) -> bool {
154 match self {
155 Self::Side {
156 file: selected,
157 side: selected_side,
158 lines,
159 } => {
160 *selected == file
161 && *selected_side == side
162 && lines.contains(&files[file].line_number(side, ix))
163 }
164 Self::Patch {
165 file: selected,
166 patch,
167 } => *selected == file && patch.contains(&files[file].patch_ix(side, ix)),
168 }
169 }
170
171 pub(crate) fn is_last(
173 &self,
174 files: &[DiffFile],
175 file: usize,
176 side: DiffSide,
177 ix: usize,
178 ) -> bool {
179 match self {
180 Self::Side {
181 file: selected,
182 side: selected_side,
183 lines,
184 } => {
185 *selected == file
186 && *selected_side == side
187 && files[file].line_number(side, ix) + 1 == lines.end
188 }
189 Self::Patch {
190 file: selected,
191 patch,
192 } => *selected == file && files[file].patch_ix(side, ix) + 1 == patch.end,
193 }
194 }
195}
196
197pub struct DiffState {
204 files: Vec<DiffFile>,
205 file_ixs: HashMap<SharedString, usize>,
207 mode: DiffMode,
208 context_lines: Option<usize>,
209 expansion_lines: usize,
210 min_collapsed_lines: usize,
211 expanded: Vec<Vec<Range<usize>>>,
212 collapsed: HashSet<SharedString>,
213 resolutions: HashMap<(SharedString, usize), DiffConflictResolution>,
214 file_rows: Vec<FileRows>,
215 header_rows: Vec<usize>,
216 rows: Rc<Vec<DisplayRow>>,
217 change_rows: Vec<usize>,
218 list: ListState,
219 horizontal_scroll: ScrollHandle,
220 focus: FocusHandle,
221 selection: TextSelectionHandle,
222 geometry: Rc<RefCell<SelectionGeometry>>,
223 selected_lines: Option<DiffLineRange>,
224 selection_anchor: Option<DiffLinePosition>,
225 selection_cursor: Option<DiffLinePosition>,
226 selecting: bool,
227 presentation_options: PresentationOptions,
228 presentations: Vec<Option<Arc<FilePresentation>>>,
229 viewport: Size<Pixels>,
230 layout: Option<LayoutKey>,
231 content_width: Pixels,
232 row_height: Option<Pixels>,
233 annotations: HashSet<(Option<DiffLinePosition>, SharedString, ElementId)>,
234 window: Option<WindowId>,
235 presentation_generation: usize,
238 preparing: Option<usize>,
239 _presentation: Task<()>,
240 _selection_subscription: Subscription,
241}
242
243impl EventEmitter<DiffEvent> for DiffState {}
244impl Focusable for DiffState {
245 fn focus_handle(&self, _: &App) -> FocusHandle {
246 self.focus.clone()
247 }
248}
249
250impl DiffState {
251 pub fn new(files: impl IntoIterator<Item = DiffFile>, cx: &mut Context<Self>) -> Self {
253 let focus = cx.focus_handle().tab_stop(true);
254 let selection = TextSelectionHandle::new("", cx);
255 let geometry = Rc::new(RefCell::new(SelectionGeometry::default()));
256 let weak = cx.entity().downgrade();
257 selection.resolve_content_key_with(
258 {
259 let weak = weak.clone();
260 let geometry = geometry.clone();
261 move |point, cx| {
262 let state = weak.upgrade()?;
263 let snapshot = state.read(cx).selection.snapshot(cx);
264 let entity = Some(state.read(cx).selection.entity_id());
265 let snapshot =
266 snapshot.filter(|snapshot| snapshot.anchor().entity_id() == entity);
267 let side = snapshot
268 .and_then(|snapshot| snapshot.anchor().content_key())
269 .map(|key| decode_key(key.value()).1);
270 geometry
271 .borrow()
272 .key_at(point, side)
273 .map(TextSelectionContentKey::new)
274 .or_else(|| {
275 snapshot
276 .filter(|snapshot| snapshot.cursor().entity_id() == entity)
277 .and_then(|snapshot| snapshot.cursor().content_key())
278 })
279 }
280 },
281 cx,
282 );
283 selection.copy_with(
284 {
285 let weak = weak.clone();
286 move |cx| {
287 weak.upgrade()
288 .map_or_else(String::new, |state| state.read(cx).selected_text(cx))
289 }
290 },
291 cx,
292 );
293 selection.focus_with(
294 {
295 let focus = focus.clone();
296 move |window, cx| window.focus(&focus, cx)
297 },
298 cx,
299 );
300 let _selection_subscription = selection.subscribe(
301 move |event, cx| {
302 let _ = weak.update(cx, |state, cx| {
303 if !state.selection.has_local_selection(cx) {
304 match event {
305 TextSelectionEvent::SelectionChanged(Some(_)) => {
306 let changed = state.selected_lines.take().is_some();
307 state.selection_anchor = None;
309 state.selection_cursor = None;
310 if changed {
311 cx.emit(DiffEvent::SelectionChanged(None));
312 }
313 }
314 TextSelectionEvent::Cleared => {
315 if state.selected_lines.take().is_some() {
319 cx.emit(DiffEvent::SelectionChanged(None));
320 }
321 }
322 _ => {}
323 }
324 }
325 cx.notify();
326 });
327 },
328 cx,
329 );
330 let files = files.into_iter().collect::<Vec<_>>();
331 let mut state = Self {
332 expanded: vec![Vec::new(); files.len()],
333 presentations: vec![None; files.len()],
334 file_ixs: file_ixs(&files),
335 files,
336 mode: DiffMode::Unified,
337 context_lines: Some(3),
338 expansion_lines: 20,
339 min_collapsed_lines: 2,
340 collapsed: HashSet::new(),
341 resolutions: HashMap::new(),
342 file_rows: Vec::new(),
343 header_rows: Vec::new(),
344 rows: Rc::default(),
345 change_rows: Vec::new(),
346 list: ListState::new(0, ListAlignment::Top, px(0.)),
347 horizontal_scroll: ScrollHandle::new(),
348 focus,
349 selection,
350 geometry,
351 selected_lines: None,
352 selection_anchor: None,
353 selection_cursor: None,
354 selecting: false,
355 presentation_options: PresentationOptions {
356 inline_unit: Some(DiffInlineUnit::Word),
357 inline_max_line_length: 1000,
358 syntax_max_line_length: 1000,
359 },
360 viewport: Size::default(),
361 layout: None,
362 content_width: px(0.),
363 row_height: None,
364 annotations: HashSet::new(),
365 window: None,
366 presentation_generation: 0,
367 preparing: None,
368 _presentation: Task::ready(()),
369 _selection_subscription,
370 };
371 state.rebuild(true);
372 state
373 }
374
375 pub fn with_mode(mut self, mode: DiffMode) -> Self {
377 self.mode = mode;
378 self.rebuild(true);
379 self
380 }
381
382 pub fn with_context_lines(mut self, lines: Option<usize>) -> Self {
385 self.context_lines = lines;
386 self.rebuild(true);
387 self
388 }
389
390 pub fn with_expansion_lines(mut self, lines: usize) -> Self {
392 self.expansion_lines = lines.max(1);
393 self
394 }
395
396 pub fn with_min_collapsed_lines(mut self, lines: usize) -> Self {
399 self.min_collapsed_lines = lines.max(1);
400 self.rebuild(true);
401 self
402 }
403
404 pub fn with_inline_unit(mut self, unit: Option<DiffInlineUnit>) -> Self {
407 self.presentation_options.inline_unit = unit;
408 self.reset_presentation();
409 self
410 }
411
412 pub fn with_inline_max_line_length(mut self, length: usize) -> Self {
414 self.presentation_options.inline_max_line_length = length;
415 self.reset_presentation();
416 self
417 }
418
419 pub fn with_syntax_max_line_length(mut self, length: usize) -> Self {
421 self.presentation_options.syntax_max_line_length = length;
422 self.reset_presentation();
423 self
424 }
425
426 pub fn set_expansion_lines(&mut self, lines: usize, cx: &mut Context<Self>) {
428 let lines = lines.max(1);
429 if self.expansion_lines == lines {
430 return;
431 }
432 self.expansion_lines = lines;
433 cx.notify();
434 }
435
436 pub fn set_min_collapsed_lines(&mut self, lines: usize, cx: &mut Context<Self>) {
438 let lines = lines.max(1);
439 if self.min_collapsed_lines == lines {
440 return;
441 }
442 self.min_collapsed_lines = lines;
443 self.rebuild_at_anchor();
444 cx.notify();
445 }
446
447 pub fn set_inline_unit(&mut self, unit: Option<DiffInlineUnit>, cx: &mut Context<Self>) {
449 if self.presentation_options.inline_unit == unit {
450 return;
451 }
452 self.presentation_options.inline_unit = unit;
453 self.reset_presentation();
454 self.ensure_presentation(cx);
455 cx.notify();
456 }
457
458 pub fn set_inline_max_line_length(&mut self, length: usize, cx: &mut Context<Self>) {
460 if self.presentation_options.inline_max_line_length == length {
461 return;
462 }
463 self.presentation_options.inline_max_line_length = length;
464 self.reset_presentation();
465 self.ensure_presentation(cx);
466 cx.notify();
467 }
468
469 pub fn set_syntax_max_line_length(&mut self, length: usize, cx: &mut Context<Self>) {
471 if self.presentation_options.syntax_max_line_length == length {
472 return;
473 }
474 self.presentation_options.syntax_max_line_length = length;
475 self.reset_presentation();
476 self.ensure_presentation(cx);
477 cx.notify();
478 }
479
480 pub fn files(&self) -> &[DiffFile] {
482 &self.files
483 }
484 pub fn mode(&self) -> DiffMode {
485 self.mode
486 }
487 pub fn context_lines(&self) -> Option<usize> {
488 self.context_lines
489 }
490 pub fn expansion_lines(&self) -> usize {
491 self.expansion_lines
492 }
493 pub fn min_collapsed_lines(&self) -> usize {
494 self.min_collapsed_lines
495 }
496 pub fn inline_unit(&self) -> Option<DiffInlineUnit> {
497 self.presentation_options.inline_unit
498 }
499 pub fn inline_max_line_length(&self) -> usize {
500 self.presentation_options.inline_max_line_length
501 }
502 pub fn syntax_max_line_length(&self) -> usize {
503 self.presentation_options.syntax_max_line_length
504 }
505 pub fn selected_lines(&self) -> Option<DiffLineRange> {
506 self.selected_lines.clone()
507 }
508 pub fn is_file_collapsed(&self, path: &str) -> bool {
510 self.collapsed.contains(path)
511 }
512
513 pub(crate) fn rows(&self) -> &Rc<Vec<DisplayRow>> {
514 &self.rows
515 }
516 pub(crate) fn list(&self) -> &ListState {
517 &self.list
518 }
519 pub(crate) fn horizontal_scroll(&self) -> &ScrollHandle {
520 &self.horizontal_scroll
521 }
522 pub(crate) fn selection(&self) -> &TextSelectionHandle {
523 &self.selection
524 }
525 pub(crate) fn geometry(&self) -> &Rc<RefCell<SelectionGeometry>> {
526 &self.geometry
527 }
528 pub(crate) fn presentations(&self) -> &[Option<Arc<FilePresentation>>] {
529 &self.presentations
530 }
531 pub(crate) fn viewport(&self) -> Size<Pixels> {
533 self.viewport
534 }
535 pub(crate) fn content_width(&self) -> Pixels {
537 self.content_width
538 }
539 pub(crate) fn set_content_width(&mut self, width: Pixels) {
540 self.content_width = width;
541 }
542 pub(crate) fn record_paint(&mut self, viewport: Size<Pixels>, painted_width: Pixels) -> bool {
545 let wider = painted_width > self.content_width;
546 if wider {
547 self.content_width = painted_width;
548 }
549 let resized = self.viewport != viewport;
550 self.viewport = viewport;
551 wider || resized
552 }
553
554 pub fn set_files(&mut self, files: impl IntoIterator<Item = DiffFile>, cx: &mut Context<Self>) {
557 if let Some(window) = self.window
558 && self.selection_is_local(cx)
559 {
560 TextSelection::clear_for_window(window, cx);
561 }
562 self.files = files.into_iter().collect();
563 self.file_ixs = file_ixs(&self.files);
564 self.collapsed
565 .retain(|path| self.file_ixs.contains_key(path));
566 self.resolutions.clear();
567 self.expanded = vec![Vec::new(); self.files.len()];
568 self.reset_presentation();
569 self.layout = None;
570 self.annotations.clear();
571 self.selection.set_local_selection(false, cx);
572 self.selection.set_fallback_copy_text("", cx);
573 self.selected_lines = None;
574 self.selection_anchor = None;
575 self.selection_cursor = None;
576 self.selecting = false;
577 self.geometry.borrow_mut().clear();
578 self.rebuild(true);
579 self.horizontal_scroll.set_offset(point(px(0.), px(0.)));
580 self.ensure_presentation(cx);
581 cx.notify();
582 }
583
584 pub fn set_mode(&mut self, mode: DiffMode, cx: &mut Context<Self>) {
587 if self.mode == mode {
588 return;
589 }
590 self.mode = mode;
591 self.rebuild_at_anchor();
592 self.horizontal_scroll.set_offset(point(px(0.), px(0.)));
593 cx.notify();
594 }
595
596 pub fn set_context_lines(&mut self, lines: Option<usize>, cx: &mut Context<Self>) {
598 if self.context_lines == lines {
599 return;
600 }
601 self.context_lines = lines;
602 self.expanded.iter_mut().for_each(Vec::clear);
603 self.rebuild_at_anchor();
604 cx.notify();
605 }
606
607 pub fn expand_unchanged(&mut self, cx: &mut Context<Self>) {
609 for (expanded, file) in self.expanded.iter_mut().zip(&self.files) {
610 *expanded = vec![0..file.pairs().len()];
611 }
612 self.rebuild_at_anchor();
613 cx.notify();
614 }
615
616 pub fn collapse_unchanged(&mut self, cx: &mut Context<Self>) {
618 self.expanded.iter_mut().for_each(Vec::clear);
619 self.rebuild_at_anchor();
620 cx.notify();
621 }
622
623 pub fn set_file_collapsed(&mut self, path: &str, collapsed: bool, cx: &mut Context<Self>) {
626 let Some(file) = self.file_ix(path) else {
627 return;
628 };
629 let path = self.files[file].path().clone();
630 let changed = if collapsed {
631 self.collapsed.insert(path)
632 } else {
633 self.collapsed.remove(&path)
634 };
635 if changed {
636 self.rebuild_at_anchor();
637 cx.notify();
638 }
639 }
640
641 pub fn conflict_resolution(&self, path: &str, ix: usize) -> Option<DiffConflictResolution> {
645 let file = &self.files[self.file_ix(path)?];
646 self.resolutions.get(&(file.path().clone(), ix)).copied()
647 }
648
649 pub fn resolve_conflict(
653 &mut self,
654 path: &str,
655 ix: usize,
656 resolution: Option<DiffConflictResolution>,
657 cx: &mut Context<Self>,
658 ) {
659 let Some(file) = self.file_ix(path) else {
660 return;
661 };
662 if ix >= self.files[file].conflicts().len() {
663 return;
664 }
665 let key = (self.files[file].path().clone(), ix);
666 let previous = match resolution {
667 Some(resolution) => self.resolutions.insert(key, resolution),
668 None => self.resolutions.remove(&key),
669 };
670 if previous != resolution {
671 self.rebuild_at_anchor();
672 cx.notify();
673 }
674 }
675
676 pub fn resolved_text(&self, path: &str) -> Option<String> {
679 let file = &self.files[self.file_ix(path)?];
680 if file.status() != DiffFileStatus::Conflicted {
681 return None;
682 }
683 let lines = file.lines(DiffSide::Modified);
684 let source = file.source(DiffSide::Modified);
685 let mut text = String::with_capacity(source.len());
686 let mut ix = 0;
687 for (conflict_ix, conflict) in file.conflicts().iter().enumerate() {
688 let resolution = *self.resolutions.get(&(file.path().clone(), conflict_ix))?;
689 for line in &lines[ix..conflict.source_lines().start] {
690 text.push_str(&source[line.source()]);
691 }
692 for range in conflict.kept(resolution) {
693 for line in &lines[range] {
694 text.push_str(&source[line.source()]);
695 }
696 }
697 ix = conflict.source_lines().end;
698 }
699 for line in &lines[ix..] {
700 text.push_str(&source[line.source()]);
701 }
702 Some(text)
703 }
704
705 pub fn set_selected_lines(&mut self, range: Option<DiffLineRange>, cx: &mut Context<Self>) {
708 let range = range.and_then(|range| self.clip_range(&range));
709 if self.selected_lines == range {
710 return;
711 }
712 self.selection_anchor = range.as_ref().map(DiffLineRange::start_position);
713 self.selection_cursor = range.as_ref().map(DiffLineRange::end_position);
714 self.selection.set_local_selection(range.is_some(), cx);
715 self.selected_lines = range;
716 cx.notify();
717 }
718
719 pub fn selected_text(&self, cx: &App) -> String {
721 if self.selection.has_local_selection(cx)
722 && let Some(selected) = self.selected_span()
723 {
724 return match selected {
725 SelectedLines::Side { file, side, lines } => {
726 self.files[file].text_for_lines(side, lines.start, lines.end - 1)
727 }
728 SelectedLines::Patch { file, patch } => {
729 let file = &self.files[file];
730 let mut text = String::new();
731 for line in &file.patch_lines()[patch] {
732 let (side, ix) = match line.line(DiffSide::Modified) {
734 Some(ix) => (DiffSide::Modified, ix),
735 None => (
736 DiffSide::Original,
737 line.line(DiffSide::Original).unwrap_or(0),
738 ),
739 };
740 if !text.is_empty() && !text.ends_with('\n') {
743 text.push('\n');
744 }
745 text.push_str(&file.source(side)[file.lines(side)[ix].source()]);
746 }
747 text
748 }
749 };
750 }
751 let Some((file, side, range)) =
752 super::selection::selected_source_range(&self.selection, cx)
753 else {
754 return String::new();
755 };
756 self.files
757 .get(file)
758 .and_then(|file| file.source(side).get(range))
759 .unwrap_or("")
760 .to_owned()
761 }
762
763 pub fn scroll_to_line(&mut self, position: DiffLinePosition, cx: &mut Context<Self>) {
766 if self.line_index(&position).is_none() {
767 return;
768 }
769 if self.collapsed.remove(position.path().as_str()) {
770 self.rebuild_at_anchor();
771 }
772 if self.expand_line(&position).is_some() {
773 self.reveal_line(&position);
774 cx.notify();
775 }
776 }
777
778 pub fn scroll_to_file(&mut self, path: &str, cx: &mut Context<Self>) {
780 let Some(item_ix) = self
781 .file_ix(path)
782 .and_then(|file| self.header_rows.get(file).copied())
783 else {
784 return;
785 };
786 self.list.scroll_to(ListOffset {
787 item_ix,
788 offset_in_item: px(0.),
789 });
790 cx.notify();
791 }
792
793 pub fn next_change(&mut self, cx: &mut Context<Self>) {
795 self.move_change(true, cx);
796 }
797 pub fn previous_change(&mut self, cx: &mut Context<Self>) {
799 self.move_change(false, cx);
800 }
801
802 pub(crate) fn expand_fold(
804 &mut self,
805 file: usize,
806 pairs: Range<usize>,
807 expansion: FoldExpansion,
808 cx: &mut Context<Self>,
809 ) {
810 let lines = self.expansion_lines;
811 let revealed = match expansion {
812 FoldExpansion::All => pairs,
813 FoldExpansion::Up => pairs.end.saturating_sub(lines).max(pairs.start)..pairs.end,
814 FoldExpansion::Down => pairs.start..(pairs.start + lines).min(pairs.end),
815 };
816 if let Some(expanded) = self.expanded.get_mut(file) {
817 expanded.push(revealed);
818 self.rebuild_at_anchor();
819 cx.notify();
820 }
821 }
822
823 pub(crate) fn choose_conflict(
825 &mut self,
826 file: usize,
827 ix: usize,
828 resolution: Option<DiffConflictResolution>,
829 cx: &mut Context<Self>,
830 ) {
831 let path = self.files[file].path().clone();
832 self.resolve_conflict(&path, ix, resolution, cx);
833 cx.emit(DiffEvent::ConflictResolved(path, ix));
834 }
835
836 pub(crate) fn toggle_file_collapsed(&mut self, file: usize, cx: &mut Context<Self>) {
838 let path = self.files[file].path().clone();
839 let collapsed = !self.collapsed.contains(&path);
840 self.set_file_collapsed(&path, collapsed, cx);
841 cx.emit(if collapsed {
842 DiffEvent::FileCollapsed(path)
843 } else {
844 DiffEvent::FileExpanded(path)
845 });
846 }
847
848 pub(crate) fn begin_line_selection(
851 &mut self,
852 position: DiffLinePosition,
853 extend: bool,
854 window: &mut Window,
855 cx: &mut Context<Self>,
856 ) {
857 if self.line_index(&position).is_none() {
858 return;
859 }
860 let anchor = self
861 .selection_anchor
862 .clone()
863 .filter(|anchor| extend && self.can_extend(anchor, &position))
864 .unwrap_or_else(|| position.clone());
865 TextSelection::clear(window, cx);
866 window.focus(&self.focus, cx);
867 self.selecting = true;
868 cx.emit(DiffEvent::SelectionStarted(range_between(
869 &anchor, &position,
870 )));
871 self.select_user_range(anchor, position, cx);
872 }
873
874 pub(crate) fn drag_line_selection(
876 &mut self,
877 position: DiffLinePosition,
878 cx: &mut Context<Self>,
879 ) {
880 let Some(anchor) = self.selection_anchor.clone() else {
881 return;
882 };
883 if !self.selecting
884 || !self.can_extend(&anchor, &position)
885 || self.selection_cursor.as_ref() == Some(&position)
886 || self.line_index(&position).is_none()
887 {
888 return;
889 }
890 self.select_user_range(anchor, position, cx);
891 }
892
893 fn can_extend(&self, anchor: &DiffLinePosition, position: &DiffLinePosition) -> bool {
897 anchor.path() == position.path()
898 && (anchor.side() == position.side() || self.mode == DiffMode::Unified)
899 }
900
901 pub(crate) fn end_line_selection(&mut self, cx: &mut Context<Self>) {
903 if !std::mem::take(&mut self.selecting) {
904 return;
905 }
906 if let Some(range) = self.selected_lines.clone() {
907 cx.emit(DiffEvent::SelectionEnded(range));
908 }
909 }
910
911 #[cfg(test)]
913 pub(crate) fn click_line(
914 &mut self,
915 position: DiffLinePosition,
916 extend: bool,
917 window: &mut Window,
918 cx: &mut Context<Self>,
919 ) {
920 self.begin_line_selection(position, extend, window, cx);
921 self.end_line_selection(cx);
922 }
923
924 pub(crate) fn keyboard_select(
925 &mut self,
926 direction: isize,
927 extend: bool,
928 window: &mut Window,
929 cx: &mut Context<Self>,
930 ) {
931 let Some((file, side)) = self.active_side(cx) else {
932 return;
933 };
934 let count = self.files[file].lines_count(side);
935 let ix = self
936 .selection_cursor
937 .as_ref()
938 .filter(|position| {
939 position.side() == side && self.file_ix(position.path()) == Some(file)
940 })
941 .and_then(|position| self.line_index(position))
942 .map(|ix| ix.saturating_add_signed(direction).min(count - 1))
943 .unwrap_or(0);
944 let position = self.position(file, side, ix);
945 let anchor = self
946 .selection_anchor
947 .clone()
948 .filter(|anchor| extend && anchor.path() == position.path())
949 .unwrap_or_else(|| position.clone());
950 TextSelection::clear(window, cx);
951 window.focus(&self.focus, cx);
952 let range = self.select_user_range(anchor, position.clone(), cx);
953 cx.emit(DiffEvent::SelectionEnded(range));
954 if let Some(row) = self.expand_line(&position) {
955 self.list.scroll_to_reveal_item(row);
956 }
957 }
958
959 pub(crate) fn select_all(&mut self, window: &mut Window, cx: &mut Context<Self>) {
960 let Some((file, side)) = self.active_side(cx) else {
961 return;
962 };
963 let count = self.files[file].lines_count(side);
964 TextSelection::clear(window, cx);
965 window.focus(&self.focus, cx);
966 let range = self.select_user_range(
967 self.position(file, side, 0),
968 self.position(file, side, count - 1),
969 cx,
970 );
971 cx.emit(DiffEvent::SelectionEnded(range));
972 }
973
974 pub(crate) fn selected_span(&self) -> Option<SelectedLines> {
976 let range = self.selected_lines.as_ref()?;
977 let file = self.file_ix(range.path())?;
978 if range.is_single_side() {
979 let (start, end) = (
980 range.start().min(range.end()),
981 range.start().max(range.end()),
982 );
983 return Some(SelectedLines::Side {
984 file,
985 side: range.side(),
986 lines: start..end + 1,
987 });
988 }
989 let document = &self.files[file];
990 let start = document.patch_ix(
991 range.side(),
992 document.line_index(range.side(), range.start())?,
993 );
994 let end = document.patch_ix(
995 range.end_side(),
996 document.line_index(range.end_side(), range.end())?,
997 );
998 Some(SelectedLines::Patch {
999 file,
1000 patch: start.min(end)..start.max(end) + 1,
1001 })
1002 }
1003
1004 pub(crate) fn set_window(&mut self, window: WindowId) {
1006 self.window = Some(window);
1007 }
1008
1009 pub(crate) fn update_layout(&mut self, key: LayoutKey, row_height: Pixels) -> bool {
1012 if self.layout.as_ref() == Some(&key) {
1013 return false;
1014 }
1015 if self.layout.is_some() {
1016 self.list.remeasure();
1017 }
1018 self.layout = Some(key);
1019 self.row_height = Some(row_height);
1020 self.apply_row_height_hint();
1021 true
1022 }
1023
1024 pub(crate) fn sync_annotations(&mut self, annotations: &[DiffAnnotation]) {
1028 let current = annotations
1029 .iter()
1030 .map(|annotation| {
1031 (
1032 annotation.position().cloned(),
1033 annotation.path().clone(),
1034 annotation.id().clone(),
1035 )
1036 })
1037 .collect::<HashSet<_>>();
1038 if current == self.annotations {
1039 return;
1040 }
1041 let rows = current
1042 .symmetric_difference(&self.annotations)
1043 .filter_map(|(position, path, _)| match position {
1044 Some(position) => self.source_row(position),
1045 None => self
1046 .file_ix(path)
1047 .and_then(|file| self.header_rows.get(file).copied()),
1048 })
1049 .collect::<Vec<_>>();
1050 for row in rows {
1051 self.list.remeasure_items(row..row + 1);
1052 }
1053 self.annotations = current;
1054 }
1055
1056 pub(crate) fn position(&self, file: usize, side: DiffSide, ix: usize) -> DiffLinePosition {
1057 let document = &self.files[file];
1058 DiffLinePosition::new(
1059 document.path().clone(),
1060 side,
1061 document.line_number(side, ix),
1062 )
1063 }
1064
1065 fn file_ix(&self, path: &str) -> Option<usize> {
1066 self.file_ixs.get(path).copied()
1067 }
1068
1069 fn line_index(&self, position: &DiffLinePosition) -> Option<usize> {
1070 self.files[self.file_ix(position.path())?].line_index(position.side(), position.line())
1071 }
1072
1073 fn clip_range(&self, range: &DiffLineRange) -> Option<DiffLineRange> {
1075 let file = &self.files[self.file_ix(range.path())?];
1076 let first_at_or_after = |side: DiffSide, line: usize| {
1077 let lines = file.lines(side);
1078 lines
1079 .get(lines.partition_point(|source| source.line_number() < line))
1080 .map(|source| source.line_number())
1081 };
1082 let last_at_or_before = |side: DiffSide, line: usize| {
1083 let lines = file.lines(side);
1084 lines
1085 .partition_point(|source| source.line_number() <= line)
1086 .checked_sub(1)
1087 .map(|ix| lines[ix].line_number())
1088 };
1089 if range.is_single_side() {
1090 let (start, end) = (
1091 range.start().min(range.end()),
1092 range.start().max(range.end()),
1093 );
1094 let start = first_at_or_after(range.side(), start)?;
1095 let end = last_at_or_before(range.side(), end)?;
1096 return (start <= end)
1097 .then(|| DiffLineRange::new(range.path().clone(), range.side(), start, end));
1098 }
1099 let start = first_at_or_after(range.side(), range.start())
1100 .or_else(|| last_at_or_before(range.side(), range.start()))?;
1101 let end = last_at_or_before(range.end_side(), range.end())
1102 .or_else(|| first_at_or_after(range.end_side(), range.end()))?;
1103 Some(
1104 DiffLineRange::new(range.path().clone(), range.side(), start, end)
1105 .with_end_side(range.end_side()),
1106 )
1107 }
1108
1109 fn selection_is_local(&self, cx: &App) -> bool {
1110 let entity = Some(self.selection.entity_id());
1111 self.selection
1112 .snapshot(cx)
1113 .is_some_and(|snapshot| snapshot.anchor().entity_id() == entity)
1114 }
1115
1116 fn active_side(&self, cx: &App) -> Option<(usize, DiffSide)> {
1118 let (file, side) = self
1119 .selection_cursor
1120 .as_ref()
1121 .and_then(|cursor| Some((self.file_ix(cursor.path())?, Some(cursor.side()))))
1122 .filter(|_| self.selected_lines.is_some())
1123 .or_else(|| {
1124 super::selection::selected_source_range(&self.selection, cx)
1125 .map(|(file, side, _)| (file, Some(side)))
1126 })
1127 .or_else(|| {
1128 let top = self.list.logical_scroll_top().item_ix;
1129 self.rows.get(top).map(|row| (row.file(), None))
1130 })?;
1131 let document = self.files.get(file)?;
1132 let side = side.unwrap_or(if document.lines_count(DiffSide::Modified) > 0 {
1133 DiffSide::Modified
1134 } else {
1135 DiffSide::Original
1136 });
1137 (document.lines_count(side) > 0).then_some((file, side))
1138 }
1139
1140 fn expand_line(&mut self, position: &DiffLinePosition) -> Option<usize> {
1141 let row = self.source_row(position)?;
1142 if let Some(DisplayRow::Fold { file, pairs }) = self.rows.get(row) {
1143 let (file, pairs) = (*file, pairs.clone());
1144 self.expanded[file].push(pairs);
1145 self.rebuild_at_anchor();
1146 }
1147 self.source_row(position)
1148 }
1149
1150 fn select_user_range(
1152 &mut self,
1153 anchor: DiffLinePosition,
1154 cursor: DiffLinePosition,
1155 cx: &mut Context<Self>,
1156 ) -> DiffLineRange {
1157 let range = range_between(&anchor, &cursor);
1158 let changed = self.selected_lines.as_ref() != Some(&range);
1159 self.selected_lines = Some(range.clone());
1160 self.selection_anchor = Some(anchor);
1161 self.selection_cursor = Some(cursor);
1162 self.selection.set_local_selection(true, cx);
1163 if changed {
1164 cx.emit(DiffEvent::SelectionChanged(Some(range.clone())));
1165 }
1166 cx.notify();
1167 range
1168 }
1169
1170 fn move_change(&mut self, forward: bool, cx: &mut Context<Self>) {
1171 let top = self.list.logical_scroll_top().item_ix;
1172 let groups = &self.change_rows;
1173 let target = if forward {
1174 groups
1175 .get(groups.partition_point(|ix| *ix <= top))
1176 .copied()
1177 .or(groups.first().copied())
1178 } else {
1179 groups
1180 .partition_point(|ix| *ix < top)
1181 .checked_sub(1)
1182 .and_then(|ix| groups.get(ix))
1183 .copied()
1184 .or(groups.last().copied())
1185 };
1186 if let Some(ix) = target {
1187 self.list.scroll_to(ListOffset {
1188 item_ix: ix,
1189 offset_in_item: px(0.),
1190 });
1191 cx.notify();
1192 }
1193 }
1194
1195 fn row_position(&self, ix: usize) -> Option<DiffLinePosition> {
1197 let row = self.rows.get(ix)?;
1198 let file = row.file();
1199 let pair_position = |original: Option<usize>, modified: Option<usize>| {
1200 modified
1201 .map(|ix| self.position(file, DiffSide::Modified, ix))
1202 .or_else(|| original.map(|ix| self.position(file, DiffSide::Original, ix)))
1203 };
1204 match row {
1205 DisplayRow::Code {
1206 original, modified, ..
1207 } => pair_position(*original, *modified),
1208 DisplayRow::Fold { pairs, .. } => self.files[file]
1209 .pairs()
1210 .get(pairs.start)
1211 .and_then(|pair| pair_position(pair.original(), pair.modified())),
1212 DisplayRow::File(_)
1213 | DisplayRow::Notice(_)
1214 | DisplayRow::Hunk { .. }
1215 | DisplayRow::Conflict { .. } => self
1216 .rows
1217 .get(ix + 1)
1218 .filter(|next| next.file() == file)
1219 .and_then(|_| self.row_position(ix + 1)),
1220 }
1221 }
1222
1223 pub(crate) fn source_row(&self, position: &DiffLinePosition) -> Option<usize> {
1224 let rows = self.file_rows.get(self.file_ix(position.path())?)?;
1225 let rows = match position.side() {
1226 DiffSide::Original => &rows.original,
1227 DiffSide::Modified => &rows.modified,
1228 };
1229 rows.get(self.line_index(position)?).copied().flatten()
1230 }
1231
1232 fn reveal_line(&self, position: &DiffLinePosition) {
1233 if let Some(item_ix) = self.source_row(position) {
1234 self.list.scroll_to(ListOffset {
1235 item_ix,
1236 offset_in_item: px(0.),
1237 });
1238 }
1239 }
1240
1241 fn rebuild_at_anchor(&mut self) {
1243 let anchor = self.row_position(self.list.logical_scroll_top().item_ix);
1244 self.rebuild(false);
1245 if let Some(anchor) = anchor {
1246 self.reveal_line(&anchor);
1247 }
1248 }
1249
1250 fn rebuild(&mut self, reset: bool) {
1253 let rows = self.project_rows();
1254 self.file_rows = source_rows(&self.files, &rows);
1255 self.change_rows = changed_groups(&rows);
1256 self.header_rows = rows
1257 .iter()
1258 .enumerate()
1259 .filter_map(|(ix, row)| matches!(row, DisplayRow::File(_)).then_some(ix))
1260 .collect();
1261 if reset {
1262 self.list.reset(rows.len());
1263 } else {
1264 let old = &self.rows;
1265 let prefix = old.iter().zip(&rows).take_while(|(a, b)| a == b).count();
1266 let suffix = old
1267 .iter()
1268 .rev()
1269 .zip(rows.iter().rev())
1270 .take(old.len().min(rows.len()) - prefix)
1271 .take_while(|(a, b)| a == b)
1272 .count();
1273 self.list
1274 .splice(prefix..old.len() - suffix, rows.len() - prefix - suffix);
1275 }
1276 self.rows = Rc::new(rows);
1277 self.apply_row_height_hint();
1278 }
1279
1280 fn project_rows(&self) -> Vec<DisplayRow> {
1281 let mut rows = Vec::new();
1282 for (ix, file) in self.files.iter().enumerate() {
1283 rows.push(DisplayRow::File(ix));
1284 if self.collapsed.contains(file.path()) {
1285 continue;
1286 }
1287 if file.pairs().is_empty() {
1288 rows.push(DisplayRow::Notice(ix));
1289 } else if file.status() == DiffFileStatus::Unchanged {
1290 rows.extend((0..file.pairs().len()).map(|line| DisplayRow::Code {
1291 file: ix,
1292 original: None,
1293 modified: Some(line),
1294 changed: false,
1295 }));
1296 } else if file.status() == DiffFileStatus::Conflicted {
1297 let resolutions = (0..file.conflicts().len())
1298 .map(|conflict| {
1299 self.resolutions
1300 .get(&(file.path().clone(), conflict))
1301 .copied()
1302 })
1303 .collect::<Vec<_>>();
1304 let visible = visible_pairs(
1305 file,
1306 self.context_lines,
1307 self.min_collapsed_lines,
1308 &self.expanded[ix],
1309 );
1310 project_conflicts(&mut rows, ix, file, &visible, &resolutions);
1311 } else {
1312 project_file(
1313 &mut rows,
1314 ix,
1315 file,
1316 self.mode,
1317 self.context_lines,
1318 self.min_collapsed_lines,
1319 &self.expanded[ix],
1320 );
1321 }
1322 }
1323 rows
1324 }
1325
1326 fn apply_row_height_hint(&self) {
1329 if let Some(height) = self.row_height {
1330 let _ = self.list.clone().with_uniform_item_height(height);
1331 }
1332 }
1333
1334 fn reset_presentation(&mut self) {
1336 self.presentations = vec![None; self.files.len()];
1337 self.presentation_generation += 1;
1338 self.preparing = None;
1339 self._presentation = Task::ready(());
1340 }
1341
1342 pub(crate) fn ensure_presentation(&mut self, cx: &mut Context<Self>) {
1346 let generation = self.presentation_generation;
1347 if self.preparing == Some(generation) {
1348 return;
1349 }
1350 let pending = self
1351 .presentations
1352 .iter()
1353 .enumerate()
1354 .filter(|(_, presentation)| presentation.is_none())
1355 .map(|(ix, _)| (ix, self.files[ix].clone()))
1356 .collect::<Vec<_>>();
1357 if pending.is_empty() {
1358 return;
1359 }
1360 let options = self.presentation_options;
1361 self.preparing = Some(generation);
1362 self._presentation = cx.spawn(async move |this, cx| {
1363 let mut highlighters = Some(SyntaxHighlighters::default());
1364 for (ix, file) in pending {
1365 let mut moved = highlighters.take();
1366 let (presentation, returned) = cx
1367 .background_spawn(async move {
1368 let presentation = FilePresentation::prepare(
1369 &file,
1370 options,
1371 moved.get_or_insert_default(),
1372 );
1373 (presentation, moved)
1374 })
1375 .await;
1376 highlighters = returned;
1377 let current = this.update(cx, |state, cx| {
1378 if state.presentation_generation != generation {
1379 return false;
1380 }
1381 if let Some(slot) = state.presentations.get_mut(ix) {
1382 *slot = Some(Arc::new(presentation));
1383 }
1384 cx.notify();
1385 true
1386 });
1387 if !matches!(current, Ok(true)) {
1388 return;
1389 }
1390 }
1391 });
1392 }
1393}
1394
1395fn range_between(anchor: &DiffLinePosition, cursor: &DiffLinePosition) -> DiffLineRange {
1397 if anchor.side() == cursor.side() {
1398 DiffLineRange::new(
1399 cursor.path().clone(),
1400 cursor.side(),
1401 anchor.line().min(cursor.line()),
1402 anchor.line().max(cursor.line()),
1403 )
1404 } else {
1405 DiffLineRange::new(
1406 cursor.path().clone(),
1407 anchor.side(),
1408 anchor.line(),
1409 cursor.line(),
1410 )
1411 .with_end_side(cursor.side())
1412 }
1413}
1414
1415fn file_ixs(files: &[DiffFile]) -> HashMap<SharedString, usize> {
1416 let mut ixs = HashMap::with_capacity(files.len());
1417 for (ix, file) in files.iter().enumerate() {
1418 ixs.entry(file.path().clone()).or_insert(ix);
1419 }
1420 ixs
1421}
1422
1423fn source_rows(files: &[DiffFile], rows: &[DisplayRow]) -> Vec<FileRows> {
1427 let mut file_rows = files
1428 .iter()
1429 .map(|file| FileRows {
1430 original: vec![None; file.lines_count(DiffSide::Original)],
1431 modified: vec![None; file.lines_count(DiffSide::Modified)],
1432 })
1433 .collect::<Vec<_>>();
1434 for (row_ix, row) in rows.iter().enumerate() {
1435 let mut map = |file: usize, original: Option<usize>, modified: Option<usize>| {
1436 if let Some(line) = original {
1437 file_rows[file].original[line] = Some(row_ix);
1438 }
1439 if let Some(line) = modified {
1440 file_rows[file].modified[line] = Some(row_ix);
1441 }
1442 };
1443 match row {
1444 DisplayRow::Code {
1445 file,
1446 original,
1447 modified,
1448 ..
1449 } => map(*file, *original, *modified),
1450 DisplayRow::Fold { file, pairs } => {
1451 for pair in &files[*file].pairs()[pairs.clone()] {
1452 map(*file, pair.original(), pair.modified());
1453 }
1454 }
1455 DisplayRow::File(_)
1456 | DisplayRow::Notice(_)
1457 | DisplayRow::Hunk { .. }
1458 | DisplayRow::Conflict { .. } => {}
1459 }
1460 }
1461 file_rows
1462}
1463
1464fn changed_groups(rows: &[DisplayRow]) -> Vec<usize> {
1465 let changed = |row: &DisplayRow| matches!(row, DisplayRow::Code { changed: true, .. });
1466 rows.iter()
1467 .enumerate()
1468 .filter_map(|(ix, row)| {
1469 (changed(row)
1470 && (ix == 0 || !changed(&rows[ix - 1]) || rows[ix - 1].file() != row.file()))
1471 .then_some(ix)
1472 })
1473 .collect()
1474}
1475
1476const SIDE_BIT: u64 = 1 << 40;
1477const OFFSET_MASK: u64 = SIDE_BIT - 1;
1478
1479pub(crate) fn encode_key(file: usize, side: DiffSide, offset: usize) -> u64 {
1481 ((file as u64) << 41)
1482 | if side == DiffSide::Modified {
1483 SIDE_BIT
1484 } else {
1485 0
1486 }
1487 | (offset as u64 & OFFSET_MASK)
1488}
1489pub(crate) fn decode_key(key: u64) -> (usize, DiffSide, usize) {
1490 (
1491 (key >> 41) as usize,
1492 if key & SIDE_BIT == 0 {
1493 DiffSide::Original
1494 } else {
1495 DiffSide::Modified
1496 },
1497 (key & OFFSET_MASK) as usize,
1498 )
1499}
1500
1501fn hunk_ends(document: &DiffFile) -> Vec<usize> {
1503 let pairs = document.pairs();
1504 let mut hunk_end = vec![pairs.len(); pairs.len()];
1505 for hunk in document.hunks() {
1506 hunk_end[hunk.pairs()].fill(hunk.pairs().end);
1507 }
1508 hunk_end
1509}
1510
1511fn visible_pairs(
1514 document: &DiffFile,
1515 context: Option<usize>,
1516 min_collapsed_lines: usize,
1517 expanded: &[Range<usize>],
1518) -> Vec<bool> {
1519 let pairs = document.pairs();
1520 let hunks = document.hunks();
1521 let mut hunk_end = vec![pairs.len(); pairs.len()];
1522 let mut hunk_start = vec![0; pairs.len()];
1523 for hunk in hunks {
1524 hunk_start[hunk.pairs()].fill(hunk.pairs().start);
1525 hunk_end[hunk.pairs()].fill(hunk.pairs().end);
1526 }
1527 let mut boundaries = vec![0isize; pairs.len() + 1];
1530 if let Some(context) = context {
1531 let mut ix = 0;
1532 while ix < pairs.len() {
1533 if !pairs[ix].is_changed() {
1534 ix += 1;
1535 continue;
1536 }
1537 let start = ix;
1538 while ix < pairs.len() && ix < hunk_end[start] && pairs[ix].is_changed() {
1539 ix += 1;
1540 }
1541 boundaries[start.saturating_sub(context).max(hunk_start[start])] += 1;
1542 boundaries[ix.saturating_add(context).min(hunk_end[start])] -= 1;
1543 }
1544 } else {
1545 boundaries[0] += 1;
1546 boundaries[pairs.len()] -= 1;
1547 }
1548 for range in expanded {
1549 let start = range.start.min(pairs.len());
1550 let end = range.end.min(pairs.len());
1551 if start < end {
1552 boundaries[start] += 1;
1553 boundaries[end] -= 1;
1554 }
1555 }
1556 let mut active = 0;
1557 let mut visible = boundaries[..pairs.len()]
1558 .iter()
1559 .map(|boundary| {
1560 active += boundary;
1561 active > 0
1562 })
1563 .collect::<Vec<_>>();
1564 let mut ix = 0;
1566 while ix < pairs.len() {
1567 if visible[ix] {
1568 ix += 1;
1569 continue;
1570 }
1571 let start = ix;
1572 while ix < pairs.len() && ix < hunk_end[start] && !visible[ix] {
1573 ix += 1;
1574 }
1575 if ix - start < min_collapsed_lines {
1576 visible[start..ix].fill(true);
1577 }
1578 }
1579 visible
1580}
1581
1582fn project_file(
1583 rows: &mut Vec<DisplayRow>,
1584 file: usize,
1585 document: &DiffFile,
1586 mode: DiffMode,
1587 context: Option<usize>,
1588 min_collapsed_lines: usize,
1589 expanded: &[Range<usize>],
1590) {
1591 let pairs = document.pairs();
1592 let hunks = document.hunks();
1593 let hunk_end = hunk_ends(document);
1594 let visible = visible_pairs(document, context, min_collapsed_lines, expanded);
1595 let mut hunk_ix = 0;
1596 let mut ix = 0;
1597 while ix < pairs.len() {
1598 while let Some(hunk) = hunks.get(hunk_ix) {
1599 if hunk.pairs().start != ix {
1600 break;
1601 }
1602 rows.push(DisplayRow::Hunk {
1603 file,
1604 hunk: hunk_ix,
1605 });
1606 hunk_ix += 1;
1607 }
1608 if !visible[ix] {
1609 let start = ix;
1610 while ix < pairs.len() && ix < hunk_end[start] && !visible[ix] {
1611 ix += 1;
1612 }
1613 rows.push(DisplayRow::Fold {
1614 file,
1615 pairs: start..ix,
1616 });
1617 } else if mode == DiffMode::Unified && pairs[ix].is_changed() {
1618 let start = ix;
1619 while ix < pairs.len() && ix < hunk_end[start] && pairs[ix].is_changed() {
1620 ix += 1;
1621 }
1622 for pair in &pairs[start..ix] {
1623 if let Some(original) = pair.original() {
1624 rows.push(DisplayRow::Code {
1625 file,
1626 original: Some(original),
1627 modified: None,
1628 changed: true,
1629 });
1630 }
1631 }
1632 for pair in &pairs[start..ix] {
1633 if let Some(modified) = pair.modified() {
1634 rows.push(DisplayRow::Code {
1635 file,
1636 original: None,
1637 modified: Some(modified),
1638 changed: true,
1639 });
1640 }
1641 }
1642 } else {
1643 let pair = pairs[ix];
1644 rows.push(DisplayRow::Code {
1645 file,
1646 original: pair.original(),
1647 modified: pair.modified(),
1648 changed: pair.is_changed(),
1649 });
1650 ix += 1;
1651 }
1652 }
1653}
1654
1655fn project_conflicts(
1658 rows: &mut Vec<DisplayRow>,
1659 file: usize,
1660 document: &DiffFile,
1661 visible: &[bool],
1662 resolutions: &[Option<DiffConflictResolution>],
1663) {
1664 let code = |line: usize, changed: bool| DisplayRow::Code {
1666 file,
1667 original: None,
1668 modified: Some(line),
1669 changed,
1670 };
1671 let conflicts = document.conflicts();
1672 let mut next = 0;
1673 let mut ix = 0;
1674 while ix < visible.len() {
1675 if let Some(conflict) = conflicts.get(next)
1676 && conflict.source_lines().start == ix
1677 {
1678 match resolutions[next] {
1679 None => {
1680 for part in [
1681 ConflictPart::Current,
1682 ConflictPart::Base,
1683 ConflictPart::Incoming,
1684 ] {
1685 if let Some(lines) = conflict.part(part) {
1686 rows.push(DisplayRow::Conflict {
1687 file,
1688 conflict: next,
1689 part,
1690 });
1691 rows.extend(lines.map(|line| code(line, true)));
1692 }
1693 }
1694 }
1695 Some(resolution) => {
1696 rows.push(DisplayRow::Conflict {
1697 file,
1698 conflict: next,
1699 part: ConflictPart::Current,
1700 });
1701 for lines in conflict.kept(resolution) {
1702 rows.extend(lines.map(|line| code(line, false)));
1703 }
1704 }
1705 }
1706 ix = conflict.source_lines().end;
1707 next += 1;
1708 continue;
1709 }
1710 if visible[ix] {
1711 rows.push(code(ix, false));
1712 ix += 1;
1713 continue;
1714 }
1715 let start = ix;
1716 while ix < visible.len() && !visible[ix] {
1717 ix += 1;
1718 }
1719 rows.push(DisplayRow::Fold {
1720 file,
1721 pairs: start..ix,
1722 });
1723 }
1724}