use super::helpers::*;
use super::*;
use crate::kit::text_model::TextModelSnapshot;
use crate::view::branch_sidebar::BranchSection;
use gitcomet_core::domain::{Diff, FileDiffImage, FileDiffText, LogScope};
use gitcomet_core::mergetool_trace::{
    self, MergetoolTraceEvent, MergetoolTraceSideStats, MergetoolTraceStage,
};
use rustc_hash::{FxHashMap, FxHashSet, FxHasher};
use std::sync::Arc;
use std::time::Instant;

fn line_ranges_intersect(a: &Range<usize>, b: &Range<usize>) -> bool {
    a.start < b.end && b.start < a.end
}

fn diff_wrap_columns_for_width(width: Pixels, char_width: Pixels) -> usize {
    let char_width = f32::from(char_width.max(px(1.0)));
    ((f32::from(width.max(px(0.0))) / char_width).floor() as usize).max(1)
}

fn diff_wrap_byte_ranges_for_source_text(
    text: &str,
    columns: usize,
) -> Vec<rows::DiffWrapByteRange> {
    let mut ranges = rows::diff_wrap_ranges_for_text(text, columns)
        .into_iter()
        .map(rows::DiffWrapByteRange::from_range)
        .collect::<Vec<_>>();
    if ranges.is_empty() {
        ranges.push(rows::DiffWrapByteRange::default());
    }
    ranges
}

fn diff_wrap_byte_ranges_for_revealed_text(
    source_text: &str,
    raw_text: Option<&str>,
    columns: usize,
) -> Vec<rows::DiffWrapByteRange> {
    let marker_text = raw_text
        .filter(|raw| crate::view::diff_utils::diff_text_display_len(raw) == source_text.len())
        .unwrap_or(source_text);
    let offset_map = rows::whitespace_visible_diff_offset_map(marker_text, true);
    let mut ranges = rows::diff_wrap_ranges_for_text(
        rows::whitespace_visible_line_text(marker_text).as_ref(),
        columns,
    )
    .into_iter()
    .map(|display_range| {
        let start = offset_map.source_offset_for_display(display_range.start);
        let end = if display_range.end >= offset_map.display_len() {
            offset_map.source_len()
        } else {
            offset_map.source_offset_for_display(display_range.end)
        };
        rows::DiffWrapByteRange { start, end }
    })
    .collect::<Vec<_>>();
    if ranges.is_empty() {
        ranges.push(rows::DiffWrapByteRange::default());
    }
    ranges
}

fn diff_wrap_byte_ranges_for_text(
    source_text: &str,
    raw_text: Option<&str>,
    columns: usize,
    reveal_whitespace_chars: bool,
) -> Vec<rows::DiffWrapByteRange> {
    if reveal_whitespace_chars {
        diff_wrap_byte_ranges_for_revealed_text(source_text, raw_text, columns)
    } else {
        diff_wrap_byte_ranges_for_source_text(source_text, columns)
    }
}

fn diff_wrap_empty_byte_ranges() -> Vec<rows::DiffWrapByteRange> {
    vec![rows::DiffWrapByteRange::default()]
}

fn diff_wrap_byte_ranges_for_file_diff_text(
    text: &gitcomet_core::file_diff::FileDiffLineText,
    columns: usize,
    reveal_whitespace_chars: bool,
) -> Vec<rows::DiffWrapByteRange> {
    let display = crate::view::file_diff_display::file_diff_display_text(text);
    diff_wrap_byte_ranges_for_text(
        display.as_ref(),
        Some(text.as_ref()),
        columns,
        reveal_whitespace_chars,
    )
}

fn diff_wrap_byte_ranges_for_optional_file_diff_text(
    text: Option<&gitcomet_core::file_diff::FileDiffLineText>,
    columns: usize,
    reveal_whitespace_chars: bool,
) -> Vec<rows::DiffWrapByteRange> {
    text.map(|text| {
        diff_wrap_byte_ranges_for_file_diff_text(text, columns, reveal_whitespace_chars)
    })
    .unwrap_or_else(diff_wrap_empty_byte_ranges)
}

fn diff_wrap_byte_range_at(
    ranges: &[rows::DiffWrapByteRange],
    wrap_ix: usize,
) -> rows::DiffWrapByteRange {
    ranges.get(wrap_ix).copied().unwrap_or_default()
}

fn shift_resolved_output_marker(
    marker: ResolvedOutputConflictMarker,
    line_delta: isize,
) -> ResolvedOutputConflictMarker {
    ResolvedOutputConflictMarker {
        conflict_ix: marker.conflict_ix,
        range_start: shifted_line_index(marker.range_start, line_delta),
        range_end: shifted_line_index(marker.range_end, line_delta),
        is_start: marker.is_start,
        is_end: marker.is_end,
        unresolved: marker.unresolved,
    }
}

fn record_resolved_outline_trace(
    path: Option<&std::path::PathBuf>,
    started: Instant,
    pane: &MainPaneView,
    output_line_count: usize,
) {
    let path = path.cloned();
    let elapsed = started.elapsed();
    let (diff_row_count, inline_row_count) = pane.conflict_resolver.two_way_row_counts();
    mergetool_trace::record_with(|| {
        MergetoolTraceEvent::new(MergetoolTraceStage::ResolvedOutlineRecompute, path, elapsed)
            .with_base(MergetoolTraceSideStats::from_text(Some(
                pane.conflict_resolver.three_way_text.base.as_ref(),
            )))
            .with_ours(MergetoolTraceSideStats::from_text(Some(
                pane.conflict_resolver.three_way_text.ours.as_ref(),
            )))
            .with_theirs(MergetoolTraceSideStats::from_text(Some(
                pane.conflict_resolver.three_way_text.theirs.as_ref(),
            )))
            .with_conflict_block_count(Some(conflict_resolver::conflict_count(
                &pane.conflict_resolver.marker_segments,
            )))
            .with_diff_row_count(Some(diff_row_count))
            .with_inline_row_count(Some(inline_row_count))
            .with_resolved_output_line_count(Some(output_line_count))
    });
}

struct ResolvedOutlineComputation {
    output_line_count: usize,
    outline: ResolvedOutlineData,
}

enum ResolvedOutlineSourceView<'a> {
    ThreeWay {
        base_text: &'a str,
        base_line_starts: &'a [usize],
        ours_text: &'a str,
        ours_line_starts: &'a [usize],
        theirs_text: &'a str,
        theirs_line_starts: &'a [usize],
    },
    TwoWay {
        ours_text: &'a str,
        ours_line_starts: &'a [usize],
        theirs_text: &'a str,
        theirs_line_starts: &'a [usize],
    },
}

impl ResolvedOutlineSourceView<'_> {
    fn view_mode(&self) -> ConflictResolverViewMode {
        match self {
            Self::ThreeWay { .. } => ConflictResolverViewMode::ThreeWay,
            Self::TwoWay { .. } => ConflictResolverViewMode::TwoWayDiff,
        }
    }
}

#[derive(Clone)]
enum OwnedResolvedOutlineSourceData {
    ThreeWay {
        base_text: Arc<str>,
        base_line_starts: Arc<[usize]>,
        ours_text: Arc<str>,
        ours_line_starts: Arc<[usize]>,
        theirs_text: Arc<str>,
        theirs_line_starts: Arc<[usize]>,
    },
    TwoWay {
        ours_text: Arc<str>,
        ours_line_starts: Arc<[usize]>,
        theirs_text: Arc<str>,
        theirs_line_starts: Arc<[usize]>,
    },
}

impl OwnedResolvedOutlineSourceData {
    fn as_view(&self) -> ResolvedOutlineSourceView<'_> {
        match self {
            Self::ThreeWay {
                base_text,
                base_line_starts,
                ours_text,
                ours_line_starts,
                theirs_text,
                theirs_line_starts,
            } => ResolvedOutlineSourceView::ThreeWay {
                base_text,
                base_line_starts,
                ours_text,
                ours_line_starts,
                theirs_text,
                theirs_line_starts,
            },
            Self::TwoWay {
                ours_text,
                ours_line_starts,
                theirs_text,
                theirs_line_starts,
            } => ResolvedOutlineSourceView::TwoWay {
                ours_text,
                ours_line_starts,
                theirs_text,
                theirs_line_starts,
            },
        }
    }
}

#[derive(Clone)]
struct BackgroundResolvedOutlineRecomputeRequest {
    output_text: Arc<str>,
    output_line_count: usize,
    marker_segments: Vec<conflict_resolver::ConflictSegment>,
    block_map: conflict_resolver::ResolvedOutputBlockMap,
    sources: OwnedResolvedOutlineSourceData,
}

struct ResolvedOutlineIncrementalBase<'a> {
    text: &'a TextModelSnapshot,
    line_starts: &'a Arc<[usize]>,
    marker_segments: &'a [conflict_resolver::ConflictSegment],
    view_mode: ConflictResolverViewMode,
}

fn compute_resolved_outline_computation(
    output_text: &str,
    output_line_count: usize,
    marker_segments: &[conflict_resolver::ConflictSegment],
    block_map: &conflict_resolver::ResolvedOutputBlockMap,
    sources: ResolvedOutlineSourceView<'_>,
) -> ResolvedOutlineComputation {
    let view_mode = sources.view_mode();
    let markers = build_resolved_output_conflict_markers(
        marker_segments,
        output_text,
        output_line_count,
        block_map,
    );
    if should_skip_resolved_outline_provenance(view_mode, output_line_count) {
        return ResolvedOutlineComputation {
            output_line_count,
            outline: ResolvedOutlineData {
                meta: Vec::new(),
                markers,
                sources_index: FxHashSet::default(),
            },
        };
    }

    let mut meta = match sources {
        ResolvedOutlineSourceView::ThreeWay {
            base_text,
            base_line_starts,
            ours_text,
            ours_line_starts,
            theirs_text,
            theirs_line_starts,
        } => conflict_resolver::compute_resolved_line_provenance_from_text_with_indexed_sources(
            output_text,
            base_text,
            base_line_starts,
            ours_text,
            ours_line_starts,
            theirs_text,
            theirs_line_starts,
        ),
        ResolvedOutlineSourceView::TwoWay {
            ours_text,
            ours_line_starts,
            theirs_text,
            theirs_line_starts,
        } => conflict_resolver::compute_resolved_line_provenance_from_text_two_way_indexed_sources(
            output_text,
            ours_text,
            ours_line_starts,
            theirs_text,
            theirs_line_starts,
        ),
    };
    apply_conflict_choice_provenance_hints(&mut meta, marker_segments, output_text, view_mode);
    let sources_index = conflict_resolver::build_resolved_output_line_sources_index_from_text(
        &meta,
        output_text,
        view_mode,
    );

    ResolvedOutlineComputation {
        output_line_count,
        outline: ResolvedOutlineData {
            meta,
            markers,
            sources_index,
        },
    }
}

fn compute_resolved_outline_computation_from_projection(
    projection: &conflict_resolver::ResolvedOutputProjection,
    marker_segments: &[conflict_resolver::ConflictSegment],
    view_mode: ConflictResolverViewMode,
    sources: Option<ResolvedOutlineSourceView<'_>>,
) -> ResolvedOutlineComputation {
    let output_line_count = projection.len();
    let block_ranges = projection.conflict_line_ranges();
    let markers = build_resolved_output_conflict_markers_from_block_ranges(
        marker_segments,
        block_ranges,
        output_line_count,
    );
    if should_skip_resolved_outline_provenance(view_mode, output_line_count) {
        return ResolvedOutlineComputation {
            output_line_count,
            outline: ResolvedOutlineData {
                meta: Vec::new(),
                markers,
                sources_index: FxHashSet::default(),
            },
        };
    }

    let Some(sources) = sources else {
        return ResolvedOutlineComputation {
            output_line_count,
            outline: ResolvedOutlineData {
                meta: Vec::new(),
                markers,
                sources_index: FxHashSet::default(),
            },
        };
    };
    let mut source_lookup: FxHashMap<&str, (conflict_resolver::ResolvedLineSource, Option<u32>)> =
        FxHashMap::default();
    match sources {
        ResolvedOutlineSourceView::ThreeWay {
            base_text,
            base_line_starts,
            ours_text,
            ours_line_starts,
            theirs_text,
            theirs_line_starts,
        } => {
            insert_lookup_from_indexed_text(
                &mut source_lookup,
                conflict_resolver::ResolvedLineSource::C,
                theirs_text,
                theirs_line_starts,
            );
            insert_lookup_from_indexed_text(
                &mut source_lookup,
                conflict_resolver::ResolvedLineSource::B,
                ours_text,
                ours_line_starts,
            );
            insert_lookup_from_indexed_text(
                &mut source_lookup,
                conflict_resolver::ResolvedLineSource::A,
                base_text,
                base_line_starts,
            );
        }
        ResolvedOutlineSourceView::TwoWay {
            ours_text,
            ours_line_starts,
            theirs_text,
            theirs_line_starts,
        } => {
            insert_lookup_from_indexed_text(
                &mut source_lookup,
                conflict_resolver::ResolvedLineSource::B,
                theirs_text,
                theirs_line_starts,
            );
            insert_lookup_from_indexed_text(
                &mut source_lookup,
                conflict_resolver::ResolvedLineSource::A,
                ours_text,
                ours_line_starts,
            );
        }
    }

    let mut meta = Vec::with_capacity(output_line_count);
    for line_ix in 0..output_line_count {
        let line = projection
            .line_text(marker_segments, line_ix)
            .unwrap_or(std::borrow::Cow::Borrowed(""));
        let (source, input_line) = source_lookup
            .get(line.as_ref())
            .copied()
            .unwrap_or((conflict_resolver::ResolvedLineSource::Manual, None));
        meta.push(conflict_resolver::ResolvedLineMeta {
            output_line: u32::try_from(line_ix).unwrap_or(u32::MAX),
            source,
            input_line,
        });
    }
    apply_conflict_choice_provenance_hints_for_ranges(
        &mut meta,
        marker_segments,
        block_ranges,
        view_mode,
    );

    let mut sources_index = FxHashSet::default();
    sources_index.reserve(meta.len());
    for (line_ix, line_meta) in meta.iter().enumerate() {
        if line_meta.source == conflict_resolver::ResolvedLineSource::Manual {
            continue;
        }
        let Some(line_no) = line_meta.input_line else {
            continue;
        };
        let Some(line) = projection.line_text(marker_segments, line_ix) else {
            continue;
        };
        sources_index.insert(conflict_resolver::SourceLineKey::new(
            view_mode,
            line_meta.source,
            line_no,
            line.as_ref(),
        ));
    }

    ResolvedOutlineComputation {
        output_line_count,
        outline: ResolvedOutlineData {
            meta,
            markers,
            sources_index,
        },
    }
}

fn insert_lookup_from_indexed_text<'a>(
    lookup: &mut FxHashMap<&'a str, (conflict_resolver::ResolvedLineSource, Option<u32>)>,
    source: conflict_resolver::ResolvedLineSource,
    text: &'a str,
    line_starts: &[usize],
) {
    let line_count = indexed_line_count(text, line_starts);
    for line_ix in (0..line_count).rev() {
        let line = rows::resolved_output_line_text(text, line_starts, line_ix);
        lookup.insert(
            line,
            (
                source,
                Some(u32::try_from(line_ix.saturating_add(1)).unwrap_or(u32::MAX)),
            ),
        );
    }
}

fn update_line_sources_index_for_range(
    index: &mut FxHashSet<conflict_resolver::SourceLineKey>,
    view_mode: ConflictResolverViewMode,
    meta: &[conflict_resolver::ResolvedLineMeta],
    text: &str,
    line_starts: &[usize],
    line_range: Range<usize>,
    insert: bool,
) {
    if line_range.start >= line_range.end {
        return;
    }
    for line_ix in line_range {
        let Some(line_meta) = meta.get(line_ix) else {
            break;
        };
        if line_meta.source == conflict_resolver::ResolvedLineSource::Manual {
            continue;
        }
        let Some(line_no) = line_meta.input_line else {
            continue;
        };
        let key = conflict_resolver::SourceLineKey::new(
            view_mode,
            line_meta.source,
            line_no,
            rows::resolved_output_line_text(text, line_starts, line_ix),
        );
        if insert {
            index.insert(key);
        } else {
            index.remove(&key);
        }
    }
}

/// The row the resolved-output column measures its width against.
///
/// O(1): the rope carries the widest row in its summary, so the measurement
/// never scans the document. Ties keep the earliest row, matching the linear
/// scan this replaced.
fn resolved_output_measure_row(snapshot: &TextModelSnapshot) -> usize {
    snapshot.rope().longest_row() as usize
}

fn preferred_scroll_master_index<const N: usize>(max_scrolls: [Pixels; N]) -> usize {
    let mut preferred_ix = 0usize;
    for ix in 1..N {
        if max_scrolls[ix] > max_scrolls[preferred_ix] {
            preferred_ix = ix;
        }
    }
    preferred_ix
}

fn clamp_raw_scroll_y(raw_y: Pixels, max_scroll: Pixels) -> Pixels {
    let max_scroll = max_scroll.max(px(0.0));
    raw_y.clamp(-max_scroll, px(0.0))
}

#[cfg(test)]
fn compute_synced_scroll_offsets<const N: usize>(
    offsets: [Pixels; N],
    max_scrolls: [Pixels; N],
    last_synced: [Pixels; N],
    preferred_ix: usize,
) -> [Pixels; N] {
    compute_synced_scroll_offsets_with_master(offsets, max_scrolls, last_synced, preferred_ix, None)
}

