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