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, ListAlignment,
11    ListOffset, ListState, Pixels, ScrollHandle, SharedString, Size, Subscription, Task, Window,
12    WindowId, point, px,
13};
14use gpui_base::{TextSelection, TextSelectionContentKey, TextSelectionEvent, TextSelectionHandle};
15
16use super::{
17    DiffAnnotation, DiffConflictResolution, DiffFile, DiffFileStatus, DiffInlineUnit,
18    DiffLinePosition, DiffLineRange, DiffMode, DiffSide,
19    conflict::ConflictPart,
20    presentation::{FilePresentation, PresentationOptions, SyntaxHighlighters},
21    selection::SelectionGeometry,
22};
23
24/// One virtualized row. Every row belongs to exactly one file.
25#[derive(Clone, Debug, PartialEq, Eq)]
26pub(crate) enum DisplayRow {
27    /// The file header, with any file-level annotations.
28    File(usize),
29    /// Binary, metadata-only or empty file summary, for a file without source rows.
30    Notice(usize),
31    Hunk {
32        file: usize,
33        hunk: usize,
34    },
35    Fold {
36        file: usize,
37        pairs: Range<usize>,
38    },
39    /// The heading of one part of a merge conflict; for a resolved conflict,
40    /// the [`ConflictPart::Current`] heading stands for the whole conflict.
41    Conflict {
42        file: usize,
43        conflict: usize,
44        part: ConflictPart,
45    },
46    Code {
47        file: usize,
48        original: Option<usize>,
49        modified: Option<usize>,
50        changed: bool,
51    },
52}
53
54impl DisplayRow {
55    pub(crate) fn file(&self) -> usize {
56        match self {
57            Self::File(file) | Self::Notice(file) => *file,
58            Self::Hunk { file, .. }
59            | Self::Fold { file, .. }
60            | Self::Conflict { file, .. }
61            | Self::Code { file, .. } => *file,
62        }
63    }
64}
65
66/// Which part of a collapsed unchanged range a disclosure reveals.
67#[derive(Clone, Copy, Debug, PartialEq, Eq)]
68pub(crate) enum FoldExpansion {
69    /// The lines just above the following source.
70    Up,
71    /// The lines just below the preceding source.
72    Down,
73    All,
74}
75
76/// Notifications from the readonly comparison surface.
77#[derive(Clone, Debug)]
78#[non_exhaustive]
79pub enum DiffEvent {
80    /// A pointer gesture began selecting source lines from a line number.
81    SelectionStarted(DiffLineRange),
82    /// A pointer, gutter or keyboard operation changed the selected source lines.
83    SelectionChanged(Option<DiffLineRange>),
84    /// A selection gesture finished; the range is ready to act on.
85    SelectionEnded(DiffLineRange),
86    /// The user expanded a collapsed file from its header.
87    FileExpanded(SharedString),
88    /// The user collapsed a file from its header.
89    FileCollapsed(SharedString),
90    /// The user resolved a merge conflict, or undid its resolution; read it
91    /// with [`DiffState::conflict_resolution`].
92    ConflictResolved(SharedString, usize),
93}
94
95/// Inputs that determine row heights and content width.
96#[derive(Clone, Debug, PartialEq)]
97pub(crate) struct LayoutKey {
98    rem: Pixels,
99    font_size: Pixels,
100    font_family: SharedString,
101    mode: DiffMode,
102    line_number: bool,
103    soft_wrap: bool,
104}
105
106impl LayoutKey {
107    pub(crate) fn new(
108        rem: Pixels,
109        font_size: Pixels,
110        font_family: SharedString,
111        mode: DiffMode,
112        line_number: bool,
113        soft_wrap: bool,
114    ) -> Self {
115        Self {
116            rem,
117            font_size,
118            font_family,
119            mode,
120            line_number,
121            soft_wrap,
122        }
123    }
124}
125
126/// The row showing each source line, or the disclosure hiding it.
127#[derive(Default)]
128struct FileRows {
129    original: Vec<Option<usize>>,
130    modified: Vec<Option<usize>>,
131}
132
133/// The selected source lines of one file, resolved for membership tests.
134#[derive(Clone, Debug, PartialEq, Eq)]
135pub(crate) enum SelectedLines {
136    /// Lines numbered `lines` on one side.
137    Side {
138        file: usize,
139        side: DiffSide,
140        lines: Range<usize>,
141    },
142    /// Patch lines in `patch` order, across both sides.
143    Patch { file: usize, patch: Range<usize> },
144}
145
146impl SelectedLines {
147    pub(crate) fn contains(
148        &self,
149        files: &[DiffFile],
150        file: usize,
151        side: DiffSide,
152        ix: usize,
153    ) -> bool {
154        match self {
155            Self::Side {
156                file: selected,
157                side: selected_side,
158                lines,
159            } => {
160                *selected == file
161                    && *selected_side == side
162                    && lines.contains(&files[file].line_number(side, ix))
163            }
164            Self::Patch {
165                file: selected,
166                patch,
167            } => *selected == file && patch.contains(&files[file].patch_ix(side, ix)),
168        }
169    }
170
171    /// The last selected line, where a selection's add-annotation button sits.
172    pub(crate) fn is_last(
173        &self,
174        files: &[DiffFile],
175        file: usize,
176        side: DiffSide,
177        ix: usize,
178    ) -> bool {
179        match self {
180            Self::Side {
181                file: selected,
182                side: selected_side,
183                lines,
184            } => {
185                *selected == file
186                    && *selected_side == side
187                    && files[file].line_number(side, ix) + 1 == lines.end
188            }
189            Self::Patch {
190                file: selected,
191                patch,
192            } => *selected == file && files[file].patch_ix(side, ix) + 1 == patch.end,
193        }
194    }
195}
196
197/// Retained focus, viewport, context expansion and selection for a
198/// [`super::Diff`] showing one or more files of a patch.
199///
200/// Create once in the owning view. Mutations notify observers; the application
201/// owns the patch and chooses when to install new files. Syntax and inline
202/// emphasis are prepared on a background thread and appear once ready.
203pub struct DiffState {
204    files: Vec<DiffFile>,
205    /// Each path's index in `files`; the first file wins a duplicated path.
206    file_ixs: HashMap<SharedString, usize>,
207    mode: DiffMode,
208    context_lines: Option<usize>,
209    expansion_lines: usize,
210    min_collapsed_lines: usize,
211    expanded: Vec<Vec<Range<usize>>>,
212    collapsed: HashSet<SharedString>,
213    resolutions: HashMap<(SharedString, usize), DiffConflictResolution>,
214    file_rows: Vec<FileRows>,
215    header_rows: Vec<usize>,
216    rows: Rc<Vec<DisplayRow>>,
217    change_rows: Vec<usize>,
218    list: ListState,
219    horizontal_scroll: ScrollHandle,
220    focus: FocusHandle,
221    selection: TextSelectionHandle,
222    geometry: Rc<RefCell<SelectionGeometry>>,
223    selected_lines: Option<DiffLineRange>,
224    selection_anchor: Option<DiffLinePosition>,
225    selection_cursor: Option<DiffLinePosition>,
226    selecting: bool,
227    presentation_options: PresentationOptions,
228    presentations: Vec<Option<Arc<FilePresentation>>>,
229    viewport: Size<Pixels>,
230    layout: Option<LayoutKey>,
231    content_width: Pixels,
232    row_height: Option<Pixels>,
233    /// The list width the row-height hint was last applied at.
234    hinted_width: Option<Pixels>,
235    annotations: HashSet<(Option<DiffLinePosition>, SharedString, ElementId)>,
236    window: Option<WindowId>,
237    /// Bumped when files or presentation options change, so a preparation
238    /// started for earlier ones never installs its results.
239    presentation_generation: usize,
240    preparing: Option<usize>,
241    _presentation: Task<()>,
242    _selection_subscription: Subscription,
243}
244
245impl EventEmitter<DiffEvent> for DiffState {}
246impl Focusable for DiffState {
247    fn focus_handle(&self, _: &App) -> FocusHandle {
248        self.focus.clone()
249    }
250}
251
252impl DiffState {
253    /// Creates a unified viewer with three unchanged lines around each change.
254    pub fn new(files: impl IntoIterator<Item = DiffFile>, cx: &mut Context<Self>) -> Self {
255        let focus = cx.focus_handle().tab_stop(true);
256        let selection = TextSelectionHandle::new("", cx);
257        let geometry = Rc::new(RefCell::new(SelectionGeometry::default()));
258        let weak = cx.entity().downgrade();
259        selection.resolve_content_key_with(
260            {
261                let weak = weak.clone();
262                let geometry = geometry.clone();
263                move |point, cx| {
264                    let state = weak.upgrade()?;
265                    let snapshot = state.read(cx).selection.snapshot(cx);
266                    let entity = Some(state.read(cx).selection.entity_id());
267                    let snapshot =
268                        snapshot.filter(|snapshot| snapshot.anchor().entity_id() == entity);
269                    let side = snapshot
270                        .and_then(|snapshot| snapshot.anchor().content_key())
271                        .map(|key| decode_key(key.value()).1);
272                    geometry
273                        .borrow()
274                        .key_at(point, side)
275                        .map(TextSelectionContentKey::new)
276                        .or_else(|| {
277                            snapshot
278                                .filter(|snapshot| snapshot.cursor().entity_id() == entity)
279                                .and_then(|snapshot| snapshot.cursor().content_key())
280                        })
281                }
282            },
283            cx,
284        );
285        selection.copy_with(
286            {
287                let weak = weak.clone();
288                move |cx| {
289                    weak.upgrade()
290                        .map_or_else(String::new, |state| state.read(cx).selected_text(cx))
291                }
292            },
293            cx,
294        );
295        selection.focus_with(
296            {
297                let focus = focus.clone();
298                move |window, cx| window.focus(&focus, cx)
299            },
300            cx,
301        );
302        let _selection_subscription = selection.subscribe(
303            move |event, cx| {
304                let _ = weak.update(cx, |state, cx| {
305                    if !state.selection.has_local_selection(cx) {
306                        match event {
307                            TextSelectionEvent::SelectionChanged(Some(_)) => {
308                                let changed = state.selected_lines.take().is_some();
309                                // A new text gesture ends the previous gutter anchor.
310                                state.selection_anchor = None;
311                                state.selection_cursor = None;
312                                if changed {
313                                    cx.emit(DiffEvent::SelectionChanged(None));
314                                }
315                            }
316                            TextSelectionEvent::Cleared => {
317                                // A replacement gutter gesture has local selection and
318                                // skips this branch. Keep its anchor for Shift-click,
319                                // but report a clear that leaves no replacement range.
320                                if state.selected_lines.take().is_some() {
321                                    cx.emit(DiffEvent::SelectionChanged(None));
322                                }
323                            }
324                            _ => {}
325                        }
326                    }
327                    cx.notify();
328                });
329            },
330            cx,
331        );
332        let files = files.into_iter().collect::<Vec<_>>();
333        let mut state = Self {
334            expanded: vec![Vec::new(); files.len()],
335            presentations: vec![None; files.len()],
336            file_ixs: file_ixs(&files),
337            files,
338            mode: DiffMode::Unified,
339            context_lines: Some(3),
340            expansion_lines: 20,
341            min_collapsed_lines: 2,
342            collapsed: HashSet::new(),
343            resolutions: HashMap::new(),
344            file_rows: Vec::new(),
345            header_rows: Vec::new(),
346            rows: Rc::default(),
347            change_rows: Vec::new(),
348            list: ListState::new(0, ListAlignment::Top, px(0.)),
349            horizontal_scroll: ScrollHandle::new(),
350            focus,
351            selection,
352            geometry,
353            selected_lines: None,
354            selection_anchor: None,
355            selection_cursor: None,
356            selecting: false,
357            presentation_options: PresentationOptions {
358                inline_unit: Some(DiffInlineUnit::Word),
359                inline_max_line_length: 1000,
360                syntax_max_line_length: 1000,
361            },
362            viewport: Size::default(),
363            layout: None,
364            content_width: px(0.),
365            row_height: None,
366            hinted_width: None,
367            annotations: HashSet::new(),
368            window: None,
369            presentation_generation: 0,
370            preparing: None,
371            _presentation: Task::ready(()),
372            _selection_subscription,
373        };
374        state.rebuild(true);
375        state
376    }
377
378    /// Sets the initial display mode. Default is [`DiffMode::Unified`].
379    pub fn with_mode(mut self, mode: DiffMode) -> Self {
380        self.mode = mode;
381        self.rebuild(true);
382        self
383    }
384
385    /// Sets the initial unchanged context around each change. Default is
386    /// `Some(3)`; `None` shows every supplied patch line.
387    pub fn with_context_lines(mut self, lines: Option<usize>) -> Self {
388        self.context_lines = lines;
389        self.rebuild(true);
390        self
391    }
392
393    /// Sets how many lines an up or down disclosure reveals. Default is 20.
394    pub fn with_expansion_lines(mut self, lines: usize) -> Self {
395        self.expansion_lines = lines.max(1);
396        self
397    }
398
399    /// Sets the shortest unchanged range that collapses; shorter ranges stay
400    /// visible. Default is 2.
401    pub fn with_min_collapsed_lines(mut self, lines: usize) -> Self {
402        self.min_collapsed_lines = lines.max(1);
403        self.rebuild(true);
404        self
405    }
406
407    /// Sets the unit inline changes between paired lines are emphasized in,
408    /// or `None` to emphasize whole lines only. Default is [`DiffInlineUnit::Word`].
409    pub fn with_inline_unit(mut self, unit: Option<DiffInlineUnit>) -> Self {
410        self.presentation_options.inline_unit = unit;
411        self.reset_presentation();
412        self
413    }
414
415    /// Sets the longest line, in bytes, compared for inline changes. Default is 1,000.
416    pub fn with_inline_max_line_length(mut self, length: usize) -> Self {
417        self.presentation_options.inline_max_line_length = length;
418        self.reset_presentation();
419        self
420    }
421
422    /// Sets the longest line, in bytes, given syntax emphasis. Default is 1,000.
423    pub fn with_syntax_max_line_length(mut self, length: usize) -> Self {
424        self.presentation_options.syntax_max_line_length = length;
425        self.reset_presentation();
426        self
427    }
428
429    /// Changes the number of lines each disclosure reveals, preserving selection and scrolling.
430    pub fn set_expansion_lines(&mut self, lines: usize, cx: &mut Context<Self>) {
431        let lines = lines.max(1);
432        if self.expansion_lines == lines {
433            return;
434        }
435        self.expansion_lines = lines;
436        cx.notify();
437    }
438
439    /// Changes the shortest unchanged range that collapses, preserving selection and scrolling.
440    pub fn set_min_collapsed_lines(&mut self, lines: usize, cx: &mut Context<Self>) {
441        let lines = lines.max(1);
442        if self.min_collapsed_lines == lines {
443            return;
444        }
445        self.min_collapsed_lines = lines;
446        self.rebuild_at_anchor();
447        cx.notify();
448    }
449
450    /// Changes the inline comparison unit without resetting selection or scrolling.
451    pub fn set_inline_unit(&mut self, unit: Option<DiffInlineUnit>, cx: &mut Context<Self>) {
452        if self.presentation_options.inline_unit == unit {
453            return;
454        }
455        self.presentation_options.inline_unit = unit;
456        self.reset_presentation();
457        self.ensure_presentation(cx);
458        cx.notify();
459    }
460
461    /// Changes the inline comparison line limit in bytes without resetting selection or scrolling.
462    pub fn set_inline_max_line_length(&mut self, length: usize, cx: &mut Context<Self>) {
463        if self.presentation_options.inline_max_line_length == length {
464            return;
465        }
466        self.presentation_options.inline_max_line_length = length;
467        self.reset_presentation();
468        self.ensure_presentation(cx);
469        cx.notify();
470    }
471
472    /// Changes the syntax emphasis line limit in bytes without resetting selection or scrolling.
473    pub fn set_syntax_max_line_length(&mut self, length: usize, cx: &mut Context<Self>) {
474        if self.presentation_options.syntax_max_line_length == length {
475            return;
476        }
477        self.presentation_options.syntax_max_line_length = length;
478        self.reset_presentation();
479        self.ensure_presentation(cx);
480        cx.notify();
481    }
482
483    /// The files in patch order.
484    pub fn files(&self) -> &[DiffFile] {
485        &self.files
486    }
487    pub fn mode(&self) -> DiffMode {
488        self.mode
489    }
490    pub fn context_lines(&self) -> Option<usize> {
491        self.context_lines
492    }
493    pub fn expansion_lines(&self) -> usize {
494        self.expansion_lines
495    }
496    pub fn min_collapsed_lines(&self) -> usize {
497        self.min_collapsed_lines
498    }
499    pub fn inline_unit(&self) -> Option<DiffInlineUnit> {
500        self.presentation_options.inline_unit
501    }
502    pub fn inline_max_line_length(&self) -> usize {
503        self.presentation_options.inline_max_line_length
504    }
505    pub fn syntax_max_line_length(&self) -> usize {
506        self.presentation_options.syntax_max_line_length
507    }
508    pub fn selected_lines(&self) -> Option<DiffLineRange> {
509        self.selected_lines.clone()
510    }
511    /// Whether the file at `path` shows only its header.
512    pub fn is_file_collapsed(&self, path: &str) -> bool {
513        self.collapsed.contains(path)
514    }
515
516    pub(crate) fn rows(&self) -> &Rc<Vec<DisplayRow>> {
517        &self.rows
518    }
519    pub(crate) fn list(&self) -> &ListState {
520        &self.list
521    }
522    pub(crate) fn horizontal_scroll(&self) -> &ScrollHandle {
523        &self.horizontal_scroll
524    }
525    pub(crate) fn selection(&self) -> &TextSelectionHandle {
526        &self.selection
527    }
528    pub(crate) fn geometry(&self) -> &Rc<RefCell<SelectionGeometry>> {
529        &self.geometry
530    }
531    pub(crate) fn presentations(&self) -> &[Option<Arc<FilePresentation>>] {
532        &self.presentations
533    }
534    /// The body's size as of the last paint.
535    pub(crate) fn viewport(&self) -> Size<Pixels> {
536        self.viewport
537    }
538    /// The width rows need to show their longest line.
539    pub(crate) fn content_width(&self) -> Pixels {
540        self.content_width
541    }
542    pub(crate) fn set_content_width(&mut self, width: Pixels) {
543        self.content_width = width;
544    }
545    /// Records the painted body size and widest painted row. Returns whether
546    /// either changed, so the frame must be laid out again.
547    pub(crate) fn record_paint(&mut self, viewport: Size<Pixels>, painted_width: Pixels) -> bool {
548        let wider = painted_width > self.content_width;
549        if wider {
550            self.content_width = painted_width;
551        }
552        let resized = self.viewport != viewport;
553        self.viewport = viewport;
554        wider || resized
555    }
556
557    /// Replaces the files and resets expansion, selection and viewport.
558    /// Collapsed files stay collapsed when their path is still present.
559    pub fn set_files(&mut self, files: impl IntoIterator<Item = DiffFile>, cx: &mut Context<Self>) {
560        if let Some(window) = self.window
561            && self.selection_is_local(cx)
562        {
563            TextSelection::clear_for_window(window, cx);
564        }
565        self.files = files.into_iter().collect();
566        self.file_ixs = file_ixs(&self.files);
567        self.collapsed
568            .retain(|path| self.file_ixs.contains_key(path));
569        self.resolutions.clear();
570        self.expanded = vec![Vec::new(); self.files.len()];
571        self.reset_presentation();
572        self.layout = None;
573        self.annotations.clear();
574        self.selection.set_local_selection(false, cx);
575        self.selection.set_fallback_copy_text("", cx);
576        self.selected_lines = None;
577        self.selection_anchor = None;
578        self.selection_cursor = None;
579        self.selecting = false;
580        self.geometry.borrow_mut().clear();
581        self.rebuild(true);
582        self.horizontal_scroll.set_offset(point(px(0.), px(0.)));
583        self.ensure_presentation(cx);
584        cx.notify();
585    }
586
587    /// Changes the display mode while preserving source-line selection and a
588    /// nearby source row.
589    pub fn set_mode(&mut self, mode: DiffMode, cx: &mut Context<Self>) {
590        if self.mode == mode {
591            return;
592        }
593        self.mode = mode;
594        self.rebuild_at_anchor();
595        self.horizontal_scroll.set_offset(point(px(0.), px(0.)));
596        cx.notify();
597    }
598
599    /// Sets surrounding context. `None` shows every supplied patch line.
600    pub fn set_context_lines(&mut self, lines: Option<usize>, cx: &mut Context<Self>) {
601        if self.context_lines == lines {
602            return;
603        }
604        self.context_lines = lines;
605        self.expanded.iter_mut().for_each(Vec::clear);
606        self.rebuild_at_anchor();
607        cx.notify();
608    }
609
610    /// Reveals all unchanged source while preserving the configured context count.
611    pub fn expand_unchanged(&mut self, cx: &mut Context<Self>) {
612        for (expanded, file) in self.expanded.iter_mut().zip(&self.files) {
613            *expanded = vec![0..file.pairs().len()];
614        }
615        self.rebuild_at_anchor();
616        cx.notify();
617    }
618
619    /// Restores the configured context disclosures.
620    pub fn collapse_unchanged(&mut self, cx: &mut Context<Self>) {
621        self.expanded.iter_mut().for_each(Vec::clear);
622        self.rebuild_at_anchor();
623        cx.notify();
624    }
625
626    /// Collapses the file at `path` to its header, or expands it, without
627    /// emitting a user event.
628    pub fn set_file_collapsed(&mut self, path: &str, collapsed: bool, cx: &mut Context<Self>) {
629        let Some(file) = self.file_ix(path) else {
630            return;
631        };
632        let path = self.files[file].path().clone();
633        let changed = if collapsed {
634            self.collapsed.insert(path)
635        } else {
636            self.collapsed.remove(&path)
637        };
638        if changed {
639            self.rebuild_at_anchor();
640            cx.notify();
641        }
642    }
643
644    /// The resolution of conflict `ix` in the file at `path`, if any.
645    /// Use [`DiffFile::conflicts`] to enumerate valid indices; `None` means
646    /// unresolved, or that the file or index does not exist.
647    pub fn conflict_resolution(&self, path: &str, ix: usize) -> Option<DiffConflictResolution> {
648        let file = &self.files[self.file_ix(path)?];
649        self.resolutions.get(&(file.path().clone(), ix)).copied()
650    }
651
652    /// Resolves conflict `ix` (an index in [`DiffFile::conflicts`]) in the file
653    /// at `path`, or clears its resolution
654    /// with `None`, without emitting a user event.
655    pub fn resolve_conflict(
656        &mut self,
657        path: &str,
658        ix: usize,
659        resolution: Option<DiffConflictResolution>,
660        cx: &mut Context<Self>,
661    ) {
662        let Some(file) = self.file_ix(path) else {
663            return;
664        };
665        if ix >= self.files[file].conflicts().len() {
666            return;
667        }
668        let key = (self.files[file].path().clone(), ix);
669        let previous = match resolution {
670            Some(resolution) => self.resolutions.insert(key, resolution),
671            None => self.resolutions.remove(&key),
672        };
673        if previous != resolution {
674            self.rebuild_at_anchor();
675            cx.notify();
676        }
677    }
678
679    /// The working file with every conflict replaced by its resolution, or
680    /// `None` while a conflict is unresolved or `path` names no conflicted file.
681    pub fn resolved_text(&self, path: &str) -> Option<String> {
682        let file = &self.files[self.file_ix(path)?];
683        if file.status() != DiffFileStatus::Conflicted {
684            return None;
685        }
686        let lines = file.lines(DiffSide::Modified);
687        let source = file.source(DiffSide::Modified);
688        let mut text = String::with_capacity(source.len());
689        let mut ix = 0;
690        for (conflict_ix, conflict) in file.conflicts().iter().enumerate() {
691            let resolution = *self.resolutions.get(&(file.path().clone(), conflict_ix))?;
692            for line in &lines[ix..conflict.source_lines().start] {
693                text.push_str(&source[line.source()]);
694            }
695            for range in conflict.kept(resolution) {
696                for line in &lines[range] {
697                    text.push_str(&source[line.source()]);
698                }
699            }
700            ix = conflict.source_lines().end;
701        }
702        for line in &lines[ix..] {
703            text.push_str(&source[line.source()]);
704        }
705        Some(text)
706    }
707
708    /// Synchronizes source-line selection without emitting a user event.
709    /// Clips to supplied patch lines; a range containing none clears selection.
710    pub fn set_selected_lines(&mut self, range: Option<DiffLineRange>, cx: &mut Context<Self>) {
711        let range = range.and_then(|range| self.clip_range(&range));
712        if self.selected_lines == range {
713            return;
714        }
715        self.selection_anchor = range.as_ref().map(DiffLineRange::start_position);
716        self.selection_cursor = range.as_ref().map(DiffLineRange::end_position);
717        self.selection.set_local_selection(range.is_some(), cx);
718        self.selected_lines = range;
719        cx.notify();
720    }
721
722    /// Returns selected source without gutters, diff markers or annotation content.
723    pub fn selected_text(&self, cx: &App) -> String {
724        if self.selection.has_local_selection(cx)
725            && let Some(selected) = self.selected_span()
726        {
727            return match selected {
728                SelectedLines::Side { file, side, lines } => {
729                    self.files[file].text_for_lines(side, lines.start, lines.end - 1)
730                }
731                SelectedLines::Patch { file, patch } => {
732                    let file = &self.files[file];
733                    let mut text = String::new();
734                    for line in &file.patch_lines()[patch] {
735                        // An unchanged line reads the same on both sides.
736                        let (side, ix) = match line.line(DiffSide::Modified) {
737                            Some(ix) => (DiffSide::Modified, ix),
738                            None => (
739                                DiffSide::Original,
740                                line.line(DiffSide::Original).unwrap_or(0),
741                            ),
742                        };
743                        // A side's unterminated final line can precede the
744                        // other side's lines in patch order.
745                        if !text.is_empty() && !text.ends_with('\n') {
746                            text.push('\n');
747                        }
748                        text.push_str(&file.source(side)[file.lines(side)[ix].source()]);
749                    }
750                    text
751                }
752            };
753        }
754        let Some((file, side, range)) =
755            super::selection::selected_source_range(&self.selection, cx)
756        else {
757            return String::new();
758        };
759        self.files
760            .get(file)
761            .and_then(|file| file.source(side).get(range))
762            .unwrap_or("")
763            .to_owned()
764    }
765
766    /// Reveals and scrolls to a source line, expanding its file and hidden
767    /// context if needed.
768    pub fn scroll_to_line(&mut self, position: DiffLinePosition, cx: &mut Context<Self>) {
769        if self.line_index(&position).is_none() {
770            return;
771        }
772        if self.collapsed.remove(position.path().as_str()) {
773            self.rebuild_at_anchor();
774        }
775        if self.expand_line(&position).is_some() {
776            self.reveal_line(&position);
777            cx.notify();
778        }
779    }
780
781    /// Scrolls to the header of the file at `path`, a [`DiffFile::path`].
782    pub fn scroll_to_file(&mut self, path: &str, cx: &mut Context<Self>) {
783        let Some(item_ix) = self
784            .file_ix(path)
785            .and_then(|file| self.header_rows.get(file).copied())
786        else {
787            return;
788        };
789        self.list.scroll_to(ListOffset {
790            item_ix,
791            offset_in_item: px(0.),
792        });
793        cx.notify();
794    }
795
796    /// Moves to the next changed group after the current viewport, across files,
797    /// wrapping to the first once the list is scrolled to its end.
798    pub fn next_change(&mut self, cx: &mut Context<Self>) {
799        self.move_change(true, cx);
800    }
801    /// Moves to the previous changed group before the current viewport, across files.
802    pub fn previous_change(&mut self, cx: &mut Context<Self>) {
803        self.move_change(false, cx);
804    }
805
806    /// Reveals part or all of a collapsed unchanged range.
807    pub(crate) fn expand_fold(
808        &mut self,
809        file: usize,
810        pairs: Range<usize>,
811        expansion: FoldExpansion,
812        cx: &mut Context<Self>,
813    ) {
814        let lines = self.expansion_lines;
815        let revealed = match expansion {
816            FoldExpansion::All => pairs,
817            FoldExpansion::Up => pairs.end.saturating_sub(lines).max(pairs.start)..pairs.end,
818            FoldExpansion::Down => pairs.start..(pairs.start + lines).min(pairs.end),
819        };
820        if let Some(expanded) = self.expanded.get_mut(file) {
821            expanded.push(revealed);
822            self.rebuild_at_anchor();
823            cx.notify();
824        }
825    }
826
827    /// Resolves a conflict from its heading and reports the user's change.
828    pub(crate) fn choose_conflict(
829        &mut self,
830        file: usize,
831        ix: usize,
832        resolution: Option<DiffConflictResolution>,
833        cx: &mut Context<Self>,
834    ) {
835        let path = self.files[file].path().clone();
836        self.resolve_conflict(&path, ix, resolution, cx);
837        cx.emit(DiffEvent::ConflictResolved(path, ix));
838    }
839
840    /// Toggles a file from its header and reports the user's change.
841    pub(crate) fn toggle_file_collapsed(&mut self, file: usize, cx: &mut Context<Self>) {
842        let path = self.files[file].path().clone();
843        let collapsed = !self.collapsed.contains(&path);
844        self.set_file_collapsed(&path, collapsed, cx);
845        cx.emit(if collapsed {
846            DiffEvent::FileCollapsed(path)
847        } else {
848            DiffEvent::FileExpanded(path)
849        });
850    }
851
852    /// Starts a line-number selection gesture, extending the current anchor in
853    /// the same file when `extend` is set.
854    pub(crate) fn begin_line_selection(
855        &mut self,
856        position: DiffLinePosition,
857        extend: bool,
858        window: &mut Window,
859        cx: &mut Context<Self>,
860    ) {
861        if self.line_index(&position).is_none() {
862            return;
863        }
864        let anchor = self
865            .selection_anchor
866            .clone()
867            .filter(|anchor| extend && self.can_extend(anchor, &position))
868            .unwrap_or_else(|| position.clone());
869        TextSelection::clear(window, cx);
870        window.focus(&self.focus, cx);
871        self.selecting = true;
872        cx.emit(DiffEvent::SelectionStarted(range_between(
873            &anchor, &position,
874        )));
875        self.select_user_range(anchor, position, cx);
876    }
877
878    /// Extends an active line-number gesture to `position` in the same file.
879    pub(crate) fn drag_line_selection(
880        &mut self,
881        position: DiffLinePosition,
882        cx: &mut Context<Self>,
883    ) {
884        let Some(anchor) = self.selection_anchor.clone() else {
885            return;
886        };
887        if !self.selecting
888            || !self.can_extend(&anchor, &position)
889            || self.selection_cursor.as_ref() == Some(&position)
890            || self.line_index(&position).is_none()
891        {
892            return;
893        }
894        self.select_user_range(anchor, position, cx);
895    }
896
897    /// Whether a selection anchored at `anchor` may extend to `position`:
898    /// within one file, and across sides only in Unified mode, whose rows
899    /// follow patch order.
900    fn can_extend(&self, anchor: &DiffLinePosition, position: &DiffLinePosition) -> bool {
901        anchor.path() == position.path()
902            && (anchor.side() == position.side() || self.mode == DiffMode::Unified)
903    }
904
905    /// Finishes an active line-number gesture.
906    pub(crate) fn end_line_selection(&mut self, cx: &mut Context<Self>) {
907        if !std::mem::take(&mut self.selecting) {
908            return;
909        }
910        if let Some(range) = self.selected_lines.clone() {
911            cx.emit(DiffEvent::SelectionEnded(range));
912        }
913    }
914
915    /// Selects one line, or extends to it, as a complete gesture.
916    #[cfg(test)]
917    pub(crate) fn click_line(
918        &mut self,
919        position: DiffLinePosition,
920        extend: bool,
921        window: &mut Window,
922        cx: &mut Context<Self>,
923    ) {
924        self.begin_line_selection(position, extend, window, cx);
925        self.end_line_selection(cx);
926    }
927
928    pub(crate) fn keyboard_select(
929        &mut self,
930        direction: isize,
931        extend: bool,
932        window: &mut Window,
933        cx: &mut Context<Self>,
934    ) {
935        let Some((file, side)) = self.active_side(cx) else {
936            return;
937        };
938        let count = self.files[file].lines_count(side);
939        let ix = self
940            .selection_cursor
941            .as_ref()
942            .filter(|position| {
943                position.side() == side && self.file_ix(position.path()) == Some(file)
944            })
945            .and_then(|position| self.line_index(position))
946            .map(|ix| ix.saturating_add_signed(direction).min(count - 1))
947            .unwrap_or(0);
948        let position = self.position(file, side, ix);
949        let anchor = self
950            .selection_anchor
951            .clone()
952            .filter(|anchor| extend && anchor.path() == position.path())
953            .unwrap_or_else(|| position.clone());
954        TextSelection::clear(window, cx);
955        window.focus(&self.focus, cx);
956        let range = self.select_user_range(anchor, position.clone(), cx);
957        cx.emit(DiffEvent::SelectionEnded(range));
958        if let Some(row) = self.expand_line(&position) {
959            self.list.scroll_to_reveal_item(row);
960        }
961    }
962
963    pub(crate) fn select_all(&mut self, window: &mut Window, cx: &mut Context<Self>) {
964        let Some((file, side)) = self.active_side(cx) else {
965            return;
966        };
967        let count = self.files[file].lines_count(side);
968        TextSelection::clear(window, cx);
969        window.focus(&self.focus, cx);
970        let range = self.select_user_range(
971            self.position(file, side, 0),
972            self.position(file, side, count - 1),
973            cx,
974        );
975        cx.emit(DiffEvent::SelectionEnded(range));
976    }
977
978    /// The selected lines, resolved for membership tests.
979    pub(crate) fn selected_span(&self) -> Option<SelectedLines> {
980        let range = self.selected_lines.as_ref()?;
981        let file = self.file_ix(range.path())?;
982        if range.is_single_side() {
983            let (start, end) = (
984                range.start().min(range.end()),
985                range.start().max(range.end()),
986            );
987            return Some(SelectedLines::Side {
988                file,
989                side: range.side(),
990                lines: start..end + 1,
991            });
992        }
993        let document = &self.files[file];
994        let start = document.patch_ix(
995            range.side(),
996            document.line_index(range.side(), range.start())?,
997        );
998        let end = document.patch_ix(
999            range.end_side(),
1000            document.line_index(range.end_side(), range.end())?,
1001        );
1002        Some(SelectedLines::Patch {
1003            file,
1004            patch: start.min(end)..start.max(end) + 1,
1005        })
1006    }
1007
1008    /// Records the window that renders this state, for clearing its text selection.
1009    pub(crate) fn set_window(&mut self, window: WindowId) {
1010        self.window = Some(window);
1011    }
1012
1013    /// Updates width and row-height measurement when fonts or layout change.
1014    /// Returns whether content width must be measured again.
1015    pub(crate) fn update_layout(&mut self, key: LayoutKey, row_height: Pixels) -> bool {
1016        if self.layout.as_ref() == Some(&key) {
1017            return false;
1018        }
1019        if self.layout.is_some() {
1020            self.list.remeasure();
1021        }
1022        self.layout = Some(key);
1023        self.row_height = Some(row_height);
1024        self.apply_row_height_hint();
1025        true
1026    }
1027
1028    /// Re-measures rows whose annotations were added, removed or replaced.
1029    /// Visible rows are re-measured every frame, so content changes inside an
1030    /// annotation need no notification.
1031    pub(crate) fn sync_annotations(&mut self, annotations: &[DiffAnnotation]) {
1032        let current = annotations
1033            .iter()
1034            .map(|annotation| {
1035                (
1036                    annotation.position().cloned(),
1037                    annotation.path().clone(),
1038                    annotation.id().clone(),
1039                )
1040            })
1041            .collect::<HashSet<_>>();
1042        if current == self.annotations {
1043            return;
1044        }
1045        let rows = current
1046            .symmetric_difference(&self.annotations)
1047            .filter_map(|(position, path, _)| match position {
1048                Some(position) => self.source_row(position),
1049                None => self
1050                    .file_ix(path)
1051                    .and_then(|file| self.header_rows.get(file).copied()),
1052            })
1053            .collect::<Vec<_>>();
1054        for row in rows {
1055            self.list.remeasure_items(row..row + 1);
1056        }
1057        self.annotations = current;
1058    }
1059
1060    pub(crate) fn position(&self, file: usize, side: DiffSide, ix: usize) -> DiffLinePosition {
1061        let document = &self.files[file];
1062        DiffLinePosition::new(
1063            document.path().clone(),
1064            side,
1065            document.line_number(side, ix),
1066        )
1067    }
1068
1069    fn file_ix(&self, path: &str) -> Option<usize> {
1070        self.file_ixs.get(path).copied()
1071    }
1072
1073    fn line_index(&self, position: &DiffLinePosition) -> Option<usize> {
1074        self.files[self.file_ix(position.path())?].line_index(position.side(), position.line())
1075    }
1076
1077    /// Clips a range to supplied lines, normalizing a single-side range.
1078    fn clip_range(&self, range: &DiffLineRange) -> Option<DiffLineRange> {
1079        let file = &self.files[self.file_ix(range.path())?];
1080        let first_at_or_after = |side: DiffSide, line: usize| {
1081            let lines = file.lines(side);
1082            lines
1083                .get(lines.partition_point(|source| source.line_number() < line))
1084                .map(|source| source.line_number())
1085        };
1086        let last_at_or_before = |side: DiffSide, line: usize| {
1087            let lines = file.lines(side);
1088            lines
1089                .partition_point(|source| source.line_number() <= line)
1090                .checked_sub(1)
1091                .map(|ix| lines[ix].line_number())
1092        };
1093        if range.is_single_side() {
1094            let (start, end) = (
1095                range.start().min(range.end()),
1096                range.start().max(range.end()),
1097            );
1098            let start = first_at_or_after(range.side(), start)?;
1099            let end = last_at_or_before(range.side(), end)?;
1100            return (start <= end)
1101                .then(|| DiffLineRange::new(range.path().clone(), range.side(), start, end));
1102        }
1103        let start = first_at_or_after(range.side(), range.start())
1104            .or_else(|| last_at_or_before(range.side(), range.start()))?;
1105        let end = last_at_or_before(range.end_side(), range.end())
1106            .or_else(|| first_at_or_after(range.end_side(), range.end()))?;
1107        Some(
1108            DiffLineRange::new(range.path().clone(), range.side(), start, end)
1109                .with_end_side(range.end_side()),
1110        )
1111    }
1112
1113    fn selection_is_local(&self, cx: &App) -> bool {
1114        let entity = Some(self.selection.entity_id());
1115        self.selection
1116            .snapshot(cx)
1117            .is_some_and(|snapshot| snapshot.anchor().entity_id() == entity)
1118    }
1119
1120    /// The file and side that keyboard selection and Select All act on.
1121    fn active_side(&self, cx: &App) -> Option<(usize, DiffSide)> {
1122        let (file, side) = self
1123            .selection_cursor
1124            .as_ref()
1125            .and_then(|cursor| Some((self.file_ix(cursor.path())?, Some(cursor.side()))))
1126            .filter(|_| self.selected_lines.is_some())
1127            .or_else(|| {
1128                super::selection::selected_source_range(&self.selection, cx)
1129                    .map(|(file, side, _)| (file, Some(side)))
1130            })
1131            .or_else(|| {
1132                let top = self.list.logical_scroll_top().item_ix;
1133                self.rows.get(top).map(|row| (row.file(), None))
1134            })?;
1135        let document = self.files.get(file)?;
1136        let side = side.unwrap_or(if document.lines_count(DiffSide::Modified) > 0 {
1137            DiffSide::Modified
1138        } else {
1139            DiffSide::Original
1140        });
1141        (document.lines_count(side) > 0).then_some((file, side))
1142    }
1143
1144    fn expand_line(&mut self, position: &DiffLinePosition) -> Option<usize> {
1145        let row = self.source_row(position)?;
1146        if let Some(DisplayRow::Fold { file, pairs }) = self.rows.get(row) {
1147            let (file, pairs) = (*file, pairs.clone());
1148            self.expanded[file].push(pairs);
1149            self.rebuild_at_anchor();
1150        }
1151        self.source_row(position)
1152    }
1153
1154    /// Selects from `anchor` to `cursor` in one file and reports a change.
1155    fn select_user_range(
1156        &mut self,
1157        anchor: DiffLinePosition,
1158        cursor: DiffLinePosition,
1159        cx: &mut Context<Self>,
1160    ) -> DiffLineRange {
1161        let range = range_between(&anchor, &cursor);
1162        let changed = self.selected_lines.as_ref() != Some(&range);
1163        self.selected_lines = Some(range.clone());
1164        self.selection_anchor = Some(anchor);
1165        self.selection_cursor = Some(cursor);
1166        self.selection.set_local_selection(true, cx);
1167        if changed {
1168            cx.emit(DiffEvent::SelectionChanged(Some(range.clone())));
1169        }
1170        cx.notify();
1171        range
1172    }
1173
1174    fn move_change(&mut self, forward: bool, cx: &mut Context<Self>) {
1175        let top = self.list.logical_scroll_top().item_ix;
1176        let groups = &self.change_rows;
1177        let target = if forward {
1178            // Scrolled to the end, the list clamps any later change below the
1179            // top, so the top never passes it and the key would stop there.
1180            // The last row's bounds answer even when the rows above the
1181            // viewport were never measured, as after a jump to the end.
1182            let at_end = self
1183                .list
1184                .item_count()
1185                .checked_sub(1)
1186                .and_then(|last| self.list.bounds_for_item(last))
1187                .is_some_and(|last| last.bottom() <= self.list.viewport_bounds().bottom());
1188            groups
1189                .get(groups.partition_point(|ix| *ix <= top))
1190                .copied()
1191                .filter(|_| !at_end)
1192                .or(groups.first().copied())
1193        } else {
1194            groups
1195                .partition_point(|ix| *ix < top)
1196                .checked_sub(1)
1197                .and_then(|ix| groups.get(ix))
1198                .copied()
1199                .or(groups.last().copied())
1200        };
1201        if let Some(ix) = target {
1202            self.list.scroll_to(ListOffset {
1203                item_ix: ix,
1204                offset_in_item: px(0.),
1205            });
1206            cx.notify();
1207        }
1208    }
1209
1210    /// The source line shown by a row, looking past headers within its file.
1211    fn row_position(&self, ix: usize) -> Option<DiffLinePosition> {
1212        let row = self.rows.get(ix)?;
1213        let file = row.file();
1214        let pair_position = |original: Option<usize>, modified: Option<usize>| {
1215            modified
1216                .map(|ix| self.position(file, DiffSide::Modified, ix))
1217                .or_else(|| original.map(|ix| self.position(file, DiffSide::Original, ix)))
1218        };
1219        match row {
1220            DisplayRow::Code {
1221                original, modified, ..
1222            } => pair_position(*original, *modified),
1223            DisplayRow::Fold { pairs, .. } => self.files[file]
1224                .pairs()
1225                .get(pairs.start)
1226                .and_then(|pair| pair_position(pair.original(), pair.modified())),
1227            DisplayRow::File(_)
1228            | DisplayRow::Notice(_)
1229            | DisplayRow::Hunk { .. }
1230            | DisplayRow::Conflict { .. } => self
1231                .rows
1232                .get(ix + 1)
1233                .filter(|next| next.file() == file)
1234                .and_then(|_| self.row_position(ix + 1)),
1235        }
1236    }
1237
1238    pub(crate) fn source_row(&self, position: &DiffLinePosition) -> Option<usize> {
1239        let rows = self.file_rows.get(self.file_ix(position.path())?)?;
1240        let rows = match position.side() {
1241            DiffSide::Original => &rows.original,
1242            DiffSide::Modified => &rows.modified,
1243        };
1244        rows.get(self.line_index(position)?).copied().flatten()
1245    }
1246
1247    fn reveal_line(&self, position: &DiffLinePosition) {
1248        if let Some(item_ix) = self.source_row(position) {
1249            self.list.scroll_to(ListOffset {
1250                item_ix,
1251                offset_in_item: px(0.),
1252            });
1253        }
1254    }
1255
1256    /// Rebuilds rows and restores the source line at the top of the viewport.
1257    fn rebuild_at_anchor(&mut self) {
1258        let anchor = self.row_position(self.list.logical_scroll_top().item_ix);
1259        self.rebuild(false);
1260        if let Some(anchor) = anchor {
1261            self.reveal_line(&anchor);
1262        }
1263    }
1264
1265    /// Projects rows. Without `reset`, only the changed middle is spliced so
1266    /// measured heights of unchanged rows survive disclosure changes.
1267    fn rebuild(&mut self, reset: bool) {
1268        let rows = self.project_rows();
1269        self.file_rows = source_rows(&self.files, &rows);
1270        self.change_rows = changed_groups(&rows);
1271        self.header_rows = rows
1272            .iter()
1273            .enumerate()
1274            .filter_map(|(ix, row)| matches!(row, DisplayRow::File(_)).then_some(ix))
1275            .collect();
1276        if reset {
1277            self.list.reset(rows.len());
1278        } else {
1279            let old = &self.rows;
1280            let prefix = old.iter().zip(&rows).take_while(|(a, b)| a == b).count();
1281            let suffix = old
1282                .iter()
1283                .rev()
1284                .zip(rows.iter().rev())
1285                .take(old.len().min(rows.len()) - prefix)
1286                .take_while(|(a, b)| a == b)
1287                .count();
1288            self.list
1289                .splice(prefix..old.len() - suffix, rows.len() - prefix - suffix);
1290        }
1291        self.rows = Rc::new(rows);
1292        self.apply_row_height_hint();
1293    }
1294
1295    fn project_rows(&self) -> Vec<DisplayRow> {
1296        let mut rows = Vec::new();
1297        for (ix, file) in self.files.iter().enumerate() {
1298            rows.push(DisplayRow::File(ix));
1299            if self.collapsed.contains(file.path()) {
1300                continue;
1301            }
1302            if file.pairs().is_empty() {
1303                rows.push(DisplayRow::Notice(ix));
1304            } else if file.status() == DiffFileStatus::Unchanged {
1305                rows.extend((0..file.pairs().len()).map(|line| DisplayRow::Code {
1306                    file: ix,
1307                    original: None,
1308                    modified: Some(line),
1309                    changed: false,
1310                }));
1311            } else if file.status() == DiffFileStatus::Conflicted {
1312                let resolutions = (0..file.conflicts().len())
1313                    .map(|conflict| {
1314                        self.resolutions
1315                            .get(&(file.path().clone(), conflict))
1316                            .copied()
1317                    })
1318                    .collect::<Vec<_>>();
1319                let visible = visible_pairs(
1320                    file,
1321                    self.context_lines,
1322                    self.min_collapsed_lines,
1323                    &self.expanded[ix],
1324                );
1325                project_conflicts(&mut rows, ix, file, &visible, &resolutions);
1326            } else {
1327                project_file(
1328                    &mut rows,
1329                    ix,
1330                    file,
1331                    self.mode,
1332                    self.context_lines,
1333                    self.min_collapsed_lines,
1334                    &self.expanded[ix],
1335                );
1336            }
1337        }
1338        rows
1339    }
1340
1341    /// Applies the row-height hint again once the list has laid out at a new
1342    /// width. Returns whether it did, so the frame must be laid out again.
1343    ///
1344    /// A list that lays out at a new width forgets every row height, hints
1345    /// included, which happens on its first layout too. The patch then
1346    /// measures only the rows rendered so far, and scrolling stops once the
1347    /// last of them reaches the bottom of the viewport. Call this after the
1348    /// list's prepaint, so the hint is back before the next wheel event.
1349    pub(crate) fn keep_row_height_hint(&mut self) -> bool {
1350        let width = self.list.viewport_bounds().size.width;
1351        if self.row_height.is_none() || self.hinted_width == Some(width) {
1352            return false;
1353        }
1354        self.hinted_width = Some(width);
1355        self.apply_row_height_hint();
1356        true
1357    }
1358
1359    /// Gives unmeasured rows a code-row height so the scrollbar is sized for
1360    /// the whole patch before every row has been rendered.
1361    fn apply_row_height_hint(&self) {
1362        if let Some(height) = self.row_height {
1363            let _ = self.list.clone().with_uniform_item_height(height);
1364        }
1365    }
1366
1367    /// Discards prepared emphasis after files or options change.
1368    fn reset_presentation(&mut self) {
1369        self.presentations = vec![None; self.files.len()];
1370        self.presentation_generation += 1;
1371        self.preparing = None;
1372        self._presentation = Task::ready(());
1373    }
1374
1375    /// Prepares syntax and inline emphasis for files that lack it, one file at
1376    /// a time on the background executor, reusing highlighters per language.
1377    /// Starts on first render, so builders applied after `new` take effect.
1378    pub(crate) fn ensure_presentation(&mut self, cx: &mut Context<Self>) {
1379        let generation = self.presentation_generation;
1380        if self.preparing == Some(generation) {
1381            return;
1382        }
1383        let pending = self
1384            .presentations
1385            .iter()
1386            .enumerate()
1387            .filter(|(_, presentation)| presentation.is_none())
1388            .map(|(ix, _)| (ix, self.files[ix].clone()))
1389            .collect::<Vec<_>>();
1390        if pending.is_empty() {
1391            return;
1392        }
1393        let options = self.presentation_options;
1394        self.preparing = Some(generation);
1395        self._presentation = cx.spawn(async move |this, cx| {
1396            let mut highlighters = Some(SyntaxHighlighters::default());
1397            for (ix, file) in pending {
1398                let mut moved = highlighters.take();
1399                let (presentation, returned) = cx
1400                    .background_spawn(async move {
1401                        let presentation = FilePresentation::prepare(
1402                            &file,
1403                            options,
1404                            moved.get_or_insert_default(),
1405                        );
1406                        (presentation, moved)
1407                    })
1408                    .await;
1409                highlighters = returned;
1410                let current = this.update(cx, |state, cx| {
1411                    if state.presentation_generation != generation {
1412                        return false;
1413                    }
1414                    if let Some(slot) = state.presentations.get_mut(ix) {
1415                        *slot = Some(Arc::new(presentation));
1416                    }
1417                    cx.notify();
1418                    true
1419                });
1420                if !matches!(current, Ok(true)) {
1421                    return;
1422                }
1423            }
1424        });
1425    }
1426}
1427
1428/// The range from `anchor` to `cursor`, normalized when both are on one side.
1429fn range_between(anchor: &DiffLinePosition, cursor: &DiffLinePosition) -> DiffLineRange {
1430    if anchor.side() == cursor.side() {
1431        DiffLineRange::new(
1432            cursor.path().clone(),
1433            cursor.side(),
1434            anchor.line().min(cursor.line()),
1435            anchor.line().max(cursor.line()),
1436        )
1437    } else {
1438        DiffLineRange::new(
1439            cursor.path().clone(),
1440            anchor.side(),
1441            anchor.line(),
1442            cursor.line(),
1443        )
1444        .with_end_side(cursor.side())
1445    }
1446}
1447
1448fn file_ixs(files: &[DiffFile]) -> HashMap<SharedString, usize> {
1449    let mut ixs = HashMap::with_capacity(files.len());
1450    for (ix, file) in files.iter().enumerate() {
1451        ixs.entry(file.path().clone()).or_insert(ix);
1452    }
1453    ixs
1454}
1455
1456/// Every source line maps to its code row or the disclosure hiding it; lines
1457/// of a collapsed file have no row. Disjoint folds visit each pair once, so
1458/// building the maps is linear.
1459fn source_rows(files: &[DiffFile], rows: &[DisplayRow]) -> Vec<FileRows> {
1460    let mut file_rows = files
1461        .iter()
1462        .map(|file| FileRows {
1463            original: vec![None; file.lines_count(DiffSide::Original)],
1464            modified: vec![None; file.lines_count(DiffSide::Modified)],
1465        })
1466        .collect::<Vec<_>>();
1467    for (row_ix, row) in rows.iter().enumerate() {
1468        let mut map = |file: usize, original: Option<usize>, modified: Option<usize>| {
1469            if let Some(line) = original {
1470                file_rows[file].original[line] = Some(row_ix);
1471            }
1472            if let Some(line) = modified {
1473                file_rows[file].modified[line] = Some(row_ix);
1474            }
1475        };
1476        match row {
1477            DisplayRow::Code {
1478                file,
1479                original,
1480                modified,
1481                ..
1482            } => map(*file, *original, *modified),
1483            DisplayRow::Fold { file, pairs } => {
1484                for pair in &files[*file].pairs()[pairs.clone()] {
1485                    map(*file, pair.original(), pair.modified());
1486                }
1487            }
1488            DisplayRow::File(_)
1489            | DisplayRow::Notice(_)
1490            | DisplayRow::Hunk { .. }
1491            | DisplayRow::Conflict { .. } => {}
1492        }
1493    }
1494    file_rows
1495}
1496
1497fn changed_groups(rows: &[DisplayRow]) -> Vec<usize> {
1498    let changed = |row: &DisplayRow| matches!(row, DisplayRow::Code { changed: true, .. });
1499    rows.iter()
1500        .enumerate()
1501        .filter_map(|(ix, row)| {
1502            (changed(row)
1503                && (ix == 0 || !changed(&rows[ix - 1]) || rows[ix - 1].file() != row.file()))
1504            .then_some(ix)
1505        })
1506        .collect()
1507}
1508
1509const SIDE_BIT: u64 = 1 << 40;
1510const OFFSET_MASK: u64 = SIDE_BIT - 1;
1511
1512/// Packs a source offset into a selection key, ordered by file, side and offset.
1513pub(crate) fn encode_key(file: usize, side: DiffSide, offset: usize) -> u64 {
1514    ((file as u64) << 41)
1515        | if side == DiffSide::Modified {
1516            SIDE_BIT
1517        } else {
1518            0
1519        }
1520        | (offset as u64 & OFFSET_MASK)
1521}
1522pub(crate) fn decode_key(key: u64) -> (usize, DiffSide, usize) {
1523    (
1524        (key >> 41) as usize,
1525        if key & SIDE_BIT == 0 {
1526            DiffSide::Original
1527        } else {
1528            DiffSide::Modified
1529        },
1530        (key & OFFSET_MASK) as usize,
1531    )
1532}
1533
1534/// The end of the hunk containing each pair; a fixture without hunks is one.
1535fn hunk_ends(document: &DiffFile) -> Vec<usize> {
1536    let pairs = document.pairs();
1537    let mut hunk_end = vec![pairs.len(); pairs.len()];
1538    for hunk in document.hunks() {
1539        hunk_end[hunk.pairs()].fill(hunk.pairs().end);
1540    }
1541    hunk_end
1542}
1543
1544/// Which pairs show as code: changes with their context, expanded ranges,
1545/// and collapsed ranges shorter than `min_collapsed_lines`.
1546fn visible_pairs(
1547    document: &DiffFile,
1548    context: Option<usize>,
1549    min_collapsed_lines: usize,
1550    expanded: &[Range<usize>],
1551) -> Vec<bool> {
1552    let pairs = document.pairs();
1553    let hunks = document.hunks();
1554    let mut hunk_end = vec![pairs.len(); pairs.len()];
1555    let mut hunk_start = vec![0; pairs.len()];
1556    for hunk in hunks {
1557        hunk_start[hunk.pairs()].fill(hunk.pairs().start);
1558        hunk_end[hunk.pairs()].fill(hunk.pairs().end);
1559    }
1560    // Interval boundaries avoid repeatedly filling overlapping context windows.
1561    // Projection remains linear in source lines plus the number of disclosures.
1562    let mut boundaries = vec![0isize; pairs.len() + 1];
1563    if let Some(context) = context {
1564        let mut ix = 0;
1565        while ix < pairs.len() {
1566            if !pairs[ix].is_changed() {
1567                ix += 1;
1568                continue;
1569            }
1570            let start = ix;
1571            while ix < pairs.len() && ix < hunk_end[start] && pairs[ix].is_changed() {
1572                ix += 1;
1573            }
1574            boundaries[start.saturating_sub(context).max(hunk_start[start])] += 1;
1575            boundaries[ix.saturating_add(context).min(hunk_end[start])] -= 1;
1576        }
1577    } else {
1578        boundaries[0] += 1;
1579        boundaries[pairs.len()] -= 1;
1580    }
1581    for range in expanded {
1582        let start = range.start.min(pairs.len());
1583        let end = range.end.min(pairs.len());
1584        if start < end {
1585            boundaries[start] += 1;
1586            boundaries[end] -= 1;
1587        }
1588    }
1589    let mut active = 0;
1590    let mut visible = boundaries[..pairs.len()]
1591        .iter()
1592        .map(|boundary| {
1593            active += boundary;
1594            active > 0
1595        })
1596        .collect::<Vec<_>>();
1597    // A disclosure shorter than the threshold would hide almost nothing.
1598    let mut ix = 0;
1599    while ix < pairs.len() {
1600        if visible[ix] {
1601            ix += 1;
1602            continue;
1603        }
1604        let start = ix;
1605        while ix < pairs.len() && ix < hunk_end[start] && !visible[ix] {
1606            ix += 1;
1607        }
1608        if ix - start < min_collapsed_lines {
1609            visible[start..ix].fill(true);
1610        }
1611    }
1612    visible
1613}
1614
1615fn project_file(
1616    rows: &mut Vec<DisplayRow>,
1617    file: usize,
1618    document: &DiffFile,
1619    mode: DiffMode,
1620    context: Option<usize>,
1621    min_collapsed_lines: usize,
1622    expanded: &[Range<usize>],
1623) {
1624    let pairs = document.pairs();
1625    let hunks = document.hunks();
1626    let hunk_end = hunk_ends(document);
1627    let visible = visible_pairs(document, context, min_collapsed_lines, expanded);
1628    let mut hunk_ix = 0;
1629    let mut ix = 0;
1630    while ix < pairs.len() {
1631        while let Some(hunk) = hunks.get(hunk_ix) {
1632            if hunk.pairs().start != ix {
1633                break;
1634            }
1635            rows.push(DisplayRow::Hunk {
1636                file,
1637                hunk: hunk_ix,
1638            });
1639            hunk_ix += 1;
1640        }
1641        if !visible[ix] {
1642            let start = ix;
1643            while ix < pairs.len() && ix < hunk_end[start] && !visible[ix] {
1644                ix += 1;
1645            }
1646            rows.push(DisplayRow::Fold {
1647                file,
1648                pairs: start..ix,
1649            });
1650        } else if mode == DiffMode::Unified && pairs[ix].is_changed() {
1651            let start = ix;
1652            while ix < pairs.len() && ix < hunk_end[start] && pairs[ix].is_changed() {
1653                ix += 1;
1654            }
1655            for pair in &pairs[start..ix] {
1656                if let Some(original) = pair.original() {
1657                    rows.push(DisplayRow::Code {
1658                        file,
1659                        original: Some(original),
1660                        modified: None,
1661                        changed: true,
1662                    });
1663                }
1664            }
1665            for pair in &pairs[start..ix] {
1666                if let Some(modified) = pair.modified() {
1667                    rows.push(DisplayRow::Code {
1668                        file,
1669                        original: None,
1670                        modified: Some(modified),
1671                        changed: true,
1672                    });
1673                }
1674            }
1675        } else {
1676            let pair = pairs[ix];
1677            rows.push(DisplayRow::Code {
1678                file,
1679                original: pair.original(),
1680                modified: pair.modified(),
1681                changed: pair.is_changed(),
1682            });
1683            ix += 1;
1684        }
1685    }
1686}
1687
1688/// Projects a conflicted working file: shared lines fold like context, and
1689/// each conflict shows its parts, or only the kept lines once resolved.
1690fn project_conflicts(
1691    rows: &mut Vec<DisplayRow>,
1692    file: usize,
1693    document: &DiffFile,
1694    visible: &[bool],
1695    resolutions: &[Option<DiffConflictResolution>],
1696) {
1697    // Each line of a conflicted file is its own pair.
1698    let code = |line: usize, changed: bool| DisplayRow::Code {
1699        file,
1700        original: None,
1701        modified: Some(line),
1702        changed,
1703    };
1704    let conflicts = document.conflicts();
1705    let mut next = 0;
1706    let mut ix = 0;
1707    while ix < visible.len() {
1708        if let Some(conflict) = conflicts.get(next)
1709            && conflict.source_lines().start == ix
1710        {
1711            match resolutions[next] {
1712                None => {
1713                    for part in [
1714                        ConflictPart::Current,
1715                        ConflictPart::Base,
1716                        ConflictPart::Incoming,
1717                    ] {
1718                        if let Some(lines) = conflict.part(part) {
1719                            rows.push(DisplayRow::Conflict {
1720                                file,
1721                                conflict: next,
1722                                part,
1723                            });
1724                            rows.extend(lines.map(|line| code(line, true)));
1725                        }
1726                    }
1727                }
1728                Some(resolution) => {
1729                    rows.push(DisplayRow::Conflict {
1730                        file,
1731                        conflict: next,
1732                        part: ConflictPart::Current,
1733                    });
1734                    for lines in conflict.kept(resolution) {
1735                        rows.extend(lines.map(|line| code(line, false)));
1736                    }
1737                }
1738            }
1739            ix = conflict.source_lines().end;
1740            next += 1;
1741            continue;
1742        }
1743        if visible[ix] {
1744            rows.push(code(ix, false));
1745            ix += 1;
1746            continue;
1747        }
1748        let start = ix;
1749        while ix < visible.len() && !visible[ix] {
1750            ix += 1;
1751        }
1752        rows.push(DisplayRow::Fold {
1753            file,
1754            pairs: start..ix,
1755        });
1756    }
1757}