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 hinted_width: Option<Pixels>,
235 annotations: HashSet<(Option<DiffLinePosition>, SharedString, ElementId)>,
236 window: Option<WindowId>,
237 presentation_generation: usize,
240 preparing: Option<usize>,
241 _presentation: Task<()>,
242 _selection_subscription: Subscription,
243}
244
245impl EventEmitter<DiffEvent> for DiffState {}
246impl Focusable for DiffState {
247 fn focus_handle(&self, _: &App) -> FocusHandle {
248 self.focus.clone()
249 }
250}
251
252impl DiffState {
253 pub fn new(files: impl IntoIterator<Item = DiffFile>, cx: &mut Context<Self>) -> Self {
255 let focus = cx.focus_handle().tab_stop(true);
256 let selection = TextSelectionHandle::new("", cx);
257 let geometry = Rc::new(RefCell::new(SelectionGeometry::default()));
258 let weak = cx.entity().downgrade();
259 selection.resolve_content_key_with(
260 {
261 let weak = weak.clone();
262 let geometry = geometry.clone();
263 move |point, cx| {
264 let state = weak.upgrade()?;
265 let snapshot = state.read(cx).selection.snapshot(cx);
266 let entity = Some(state.read(cx).selection.entity_id());
267 let snapshot =
268 snapshot.filter(|snapshot| snapshot.anchor().entity_id() == entity);
269 let side = snapshot
270 .and_then(|snapshot| snapshot.anchor().content_key())
271 .map(|key| decode_key(key.value()).1);
272 geometry
273 .borrow()
274 .key_at(point, side)
275 .map(TextSelectionContentKey::new)
276 .or_else(|| {
277 snapshot
278 .filter(|snapshot| snapshot.cursor().entity_id() == entity)
279 .and_then(|snapshot| snapshot.cursor().content_key())
280 })
281 }
282 },
283 cx,
284 );
285 selection.copy_with(
286 {
287 let weak = weak.clone();
288 move |cx| {
289 weak.upgrade()
290 .map_or_else(String::new, |state| state.read(cx).selected_text(cx))
291 }
292 },
293 cx,
294 );
295 selection.focus_with(
296 {
297 let focus = focus.clone();
298 move |window, cx| window.focus(&focus, cx)
299 },
300 cx,
301 );
302 let _selection_subscription = selection.subscribe(
303 move |event, cx| {
304 let _ = weak.update(cx, |state, cx| {
305 if !state.selection.has_local_selection(cx) {
306 match event {
307 TextSelectionEvent::SelectionChanged(Some(_)) => {
308 let changed = state.selected_lines.take().is_some();
309 state.selection_anchor = None;
311 state.selection_cursor = None;
312 if changed {
313 cx.emit(DiffEvent::SelectionChanged(None));
314 }
315 }
316 TextSelectionEvent::Cleared => {
317 if state.selected_lines.take().is_some() {
321 cx.emit(DiffEvent::SelectionChanged(None));
322 }
323 }
324 _ => {}
325 }
326 }
327 cx.notify();
328 });
329 },
330 cx,
331 );
332 let files = files.into_iter().collect::<Vec<_>>();
333 let mut state = Self {
334 expanded: vec![Vec::new(); files.len()],
335 presentations: vec![None; files.len()],
336 file_ixs: file_ixs(&files),
337 files,
338 mode: DiffMode::Unified,
339 context_lines: Some(3),
340 expansion_lines: 20,
341 min_collapsed_lines: 2,
342 collapsed: HashSet::new(),
343 resolutions: HashMap::new(),
344 file_rows: Vec::new(),
345 header_rows: Vec::new(),
346 rows: Rc::default(),
347 change_rows: Vec::new(),
348 list: ListState::new(0, ListAlignment::Top, px(0.)),
349 horizontal_scroll: ScrollHandle::new(),
350 focus,
351 selection,
352 geometry,
353 selected_lines: None,
354 selection_anchor: None,
355 selection_cursor: None,
356 selecting: false,
357 presentation_options: PresentationOptions {
358 inline_unit: Some(DiffInlineUnit::Word),
359 inline_max_line_length: 1000,
360 syntax_max_line_length: 1000,
361 },
362 viewport: Size::default(),
363 layout: None,
364 content_width: px(0.),
365 row_height: None,
366 hinted_width: None,
367 annotations: HashSet::new(),
368 window: None,
369 presentation_generation: 0,
370 preparing: None,
371 _presentation: Task::ready(()),
372 _selection_subscription,
373 };
374 state.rebuild(true);
375 state
376 }
377
378 pub fn with_mode(mut self, mode: DiffMode) -> Self {
380 self.mode = mode;
381 self.rebuild(true);
382 self
383 }
384
385 pub fn with_context_lines(mut self, lines: Option<usize>) -> Self {
388 self.context_lines = lines;
389 self.rebuild(true);
390 self
391 }
392
393 pub fn with_expansion_lines(mut self, lines: usize) -> Self {
395 self.expansion_lines = lines.max(1);
396 self
397 }
398
399 pub fn with_min_collapsed_lines(mut self, lines: usize) -> Self {
402 self.min_collapsed_lines = lines.max(1);
403 self.rebuild(true);
404 self
405 }
406
407 pub fn with_inline_unit(mut self, unit: Option<DiffInlineUnit>) -> Self {
410 self.presentation_options.inline_unit = unit;
411 self.reset_presentation();
412 self
413 }
414
415 pub fn with_inline_max_line_length(mut self, length: usize) -> Self {
417 self.presentation_options.inline_max_line_length = length;
418 self.reset_presentation();
419 self
420 }
421
422 pub fn with_syntax_max_line_length(mut self, length: usize) -> Self {
424 self.presentation_options.syntax_max_line_length = length;
425 self.reset_presentation();
426 self
427 }
428
429 pub fn set_expansion_lines(&mut self, lines: usize, cx: &mut Context<Self>) {
431 let lines = lines.max(1);
432 if self.expansion_lines == lines {
433 return;
434 }
435 self.expansion_lines = lines;
436 cx.notify();
437 }
438
439 pub fn set_min_collapsed_lines(&mut self, lines: usize, cx: &mut Context<Self>) {
441 let lines = lines.max(1);
442 if self.min_collapsed_lines == lines {
443 return;
444 }
445 self.min_collapsed_lines = lines;
446 self.rebuild_at_anchor();
447 cx.notify();
448 }
449
450 pub fn set_inline_unit(&mut self, unit: Option<DiffInlineUnit>, cx: &mut Context<Self>) {
452 if self.presentation_options.inline_unit == unit {
453 return;
454 }
455 self.presentation_options.inline_unit = unit;
456 self.reset_presentation();
457 self.ensure_presentation(cx);
458 cx.notify();
459 }
460
461 pub fn set_inline_max_line_length(&mut self, length: usize, cx: &mut Context<Self>) {
463 if self.presentation_options.inline_max_line_length == length {
464 return;
465 }
466 self.presentation_options.inline_max_line_length = length;
467 self.reset_presentation();
468 self.ensure_presentation(cx);
469 cx.notify();
470 }
471
472 pub fn set_syntax_max_line_length(&mut self, length: usize, cx: &mut Context<Self>) {
474 if self.presentation_options.syntax_max_line_length == length {
475 return;
476 }
477 self.presentation_options.syntax_max_line_length = length;
478 self.reset_presentation();
479 self.ensure_presentation(cx);
480 cx.notify();
481 }
482
483 pub fn files(&self) -> &[DiffFile] {
485 &self.files
486 }
487 pub fn mode(&self) -> DiffMode {
488 self.mode
489 }
490 pub fn context_lines(&self) -> Option<usize> {
491 self.context_lines
492 }
493 pub fn expansion_lines(&self) -> usize {
494 self.expansion_lines
495 }
496 pub fn min_collapsed_lines(&self) -> usize {
497 self.min_collapsed_lines
498 }
499 pub fn inline_unit(&self) -> Option<DiffInlineUnit> {
500 self.presentation_options.inline_unit
501 }
502 pub fn inline_max_line_length(&self) -> usize {
503 self.presentation_options.inline_max_line_length
504 }
505 pub fn syntax_max_line_length(&self) -> usize {
506 self.presentation_options.syntax_max_line_length
507 }
508 pub fn selected_lines(&self) -> Option<DiffLineRange> {
509 self.selected_lines.clone()
510 }
511 pub fn is_file_collapsed(&self, path: &str) -> bool {
513 self.collapsed.contains(path)
514 }
515
516 pub(crate) fn rows(&self) -> &Rc<Vec<DisplayRow>> {
517 &self.rows
518 }
519 pub(crate) fn list(&self) -> &ListState {
520 &self.list
521 }
522 pub(crate) fn horizontal_scroll(&self) -> &ScrollHandle {
523 &self.horizontal_scroll
524 }
525 pub(crate) fn selection(&self) -> &TextSelectionHandle {
526 &self.selection
527 }
528 pub(crate) fn geometry(&self) -> &Rc<RefCell<SelectionGeometry>> {
529 &self.geometry
530 }
531 pub(crate) fn presentations(&self) -> &[Option<Arc<FilePresentation>>] {
532 &self.presentations
533 }
534 pub(crate) fn viewport(&self) -> Size<Pixels> {
536 self.viewport
537 }
538 pub(crate) fn content_width(&self) -> Pixels {
540 self.content_width
541 }
542 pub(crate) fn set_content_width(&mut self, width: Pixels) {
543 self.content_width = width;
544 }
545 pub(crate) fn record_paint(&mut self, viewport: Size<Pixels>, painted_width: Pixels) -> bool {
548 let wider = painted_width > self.content_width;
549 if wider {
550 self.content_width = painted_width;
551 }
552 let resized = self.viewport != viewport;
553 self.viewport = viewport;
554 wider || resized
555 }
556
557 pub fn set_files(&mut self, files: impl IntoIterator<Item = DiffFile>, cx: &mut Context<Self>) {
560 if let Some(window) = self.window
561 && self.selection_is_local(cx)
562 {
563 TextSelection::clear_for_window(window, cx);
564 }
565 self.files = files.into_iter().collect();
566 self.file_ixs = file_ixs(&self.files);
567 self.collapsed
568 .retain(|path| self.file_ixs.contains_key(path));
569 self.resolutions.clear();
570 self.expanded = vec![Vec::new(); self.files.len()];
571 self.reset_presentation();
572 self.layout = None;
573 self.annotations.clear();
574 self.selection.set_local_selection(false, cx);
575 self.selection.set_fallback_copy_text("", cx);
576 self.selected_lines = None;
577 self.selection_anchor = None;
578 self.selection_cursor = None;
579 self.selecting = false;
580 self.geometry.borrow_mut().clear();
581 self.rebuild(true);
582 self.horizontal_scroll.set_offset(point(px(0.), px(0.)));
583 self.ensure_presentation(cx);
584 cx.notify();
585 }
586
587 pub fn set_mode(&mut self, mode: DiffMode, cx: &mut Context<Self>) {
590 if self.mode == mode {
591 return;
592 }
593 self.mode = mode;
594 self.rebuild_at_anchor();
595 self.horizontal_scroll.set_offset(point(px(0.), px(0.)));
596 cx.notify();
597 }
598
599 pub fn set_context_lines(&mut self, lines: Option<usize>, cx: &mut Context<Self>) {
601 if self.context_lines == lines {
602 return;
603 }
604 self.context_lines = lines;
605 self.expanded.iter_mut().for_each(Vec::clear);
606 self.rebuild_at_anchor();
607 cx.notify();
608 }
609
610 pub fn expand_unchanged(&mut self, cx: &mut Context<Self>) {
612 for (expanded, file) in self.expanded.iter_mut().zip(&self.files) {
613 *expanded = vec![0..file.pairs().len()];
614 }
615 self.rebuild_at_anchor();
616 cx.notify();
617 }
618
619 pub fn collapse_unchanged(&mut self, cx: &mut Context<Self>) {
621 self.expanded.iter_mut().for_each(Vec::clear);
622 self.rebuild_at_anchor();
623 cx.notify();
624 }
625
626 pub fn set_file_collapsed(&mut self, path: &str, collapsed: bool, cx: &mut Context<Self>) {
629 let Some(file) = self.file_ix(path) else {
630 return;
631 };
632 let path = self.files[file].path().clone();
633 let changed = if collapsed {
634 self.collapsed.insert(path)
635 } else {
636 self.collapsed.remove(&path)
637 };
638 if changed {
639 self.rebuild_at_anchor();
640 cx.notify();
641 }
642 }
643
644 pub fn conflict_resolution(&self, path: &str, ix: usize) -> Option<DiffConflictResolution> {
648 let file = &self.files[self.file_ix(path)?];
649 self.resolutions.get(&(file.path().clone(), ix)).copied()
650 }
651
652 pub fn resolve_conflict(
656 &mut self,
657 path: &str,
658 ix: usize,
659 resolution: Option<DiffConflictResolution>,
660 cx: &mut Context<Self>,
661 ) {
662 let Some(file) = self.file_ix(path) else {
663 return;
664 };
665 if ix >= self.files[file].conflicts().len() {
666 return;
667 }
668 let key = (self.files[file].path().clone(), ix);
669 let previous = match resolution {
670 Some(resolution) => self.resolutions.insert(key, resolution),
671 None => self.resolutions.remove(&key),
672 };
673 if previous != resolution {
674 self.rebuild_at_anchor();
675 cx.notify();
676 }
677 }
678
679 pub fn resolved_text(&self, path: &str) -> Option<String> {
682 let file = &self.files[self.file_ix(path)?];
683 if file.status() != DiffFileStatus::Conflicted {
684 return None;
685 }
686 let lines = file.lines(DiffSide::Modified);
687 let source = file.source(DiffSide::Modified);
688 let mut text = String::with_capacity(source.len());
689 let mut ix = 0;
690 for (conflict_ix, conflict) in file.conflicts().iter().enumerate() {
691 let resolution = *self.resolutions.get(&(file.path().clone(), conflict_ix))?;
692 for line in &lines[ix..conflict.source_lines().start] {
693 text.push_str(&source[line.source()]);
694 }
695 for range in conflict.kept(resolution) {
696 for line in &lines[range] {
697 text.push_str(&source[line.source()]);
698 }
699 }
700 ix = conflict.source_lines().end;
701 }
702 for line in &lines[ix..] {
703 text.push_str(&source[line.source()]);
704 }
705 Some(text)
706 }
707
708 pub fn set_selected_lines(&mut self, range: Option<DiffLineRange>, cx: &mut Context<Self>) {
711 let range = range.and_then(|range| self.clip_range(&range));
712 if self.selected_lines == range {
713 return;
714 }
715 self.selection_anchor = range.as_ref().map(DiffLineRange::start_position);
716 self.selection_cursor = range.as_ref().map(DiffLineRange::end_position);
717 self.selection.set_local_selection(range.is_some(), cx);
718 self.selected_lines = range;
719 cx.notify();
720 }
721
722 pub fn selected_text(&self, cx: &App) -> String {
724 if self.selection.has_local_selection(cx)
725 && let Some(selected) = self.selected_span()
726 {
727 return match selected {
728 SelectedLines::Side { file, side, lines } => {
729 self.files[file].text_for_lines(side, lines.start, lines.end - 1)
730 }
731 SelectedLines::Patch { file, patch } => {
732 let file = &self.files[file];
733 let mut text = String::new();
734 for line in &file.patch_lines()[patch] {
735 let (side, ix) = match line.line(DiffSide::Modified) {
737 Some(ix) => (DiffSide::Modified, ix),
738 None => (
739 DiffSide::Original,
740 line.line(DiffSide::Original).unwrap_or(0),
741 ),
742 };
743 if !text.is_empty() && !text.ends_with('\n') {
746 text.push('\n');
747 }
748 text.push_str(&file.source(side)[file.lines(side)[ix].source()]);
749 }
750 text
751 }
752 };
753 }
754 let Some((file, side, range)) =
755 super::selection::selected_source_range(&self.selection, cx)
756 else {
757 return String::new();
758 };
759 self.files
760 .get(file)
761 .and_then(|file| file.source(side).get(range))
762 .unwrap_or("")
763 .to_owned()
764 }
765
766 pub fn scroll_to_line(&mut self, position: DiffLinePosition, cx: &mut Context<Self>) {
769 if self.line_index(&position).is_none() {
770 return;
771 }
772 if self.collapsed.remove(position.path().as_str()) {
773 self.rebuild_at_anchor();
774 }
775 if self.expand_line(&position).is_some() {
776 self.reveal_line(&position);
777 cx.notify();
778 }
779 }
780
781 pub fn scroll_to_file(&mut self, path: &str, cx: &mut Context<Self>) {
783 let Some(item_ix) = self
784 .file_ix(path)
785 .and_then(|file| self.header_rows.get(file).copied())
786 else {
787 return;
788 };
789 self.list.scroll_to(ListOffset {
790 item_ix,
791 offset_in_item: px(0.),
792 });
793 cx.notify();
794 }
795
796 pub fn next_change(&mut self, cx: &mut Context<Self>) {
799 self.move_change(true, cx);
800 }
801 pub fn previous_change(&mut self, cx: &mut Context<Self>) {
803 self.move_change(false, cx);
804 }
805
806 pub(crate) fn expand_fold(
808 &mut self,
809 file: usize,
810 pairs: Range<usize>,
811 expansion: FoldExpansion,
812 cx: &mut Context<Self>,
813 ) {
814 let lines = self.expansion_lines;
815 let revealed = match expansion {
816 FoldExpansion::All => pairs,
817 FoldExpansion::Up => pairs.end.saturating_sub(lines).max(pairs.start)..pairs.end,
818 FoldExpansion::Down => pairs.start..(pairs.start + lines).min(pairs.end),
819 };
820 if let Some(expanded) = self.expanded.get_mut(file) {
821 expanded.push(revealed);
822 self.rebuild_at_anchor();
823 cx.notify();
824 }
825 }
826
827 pub(crate) fn choose_conflict(
829 &mut self,
830 file: usize,
831 ix: usize,
832 resolution: Option<DiffConflictResolution>,
833 cx: &mut Context<Self>,
834 ) {
835 let path = self.files[file].path().clone();
836 self.resolve_conflict(&path, ix, resolution, cx);
837 cx.emit(DiffEvent::ConflictResolved(path, ix));
838 }
839
840 pub(crate) fn toggle_file_collapsed(&mut self, file: usize, cx: &mut Context<Self>) {
842 let path = self.files[file].path().clone();
843 let collapsed = !self.collapsed.contains(&path);
844 self.set_file_collapsed(&path, collapsed, cx);
845 cx.emit(if collapsed {
846 DiffEvent::FileCollapsed(path)
847 } else {
848 DiffEvent::FileExpanded(path)
849 });
850 }
851
852 pub(crate) fn begin_line_selection(
855 &mut self,
856 position: DiffLinePosition,
857 extend: bool,
858 window: &mut Window,
859 cx: &mut Context<Self>,
860 ) {
861 if self.line_index(&position).is_none() {
862 return;
863 }
864 let anchor = self
865 .selection_anchor
866 .clone()
867 .filter(|anchor| extend && self.can_extend(anchor, &position))
868 .unwrap_or_else(|| position.clone());
869 TextSelection::clear(window, cx);
870 window.focus(&self.focus, cx);
871 self.selecting = true;
872 cx.emit(DiffEvent::SelectionStarted(range_between(
873 &anchor, &position,
874 )));
875 self.select_user_range(anchor, position, cx);
876 }
877
878 pub(crate) fn drag_line_selection(
880 &mut self,
881 position: DiffLinePosition,
882 cx: &mut Context<Self>,
883 ) {
884 let Some(anchor) = self.selection_anchor.clone() else {
885 return;
886 };
887 if !self.selecting
888 || !self.can_extend(&anchor, &position)
889 || self.selection_cursor.as_ref() == Some(&position)
890 || self.line_index(&position).is_none()
891 {
892 return;
893 }
894 self.select_user_range(anchor, position, cx);
895 }
896
897 fn can_extend(&self, anchor: &DiffLinePosition, position: &DiffLinePosition) -> bool {
901 anchor.path() == position.path()
902 && (anchor.side() == position.side() || self.mode == DiffMode::Unified)
903 }
904
905 pub(crate) fn end_line_selection(&mut self, cx: &mut Context<Self>) {
907 if !std::mem::take(&mut self.selecting) {
908 return;
909 }
910 if let Some(range) = self.selected_lines.clone() {
911 cx.emit(DiffEvent::SelectionEnded(range));
912 }
913 }
914
915 #[cfg(test)]
917 pub(crate) fn click_line(
918 &mut self,
919 position: DiffLinePosition,
920 extend: bool,
921 window: &mut Window,
922 cx: &mut Context<Self>,
923 ) {
924 self.begin_line_selection(position, extend, window, cx);
925 self.end_line_selection(cx);
926 }
927
928 pub(crate) fn keyboard_select(
929 &mut self,
930 direction: isize,
931 extend: bool,
932 window: &mut Window,
933 cx: &mut Context<Self>,
934 ) {
935 let Some((file, side)) = self.active_side(cx) else {
936 return;
937 };
938 let count = self.files[file].lines_count(side);
939 let ix = self
940 .selection_cursor
941 .as_ref()
942 .filter(|position| {
943 position.side() == side && self.file_ix(position.path()) == Some(file)
944 })
945 .and_then(|position| self.line_index(position))
946 .map(|ix| ix.saturating_add_signed(direction).min(count - 1))
947 .unwrap_or(0);
948 let position = self.position(file, side, ix);
949 let anchor = self
950 .selection_anchor
951 .clone()
952 .filter(|anchor| extend && anchor.path() == position.path())
953 .unwrap_or_else(|| position.clone());
954 TextSelection::clear(window, cx);
955 window.focus(&self.focus, cx);
956 let range = self.select_user_range(anchor, position.clone(), cx);
957 cx.emit(DiffEvent::SelectionEnded(range));
958 if let Some(row) = self.expand_line(&position) {
959 self.list.scroll_to_reveal_item(row);
960 }
961 }
962
963 pub(crate) fn select_all(&mut self, window: &mut Window, cx: &mut Context<Self>) {
964 let Some((file, side)) = self.active_side(cx) else {
965 return;
966 };
967 let count = self.files[file].lines_count(side);
968 TextSelection::clear(window, cx);
969 window.focus(&self.focus, cx);
970 let range = self.select_user_range(
971 self.position(file, side, 0),
972 self.position(file, side, count - 1),
973 cx,
974 );
975 cx.emit(DiffEvent::SelectionEnded(range));
976 }
977
978 pub(crate) fn selected_span(&self) -> Option<SelectedLines> {
980 let range = self.selected_lines.as_ref()?;
981 let file = self.file_ix(range.path())?;
982 if range.is_single_side() {
983 let (start, end) = (
984 range.start().min(range.end()),
985 range.start().max(range.end()),
986 );
987 return Some(SelectedLines::Side {
988 file,
989 side: range.side(),
990 lines: start..end + 1,
991 });
992 }
993 let document = &self.files[file];
994 let start = document.patch_ix(
995 range.side(),
996 document.line_index(range.side(), range.start())?,
997 );
998 let end = document.patch_ix(
999 range.end_side(),
1000 document.line_index(range.end_side(), range.end())?,
1001 );
1002 Some(SelectedLines::Patch {
1003 file,
1004 patch: start.min(end)..start.max(end) + 1,
1005 })
1006 }
1007
1008 pub(crate) fn set_window(&mut self, window: WindowId) {
1010 self.window = Some(window);
1011 }
1012
1013 pub(crate) fn update_layout(&mut self, key: LayoutKey, row_height: Pixels) -> bool {
1016 if self.layout.as_ref() == Some(&key) {
1017 return false;
1018 }
1019 if self.layout.is_some() {
1020 self.list.remeasure();
1021 }
1022 self.layout = Some(key);
1023 self.row_height = Some(row_height);
1024 self.apply_row_height_hint();
1025 true
1026 }
1027
1028 pub(crate) fn sync_annotations(&mut self, annotations: &[DiffAnnotation]) {
1032 let current = annotations
1033 .iter()
1034 .map(|annotation| {
1035 (
1036 annotation.position().cloned(),
1037 annotation.path().clone(),
1038 annotation.id().clone(),
1039 )
1040 })
1041 .collect::<HashSet<_>>();
1042 if current == self.annotations {
1043 return;
1044 }
1045 let rows = current
1046 .symmetric_difference(&self.annotations)
1047 .filter_map(|(position, path, _)| match position {
1048 Some(position) => self.source_row(position),
1049 None => self
1050 .file_ix(path)
1051 .and_then(|file| self.header_rows.get(file).copied()),
1052 })
1053 .collect::<Vec<_>>();
1054 for row in rows {
1055 self.list.remeasure_items(row..row + 1);
1056 }
1057 self.annotations = current;
1058 }
1059
1060 pub(crate) fn position(&self, file: usize, side: DiffSide, ix: usize) -> DiffLinePosition {
1061 let document = &self.files[file];
1062 DiffLinePosition::new(
1063 document.path().clone(),
1064 side,
1065 document.line_number(side, ix),
1066 )
1067 }
1068
1069 fn file_ix(&self, path: &str) -> Option<usize> {
1070 self.file_ixs.get(path).copied()
1071 }
1072
1073 fn line_index(&self, position: &DiffLinePosition) -> Option<usize> {
1074 self.files[self.file_ix(position.path())?].line_index(position.side(), position.line())
1075 }
1076
1077 fn clip_range(&self, range: &DiffLineRange) -> Option<DiffLineRange> {
1079 let file = &self.files[self.file_ix(range.path())?];
1080 let first_at_or_after = |side: DiffSide, line: usize| {
1081 let lines = file.lines(side);
1082 lines
1083 .get(lines.partition_point(|source| source.line_number() < line))
1084 .map(|source| source.line_number())
1085 };
1086 let last_at_or_before = |side: DiffSide, line: usize| {
1087 let lines = file.lines(side);
1088 lines
1089 .partition_point(|source| source.line_number() <= line)
1090 .checked_sub(1)
1091 .map(|ix| lines[ix].line_number())
1092 };
1093 if range.is_single_side() {
1094 let (start, end) = (
1095 range.start().min(range.end()),
1096 range.start().max(range.end()),
1097 );
1098 let start = first_at_or_after(range.side(), start)?;
1099 let end = last_at_or_before(range.side(), end)?;
1100 return (start <= end)
1101 .then(|| DiffLineRange::new(range.path().clone(), range.side(), start, end));
1102 }
1103 let start = first_at_or_after(range.side(), range.start())
1104 .or_else(|| last_at_or_before(range.side(), range.start()))?;
1105 let end = last_at_or_before(range.end_side(), range.end())
1106 .or_else(|| first_at_or_after(range.end_side(), range.end()))?;
1107 Some(
1108 DiffLineRange::new(range.path().clone(), range.side(), start, end)
1109 .with_end_side(range.end_side()),
1110 )
1111 }
1112
1113 fn selection_is_local(&self, cx: &App) -> bool {
1114 let entity = Some(self.selection.entity_id());
1115 self.selection
1116 .snapshot(cx)
1117 .is_some_and(|snapshot| snapshot.anchor().entity_id() == entity)
1118 }
1119
1120 fn active_side(&self, cx: &App) -> Option<(usize, DiffSide)> {
1122 let (file, side) = self
1123 .selection_cursor
1124 .as_ref()
1125 .and_then(|cursor| Some((self.file_ix(cursor.path())?, Some(cursor.side()))))
1126 .filter(|_| self.selected_lines.is_some())
1127 .or_else(|| {
1128 super::selection::selected_source_range(&self.selection, cx)
1129 .map(|(file, side, _)| (file, Some(side)))
1130 })
1131 .or_else(|| {
1132 let top = self.list.logical_scroll_top().item_ix;
1133 self.rows.get(top).map(|row| (row.file(), None))
1134 })?;
1135 let document = self.files.get(file)?;
1136 let side = side.unwrap_or(if document.lines_count(DiffSide::Modified) > 0 {
1137 DiffSide::Modified
1138 } else {
1139 DiffSide::Original
1140 });
1141 (document.lines_count(side) > 0).then_some((file, side))
1142 }
1143
1144 fn expand_line(&mut self, position: &DiffLinePosition) -> Option<usize> {
1145 let row = self.source_row(position)?;
1146 if let Some(DisplayRow::Fold { file, pairs }) = self.rows.get(row) {
1147 let (file, pairs) = (*file, pairs.clone());
1148 self.expanded[file].push(pairs);
1149 self.rebuild_at_anchor();
1150 }
1151 self.source_row(position)
1152 }
1153
1154 fn select_user_range(
1156 &mut self,
1157 anchor: DiffLinePosition,
1158 cursor: DiffLinePosition,
1159 cx: &mut Context<Self>,
1160 ) -> DiffLineRange {
1161 let range = range_between(&anchor, &cursor);
1162 let changed = self.selected_lines.as_ref() != Some(&range);
1163 self.selected_lines = Some(range.clone());
1164 self.selection_anchor = Some(anchor);
1165 self.selection_cursor = Some(cursor);
1166 self.selection.set_local_selection(true, cx);
1167 if changed {
1168 cx.emit(DiffEvent::SelectionChanged(Some(range.clone())));
1169 }
1170 cx.notify();
1171 range
1172 }
1173
1174 fn move_change(&mut self, forward: bool, cx: &mut Context<Self>) {
1175 let top = self.list.logical_scroll_top().item_ix;
1176 let groups = &self.change_rows;
1177 let target = if forward {
1178 let at_end = self
1183 .list
1184 .item_count()
1185 .checked_sub(1)
1186 .and_then(|last| self.list.bounds_for_item(last))
1187 .is_some_and(|last| last.bottom() <= self.list.viewport_bounds().bottom());
1188 groups
1189 .get(groups.partition_point(|ix| *ix <= top))
1190 .copied()
1191 .filter(|_| !at_end)
1192 .or(groups.first().copied())
1193 } else {
1194 groups
1195 .partition_point(|ix| *ix < top)
1196 .checked_sub(1)
1197 .and_then(|ix| groups.get(ix))
1198 .copied()
1199 .or(groups.last().copied())
1200 };
1201 if let Some(ix) = target {
1202 self.list.scroll_to(ListOffset {
1203 item_ix: ix,
1204 offset_in_item: px(0.),
1205 });
1206 cx.notify();
1207 }
1208 }
1209
1210 fn row_position(&self, ix: usize) -> Option<DiffLinePosition> {
1212 let row = self.rows.get(ix)?;
1213 let file = row.file();
1214 let pair_position = |original: Option<usize>, modified: Option<usize>| {
1215 modified
1216 .map(|ix| self.position(file, DiffSide::Modified, ix))
1217 .or_else(|| original.map(|ix| self.position(file, DiffSide::Original, ix)))
1218 };
1219 match row {
1220 DisplayRow::Code {
1221 original, modified, ..
1222 } => pair_position(*original, *modified),
1223 DisplayRow::Fold { pairs, .. } => self.files[file]
1224 .pairs()
1225 .get(pairs.start)
1226 .and_then(|pair| pair_position(pair.original(), pair.modified())),
1227 DisplayRow::File(_)
1228 | DisplayRow::Notice(_)
1229 | DisplayRow::Hunk { .. }
1230 | DisplayRow::Conflict { .. } => self
1231 .rows
1232 .get(ix + 1)
1233 .filter(|next| next.file() == file)
1234 .and_then(|_| self.row_position(ix + 1)),
1235 }
1236 }
1237
1238 pub(crate) fn source_row(&self, position: &DiffLinePosition) -> Option<usize> {
1239 let rows = self.file_rows.get(self.file_ix(position.path())?)?;
1240 let rows = match position.side() {
1241 DiffSide::Original => &rows.original,
1242 DiffSide::Modified => &rows.modified,
1243 };
1244 rows.get(self.line_index(position)?).copied().flatten()
1245 }
1246
1247 fn reveal_line(&self, position: &DiffLinePosition) {
1248 if let Some(item_ix) = self.source_row(position) {
1249 self.list.scroll_to(ListOffset {
1250 item_ix,
1251 offset_in_item: px(0.),
1252 });
1253 }
1254 }
1255
1256 fn rebuild_at_anchor(&mut self) {
1258 let anchor = self.row_position(self.list.logical_scroll_top().item_ix);
1259 self.rebuild(false);
1260 if let Some(anchor) = anchor {
1261 self.reveal_line(&anchor);
1262 }
1263 }
1264
1265 fn rebuild(&mut self, reset: bool) {
1268 let rows = self.project_rows();
1269 self.file_rows = source_rows(&self.files, &rows);
1270 self.change_rows = changed_groups(&rows);
1271 self.header_rows = rows
1272 .iter()
1273 .enumerate()
1274 .filter_map(|(ix, row)| matches!(row, DisplayRow::File(_)).then_some(ix))
1275 .collect();
1276 if reset {
1277 self.list.reset(rows.len());
1278 } else {
1279 let old = &self.rows;
1280 let prefix = old.iter().zip(&rows).take_while(|(a, b)| a == b).count();
1281 let suffix = old
1282 .iter()
1283 .rev()
1284 .zip(rows.iter().rev())
1285 .take(old.len().min(rows.len()) - prefix)
1286 .take_while(|(a, b)| a == b)
1287 .count();
1288 self.list
1289 .splice(prefix..old.len() - suffix, rows.len() - prefix - suffix);
1290 }
1291 self.rows = Rc::new(rows);
1292 self.apply_row_height_hint();
1293 }
1294
1295 fn project_rows(&self) -> Vec<DisplayRow> {
1296 let mut rows = Vec::new();
1297 for (ix, file) in self.files.iter().enumerate() {
1298 rows.push(DisplayRow::File(ix));
1299 if self.collapsed.contains(file.path()) {
1300 continue;
1301 }
1302 if file.pairs().is_empty() {
1303 rows.push(DisplayRow::Notice(ix));
1304 } else if file.status() == DiffFileStatus::Unchanged {
1305 rows.extend((0..file.pairs().len()).map(|line| DisplayRow::Code {
1306 file: ix,
1307 original: None,
1308 modified: Some(line),
1309 changed: false,
1310 }));
1311 } else if file.status() == DiffFileStatus::Conflicted {
1312 let resolutions = (0..file.conflicts().len())
1313 .map(|conflict| {
1314 self.resolutions
1315 .get(&(file.path().clone(), conflict))
1316 .copied()
1317 })
1318 .collect::<Vec<_>>();
1319 let visible = visible_pairs(
1320 file,
1321 self.context_lines,
1322 self.min_collapsed_lines,
1323 &self.expanded[ix],
1324 );
1325 project_conflicts(&mut rows, ix, file, &visible, &resolutions);
1326 } else {
1327 project_file(
1328 &mut rows,
1329 ix,
1330 file,
1331 self.mode,
1332 self.context_lines,
1333 self.min_collapsed_lines,
1334 &self.expanded[ix],
1335 );
1336 }
1337 }
1338 rows
1339 }
1340
1341 pub(crate) fn keep_row_height_hint(&mut self) -> bool {
1350 let width = self.list.viewport_bounds().size.width;
1351 if self.row_height.is_none() || self.hinted_width == Some(width) {
1352 return false;
1353 }
1354 self.hinted_width = Some(width);
1355 self.apply_row_height_hint();
1356 true
1357 }
1358
1359 fn apply_row_height_hint(&self) {
1362 if let Some(height) = self.row_height {
1363 let _ = self.list.clone().with_uniform_item_height(height);
1364 }
1365 }
1366
1367 fn reset_presentation(&mut self) {
1369 self.presentations = vec![None; self.files.len()];
1370 self.presentation_generation += 1;
1371 self.preparing = None;
1372 self._presentation = Task::ready(());
1373 }
1374
1375 pub(crate) fn ensure_presentation(&mut self, cx: &mut Context<Self>) {
1379 let generation = self.presentation_generation;
1380 if self.preparing == Some(generation) {
1381 return;
1382 }
1383 let pending = self
1384 .presentations
1385 .iter()
1386 .enumerate()
1387 .filter(|(_, presentation)| presentation.is_none())
1388 .map(|(ix, _)| (ix, self.files[ix].clone()))
1389 .collect::<Vec<_>>();
1390 if pending.is_empty() {
1391 return;
1392 }
1393 let options = self.presentation_options;
1394 self.preparing = Some(generation);
1395 self._presentation = cx.spawn(async move |this, cx| {
1396 let mut highlighters = Some(SyntaxHighlighters::default());
1397 for (ix, file) in pending {
1398 let mut moved = highlighters.take();
1399 let (presentation, returned) = cx
1400 .background_spawn(async move {
1401 let presentation = FilePresentation::prepare(
1402 &file,
1403 options,
1404 moved.get_or_insert_default(),
1405 );
1406 (presentation, moved)
1407 })
1408 .await;
1409 highlighters = returned;
1410 let current = this.update(cx, |state, cx| {
1411 if state.presentation_generation != generation {
1412 return false;
1413 }
1414 if let Some(slot) = state.presentations.get_mut(ix) {
1415 *slot = Some(Arc::new(presentation));
1416 }
1417 cx.notify();
1418 true
1419 });
1420 if !matches!(current, Ok(true)) {
1421 return;
1422 }
1423 }
1424 });
1425 }
1426}
1427
1428fn range_between(anchor: &DiffLinePosition, cursor: &DiffLinePosition) -> DiffLineRange {
1430 if anchor.side() == cursor.side() {
1431 DiffLineRange::new(
1432 cursor.path().clone(),
1433 cursor.side(),
1434 anchor.line().min(cursor.line()),
1435 anchor.line().max(cursor.line()),
1436 )
1437 } else {
1438 DiffLineRange::new(
1439 cursor.path().clone(),
1440 anchor.side(),
1441 anchor.line(),
1442 cursor.line(),
1443 )
1444 .with_end_side(cursor.side())
1445 }
1446}
1447
1448fn file_ixs(files: &[DiffFile]) -> HashMap<SharedString, usize> {
1449 let mut ixs = HashMap::with_capacity(files.len());
1450 for (ix, file) in files.iter().enumerate() {
1451 ixs.entry(file.path().clone()).or_insert(ix);
1452 }
1453 ixs
1454}
1455
1456fn source_rows(files: &[DiffFile], rows: &[DisplayRow]) -> Vec<FileRows> {
1460 let mut file_rows = files
1461 .iter()
1462 .map(|file| FileRows {
1463 original: vec![None; file.lines_count(DiffSide::Original)],
1464 modified: vec![None; file.lines_count(DiffSide::Modified)],
1465 })
1466 .collect::<Vec<_>>();
1467 for (row_ix, row) in rows.iter().enumerate() {
1468 let mut map = |file: usize, original: Option<usize>, modified: Option<usize>| {
1469 if let Some(line) = original {
1470 file_rows[file].original[line] = Some(row_ix);
1471 }
1472 if let Some(line) = modified {
1473 file_rows[file].modified[line] = Some(row_ix);
1474 }
1475 };
1476 match row {
1477 DisplayRow::Code {
1478 file,
1479 original,
1480 modified,
1481 ..
1482 } => map(*file, *original, *modified),
1483 DisplayRow::Fold { file, pairs } => {
1484 for pair in &files[*file].pairs()[pairs.clone()] {
1485 map(*file, pair.original(), pair.modified());
1486 }
1487 }
1488 DisplayRow::File(_)
1489 | DisplayRow::Notice(_)
1490 | DisplayRow::Hunk { .. }
1491 | DisplayRow::Conflict { .. } => {}
1492 }
1493 }
1494 file_rows
1495}
1496
1497fn changed_groups(rows: &[DisplayRow]) -> Vec<usize> {
1498 let changed = |row: &DisplayRow| matches!(row, DisplayRow::Code { changed: true, .. });
1499 rows.iter()
1500 .enumerate()
1501 .filter_map(|(ix, row)| {
1502 (changed(row)
1503 && (ix == 0 || !changed(&rows[ix - 1]) || rows[ix - 1].file() != row.file()))
1504 .then_some(ix)
1505 })
1506 .collect()
1507}
1508
1509const SIDE_BIT: u64 = 1 << 40;
1510const OFFSET_MASK: u64 = SIDE_BIT - 1;
1511
1512pub(crate) fn encode_key(file: usize, side: DiffSide, offset: usize) -> u64 {
1514 ((file as u64) << 41)
1515 | if side == DiffSide::Modified {
1516 SIDE_BIT
1517 } else {
1518 0
1519 }
1520 | (offset as u64 & OFFSET_MASK)
1521}
1522pub(crate) fn decode_key(key: u64) -> (usize, DiffSide, usize) {
1523 (
1524 (key >> 41) as usize,
1525 if key & SIDE_BIT == 0 {
1526 DiffSide::Original
1527 } else {
1528 DiffSide::Modified
1529 },
1530 (key & OFFSET_MASK) as usize,
1531 )
1532}
1533
1534fn hunk_ends(document: &DiffFile) -> Vec<usize> {
1536 let pairs = document.pairs();
1537 let mut hunk_end = vec![pairs.len(); pairs.len()];
1538 for hunk in document.hunks() {
1539 hunk_end[hunk.pairs()].fill(hunk.pairs().end);
1540 }
1541 hunk_end
1542}
1543
1544fn visible_pairs(
1547 document: &DiffFile,
1548 context: Option<usize>,
1549 min_collapsed_lines: usize,
1550 expanded: &[Range<usize>],
1551) -> Vec<bool> {
1552 let pairs = document.pairs();
1553 let hunks = document.hunks();
1554 let mut hunk_end = vec![pairs.len(); pairs.len()];
1555 let mut hunk_start = vec![0; pairs.len()];
1556 for hunk in hunks {
1557 hunk_start[hunk.pairs()].fill(hunk.pairs().start);
1558 hunk_end[hunk.pairs()].fill(hunk.pairs().end);
1559 }
1560 let mut boundaries = vec![0isize; pairs.len() + 1];
1563 if let Some(context) = context {
1564 let mut ix = 0;
1565 while ix < pairs.len() {
1566 if !pairs[ix].is_changed() {
1567 ix += 1;
1568 continue;
1569 }
1570 let start = ix;
1571 while ix < pairs.len() && ix < hunk_end[start] && pairs[ix].is_changed() {
1572 ix += 1;
1573 }
1574 boundaries[start.saturating_sub(context).max(hunk_start[start])] += 1;
1575 boundaries[ix.saturating_add(context).min(hunk_end[start])] -= 1;
1576 }
1577 } else {
1578 boundaries[0] += 1;
1579 boundaries[pairs.len()] -= 1;
1580 }
1581 for range in expanded {
1582 let start = range.start.min(pairs.len());
1583 let end = range.end.min(pairs.len());
1584 if start < end {
1585 boundaries[start] += 1;
1586 boundaries[end] -= 1;
1587 }
1588 }
1589 let mut active = 0;
1590 let mut visible = boundaries[..pairs.len()]
1591 .iter()
1592 .map(|boundary| {
1593 active += boundary;
1594 active > 0
1595 })
1596 .collect::<Vec<_>>();
1597 let mut ix = 0;
1599 while ix < pairs.len() {
1600 if visible[ix] {
1601 ix += 1;
1602 continue;
1603 }
1604 let start = ix;
1605 while ix < pairs.len() && ix < hunk_end[start] && !visible[ix] {
1606 ix += 1;
1607 }
1608 if ix - start < min_collapsed_lines {
1609 visible[start..ix].fill(true);
1610 }
1611 }
1612 visible
1613}
1614
1615fn project_file(
1616 rows: &mut Vec<DisplayRow>,
1617 file: usize,
1618 document: &DiffFile,
1619 mode: DiffMode,
1620 context: Option<usize>,
1621 min_collapsed_lines: usize,
1622 expanded: &[Range<usize>],
1623) {
1624 let pairs = document.pairs();
1625 let hunks = document.hunks();
1626 let hunk_end = hunk_ends(document);
1627 let visible = visible_pairs(document, context, min_collapsed_lines, expanded);
1628 let mut hunk_ix = 0;
1629 let mut ix = 0;
1630 while ix < pairs.len() {
1631 while let Some(hunk) = hunks.get(hunk_ix) {
1632 if hunk.pairs().start != ix {
1633 break;
1634 }
1635 rows.push(DisplayRow::Hunk {
1636 file,
1637 hunk: hunk_ix,
1638 });
1639 hunk_ix += 1;
1640 }
1641 if !visible[ix] {
1642 let start = ix;
1643 while ix < pairs.len() && ix < hunk_end[start] && !visible[ix] {
1644 ix += 1;
1645 }
1646 rows.push(DisplayRow::Fold {
1647 file,
1648 pairs: start..ix,
1649 });
1650 } else if mode == DiffMode::Unified && pairs[ix].is_changed() {
1651 let start = ix;
1652 while ix < pairs.len() && ix < hunk_end[start] && pairs[ix].is_changed() {
1653 ix += 1;
1654 }
1655 for pair in &pairs[start..ix] {
1656 if let Some(original) = pair.original() {
1657 rows.push(DisplayRow::Code {
1658 file,
1659 original: Some(original),
1660 modified: None,
1661 changed: true,
1662 });
1663 }
1664 }
1665 for pair in &pairs[start..ix] {
1666 if let Some(modified) = pair.modified() {
1667 rows.push(DisplayRow::Code {
1668 file,
1669 original: None,
1670 modified: Some(modified),
1671 changed: true,
1672 });
1673 }
1674 }
1675 } else {
1676 let pair = pairs[ix];
1677 rows.push(DisplayRow::Code {
1678 file,
1679 original: pair.original(),
1680 modified: pair.modified(),
1681 changed: pair.is_changed(),
1682 });
1683 ix += 1;
1684 }
1685 }
1686}
1687
1688fn project_conflicts(
1691 rows: &mut Vec<DisplayRow>,
1692 file: usize,
1693 document: &DiffFile,
1694 visible: &[bool],
1695 resolutions: &[Option<DiffConflictResolution>],
1696) {
1697 let code = |line: usize, changed: bool| DisplayRow::Code {
1699 file,
1700 original: None,
1701 modified: Some(line),
1702 changed,
1703 };
1704 let conflicts = document.conflicts();
1705 let mut next = 0;
1706 let mut ix = 0;
1707 while ix < visible.len() {
1708 if let Some(conflict) = conflicts.get(next)
1709 && conflict.source_lines().start == ix
1710 {
1711 match resolutions[next] {
1712 None => {
1713 for part in [
1714 ConflictPart::Current,
1715 ConflictPart::Base,
1716 ConflictPart::Incoming,
1717 ] {
1718 if let Some(lines) = conflict.part(part) {
1719 rows.push(DisplayRow::Conflict {
1720 file,
1721 conflict: next,
1722 part,
1723 });
1724 rows.extend(lines.map(|line| code(line, true)));
1725 }
1726 }
1727 }
1728 Some(resolution) => {
1729 rows.push(DisplayRow::Conflict {
1730 file,
1731 conflict: next,
1732 part: ConflictPart::Current,
1733 });
1734 for lines in conflict.kept(resolution) {
1735 rows.extend(lines.map(|line| code(line, false)));
1736 }
1737 }
1738 }
1739 ix = conflict.source_lines().end;
1740 next += 1;
1741 continue;
1742 }
1743 if visible[ix] {
1744 rows.push(code(ix, false));
1745 ix += 1;
1746 continue;
1747 }
1748 let start = ix;
1749 while ix < visible.len() && !visible[ix] {
1750 ix += 1;
1751 }
1752 rows.push(DisplayRow::Fold {
1753 file,
1754 pairs: start..ix,
1755 });
1756 }
1757}