fn compute_synced_scroll_offsets_with_master<const N: usize>(
    offsets: [Pixels; N],
    max_scrolls: [Pixels; N],
    last_synced: [Pixels; N],
    preferred_ix: usize,
    explicit_master_ix: Option<usize>,
) -> [Pixels; N] {
    if N == 0 {
        return offsets;
    }
    if offsets.iter().all(|offset| *offset == offsets[0]) {
        return offsets;
    }

    let preferred_ix = preferred_ix.min(N.saturating_sub(1));
    let mut changed_count = 0usize;
    let mut sole_changed_ix = preferred_ix;
    let mut preferred_changed = false;
    let mut largest_changed_ix = preferred_ix;

    for ix in 0..N {
        // GPUI clamps explicit offsets during paint, after this synchronizer
        // runs. If a pane's maximum changed between frames, treat the painted
        // clamp of our previous target as unchanged rather than fresh user
        // input from that follower.
        let last_at_current_max = clamp_raw_scroll_y(last_synced[ix], max_scrolls[ix]);
        if offsets[ix] == last_at_current_max {
            continue;
        }

        if changed_count == 0 || max_scrolls[ix] > max_scrolls[largest_changed_ix] {
            largest_changed_ix = ix;
        }
        if ix == preferred_ix {
            preferred_changed = true;
        }
        sole_changed_ix = ix;
        changed_count += 1;
    }

    let explicit_master_ix = explicit_master_ix.filter(|&ix| ix < N);
    if changed_count == 0 && explicit_master_ix.is_none() {
        // Nothing moved since the last sync — leave the offsets exactly as they
        // are. Re-driving everyone onto `preferred_ix` here would let a
        // transient flip in which handle is "widest" (the resolved output sizes
        // itself from a monospace width estimate, the columns from measured
        // rows) yank a clamped follower to a different offset with no user
        // input, which reads as a horizontal snap-back. Realignment happens on
        // the next real scroll instead.
        return offsets;
    }

    let master_ix = if let Some(explicit_master_ix) = explicit_master_ix {
        explicit_master_ix
    } else if changed_count == 1 {
        sole_changed_ix
    } else if preferred_changed {
        preferred_ix
    } else {
        largest_changed_ix
    };
    let master_y = offsets[master_ix];

    std::array::from_fn(|ix| clamp_raw_scroll_y(master_y, max_scrolls[ix]))
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum SyncedScrollAxis {
    Horizontal,
    Vertical,
}

impl SyncedScrollAxis {
    const fn includes(self, mode: DiffScrollSync) -> bool {
        match self {
            Self::Horizontal => mode.includes_horizontal(),
            Self::Vertical => mode.includes_vertical(),
        }
    }

    const fn offset_component(self, offset: Point<Pixels>) -> Pixels {
        match self {
            Self::Horizontal => offset.x,
            Self::Vertical => offset.y,
        }
    }

    const fn max_scroll_component(self, max_offset: Size<Pixels>) -> Pixels {
        match self {
            Self::Horizontal => max_offset.width,
            Self::Vertical => max_offset.height,
        }
    }

    fn with_offset_component(self, offset: Point<Pixels>, value: Pixels) -> Point<Pixels> {
        match self {
            Self::Horizontal => point(value, offset.y),
            Self::Vertical => point(offset.x, value),
        }
    }
}

pub(super) fn uniform_list_base_handle(handle: &UniformListScrollHandle) -> ScrollHandle {
    handle.0.borrow().base_handle.clone()
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum ConflictPreviewSyncGroup {
    /// Three-way, unfolded: base/ours/theirs columns and the resolved output
    /// all render full line spaces.
    ColumnsAndOutput,
    /// Three-way with hide-resolved or collapsed context: the columns share a
    /// projected row space the output does not.
    ColumnsOnly,
    /// Two-way: left (base handle) and right (theirs handle) sync as a pair;
    /// the ours handle is unused and the output owns its own scroll space.
    /// Used for block-local giant-file rows, and for the aligned view when the
    /// output-scroll-sync setting is off or the columns are folded.
    TwoWayPair,
    /// Two-way aligned, unfolded, with output scroll sync on: the left/right
    /// columns and the resolved output share the whole-file aligned row space.
    TwoWayPairAndOutput,
}

/// Sync one axis of the conflict-preview handle set for the given group.
///
/// Handles outside the group keep their own offsets; their baseline entries
/// are refreshed each frame so switching groups never sees phantom changes.
fn sync_conflict_preview_axis(
    handles: &[ScrollHandle; 4],
    last_synced: &mut [Pixels; 4],
    axis: SyncedScrollAxis,
    mode: DiffScrollSync,
    group: ConflictPreviewSyncGroup,
    explicit_master_ix: Option<usize>,
) {
    // An editable output lays out at content width and therefore has a real
    // horizontal range. Keep it in the raw-pixel sync group so either side can
    // drive the other. A streamed/short output can still have a zero range; in
    // that case exclude it so its clamp at zero cannot pull overflowing source
    // columns back to the start.
    let output_has_horizontal_range = handles[3].max_offset().x > px(0.0);
    let group = match (axis, group, output_has_horizontal_range) {
        (SyncedScrollAxis::Horizontal, ConflictPreviewSyncGroup::ColumnsAndOutput, false) => {
            ConflictPreviewSyncGroup::ColumnsOnly
        }
        (SyncedScrollAxis::Horizontal, ConflictPreviewSyncGroup::TwoWayPairAndOutput, false) => {
            ConflictPreviewSyncGroup::TwoWayPair
        }
        // Vertically the resolved output stands on its own, as KDiff3's merge
        // result window does: it owns a scrollbar the diff windows are not
        // connected to. The columns share one aligned row space, so keeping
        // them together is exact; the output is a different document whose
        // lines correspond to aligned rows only through the merge structure,
        // and on a file that changes every few rows there is no continuous
        // correspondence to follow. Tying them together made the output creep
        // relative to the diffs instead of tracking them. The two are brought
        // together on navigation instead, where the block being visited gives
        // an exact position in both.
        //
        // Horizontally they stay coupled, which KDiff3 also does — one shared
        // horizontal scrollbar drives all three inputs and the merge result.
        (SyncedScrollAxis::Vertical, ConflictPreviewSyncGroup::ColumnsAndOutput, _) => {
            ConflictPreviewSyncGroup::ColumnsOnly
        }
        (SyncedScrollAxis::Vertical, ConflictPreviewSyncGroup::TwoWayPairAndOutput, _) => {
            ConflictPreviewSyncGroup::TwoWayPair
        }
        (_, group, _) => group,
    };
    match group {
        ConflictPreviewSyncGroup::ColumnsAndOutput => {
            maybe_sync_synced_scroll_offsets_with_master(
                handles,
                last_synced,
                axis,
                mode,
                explicit_master_ix,
            );
        }
        ConflictPreviewSyncGroup::ColumnsOnly => {
            let columns = [handles[0].clone(), handles[1].clone(), handles[2].clone()];
            let mut columns_last = [last_synced[0], last_synced[1], last_synced[2]];
            maybe_sync_synced_scroll_offsets_with_master(
                &columns,
                &mut columns_last,
                axis,
                mode,
                explicit_master_ix.filter(|&ix| ix < 3),
            );
            last_synced[..3].copy_from_slice(&columns_last);
            last_synced[3] = axis.offset_component(handles[3].offset());
        }
        ConflictPreviewSyncGroup::TwoWayPair => {
            let pair = [handles[0].clone(), handles[2].clone()];
            let mut pair_last = [last_synced[0], last_synced[2]];
            let pair_master = match explicit_master_ix {
                Some(0) => Some(0),
                Some(2) => Some(1),
                _ => None,
            };
            maybe_sync_synced_scroll_offsets_with_master(
                &pair,
                &mut pair_last,
                axis,
                mode,
                pair_master,
            );
            last_synced[0] = pair_last[0];
            last_synced[2] = pair_last[1];
            last_synced[1] = axis.offset_component(handles[1].offset());
            last_synced[3] = axis.offset_component(handles[3].offset());
        }
        ConflictPreviewSyncGroup::TwoWayPairAndOutput => {
            let group = [handles[0].clone(), handles[2].clone(), handles[3].clone()];
            let mut group_last = [last_synced[0], last_synced[2], last_synced[3]];
            maybe_sync_synced_scroll_offsets_with_master(
                &group,
                &mut group_last,
                axis,
                mode,
                match explicit_master_ix {
                    Some(0) => Some(0),
                    Some(2) => Some(1),
                    Some(3) => Some(2),
                    _ => None,
                },
            );
            last_synced[0] = group_last[0];
            last_synced[2] = group_last[1];
            last_synced[3] = group_last[2];
            last_synced[1] = axis.offset_component(handles[1].offset());
        }
    }
}

fn snapshot_synced_scroll_offsets<const N: usize>(
    handles: &[ScrollHandle; N],
    axis: SyncedScrollAxis,
) -> [Pixels; N] {
    std::array::from_fn(|ix| axis.offset_component(handles[ix].offset()))
}

fn sync_synced_scroll_offsets_with_master<const N: usize>(
    handles: &[ScrollHandle; N],
    last_synced: &mut [Pixels; N],
    axis: SyncedScrollAxis,
    explicit_master_ix: Option<usize>,
) {
    let offsets: [Point<Pixels>; N] = std::array::from_fn(|ix| handles[ix].offset());
    let max_scrolls: [Pixels; N] = std::array::from_fn(|ix| {
        axis.max_scroll_component(handles[ix].max_offset().into())
            .max(px(0.0))
    });
    let offset_components: [Pixels; N] =
        std::array::from_fn(|ix| axis.offset_component(offsets[ix]));
    let targets = compute_synced_scroll_offsets_with_master(
        offset_components,
        max_scrolls,
        *last_synced,
        preferred_scroll_master_index(max_scrolls),
        explicit_master_ix,
    );

    if axis == SyncedScrollAxis::Horizontal && std::env::var_os("GC_SCROLL_DEBUG").is_some() {
        let f = |arr: &[Pixels; N]| {
            arr.iter()
                .map(|p| format!("{:.0}", f32::from(*p)))
                .collect::<Vec<_>>()
                .join(",")
        };
        eprintln!(
            "[hsync] off=[{}] max=[{}] last=[{}] -> tgt=[{}]",
            f(&offset_components),
            f(&max_scrolls),
            f(last_synced),
            f(&targets),
        );
    }

    for ix in 0..N {
        if axis.offset_component(offsets[ix]) != targets[ix] {
            handles[ix].set_offset(axis.with_offset_component(offsets[ix], targets[ix]));
        }
    }
    *last_synced = targets;
}

fn maybe_sync_synced_scroll_offsets<const N: usize>(
    handles: &[ScrollHandle; N],
    last_synced: &mut [Pixels; N],
    axis: SyncedScrollAxis,
    mode: DiffScrollSync,
) {
    maybe_sync_synced_scroll_offsets_with_master(handles, last_synced, axis, mode, None);
}

fn maybe_sync_synced_scroll_offsets_with_master<const N: usize>(
    handles: &[ScrollHandle; N],
    last_synced: &mut [Pixels; N],
    axis: SyncedScrollAxis,
    mode: DiffScrollSync,
    explicit_master_ix: Option<usize>,
) {
    if axis.includes(mode) {
        sync_synced_scroll_offsets_with_master(handles, last_synced, axis, explicit_master_ix);
    } else {
        *last_synced = snapshot_synced_scroll_offsets(handles, axis);
    }
}

/// Resolve the file path and blame source for a diff target, or `None` for
/// targets that do not support blame annotation (e.g. whole-commit diffs with no
/// selected path).
///
/// Committed-file diffs blame the committed revision shown on the new side.
/// Working-tree diffs blame the displayed new-side content for their area (see
/// [`gitcomet_core::services::Repo::blame_worktree_file`]); lines not yet
/// committed are surfaced as "Not Committed Yet". In both cases blame is
/// computed against the exact content rendered on the new side, so the 1:1
/// `new_line` mapping in the annotation column stays correct.
fn blame_path_rev_for_target(
    target: &DiffTarget,
) -> Option<(std::path::PathBuf, gitcomet_core::domain::BlameSource)> {
    use gitcomet_core::domain::BlameSource;
    match target {
        DiffTarget::WorkingTree { path, area } => {
            Some((path.clone(), BlameSource::WorkingTree(*area)))
        }
        DiffTarget::Commit {
            commit_id,
            path: Some(path),
        } => Some((
            path.clone(),
            BlameSource::Revision(Some(commit_id.0.to_string())),
        )),
        DiffTarget::CommitRange {
            to_commit_id,
            path: Some(path),
            ..
        } => Some((
            path.clone(),
            match to_commit_id {
                Some(to_commit_id) => BlameSource::Revision(Some(to_commit_id.0.to_string())),
                // Working-tree tip: the new side is the worktree file.
                None => BlameSource::WorkingTree(gitcomet_core::domain::DiffArea::Unstaged),
            },
        )),
        _ => None,
    }
}

impl MainPaneView {
    pub(in crate::view) fn sync_interactive_commit_editor_states(&mut self) {
        let repos_with_setup: Vec<RepoId> = self
            .state
            .repos
            .iter()
            .filter(|r| {
                r.interactive_rebase_setup.is_some() || r.interactive_cherry_pick_setup.is_some()
            })
            .map(|r| r.id)
            .collect();
        self.interactive_rebase_states
            .retain(|repo_id, _| repos_with_setup.contains(repo_id));
        for repo in self.state.repos.iter() {
            if let Some(setup) = repo.interactive_rebase_setup.as_ref() {
                let Loadable::Ready(entries) = &setup.entries else {
                    continue;
                };
                let replace = self
                    .interactive_rebase_states
                    .get(&repo.id)
                    .is_none_or(|st| {
                        st.mode != ICommitEditorMode::Rebase || st.original_entries != *entries
                    });
                if replace {
                    self.interactive_rebase_states.insert(
                        repo.id,
                        IRebaseViewState {
                            mode: ICommitEditorMode::Rebase,
                            entries: entries.clone(),
                            original_entries: entries.clone(),
                            ..Default::default()
                        },
                    );
                }
            } else if let Some(setup) = repo.interactive_cherry_pick_setup.as_ref() {
                if !matches!(setup.full_messages, Loadable::Ready(())) {
                    // Do not retain subject-only view-local entries from this
                    // or a replaced setup while full messages are pending.
                    self.interactive_rebase_states.remove(&repo.id);
                    continue;
                }
                let source_colors = setup
                    .source_colors
                    .iter()
                    .cloned()
                    .collect::<FxHashMap<_, _>>();
                // A repeated state application for the same setup must not
                // replace view-local reordering or action edits. A different
                // id set is a genuinely new setup.
                let same_plan =
                    self.interactive_rebase_states.get(&repo.id).is_some_and(
                        |st: &IRebaseViewState| {
                            st.mode == ICommitEditorMode::CherryPick
                                && st.original_entries.len() == setup.entries.len()
                                && st.original_entries.iter().zip(setup.entries.iter()).all(
                                    |(current, incoming)| current.commit_id == incoming.commit_id,
                                )
                        },
                    );
                if same_plan {
                    let st = self
                        .interactive_rebase_states
                        .get_mut(&repo.id)
                        .expect("same_plan implies the state exists");
                    for (current, incoming) in
                        st.original_entries.iter_mut().zip(setup.entries.iter())
                    {
                        current.message = incoming.message.clone();
                    }
                    for entry in st.entries.iter_mut() {
                        if let Some(incoming) = setup
                            .entries
                            .iter()
                            .find(|incoming| incoming.commit_id == entry.commit_id)
                        {
                            entry.message = incoming.message.clone();
                        }
                    }
                } else {
                    self.interactive_rebase_states.insert(
                        repo.id,
                        IRebaseViewState {
                            mode: ICommitEditorMode::CherryPick,
                            entries: setup.entries.clone(),
                            original_entries: setup.entries.clone(),
                            source_colors,
                            ..Default::default()
                        },
                    );
                }
            }
        }
    }

    pub(super) fn notify_fingerprint_for(state: &AppState) -> u64 {
        use std::hash::{Hash, Hasher};

        let mut hasher = FxHasher::default();
        state.active_repo.hash(&mut hasher);

        if let Some(repo_id) = state.active_repo
            && let Some(repo) = state.repos.iter().find(|r| r.id == repo_id)
        {
            match repo.diff_state.diff_target.as_ref() {
                Some(DiffTarget::WorkingTree { path, area }) => {
                    0u8.hash(&mut hasher);
                    path.hash(&mut hasher);
                    match area {
                        DiffArea::Staged => 0u8.hash(&mut hasher),
                        DiffArea::Unstaged => 1u8.hash(&mut hasher),
                    }
                }
                Some(DiffTarget::Commit { commit_id, path }) => {
                    1u8.hash(&mut hasher);
                    commit_id.hash(&mut hasher);
                    path.hash(&mut hasher);
                }
                Some(DiffTarget::CommitRange {
                    from_commit_id,
                    to_commit_id,
                    path,
                }) => {
                    2u8.hash(&mut hasher);
                    from_commit_id.hash(&mut hasher);
                    to_commit_id.hash(&mut hasher);
                    path.hash(&mut hasher);
                }
                None => {
                    3u8.hash(&mut hasher);
                }
            }
            repo.diff_state.diff_state_rev.hash(&mut hasher);
            // The historical-browse tint keys off content-preview mode, which can
            // share a diff_target with a plain diff of the same commit+path.
            repo.diff_state.content_preview.hash(&mut hasher);
            // Entering or leaving the editor swaps the whole content body and
            // the toolbar; without this the pane would not re-render for it.
            repo.diff_state.edit_mode.hash(&mut hasher);
            repo.conflict_state.conflict_rev.hash(&mut hasher);

            // Only include status changes when viewing a working tree diff.
            let status_rev = if matches!(
                repo.diff_state.diff_target,
                Some(DiffTarget::WorkingTree { .. })
            ) {
                repo.status_cache_rev()
            } else {
                0
            };
            status_rev.hash(&mut hasher);
            let commit_details_rev = if matches!(
                repo.diff_state.diff_target,
                Some(DiffTarget::Commit { path: Some(_), .. })
            ) {
                repo.history_state.commit_details_rev
            } else {
                0
            };
            commit_details_rev.hash(&mut hasher);
            // The historical-browse tint keys off the file browser source.
            repo.file_browser.file_browser_rev.hash(&mut hasher);

            match &repo.interactive_rebase_setup {
                Some(setup) => {
                    1u8.hash(&mut hasher);
                    setup.base.hash(&mut hasher);
                    match &setup.entries {
                        Loadable::NotLoaded => 0u8.hash(&mut hasher),
                        Loadable::Loading => 1u8.hash(&mut hasher),
                        Loadable::Ready(_) => 2u8.hash(&mut hasher),
                        Loadable::Error(err) => {
                            3u8.hash(&mut hasher);
                            err.hash(&mut hasher);
                        }
                    }
                }
                None => {
                    0u8.hash(&mut hasher);
                }
            }
            match &repo.interactive_cherry_pick_setup {
                Some(setup) => {
                    1u8.hash(&mut hasher);
                    setup.entries.len().hash(&mut hasher);
                    for entry in &setup.entries {
                        entry.commit_id.hash(&mut hasher);
                        entry.summary.hash(&mut hasher);
                    }
                    setup.source_colors.hash(&mut hasher);
                    match &setup.full_messages {
                        Loadable::NotLoaded => 0u8.hash(&mut hasher),
                        Loadable::Loading => 1u8.hash(&mut hasher),
                        Loadable::Ready(()) => 2u8.hash(&mut hasher),
                        Loadable::Error(error) => {
                            3u8.hash(&mut hasher);
                            error.hash(&mut hasher);
                        }
                    }
                }
                None => 0u8.hash(&mut hasher),
            }
            // Blame/annotate data — when blame loads for the first time or changes
            // target, the annotation sidebar needs to repaint.
            repo.history_state.blame_path.hash(&mut hasher);
            repo.history_state.blame_source.hash(&mut hasher);
            matches!(
                &repo.history_state.blame,
                gitcomet_state::model::Loadable::Ready(_)
            )
            .hash(&mut hasher);
        }

        hasher.finish()
    }

    pub(in crate::view) fn clear_diff_selection_or_exit(
        &mut self,
        repo_id: RepoId,
        cx: &mut gpui::Context<Self>,
    ) {
        match clear_diff_selection_action(self.view_mode) {
            ClearDiffSelectionAction::ClearSelection => {
                self.store.dispatch(Msg::ClearDiffSelection { repo_id });
            }
            ClearDiffSelectionAction::ExitFocusedMergetool => {
                self.set_focused_mergetool_exit_code(FOCUSED_MERGETOOL_EXIT_CANCELED);
                cx.quit();
            }
        }
    }

    pub(in crate::view) fn reveal_history_commit(
        &mut self,
        repo_id: RepoId,
        commit_id: CommitId,
        fallback_scope: Option<LogScope>,
        cx: &mut gpui::Context<Self>,
    ) {
        if matches!(
            clear_diff_selection_action(self.view_mode),
            ClearDiffSelectionAction::ExitFocusedMergetool
        ) {
            self.clear_diff_selection_or_exit(repo_id, cx);
            return;
        }

        self.clear_diff_selection_or_exit(repo_id, cx);
        // Resolve and show the commit immediately; the history walk below only
        // has to find its row. Without this the details pane would sit on the
        // working tree — or flip in and out of it — for the whole walk.
        self.store.dispatch(Msg::RevealCommit {
            repo_id,
            reference: commit_id.clone(),
        });
        self.history_view.update(cx, |view, cx| {
            view.request_reveal_commit(repo_id, commit_id, fallback_scope, cx);
        });
        cx.notify();
    }

    pub(in crate::view) fn reveal_history_worktree(
        &mut self,
        repo_id: RepoId,
        worktree_path: std::path::PathBuf,
        is_current: bool,
        head: Option<CommitId>,
        cx: &mut gpui::Context<Self>,
    ) {
        self.history_view.update(cx, |view, cx| {
            view.reveal_worktree(repo_id, worktree_path, is_current, head, cx);
        });
    }

    pub(in crate::view) fn reveal_history_branch_commit(
        &mut self,
        repo_id: RepoId,
        section: BranchSection,
        branch_name: &str,
        commit_id: CommitId,
        fallback_scope: Option<LogScope>,
        cx: &mut gpui::Context<Self>,
    ) {
        let branch_name = branch_name.to_string();
        self.history_view.update(cx, |view, cx| {
            view.set_selected_branch(repo_id, section, &branch_name, cx);
        });
        self.reveal_history_commit(repo_id, commit_id, fallback_scope, cx);
    }

    pub(super) fn set_focused_mergetool_exit_code(&self, code: i32) {
        if let Some(exit_code) = &self.focused_mergetool_exit_code {
            exit_code.store(code, Ordering::SeqCst);
        }
    }

    pub(super) fn focused_mergetool_labels_or_default(&self) -> FocusedMergetoolLabels {
        self.focused_mergetool_labels
            .clone()
            .unwrap_or(FocusedMergetoolLabels {
                local: "LOCAL".to_string(),
                remote: "REMOTE".to_string(),
                base: "BASE".to_string(),
            })
    }

    pub(in crate::view) fn focused_mergetool_save_and_exit(
        &mut self,
        repo_id: RepoId,
        path: std::path::PathBuf,
        cx: &mut gpui::Context<Self>,
    ) {
        use gitcomet_core::conflict_output::ConflictMarkerLabels;

        let Some(repo) = self.state.repos.iter().find(|repo| repo.id == repo_id) else {
            self.set_focused_mergetool_exit_code(FOCUSED_MERGETOOL_EXIT_ERROR);
            cx.quit();
            return;
        };

        let labels = self.focused_mergetool_labels_or_default();
        let materialized_output = (!self.conflict_resolved_output_is_streamed()).then(|| {
            self.conflict_resolver_input
                .read_with(cx, |input, _| input.text().to_string())
        });
        let save_payload = build_focused_mergetool_save_payload(
            &self.conflict_resolver.marker_segments,
            &self.conflict_resolver.conflict_region_indices,
            &self.conflict_resolved_output_block_map,
            materialized_output.as_deref(),
            ConflictMarkerLabels {
                local: labels.local.as_str(),
                remote: labels.remote.as_str(),
                base: labels.base.as_str(),
            },
        );
        if save_payload.total_conflicts != save_payload.resolved_conflicts
            || conflict_resolver::text_contains_conflict_markers(&save_payload.output)
        {
            cx.notify();
            return;
        }
        let output = save_payload.output;
        let exit_code = focused_mergetool_save_exit_code(
            save_payload.total_conflicts,
            save_payload.resolved_conflicts,
        );
        let full_path = repo.spec.workdir.join(&path);
        self.finish_focused_mergetool_output(
            &full_path,
            FocusedMergetoolOutput::Write(output.as_bytes()),
            exit_code,
            cx,
        );
    }

    pub(in crate::view) fn focused_mergetool_write_side_and_exit(
        &self,
        repo_id: RepoId,
        path: &std::path::Path,
        bytes: &[u8],
        cx: &mut gpui::Context<Self>,
    ) {
        let Some(repo) = self.state.repos.iter().find(|repo| repo.id == repo_id) else {
            self.set_focused_mergetool_exit_code(FOCUSED_MERGETOOL_EXIT_ERROR);
            cx.quit();
            return;
        };
        let full_path = repo.spec.workdir.join(path);
        self.finish_focused_mergetool_output(
            &full_path,
            FocusedMergetoolOutput::Write(bytes),
            FOCUSED_MERGETOOL_EXIT_SUCCESS,
            cx,
        );
    }

    pub(in crate::view) fn focused_mergetool_delete_and_exit(
        &self,
        repo_id: RepoId,
        path: &std::path::Path,
        cx: &mut gpui::Context<Self>,
    ) {
        let Some(repo) = self.state.repos.iter().find(|repo| repo.id == repo_id) else {
            self.set_focused_mergetool_exit_code(FOCUSED_MERGETOOL_EXIT_ERROR);
            cx.quit();
            return;
        };
        let full_path = repo.spec.workdir.join(path);
        self.finish_focused_mergetool_output(
            &full_path,
            FocusedMergetoolOutput::Delete,
            FOCUSED_MERGETOOL_EXIT_SUCCESS,
            cx,
        );
    }

    fn finish_focused_mergetool_output(
        &self,
        path: &std::path::Path,
        output: FocusedMergetoolOutput<'_>,
        success_exit_code: i32,
        cx: &mut gpui::Context<Self>,
    ) {
        match apply_focused_mergetool_output(path, output) {
            Ok(()) => self.set_focused_mergetool_exit_code(success_exit_code),
            Err(err) => {
                let operation = match output {
                    FocusedMergetoolOutput::Write(_) => "write merged output to",
                    FocusedMergetoolOutput::Delete => "delete merged output",
                };
                eprintln!("Failed to {operation} {}: {err}", path.display());
                self.set_focused_mergetool_exit_code(FOCUSED_MERGETOOL_EXIT_ERROR);
            }
        }
        cx.quit();
    }

    #[allow(clippy::too_many_arguments)]
    pub(in crate::view) fn new(
        store: Arc<AppStore>,
        ui_model: Entity<AppUiModel>,
        theme: AppTheme,
        date_time_format: DateTimeFormat,
        timezone: Timezone,
        show_timezone: bool,
        history_relative_dates: bool,
        history_highlight_commit_chain: bool,
        diff_scroll_sync: DiffScrollSync,
        diff_content_mode: DiffContentMode,
        diff_whitespace_mode: DiffWhitespaceMode,
        diff_view_mode: DiffViewMode,
        annotate_enabled: bool,
        diff_reveal_whitespace_chars: bool,
        diff_word_wrap: bool,
        diff_show_line_numbers: bool,
        auto_save_file_edits: bool,
        history_show_graph: bool,
        history_show_author: bool,
        history_show_date: bool,
        history_show_sha: bool,
        history_show_tags: bool,
        history_auto_fetch_tags_on_repo_activation: bool,
        view_mode: GitCometViewMode,
        focused_mergetool_labels: Option<FocusedMergetoolLabels>,
        focused_mergetool_exit_code: Option<Arc<AtomicI32>>,
        root_view: WeakEntity<GitCometView>,
        tooltip_host: WeakEntity<TooltipHost>,
        window: &mut Window,
        cx: &mut gpui::Context<Self>,
    ) -> Self {
        let state = Arc::clone(&ui_model.read(cx).state);
        let initial_fingerprint = Self::notify_fingerprint_for(&state);
        let subscription = cx.observe(&ui_model, |this, model, cx| {
            let next = Arc::clone(&model.read(cx).state);
            let next_fingerprint = Self::notify_fingerprint_for(&next);
            if next_fingerprint == this.notify_fingerprint {
                this.state = next;
                return;
            }

            this.notify_fingerprint = next_fingerprint;
            this.apply_state_snapshot(next, cx);
            cx.notify();
        });

        let diff_raw_input = cx.new(|cx| {
            components::TextInput::new(
                components::TextInputOptions {
                    multiline: true,
                    read_only: true,
                    ..Default::default()
                },
                window,
                cx,
            )
        });
        let submodule_hash_inputs = (0..4)
            .map(|_| {
                cx.new(|cx| {
                    let mut input = components::TextInput::new(
                        components::TextInputOptions {
                            read_only: true,
                            ..Default::default()
                        },
                        window,
                        cx,
                    );
                    input.set_read_only(true, cx);
                    input
                })
            })
            .collect::<Vec<_>>();

        let conflict_resolver_input = cx.new(|cx| {
            let mut input = components::TextInput::new(
                components::TextInputOptions {
                    placeholder: "Resolve file contents…".into(),
                    multiline: true,
                    chromeless: true,
                    ..Default::default()
                },
                window,
                cx,
            );
            input.set_suppress_right_click(true);
            input.set_line_height(
                Some(ui_scale::design_px_from_percent(
                    20.0,
                    ui_scale::current(cx).percent,
                )),
                cx,
            );
            input
        });

        let conflict_resolver_subscription =
            cx.observe(&conflict_resolver_input, |this, input, cx| {
                let _perf_scope = crate::view::perf::span(
                    crate::view::perf::ViewPerfSpan::ResolvedOutputEditObserve,
                );
                let (output_snapshot, edit_deltas) = input.update(cx, |input, _| {
                    (input.text_snapshot(), input.drain_recent_utf8_edit_deltas())
                });
                let outline_edit_delta = (edit_deltas.len() == 1)
                    .then(|| edit_deltas.first().cloned())
                    .flatten();
                // Fold the tree forward before anything else looks at the
                // buffer, so the very next frame paints from a tree that
                // already describes what was just typed.
                let syntax_edit = coalesce_resolved_output_edit_deltas(&edit_deltas);
                this.apply_conflict_resolved_output_edit_deltas(
                    edit_deltas,
                    &output_snapshot.rope(),
                );
                if !this.conflict_resolved_output_is_streamed() {
                    this.refresh_conflict_resolved_output_syntax(&output_snapshot, syntax_edit, cx);
                }
                let source_revision = ResolvedOutputSourceRevision::from_snapshot(&output_snapshot);
                let output_modified = resolved_output_snapshot_is_modified(
                    this.conflict_resolved_output_saved_snapshot.as_ref(),
                    &output_snapshot,
                );
                if this.conflict_resolved_output_modified != output_modified {
                    this.conflict_resolved_output_modified = output_modified;
                    cx.notify();
                }
                let outline_delta = resolved_outline_delta_for_snapshot_transition(
                    &this.conflict_resolved_preview_text,
                    &output_snapshot,
                    outline_edit_delta,
                );

                let path = this.conflict_resolver.path.clone();
                let needs_update = this.conflict_resolved_preview_path.as_ref() != path.as_ref()
                    || this.conflict_resolved_preview_source_revision != Some(source_revision);
                if !needs_update {
                    return;
                }

                this.conflict_resolved_preview_path = path.clone();
                this.conflict_resolved_preview_source_revision = Some(source_revision);
                this.schedule_conflict_resolved_outline_recompute(
                    path,
                    source_revision,
                    outline_delta,
                    cx,
                );
                // The Save gates derive effective resolutions from the live
                // editor text, so the containing toolbar must re-render for
                // every edit even while session state remains deferred.
                cx.notify();
            });

        let file_editor_scroll = ScrollHandle::new();
        let file_editor_input = cx.new(|cx| {
            let mut input = components::TextInput::new(
                components::TextInputOptions {
                    multiline: true,
                    chromeless: true,
                    ..Default::default()
                },
                window,
                cx,
            );
            // The input lays out at its content width inside an
            // `overflow_scroll` container, which is what gives that container a
            // horizontal extent to scroll — the same arrangement the resolved
            // output uses.
            input.set_content_width_layout(true);
            input.set_vertical_scroll_handle(Some(file_editor_scroll.clone()));
            input.set_line_height(
                Some(ui_scale::design_px_from_percent(
                    20.0,
                    ui_scale::current(cx).percent,
                )),
                cx,
            );
            input
        });
        let file_editor_subscription = cx.observe(&file_editor_input, |this, _input, cx| {
            this.on_file_editor_edited(cx);
        });

        let diff_search_scroll = ScrollHandle::new();
        let diff_search_input = cx.new(|cx| {
            let mut input = components::TextInput::new(
                components::TextInputOptions {
                    placeholder: "Search diff".into(),
                    multiline: true,
                    ..Default::default()
                },
                window,
                cx,
            );
            input.set_submit_on_enter(true);
            input.set_vertical_scroll_handle(Some(diff_search_scroll.clone()));
            input.set_vertical_padding(Some(px(4.0)), cx);
            input.set_line_height(
                Some(ui_scale::design_px_from_percent(
                    18.0,
                    ui_scale::current(cx).percent,
                )),
                cx,
            );
            input
        });
        let diff_search_subscription = cx.observe(&diff_search_input, |this, input, cx| {
            if input.update(cx, |input, _| input.take_enter_pressed()) {
                if this.diff_search_active {
                    this.diff_search_next_match();
                    cx.notify();
                }
                return;
            }
            let next: SharedString = input.read(cx).text().to_string().into();
            if this.diff_search_query != next {
                let previous_query = this.diff_search_query.clone();
                this.diff_search_query = next.clone();
                if next.is_empty() {
                    this.diff_search_scroll.set_offset(point(px(0.0), px(0.0)));
                }
                this.invalidate_diff_text_query_overlay_cache(
                    next.as_ref(),
                    this.diff_search_options,
                );
                this.clear_worktree_preview_segments_cache();
                this.clear_conflict_diff_query_overlay_caches();
                if next.is_empty() {
                    this.diff_search_cancel_pending_query_recompute();
                    this.diff_search_recompute_matches_for_query_change(previous_query.as_ref());
                } else {
                    this.diff_search_schedule_query_recompute(previous_query, cx);
                }
                cx.notify();
            }
        });

        let diff_panel_focus_handle = cx.focus_handle().tab_index(0).tab_stop(false);

        let last_window_size = window.viewport_size();
        let ui_scale_percent = ui_scale::current(cx).percent;
        let history_view = cx.new(|cx| {
            super::HistoryView::new(
                Arc::clone(&store),
                ui_model.clone(),
                theme,
                ui_scale_percent,
                date_time_format,
                timezone,
                show_timezone,
                history_relative_dates,
                history_highlight_commit_chain,
                history_show_graph,
                history_show_author,
                history_show_date,
                history_show_sha,
                history_show_tags,
                history_auto_fetch_tags_on_repo_activation,
                root_view.clone(),
                last_window_size,
                window,
                cx,
            )
        });

        let mut pane = Self {
            store,
            state,
            view_mode,
            focused_mergetool_labels,
            focused_mergetool_exit_code,
            theme,
            date_time_format,
            _ui_model_subscription: subscription,
            root_view,
            tooltip_host,
            notify_fingerprint: initial_fingerprint,
            active_context_menu_invoker: None,
            last_window_size: size(px(0.0), px(0.0)),
            layout_sidebar_render_width: px(280.0),
            layout_details_render_width: px(420.0),
            layout_sidebar_collapsed: false,
            layout_details_collapsed: false,
            reveal_whitespace_chars: diff_reveal_whitespace_chars,
            mergetool_auto_advance: true,
            mergetool_collapse_unchanged: false,
            mergetool_output_scroll_sync: true,
            mergetool_show_line_numbers: true,
            mergetool_view_three_way: true,
            diff_view: diff_view_mode,
            annotate_enabled,
            annotate_column_width: rows::DIFF_ANNOTATION_COLUMN_WIDTH_PX,
            annotate_resize: None,
            blame_annot_hover: None,
            diff_stage_gutter_hover: None,
            diff_stage_gutter_cells: FxHashMap::default(),
            blame_time_range_cache: None,
            rendered_preview_modes: RenderedPreviewModes::default(),
            diff_word_wrap,
            diff_show_line_numbers,
            diff_scroll_sync,
            diff_content_mode,
            diff_whitespace_mode,
            diff_split_ratio: 0.5,
            diff_split_resize: None,
            diff_split_last_synced_x: [px(0.0); 2],
            diff_split_last_synced_y: [px(0.0); 2],
            diff_horizontal_scroll: DiffHorizontalScrollState::new(),
            diff_cache_repo_id: None,
            diff_cache_rev: 0,
            diff_cache_content_signature: None,
            diff_cache_target: None,
            diff_cache: Vec::new(),
            diff_row_provider: None,
            diff_split_row_provider: None,
            diff_file_for_src_ix: Vec::new(),
            diff_language_for_src_ix: Vec::new(),
            diff_yaml_block_scalar_for_src_ix: Vec::new(),
            diff_click_kinds: Vec::new(),
            diff_line_kind_for_src_ix: Vec::new(),
            diff_visual_line_kind_for_src_ix: Vec::new(),
            diff_hide_unified_header_for_src_ix: Vec::new(),
            diff_header_display_cache: FxHashMap::default(),
            diff_split_cache: Vec::new(),
            diff_split_cache_len: 0,
            diff_panel_focus_handle,
            diff_autoscroll_pending: false,
            diff_raw_input,
            submodule_hash_inputs,
            diff_visible_indices: Vec::new(),
            diff_visible_inline_map: None,
            diff_wrap_visible_rows: Vec::new(),
            diff_wrap_visible_cache_key: None,
            collapsed_diff_hunks: Vec::new(),
            collapsed_diff_hunk_ix_by_src_ix: FxHashMap::default(),
            collapsed_diff_reveals: FxHashMap::default(),
            collapsed_diff_visible_rows: Vec::new(),
            collapsed_diff_hunk_visible_indices: Vec::new(),
            collapsed_diff_header_display_cache: FxHashMap::default(),
            collapsed_diff_projection_identity: None,
            diff_visible_cache_len: 0,
            diff_visible_view: DiffViewMode::Split,
            diff_visible_is_file_view: false,
            diff_visible_projection_rev: 0,
            diff_visible_cache_projection_rev: u64::MAX,
            diff_scrollbar_markers_cache: Vec::new(),
            diff_word_highlights: Vec::new(),
            diff_word_highlights_inflight: None,
            diff_file_stats: Vec::new(),
            diff_text_segments_cache: Vec::new(),
            diff_text_query_segments_cache: Vec::new(),
            diff_text_query_cache_query: SharedString::default(),
            diff_text_query_cache_options: Default::default(),
            diff_text_query_cache_matcher: None,
            diff_text_query_cache_generation: 0,
            diff_selection_anchor: None,
            diff_selection_range: None,
            diff_text_selecting: false,
            diff_text_anchor: None,
            diff_text_head: None,
            diff_text_autoscroll_seq: 0,
            diff_text_autoscroll_target: None,
            diff_text_last_mouse_pos: point(px(0.0), px(0.0)),
            diff_suppress_clicks_remaining: 0,
            diff_text_hitboxes: FxHashMap::default(),
            diff_search_horizontal_reveal: None,
            conflict_text_hitboxes: FxHashMap::default(),
            diff_text_layout_cache_epoch: 0,
            diff_text_layout_cache: FxHashMap::default(),
            diff_search_active: false,
            diff_search_query: "".into(),
            diff_search_options: Default::default(),
            diff_search_regex_error: None,
            diff_search_matches: Vec::new(),
            diff_search_inline_patch_trigram_index: None,
            diff_search_match_ix: None,
            diff_search_debounce_seq: 0,
            diff_search_pending_previous_query: None,
            diff_search_scroll,
            diff_search_input,
            _diff_search_subscription: diff_search_subscription,
            file_diff_cache_repo_id: None,
            file_diff_cache_rev: 0,
            file_diff_cache_content_signature: None,
            file_diff_cache_whitespace_mode: diff_whitespace_mode,
            file_diff_cache_target: None,
            file_diff_cache_error: None,
            file_diff_cache_path: None,
            file_diff_cache_language: None,
            file_diff_cache_rows: Vec::new(),
            file_diff_row_provider: None,
            file_diff_old_text: SharedString::default(),
            file_diff_old_line_starts: Arc::default(),
            file_diff_old_line_to_row: Arc::default(),
            file_diff_old_line_to_inline_row: Arc::default(),
            file_diff_new_text: SharedString::default(),
            file_diff_new_line_starts: Arc::default(),
            file_diff_new_line_to_row: Arc::default(),
            file_diff_new_line_to_inline_row: Arc::default(),
            file_diff_inline_cache: Vec::new(),
            file_diff_inline_row_provider: None,
            file_diff_inline_text: SharedString::default(),
            file_diff_inline_word_highlights: rows::new_lru_cache(
                FILE_DIFF_WORD_HIGHLIGHT_CACHE_MAX_ENTRIES,
            ),
            file_diff_split_word_highlights: rows::new_lru_cache(
                FILE_DIFF_WORD_HIGHLIGHT_CACHE_MAX_ENTRIES,
            ),
            file_diff_cache_seq: 0,
            file_diff_cache_inflight: None,
            file_diff_syntax_generation: 0,
            file_diff_style_cache_epochs: FileDiffStyleCacheEpochs::default(),
            syntax_chunk_poll_task: None,
            prepared_syntax_documents: FxHashMap::default(),
            #[cfg(test)]
            diff_syntax_budget_override: None,
            file_markdown_preview_cache_repo_id: None,
            file_markdown_preview_cache_rev: 0,
            file_markdown_preview_cache_content_signature: None,
            file_markdown_preview_cache_target: None,
            file_markdown_preview: Loadable::NotLoaded,
            markdown_preview_wrap: MarkdownPreviewWrapCache::default(),
            markdown_preview_reveal: Default::default(),
            file_markdown_preview_seq: 0,
            file_markdown_preview_inflight: None,
            file_image_diff_cache_repo_id: None,
            file_image_diff_cache_rev: 0,
            file_image_diff_cache_content_signature: None,
            file_image_diff_cache_target: None,
            file_image_diff_cache_seq: 0,
            file_image_diff_cache_inflight: None,
            file_image_diff_cache_path: None,
            file_image_diff_cache_old: None,
            file_image_diff_cache_new: None,
            file_image_diff_cache_old_svg_path: None,
            file_image_diff_cache_new_svg_path: None,
            worktree_preview_path: None,
            worktree_preview_source_path: None,
            worktree_preview: Loadable::NotLoaded,
            worktree_preview_source_len: 0,
            worktree_preview_text: SharedString::default(),
            worktree_preview_line_starts: Arc::default(),
            worktree_preview_line_flags: Arc::default(),
            worktree_preview_search_trigram_index: None,
            worktree_preview_content_rev: 0,
            worktree_markdown_preview_path: None,
            worktree_markdown_preview_source_rev: 0,
            worktree_markdown_preview: Loadable::NotLoaded,
            worktree_markdown_preview_picture_sizes: Default::default(),
            worktree_markdown_preview_block_scrolls: Default::default(),
            worktree_markdown_preview_blocks: Default::default(),
            worktree_markdown_preview_image_waits: FxHashSet::default(),
            worktree_markdown_preview_seq: 0,
            worktree_markdown_preview_inflight: None,
            worktree_preview_segments_cache_path: None,
            worktree_preview_syntax_language: None,
            worktree_preview_style_cache_epoch: 0,
            worktree_preview_cache_write_blocked_until_rev: None,
            worktree_preview_segments_cache: FxHashMap::default(),
            diff_preview_is_new_file: false,
            file_editor_input,
            _file_editor_input_subscription: file_editor_subscription,
            file_editor_key: None,
            file_editor_language: None,
            file_editor_loading: false,
            file_editor_loaded_status_rev: 0,
            file_editor_error: None,
            file_editor_dirty: false,
            file_editor_first_dirty_line: None,
            unsaved_file_edits_rev: 0,
            file_editor_saved_fingerprint: None,
            file_editor_stash: FxHashMap::default(),
            file_editor_autosave: None,
            file_editor_live_syntax: None,
            file_editor_live_syntax_source: None,
            file_editor_live_syntax_building: None,
            file_editor_live_syntax_build: None,
            file_editor_live_syntax_reparse: None,
            file_editor_bracket_match: None,
            file_editor_search_matches: Vec::new(),
            file_editor_search_source: None,
            file_editor_search_rev: 0,
            file_editor_search_applied_rev: 0,
            file_editor_search_reveal_rev: 0,
            file_editor_search_reveal_applied_rev: 0,
            file_editor_search_reveal_x_pending: false,
            file_editor_provider_theme_epoch: 1,
            file_editor_scroll,
            file_editor_gutter_scroll: UniformListScrollHandle::new(),
            file_editor_gutter_row_height: ui_scale::design_px_from_percent(
                RESOLVED_OUTPUT_ROW_HEIGHT_PX,
                ui_scale::current(cx).percent,
            ),
            conflict_resolved_gutter_row_height: ui_scale::design_px_from_percent(
                RESOLVED_OUTPUT_ROW_HEIGHT_PX,
                ui_scale::current(cx).percent,
            ),
            file_editor_blame: None,
            file_editor_blame_width: px(0.0),
            file_editor_wrap_row_starts: Vec::new(),
            auto_save_file_edits,
            conflict_resolver_input,
            _conflict_resolver_input_subscription: conflict_resolver_subscription,
            conflict_resolver: ConflictResolverUiState::default(),
            conflict_open_summary_toasted_files: FxHashSet::default(),
            conflict_resolver_vsplit_ratio: 0.6,
            conflict_resolver_vsplit_resize: None,
            conflict_three_way_col_ratios: [1.0 / 3.0, 2.0 / 3.0],
            conflict_three_way_col_widths: [px(0.0); 3],
            conflict_hsplit_resize: None,
            conflict_diff_split_ratio: 0.5,
            conflict_diff_split_resize: None,
            conflict_diff_split_col_widths: [px(0.0); 2],
            conflict_canvas_rows_enabled: conflict_canvas_rows_enabled_from_env(),
            conflict_diff_segments_cache_split:
                conflict_resolver::ConflictSplitStyledTextCache::default(),
            conflict_diff_query_segments_cache_split:
                conflict_resolver::ConflictSplitStyledTextCache::default(),
            conflict_diff_query_cache_query: SharedString::default(),
            conflict_diff_query_cache_options: Default::default(),
            conflict_three_way_segments_cache: FxHashMap::default(),
            conflict_three_way_query_segments_cache: FxHashMap::default(),
            conflict_three_way_prepared_syntax_documents: ThreeWaySides::default(),
            conflict_three_way_syntax_inflight: ThreeWaySides::default(),
            conflict_resolved_preview_path: None,
            conflict_resolved_preview_source_revision: None,
            conflict_resolved_output_saved_snapshot: None,
            conflict_resolved_output_modified: false,
            conflict_resolved_output_projection: None,
            conflict_resolved_output_block_map: conflict_resolver::ResolvedOutputBlockMap::default(
            ),
            conflict_resolved_preview_text: TextModelSnapshot::default(),
            conflict_resolved_preview_syntax_language: None,
            conflict_resolved_preview_line_count: 0,
            conflict_resolved_preview_line_starts: Arc::default(),
            conflict_resolved_output_live_syntax: None,
            conflict_resolved_output_live_syntax_reparse: None,
            conflict_resolved_output_live_syntax_source: None,
            conflict_resolved_output_provider_theme_epoch: 1,
            conflict_resolved_output_highlighted_conflict: None,
            conflict_resolved_output_unresolved_rows: None,
            #[cfg(test)]
            conflict_resolved_output_full_scans: 0,
            conflict_resolved_output_live_syntax_building: None,
            conflict_resolved_output_live_syntax_build: None,
            conflict_resolved_output_measure_row: 0,
            conflict_resolved_outline_stash: None,
            #[cfg(test)]
            conflict_resolved_outline_background_delay_override: None,
            history_view,
            diff_scroll: UniformListScrollHandle::default(),
            diff_split_right_scroll: UniformListScrollHandle::default(),
            conflict_resolver_diff_scroll: UniformListScrollHandle::default(),
            conflict_preview_ours_scroll: UniformListScrollHandle::default(),
            conflict_preview_theirs_scroll: UniformListScrollHandle::default(),
            conflict_preview_last_synced_x: [px(0.0); 4],
            conflict_preview_last_synced_y: [px(0.0); 4],
            conflict_preview_vertical_wheel_master: None,
            conflict_output_gutter_wheel_sync_pending: false,
            conflict_resolved_preview_scroll: UniformListScrollHandle::default(),
            conflict_resolved_output_editor_scroll: ScrollHandle::new(),
            conflict_resolved_preview_gutter_scroll: UniformListScrollHandle::default(),
            conflict_resolved_preview_gutter_last_synced_y: [px(0.0); 2],
            worktree_preview_scroll: UniformListScrollHandle::default(),
            path_display_cache: std::cell::RefCell::new(path_display::PathDisplayCache::default()),
            interactive_rebase_states: FxHashMap::default(),
        };

        pane.set_theme(theme, cx);
        pane.ensure_rendered_patch_diff_cache(cx);
        pane
    }

    pub(in crate::view) fn sync_root_layout_snapshot(&mut self, cx: &mut gpui::Context<Self>) {
        let fallback_sidebar = self.layout_sidebar_render_width;
        let fallback_details = self.layout_details_render_width;
        let fallback_sidebar_collapsed = self.layout_sidebar_collapsed;
        let fallback_details_collapsed = self.layout_details_collapsed;

        let (sidebar_w, details_w, sidebar_collapsed, details_collapsed) = self
            .root_view
            .read_with(cx, |root, _cx| {
                (
                    root.sidebar_render_width,
                    root.details_render_width,
                    root.sidebar_collapsed,
                    root.details_collapsed,
                )
            })
            .unwrap_or((
                fallback_sidebar,
                fallback_details,
                fallback_sidebar_collapsed,
                fallback_details_collapsed,
            ));

        self.layout_sidebar_render_width = sidebar_w;
        self.layout_details_render_width = details_w;
        self.layout_sidebar_collapsed = sidebar_collapsed;
        self.layout_details_collapsed = details_collapsed;
    }

    pub(in crate::view) fn set_theme(&mut self, theme: AppTheme, cx: &mut gpui::Context<Self>) {
        self.theme = theme;
        self.conflict_resolved_output_provider_theme_epoch = self
            .conflict_resolved_output_provider_theme_epoch
            .wrapping_add(1)
            .max(1);
        self.file_editor_provider_theme_epoch =
            self.file_editor_provider_theme_epoch.wrapping_add(1).max(1);
        self.clear_diff_text_style_caches();
        self.clear_worktree_preview_segments_cache();
        self.clear_conflict_diff_style_caches();
        self.conflict_three_way_segments_cache.clear();
        self.conflict_three_way_query_segments_cache.clear();
        self.diff_raw_input
            .update(cx, |input, cx| input.set_theme(theme, cx));
        self.diff_search_input
            .update(cx, |input, cx| input.set_theme(theme, cx));
        self.conflict_resolver_input
            .update(cx, |input, cx| input.set_theme(theme, cx));
        self.file_editor_input
            .update(cx, |input, cx| input.set_theme(theme, cx));
        self.rebind_file_editor_highlight_provider(cx);
        if self.conflict_resolved_output_is_streamed() {
            self.conflict_resolved_preview_syntax_language = self
                .conflict_resolved_preview_path
                .as_ref()
                .and_then(rows::diff_syntax_language_for_path);
            self.conflict_resolved_output_measure_row = self
                .conflict_resolved_output_projection
                .as_ref()
                .map(conflict_resolver::ResolvedOutputProjection::widest_line_ix)
                .unwrap_or(0);
        } else {
            let output_snapshot = self
                .conflict_resolver_input
                .read_with(cx, |input, _| input.text_snapshot());
            self.conflict_resolved_preview_line_starts = output_snapshot.shared_line_starts();
            self.conflict_resolved_preview_line_count = output_snapshot.line_count().max(1);
            self.conflict_resolved_output_measure_row =
                resolved_output_measure_row(&output_snapshot);
            self.refresh_conflict_resolved_output_syntax(&output_snapshot, None, cx);
        }
        self.history_view
            .update(cx, |view, cx| view.set_theme(theme, cx));
        cx.notify();
    }

    pub(in crate::view) fn apply_ui_scale_percent(
        &mut self,
        previous_percent: u32,
        next_percent: u32,
        cx: &mut gpui::Context<Self>,
    ) {
        self.conflict_resolver_input.update(cx, |input, cx| {
            input.set_line_height(
                Some(ui_scale::design_px_from_percent(20.0, next_percent)),
                cx,
            );
        });
        // The editor's gutter sizes its rows from the same scale, so leaving
        // the buffer at the old line height would put the numbers out of step
        // with the code they label.
        self.file_editor_input.update(cx, |input, cx| {
            input.set_line_height(
                Some(ui_scale::design_px_from_percent(20.0, next_percent)),
                cx,
            );
        });
        self.history_view.update(cx, |view, cx| {
            view.apply_ui_scale_percent(previous_percent, next_percent, cx);
        });
        cx.notify();
    }

    pub(in crate::view) fn invalidate_font_metrics(&mut self, cx: &mut gpui::Context<Self>) {
        self.diff_text_hitboxes.clear();
        self.diff_stage_gutter_cells.clear();
        self.diff_text_layout_cache_epoch = self.diff_text_layout_cache_epoch.wrapping_add(1);
        self.diff_text_layout_cache.clear();
        cx.notify();
    }

    pub(in crate::view) fn reset_diff_horizontal_scroll_state(&mut self) {
        self.diff_horizontal_scroll.reset();
        // A reveal names a row in the view it was armed over. That view is gone,
        // so the request must go with it rather than fire against whatever row
        // now holds that index.
        self.diff_search_horizontal_reveal = None;
        self.markdown_preview_reveal.clear();
    }

    pub(in crate::view) fn diff_horizontal_content_width(&self) -> Pixels {
        self.diff_horizontal_content_width_for_column(DiffHorizontalScrollColumn::Primary)
    }

    pub(in crate::view) fn diff_horizontal_content_width_for_column(
        &self,
        column: DiffHorizontalScrollColumn,
    ) -> Pixels {
        self.diff_horizontal_scroll.content_widths[column.index()]
    }

    pub(in crate::view) fn diff_horizontal_layout_min_width(
        &self,
        column: DiffHorizontalScrollColumn,
    ) -> Pixels {
        self.diff_horizontal_content_width_for_column(column)
    }

    pub(in crate::view) fn record_diff_horizontal_content_width(
        &mut self,
        width: Pixels,
        cx: &mut gpui::Context<Self>,
    ) {
        self.record_diff_horizontal_content_width_for_column(
            DiffHorizontalScrollColumn::Primary,
            width,
            cx,
        );
    }

    pub(in crate::view) fn record_diff_horizontal_content_width_for_column(
        &mut self,
        column: DiffHorizontalScrollColumn,
        width: Pixels,
        cx: &mut gpui::Context<Self>,
    ) {
        if self.diff_word_wrap {
            return;
        }

        if self
            .diff_horizontal_scroll
            .record_content_width(column, width)
        {
            cx.notify();
        }
    }

    pub(in crate::view) fn diff_vertical_scrollbar_gutter_for_column(
        &self,
        _column: DiffHorizontalScrollColumn,
        _handle: UniformListScrollHandle,
    ) -> Pixels {
        components::Scrollbar::gutter(components::ScrollbarAxis::Vertical)
    }

    #[cfg(test)]
    pub(in crate::view) fn diff_horizontal_scroll_max_offset_for_viewport(
        &self,
        column: DiffHorizontalScrollColumn,
        viewport_width: Pixels,
    ) -> Pixels {
        let viewport_width = viewport_width.max(px(0.0));
        let content_width = self.diff_horizontal_content_width_for_column(column);
        (content_width - viewport_width).max(px(0.0))
    }

    pub(in crate::view) fn conflict_resolved_output_is_streamed(&self) -> bool {
        self.conflict_resolved_output_projection.is_some()
    }

    pub(in crate::view) fn rebuild_conflict_resolved_output_block_map(
        &mut self,
        cx: &mut gpui::Context<Self>,
    ) {
        if self.conflict_resolver.output_is_protected {
            self.conflict_resolved_output_block_map =
                conflict_resolver::ResolvedOutputBlockMap::default();
            return;
        }
        let map = conflict_resolver::ResolvedOutputBlockMap::from_segments(
            &self.conflict_resolver.marker_segments,
        );
        if self.conflict_resolved_output_is_streamed()
            || self.conflict_resolver_input.read_with(cx, |input, _| {
                map.is_valid_for(&self.conflict_resolver.marker_segments, input.text())
            })
        {
            self.conflict_resolved_output_block_map = map;
        } else {
            self.conflict_resolved_output_block_map =
                conflict_resolver::ResolvedOutputBlockMap::default();
        }
    }

    pub(super) fn apply_conflict_resolved_output_edit_deltas(
        &mut self,
        edit_deltas: Vec<(Range<usize>, Range<usize>)>,
        output_text: &(impl conflict_resolver::ResolvedOutputSource + ?Sized),
    ) {
        if edit_deltas.is_empty() {
            return;
        }
        if !self
            .conflict_resolved_output_block_map
            .apply_edit_deltas(edit_deltas)
            || !self
                .conflict_resolved_output_block_map
                .is_valid_for(&self.conflict_resolver.marker_segments, output_text)
        {
            self.conflict_resolved_output_block_map =
                conflict_resolver::ResolvedOutputBlockMap::default();
        }
    }

    pub(in crate::view) fn conflict_resolved_output_is_modified(&self) -> bool {
        self.conflict_resolved_output_modified
    }

    pub(in crate::view) fn mark_conflict_resolved_output_saved(
        &mut self,
        cx: &mut gpui::Context<Self>,
    ) {
        self.conflict_resolved_output_saved_snapshot =
            (!self.conflict_resolved_output_is_streamed()).then(|| {
                self.conflict_resolver_input
                    .read_with(cx, |input, _| input.text_snapshot())
            });
        self.conflict_resolved_output_modified = false;
    }

    fn sync_conflict_resolved_preview_projection(
        &mut self,
        projection: conflict_resolver::ResolvedOutputProjection,
        path: Option<&std::path::PathBuf>,
    ) {
        self.conflict_resolved_output_block_map =
            conflict_resolver::ResolvedOutputBlockMap::from_segments(
                &self.conflict_resolver.marker_segments,
            );
        self.conflict_resolved_output_projection = Some(projection.clone());
        self.conflict_resolved_preview_path = path.cloned();
        self.conflict_resolved_preview_source_revision = None;
        self.conflict_resolved_preview_text = TextModelSnapshot::default();
        self.conflict_resolved_preview_syntax_language =
            path.and_then(rows::diff_syntax_language_for_path);
        self.conflict_resolved_preview_line_count = projection.len();
        self.conflict_resolved_preview_line_starts = Arc::default();
        self.conflict_resolved_output_measure_row = projection.widest_line_ix();
        self.conflict_resolved_outline_stash = None;
        self.conflict_resolver.resolved_output_visible_dirty = true;
    }

    pub(in crate::view) fn refresh_streamed_resolved_output_preview_from_projection(
        &mut self,
        projection: conflict_resolver::ResolvedOutputProjection,
        path: Option<&std::path::PathBuf>,
    ) {
        let trace_started = Instant::now();
        let output_line_count = projection.len();
        let view_mode = self.conflict_resolver.view_mode;
        let computed = compute_resolved_outline_computation_from_projection(
            &projection,
            &self.conflict_resolver.marker_segments,
            view_mode,
            (!should_skip_resolved_outline_provenance(view_mode, output_line_count))
                .then(|| self.resolved_outline_source_view()),
        );
        self.sync_conflict_resolved_preview_projection(projection, path);
        self.apply_resolved_outline_computation(path, trace_started, computed);
    }

    pub(in crate::view) fn refresh_streamed_resolved_output_preview_from_markers(
        &mut self,
        path: Option<&std::path::PathBuf>,
    ) {
        let projection = conflict_resolver::ResolvedOutputProjection::from_segments(
            &self.conflict_resolver.marker_segments,
        );
        self.refresh_streamed_resolved_output_preview_from_projection(projection, path);
    }

    pub(in crate::view) fn ensure_conflict_resolved_output_materialized(
        &mut self,
        cx: &mut gpui::Context<Self>,
    ) {
        if !self.conflict_resolved_output_is_streamed() {
            return;
        }

        // No size ceiling here. The output pane is editable by definition, and a
        // read-only fallback above some line count is a worse answer than a
        // slower one: the user opened a merge to resolve it. The buffer is
        // rope-backed and every hot path (syntax refresh, unresolved-row scan,
        // shaping) reads windows rather than the whole document, so cost scales
        // with the visible region plus the conflict count, not the file.
        let resolved =
            conflict_resolver::generate_resolved_text(&self.conflict_resolver.marker_segments);
        let path = self.conflict_resolver.path.clone();
        self.conflict_resolved_output_projection = None;
        self.conflict_resolved_preview_path = path.clone();
        self.fill_conflict_resolved_output_buffer(resolved, cx);
        self.conflict_resolved_preview_source_revision =
            Some(self.conflict_resolver_input.read_with(cx, |input, _| {
                ResolvedOutputSourceRevision::from_snapshot(&input.text_snapshot())
            }));
        self.rebuild_conflict_resolved_output_block_map(cx);
        self.recompute_conflict_resolved_outline_and_provenance(path.as_ref(), cx);
    }

    /// Load merged text into the resolved-output editor.
    ///
    /// Every path that fills this buffer goes through here, because filling it
    /// has one non-obvious obligation: `set_text` leaves the caret at
    /// end-of-document, and the pane opens scrolled to the top, so a caret
    /// parked at the far end sends the first arrow key (or undo) autoscrolling
    /// to the bottom of a file the user was reading from the top. Park it where
    /// the view actually is; the user has not placed a caret yet.
    pub(in crate::view) fn fill_conflict_resolved_output_buffer(
        &mut self,
        text: impl Into<SharedString>,
        cx: &mut gpui::Context<Self>,
    ) {
        let text = text.into();
        let line_ending = crate::kit::TextInput::detect_line_ending(text.as_ref());
        let theme = self.theme;
        self.conflict_resolver_input.update(cx, |input, cx| {
            input.set_theme(theme, cx);
            input.set_line_ending(line_ending);
            input.set_text(text, cx);
            input.set_selected_range(0..0, false, cx);
        });
    }

    /// Configure the resolved-output `TextInput` for rendering as the editable
    /// output pane. This is called from the render path, so it must stay cheap
    /// and side-effect free: the merged text is materialized into the buffer at
    /// bootstrap (see [`ensure_conflict_resolved_output_materialized`]), not here.
    /// It only points the editor at its shared scroll handle so the line-number
    /// gutter and the column scroll-sync group stay coupled to it.
    pub(in crate::view) fn prepare_conflict_resolved_output_editor(
        &mut self,
        cx: &mut gpui::Context<Self>,
    ) {
        self.sync_conflict_resolved_output_active_conflict_highlight(cx);
        let scroll = self.conflict_resolved_output_editor_scroll.clone();
        let theme = self.theme;
        self.conflict_resolver_input.update(cx, |input, cx| {
            input.set_theme(theme, cx);
            input.set_read_only(false, cx);
            input.set_vertical_scroll_handle(Some(scroll));
            // Lay the editor out at content width so its `overflow_scroll`
            // container carries a horizontal `max_offset` on the shared handle,
            // letting the resolved output scroll-sync with the source columns
            // on the horizontal axis too.
            input.set_content_width_layout(true);
        });
    }

    /// Unresolved rows for `snapshot`, from the cache when it is still current.
    ///
    /// A miss only happens when navigation runs before any refresh has scanned
    /// this revision; the scan is then done once and cached like any other.
    fn conflict_resolved_output_unresolved_rows_for(
        &mut self,
        snapshot: &TextModelSnapshot,
    ) -> UnresolvedRows {
        let key = ResolvedOutputKey::new(
            snapshot,
            &self.conflict_resolver.marker_segments,
            &self.conflict_resolved_output_block_map,
        );
        if let Some((cached_for, rows)) = self.conflict_resolved_output_unresolved_rows.as_ref()
            && *cached_for == key
        {
            return Arc::clone(rows);
        }

        #[cfg(test)]
        {
            self.conflict_resolved_output_full_scans += 1;
        }
        let rows = resolved_output_unresolved_rows(
            &self.conflict_resolver.marker_segments,
            &snapshot.rope(),
            &self.conflict_resolved_output_block_map,
        );
        self.conflict_resolved_output_unresolved_rows = Some((key, Arc::clone(&rows)));
        rows
    }

    /// Rebuild the output highlights when conflict navigation lands on another
    /// conflict, so the yellow wash follows the selection.
    ///
    /// Every other refresh path hangs off the text or the tree, and navigation
    /// moves neither — it only reassigns `active_conflict`, from a dozen call
    /// sites on a state struct that cannot reach the input. Comparing against the
    /// conflict the installed provider was built for catches all of them in one
    /// place, and makes the common render (nothing moved) a single comparison.
    fn sync_conflict_resolved_output_active_conflict_highlight(
        &mut self,
        cx: &mut gpui::Context<Self>,
    ) {
        if self.conflict_resolved_output_highlighted_conflict
            == self.conflict_resolver.active_conflict
        {
            return;
        }
        self.conflict_resolved_output_highlighted_conflict = self.conflict_resolver.active_conflict;
        // Streamed output is drawn row by row from the projection, which reads
        // the active conflict as it renders; only the editable buffer carries
        // highlights that have to be reinstalled.
        if self.conflict_resolved_output_is_streamed() {
            return;
        }
        // With a tree in hand, rebinding the provider is the whole job:
        // navigation moves no text, so the tree, the placeholder mask and the
        // protected spans all still stand. Going through the full syntax refresh
        // would redo them on every jump — and on the tree-less arm it would
        // re-tokenize the entire document, which is exactly the kind of
        // per-keypress cost the live engine exists to avoid.
        if self.conflict_resolved_output_live_syntax.is_some() {
            self.rebind_conflict_resolved_output_highlight_provider(cx);
            return;
        }
        let output_snapshot = self
            .conflict_resolver_input
            .read_with(cx, |input, _| input.text_snapshot());
        self.refresh_conflict_resolved_output_syntax(&output_snapshot, None, cx);
    }

    pub(in crate::view) fn current_conflict_resolved_output_text(
        &self,
        cx: &mut gpui::Context<Self>,
    ) -> String {
        if self.conflict_resolved_output_is_streamed() {
            conflict_resolver::generate_resolved_text(&self.conflict_resolver.marker_segments)
        } else {
            self.conflict_resolver_input
                .read_with(cx, |input, _| input.text().to_string())
        }
    }

    pub(in crate::view) fn conflict_resolver_save_contents_from_text(
        &mut self,
        text: String,
    ) -> String {
        self.conflict_resolver_sync_session_resolutions_from_output(&text);
        text
    }

    pub(in crate::view) fn ensure_prepared_syntax_chunk_poll(
        &mut self,
        cx: &mut gpui::Context<Self>,
    ) {
        if self.syntax_chunk_poll_task.is_some() {
            return;
        }

        if !crate::ui_runtime::current().uses_background_compute() {
            while self.apply_prepared_syntax_chunk_updates(cx) {
                std::thread::sleep(std::time::Duration::from_millis(1));
            }
            self.syntax_chunk_poll_task = None;
            return;
        }

        let task = cx.spawn(
            async move |view: WeakEntity<MainPaneView>, cx: &mut gpui::AsyncApp| loop {
                let should_continue = view
                    .update(cx, |this, cx| this.apply_prepared_syntax_chunk_updates(cx))
                    .unwrap_or(false);

                if !should_continue {
                    break;
                }

                smol::Timer::after(std::time::Duration::from_millis(16)).await;
            },
        );
        self.syntax_chunk_poll_task = Some(task);
    }

    fn apply_prepared_syntax_chunk_updates(&mut self, cx: &mut gpui::Context<Self>) -> bool {
        let mut applied = false;

        let split_left_applied = self
            .file_diff_split_prepared_syntax_document(DiffTextRegion::SplitLeft)
            .map(rows::drain_completed_prepared_diff_syntax_chunk_builds_for_document)
            .unwrap_or(0);
        if split_left_applied > 0 {
            self.file_diff_style_cache_epochs.bump_left();
            applied = true;
        }

        let split_right_applied = self
            .file_diff_split_prepared_syntax_document(DiffTextRegion::SplitRight)
            .map(rows::drain_completed_prepared_diff_syntax_chunk_builds_for_document)
            .unwrap_or(0);
        if split_right_applied > 0 {
            self.file_diff_style_cache_epochs.bump_right();
            applied = true;
        }

        let worktree_preview_applied = self
            .worktree_preview_prepared_syntax_document()
            .map(rows::drain_completed_prepared_diff_syntax_chunk_builds_for_document)
            .unwrap_or(0);
        if worktree_preview_applied > 0 {
            self.worktree_preview_style_cache_epoch =
                self.worktree_preview_style_cache_epoch.wrapping_add(1);
            applied = true;
        }

        if rows::drain_completed_prepared_diff_syntax_chunk_builds() > 0 {
            applied = true;
        }

        if applied {
            cx.notify();
        }

        let pending = rows::has_pending_prepared_diff_syntax_chunk_builds();
        if !pending {
            self.syntax_chunk_poll_task = None;
        }
        pending
    }

    /// Build the first tree off-thread after the foreground budget ran out.
    ///
    /// Guarded on the revision it is building for, so a burst of refreshes over
    /// the same text schedules one parse rather than one per call. A result for
    /// text the buffer has since moved past is never installed — it is re-issued
    /// against the current text instead, so the pane cannot be left on the
    /// heuristic fallback by an edit that raced the parse.
    fn ensure_conflict_resolved_output_live_syntax_build(
        &mut self,
        language: rows::DiffSyntaxLanguage,
        rope: crate::kit::rope::Rope,
        mask: Arc<[Range<usize>]>,
        revision: ResolvedOutputSourceRevision,
        cx: &mut gpui::Context<Self>,
    ) {
        if self.conflict_resolved_output_live_syntax_building == Some(revision) {
            return;
        }
        self.conflict_resolved_output_live_syntax_building = Some(revision);

        let build_mask = Arc::clone(&mask);
        self.conflict_resolved_output_live_syntax_build =
            Some(cx.spawn(async move |view: WeakEntity<MainPaneView>, cx| {
                let build = move || rows::LiveSyntaxDocument::new(language, rope, build_mask, None);
                let built = if crate::ui_runtime::current().uses_background_compute() {
                    smol::unblock(build).await
                } else {
                    build()
                };
                let _ = view.update(cx, |this, cx| {
                    if this.conflict_resolved_output_live_syntax_building != Some(revision) {
                        // A newer generation owns the guard. Leave it alone --
                        // clearing it here would let its own scheduling check
                        // pass again and start a duplicate build.
                        return;
                    }
                    this.conflict_resolved_output_live_syntax_building = None;
                    let Some(document) = built else {
                        // Unbudgeted, so this is not a timeout: the text is past
                        // the size ceiling or the language has no wired grammar.
                        // Both are permanent, so re-issuing would spin. The
                        // heuristic arm of the refresh is the right answer here,
                        // and it is the same one the diff panes take.
                        return;
                    };
                    // Zed's `parse_again` (`Buffer::reparse`): a result for text
                    // the buffer has moved past is useless, but so is waiting --
                    // nothing else is guaranteed to come along and ask again, so
                    // re-issue from where the buffer is now.
                    let still_current = this.conflict_resolver_input.read_with(cx, |input, _| {
                        ResolvedOutputSourceRevision::from_snapshot(&input.text_snapshot())
                    }) == revision;
                    if !still_current {
                        this.reissue_conflict_resolved_output_live_syntax_build(cx);
                        return;
                    }
                    this.conflict_resolved_output_live_syntax = Some(document);
                    // Record what it was built for, or the next refresh sees a
                    // stale source, retries in the foreground, fails the budget
                    // again and schedules another build -- forever.
                    this.conflict_resolved_output_live_syntax_source = Some((revision, mask));
                    this.rebind_conflict_resolved_output_highlight_provider(cx);
                });
            }));
    }

    /// Re-run the off-thread first parse against the buffer as it stands now.
    ///
    /// Called when a build lands for text the buffer has already moved past.
    /// Recomputes the source the way [`Self::refresh_conflict_resolved_output_syntax`]
    /// does, so the two cannot disagree about what the tree is being built over.
    /// A no-op once a document exists -- from there on, edits go through
    /// `sync`, which always has a tree to fall back on.
    fn reissue_conflict_resolved_output_live_syntax_build(&mut self, cx: &mut gpui::Context<Self>) {
        if self.conflict_resolved_output_live_syntax.is_some() {
            return;
        }
        let Some(language) = self.conflict_resolved_preview_syntax_language else {
            return;
        };
        let output_snapshot = self
            .conflict_resolver_input
            .read_with(cx, |input, _| input.text_snapshot());
        let rope = output_snapshot.rope();
        let protected_ranges = resolved_output_placeholder_protected_ranges(&rope);
        let mask = resolved_output_live_syntax_mask(protected_ranges.as_ref(), &rope);
        let revision = ResolvedOutputSourceRevision::from_snapshot(&output_snapshot);
        self.ensure_conflict_resolved_output_live_syntax_build(
            language,
            output_snapshot.rope(),
            mask,
            revision,
            cx,
        );
    }

    /// Finish a reparse the foreground budget could not.
    ///
    /// Only reachable when an edit landed on a document too large to reparse in
    /// the budget. The viewport is not blocked meanwhile: the `tree.edit()`ed
    /// tree is already positionally correct, so it keeps painting — this just
    /// restores exactness near the edit.
    fn ensure_conflict_resolved_output_live_syntax_reparse(
        &mut self,
        cx: &mut gpui::Context<Self>,
    ) {
        let Some(request) = self
            .conflict_resolved_output_live_syntax
            .as_ref()
            .and_then(rows::LiveSyntaxDocument::background_reparse_request)
        else {
            self.conflict_resolved_output_live_syntax_reparse = None;
            return;
        };

        self.conflict_resolved_output_live_syntax_reparse =
            Some(cx.spawn(async move |view: WeakEntity<MainPaneView>, cx| {
                let reparse = move || rows::live_syntax_reparse(request);
                let parsed = if crate::ui_runtime::current().uses_background_compute() {
                    smol::unblock(reparse).await
                } else {
                    reparse()
                };
                let Some((version, tree, injections)) = parsed else {
                    return;
                };
                let _ = view.update(cx, |this, cx| {
                    let adopted = this
                        .conflict_resolved_output_live_syntax
                        .as_mut()
                        .is_some_and(|document| {
                            document.adopt_background_tree(version, tree, injections)
                        });
                    if !adopted {
                        // The buffer moved while this was in flight, so the tree
                        // describes text that no longer exists. Re-issue from
                        // wherever the document is now.
                        this.conflict_resolved_output_live_syntax_reparse = None;
                        this.ensure_conflict_resolved_output_live_syntax_reparse(cx);
                        return;
                    }
                    this.conflict_resolved_output_live_syntax_reparse = None;
                    this.rebind_conflict_resolved_output_highlight_provider(cx);
                });
            }));
    }

    /// Hand the input a provider over the document's current tree.
    fn rebind_conflict_resolved_output_highlight_provider(&mut self, cx: &mut gpui::Context<Self>) {
        let Some((version, snapshot)) = self
            .conflict_resolved_output_live_syntax
            .as_ref()
            .map(|document| (document.version(), document.snapshot(self.theme)))
        else {
            return;
        };
        let output_snapshot = self
            .conflict_resolver_input
            .read_with(cx, |input, _| input.text_snapshot());
        self.conflict_resolved_output_highlighted_conflict = self.conflict_resolver.active_conflict;
        // Reuse the scan from the last refresh when the text has not moved.
        // Rebinding happens on every conflict jump, and rescanning here is what
        // made navigation scale with the file rather than with the conflict.
        let rows = self.conflict_resolved_output_unresolved_rows_for(&output_snapshot);
        let unresolved_spans = resolved_output_unresolved_spans_for_active(
            rows.as_ref(),
            self.conflict_resolver.active_conflict,
        );
        let binding_key = resolved_output_live_provider_binding_key(
            version,
            self.conflict_resolved_output_provider_theme_epoch,
            &unresolved_spans,
        );
        let provider =
            resolved_output_live_highlight_provider(self.theme, snapshot, unresolved_spans);
        let source_len = output_snapshot.len();
        self.conflict_resolver_input.update(cx, |input, cx| {
            input.set_highlight_provider_with_key(binding_key, provider, source_len, cx);
        });
    }

    /// Bring the resolved output's live tree up to date with `output_snapshot`
    /// and rebind the highlight provider to it.
    ///
    /// `edit` is the coalesced `(replaced, inserted)` span, or `None` when the
    /// text was replaced wholesale (bootstrap, a conflict resolution, an undo of
    /// one) — which reparses from scratch.
    ///
    /// Cheap enough to run on the keystroke: the tree is edited in place and the
    /// reparse reuses it, rather than rebuilding the prepared document this
    /// replaced.
    ///
    /// The root parse is incremental; the *injected* layers are not. A reparse
    /// re-runs the injection query over the whole document and reparses every
    /// injected region from scratch, each with its own copy of the foreground
    /// budget. On a document with many injections (fenced blocks, `<script>`
    /// bodies) that is the dominant per-keystroke cost and does scale with the
    /// document — the outstanding gap against Zed's `SyntaxMap`, which keys
    /// layers by (language, range) and reparses them incrementally.
    fn refresh_conflict_resolved_output_syntax(
        &mut self,
        output_snapshot: &TextModelSnapshot,
        edit: Option<(Range<usize>, Range<usize>)>,
        cx: &mut gpui::Context<Self>,
    ) {
        // Everything below is derived from the *text*. When the text has not
        // moved, all of it still stands and the only thing that can need
        // updating is the provider binding — a theme change, or a different
        // conflict wearing the wash.
        //
        // Only when the caller reports no edit. An `edit` is the caller stating
        // that the text moved, and the tree must be folded forward even if the
        // revision happens to look settled — skipping the sync there leaves the
        // tree describing the pre-edit text, which shows up as the row you just
        // typed into keeping its old colours.
        let revision = ResolvedOutputSourceRevision::from_snapshot(output_snapshot);
        let text_is_unchanged = self
            .conflict_resolved_output_live_syntax_source
            .as_ref()
            .is_some_and(|(built_for, _)| *built_for == revision);
        // The language has to match too, not just the text. The reuse check
        // that would drop a document built by the wrong grammar lives *below*
        // this return, so leaving it out lets a language change with unchanged
        // text keep the previous grammar's tree — the state
        // `conflict_resolver_invalidate_resolved_outline` leaves behind, which
        // clears the language but not the live document.
        let language_is_unchanged = self
            .conflict_resolved_output_live_syntax
            .as_ref()
            .is_some_and(|document| {
                Some(document.language()) == self.conflict_resolved_preview_syntax_language
            });
        if edit.is_none() && text_is_unchanged && language_is_unchanged {
            self.conflict_resolved_output_highlighted_conflict =
                self.conflict_resolver.active_conflict;
            self.rebind_conflict_resolved_output_highlight_provider(cx);
            return;
        }

        // Every read below goes through the rope, and nothing on this path
        // builds either whole-document cache — not the flattened string, and
        // not the line-start array, which is the quieter of the two and was the
        // one that lingered here. `no_materialization_tests` asserts both.
        let rope = output_snapshot.rope();
        self.conflict_resolved_output_highlighted_conflict = self.conflict_resolver.active_conflict;
        #[cfg(test)]
        {
            self.conflict_resolved_output_full_scans += 1;
        }
        let unresolved_rows = resolved_output_unresolved_rows(
            &self.conflict_resolver.marker_segments,
            &rope,
            &self.conflict_resolved_output_block_map,
        );
        self.conflict_resolved_output_unresolved_rows = Some((
            ResolvedOutputKey::new(
                output_snapshot,
                &self.conflict_resolver.marker_segments,
                &self.conflict_resolved_output_block_map,
            ),
            Arc::clone(&unresolved_rows),
        ));
        let unresolved_spans = resolved_output_unresolved_spans_for_active(
            unresolved_rows.as_ref(),
            self.conflict_resolver.active_conflict,
        );
        // The placeholder rows are a rendering of open decisions, so hand them
        // to the buffer as uneditable spans — and hide the same spans from the
        // parser, which would otherwise read `<Merge Conflict>` as code.
        let protected_ranges = resolved_output_placeholder_protected_ranges(&rope);
        let mask = resolved_output_live_syntax_mask(protected_ranges.as_ref(), &rope);
        let budget = Some(self.full_document_syntax_budget().foreground_parse);

        let language = self.conflict_resolved_preview_syntax_language;
        let reusable = self
            .conflict_resolved_output_live_syntax
            .as_ref()
            .is_some_and(|document| Some(document.language()) == language);
        if !reusable {
            self.conflict_resolved_output_live_syntax = None;
            self.conflict_resolved_output_live_syntax_source = None;
        }

        let revision = ResolvedOutputSourceRevision::from_snapshot(output_snapshot);
        let current = self
            .conflict_resolved_output_live_syntax_source
            .as_ref()
            .is_some_and(|(built_for, built_mask)| {
                *built_for == revision && built_mask.as_ref() == mask.as_ref()
            });

        match self.conflict_resolved_output_live_syntax.as_mut() {
            // Nothing about the buffer moved. The tree stands, and so does its
            // version — the binding key below folds in the theme and the
            // unresolved spans, so an overlay change still rebinds while a
            // no-op re-entry does not, which is what stops this method from
            // re-triggering the observe that called it.
            Some(_) if current => {}
            Some(document) => {
                let outcome = document.sync(rope.clone(), Arc::clone(&mask), edit, budget);
                if outcome == rows::LiveSyntaxSyncOutcome::Abandoned {
                    // The edit took the buffer past the size ceiling, so the
                    // document now describes text that no longer exists. Drop it
                    // and take the heuristic arm below, which is the same answer
                    // a buffer that started out this large would have got.
                    self.conflict_resolved_output_live_syntax = None;
                    self.conflict_resolved_output_live_syntax_source = None;
                } else {
                    self.conflict_resolved_output_live_syntax_source =
                        Some((revision, Arc::clone(&mask)));
                }
            }
            None => {
                // Zed's fast path (`Buffer::reparse` under `sync_parse_timeout`):
                // worth a budgeted attempt because a small buffer finishes inside
                // it and never shows a frame of unhighlighted text. Skipped when
                // a build for exactly this text is already off-thread -- that
                // attempt has demonstrably failed once, so re-running it on the
                // keystroke path is pure latency.
                let already_building =
                    self.conflict_resolved_output_live_syntax_building == Some(revision);
                self.conflict_resolved_output_live_syntax =
                    language.filter(|_| !already_building).and_then(|language| {
                        rows::LiveSyntaxDocument::new(
                            language,
                            rope.clone(),
                            Arc::clone(&mask),
                            budget,
                        )
                    });
                self.conflict_resolved_output_live_syntax_source = self
                    .conflict_resolved_output_live_syntax
                    .is_some()
                    .then(|| (revision, Arc::clone(&mask)));

                // A first parse has no tree to fall back on, so exhausting the
                // foreground budget leaves nothing at all -- and an incremental
                // reparse can never rescue it, because there is no document to
                // reparse. Finish it off-thread instead. Not a rare path: the
                // live budget is 1ms and a cold parse of a ~10KB file is
                // already over it, so without this the resolved output would
                // sit on heuristic tokens for the whole session.
                if let Some(language) =
                    language.filter(|_| self.conflict_resolved_output_live_syntax.is_none())
                {
                    self.ensure_conflict_resolved_output_live_syntax_build(
                        language,
                        rope.clone(),
                        Arc::clone(&mask),
                        revision,
                        cx,
                    );
                }
            }
        }

        let live = self
            .conflict_resolved_output_live_syntax
            .as_ref()
            .map(|document| (document.version(), document.snapshot(self.theme)));
        self.ensure_conflict_resolved_output_live_syntax_reparse(cx);

        self.conflict_resolver_input.update(cx, |input, cx| {
            input.set_protected_ranges(protected_ranges);
            match live {
                Some((version, snapshot)) => {
                    let provider = resolved_output_live_highlight_provider(
                        self.theme,
                        snapshot,
                        unresolved_spans.clone(),
                    );
                    // Rebinding under a fresh key whenever the text moved is
                    // load-bearing: it resets the interpolation that would
                    // otherwise map these already-current highlights through a
                    // stale patch.
                    let binding_key = resolved_output_live_provider_binding_key(
                        version,
                        self.conflict_resolved_output_provider_theme_epoch,
                        &unresolved_spans,
                    );
                    input.set_highlight_provider_with_key(binding_key, provider, rope.len(), cx);
                }
                None => {
                    // Heuristic tokens, with the open conflicts still called out
                    // in red. Reachable in exactly two states, both permanent and
                    // both shared with the diff panes above -- the language has
                    // no wired grammar, or the text is past
                    // `PREPARED_DIFF_SYNTAX_DOCUMENT_MAX_TEXT_BYTES`. It is *not*
                    // a general fallback: the tokenizer knows only keywords,
                    // strings, numbers and comments, so landing here while a
                    // grammar exists is precisely the bug where the output stops
                    // matching the panes above it. A budget-exhausted first parse
                    // must go to `ensure_conflict_resolved_output_live_syntax_build`
                    // instead.
                    //
                    // A provider rather than a whole-document `set_highlights`:
                    // this arm is reached by the *largest* buffers, and the
                    // tokenizer is line-local, so answering per window is both
                    // exact and proportional to the viewport.
                    let provider = resolved_output_heuristic_highlight_provider(
                        self.theme,
                        rope.clone(),
                        language,
                        unresolved_spans.clone(),
                    );
                    let binding_key = resolved_output_heuristic_provider_binding_key(
                        revision,
                        self.conflict_resolved_output_provider_theme_epoch,
                        &unresolved_spans,
                    );
                    input.set_highlight_provider_with_key(binding_key, provider, rope.len(), cx);
                }
            }
        });
    }

    /// Schedule a background tree-sitter parse for one merge-input side.
    ///
    /// When the parse completes, the prepared document is injected into the
    /// global cache and the three-way styled-text cache is cleared so the next
    /// render picks up document-based syntax highlighting.
    pub(in crate::view) fn ensure_conflict_three_way_background_syntax_prepare(
        &mut self,
        side: ThreeWayColumn,
        text: SharedString,
        line_starts: Arc<[usize]>,
        language: rows::DiffSyntaxLanguage,
        source_hash: Option<u64>,
        cx: &mut gpui::Context<Self>,
    ) {
        if self.conflict_three_way_syntax_inflight[side] {
            return;
        }
        self.conflict_three_way_syntax_inflight[side] = true;
        let expected_source_hash = source_hash;
        cx.spawn(
            async move |view: WeakEntity<MainPaneView>, cx: &mut gpui::AsyncApp| {
                let prepare_document = move || {
                    rows::prepare_diff_syntax_document_in_background_text_with_reuse(
                        language,
                        rows::DiffSyntaxMode::Auto,
                        text,
                        line_starts,
                        None,
                        None,
                    )
                };
                let parsed = if crate::ui_runtime::current().uses_background_compute() {
                    smol::unblock(prepare_document).await
                } else {
                    prepare_document()
                };

                let _ = view.update(cx, |this, cx| {
                    this.conflict_three_way_syntax_inflight[side] = false;

                    // Stale: source hash changed while we were parsing.
                    if this.conflict_resolver.source_hash != expected_source_hash {
                        return;
                    }

                    if let Some(parsed) = parsed {
                        let document =
                            rows::inject_background_prepared_diff_syntax_document(parsed);
                        this.conflict_three_way_prepared_syntax_documents[side] = Some(document);
                        // Invalidate cached styled text so the next render uses
                        // the prepared document across three-way and two-way
                        // conflict views instead of per-line fallback styling.
                        this.clear_conflict_diff_style_caches_preserving_query();
                        this.conflict_three_way_segments_cache.clear();
                        this.conflict_three_way_query_segments_cache.clear();
                        cx.notify();
                    }
                });
            },
        )
        .detach();
    }

    pub(in crate::view) fn clear_diff_text_query_overlay_cache(&mut self) {
        self.diff_text_query_segments_cache.clear();
        self.diff_text_query_cache_query = SharedString::default();
        self.diff_text_query_cache_options = Default::default();
        self.diff_text_query_cache_matcher = None;
        self.diff_text_query_cache_generation =
            self.diff_text_query_cache_generation.wrapping_add(1);
    }

    pub(in crate::view) fn invalidate_diff_text_query_overlay_cache(
        &mut self,
        query: &str,
        options: super::diff_search::DiffSearchOptions,
    ) {
        if self.diff_text_query_cache_query.as_ref() != query
            || self.diff_text_query_cache_options != options
        {
            self.diff_text_query_cache_query = query.to_string().into();
            self.diff_text_query_cache_options = options;
            self.diff_text_query_cache_matcher = (!query.is_empty())
                .then(|| super::diff_search::DiffSearchMatcher::new(query, options));
            self.diff_text_query_cache_generation =
                self.diff_text_query_cache_generation.wrapping_add(1);
        }
    }

    pub(in crate::view) fn sync_diff_text_query_overlay_cache(
        &mut self,
        query: &str,
        options: super::diff_search::DiffSearchOptions,
    ) {
        self.invalidate_diff_text_query_overlay_cache(query, options);
    }

    pub(in crate::view) fn clear_diff_text_style_caches(&mut self) {
        self.diff_text_segments_cache.clear();
        self.clear_diff_text_query_overlay_cache();
    }

    pub(in crate::view) fn clear_worktree_preview_segments_cache(&mut self) {
        self.worktree_preview_segments_cache.clear();
        self.worktree_preview_cache_write_blocked_until_rev = None;
    }

    pub(in crate::view) fn clear_conflict_diff_query_overlay_caches(&mut self) {
        self.conflict_diff_query_segments_cache_split.clear();
        self.conflict_three_way_query_segments_cache.clear();
        self.conflict_diff_query_cache_query = SharedString::default();
        self.conflict_diff_query_cache_options = Default::default();
    }

    pub(in crate::view) fn clear_conflict_diff_style_caches_preserving_query(&mut self) {
        self.conflict_diff_segments_cache_split.clear();
        self.conflict_diff_query_segments_cache_split.clear();
        self.conflict_three_way_query_segments_cache.clear();
    }

    pub(in crate::view) fn sync_conflict_diff_query_overlay_caches(
        &mut self,
        query: &str,
        options: super::diff_search::DiffSearchOptions,
    ) {
        if self.conflict_diff_query_cache_query.as_ref() != query
            || self.conflict_diff_query_cache_options != options
        {
            self.conflict_diff_query_cache_query = query.to_string().into();
            self.conflict_diff_query_cache_options = options;
            self.conflict_diff_query_segments_cache_split.clear();
            self.conflict_three_way_query_segments_cache.clear();
        }
    }

    pub(in crate::view) fn clear_conflict_diff_style_caches(&mut self) {
        self.clear_conflict_diff_style_caches_preserving_query();
        self.conflict_diff_query_cache_query = SharedString::default();
        self.conflict_diff_query_cache_options = Default::default();
    }

    pub(super) fn conflict_resolver_invalidate_resolved_outline(&mut self) {
        self.conflict_resolver.resolver_pending_recompute_seq = self
            .conflict_resolver
            .resolver_pending_recompute_seq
            .wrapping_add(1);
        self.conflict_resolved_preview_path = None;
        self.conflict_resolved_preview_source_revision = None;
        self.conflict_resolved_output_projection = None;
        self.conflict_resolved_preview_text = TextModelSnapshot::default();
        self.conflict_resolved_preview_syntax_language = None;
        self.conflict_resolved_preview_line_count = 0;
        self.conflict_resolved_preview_line_starts = Arc::default();
        self.conflict_resolved_output_measure_row = 0;
        self.conflict_resolved_outline_stash = None;
        self.conflict_three_way_prepared_syntax_documents = ThreeWaySides::default();
        self.conflict_three_way_syntax_inflight = ThreeWaySides::default();
        self.conflict_three_way_segments_cache.clear();
        self.conflict_three_way_query_segments_cache.clear();
        self.conflict_resolver.resolved_outline = ResolvedOutlineData::default();
        self.conflict_resolver.resolved_output_visible_dirty = true;
    }

    fn resolved_outline_source_view(&self) -> ResolvedOutlineSourceView<'_> {
        match self.conflict_resolver.view_mode {
            ConflictResolverViewMode::ThreeWay => ResolvedOutlineSourceView::ThreeWay {
                base_text: &self.conflict_resolver.three_way_text.base,
                base_line_starts: self
                    .conflict_resolver
                    .three_way_line_starts_ref(ThreeWayColumn::Base),
                ours_text: &self.conflict_resolver.three_way_text.ours,
                ours_line_starts: self
                    .conflict_resolver
                    .three_way_line_starts_ref(ThreeWayColumn::Ours),
                theirs_text: &self.conflict_resolver.three_way_text.theirs,
                theirs_line_starts: self
                    .conflict_resolver
                    .three_way_line_starts_ref(ThreeWayColumn::Theirs),
            },
            ConflictResolverViewMode::TwoWayDiff => ResolvedOutlineSourceView::TwoWay {
                ours_text: &self.conflict_resolver.three_way_text.ours,
                ours_line_starts: self
                    .conflict_resolver
                    .three_way_line_starts_ref(ThreeWayColumn::Ours),
                theirs_text: &self.conflict_resolver.three_way_text.theirs,
                theirs_line_starts: self
                    .conflict_resolver
                    .three_way_line_starts_ref(ThreeWayColumn::Theirs),
            },
        }
    }

    /// Snapshot everything the outline recompute needs, so it can run detached.
    ///
    /// This materializes the output, and unlike the syntax path it is not an
    /// artifact worth removing: the outline assigns a
    /// provenance to *every* row by comparing its text against the three source
    /// sides, so the work is O(document) whatever it reads through, and the copy
    /// is a small constant beside it.
    ///
    /// What keeps that off the keystroke path is *where* it is called from, not
    /// its cost: both the production task and the synchronous test arm build the
    /// request only once the debounce
    /// (`CONFLICT_RESOLVED_OUTLINE_DEBOUNCE_MS`) has settled and the recompute
    /// is going to run. Hoisting this call above that check charges every
    /// keystroke for a copy of the document that is then discarded.
    fn background_resolved_outline_recompute_request(
        &self,
        output_snapshot: &TextModelSnapshot,
    ) -> BackgroundResolvedOutlineRecomputeRequest {
        let output_text: Arc<str> = output_snapshot.as_shared_string().into();
        let output_line_count = output_snapshot.shared_line_starts().len().max(1);
        let sources = match self.conflict_resolver.view_mode {
            ConflictResolverViewMode::ThreeWay => OwnedResolvedOutlineSourceData::ThreeWay {
                base_text: self.conflict_resolver.three_way_text.base.clone().into(),
                base_line_starts: self
                    .conflict_resolver
                    .three_way_shared_line_starts(ThreeWayColumn::Base),
                ours_text: self.conflict_resolver.three_way_text.ours.clone().into(),
                ours_line_starts: self
                    .conflict_resolver
                    .three_way_shared_line_starts(ThreeWayColumn::Ours),
                theirs_text: self.conflict_resolver.three_way_text.theirs.clone().into(),
                theirs_line_starts: self
                    .conflict_resolver
                    .three_way_shared_line_starts(ThreeWayColumn::Theirs),
            },
            ConflictResolverViewMode::TwoWayDiff => OwnedResolvedOutlineSourceData::TwoWay {
                ours_text: self.conflict_resolver.three_way_text.ours.clone().into(),
                ours_line_starts: self
                    .conflict_resolver
                    .three_way_shared_line_starts(ThreeWayColumn::Ours),
                theirs_text: self.conflict_resolver.three_way_text.theirs.clone().into(),
                theirs_line_starts: self
                    .conflict_resolver
                    .three_way_shared_line_starts(ThreeWayColumn::Theirs),
            },
        };

        BackgroundResolvedOutlineRecomputeRequest {
            output_text,
            output_line_count,
            marker_segments: self.conflict_resolver.marker_segments.clone(),
            block_map: self.conflict_resolved_output_block_map.clone(),
            sources,
        }
    }

    fn stash_current_conflict_resolved_outline_state(&mut self) {
        let line_count = self.conflict_resolved_preview_line_count;
        if line_count == 0
            || self.conflict_resolver.resolved_outline.meta.len() != line_count
            || self.conflict_resolver.resolved_outline.markers.len() != line_count
        {
            return;
        }

        self.conflict_resolved_outline_stash = Some(StashedResolvedOutlineState {
            text: self.conflict_resolved_preview_text.clone(),
            line_starts: self.conflict_resolved_preview_line_starts.clone(),
            marker_segments: self.conflict_resolver.marker_segments.clone(),
            view_mode: self.conflict_resolver.view_mode,
            outline: self.conflict_resolver.resolved_outline.clone(),
        });
    }

    fn resolved_outline_incremental_base(&self) -> Option<ResolvedOutlineIncrementalBase<'_>> {
        if self.conflict_resolved_output_is_streamed() {
            return None;
        }
        if let Some(stash) = self.conflict_resolved_outline_stash.as_ref() {
            return Some(ResolvedOutlineIncrementalBase {
                text: &stash.text,
                line_starts: &stash.line_starts,
                marker_segments: &stash.marker_segments,
                view_mode: stash.view_mode,
            });
        }

        let line_count = self.conflict_resolved_preview_line_count;
        if line_count == 0
            || self.conflict_resolver.resolved_outline.meta.len() != line_count
            || self.conflict_resolver.resolved_outline.markers.len() != line_count
        {
            return None;
        }

        Some(ResolvedOutlineIncrementalBase {
            text: &self.conflict_resolved_preview_text,
            line_starts: &self.conflict_resolved_preview_line_starts,
            marker_segments: &self.conflict_resolver.marker_segments,
            view_mode: self.conflict_resolver.view_mode,
        })
    }

    fn sync_conflict_resolved_preview_snapshot(
        &mut self,
        output_snapshot: &TextModelSnapshot,
        path: Option<&std::path::PathBuf>,
        clear_outline: bool,
        cx: &mut gpui::Context<Self>,
    ) {
        if clear_outline {
            self.stash_current_conflict_resolved_outline_state();
        }
        self.conflict_resolved_preview_source_revision =
            Some(ResolvedOutputSourceRevision::from_snapshot(output_snapshot));
        self.conflict_resolved_preview_line_starts = output_snapshot.shared_line_starts();
        self.conflict_resolved_preview_syntax_language =
            path.and_then(rows::diff_syntax_language_for_path);
        self.conflict_resolved_preview_line_count = output_snapshot.line_count().max(1);
        self.conflict_resolved_output_measure_row = resolved_output_measure_row(output_snapshot);
        // Syntax no longer *waits* on this debounce — it tracks the buffer on
        // the keystroke, in the `cx.observe` on `conflict_resolver_input`. The
        // call stays because this method is also how the language arrives
        // (from `path`) and how a wholesale text replacement lands, neither of
        // which produces edit deltas. It reparses only if the buffer actually
        // differs from what the tree already describes, so on the common path
        // it is a version bump and nothing more.
        self.refresh_conflict_resolved_output_syntax(output_snapshot, None, cx);
        self.conflict_resolved_preview_text = output_snapshot.clone();

        if clear_outline {
            self.conflict_resolver.resolved_outline = ResolvedOutlineData::default();
            self.conflict_resolver.resolved_output_visible_dirty = true;
            self.conflict_resolver.resolved_outline_gutter_rows.clear();
        }
    }

    fn apply_resolved_outline_computation(
        &mut self,
        path: Option<&std::path::PathBuf>,
        trace_started: Instant,
        computed: ResolvedOutlineComputation,
    ) {
        self.conflict_resolved_outline_stash = None;
        self.conflict_resolver.resolved_outline = computed.outline;
        self.conflict_resolver.resolved_output_visible_dirty = true;
        self.conflict_resolver.resolved_outline_gutter_rows.clear();
        record_resolved_outline_trace(path, trace_started, self, computed.output_line_count);
    }

    pub(super) fn recompute_conflict_resolved_outline_and_provenance(
        &mut self,
        path: Option<&std::path::PathBuf>,
        cx: &mut gpui::Context<Self>,
    ) {
        if self.conflict_resolved_output_is_streamed() {
            let _ = cx;
            self.refresh_streamed_resolved_output_preview_from_markers(path);
            return;
        }
        let _perf_scope = perf::span(ViewPerfSpan::RecomputeResolvedOutline);
        let trace_started = Instant::now();
        let output_snapshot = self
            .conflict_resolver_input
            .read_with(cx, |input, _| input.text_snapshot());
        let output_text = output_snapshot.as_ref();
        let output_line_count = output_snapshot.shared_line_starts().len().max(1);
        let computed = compute_resolved_outline_computation(
            output_text,
            output_line_count,
            &self.conflict_resolver.marker_segments,
            &self.conflict_resolved_output_block_map,
            self.resolved_outline_source_view(),
        );
        self.sync_conflict_resolved_preview_snapshot(&output_snapshot, path, false, cx);
        self.apply_resolved_outline_computation(path, trace_started, computed);
    }

    fn recompute_conflict_resolved_outline_and_provenance_incremental(
        &mut self,
        path: Option<&std::path::PathBuf>,
        delta: ResolvedOutlineDelta,
        cx: &mut gpui::Context<Self>,
    ) -> bool {
        if self.conflict_resolved_output_is_streamed() {
            let _ = path;
            let _ = delta;
            let _ = cx;
            return false;
        }
        let Some(base) = self.resolved_outline_incremental_base() else {
            return false;
        };
        let old_text_snapshot = base.text.clone();
        let old_text = old_text_snapshot.as_ref();
        let output_snapshot = self
            .conflict_resolver_input
            .read_with(cx, |input, _| input.text_snapshot());
        let output_text = output_snapshot.as_ref();
        let old_line_starts = base.line_starts.clone();
        let old_line_count = old_line_starts.len().max(1);
        let new_line_starts = output_snapshot.shared_line_starts();
        let new_line_count = new_line_starts.len().max(1);
        if old_line_starts.is_empty() {
            return false;
        }
        let used_stash = self.conflict_resolved_outline_stash.is_some();
        let delta = if used_stash {
            resolved_outline_delta_between_texts(old_text, output_text)
        } else {
            Some(delta)
        };
        let Some(delta) = delta else {
            return false;
        };
        if delta.old_range.start > delta.old_range.end
            || delta.new_range.start > delta.new_range.end
            || delta.old_range.end > old_text.len()
            || delta.new_range.end > output_text.len()
        {
            return false;
        }

        let old_dirty_lines = dirty_byte_range_to_line_range(
            old_line_starts.as_ref(),
            old_text.len(),
            delta.old_range.clone(),
        );
        let new_dirty_lines = dirty_byte_range_to_line_range(
            new_line_starts.as_ref(),
            output_text.len(),
            delta.new_range.clone(),
        );
        let mut old_affected = old_dirty_lines.clone();
        let mut new_affected = new_dirty_lines.clone();
        old_affected.start = old_affected.start.saturating_sub(1);
        old_affected.end = old_affected.end.saturating_add(1).min(old_line_count);
        new_affected.start = new_affected.start.saturating_sub(1);
        new_affected.end = new_affected.end.saturating_add(1).min(new_line_count);

        let Some(old_block_ranges) =
            resolved_output_conflict_block_ranges_in_text(base.marker_segments, old_text)
        else {
            return false;
        };
        let new_block_ranges = match resolved_output_conflict_block_line_ranges(
            &self.conflict_resolver.marker_segments,
            output_text,
            &self.conflict_resolved_output_block_map,
        ) {
            Some(ranges) if ranges.len() == old_block_ranges.len() => ranges,
            _ => remap_resolved_output_conflict_block_ranges_for_delta(
                old_block_ranges.as_slice(),
                old_dirty_lines.clone(),
                new_dirty_lines.clone(),
                new_line_count,
            ),
        };
        if old_block_ranges.len() != new_block_ranges.len() {
            return false;
        }

        let mut touched_conflicts: FxHashSet<usize> = FxHashSet::default();
        for (conflict_ix, range) in old_block_ranges.iter().enumerate() {
            if line_ranges_intersect(range, &old_affected) {
                touched_conflicts.insert(conflict_ix);
            }
        }
        for (conflict_ix, range) in new_block_ranges.iter().enumerate() {
            if line_ranges_intersect(range, &new_affected) {
                touched_conflicts.insert(conflict_ix);
            }
        }
        for conflict_ix in &touched_conflicts {
            if let Some(old_range) = old_block_ranges.get(*conflict_ix) {
                old_affected.start = old_affected.start.min(old_range.start);
                old_affected.end = old_affected.end.max(old_range.end).min(old_line_count);
            }
            if let Some(new_range) = new_block_ranges.get(*conflict_ix) {
                new_affected.start = new_affected.start.min(new_range.start);
                new_affected.end = new_affected.end.max(new_range.end).min(new_line_count);
            }
        }

        let mut recompute_conflicts = Vec::new();
        for (conflict_ix, new_range) in new_block_ranges.iter().enumerate() {
            if line_ranges_intersect(new_range, &new_affected) {
                recompute_conflicts.push(conflict_ix);
                if let Some(old_range) = old_block_ranges.get(conflict_ix) {
                    old_affected.start = old_affected.start.min(old_range.start);
                    old_affected.end = old_affected.end.max(old_range.end).min(old_line_count);
                }
                new_affected.start = new_affected.start.min(new_range.start);
                new_affected.end = new_affected.end.max(new_range.end).min(new_line_count);
            }
        }
        if old_affected.start != new_affected.start {
            return false;
        }

        let old_view_mode = base.view_mode;
        let new_view_mode = self.conflict_resolver.view_mode;
        let middle_meta = {
            let mut source_lookup: FxHashMap<
                &str,
                (conflict_resolver::ResolvedLineSource, Option<u32>),
            > = FxHashMap::default();
            match new_view_mode {
                ConflictResolverViewMode::ThreeWay => {
                    insert_lookup_from_indexed_text(
                        &mut source_lookup,
                        conflict_resolver::ResolvedLineSource::C,
                        &self.conflict_resolver.three_way_text.theirs,
                        self.conflict_resolver
                            .three_way_line_starts_ref(ThreeWayColumn::Theirs),
                    );
                    insert_lookup_from_indexed_text(
                        &mut source_lookup,
                        conflict_resolver::ResolvedLineSource::B,
                        &self.conflict_resolver.three_way_text.ours,
                        self.conflict_resolver
                            .three_way_line_starts_ref(ThreeWayColumn::Ours),
                    );
                    insert_lookup_from_indexed_text(
                        &mut source_lookup,
                        conflict_resolver::ResolvedLineSource::A,
                        &self.conflict_resolver.three_way_text.base,
                        self.conflict_resolver
                            .three_way_line_starts_ref(ThreeWayColumn::Base),
                    );
                }
                ConflictResolverViewMode::TwoWayDiff => {
                    insert_lookup_from_indexed_text(
                        &mut source_lookup,
                        conflict_resolver::ResolvedLineSource::B,
                        &self.conflict_resolver.three_way_text.theirs,
                        self.conflict_resolver
                            .three_way_line_starts_ref(ThreeWayColumn::Theirs),
                    );
                    insert_lookup_from_indexed_text(
                        &mut source_lookup,
                        conflict_resolver::ResolvedLineSource::A,
                        &self.conflict_resolver.three_way_text.ours,
                        self.conflict_resolver
                            .three_way_line_starts_ref(ThreeWayColumn::Ours),
                    );
                }
            }

            let mut middle_meta = Vec::with_capacity(new_affected.len());
            for line_ix in new_affected.clone() {
                let output_line =
                    rows::resolved_output_line_text(output_text, new_line_starts.as_ref(), line_ix);
                let (mut source, mut input_line) = source_lookup
                    .get(output_line)
                    .copied()
                    .unwrap_or((conflict_resolver::ResolvedLineSource::Manual, None));
                if new_dirty_lines.contains(&line_ix) {
                    source = conflict_resolver::ResolvedLineSource::Manual;
                    input_line = None;
                }
                middle_meta.push(conflict_resolver::ResolvedLineMeta {
                    output_line: u32::try_from(line_ix).unwrap_or(u32::MAX),
                    source,
                    input_line,
                });
            }
            middle_meta
        };

        let old_outline = if used_stash {
            self.conflict_resolved_outline_stash
                .as_ref()
                .map(|stash| stash.outline.clone())
                .unwrap_or_default()
        } else {
            std::mem::take(&mut self.conflict_resolver.resolved_outline)
        };
        let old_meta = old_outline.meta;
        let old_markers = old_outline.markers;
        let mut next_sources_index = old_outline.sources_index;
        let line_delta = new_affected.len() as isize - old_affected.len() as isize;

        let mut next_meta = Vec::with_capacity(new_line_count);
        next_meta.extend(
            old_meta
                .iter()
                .take(old_affected.start.min(old_meta.len()))
                .cloned(),
        );
        next_meta.extend(middle_meta);
        for entry in old_meta.iter().skip(old_affected.end.min(old_meta.len())) {
            let mut shifted = entry.clone();
            shifted.output_line =
                u32::try_from(shifted_line_index(entry.output_line as usize, line_delta))
                    .unwrap_or(u32::MAX);
            next_meta.push(shifted);
        }
        apply_conflict_choice_provenance_hints(
            &mut next_meta,
            &self.conflict_resolver.marker_segments,
            output_text,
            new_view_mode,
        );

        let mut next_markers = vec![None; new_line_count];
        for (line_ix, marker) in old_markers
            .iter()
            .copied()
            .enumerate()
            .take(old_affected.start.min(old_markers.len()))
        {
            if line_ix < new_line_count {
                next_markers[line_ix] = marker;
            }
        }
        for (old_line_ix, marker) in old_markers
            .iter()
            .copied()
            .enumerate()
            .skip(old_affected.end.min(old_markers.len()))
        {
            let Some(marker) = marker else {
                continue;
            };
            let new_line_ix = shifted_line_index(old_line_ix, line_delta);
            if new_line_ix < new_line_count {
                next_markers[new_line_ix] = Some(shift_resolved_output_marker(marker, line_delta));
            }
        }
        let blocks: Vec<&conflict_resolver::ConflictBlock> = self
            .conflict_resolver
            .marker_segments
            .iter()
            .filter_map(|seg| match seg {
                conflict_resolver::ConflictSegment::Block(block) => Some(block),
                _ => None,
            })
            .collect();
        for conflict_ix in recompute_conflicts {
            let block = blocks[conflict_ix];
            let range = new_block_ranges[conflict_ix].clone();
            let marker_ranges = conflict_marker_ranges_for_block(block, range);
            write_conflict_markers_for_ranges(
                &mut next_markers,
                conflict_ix,
                !block.resolved,
                marker_ranges.as_slice(),
            );
        }

        update_line_sources_index_for_range(
            &mut next_sources_index,
            old_view_mode,
            old_meta.as_slice(),
            old_text,
            old_line_starts.as_ref(),
            old_affected.clone(),
            false,
        );
        update_line_sources_index_for_range(
            &mut next_sources_index,
            new_view_mode,
            next_meta.as_slice(),
            output_text,
            new_line_starts.as_ref(),
            new_affected.clone(),
            true,
        );

        self.conflict_resolved_preview_syntax_language =
            path.and_then(rows::diff_syntax_language_for_path);
        self.conflict_resolved_preview_source_revision = Some(
            ResolvedOutputSourceRevision::from_snapshot(&output_snapshot),
        );
        self.conflict_resolved_preview_line_count = new_line_count;
        self.conflict_resolved_preview_line_starts = new_line_starts;
        self.conflict_resolved_output_measure_row = resolved_output_measure_row(&output_snapshot);
        // The text already reached the live tree on the keystroke. This call is
        // here for what the outline recompute itself changed: the language (the
        // path may have only just resolved) and the unresolved-conflict overlay,
        // which is derived from the marker segments this delta rewrote. It
        // reparses only if the buffer really is different.
        self.refresh_conflict_resolved_output_syntax(&output_snapshot, None, cx);
        self.conflict_resolved_outline_stash = None;
        self.conflict_resolver.resolved_outline = ResolvedOutlineData {
            meta: next_meta,
            markers: next_markers,
            sources_index: next_sources_index,
        };
        self.conflict_resolver.resolved_output_visible_dirty = true;
        self.conflict_resolver.resolved_outline_gutter_rows.clear();
        self.conflict_resolved_preview_text = output_snapshot;
        true
    }

    pub(super) fn conflict_resolver_scroll_resolved_output_to_line(
        &self,
        target_line_ix: usize,
        line_count: usize,
    ) {
        if line_count == 0 {
            return;
        }
        // Deferred item scrolls apply at the next layout pass, so they work
        // before the lists have ever laid out (initial open) and cannot be
        // clamped against stale bounds. Scrolling the gutter and output
        // lists together leaves the per-frame offset sync nothing to
        // arbitrate, which previously ping-ponged the output back to the
        // top of the file.
        let target_line = target_line_ix.min(line_count.saturating_sub(1));
        // Collapsed context mode: the output lists are in fold-projected row
        // space, so address the row showing the line (or its fold).
        let target = self.resolved_output_visible_ix_for_line(target_line);
        self.conflict_resolved_preview_scroll
            .scroll_to_item_strict(target, gpui::ScrollStrategy::Center);
        self.conflict_resolved_preview_gutter_scroll
            .scroll_to_item_strict(target, gpui::ScrollStrategy::Center);
    }

    pub(super) fn conflict_resolver_scroll_resolved_output_to_line_in_text(
        &self,
        target_line_ix: usize,
        output_text: &str,
    ) {
        let line_count = count_newlines(output_text).saturating_add(1);
        self.conflict_resolver_scroll_resolved_output_to_line(target_line_ix, line_count);
    }

    pub(super) fn schedule_conflict_resolved_outline_recompute(
        &mut self,
        path: Option<std::path::PathBuf>,
        source_revision: ResolvedOutputSourceRevision,
        delta: Option<ResolvedOutlineDelta>,
        cx: &mut gpui::Context<Self>,
    ) {
        if self.conflict_resolved_output_is_streamed() {
            let _ = source_revision;
            let _ = delta;
            self.refresh_streamed_resolved_output_preview_from_markers(path.as_ref());
            cx.notify();
            return;
        }
        self.conflict_resolver.resolver_pending_recompute_seq = self
            .conflict_resolver
            .resolver_pending_recompute_seq
            .wrapping_add(1);
        let seq = self.conflict_resolver.resolver_pending_recompute_seq;

        #[cfg(test)]
        {
            let did_incremental = delta.clone().is_some_and(|delta| {
                self.recompute_conflict_resolved_outline_and_provenance_incremental(
                    path.as_ref(),
                    delta,
                    cx,
                )
            });
            if did_incremental {
                cx.notify();
                return;
            }

            let trace_started = Instant::now();
            let output_snapshot = self
                .conflict_resolver_input
                .read_with(cx, |input, _| input.text_snapshot());
            let background_delay = self
                .conflict_resolved_outline_background_delay_override
                .unwrap_or_default();
            self.sync_conflict_resolved_preview_snapshot(&output_snapshot, path.as_ref(), true, cx);

            if background_delay.is_zero()
                && self.conflict_resolver.resolver_pending_recompute_seq == seq
                && self.conflict_resolved_preview_source_revision == Some(source_revision)
                && self.conflict_resolved_preview_path.as_ref() == path.as_ref()
            {
                // Built here rather than above so this arm matches production,
                // where the request is assembled inside the debounced task. It
                // copies the document, so hoisting it would charge every
                // keystroke for an outline that only runs once per burst.
                let request = self.background_resolved_outline_recompute_request(&output_snapshot);
                let computed = compute_resolved_outline_computation(
                    request.output_text.as_ref(),
                    request.output_line_count,
                    &request.marker_segments,
                    &request.block_map,
                    request.sources.as_view(),
                );
                self.apply_resolved_outline_computation(path.as_ref(), trace_started, computed);
            }

            cx.notify();
        }

        #[cfg(not(test))]
        {
            cx.spawn(
                async move |view: WeakEntity<MainPaneView>, cx: &mut gpui::AsyncApp| {
                    smol::Timer::after(Duration::from_millis(
                        CONFLICT_RESOLVED_OUTLINE_DEBOUNCE_MS,
                    ))
                    .await;
                    let request = view.update(cx, |this, cx| {
                        if this.conflict_resolver.resolver_pending_recompute_seq != seq {
                            return None;
                        }
                        if this.conflict_resolved_preview_source_revision != Some(source_revision)
                            || this.conflict_resolved_preview_path.as_ref() != path.as_ref()
                        {
                            return None;
                        }
                        let did_incremental = delta.clone().is_some_and(|delta| {
                            this.recompute_conflict_resolved_outline_and_provenance_incremental(
                                path.as_ref(),
                                delta,
                                cx,
                            )
                        });
                        if !did_incremental {
                            let trace_started = Instant::now();
                            let output_snapshot = this
                                .conflict_resolver_input
                                .read_with(cx, |input, _| input.text_snapshot());
                            let request = this
                                .background_resolved_outline_recompute_request(&output_snapshot);
                            let background_delay = Duration::default();
                            this.sync_conflict_resolved_preview_snapshot(
                                &output_snapshot,
                                path.as_ref(),
                                true,
                                cx,
                            );
                            cx.notify();
                            return Some((request, trace_started, background_delay));
                        }

                        cx.notify();
                        None
                    });
                    let Some((request, trace_started, background_delay)) = request.ok().flatten()
                    else {
                        return;
                    };

                    if !background_delay.is_zero() {
                        smol::Timer::after(background_delay).await;
                    }

                    let compute_outline = move || {
                        compute_resolved_outline_computation(
                            request.output_text.as_ref(),
                            request.output_line_count,
                            &request.marker_segments,
                            &request.block_map,
                            request.sources.as_view(),
                        )
                    };
                    let computed = smol::unblock(compute_outline).await;

                    let _ = view.update(cx, |this, cx| {
                        if this.conflict_resolver.resolver_pending_recompute_seq != seq {
                            return;
                        }
                        if this.conflict_resolved_preview_source_revision != Some(source_revision)
                            || this.conflict_resolved_preview_path.as_ref() != path.as_ref()
                        {
                            return;
                        }

                        this.apply_resolved_outline_computation(
                            path.as_ref(),
                            trace_started,
                            computed,
                        );
                        cx.notify();
                    });
                },
            )
            .detach();
        }
    }

    #[cfg(test)]
    pub(in crate::view) fn recompute_conflict_resolved_outline_for_tests(
        &mut self,
        cx: &mut gpui::Context<Self>,
    ) {
        let path = self.conflict_resolver.path.clone();
        self.recompute_conflict_resolved_outline_and_provenance(path.as_ref(), cx);
    }

    #[cfg(test)]
    pub(in crate::view) fn set_conflict_resolved_outline_background_delay_override_for_tests(
        &mut self,
        delay: Duration,
    ) {
        self.conflict_resolved_outline_background_delay_override = Some(delay);
    }

    pub(in crate::view) fn set_active_context_menu_invoker(
        &mut self,
        next: Option<SharedString>,
        cx: &mut gpui::Context<Self>,
    ) {
        if self.active_context_menu_invoker == next {
            return;
        }
        self.active_context_menu_invoker = next.clone();
        self.history_view.update(cx, |view, cx| {
            view.set_active_context_menu_invoker(next, cx)
        });
        cx.notify();
    }

    pub(in crate::view) fn set_date_time_format(
        &mut self,
        next: DateTimeFormat,
        cx: &mut gpui::Context<Self>,
    ) {
        if self.date_time_format == next {
            return;
        }
        self.date_time_format = next;
        self.history_view
            .update(cx, |view, cx| view.set_date_time_format(next, cx));
        cx.notify();
    }

    pub(in crate::view) fn set_history_highlight_commit_chain(
        &mut self,
        enabled: bool,
        cx: &mut gpui::Context<Self>,
    ) {
        self.history_view.update(cx, |view, cx| {
            view.set_history_highlight_commit_chain(enabled, cx)
        });
        cx.notify();
    }

    pub(in crate::view) fn history_highlight_commit_chain(&self, cx: &App) -> bool {
        self.history_view.read(cx).history_highlight_commit_chain
    }

    pub(in crate::view) fn set_history_relative_dates(
        &mut self,
        enabled: bool,
        cx: &mut gpui::Context<Self>,
    ) {
        self.history_view
            .update(cx, |view, cx| view.set_history_relative_dates(enabled, cx));
        cx.notify();
    }

    pub(in crate::view) fn history_relative_dates(&self, cx: &App) -> bool {
        self.history_view.read(cx).history_relative_dates
    }

    pub(in crate::view) fn set_timezone(&mut self, next: Timezone, cx: &mut gpui::Context<Self>) {
        self.history_view
            .update(cx, |view, cx| view.set_timezone(next, cx));
        cx.notify();
    }

    pub(in crate::view) fn set_show_timezone(
        &mut self,
        enabled: bool,
        cx: &mut gpui::Context<Self>,
    ) {
        self.history_view
            .update(cx, |view, cx| view.set_show_timezone(enabled, cx));
        cx.notify();
    }

    pub(in crate::view) fn set_diff_scroll_sync(
        &mut self,
        next: DiffScrollSync,
        cx: &mut gpui::Context<Self>,
    ) {
        if self.diff_scroll_sync == next {
            return;
        }

        self.diff_scroll_sync = next;
        self.sync_diff_split_scroll();
        self.sync_conflict_preview_scroll();
        cx.notify();
    }

    pub(in crate::view) fn set_diff_view_mode(
        &mut self,
        next: DiffViewMode,
        cx: &mut gpui::Context<Self>,
    ) {
        if self.diff_view == next {
            return;
        }

        self.diff_view = next;
        // Inline keys styled segments by `row_ix` while split keys them by
        // `row_ix * 2` / `row_ix * 2 + 1` (`file_diff_split_cache_key`) against
        // the same `split_left`/`split_right` epochs, so the two key spaces
        // alias. Clear on every mode change, not just the toolbar/hotkey ones.
        self.clear_diff_text_style_caches();
        if self.diff_search_has_query() {
            self.diff_search_recompute_matches_preserving_current();
        }
        cx.notify();
    }

    pub(in crate::view) fn set_annotate_enabled(
        &mut self,
        next: bool,
        cx: &mut gpui::Context<Self>,
    ) {
        if self.annotate_enabled == next {
            return;
        }

        self.annotate_enabled = next;
        // The annotation column changes the available text width, so word-wrap
        // column counts and wrapped-row projection must be recomputed.
        self.invalidate_diff_wrap_visible_cache();
        if next {
            // An explicit toggle on: retry a previously failed blame for the same
            // target (force = true). The per-frame Render path never forces.
            self.request_blame_for_current_target(true, cx);
        }
        cx.notify();
    }

    /// Scaled pixel width of the annotation column at the current ui scale.
    pub(in crate::view) fn annotate_column_width_px(&self, ui_scale_percent: u32) -> Pixels {
        crate::ui_scale::design_px_from_percent(self.annotate_column_width, ui_scale_percent)
    }

    /// Whether the annotation column should be shown for the currently rendered
    /// diff target. Requires the user toggle to be on AND the target to support
    /// blame (committed-file and working-tree views — see
    /// [`blame_path_rev_for_target`]).
    pub(in crate::view) fn annotation_active(&self) -> bool {
        self.annotate_enabled
            && self
                .rendered_diff_target()
                .and_then(blame_path_rev_for_target)
                .is_some()
    }

    /// Whether the loaded (or retained) blame describes the diff target being
    /// rendered right now. `blame_path`/`blame_source` follow the store snapshot,
    /// which lags the dispatch by at least a frame, so just after a file switch
    /// they still name the previous file — its annotations must not be painted
    /// over the new one's rows.
    pub(in crate::view) fn blame_matches_rendered_target(&self) -> bool {
        let Some((path, source)) = self
            .rendered_diff_target()
            .and_then(blame_path_rev_for_target)
        else {
            return false;
        };
        self.active_repo().is_some_and(|repo| {
            repo.history_state.blame_path.as_deref() == Some(path.as_path())
                && repo.history_state.blame_source.as_ref() == Some(&source)
        })
    }

    /// Record the hovered blame annotation sub-area and drive the shared tooltip
    /// host. `next` is the (row, area) now hovered, or `None` when leaving; the
    /// blame canvas repaints on `notify` and renders the accent highlight from
    /// this state. Callers gate this so it only runs when the hover changes.
    pub(in crate::view) fn update_blame_annot_hover(
        &mut self,
        next: Option<(usize, rows::AnnotArea)>,
        tooltip: Option<SharedString>,
        cx: &mut gpui::Context<Self>,
    ) {
        if self.blame_annot_hover == next {
            return;
        }
        self.blame_annot_hover = next;
        // Only a pointer on the button itself owns a stage tooltip; merely
        // hovering the row shows the button without one.
        let stage_hover_owns_tooltip = self
            .diff_stage_gutter_hover
            .is_some_and(|hover| hover.on_button);
        self.apply_diff_hover_tooltip(tooltip, stage_hover_owns_tooltip, cx);
        cx.notify();
    }

    /// Drop a stage-gutter hover whose button was not painted in the frame just
    /// gone. Called while `diff_stage_gutter_cells` still holds that frame's
    /// buttons, so an entry missing from it means the row no longer offers one
    /// and can no longer clear the hover itself. Without this the button and its
    /// tooltip stay pinned under a pointer that is over something else.
    pub(in crate::view) fn clear_diff_stage_gutter_hover_if_unpainted(
        &mut self,
        cx: &mut gpui::Context<Self>,
    ) {
        let unpainted = self.diff_stage_gutter_hover.is_some_and(|hover| {
            !self
                .diff_stage_gutter_cells
                .contains_key(&(hover.visible_ix, hover.slot))
        });
        if unpainted {
            self.update_diff_stage_gutter_hover(None, None, cx);
        }
    }

    /// Record the hovered stage/unstage gutter button and drive the shared
    /// tooltip host, mirroring [`Self::update_blame_annot_hover`]. The row canvas
    /// paints the button from this state (never from the live cursor), so it
    /// stays in step with the value folded into the canvas revision key.
    pub(in crate::view) fn update_diff_stage_gutter_hover(
        &mut self,
        next: Option<rows::DiffStageHover>,
        tooltip: Option<SharedString>,
        cx: &mut gpui::Context<Self>,
    ) {
        if self.diff_stage_gutter_hover == next {
            return;
        }
        self.diff_stage_gutter_hover = next;
        let blame_hover_owns_tooltip = self.blame_annot_hover.is_some();
        self.apply_diff_hover_tooltip(tooltip, blame_hover_owns_tooltip, cx);
        cx.notify();
    }

    /// Shared tooltip plumbing for the two diff-row hover systems (blame column
    /// and stage gutter). Both write to the same host, so a hover that is leaving
    /// must not clear a tooltip the other one just set: `other_hover_active` says
    /// whether the other system currently owns the tooltip.
    fn apply_diff_hover_tooltip(
        &mut self,
        tooltip: Option<SharedString>,
        other_hover_active: bool,
        cx: &mut gpui::Context<Self>,
    ) {
        let Some(host) = self.tooltip_host.upgrade() else {
            return;
        };
        host.update(cx, |host, cx| match tooltip {
            Some(text) => {
                host.set_tooltip_text_if_changed(Some(text), cx);
            }
            None => {
                if !other_hover_active {
                    host.clear_tooltip(cx);
                }
            }
        });
    }

    /// Drop the cached wrapped-row projection so it is recomputed against the
    /// current text width (which depends on whether the annotation column is
    /// shown).
    pub(in crate::view) fn invalidate_diff_wrap_visible_cache(&mut self) {
        self.diff_wrap_visible_rows.clear();
        self.diff_wrap_visible_cache_key = None;
    }

    /// When annotate is on, ensure blame for the currently displayed file/rev is
    /// loaded. Derives the path and revision from the rendered diff target and
    /// dispatches `LoadBlame`, skipping redundant loads.
    pub(in crate::view) fn request_blame_for_current_target(
        &mut self,
        force: bool,
        _cx: &mut gpui::Context<Self>,
    ) {
        let Some(repo_id) = self.active_repo_id() else {
            return;
        };
        let Some((path, source)) = self
            .rendered_diff_target()
            .and_then(blame_path_rev_for_target)
        else {
            return;
        };

        if let Some(repo) = self.active_repo() {
            let history = &repo.history_state;
            let same_target = history.blame_path.as_deref() == Some(path.as_path())
                && history.blame_source.as_ref() == Some(&source);
            if !should_request_blame(same_target, &history.blame, force) {
                return;
            }
        }

        self.store.dispatch(Msg::LoadBlame {
            repo_id,
            path,
            source,
        });
    }

    pub(in crate::view) fn set_diff_content_mode(
        &mut self,
        next: DiffContentMode,
        cx: &mut gpui::Context<Self>,
    ) {
        if self.diff_content_mode == next {
            return;
        }

        self.diff_content_mode = next;
        self.diff_selection_anchor = None;
        self.diff_selection_range = None;
        self.clear_diff_text_style_caches();
        self.clear_diff_text_query_overlay_cache();
        self.clear_conflict_diff_style_caches();
        self.clear_conflict_diff_query_overlay_caches();
        self.clear_worktree_preview_segments_cache();
        self.reset_collapsed_diff_projection(false);
        self.ensure_rendered_patch_diff_cache(cx);
        if self.current_main_diff_supports_diff_content_toggle() {
            self.ensure_file_diff_cache(cx);
        }
        if self.current_main_diff_wants_file_diff() {
            self.ensure_file_image_diff_cache(cx);
        }
        if self.diff_search_has_query() {
            self.diff_search_recompute_matches_preserving_current();
        }
        cx.notify();
    }

    pub(in crate::view) fn set_diff_whitespace_mode(
        &mut self,
        next: DiffWhitespaceMode,
        cx: &mut gpui::Context<Self>,
    ) {
        if self.diff_whitespace_mode == next {
            return;
        }

        self.diff_whitespace_mode = next;
        self.diff_selection_anchor = None;
        self.diff_selection_range = None;
        self.rebuild_patch_visual_line_kinds_from_current_diff();
        self.diff_word_highlights.clear();
        self.diff_word_highlights_inflight = None;
        self.reset_file_diff_word_highlight_caches();
        self.clear_diff_text_style_caches();
        self.clear_diff_text_query_overlay_cache();
        self.clear_conflict_diff_style_caches();
        self.clear_conflict_diff_query_overlay_caches();
        self.conflict_three_way_segments_cache.clear();
        self.conflict_three_way_query_segments_cache.clear();
        self.clear_worktree_preview_segments_cache();
        self.reset_collapsed_diff_projection(false);
        self.diff_visible_cache_len = 0;
        self.diff_visible_cache_projection_rev = u64::MAX;
        self.diff_scrollbar_markers_cache.clear();
        if self.current_main_diff_supports_diff_content_toggle() {
            self.reset_file_diff_cache_data();
            self.ensure_file_diff_cache(cx);
        }
        if self.diff_search_active && !self.diff_search_query.is_empty() {
            self.diff_search_recompute_matches_preserving_current();
        }
        cx.notify();
    }

    pub(in crate::view) fn set_diff_reveal_whitespace_chars(
        &mut self,
        next: bool,
        cx: &mut gpui::Context<Self>,
    ) {
        if self.reveal_whitespace_chars == next {
            return;
        }

        self.reveal_whitespace_chars = next;
        self.clear_diff_text_style_caches();
        self.clear_conflict_diff_style_caches();
        self.conflict_three_way_segments_cache.clear();
        self.conflict_three_way_query_segments_cache.clear();
        self.diff_wrap_visible_cache_key = None;
        self.diff_wrap_visible_rows.clear();
        cx.notify();
    }

    pub(in crate::view) fn set_diff_word_wrap(&mut self, next: bool, cx: &mut gpui::Context<Self>) {
        if self.diff_word_wrap == next {
            return;
        }

        self.diff_word_wrap = next;
        self.diff_wrap_visible_cache_key = None;
        self.diff_wrap_visible_rows.clear();
        self.reset_diff_horizontal_scroll_state();
        cx.notify();
    }

    pub(in crate::view) fn set_diff_show_line_numbers(
        &mut self,
        next: bool,
        cx: &mut gpui::Context<Self>,
    ) {
        if self.diff_show_line_numbers == next {
            return;
        }

        self.diff_show_line_numbers = next;
        self.diff_wrap_visible_cache_key = None;
        self.reset_diff_horizontal_scroll_state();
        cx.notify();
    }

    pub(in crate::view) fn active_repo_id(&self) -> Option<RepoId> {
        self.state.active_repo
    }

    pub(in crate::view) fn active_repo(&self) -> Option<&RepoState> {
        let repo_id = self.active_repo_id()?;
        self.state.repos.iter().find(|r| r.id == repo_id)
    }

    pub(in crate::view) fn active_inline_submodule_diff(
        &self,
    ) -> Option<&gitcomet_state::model::InlineSubmoduleDiffState> {
        self.active_repo()?
            .diff_state
            .inline_submodule_diff
            .as_ref()
    }

    pub(in crate::view) fn selected_inline_submodule_diff_entry(
        &self,
    ) -> Option<&gitcomet_state::model::InlineSubmoduleDiffEntry> {
        let inline = self.active_inline_submodule_diff()?;
        inline.entries.get(inline.selected_ix)
    }

    pub(in crate::view) fn is_inline_submodule_diff_active(&self) -> bool {
        self.active_inline_submodule_diff().is_some()
    }

    pub(in crate::view) fn rendered_diff_target(&self) -> Option<&DiffTarget> {
        self.active_inline_submodule_diff()
            .map(|inline| &inline.target)
            .or_else(|| self.active_repo()?.diff_state.diff_target.as_ref())
    }

    /// Whether the content pane is showing a file's full content *at the commit
    /// the file browser is pinned to*, i.e. whether it earns the historical
    /// browse tint. See [`historical_browse_content`].
    pub(in crate::view) fn historical_browse_content_active(&self) -> bool {
        let Some(repo) = self.active_repo() else {
            return false;
        };
        historical_browse_content(repo, self.rendered_diff_target())
    }

    pub(in crate::view) fn rendered_patch_diff_loadable(
        &self,
    ) -> Option<&gitcomet_state::model::Loadable<gitcomet_state::model::Shared<Diff>>> {
        if let Some(inline) = self.active_inline_submodule_diff() {
            Some(&inline.diff)
        } else {
            self.active_repo().map(|repo| &repo.diff_state.diff)
        }
    }

    pub(in crate::view) fn rendered_patch_diff_rev(&self) -> u64 {
        self.active_inline_submodule_diff()
            .map(|inline| inline.diff_rev)
            .or_else(|| self.active_repo().map(|repo| repo.diff_state.diff_rev))
            .unwrap_or(0)
    }

    fn rendered_file_target_path(target: &DiffTarget) -> Option<&std::path::Path> {
        match target {
            DiffTarget::WorkingTree { path, .. } => Some(path.as_path()),
            DiffTarget::Commit {
                path: Some(path), ..
            }
            | DiffTarget::CommitRange {
                path: Some(path), ..
            } => Some(path.as_path()),
            DiffTarget::Commit { path: None, .. } | DiffTarget::CommitRange { path: None, .. } => {
                None
            }
        }
    }

    pub(in crate::view) fn rendered_file_diff_loadable(
        &self,
    ) -> Option<&gitcomet_state::model::Loadable<Option<gitcomet_state::model::Shared<FileDiffText>>>>
    {
        if let Some(inline) = self.active_inline_submodule_diff() {
            Some(&inline.diff_file)
        } else {
            self.active_repo().map(|repo| &repo.diff_state.diff_file)
        }
    }

    pub(in crate::view) fn rendered_file_image_diff_loadable(
        &self,
    ) -> Option<
        &gitcomet_state::model::Loadable<Option<gitcomet_state::model::Shared<FileDiffImage>>>,
    > {
        if let Some(inline) = self.active_inline_submodule_diff() {
            Some(&inline.diff_file_image)
        } else {
            self.active_repo()
                .map(|repo| &repo.diff_state.diff_file_image)
        }
    }

    pub(in crate::view) fn rendered_file_diff_rev(&self) -> u64 {
        self.active_inline_submodule_diff()
            .map(|inline| inline.diff_file_rev)
            .or_else(|| self.active_repo().map(|repo| repo.diff_state.diff_file_rev))
            .unwrap_or(0)
    }

    pub(in crate::view) fn rendered_diff_workdir(&self) -> Option<&std::path::Path> {
        self.active_inline_submodule_diff()
            .map(|inline| inline.submodule_repo_path.as_path())
            .or_else(|| self.active_repo().map(|repo| repo.spec.workdir.as_path()))
    }

    pub(in crate::view) fn rendered_file_diff_identity(
        &self,
    ) -> Option<(
        RepoId,
        u64,
        DiffTarget,
        std::path::PathBuf,
        std::path::PathBuf,
    )> {
        let repo_id = self.active_repo_id()?;
        let diff_file_rev = self.rendered_file_diff_rev();
        let diff_target = self.rendered_diff_target()?.clone();
        let workdir = self.rendered_diff_workdir()?.to_path_buf();
        let rel_path = Self::rendered_file_target_path(&diff_target)?;
        let abs_path = workdir.join(rel_path);
        Some((repo_id, diff_file_rev, diff_target, workdir, abs_path))
    }

    pub(in crate::view) fn supports_diff_content_mode_toggle(&self, is_file_preview: bool) -> bool {
        !is_file_preview
            && !self.is_worktree_target_directory()
            && Self::is_file_diff_target(self.rendered_diff_target())
    }

    /// The diff mode actually in effect. Collapsed hides the unchanged parts of
    /// a patch, so a target the state layer loads as whole-file content — an
    /// added, deleted, or untracked file, which has no patch — has nothing to
    /// collapse and stays on Full however the setting is set.
    pub(in crate::view) fn effective_diff_content_mode(&self) -> DiffContentMode {
        if self.diff_content_mode == DiffContentMode::Collapsed
            && matches!(
                self.rendered_patch_diff_loadable(),
                Some(Loadable::NotLoaded)
            )
        {
            return DiffContentMode::Full;
        }
        self.diff_content_mode
    }

    pub(in crate::view) fn wants_file_diff_view(&self, is_file_preview: bool) -> bool {
        self.effective_diff_content_mode() == DiffContentMode::Full
            && self.supports_diff_content_mode_toggle(is_file_preview)
    }

    pub(in crate::view) fn wants_collapsed_diff_view(&self, is_file_preview: bool) -> bool {
        self.effective_diff_content_mode() == DiffContentMode::Collapsed
            && self.supports_diff_content_mode_toggle(is_file_preview)
    }

    fn current_main_diff_supports_diff_content_toggle(&self) -> bool {
        let inline_submodule_diff_active = self.is_inline_submodule_diff_active();
        let has_submodule_summary = self
            .active_repo()
            .is_some_and(|repo| !matches!(repo.diff_state.submodule_summary, Loadable::NotLoaded));
        let untracked_directory_notice = if has_submodule_summary || inline_submodule_diff_active {
            None
        } else {
            self.untracked_directory_notice()
        };
        let is_file_preview = self.is_file_preview_active()
            && untracked_directory_notice.is_none()
            && !has_submodule_summary
            && !inline_submodule_diff_active;
        (inline_submodule_diff_active || !has_submodule_summary)
            && self.supports_diff_content_mode_toggle(is_file_preview)
    }

    fn current_main_diff_wants_file_diff(&self) -> bool {
        let inline_submodule_diff_active = self.is_inline_submodule_diff_active();
        let has_submodule_summary = self
            .active_repo()
            .is_some_and(|repo| !matches!(repo.diff_state.submodule_summary, Loadable::NotLoaded));
        let untracked_directory_notice = if has_submodule_summary || inline_submodule_diff_active {
            None
        } else {
            self.untracked_directory_notice()
        };
        let is_file_preview = self.is_file_preview_active()
            && untracked_directory_notice.is_none()
            && !has_submodule_summary
            && !inline_submodule_diff_active;
        self.current_main_diff_supports_diff_content_toggle()
            && self.wants_file_diff_view(is_file_preview)
    }

    fn rendered_patch_diff_cache_is_current(&self) -> bool {
        self.active_repo_id().is_some_and(|repo_id| {
            self.diff_cache_repo_id == Some(repo_id)
                && self.diff_cache_rev == self.rendered_patch_diff_rev()
                && self.diff_cache_target == self.rendered_diff_target().cloned()
        })
    }

    fn rendered_file_diff_cache_is_current(&self) -> bool {
        let Some((repo_id, diff_file_rev, diff_target, _workdir, abs_path)) =
            self.rendered_file_diff_identity()
        else {
            return false;
        };

        self.file_diff_cache_repo_id == Some(repo_id)
            && self.file_diff_cache_rev == diff_file_rev
            && self.file_diff_cache_target == Some(diff_target)
            && self.file_diff_cache_whitespace_mode == self.diff_whitespace_mode
            && self.file_diff_cache_path.as_ref() == Some(&abs_path)
    }

    pub(in crate::view) fn is_collapsed_diff_projection_active(&self) -> bool {
        self.effective_diff_content_mode() == DiffContentMode::Collapsed
            && self.current_main_diff_supports_diff_content_toggle()
            && self.rendered_patch_diff_cache_is_current()
            && self.rendered_file_diff_cache_is_current()
    }

    pub(in crate::view) fn collapsed_visible_row(
        &self,
        visible_ix: usize,
    ) -> Option<CollapsedDiffVisibleRow> {
        self.collapsed_diff_visible_rows.get(visible_ix).copied()
    }

    pub(in crate::view) fn current_collapsed_diff_projection_identity(
        &self,
    ) -> Option<CollapsedDiffProjectionIdentity> {
        let (repo_id, _diff_file_rev, diff_target, _workdir, abs_path) =
            self.rendered_file_diff_identity()?;
        Some(CollapsedDiffProjectionIdentity {
            repo_id,
            diff_target,
            file_path: abs_path,
            diff_whitespace_mode: self.diff_whitespace_mode,
            patch_content_signature: self.diff_cache_content_signature,
            file_content_signature: self.file_diff_cache_content_signature,
        })
    }

    pub(in crate::view) fn reset_collapsed_diff_projection(&mut self, clear_reveals: bool) {
        self.collapsed_diff_hunks.clear();
        self.collapsed_diff_hunk_ix_by_src_ix.clear();
        if clear_reveals {
            self.collapsed_diff_reveals.clear();
            self.collapsed_diff_projection_identity = None;
        }
        self.collapsed_diff_visible_rows.clear();
        self.collapsed_diff_hunk_visible_indices.clear();
        self.collapsed_diff_header_display_cache.clear();
        self.diff_visible_projection_rev = self.diff_visible_projection_rev.wrapping_add(1);
        if clear_reveals {
            self.diff_visible_cache_projection_rev = u64::MAX;
        }
    }

    pub(in crate::view) fn invalidate_collapsed_diff_visible_projection(&mut self) {
        self.collapsed_diff_visible_rows.clear();
        self.collapsed_diff_hunk_visible_indices.clear();
        self.collapsed_diff_header_display_cache.clear();
        self.diff_visible_projection_rev = self.diff_visible_projection_rev.wrapping_add(1);
    }

    // Apply the mode inside the pane first, then sync the root preference
    // without re-entering `main_pane.update(...)`.
    pub(in crate::view) fn set_diff_content_mode_and_persist(
        &mut self,
        next: DiffContentMode,
        cx: &mut gpui::Context<Self>,
    ) {
        if self.diff_content_mode != next {
            self.set_diff_content_mode(next, cx);
        }
        let root_view = self.root_view.clone();
        let _ = root_view.update(cx, |root, cx| {
            root.sync_diff_content_mode_from_pane(next, cx);
        });
    }

    pub(in crate::view) fn set_diff_whitespace_mode_and_persist(
        &mut self,
        next: DiffWhitespaceMode,
        cx: &mut gpui::Context<Self>,
    ) {
        if self.diff_whitespace_mode != next {
            self.set_diff_whitespace_mode(next, cx);
        }
        let root_view = self.root_view.clone();
        let _ = root_view.update(cx, |root, cx| {
            root.sync_diff_whitespace_mode_from_pane(next, cx);
        });
    }

    pub(in crate::view) fn set_diff_reveal_whitespace_chars_and_persist(
        &mut self,
        next: bool,
        cx: &mut gpui::Context<Self>,
    ) {
        if self.reveal_whitespace_chars != next {
            self.set_diff_reveal_whitespace_chars(next, cx);
        }
        let root_view = self.root_view.clone();
        let _ = root_view.update(cx, |root, cx| {
            root.sync_diff_reveal_whitespace_chars_from_pane(next, cx);
        });
    }

    pub(in crate::view) fn set_diff_word_wrap_and_persist(
        &mut self,
        next: bool,
        cx: &mut gpui::Context<Self>,
    ) {
        if self.diff_word_wrap != next {
            self.set_diff_word_wrap(next, cx);
        }
        let root_view = self.root_view.clone();
        let _ = root_view.update(cx, |root, cx| {
            root.sync_diff_word_wrap_from_pane(next, cx);
        });
    }

    pub(in crate::view) fn set_diff_show_line_numbers_and_persist(
        &mut self,
        next: bool,
        cx: &mut gpui::Context<Self>,
    ) {
        if self.diff_show_line_numbers != next {
            self.set_diff_show_line_numbers(next, cx);
        }
        let root_view = self.root_view.clone();
        let _ = root_view.update(cx, |root, cx| {
            root.sync_diff_show_line_numbers_from_pane(next, cx);
        });
    }

    fn rendered_diff_target_for_state(state: &AppState) -> Option<DiffTarget> {
        let repo_id = state.active_repo?;
        let repo = state.repos.iter().find(|repo| repo.id == repo_id)?;
        repo.diff_state
            .inline_submodule_diff
            .as_ref()
            .map(|inline| inline.target.clone())
            .or_else(|| repo.diff_state.diff_target.clone())
    }

    pub(in crate::view) fn history_visible_column_preferences(
        &self,
        cx: &gpui::App,
    ) -> (bool, bool, bool, bool) {
        self.history_view
            .read(cx)
            .history_visible_column_preferences()
    }

    /// Persisted merge tool preferences: (auto-advance, collapse-unchanged
    /// default, output scroll sync, show line numbers). Read by the root view's
    /// UI settings persist.
    pub(in crate::view) fn mergetool_preferences(&self) -> (bool, bool, bool, bool) {
        (
            self.mergetool_auto_advance,
            self.mergetool_collapse_unchanged,
            self.mergetool_output_scroll_sync,
            self.mergetool_show_line_numbers,
        )
    }

    pub(in crate::view) fn schedule_ui_settings_persist(&mut self, cx: &mut gpui::Context<Self>) {
        let _ = self.root_view.update(cx, |root, cx| {
            root.schedule_ui_settings_persist(cx);
        });
    }

    pub(in crate::view) fn set_mergetool_auto_advance_and_persist(
        &mut self,
        next: bool,
        cx: &mut gpui::Context<Self>,
    ) {
        if self.mergetool_auto_advance == next {
            return;
        }
        self.mergetool_auto_advance = next;
        self.schedule_ui_settings_persist(cx);
        cx.notify();
    }

    pub(in crate::view) fn set_mergetool_output_scroll_sync_and_persist(
        &mut self,
        next: bool,
        cx: &mut gpui::Context<Self>,
    ) {
        if self.mergetool_output_scroll_sync == next {
            return;
        }
        self.mergetool_output_scroll_sync = next;
        self.schedule_ui_settings_persist(cx);
        cx.notify();
    }

    pub(in crate::view) fn set_mergetool_view_three_way_and_persist(
        &mut self,
        next: bool,
        cx: &mut gpui::Context<Self>,
    ) {
        if self.mergetool_view_three_way == next {
            return;
        }
        self.mergetool_view_three_way = next;
        // Unlike the cog-menu setters this can run while the root view is
        // already being updated (view-mode toggles), so schedule the persist
        // after the current update flush.
        let root_view = self.root_view.clone();
        cx.defer(move |cx| {
            let _ = root_view.update(cx, |root, cx| {
                root.schedule_ui_settings_persist(cx);
            });
        });
        cx.notify();
    }

    pub(in crate::view) fn set_mergetool_show_line_numbers_and_persist(
        &mut self,
        next: bool,
        cx: &mut gpui::Context<Self>,
    ) {
        if self.mergetool_show_line_numbers == next {
            return;
        }
        self.mergetool_show_line_numbers = next;
        self.schedule_ui_settings_persist(cx);
        cx.notify();
    }

    pub(in crate::view) fn history_tag_preferences(&self, cx: &gpui::App) -> (bool, bool) {
        self.history_view.read(cx).history_tag_preferences()
    }

    pub(in crate::view) fn set_history_column_preferences(
        &mut self,
        show_graph: bool,
        show_author: bool,
        show_date: bool,
        show_sha: bool,
        cx: &mut gpui::Context<Self>,
    ) {
        self.history_view.update(cx, |view, cx| {
            view.set_history_column_preferences(show_graph, show_author, show_date, show_sha, cx);
        });
        cx.notify();
    }

    pub(in crate::view) fn set_history_tag_preferences(
        &mut self,
        show_tags: bool,
        auto_fetch_tags_on_repo_activation: bool,
        cx: &mut gpui::Context<Self>,
    ) {
        self.history_view.update(cx, |view, cx| {
            view.set_history_tag_preferences(show_tags, auto_fetch_tags_on_repo_activation, cx);
        });
        cx.notify();
    }

    pub(in crate::view) fn reset_history_column_widths(&mut self, cx: &mut gpui::Context<Self>) {
        self.history_view.update(cx, |view, cx| {
            view.reset_history_column_widths();
            cx.notify();
        });
        cx.notify();
    }

    pub(in crate::view) fn open_popover_at(
        &mut self,
        kind: PopoverKind,
        anchor: Point<Pixels>,
        window: &mut Window,
        cx: &mut gpui::Context<Self>,
    ) {
        let root_view = self.root_view.clone();
        let window_handle = window.window_handle();
        cx.defer(move |cx| {
            let _ = window_handle.update(cx, |_, window, cx| {
                let _ = root_view.update(cx, |root, cx| {
                    root.open_popover_at(kind, anchor, window, cx);
                });
            });
        });
    }

    pub(in crate::view) fn open_popover_for_bounds(
        &mut self,
        kind: PopoverKind,
        anchor_bounds: Bounds<Pixels>,
        window: &mut Window,
        cx: &mut gpui::Context<Self>,
    ) {
        let root_view = self.root_view.clone();
        let window_handle = window.window_handle();
        cx.defer(move |cx| {
            let _ = window_handle.update(cx, |_, window, cx| {
                let _ = root_view.update(cx, |root, cx| {
                    root.open_popover_for_bounds(kind, anchor_bounds, window, cx);
                });
            });
        });
    }

    pub(in crate::view) fn activate_context_menu_invoker(
        &mut self,
        invoker: SharedString,
        cx: &mut gpui::Context<Self>,
    ) {
        let _ = self.root_view.update(cx, move |root, cx| {
            root.set_active_context_menu_invoker(Some(invoker), cx);
        });
    }

    #[allow(clippy::too_many_arguments)]
    pub(in crate::view) fn open_conflict_resolver_input_row_context_menu(
        &mut self,
        invoker: SharedString,
        line_label: SharedString,
        line_target: ResolverPickTarget,
        chunk_label: SharedString,
        chunk_target: ResolverPickTarget,
        anchor: Point<Pixels>,
        window: &mut Window,
        cx: &mut gpui::Context<Self>,
    ) {
        self.activate_context_menu_invoker(invoker, cx);
        self.open_popover_at(
            PopoverKind::ConflictResolverInputRowMenu {
                line_label,
                line_target,
                chunk_label,
                chunk_target,
            },
            anchor,
            window,
            cx,
        );
    }

    #[allow(clippy::too_many_arguments)]
    pub(in crate::view) fn open_conflict_resolver_chunk_context_menu(
        &mut self,
        invoker: SharedString,
        conflict_ix: usize,
        has_base: bool,
        is_three_way: bool,
        selected_choices: Vec<conflict_resolver::ConflictChoice>,
        output_line_ix: Option<usize>,
        anchor: Point<Pixels>,
        window: &mut Window,
        cx: &mut gpui::Context<Self>,
    ) {
        self.activate_context_menu_invoker(invoker, cx);
        // Opening the chunk menu selects that conflict and brings the
        // *other* pane to it — the pane the user right-clicked is already in
        // view and must not jump under the open menu. Reveals are non-strict:
        // nothing scrolls when the target rows are already fully visible.
        self.conflict_resolver_select_conflict(conflict_ix, cx);
        if output_line_ix.is_some() {
            // Invoked from the resolved output: reveal the source columns.
            if let Some(vi) = self.conflict_resolver_visible_ix_for_conflict(conflict_ix) {
                self.conflict_resolver_reveal_all_columns(vi);
            }
        } else {
            // Invoked from a source column: reveal the resolved output chunk.
            let output_text = (!self.conflict_resolved_output_is_streamed()).then(|| {
                self.conflict_resolver_input
                    .read_with(cx, |input, _| input.text().to_string())
            });
            let line_count = output_text
                .as_ref()
                .map(|text| text.split('\n').count().max(1))
                .unwrap_or_else(|| self.conflict_resolved_preview_line_count.max(1));
            if let Some(line) = self.conflict_resolver_output_line_for_conflict(
                conflict_ix,
                output_text.as_deref().unwrap_or(""),
            ) {
                self.conflict_resolver_reveal_resolved_output_line(line, line_count);
            }
        }
        let split_selection_rows = self.conflict_resolver_split_selection_row_count(conflict_ix);
        let (join_previous_region, join_next_region) =
            self.conflict_resolver_join_region_targets(conflict_ix);
        self.open_popover_at(
            PopoverKind::ConflictResolverChunkMenu {
                conflict_ix,
                has_base,
                is_three_way,
                selected_choices,
                output_line_ix,
                split_selection_rows,
                join_previous_region,
                join_next_region,
                alignment_marked_columns: self.conflict_resolver_alignment_marked_columns(),
                has_manual_alignments: self.conflict_resolver_has_manual_alignments(),
                output_is_protected: self.conflict_resolver.output_is_protected,
            },
            anchor,
            window,
            cx,
        );
    }

    pub(in crate::view) fn conflict_resolver_selected_choices_for_conflict_ix(
        &self,
        conflict_ix: usize,
    ) -> Vec<conflict_resolver::ConflictChoice> {
        conflict_group_selected_choices_for_ix(
            &self.conflict_resolver.marker_segments,
            &self.conflict_resolver.conflict_region_indices,
            conflict_ix,
        )
    }

    pub(in crate::view) fn conflict_resolver_has_base_for_conflict_ix(
        &self,
        conflict_ix: usize,
    ) -> bool {
        self.conflict_resolver
            .marker_segments
            .iter()
            .filter_map(|seg| match seg {
                conflict_resolver::ConflictSegment::Block(block) => Some(block.base.is_some()),
                _ => None,
            })
            .nth(conflict_ix)
            .unwrap_or(false)
    }

    pub(in crate::view) fn conflict_resolver_split_selection_row_count(
        &self,
        conflict_ix: usize,
    ) -> Option<usize> {
        let selection = self.conflict_resolver.row_selection?;
        if selection.selecting || selection.conflict_ix != conflict_ix {
            return None;
        }
        self.conflict_resolver.split_boundaries_for_selection()?;
        Some(selection.row_range().count())
    }

    fn conflict_resolver_join_region_targets(
        &self,
        conflict_ix: usize,
    ) -> (
        Option<ConflictResolverJoinTarget>,
        Option<ConflictResolverJoinTarget>,
    ) {
        let Some(region_index) = self
            .conflict_resolver
            .conflict_region_indices
            .get(conflict_ix)
            .copied()
        else {
            return (None, None);
        };
        if self
            .conflict_resolver
            .conflict_region_indices
            .iter()
            .filter(|&&index| index == region_index)
            .take(2)
            .count()
            != 1
        {
            return (None, None);
        }
        let Some(repo_id) = self
            .conflict_resolver
            .repo_id
            .or_else(|| self.active_repo_id())
        else {
            return (None, None);
        };
        let Some(path) = self.conflict_resolver.dispatch_path() else {
            return (None, None);
        };
        let Some(repo) = self.state.repos.iter().find(|repo| repo.id == repo_id) else {
            return (None, None);
        };
        if repo.conflict_state.conflict_rev != self.conflict_resolver.conflict_rev {
            return (None, None);
        }
        let Some(session) = repo.conflict_state.conflict_session.as_ref() else {
            return (None, None);
        };
        if session.path != path.as_path()
            || session.strategy
                != gitcomet_core::conflict_session::ConflictResolverStrategy::FullTextResolver
            || region_index >= session.regions.len()
        {
            return (None, None);
        }

        let target = |first_region_index| ConflictResolverJoinTarget {
            repo_id,
            path: path.clone(),
            conflict_rev: repo.conflict_state.conflict_rev,
            first_region_index,
        };
        let visible_ix_for_unique_region = |wanted: usize| {
            let mut matches = self
                .conflict_resolver
                .conflict_region_indices
                .iter()
                .enumerate()
                .filter_map(|(ix, &region)| (region == wanted).then_some(ix));
            let first = matches.next()?;
            matches.next().is_none().then_some(first)
        };
        let previous = region_index.checked_sub(1).and_then(|previous_region| {
            let previous_ix = visible_ix_for_unique_region(previous_region)?;
            (previous_ix.checked_add(1) == Some(conflict_ix)
                && self
                    .conflict_resolver
                    .conflict_blocks_have_joinable_context(previous_ix, conflict_ix))
            .then(|| target(previous_region))
        });
        let next = region_index.checked_add(1).and_then(|next_region| {
            if next_region >= session.regions.len() {
                return None;
            }
            let next_ix = visible_ix_for_unique_region(next_region)?;
            (conflict_ix.checked_add(1) == Some(next_ix)
                && self
                    .conflict_resolver
                    .conflict_blocks_have_joinable_context(conflict_ix, next_ix))
            .then(|| target(region_index))
        });
        (previous, next)
    }

    pub(in crate::view) fn open_conflict_resolver_output_context_menu(
        &mut self,
        anchor: Point<Pixels>,
        window: &mut Window,
        cx: &mut gpui::Context<Self>,
    ) {
        let (selected_text, cursor_offset, clicked_offset, content) =
            self.conflict_resolver_input.read_with(cx, |i, _| {
                (
                    i.selected_text(),
                    i.cursor_offset(),
                    i.offset_for_position(anchor),
                    i.text().to_string(),
                )
            });
        let context_line =
            conflict_resolver_output_context_line(&content, cursor_offset, Some(clicked_offset));

        self.open_conflict_resolver_output_context_menu_at_line(
            context_line,
            selected_text,
            content,
            anchor,
            window,
            cx,
        );
    }

    pub(in crate::view) fn open_conflict_resolver_output_context_menu_for_line(
        &mut self,
        line_ix: usize,
        anchor: Point<Pixels>,
        window: &mut Window,
        cx: &mut gpui::Context<Self>,
    ) {
        if self.conflict_resolved_output_is_streamed() {
            let context_line =
                line_ix.min(self.conflict_resolved_preview_line_count.saturating_sub(1));
            self.open_conflict_resolver_output_context_menu_at_line(
                context_line,
                None,
                String::new(),
                anchor,
                window,
                cx,
            );
            return;
        }

        let content = self
            .conflict_resolver_input
            .read_with(cx, |i, _| i.text().to_string());
        let context_line = line_ix.min(self.conflict_resolved_preview_line_count.saturating_sub(1));
        let cursor_offset = line_start_offset_for_index(
            self.conflict_resolved_preview_line_starts.as_ref(),
            content.len(),
            context_line,
        );
        self.conflict_resolver_input.update(cx, |input, cx| {
            input.set_cursor_offset(cursor_offset, cx);
        });

        self.open_conflict_resolver_output_context_menu_at_line(
            context_line,
            None,
            content,
            anchor,
            window,
            cx,
        );
    }

    fn open_conflict_resolver_output_context_menu_at_line(
        &mut self,
        context_line: usize,
        selected_text: Option<String>,
        content: String,
        anchor: Point<Pixels>,
        window: &mut Window,
        cx: &mut gpui::Context<Self>,
    ) {
        let conflict_marker = if self.conflict_resolved_output_is_streamed() {
            self.conflict_resolver
                .resolved_outline
                .markers
                .get(context_line)
                .copied()
                .flatten()
        } else {
            resolved_output_marker_for_line(
                &self.conflict_resolver.marker_segments,
                &content,
                context_line,
                &self.conflict_resolved_output_block_map,
            )
        };
        if let Some(marker) = conflict_marker {
            let is_three_way = self.conflict_resolver.view_mode
                == conflict_resolver::ConflictResolverViewMode::ThreeWay;
            let selected_choices =
                self.conflict_resolver_selected_choices_for_conflict_ix(marker.conflict_ix);
            let has_base = self.conflict_resolver_has_base_for_conflict_ix(marker.conflict_ix);
            let invoker: SharedString = format!(
                "resolver_output_chunk_menu_{}_{}",
                marker.conflict_ix, context_line
            )
            .into();
            self.open_conflict_resolver_chunk_context_menu(
                invoker,
                marker.conflict_ix,
                has_base,
                is_three_way,
                selected_choices,
                Some(context_line),
                anchor,
                window,
                cx,
            );
            return;
        }

        let is_three_way = self.conflict_resolver.view_mode
            == conflict_resolver::ConflictResolverViewMode::ThreeWay;

        let (has_source_a, has_source_b, has_source_c) = if is_three_way {
            (
                self.conflict_resolver
                    .three_way_has_line(ThreeWayColumn::Base, context_line),
                self.conflict_resolver
                    .three_way_has_line(ThreeWayColumn::Ours, context_line),
                self.conflict_resolver
                    .three_way_has_line(ThreeWayColumn::Theirs, context_line),
            )
        } else {
            {
                let row = self
                    .conflict_resolver
                    .two_way_split_row_by_source(context_line);
                (
                    row.as_ref().and_then(|r| r.old.as_ref()).is_some(),
                    row.as_ref().and_then(|r| r.new.as_ref()).is_some(),
                    false,
                )
            }
        };

        self.open_popover_at(
            PopoverKind::ConflictResolverOutputMenu {
                cursor_line: context_line,
                selected_text,
                has_source_a,
                has_source_b,
                has_source_c,
                is_three_way,
            },
            anchor,
            window,
            cx,
        );
    }

    /// Paths a stage/unstage shortcut should act on when the file it targets is
    /// part of a multi-file status selection: the whole selection, resolved the
    /// same way the status row button and the context menu resolve it. `None`
    /// means there is no such selection and the caller keeps acting on the one
    /// file it already resolved.
    ///
    /// Reads only. The shortcut may still raise a confirmation the user cancels,
    /// so [`Self::clear_status_selection_for_shortcut`] is a separate step the
    /// caller owes once it commits to the action.
    pub(in crate::view) fn status_selection_for_shortcut(
        &mut self,
        repo_id: RepoId,
        area: DiffArea,
        path: &std::path::PathBuf,
        cx: &mut gpui::Context<Self>,
    ) -> Option<Vec<std::path::PathBuf>> {
        self.root_view
            .update(cx, |root, cx| {
                let (paths, used_selection) = root
                    .details_pane
                    .read(cx)
                    .status_selected_paths_for_action(repo_id, area, path);
                used_selection.then_some(paths)
            })
            .ok()
            .flatten()
    }

    /// Drop the row selection a shortcut has just acted on.
    pub(in crate::view) fn clear_status_selection_for_shortcut(
        &mut self,
        repo_id: RepoId,
        cx: &mut gpui::Context<Self>,
    ) {
        let _ = self.root_view.update(cx, |root, cx| {
            root.details_pane.update(cx, |pane, cx| {
                pane.clear_status_multi_selection(repo_id);
                cx.notify();
            });
        });
    }

    /// Raise the unresolved-conflict confirmation if staging `paths` would mark
    /// files resolved while they still contain conflict markers. Returns whether
    /// the dialog took over, in which case the caller must not stage: the dialog
    /// dispatches it if the user goes ahead. Unstaging never marks anything
    /// resolved, so it is left alone.
    pub(in crate::view) fn confirm_stage_conflict_markers(
        &mut self,
        repo_id: RepoId,
        area: DiffArea,
        paths: Vec<std::path::PathBuf>,
        clear_selection: bool,
        window: &mut Window,
        cx: &mut gpui::Context<Self>,
    ) -> bool {
        if area != DiffArea::Unstaged {
            return false;
        }
        let Some(confirm) = crate::view::conflict_markers::stage_confirm_popover(
            &self.state,
            repo_id,
            paths,
            clear_selection,
        ) else {
            return false;
        };
        let anchor = crate::view::conflict_markers::centered_dialog_anchor(window);
        self.open_popover_at(confirm, anchor, window, cx);
        cx.notify();
        true
    }

    /// Stage (or unstage) a whole status selection in one batch, clearing the
    /// diff selection first because every one of those files is about to move to
    /// the other section. Same order the context menu uses.
    pub(in crate::view) fn stage_or_unstage_status_paths(
        &mut self,
        repo_id: RepoId,
        area: DiffArea,
        paths: Vec<std::path::PathBuf>,
    ) {
        self.store.dispatch(Msg::ClearDiffSelection { repo_id });
        let paths = paths.into();
        self.store.dispatch(match area {
            DiffArea::Unstaged => Msg::StagePaths { repo_id, paths },
            DiffArea::Staged => Msg::UnstagePaths { repo_id, paths },
        });
    }

    pub(in crate::view) fn clear_status_multi_selection(
        &mut self,
        repo_id: RepoId,
        cx: &mut gpui::Context<Self>,
    ) {
        let _ = self.root_view.update(cx, |root, cx| {
            root.details_pane.update(cx, |pane, cx| {
                pane.status_multi_selection.remove(&repo_id);
                cx.notify();
            });
        });
    }

    pub(in crate::view) fn open_submodule_inner_diff(
        &mut self,
        submodule_repo_path: std::path::PathBuf,
        target: DiffTarget,
        cx: &mut gpui::Context<Self>,
    ) {
        let _ = self.root_view.update(cx, move |root, cx| {
            root.submodule_diff_bootstrap =
                Some(SubmoduleDiffBootstrap::new(submodule_repo_path, target));
            root.drive_submodule_diff_bootstrap();
            cx.notify();
        });
    }

    pub(in crate::view) fn active_change_tracking_view(
        &self,
        cx: &mut gpui::Context<Self>,
    ) -> ChangeTrackingView {
        self.root_view
            .update(cx, |root, _cx| root.change_tracking_view)
            .unwrap_or(ChangeTrackingView::Combined)
    }

    pub(in crate::view) fn scroll_status_section_to_ix(
        &mut self,
        section: StatusSection,
        ix: usize,
        cx: &mut gpui::Context<Self>,
    ) {
        let _ = self.root_view.update(cx, |root, cx| {
            root.details_pane
                .update(cx, |pane: &mut DetailsPaneView, cx| {
                    match section {
                        StatusSection::CombinedUnstaged | StatusSection::Unstaged => pane
                            .unstaged_scroll
                            .scroll_to_item_strict(ix, gpui::ScrollStrategy::Center),
                        StatusSection::Untracked => pane
                            .untracked_scroll
                            .scroll_to_item_strict(ix, gpui::ScrollStrategy::Center),
                        StatusSection::Staged => pane
                            .staged_scroll
                            .scroll_to_item_strict(ix, gpui::ScrollStrategy::Center),
                    }
                    cx.notify();
                });
        });
    }

    pub(in crate::view) fn scroll_commit_details_file_to_ix(
        &mut self,
        ix: usize,
        cx: &mut gpui::Context<Self>,
    ) {
        let _ = self.root_view.update(cx, |root, cx| {
            root.details_pane
                .update(cx, |pane: &mut DetailsPaneView, cx| {
                    pane.commit_files_scroll
                        .scroll_to_item_strict(ix, gpui::ScrollStrategy::Center);
                    cx.notify();
                });
        });
    }

    pub(super) fn apply_state_snapshot(
        &mut self,
        next: Arc<AppState>,
        cx: &mut gpui::Context<Self>,
    ) {
        let prev_active_repo_id = self.state.active_repo;
        let prev_diff_target = Self::rendered_diff_target_for_state(self.state.as_ref());

        let next_repo_id = next.active_repo;
        let next_diff_target = Self::rendered_diff_target_for_state(next.as_ref());

        if prev_diff_target != next_diff_target {
            self.clear_diff_selection_state();
            self.diff_autoscroll_pending = next_diff_target.is_some();
            self.worktree_preview_path = None;
            self.worktree_preview = Loadable::NotLoaded;
            self.worktree_preview_content_rev = 0;
            self.worktree_markdown_preview_path = None;
            self.worktree_markdown_preview_source_rev = 0;
            self.worktree_markdown_preview = Loadable::NotLoaded;
            self.worktree_markdown_preview_inflight = None;
            self.worktree_preview_syntax_language = None;
            self.reset_worktree_preview_source_state();
            self.reset_diff_horizontal_scroll_state();
            self.reset_collapsed_diff_projection(true);
        }

        self.state = next;
        // A closed repo tab takes its `RepoId` with it; buffers stashed under it
        // can never be saved again and would block every future close.
        self.prune_orphaned_file_editor_stash();

        self.sync_conflict_resolver(cx);
        self.ensure_file_image_diff_cache(cx);
        if self.current_main_diff_supports_diff_content_toggle() {
            self.ensure_file_diff_cache(cx);
        }

        if prev_active_repo_id != next_repo_id {
            self.history_view.update(cx, |view, _| {
                view.history_scroll
                    .scroll_to_item_strict(0, gpui::ScrollStrategy::Top);
            });
        }

        self.ensure_rendered_patch_diff_cache(cx);

        // Sync per-repo interactive commit editing state. Each repo with a setup
        // gets its own `IRebaseViewState`, populated once its entries become Ready
        // and kept (with local edits) across repo-tab switches. State for repos
        // whose setup is gone (cancelled, started, repo closed) is dropped.
        self.sync_interactive_commit_editor_states();

        // History caches are now managed by HistoryView.
    }

    pub(in crate::view) fn cached_path_display(&self, path: &std::path::Path) -> SharedString {
        let mut cache = self.path_display_cache.borrow_mut();
        path_display::cached_path_display(&mut cache, path)
    }

    pub(in crate::view) fn touch_diff_text_layout_cache(
        &mut self,
        key: u64,
        layout: Option<ShapedLine>,
    ) {
        let epoch = self.diff_text_layout_cache_epoch;
        match layout {
            Some(layout) => {
                self.diff_text_layout_cache.insert(
                    key,
                    DiffTextLayoutCacheEntry {
                        layout,
                        last_used_epoch: epoch,
                    },
                );
            }
            None => {
                if let Some(entry) = self.diff_text_layout_cache.get_mut(&key) {
                    entry.last_used_epoch = epoch;
                }
            }
        }
    }

    /// Prune the layout cache if it has grown past the high-water mark.
    /// Call once per render frame (after bumping the epoch), **not** from
    /// the per-row `touch_diff_text_layout_cache` hot path.
    pub(in crate::view) fn prune_diff_text_layout_cache(&mut self) {
        if self.diff_text_layout_cache.len()
            <= DIFF_TEXT_LAYOUT_CACHE_MAX_ENTRIES + DIFF_TEXT_LAYOUT_CACHE_PRUNE_OVERAGE
        {
            return;
        }

        let over_by = self
            .diff_text_layout_cache
            .len()
            .saturating_sub(DIFF_TEXT_LAYOUT_CACHE_MAX_ENTRIES);
        if over_by == 0 {
            return;
        }

        let mut by_age: Vec<(u64, u64)> = self
            .diff_text_layout_cache
            .iter()
            .map(|(k, v)| (*k, v.last_used_epoch))
            .collect();
        by_age.sort_by_key(|(_, last_used)| *last_used);

        for (key, _) in by_age.into_iter().take(over_by) {
            self.diff_text_layout_cache.remove(&key);
        }
    }

    pub(in crate::view) fn diff_text_segments_cache_get(
        &self,
        key: usize,
        syntax_epoch: u64,
    ) -> Option<&CachedDiffStyledText> {
        versioned_cached_diff_styled_text_is_current(
            self.diff_text_segments_cache
                .get(key)
                .and_then(Option::as_ref),
            syntax_epoch,
        )
    }

    pub(in crate::view) fn file_diff_split_cache_key(
        &self,
        row_ix: usize,
        region: DiffTextRegion,
    ) -> Option<usize> {
        let base = row_ix.checked_mul(2)?;
        match region {
            DiffTextRegion::SplitLeft => Some(base),
            DiffTextRegion::SplitRight => base.checked_add(1),
            DiffTextRegion::Inline => None,
        }
    }

    pub(in crate::view) fn diff_text_segments_cache_set(
        &mut self,
        key: usize,
        syntax_epoch: u64,
        value: CachedDiffStyledText,
    ) -> &CachedDiffStyledText {
        if self.diff_text_segments_cache.len() <= key {
            self.diff_text_segments_cache.resize_with(key + 1, || None);
        }
        self.diff_text_segments_cache[key] = Some(VersionedCachedDiffStyledText {
            syntax_epoch,
            query_generation: 0,
            styled: value,
        });
        if self.diff_text_query_segments_cache.len() > key {
            self.diff_text_query_segments_cache[key] = None;
        }
        self.diff_text_segments_cache[key]
            .as_ref()
            .map(|entry| &entry.styled)
            .expect("just set")
    }

    /// Returns the current diff search query, or an empty `SharedString` if search is inactive.
    pub(in crate::view) fn diff_search_query_or_empty(&self) -> SharedString {
        if self.diff_search_active {
            self.diff_search_query.clone()
        } else {
            SharedString::default()
        }
    }

    /// Returns the syntax mode for patch diff views (non-full-document).
    /// Uses `Auto` for small diffs and `HeuristicOnly` for large ones.
    pub(in crate::view) fn patch_diff_syntax_mode(&self) -> rows::DiffSyntaxMode {
        if self.patch_diff_row_len() <= rows::MAX_LINES_FOR_SYNTAX_HIGHLIGHTING {
            rows::DiffSyntaxMode::Auto
        } else {
            rows::DiffSyntaxMode::HeuristicOnly
        }
    }

    pub(in crate::view) fn conflict_row_styling_enabled(&self) -> bool {
        !self.conflict_resolver.is_binary_conflict
    }

    pub(in crate::view) fn conflict_row_syntax_language(&self) -> Option<rows::DiffSyntaxLanguage> {
        self.conflict_resolver.conflict_syntax_language
    }

    pub(in crate::view) fn worktree_preview_segments_cache_get(
        &self,
        key: usize,
    ) -> Option<&CachedDiffStyledText> {
        versioned_cached_diff_styled_text_is_current(
            self.worktree_preview_segments_cache.get(&key),
            self.worktree_preview_style_cache_epoch,
        )
    }

    pub(in crate::view) fn worktree_preview_segments_cache_set(
        &mut self,
        key: usize,
        value: CachedDiffStyledText,
    ) {
        self.worktree_preview_segments_cache.insert(
            key,
            VersionedCachedDiffStyledText {
                syntax_epoch: self.worktree_preview_style_cache_epoch,
                query_generation: 0,
                styled: value,
            },
        );
    }

    pub(in crate::view) fn is_file_diff_view_active(&self) -> bool {
        self.effective_diff_content_mode() == DiffContentMode::Full
            && self.rendered_file_diff_cache_is_current()
    }

    /// Whether the rasterized image diff on screen belongs to the current
    /// target. Deliberately not gated on [`DiffContentMode`]: an image has no
    /// collapsed form, so its rendered view is the same in either diff mode.
    pub(in crate::view) fn is_file_image_diff_view_active(&self) -> bool {
        let Some((repo_id, diff_file_rev, diff_target, _workdir, abs_path)) =
            self.rendered_file_diff_identity()
        else {
            return false;
        };
        self.file_image_diff_cache_repo_id == Some(repo_id)
            && self.file_image_diff_cache_rev == diff_file_rev
            && self.file_image_diff_cache_target == Some(diff_target)
            && self.file_image_diff_cache_path.as_ref() == Some(&abs_path)
            && (self.file_image_diff_cache_old.is_some()
                || self.file_image_diff_cache_new.is_some()
                || self.file_image_diff_cache_old_svg_path.is_some()
                || self.file_image_diff_cache_new_svg_path.is_some())
    }

    pub(in crate::view) fn consume_suppress_click_after_drag(&mut self) -> bool {
        if self.diff_suppress_clicks_remaining > 0 {
            self.diff_suppress_clicks_remaining =
                self.diff_suppress_clicks_remaining.saturating_sub(1);
            return true;
        }
        false
    }

    fn diff_source_visible_len(&self) -> usize {
        // A file preview has no diff rows: its source rows are the file's
        // lines, and they wrap through the same projection.
        if self.is_file_preview_active() {
            return self.worktree_preview_line_count().unwrap_or(0);
        }
        if self.is_collapsed_diff_projection_active() {
            return self.collapsed_diff_visible_rows.len();
        }
        self.diff_visible_inline_map
            .as_ref()
            .map(|map| map.visible_len())
            .unwrap_or_else(|| self.diff_visible_indices.len())
    }

    /// True when the *text diff's* wrap projection maps list positions to
    /// source rows.
    ///
    /// The rendered markdown preview keeps its own visual-row mapping and
    /// never refreshes these rows, so a preview opened after a wrapped text
    /// diff would otherwise be remapped through that diff's stale rows.
    fn diff_wrap_projection_active(&self) -> bool {
        self.diff_word_wrap
            && self.diff_wrap_visible_cache_key.is_some()
            && !self.is_markdown_preview_active()
    }

    pub(in crate::view) fn diff_visible_len(&self) -> usize {
        if self.diff_wrap_projection_active() {
            return self.diff_wrap_visible_rows.len();
        }
        self.diff_source_visible_len()
    }

    pub(in crate::view) fn diff_source_visible_ix_for_visible_ix(
        &self,
        visible_ix: usize,
    ) -> Option<usize> {
        if self.diff_wrap_projection_active() {
            return self
                .diff_wrap_visible_rows
                .get(visible_ix)
                .map(|row| row.source_visible_ix);
        }
        Some(visible_ix)
    }

    pub(in crate::view) fn diff_visual_ix_for_source_visible_ix(
        &self,
        source_visible_ix: usize,
    ) -> usize {
        if !self.diff_wrap_projection_active() {
            return source_visible_ix;
        }

        let visual_ix = self
            .diff_wrap_visible_rows
            .partition_point(|row| row.source_visible_ix < source_visible_ix);
        if self
            .diff_wrap_visible_rows
            .get(visual_ix)
            .is_some_and(|row| row.source_visible_ix == source_visible_ix)
        {
            visual_ix
        } else {
            source_visible_ix
        }
    }

    pub(in crate::view) fn diff_source_mapped_ix_for_visible_ix(
        &self,
        visible_ix: usize,
    ) -> Option<usize> {
        if self.is_collapsed_diff_projection_active() {
            return self
                .collapsed_visible_row(visible_ix)
                .and_then(CollapsedDiffVisibleRow::row_ix);
        }
        if let Some(map) = self.diff_visible_inline_map.as_ref() {
            return map.src_ix_for_visible_ix(visible_ix);
        }
        self.diff_visible_indices.get(visible_ix).copied()
    }

    pub(in crate::view) fn diff_mapped_ix_for_visible_ix(
        &self,
        visible_ix: usize,
    ) -> Option<usize> {
        if self.diff_word_wrap
            && let Some(row) = self.diff_wrap_visible_rows.get(visible_ix)
        {
            return self.diff_source_mapped_ix_for_visible_ix(row.source_visible_ix);
        }
        self.diff_source_mapped_ix_for_visible_ix(visible_ix)
    }

    pub(in crate::view) fn diff_text_wrap_for_visible_ix(
        &self,
        visible_ix: usize,
    ) -> Option<rows::DiffTextWrapSlice> {
        if !self.diff_wrap_projection_active() {
            return None;
        }
        let row = self.diff_wrap_visible_rows.get(visible_ix)?;
        let is_split_source = row.wrap_ix > 0
            || self
                .diff_wrap_visible_rows
                .get(visible_ix.saturating_add(1))
                .is_some_and(|next| next.source_visible_ix == row.source_visible_ix);
        if !is_split_source {
            return None;
        }
        let key = self.diff_wrap_visible_cache_key?;
        let columns = if self.is_file_preview_active() {
            // The preview is one column whatever the diff view is set to.
            key.preview_columns
        } else {
            match self.diff_view {
                DiffViewMode::Inline => key.inline_columns,
                DiffViewMode::Split => key.split_columns,
            }
        };
        Some(rows::DiffTextWrapSlice {
            wrap_ix: row.wrap_ix,
            wrap_columns: columns,
            primary_range: row.primary_range,
            secondary_range: row.secondary_range,
        })
    }

    pub(in crate::view) fn ensure_diff_wrap_visible_rows(
        &mut self,
        window: &mut gpui::Window,
        cx: &mut gpui::Context<Self>,
    ) {
        if !self.diff_word_wrap {
            if self.diff_wrap_visible_cache_key.take().is_some()
                || !self.diff_wrap_visible_rows.is_empty()
            {
                self.diff_wrap_visible_rows.clear();
                self.diff_scrollbar_markers_cache = self.compute_diff_scrollbar_markers();
                if self.diff_search_has_query() {
                    self.diff_search_recompute_matches_for_current_view_preserving_current();
                }
            }
            return;
        }

        let source_len = self.diff_source_visible_len();
        let (inline_columns, split_columns) = self.diff_wrap_columns(window, cx);
        let preview_columns = self.worktree_preview_wrap_columns(window, cx);
        let key = DiffWrapVisibleCacheKey {
            source_len,
            diff_view: self.diff_view,
            is_file_view: self.is_file_diff_view_active(),
            preview_columns,
            preview_content_rev: if self.is_file_preview_active() {
                self.worktree_preview_content_rev
            } else {
                0
            },
            collapsed_projection_active: self.is_collapsed_diff_projection_active(),
            projection_rev: if self.is_collapsed_diff_projection_active() {
                self.diff_visible_projection_rev
            } else {
                0
            },
            diff_cache_rev: self.diff_cache_rev,
            file_diff_cache_seq: self.file_diff_cache_seq,
            inline_columns,
            split_columns,
            reveal_whitespace_chars: self.reveal_whitespace_chars,
        };
        if self.diff_wrap_visible_cache_key == Some(key) {
            return;
        }

        self.diff_wrap_visible_rows.clear();
        self.diff_wrap_visible_rows.reserve(source_len);
        for source_visible_ix in 0..source_len {
            let (primary_ranges, secondary_ranges) = self.diff_wrap_ranges_for_source_visible_ix(
                source_visible_ix,
                inline_columns,
                split_columns,
                preview_columns,
            );
            let row_count = primary_ranges.len().max(secondary_ranges.len()).max(1);
            for wrap_ix in 0..row_count {
                self.diff_wrap_visible_rows.push(DiffWrapVisualRow {
                    source_visible_ix,
                    wrap_ix,
                    primary_range: diff_wrap_byte_range_at(&primary_ranges, wrap_ix),
                    secondary_range: diff_wrap_byte_range_at(&secondary_ranges, wrap_ix),
                });
            }
        }
        self.diff_wrap_visible_cache_key = Some(key);
        self.diff_scrollbar_markers_cache = self.compute_diff_scrollbar_markers();
        if self.diff_search_has_query() {
            self.diff_search_recompute_matches_for_current_view_preserving_current();
        }
    }

    /// Font the wrapped diff rows are painted in — the same family the rows
    /// container applies via `.font_family(editor_font_family)`. Wrap widths
    /// must be measured in it, never in the ambient text style.
    pub(in crate::view) fn diff_wrap_measure_font_family(
        &self,
        cx: &mut gpui::Context<Self>,
    ) -> gpui::SharedString {
        crate::font_preferences::current_editor_font_family(cx).into()
    }

    pub(in crate::view) fn diff_wrap_columns(
        &self,
        window: &mut gpui::Window,
        cx: &mut gpui::Context<Self>,
    ) -> (usize, usize) {
        let ui_scale_percent = crate::ui_scale::UiScale::current(cx).percent();
        let vertical_gutter = components::Scrollbar::gutter(components::ScrollbarAxis::Vertical);
        let content_width = (self.main_pane_content_width(cx) - vertical_gutter).max(px(0.0));
        // Measured in the editor font the rows are painted in, not in the
        // ambient UI font that is still current while this element tree is
        // being built. See `diff_text_wrap_char_width`.
        let char_width =
            rows::diff_canvas_text_wrap_char_width(window, self.diff_wrap_measure_font_family(cx));
        let pad = rows::diff_canvas_row_horizontal_padding(ui_scale_percent);
        let inline_text_start = if self.diff_show_line_numbers {
            rows::diff_canvas_inline_text_start(ui_scale_percent)
        } else {
            pad
        };
        let single_text_start = if self.diff_show_line_numbers {
            rows::diff_canvas_single_column_text_start(ui_scale_percent)
        } else {
            pad
        };
        // Inline annotate reserves a fixed column at the left, narrowing the
        // available text width for word wrapping.
        let annotation_width = if self.annotation_active() {
            self.annotate_column_width_px(ui_scale_percent)
        } else {
            px(0.0)
        };
        let inline_columns = diff_wrap_columns_for_width(
            content_width - annotation_width - inline_text_start - pad,
            char_width,
        );

        let (left_w, right_w) =
            crate::view::diff_split_column_widths(content_width, self.diff_split_ratio);
        // The annotation column narrows the left split column; subtract it from
        // the shared wrap width so wrapped text stays within the left column.
        let split_text_width =
            left_w.min(right_w).max(px(0.0)) - annotation_width - single_text_start - pad;
        let split_columns = diff_wrap_columns_for_width(split_text_width, char_width);
        (inline_columns, split_columns)
    }

    /// Columns a wrapped file preview row may use.
    ///
    /// Neither of the diff's two widths describes it: an inline diff row
    /// reserves two gutter cells for the old and new line numbers, and a split
    /// row only gets half the pane. A preview row is one column with one
    /// gutter, so it measures its own.
    pub(in crate::view) fn worktree_preview_wrap_columns(
        &self,
        window: &mut gpui::Window,
        cx: &mut gpui::Context<Self>,
    ) -> usize {
        let ui_scale_percent = crate::ui_scale::UiScale::current(cx).percent();
        let vertical_gutter = components::Scrollbar::gutter(components::ScrollbarAxis::Vertical);
        let content_width = (self.main_pane_content_width(cx) - vertical_gutter).max(px(0.0));
        let char_width =
            rows::diff_canvas_text_wrap_char_width(window, self.diff_wrap_measure_font_family(cx));
        let pad = rows::diff_canvas_row_horizontal_padding(ui_scale_percent);
        let text_start = if self.diff_show_line_numbers {
            rows::diff_canvas_single_column_text_start(ui_scale_percent)
        } else {
            pad
        };
        let annotation_width = if self.annotation_active() {
            self.annotate_column_width_px(ui_scale_percent)
        } else {
            px(0.0)
        };
        // The change bar is only drawn for a wholly added or removed file, but
        // it is always subtracted: wrapping a few pixels early is invisible,
        // wrapping late runs the last character under the scrollbar.
        let change_bar = rows::diff_canvas_change_bar_width(ui_scale_percent);
        diff_wrap_columns_for_width(
            content_width - annotation_width - change_bar - text_start - pad,
            char_width,
        )
    }

    /// Widths a wrapped markdown preview row may occupy: the full content
    /// width for the inline and worktree lists, and the narrower of the two
    /// split columns for the side-by-side lists, so both columns wrap
    /// identically and stay row-aligned.
    pub(in crate::view) fn markdown_preview_wrap_widths(
        &self,
        cx: &mut gpui::Context<Self>,
    ) -> (Pixels, Pixels) {
        let vertical_gutter = components::Scrollbar::gutter(components::ScrollbarAxis::Vertical);
        let content_width = (self.main_pane_content_width(cx) - vertical_gutter).max(px(0.0));
        let (left_w, right_w) =
            crate::view::diff_split_column_widths(content_width, self.diff_split_ratio);
        (content_width, left_w.min(right_w).max(px(0.0)))
    }

    fn diff_wrap_ranges_for_source_visible_ix(
        &self,
        source_visible_ix: usize,
        inline_columns: usize,
        split_columns: usize,
        preview_columns: usize,
    ) -> (Vec<rows::DiffWrapByteRange>, Vec<rows::DiffWrapByteRange>) {
        // A file preview is one column of plain file lines.
        if self.is_file_preview_active() {
            return (
                diff_wrap_byte_ranges_for_optional_file_diff_text(
                    self.worktree_preview_line_raw_text(source_visible_ix)
                        .as_ref(),
                    preview_columns,
                    self.reveal_whitespace_chars,
                ),
                diff_wrap_empty_byte_ranges(),
            );
        }
        if self.is_collapsed_diff_projection_active() {
            let Some(row) = self.collapsed_visible_row(source_visible_ix) else {
                return (diff_wrap_empty_byte_ranges(), diff_wrap_empty_byte_ranges());
            };
            return match row {
                CollapsedDiffVisibleRow::HunkHeader { .. } => {
                    (diff_wrap_empty_byte_ranges(), diff_wrap_empty_byte_ranges())
                }
                CollapsedDiffVisibleRow::FileRow { row_ix } => match self.diff_view {
                    DiffViewMode::Inline => {
                        let Some(row) = self.file_diff_inline_render_data(row_ix) else {
                            return (diff_wrap_empty_byte_ranges(), diff_wrap_empty_byte_ranges());
                        };
                        (
                            diff_wrap_byte_ranges_for_file_diff_text(
                                &row.text,
                                inline_columns,
                                self.reveal_whitespace_chars,
                            ),
                            diff_wrap_empty_byte_ranges(),
                        )
                    }
                    DiffViewMode::Split => {
                        let Some(row) = self.file_diff_split_render_data(row_ix) else {
                            return (diff_wrap_empty_byte_ranges(), diff_wrap_empty_byte_ranges());
                        };
                        (
                            diff_wrap_byte_ranges_for_optional_file_diff_text(
                                row.old.as_ref(),
                                split_columns,
                                self.reveal_whitespace_chars,
                            ),
                            diff_wrap_byte_ranges_for_optional_file_diff_text(
                                row.new.as_ref(),
                                split_columns,
                                self.reveal_whitespace_chars,
                            ),
                        )
                    }
                },
            };
        }

        let Some(mapped_ix) = self.diff_source_mapped_ix_for_visible_ix(source_visible_ix) else {
            return (diff_wrap_empty_byte_ranges(), diff_wrap_empty_byte_ranges());
        };
        if self.is_file_diff_view_active() {
            return match self.diff_view {
                DiffViewMode::Inline => {
                    if let Some(row) = self.file_diff_inline_render_data(mapped_ix) {
                        return (
                            diff_wrap_byte_ranges_for_file_diff_text(
                                &row.text,
                                inline_columns,
                                self.reveal_whitespace_chars,
                            ),
                            diff_wrap_empty_byte_ranges(),
                        );
                    }
                    let Some(line) = self.file_diff_inline_row(mapped_ix) else {
                        return (diff_wrap_empty_byte_ranges(), diff_wrap_empty_byte_ranges());
                    };
                    let text = self
                        .diff_text_full_line_for_region(source_visible_ix, DiffTextRegion::Inline);
                    (
                        diff_wrap_byte_ranges_for_text(
                            text.as_ref(),
                            Some(crate::view::diff_utils::diff_content_text(&line)),
                            inline_columns,
                            self.reveal_whitespace_chars,
                        ),
                        diff_wrap_empty_byte_ranges(),
                    )
                }
                DiffViewMode::Split => {
                    let Some(row) = self.file_diff_split_render_data(mapped_ix) else {
                        return (diff_wrap_empty_byte_ranges(), diff_wrap_empty_byte_ranges());
                    };
                    (
                        diff_wrap_byte_ranges_for_optional_file_diff_text(
                            row.old.as_ref(),
                            split_columns,
                            self.reveal_whitespace_chars,
                        ),
                        diff_wrap_byte_ranges_for_optional_file_diff_text(
                            row.new.as_ref(),
                            split_columns,
                            self.reveal_whitespace_chars,
                        ),
                    )
                }
            };
        }

        match self.diff_view {
            DiffViewMode::Inline => {
                let click_kind = self
                    .diff_click_kinds
                    .get(mapped_ix)
                    .copied()
                    .unwrap_or(DiffClickKind::Line);
                if click_kind != DiffClickKind::Line {
                    return (diff_wrap_empty_byte_ranges(), diff_wrap_empty_byte_ranges());
                }
                let Some(line) = self.patch_diff_row(mapped_ix) else {
                    return (diff_wrap_empty_byte_ranges(), diff_wrap_empty_byte_ranges());
                };
                let text =
                    self.diff_text_full_line_for_region(source_visible_ix, DiffTextRegion::Inline);
                (
                    diff_wrap_byte_ranges_for_text(
                        text.as_ref(),
                        Some(line.text.as_ref()),
                        inline_columns,
                        self.reveal_whitespace_chars,
                    ),
                    diff_wrap_empty_byte_ranges(),
                )
            }
            DiffViewMode::Split => match self.patch_diff_split_row(mapped_ix) {
                Some(PatchSplitRow::Aligned { row, .. }) => {
                    let left = self.diff_text_full_line_for_region(
                        source_visible_ix,
                        DiffTextRegion::SplitLeft,
                    );
                    let right = self.diff_text_full_line_for_region(
                        source_visible_ix,
                        DiffTextRegion::SplitRight,
                    );
                    (
                        diff_wrap_byte_ranges_for_text(
                            left.as_ref(),
                            row.old.as_ref().map(|text| text.as_ref()),
                            split_columns,
                            self.reveal_whitespace_chars,
                        ),
                        diff_wrap_byte_ranges_for_text(
                            right.as_ref(),
                            row.new.as_ref().map(|text| text.as_ref()),
                            split_columns,
                            self.reveal_whitespace_chars,
                        ),
                    )
                }
                Some(PatchSplitRow::Raw { src_ix, click_kind }) => {
                    if click_kind != DiffClickKind::Line {
                        return (diff_wrap_empty_byte_ranges(), diff_wrap_empty_byte_ranges());
                    }
                    let Some(line) = self.patch_diff_row(src_ix) else {
                        return (diff_wrap_empty_byte_ranges(), diff_wrap_empty_byte_ranges());
                    };
                    let left = self.diff_text_full_line_for_region(
                        source_visible_ix,
                        DiffTextRegion::SplitLeft,
                    );
                    let right = self.diff_text_full_line_for_region(
                        source_visible_ix,
                        DiffTextRegion::SplitRight,
                    );
                    (
                        diff_wrap_byte_ranges_for_text(
                            left.as_ref(),
                            (!left.is_empty()).then_some(line.text.as_ref()),
                            split_columns,
                            self.reveal_whitespace_chars,
                        ),
                        diff_wrap_byte_ranges_for_text(
                            right.as_ref(),
                            (!right.is_empty()).then_some(line.text.as_ref()),
                            split_columns,
                            self.reveal_whitespace_chars,
                        ),
                    )
                }
                None => (diff_wrap_empty_byte_ranges(), diff_wrap_empty_byte_ranges()),
            },
        }
    }

    /// Patch source lines behind a full-file diff row. The file-diff and
    /// collapsed views render whole file texts rather than patch rows, so a row
    /// is matched back to the patch by file path plus line number.
    pub(in crate::view) fn patch_src_ixs_for_file_diff_row(&self, row_ix: usize) -> Vec<usize> {
        let (old_line, new_line) = match self.diff_view {
            DiffViewMode::Inline => {
                let Some(line) = self.file_diff_inline_render_data(row_ix) else {
                    return Vec::new();
                };
                (line.old_line, line.new_line)
            }
            DiffViewMode::Split => {
                let Some(row) = self.file_diff_split_render_data(row_ix) else {
                    return Vec::new();
                };
                (row.old_line, row.new_line)
            }
        };
        self.patch_src_ixs_for_file_line(old_line, new_line)
    }

    fn patch_src_ixs_for_file_line(
        &self,
        old_line: Option<u32>,
        new_line: Option<u32>,
    ) -> Vec<usize> {
        // A row with no line number on either side cannot identify a patch line.
        if old_line.is_none() && new_line.is_none() {
            return Vec::new();
        }
        let Some(abs) = self.file_diff_cache_path.as_ref() else {
            return Vec::new();
        };
        let Some(workdir) = self.rendered_diff_workdir() else {
            return Vec::new();
        };
        let rel = abs.strip_prefix(workdir).unwrap_or(abs);
        // Git diffs use forward slashes even on Windows.
        let rel_str = rel.to_str().map(|text| text.replace('\\', "/"));

        let mut out = Vec::with_capacity(2);
        for src_ix in 0..self.patch_diff_row_len() {
            if self
                .diff_file_for_src_ix
                .get(src_ix)
                .and_then(|p| p.as_deref())
                != rel_str.as_deref()
            {
                continue;
            }
            let Some(line) = self.patch_diff_row(src_ix) else {
                continue;
            };
            let matched = match line.kind {
                gitcomet_core::domain::DiffLineKind::Add => line.new_line == new_line,
                gitcomet_core::domain::DiffLineKind::Remove
                | gitcomet_core::domain::DiffLineKind::Context => line.old_line == old_line,
                gitcomet_core::domain::DiffLineKind::Header
                | gitcomet_core::domain::DiffLineKind::Hunk => false,
            };
            if matched {
                out.push(src_ix);
            }
        }
        out.sort_unstable();
        out.dedup();
        out
    }

    pub(in crate::view) fn diff_src_ixs_for_visible_ix(&self, visible_ix: usize) -> Vec<usize> {
        if self.is_collapsed_diff_projection_active() {
            let Some(source_visible_ix) = self.diff_source_visible_ix_for_visible_ix(visible_ix)
            else {
                return Vec::new();
            };
            let Some(row) = self.collapsed_visible_row(source_visible_ix) else {
                return Vec::new();
            };
            match row {
                CollapsedDiffVisibleRow::HunkHeader { .. } => {
                    return row.header_action_src_ix().into_iter().collect();
                }
                CollapsedDiffVisibleRow::FileRow { row_ix } => {
                    return self.patch_src_ixs_for_file_diff_row(row_ix);
                }
            }
        }

        let Some(mapped_ix) = self.diff_mapped_ix_for_visible_ix(visible_ix) else {
            return Vec::new();
        };

        if self.is_file_diff_view_active() {
            return self.patch_src_ixs_for_file_diff_row(mapped_ix);
        }

        match self.diff_view {
            DiffViewMode::Inline => vec![mapped_ix],
            DiffViewMode::Split => {
                let Some(row) = self.patch_diff_split_row(mapped_ix) else {
                    return Vec::new();
                };
                match row {
                    PatchSplitRow::Raw { src_ix, .. } => vec![src_ix],
                    PatchSplitRow::Aligned {
                        old_src_ix,
                        new_src_ix,
                        ..
                    } => {
                        let mut out = Vec::with_capacity(2);
                        if let Some(ix) = old_src_ix {
                            out.push(ix);
                        }
                        if let Some(ix) = new_src_ix
                            && out.first().copied() != Some(ix)
                        {
                            out.push(ix);
                        }
                        out
                    }
                }
            }
        }
    }

    pub(super) fn diff_enclosing_hunk_src_ix(&self, src_ix: usize) -> Option<usize> {
        let src_ix = src_ix.min(self.patch_diff_row_len().saturating_sub(1));
        for ix in (0..=src_ix).rev() {
            let line = self.patch_diff_row(ix)?;
            if matches!(line.kind, gitcomet_core::domain::DiffLineKind::Header)
                && line.text.starts_with("diff --git ")
            {
                break;
            }
            if matches!(line.kind, gitcomet_core::domain::DiffLineKind::Hunk) {
                return Some(ix);
            }
        }
        None
    }

    pub(in crate::view) fn select_all_diff_text(&mut self) {
        // Markdown preview (both file preview and diff preview) uses
        // markdown preview row counts instead of source-text line counts.
        if self.is_markdown_preview_active() {
            let Some(count) = self.markdown_preview_row_count() else {
                return;
            };
            if count == 0 {
                return;
            }
            let region = if self.is_file_preview_active() {
                DiffTextRegion::Inline
            } else {
                match self.diff_view {
                    DiffViewMode::Inline => DiffTextRegion::Inline,
                    DiffViewMode::Split => self
                        .diff_text_head
                        .or(self.diff_text_anchor)
                        .map(|p| p.region)
                        .filter(|r| {
                            matches!(r, DiffTextRegion::SplitLeft | DiffTextRegion::SplitRight)
                        })
                        .unwrap_or(DiffTextRegion::SplitLeft),
                }
            };
            let end_visible_ix = count - 1;
            let end_offset = self.diff_text_line_len_for_region(end_visible_ix, region);

            self.diff_text_selecting = false;
            self.diff_text_anchor = Some(DiffTextPos {
                source_visible_ix: 0,
                region,
                offset: 0,
            });
            self.diff_text_head = Some(DiffTextPos {
                source_visible_ix: end_visible_ix,
                region,
                offset: end_offset,
            });
            self.sync_diff_focus_to_text_selection();
            return;
        }

        if self.is_file_preview_active() {
            let Some(count) = self.worktree_preview_line_count() else {
                return;
            };
            if count == 0 {
                return;
            }
            let end_visible_ix = count - 1;
            let end_offset =
                self.diff_text_line_len_for_region(end_visible_ix, DiffTextRegion::Inline);

            self.diff_text_selecting = false;
            self.diff_text_anchor = Some(DiffTextPos {
                source_visible_ix: 0,
                region: DiffTextRegion::Inline,
                offset: 0,
            });
            self.diff_text_head = Some(DiffTextPos {
                source_visible_ix: end_visible_ix,
                region: DiffTextRegion::Inline,
                offset: end_offset,
            });
            self.sync_diff_focus_to_text_selection();
            return;
        }

        if self.diff_source_visible_len() == 0 {
            return;
        }

        let start_region = match self.diff_view {
            DiffViewMode::Inline => DiffTextRegion::Inline,
            DiffViewMode::Split => self
                .diff_text_head
                .or(self.diff_text_anchor)
                .map(|p| p.region)
                .filter(|r| matches!(r, DiffTextRegion::SplitLeft | DiffTextRegion::SplitRight))
                .unwrap_or(DiffTextRegion::SplitLeft),
        };

        let end_visible_ix = self.diff_source_visible_len() - 1;
        let end_region = start_region;
        let end_offset = self
            .diff_text_full_line_for_region(end_visible_ix, end_region)
            .len();

        self.diff_text_selecting = false;
        self.diff_text_anchor = Some(DiffTextPos {
            source_visible_ix: 0,
            region: start_region,
            offset: 0,
        });
        self.diff_text_head = Some(DiffTextPos {
            source_visible_ix: end_visible_ix,
            region: end_region,
            offset: end_offset,
        });
        self.sync_diff_focus_to_text_selection();
    }

    pub(super) fn split_next_boundary_visible_ix(
        &self,
        from_visible_ix: usize,
        is_boundary: impl Fn(&PatchSplitRow) -> bool,
    ) -> Option<usize> {
        let visible_len = self.diff_visible_len();
        let from_visible_ix = from_visible_ix.min(visible_len.saturating_sub(1));
        for visible_ix in (from_visible_ix + 1)..visible_len {
            let row_ix = self.diff_mapped_ix_for_visible_ix(visible_ix)?;
            let row = self.patch_diff_split_row(row_ix)?;
            if is_boundary(&row) {
                return Some(visible_ix.saturating_sub(1));
            }
        }
        None
    }

    pub(super) fn diff_next_boundary_visible_ix(
        &self,
        from_visible_ix: usize,
        is_boundary: impl Fn(usize) -> bool,
    ) -> Option<usize> {
        let visible_len = self.diff_visible_len();
        let from_visible_ix = from_visible_ix.min(visible_len.saturating_sub(1));
        for visible_ix in (from_visible_ix + 1)..visible_len {
            let src_ix = self.diff_mapped_ix_for_visible_ix(visible_ix)?;
            if is_boundary(src_ix) {
                return Some(visible_ix.saturating_sub(1));
            }
        }
        None
    }

    fn diff_split_scroll_handles(&self) -> [ScrollHandle; 2] {
        [
            uniform_list_base_handle(&self.diff_scroll),
            uniform_list_base_handle(&self.diff_split_right_scroll),
        ]
    }

    fn conflict_preview_scroll_handles(&self) -> [ScrollHandle; 4] {
        [
            uniform_list_base_handle(&self.conflict_resolver_diff_scroll),
            uniform_list_base_handle(&self.conflict_preview_ours_scroll),
            uniform_list_base_handle(&self.conflict_preview_theirs_scroll),
            self.conflict_resolved_output_editor_scroll.clone(),
        ]
    }

    /// Forward a horizontal wheel gesture over the resolved-output pane onto the
    /// diff columns. Native scrolling moves the output's content-width handle;
    /// forwarding the same delta lets the narrower columns respond immediately,
    /// and the normal bidirectional sync reconciles their clamped offsets.
    pub(in crate::view) fn forward_conflict_output_horizontal_wheel(
        &self,
        event: &gpui::ScrollWheelEvent,
        window: &gpui::Window,
    ) -> bool {
        // Only when output/column sync and horizontal diff sync are enabled.
        if !self.mergetool_output_scroll_sync || !self.diff_scroll_sync.includes_horizontal() {
            return false;
        }
        let delta_x = event.delta.pixel_delta(window.line_height()).x;
        if delta_x == px(0.0) {
            return false;
        }
        let handles = self.conflict_preview_scroll_handles();
        // Indices into `handles`: base/ours/theirs are the diff columns. Native
        // overflow handling owns the output handle at index 3.
        let columns: &[usize] = match self.conflict_resolver.view_mode {
            ConflictResolverViewMode::ThreeWay => &[0, 1, 2],
            ConflictResolverViewMode::TwoWayDiff => &[0, 2],
        };
        let mut changed = false;
        for &ix in columns {
            let handle = &handles[ix];
            let max_x = handle.max_offset().x.max(px(0.0));
            if max_x <= px(0.0) {
                continue;
            }
            let cur = handle.offset();
            // Mirrors gpui's own overflow-scroll wheel handling: add the raw
            // delta, then clamp into the scrollable range [-max_x, 0].
            let next_x = (cur.x + delta_x).clamp(-max_x, px(0.0));
            if next_x != cur.x {
                handle.set_offset(point(next_x, cur.y));
                changed = true;
            }
        }
        changed
    }

    pub(in crate::view) fn sync_diff_split_scroll(&mut self) {
        let handles = self.diff_split_scroll_handles();
        maybe_sync_synced_scroll_offsets(
            &handles,
            &mut self.diff_split_last_synced_y,
            SyncedScrollAxis::Vertical,
            self.diff_scroll_sync,
        );
        maybe_sync_synced_scroll_offsets(
            &handles,
            &mut self.diff_split_last_synced_x,
            SyncedScrollAxis::Horizontal,
            self.diff_scroll_sync,
        );
    }

    pub(in crate::view) fn record_conflict_vertical_wheel_master(&mut self, master_ix: usize) {
        self.conflict_preview_vertical_wheel_master = Some(master_ix);
        self.conflict_output_gutter_wheel_sync_pending = true;
    }

    pub(in crate::view) fn sync_conflict_preview_scroll(&mut self) {
        let vertical_wheel_master = self.conflict_preview_vertical_wheel_master.take();
        let handles = self.conflict_preview_scroll_handles();
        let group = self.conflict_preview_sync_group();
        for (axis, last_synced) in [
            (
                SyncedScrollAxis::Vertical,
                &mut self.conflict_preview_last_synced_y,
            ),
            (
                SyncedScrollAxis::Horizontal,
                &mut self.conflict_preview_last_synced_x,
            ),
        ] {
            sync_conflict_preview_axis(
                &handles,
                last_synced,
                axis,
                self.diff_scroll_sync,
                group,
                if axis == SyncedScrollAxis::Vertical {
                    vertical_wheel_master
                } else {
                    None
                },
            );
        }
    }

    /// Which conflict-preview lists share a row space and may be raw-offset
    /// synced in the current resolver mode.
    ///
    /// The resolved output renders full merged lines, so it only joins the
    /// group when the columns render an unfolded whole-file row space — the
    /// three-way unfolded columns or the section 30 aligned two-way full mode — and
    /// only when the merge-tool output-scroll-sync setting is on. Folded
    /// column spaces (hide-resolved / collapsed context) and block-local
    /// giant-file two-way rows keep the output independent, because raw
    /// offsets are meaningless across mismatched row spaces.
    fn conflict_preview_sync_group(&self) -> ConflictPreviewSyncGroup {
        let folded =
            self.conflict_resolver.hide_resolved || self.conflict_resolver.collapse_context;
        let output_follows = self.mergetool_output_scroll_sync;
        match self.conflict_resolver.view_mode {
            ConflictResolverViewMode::ThreeWay => {
                if folded || !output_follows {
                    ConflictPreviewSyncGroup::ColumnsOnly
                } else {
                    ConflictPreviewSyncGroup::ColumnsAndOutput
                }
            }
            ConflictResolverViewMode::TwoWayDiff => {
                if !self.conflict_resolver.two_way_uses_aligned_rows() || folded || !output_follows
                {
                    ConflictPreviewSyncGroup::TwoWayPair
                } else {
                    ConflictPreviewSyncGroup::TwoWayPairAndOutput
                }
            }
        }
    }

    pub(in crate::view) fn sync_conflict_resolved_output_gutter_scroll(&mut self) {
        let handles = [
            uniform_list_base_handle(&self.conflict_resolved_preview_gutter_scroll),
            self.conflict_resolved_output_editor_scroll.clone(),
        ];
        let explicit_master_ix = self.conflict_output_gutter_wheel_sync_pending.then_some(1);
        self.conflict_output_gutter_wheel_sync_pending = false;
        sync_synced_scroll_offsets_with_master(
            &handles,
            &mut self.conflict_resolved_preview_gutter_last_synced_y,
            SyncedScrollAxis::Vertical,
            explicit_master_ix,
        );
    }

    pub(in crate::view) fn main_pane_content_width(&self, cx: &mut gpui::Context<Self>) -> Pixels {
        let _ = cx;

        super::pane_content_width_for_layout(
            self.last_window_size.width,
            self.layout_sidebar_render_width,
            self.layout_details_render_width,
            self.layout_sidebar_collapsed,
            self.layout_details_collapsed,
        )
    }
}

