Skip to main content

gpui_component/diff/
state.rs

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/// One virtualized row. Every row belongs to exactly one file.
27#[derive(Clone, Debug, PartialEq, Eq)]
28pub(crate) enum DisplayRow {
29    /// The file header, with any file-level annotations.
30    File(usize),
31    /// Binary, metadata-only or empty file summary, for a file without source rows.
32    Notice(usize),
33    Hunk {
34        file: usize,
35        hunk: usize,
36    },
37    Fold {
38        file: usize,
39        pairs: Range<usize>,
40    },
41    /// The heading of one part of a merge conflict; for a resolved conflict,
42    /// the [`ConflictPart::Current`] heading stands for the whole conflict.
43    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/// Which part of a collapsed unchanged range a disclosure reveals.
69#[derive(Clone, Copy, Debug, PartialEq, Eq)]
70pub(crate) enum FoldExpansion {
71    /// The lines just above the following source.
72    Up,
73    /// The lines just below the preceding source.
74    Down,
75    All,
76}
77
78/// Notifications from the readonly comparison surface.
79#[derive(Clone, Debug)]
80#[non_exhaustive]
81pub enum DiffEvent {
82    /// A pointer gesture began selecting source lines from a line number.
83    SelectionStarted(DiffLineRange),
84    /// A pointer, gutter or keyboard operation changed the selected source lines.
85    SelectionChanged(Option<DiffLineRange>),
86    /// A selection gesture finished; the range is ready to act on.
87    SelectionEnded(DiffLineRange),
88    /// The user expanded a collapsed file from its header.
89    FileExpanded(SharedString),
90    /// The user collapsed a file from its header.
91    FileCollapsed(SharedString),
92    /// The user resolved a merge conflict, or undid its resolution; read it
93    /// with [`DiffState::conflict_resolution`].
94    ConflictResolved(SharedString, usize),
95}
96
97/// Inputs that determine row heights and content width.
98#[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/// The row showing each source line, or the disclosure hiding it.
129#[derive(Default)]
130struct FileRows {
131    original: Vec<Option<usize>>,
132    modified: Vec<Option<usize>>,
133}
134
135/// The selected source lines of one file, resolved for membership tests.
136#[derive(Clone, Debug, PartialEq, Eq)]
137pub(crate) enum SelectedLines {
138    /// Lines numbered `lines` on one side.
139    Side {
140        file: usize,
141        side: DiffSide,
142        lines: Range<usize>,
143    },
144    /// Patch lines in `patch` order, across both sides.
145    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    /// The last selected line, where a selection's add-annotation button sits.
174    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
199/// Retained focus, viewport, context expansion and selection for a
200/// [`super::Diff`] showing one or more files of a patch.
201///
202/// Create once in the owning view. Mutations notify observers; the application
203/// owns the patch and chooses when to install new files. Syntax and inline
204/// emphasis are prepared on a background thread and appear once ready.
205pub struct DiffState {
206    files: Vec<DiffFile>,
207    /// Each path's index in `files`; the first file wins a duplicated path.
208    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    /// The list width the row-height hint was last applied at.
237    hinted_width: Option<Pixels>,
238    annotations: HashSet<(Option<DiffLinePosition>, SharedString, ElementId)>,
239    window: Option<WindowId>,
240    /// Bumped when files or presentation options change, so a preparation
241    /// started for earlier ones never installs its results.
242    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    /// Creates a unified viewer with three unchanged lines around each change.
257    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                                // A new text gesture ends the previous gutter anchor.
313                                state.selection_anchor = None;
314                                state.selection_cursor = None;
315                                if changed {
316                                    cx.emit(DiffEvent::SelectionChanged(None));
317                                }
318                            }
319                            TextSelectionEvent::Cleared => {
320                                // A replacement gutter gesture has local selection and
321                                // skips this branch. Keep its anchor for Shift-click,
322                                // but report a clear that leaves no replacement range.
323                                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    /// Sets the initial display mode. Default is [`DiffMode::Unified`].
383    pub fn with_mode(mut self, mode: DiffMode) -> Self {
384        self.mode = mode;
385        self.rebuild(true);
386        self
387    }
388
389    /// Sets the initial unchanged context around each change. Default is
390    /// `Some(3)`; `None` shows every supplied patch line.
391    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    /// Sets how many lines an up or down disclosure reveals. Default is 20.
398    pub fn with_expansion_lines(mut self, lines: usize) -> Self {
399        self.expansion_lines = lines.max(1);
400        self
401    }
402
403    /// Sets the shortest unchanged range that collapses; shorter ranges stay
404    /// visible. Default is 2.
405    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    /// Sets the unit inline changes between paired lines are emphasized in,
412    /// or `None` to emphasize whole lines only. Default is [`DiffInlineUnit::Word`].
413    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    /// Sets the longest line, in bytes, compared for inline changes. Default is 1,000.
420    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    /// Sets the longest line, in bytes, given syntax emphasis. Default is 1,000.
427    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    /// Highlights syntax with the application's highlighter instead of GPUI
434    /// Kit's. Given a file's [language](DiffFile::language) and the source
435    /// text of one side of one hunk, it returns byte ranges into that text
436    /// with their styles; a range that splits a character is ignored.
437    ///
438    /// It runs on the background executor, once for each side of each hunk,
439    /// so an unterminated comment in one hunk cannot color the next. Styles
440    /// are kept as returned rather than resolved against the theme when a line
441    /// paints; set a new highlighter when its colors change.
442    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    /// Replaces the application's highlighter, preparing syntax again without
452    /// resetting selection or scrolling. See [`Self::with_syntax_highlighter`].
453    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    /// Returns to GPUI Kit's highlighter, preparing syntax again without
461    /// resetting selection or scrolling.
462    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    /// Changes the number of lines each disclosure reveals, preserving selection and scrolling.
478    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    /// Changes the shortest unchanged range that collapses, preserving selection and scrolling.
488    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    /// Changes the inline comparison unit without resetting selection or scrolling.
499    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    /// Changes the inline comparison line limit in bytes without resetting selection or scrolling.
510    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    /// Changes the syntax emphasis line limit in bytes without resetting selection or scrolling.
521    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    /// The files in patch order.
532    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    /// Whether the file at `path` shows only its header.
560    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    /// The body's size as of the last paint.
583    pub(crate) fn viewport(&self) -> Size<Pixels> {
584        self.viewport
585    }
586    /// The width rows need to show their longest line.
587    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    /// Records the painted body size and widest painted row. Returns whether
594    /// either changed, so the frame must be laid out again.
595    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    /// Replaces the files and resets expansion, selection and viewport.
606    /// Collapsed files stay collapsed when their path is still present.
607    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    /// Changes the display mode while preserving source-line selection and a
636    /// nearby source row.
637    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    /// Sets surrounding context. `None` shows every supplied patch line.
648    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    /// Reveals all unchanged source while preserving the configured context count.
659    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    /// Restores the configured context disclosures.
668    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    /// Collapses the file at `path` to its header, or expands it, without
675    /// emitting a user event.
676    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    /// The resolution of conflict `ix` in the file at `path`, if any.
693    /// Use [`DiffFile::conflicts`] to enumerate valid indices; `None` means
694    /// unresolved, or that the file or index does not exist.
695    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    /// Resolves conflict `ix` (an index in [`DiffFile::conflicts`]) in the file
701    /// at `path`, or clears its resolution
702    /// with `None`, without emitting a user event.
703    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    /// The working file with every conflict replaced by its resolution, or
728    /// `None` while a conflict is unresolved or `path` names no conflicted file.
729    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    /// Synchronizes source-line selection without emitting a user event.
757    /// Clips to supplied patch lines; a range containing none clears selection.
758    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    /// Returns selected source without gutters, diff markers or annotation content.
771    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                        // An unchanged line reads the same on both sides.
784                        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                        // A side's unterminated final line can precede the
792                        // other side's lines in patch order.
793                        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    /// Reveals and scrolls to a source line, expanding its file and hidden
815    /// context if needed.
816    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    /// Scrolls to the header of the file at `path`, a [`DiffFile::path`].
830    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    /// Moves to the next changed group after the current viewport, across files,
845    /// wrapping to the first once the list is scrolled to its end.
846    pub fn next_change(&mut self, cx: &mut Context<Self>) {
847        self.move_change(true, cx);
848    }
849    /// Moves to the previous changed group before the current viewport, across files.
850    pub fn previous_change(&mut self, cx: &mut Context<Self>) {
851        self.move_change(false, cx);
852    }
853
854    /// Reveals part or all of a collapsed unchanged range.
855    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    /// Resolves a conflict from its heading and reports the user's change.
876    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    /// Toggles a file from its header and reports the user's change.
889    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    /// Starts a line-number selection gesture, extending the current anchor in
901    /// the same file when `extend` is set.
902    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    /// Extends an active line-number gesture to `position` in the same file.
927    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    /// Whether a selection anchored at `anchor` may extend to `position`:
946    /// within one file, and across sides only in Unified mode, whose rows
947    /// follow patch order.
948    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    /// Finishes an active line-number gesture.
954    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    /// Selects one line, or extends to it, as a complete gesture.
964    #[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    /// The selected lines, resolved for membership tests.
1027    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    /// Records the window that renders this state, for clearing its text selection.
1057    pub(crate) fn set_window(&mut self, window: WindowId) {
1058        self.window = Some(window);
1059    }
1060
1061    /// Updates width and row-height measurement when fonts or layout change.
1062    /// Returns whether content width must be measured again.
1063    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    /// Re-measures rows whose annotations were added, removed or replaced.
1077    /// Visible rows are re-measured every frame, so content changes inside an
1078    /// annotation need no notification.
1079    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    /// Clips a range to supplied lines, normalizing a single-side range.
1126    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    /// The file and side that keyboard selection and Select All act on.
1169    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    /// Selects from `anchor` to `cursor` in one file and reports a change.
1203    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            // Scrolled to the end, the list clamps any later change below the
1227            // top, so the top never passes it and the key would stop there.
1228            // The last row's bounds answer even when the rows above the
1229            // viewport were never measured, as after a jump to the end.
1230            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    /// The source line shown by a row, looking past headers within its file.
1259    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    /// Rebuilds rows and restores the source line at the top of the viewport.
1305    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    /// Projects rows. Without `reset`, only the changed middle is spliced so
1314    /// measured heights of unchanged rows survive disclosure changes.
1315    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    /// Applies the row-height hint again once the list has laid out at a new
1390    /// width. Returns whether it did, so the frame must be laid out again.
1391    ///
1392    /// A list that lays out at a new width forgets every row height, hints
1393    /// included, which happens on its first layout too. The patch then
1394    /// measures only the rows rendered so far, and scrolling stops once the
1395    /// last of them reaches the bottom of the viewport. Call this after the
1396    /// list's prepaint, so the hint is back before the next wheel event.
1397    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    /// Gives unmeasured rows a code-row height so the scrollbar is sized for
1408    /// the whole patch before every row has been rendered.
1409    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    /// Discards prepared emphasis after files or options change.
1416    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    /// Prepares syntax and inline emphasis for files that lack it, one file at
1424    /// a time on the background executor, reusing highlighters per language.
1425    /// Starts on first render, so builders applied after `new` take effect.
1426    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
1479/// The range from `anchor` to `cursor`, normalized when both are on one side.
1480fn 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
1507/// Every source line maps to its code row or the disclosure hiding it; lines
1508/// of a collapsed file have no row. Disjoint folds visit each pair once, so
1509/// building the maps is linear.
1510fn 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
1563/// Packs a source offset into a selection key, ordered by file, side and offset.
1564pub(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
1585/// The end of the hunk containing each pair; a fixture without hunks is one.
1586fn 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
1595/// Which pairs show as code: changes with their context, expanded ranges,
1596/// and collapsed ranges shorter than `min_collapsed_lines`.
1597fn 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    // Interval boundaries avoid repeatedly filling overlapping context windows.
1612    // Projection remains linear in source lines plus the number of disclosures.
1613    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    // A disclosure shorter than the threshold would hide almost nothing.
1649    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
1739/// Projects a conflicted working file: shared lines fold like context, and
1740/// each conflict shows its parts, or only the kept lines once resolved.
1741fn project_conflicts(
1742    rows: &mut Vec<DisplayRow>,
1743    file: usize,
1744    document: &DiffFile,
1745    visible: &[bool],
1746    resolutions: &[Option<DiffConflictResolution>],
1747) {
1748    // Each line of a conflicted file is its own pair.
1749    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}