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