/// Decide whether a blame (re)load should be dispatched for the rendered target.
///
/// `same_target` is whether the currently loaded blame is for the same
/// file/source. `force` requests a retry of a previous failure (an explicit user
/// toggle); the per-frame Render path passes `false` so a persistent error does
/// not cause a dispatch-every-frame loop.
fn should_request_blame<T>(
    same_target: bool,
    blame: &gitcomet_state::model::Loadable<T>,
    force: bool,
) -> bool {
    use gitcomet_state::model::Loadable;
    if !same_target {
        // A new or changed target always (re)loads.
        return true;
    }
    match blame {
        // Already loaded or in flight for this target: nothing to do.
        Loadable::Ready(_) | Loadable::Loading => false,
        // A previous attempt failed: retry only on an explicit user toggle.
        Loadable::Error(_) => force,
        Loadable::NotLoaded => true,
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn notify_fingerprint_tracks_cherry_pick_message_readiness() {
        use gitcomet_core::domain::RepoSpec;
        use gitcomet_core::services::{InteractiveRebaseAction, InteractiveRebaseEntry};
        use gitcomet_state::model::{InteractiveCherryPickSetup, RepoState};
        use std::path::PathBuf;

        let mut state = AppState::default();
        state.active_repo = Some(RepoId(1));
        state.repos.push(RepoState::new_opening(
            RepoId(1),
            RepoSpec {
                workdir: PathBuf::from("/tmp/repo"),
            },
        ));
        let without_setup = MainPaneView::notify_fingerprint_for(&state);

        state.repos[0].interactive_cherry_pick_setup = Some(InteractiveCherryPickSetup {
            entries: vec![InteractiveRebaseEntry {
                action: InteractiveRebaseAction::Pick,
                commit_id: "1111111111111111111111111111111111111111".to_string(),
                summary: "subject".to_string(),
                message: "subject".to_string(),
                new_message: None,
            }],
            source_colors: vec![],
            full_messages: Loadable::Loading,
        });
        let loading = MainPaneView::notify_fingerprint_for(&state);
        assert_ne!(loading, without_setup);

        state.repos[0]
            .interactive_cherry_pick_setup
            .as_mut()
            .expect("setup")
            .full_messages = Loadable::Ready(());
        let ready = MainPaneView::notify_fingerprint_for(&state);
        assert_ne!(ready, loading);
    }

    #[test]
    fn should_request_blame_retries_failure_only_when_forced() {
        use gitcomet_state::model::Loadable;
        // A new/changed target always loads, regardless of state or force.
        assert!(should_request_blame(
            false,
            &Loadable::<()>::Ready(()),
            false
        ));
        assert!(should_request_blame(
            false,
            &Loadable::<()>::Error("x".into()),
            false
        ));
        // Same target, healthy or in flight: never reload (even when forced), so a
        // toggle-on doesn't re-blame an already-loaded file.
        assert!(!should_request_blame(
            true,
            &Loadable::<()>::Ready(()),
            true
        ));
        assert!(!should_request_blame(true, &Loadable::<()>::Loading, true));
        // Same target, not yet loaded: load.
        assert!(should_request_blame(
            true,
            &Loadable::<()>::NotLoaded,
            false
        ));
        // Same target, failed: never retry from the per-frame Render path
        // (force=false), but retry on an explicit toggle (force=true).
        assert!(!should_request_blame(
            true,
            &Loadable::<()>::Error("e".into()),
            false
        ));
        assert!(should_request_blame(
            true,
            &Loadable::<()>::Error("e".into()),
            true
        ));
    }

    #[test]
    fn clamp_raw_scroll_y_uses_gpui_negative_offset_range() {
        assert_eq!(clamp_raw_scroll_y(px(-180.0), px(120.0)), px(-120.0));
        assert_eq!(clamp_raw_scroll_y(px(180.0), px(120.0)), px(0.0));
        assert_eq!(clamp_raw_scroll_y(px(-40.0), px(120.0)), px(-40.0));
    }

    #[test]
    fn synced_scroll_offsets_keep_longer_pane_as_master_after_shorter_clamps() {
        let targets = compute_synced_scroll_offsets(
            [px(-100.0), px(-500.0)],
            [px(100.0), px(500.0)],
            [px(-90.0), px(-90.0)],
            1,
        );

        assert_eq!(targets, [px(-100.0), px(-500.0)]);
    }

    #[test]
    fn synced_scroll_offsets_follow_shorter_pane_when_user_scrolled_it() {
        let targets = compute_synced_scroll_offsets(
            [px(-100.0), px(-320.0)],
            [px(100.0), px(500.0)],
            [px(-80.0), px(-320.0)],
            1,
        );

        assert_eq!(targets, [px(-100.0), px(-100.0)]);
    }

    #[test]
    fn synced_scroll_offsets_support_four_panes_when_output_is_scrolled() {
        let targets = compute_synced_scroll_offsets(
            [px(-100.0), px(-100.0), px(-100.0), px(-320.0)],
            [px(100.0), px(100.0), px(100.0), px(500.0)],
            [px(-100.0), px(-100.0), px(-100.0), px(-80.0)],
            3,
        );

        assert_eq!(targets, [px(-100.0), px(-100.0), px(-100.0), px(-320.0)]);
    }

    #[test]
    fn synced_scroll_offsets_hold_steady_when_nothing_changed() {
        // A clamped follower (shorter pane, offset -100) sits alongside a master
        // scrolled further (-320). Nothing moved since the last sync (offsets ==
        // last_synced), so even though the offsets are unequal the follower must
        // stay put — re-driving it onto the widest handle here is the idle-frame
        // snap-back the horizontal output sync used to produce.
        let steady = [px(-100.0), px(-320.0)];
        let targets = compute_synced_scroll_offsets(steady, [px(100.0), px(500.0)], steady, 1);

        assert_eq!(targets, steady);
    }

    #[test]
    fn synced_scroll_offsets_do_not_promote_a_follower_clamped_during_paint() {
        let steady = [px(-100.0), px(-500.0)];
        let targets = compute_synced_scroll_offsets(
            steady,
            [px(100.0), px(500.0)],
            // The previous render requested -120 for the shorter follower;
            // GPUI painted it at its current -100 maximum afterward.
            [px(-120.0), px(-500.0)],
            1,
        );

        assert_eq!(targets, steady);
    }

    #[test]
    fn explicit_wheel_master_wins_when_multiple_handles_changed() {
        let targets = compute_synced_scroll_offsets_with_master(
            [px(0.0), px(-100.0)],
            [px(500.0), px(500.0)],
            [px(-100.0), px(0.0)],
            0,
            Some(1),
        );

        assert_eq!(targets, [px(-100.0), px(-100.0)]);
    }

    #[test]
    fn explicit_wheel_master_at_top_pulls_stale_follower_to_top() {
        let targets = compute_synced_scroll_offsets_with_master(
            [px(0.0), px(-100.0)],
            [px(500.0), px(500.0)],
            [px(0.0), px(-100.0)],
            1,
            Some(0),
        );

        assert_eq!(targets, [px(0.0), px(0.0)]);
    }

    #[test]
    fn revealed_whitespace_wrap_ranges_follow_rendered_tab_markers() {
        let hidden = diff_wrap_byte_ranges_for_text("a    b", Some("a\tb"), 4, false)
            .into_iter()
            .map(rows::DiffWrapByteRange::range)
            .collect::<Vec<_>>();
        assert_eq!(hidden, vec![0..4, 4..6]);

        let revealed = diff_wrap_byte_ranges_for_text("a    b", Some("a\tb"), 4, true)
            .into_iter()
            .map(rows::DiffWrapByteRange::range)
            .collect::<Vec<_>>();
        assert_eq!(revealed, vec![0..6]);
    }
}
