1805 lines
69 KiB
Rust
1805 lines
69 KiB
Rust
use super::*;
|
|
|
|
pub(super) fn build_resolved_output_syntax_highlights(
|
|
theme: AppTheme,
|
|
output_text: &str,
|
|
language: Option<rows::DiffSyntaxLanguage>,
|
|
) -> Vec<(Range<usize>, gpui::HighlightStyle)> {
|
|
let Some(language) = language else {
|
|
return Vec::new();
|
|
};
|
|
let line_count = count_newlines(output_text).saturating_add(1);
|
|
let syntax_mode = if line_count <= CONFLICT_RESOLVED_OUTLINE_AUTO_SYNTAX_MAX_LINES {
|
|
rows::DiffSyntaxMode::Auto
|
|
} else {
|
|
rows::DiffSyntaxMode::HeuristicOnly
|
|
};
|
|
|
|
let mut highlights = Vec::new();
|
|
let mut line_offset = 0usize;
|
|
for (line_ix, line) in output_text.split('\n').enumerate() {
|
|
for (range, style) in rows::syntax_highlights_for_line(theme, line, language, syntax_mode) {
|
|
highlights.push((
|
|
(line_offset + range.start)..(line_offset + range.end),
|
|
style,
|
|
));
|
|
}
|
|
line_offset += line.len();
|
|
if line_ix + 1 < line_count {
|
|
line_offset += 1;
|
|
}
|
|
}
|
|
highlights
|
|
}
|
|
|
|
pub(super) fn split_text_lines_owned(text: &str) -> Vec<String> {
|
|
if text.is_empty() {
|
|
Vec::new()
|
|
} else {
|
|
text.split('\n').map(|line| line.to_string()).collect()
|
|
}
|
|
}
|
|
|
|
pub(super) fn count_newlines(text: &str) -> usize {
|
|
text.as_bytes().iter().filter(|&&b| b == b'\n').count()
|
|
}
|
|
|
|
pub(super) fn build_line_starts(text: &str) -> Vec<usize> {
|
|
let mut line_starts = Vec::with_capacity(count_newlines(text).saturating_add(1));
|
|
line_starts.push(0usize);
|
|
for (ix, byte) in text.as_bytes().iter().enumerate() {
|
|
if *byte == b'\n' {
|
|
line_starts.push(ix.saturating_add(1));
|
|
}
|
|
}
|
|
line_starts
|
|
}
|
|
|
|
pub(super) fn line_start_offset_for_index(
|
|
line_starts: &[usize],
|
|
text_len: usize,
|
|
line_ix: usize,
|
|
) -> usize {
|
|
line_starts.get(line_ix).copied().unwrap_or(text_len)
|
|
}
|
|
|
|
pub(super) fn source_line_count(text: &str) -> usize {
|
|
if text.is_empty() {
|
|
0
|
|
} else {
|
|
text.lines().count()
|
|
}
|
|
}
|
|
|
|
/// Number of logical rows produced by `split('\n')` (always at least 1).
|
|
pub(super) fn split_line_count(text: &str) -> usize {
|
|
count_newlines(text).saturating_add(1)
|
|
}
|
|
|
|
/// Byte range of line content at `line_ix` (without trailing newline).
|
|
///
|
|
/// Uses `split('\n')` row semantics, so trailing newline creates a final empty row.
|
|
pub(super) fn line_content_byte_range_for_index(
|
|
text: &str,
|
|
line_ix: usize,
|
|
) -> Option<Range<usize>> {
|
|
let line_count = split_line_count(text);
|
|
if line_ix >= line_count {
|
|
return None;
|
|
}
|
|
let line_starts = build_line_starts(text);
|
|
let text_len = text.len();
|
|
let start = line_starts.get(line_ix).copied().unwrap_or(text_len);
|
|
let mut end = line_starts
|
|
.get(line_ix.saturating_add(1))
|
|
.copied()
|
|
.unwrap_or(text_len)
|
|
.min(text_len);
|
|
if end > start && text.as_bytes().get(end.saturating_sub(1)) == Some(&b'\n') {
|
|
end = end.saturating_sub(1);
|
|
}
|
|
Some(start..end)
|
|
}
|
|
|
|
/// Build insertion text for appending one logical line to output.
|
|
pub(super) fn append_line_insertion_text(existing: &str, line: &str) -> String {
|
|
let needs_leading_newline = !existing.is_empty() && !existing.ends_with('\n');
|
|
let mut out = String::with_capacity(
|
|
line.len()
|
|
.saturating_add(1)
|
|
.saturating_add(usize::from(needs_leading_newline)),
|
|
);
|
|
if needs_leading_newline {
|
|
out.push('\n');
|
|
}
|
|
out.push_str(line);
|
|
out.push('\n');
|
|
out
|
|
}
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub(super) struct ResolvedOutlineDelta {
|
|
pub(super) old_range: Range<usize>,
|
|
pub(super) new_range: Range<usize>,
|
|
}
|
|
|
|
pub(super) fn resolved_outline_delta_between_texts(
|
|
old_text: &str,
|
|
new_text: &str,
|
|
) -> Option<ResolvedOutlineDelta> {
|
|
if old_text == new_text {
|
|
return None;
|
|
}
|
|
|
|
let old = old_text.as_bytes();
|
|
let new = new_text.as_bytes();
|
|
let old_len = old.len();
|
|
let new_len = new.len();
|
|
|
|
let mut prefix = 0usize;
|
|
let prefix_max = old_len.min(new_len);
|
|
while prefix < prefix_max && old[prefix] == new[prefix] {
|
|
prefix = prefix.saturating_add(1);
|
|
}
|
|
while prefix > 0 && (!old_text.is_char_boundary(prefix) || !new_text.is_char_boundary(prefix)) {
|
|
prefix = prefix.saturating_sub(1);
|
|
}
|
|
|
|
let mut suffix = 0usize;
|
|
while suffix < old_len.saturating_sub(prefix)
|
|
&& suffix < new_len.saturating_sub(prefix)
|
|
&& old[old_len.saturating_sub(1 + suffix)] == new[new_len.saturating_sub(1 + suffix)]
|
|
{
|
|
suffix = suffix.saturating_add(1);
|
|
}
|
|
while suffix > 0
|
|
&& (!old_text.is_char_boundary(old_len.saturating_sub(suffix))
|
|
|| !new_text.is_char_boundary(new_len.saturating_sub(suffix)))
|
|
{
|
|
suffix = suffix.saturating_sub(1);
|
|
}
|
|
|
|
Some(ResolvedOutlineDelta {
|
|
old_range: prefix..old_len.saturating_sub(suffix),
|
|
new_range: prefix..new_len.saturating_sub(suffix),
|
|
})
|
|
}
|
|
|
|
fn line_index_for_byte_offset(line_starts: &[usize], byte_offset: usize) -> usize {
|
|
if line_starts.is_empty() {
|
|
return 0;
|
|
}
|
|
line_starts
|
|
.partition_point(|&start| start <= byte_offset)
|
|
.saturating_sub(1)
|
|
}
|
|
|
|
pub(super) fn dirty_byte_range_to_line_range(
|
|
line_starts: &[usize],
|
|
text_len: usize,
|
|
dirty_range: Range<usize>,
|
|
) -> Range<usize> {
|
|
let line_count = line_starts.len().max(1);
|
|
let start_byte = dirty_range.start.min(text_len);
|
|
let end_byte = dirty_range.end.min(text_len);
|
|
let start_line = line_index_for_byte_offset(line_starts, start_byte).min(line_count - 1);
|
|
let end_line_exclusive = if dirty_range.is_empty() {
|
|
start_line.saturating_add(1)
|
|
} else {
|
|
line_index_for_byte_offset(line_starts, end_byte).saturating_add(1)
|
|
}
|
|
.clamp(start_line.saturating_add(1), line_count);
|
|
start_line..end_line_exclusive
|
|
}
|
|
|
|
pub(super) fn shifted_line_index(ix: usize, delta: isize) -> usize {
|
|
if delta >= 0 {
|
|
ix.saturating_add(delta as usize)
|
|
} else {
|
|
ix.saturating_sub((-delta) as usize)
|
|
}
|
|
}
|
|
|
|
pub(super) fn remap_line_keyed_cache_for_delta<T>(
|
|
cache: &mut HashMap<usize, T>,
|
|
old_range: Range<usize>,
|
|
new_range: Range<usize>,
|
|
) {
|
|
let shift = new_range.len() as isize - old_range.len() as isize;
|
|
let previous = std::mem::take(cache);
|
|
for (line_ix, value) in previous {
|
|
if line_ix < old_range.start {
|
|
cache.insert(line_ix, value);
|
|
continue;
|
|
}
|
|
if line_ix >= old_range.end {
|
|
cache.insert(shifted_line_index(line_ix, shift), value);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(super) fn resolved_output_conflict_block_ranges_in_text(
|
|
marker_segments: &[conflict_resolver::ConflictSegment],
|
|
output_text: &str,
|
|
) -> Option<Vec<Range<usize>>> {
|
|
fn is_line_boundary(text: &str, byte_ix: usize) -> bool {
|
|
if byte_ix == 0 || byte_ix == text.len() {
|
|
return true;
|
|
}
|
|
text.as_bytes()
|
|
.get(byte_ix.saturating_sub(1))
|
|
.is_some_and(|b| *b == b'\n')
|
|
}
|
|
|
|
let mut ranges = Vec::new();
|
|
let mut cursor = 0usize;
|
|
let mut line_offset = 0usize;
|
|
for seg in marker_segments {
|
|
match seg {
|
|
conflict_resolver::ConflictSegment::Text(text) => {
|
|
let tail = output_text.get(cursor..)?;
|
|
if !tail.starts_with(text) {
|
|
return None;
|
|
}
|
|
cursor = cursor.saturating_add(text.len());
|
|
line_offset = line_offset.saturating_add(count_newlines(text));
|
|
}
|
|
conflict_resolver::ConflictSegment::Block(block) => {
|
|
let expected = conflict_resolver::generate_resolved_text(&[
|
|
conflict_resolver::ConflictSegment::Block(block.clone()),
|
|
]);
|
|
let tail = output_text.get(cursor..)?;
|
|
if !tail.starts_with(&expected) {
|
|
return None;
|
|
}
|
|
let end = cursor.saturating_add(expected.len());
|
|
if end < cursor
|
|
|| !is_line_boundary(output_text, cursor)
|
|
|| !is_line_boundary(output_text, end)
|
|
{
|
|
return None;
|
|
}
|
|
let start_line = line_offset;
|
|
let mut end_line = line_offset.saturating_add(count_newlines(&expected));
|
|
if end == output_text.len() && !expected.is_empty() {
|
|
end_line = end_line.saturating_add(1);
|
|
}
|
|
ranges.push(start_line..end_line);
|
|
line_offset = line_offset.saturating_add(count_newlines(&expected));
|
|
cursor = end;
|
|
}
|
|
}
|
|
}
|
|
|
|
Some(ranges)
|
|
}
|
|
|
|
pub(super) fn conflict_marker_ranges_for_block(
|
|
block: &conflict_resolver::ConflictBlock,
|
|
line_range: Range<usize>,
|
|
) -> Vec<Range<usize>> {
|
|
let mut marker_ranges = Vec::new();
|
|
if !block.resolved
|
|
&& let Some(relative_subranges) = unresolved_decision_ranges_for_block(block)
|
|
.or_else(|| unresolved_subchunk_conflict_ranges_for_block(block))
|
|
{
|
|
for relative in relative_subranges {
|
|
let start = line_range
|
|
.start
|
|
.saturating_add(relative.start)
|
|
.min(line_range.end);
|
|
let end = line_range
|
|
.start
|
|
.saturating_add(relative.end)
|
|
.min(line_range.end);
|
|
marker_ranges.push(start..end);
|
|
}
|
|
}
|
|
if marker_ranges.is_empty() {
|
|
marker_ranges.push(line_range);
|
|
}
|
|
marker_ranges
|
|
}
|
|
|
|
pub(super) fn write_conflict_markers_for_ranges(
|
|
markers: &mut [Option<ResolvedOutputConflictMarker>],
|
|
conflict_ix: usize,
|
|
unresolved: bool,
|
|
marker_ranges: &[Range<usize>],
|
|
) {
|
|
let output_line_count = markers.len();
|
|
if output_line_count == 0 {
|
|
return;
|
|
}
|
|
|
|
for marker_range in marker_ranges {
|
|
if marker_range.start < marker_range.end {
|
|
let end = marker_range.end.min(output_line_count);
|
|
for (line_ix, marker_slot) in markers
|
|
.iter_mut()
|
|
.enumerate()
|
|
.take(end)
|
|
.skip(marker_range.start)
|
|
{
|
|
*marker_slot = Some(ResolvedOutputConflictMarker {
|
|
conflict_ix,
|
|
range_start: marker_range.start,
|
|
range_end: marker_range.end,
|
|
is_start: line_ix == marker_range.start,
|
|
is_end: line_ix + 1 == marker_range.end,
|
|
unresolved,
|
|
});
|
|
}
|
|
continue;
|
|
}
|
|
|
|
let anchor = marker_range.start.min(output_line_count.saturating_sub(1));
|
|
markers[anchor] = Some(ResolvedOutputConflictMarker {
|
|
conflict_ix,
|
|
range_start: marker_range.start,
|
|
range_end: marker_range.end,
|
|
is_start: true,
|
|
is_end: true,
|
|
unresolved,
|
|
});
|
|
}
|
|
}
|
|
|
|
pub(super) fn output_line_range_for_conflict_block_in_text(
|
|
segments: &[conflict_resolver::ConflictSegment],
|
|
output_text: &str,
|
|
conflict_ix: usize,
|
|
) -> Option<Range<usize>> {
|
|
resolved_output_conflict_block_ranges_in_text(segments, output_text)
|
|
.and_then(|ranges| ranges.get(conflict_ix).cloned())
|
|
}
|
|
|
|
pub(super) fn conflict_fragment_text_for_choice(
|
|
base: &str,
|
|
ours: &str,
|
|
theirs: &str,
|
|
choice: conflict_resolver::ConflictChoice,
|
|
) -> String {
|
|
match choice {
|
|
conflict_resolver::ConflictChoice::Base => base.to_string(),
|
|
conflict_resolver::ConflictChoice::Ours => ours.to_string(),
|
|
conflict_resolver::ConflictChoice::Theirs => theirs.to_string(),
|
|
conflict_resolver::ConflictChoice::Both => {
|
|
let mut out = String::with_capacity(ours.len().saturating_add(theirs.len()));
|
|
out.push_str(ours);
|
|
out.push_str(theirs);
|
|
out
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(super) fn unresolved_subchunk_conflict_ranges_for_block(
|
|
block: &conflict_resolver::ConflictBlock,
|
|
) -> Option<Vec<Range<usize>>> {
|
|
use gitcomet_core::conflict_session::Subchunk;
|
|
|
|
let base = block.base.as_deref()?;
|
|
let subchunks = gitcomet_core::conflict_session::split_conflict_into_subchunks(
|
|
base,
|
|
&block.ours,
|
|
&block.theirs,
|
|
)?;
|
|
let mut ranges = Vec::new();
|
|
let mut line_offset = 0usize;
|
|
for subchunk in subchunks {
|
|
let (fragment, is_conflict) = match subchunk {
|
|
Subchunk::Resolved(text) => (text, false),
|
|
Subchunk::Conflict { base, ours, theirs } => (
|
|
conflict_fragment_text_for_choice(&base, &ours, &theirs, block.choice),
|
|
true,
|
|
),
|
|
};
|
|
let start = line_offset;
|
|
line_offset = line_offset.saturating_add(count_newlines(&fragment));
|
|
if is_conflict {
|
|
ranges.push(start..line_offset);
|
|
}
|
|
}
|
|
if ranges.is_empty() {
|
|
None
|
|
} else {
|
|
Some(ranges)
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub(super) struct UnresolvedDecisionRegion {
|
|
pub(super) row_range: Range<usize>,
|
|
pub(super) selected_line_range: Range<usize>,
|
|
pub(super) alternate_line_range: Range<usize>,
|
|
pub(super) has_non_emitting_rows: bool,
|
|
}
|
|
|
|
pub(super) fn unresolved_decision_regions_for_block(
|
|
block: &conflict_resolver::ConflictBlock,
|
|
) -> Option<Vec<UnresolvedDecisionRegion>> {
|
|
let (left, right, choose_left) = match block.choice {
|
|
conflict_resolver::ConflictChoice::Ours => (&block.ours, &block.theirs, true),
|
|
conflict_resolver::ConflictChoice::Theirs => (&block.theirs, &block.ours, false),
|
|
_ => return None,
|
|
};
|
|
let rows_with_anchors = gitcomet_core::file_diff::side_by_side_rows_with_anchors(left, right);
|
|
let rows = rows_with_anchors.rows;
|
|
let regions = rows_with_anchors.anchors.region_anchors;
|
|
if rows.is_empty() || regions.is_empty() {
|
|
return None;
|
|
}
|
|
|
|
let mut selected_prefix = Vec::with_capacity(rows.len().saturating_add(1));
|
|
selected_prefix.push(0usize);
|
|
let mut selected_count = 0usize;
|
|
let mut alternate_prefix = Vec::with_capacity(rows.len().saturating_add(1));
|
|
alternate_prefix.push(0usize);
|
|
let mut alternate_count = 0usize;
|
|
for row in &rows {
|
|
let selected_emits = if choose_left {
|
|
row.old.is_some()
|
|
} else {
|
|
row.new.is_some()
|
|
};
|
|
if selected_emits {
|
|
selected_count = selected_count.saturating_add(1);
|
|
}
|
|
selected_prefix.push(selected_count);
|
|
|
|
let alternate_emits = if choose_left {
|
|
row.new.is_some()
|
|
} else {
|
|
row.old.is_some()
|
|
};
|
|
if alternate_emits {
|
|
alternate_count = alternate_count.saturating_add(1);
|
|
}
|
|
alternate_prefix.push(alternate_count);
|
|
}
|
|
|
|
let mut decision_regions: Vec<UnresolvedDecisionRegion> = Vec::with_capacity(regions.len());
|
|
for region in regions {
|
|
let row_start = region.row_start.min(rows.len());
|
|
let row_end = region.row_end_exclusive.min(rows.len());
|
|
let selected_line_range = selected_prefix[row_start]..selected_prefix[row_end];
|
|
let alternate_line_range = alternate_prefix[row_start]..alternate_prefix[row_end];
|
|
let has_non_emitting_rows = rows[row_start..row_end].iter().any(|row| {
|
|
let emits = if choose_left {
|
|
row.old.is_some()
|
|
} else {
|
|
row.new.is_some()
|
|
};
|
|
!emits
|
|
});
|
|
|
|
if let Some(last) = decision_regions.last_mut()
|
|
&& last.selected_line_range == selected_line_range
|
|
{
|
|
last.row_range.end = row_end;
|
|
last.alternate_line_range.end =
|
|
last.alternate_line_range.end.max(alternate_line_range.end);
|
|
last.has_non_emitting_rows |= has_non_emitting_rows;
|
|
continue;
|
|
}
|
|
|
|
decision_regions.push(UnresolvedDecisionRegion {
|
|
row_range: row_start..row_end,
|
|
selected_line_range,
|
|
alternate_line_range,
|
|
has_non_emitting_rows,
|
|
});
|
|
}
|
|
if decision_regions.is_empty() {
|
|
return None;
|
|
}
|
|
|
|
// Merge nearby non-zero ranges into one logical decision chunk while
|
|
// preserving insertion anchors as independent picks.
|
|
const MERGE_GAP_LINES: usize = 1;
|
|
let mut merged: Vec<UnresolvedDecisionRegion> = Vec::with_capacity(decision_regions.len());
|
|
for next in decision_regions {
|
|
if let Some(prev) = merged.last_mut() {
|
|
let prev_zero = prev.selected_line_range.start == prev.selected_line_range.end;
|
|
let next_zero = next.selected_line_range.start == next.selected_line_range.end;
|
|
let can_merge = if prev_zero || next_zero {
|
|
prev_zero
|
|
&& next_zero
|
|
&& next.selected_line_range.start
|
|
<= prev.selected_line_range.end.saturating_add(MERGE_GAP_LINES)
|
|
} else {
|
|
// Keep ranges with insertion/deletion-only rows separate so
|
|
// structural additions (e.g. trailing inserted methods) don't
|
|
// collapse into preceding modification chunks.
|
|
!prev.has_non_emitting_rows
|
|
&& !next.has_non_emitting_rows
|
|
&& next.selected_line_range.start
|
|
<= prev.selected_line_range.end.saturating_add(MERGE_GAP_LINES)
|
|
};
|
|
if can_merge {
|
|
prev.row_range.end = next.row_range.end;
|
|
prev.selected_line_range.end = prev
|
|
.selected_line_range
|
|
.end
|
|
.max(next.selected_line_range.end);
|
|
prev.alternate_line_range.end = prev
|
|
.alternate_line_range
|
|
.end
|
|
.max(next.alternate_line_range.end);
|
|
prev.has_non_emitting_rows |= next.has_non_emitting_rows;
|
|
continue;
|
|
}
|
|
}
|
|
merged.push(next);
|
|
}
|
|
|
|
Some(merged)
|
|
}
|
|
|
|
pub(super) fn unresolved_decision_ranges_for_block(
|
|
block: &conflict_resolver::ConflictBlock,
|
|
) -> Option<Vec<Range<usize>>> {
|
|
unresolved_decision_regions_for_block(block).map(|regions| {
|
|
regions
|
|
.into_iter()
|
|
.map(|region| region.selected_line_range)
|
|
.collect()
|
|
})
|
|
}
|
|
|
|
pub(super) fn build_resolved_output_conflict_markers(
|
|
marker_segments: &[conflict_resolver::ConflictSegment],
|
|
output_text: &str,
|
|
output_line_count: usize,
|
|
) -> Vec<Option<ResolvedOutputConflictMarker>> {
|
|
let mut markers = vec![None; output_line_count];
|
|
if output_line_count == 0 {
|
|
return markers;
|
|
}
|
|
|
|
let Some(block_ranges) =
|
|
resolved_output_conflict_block_ranges_in_text(marker_segments, output_text)
|
|
else {
|
|
return markers;
|
|
};
|
|
|
|
for (conflict_ix, (block, range)) in marker_segments
|
|
.iter()
|
|
.filter_map(|seg| match seg {
|
|
conflict_resolver::ConflictSegment::Block(block) => Some(block),
|
|
_ => None,
|
|
})
|
|
.zip(block_ranges.into_iter())
|
|
.enumerate()
|
|
{
|
|
let marker_ranges = conflict_marker_ranges_for_block(block, range);
|
|
write_conflict_markers_for_ranges(
|
|
&mut markers,
|
|
conflict_ix,
|
|
!block.resolved,
|
|
marker_ranges.as_slice(),
|
|
);
|
|
}
|
|
|
|
markers
|
|
}
|
|
|
|
pub(super) fn push_conflict_text_segment(
|
|
segments: &mut Vec<conflict_resolver::ConflictSegment>,
|
|
text: String,
|
|
) {
|
|
if text.is_empty() {
|
|
return;
|
|
}
|
|
if let Some(conflict_resolver::ConflictSegment::Text(prev)) = segments.last_mut() {
|
|
prev.push_str(&text);
|
|
return;
|
|
}
|
|
segments.push(conflict_resolver::ConflictSegment::Text(text));
|
|
}
|
|
|
|
pub(super) fn resolved_output_markers_for_text(
|
|
marker_segments: &[conflict_resolver::ConflictSegment],
|
|
output_text: &str,
|
|
) -> Vec<Option<ResolvedOutputConflictMarker>> {
|
|
let output_line_count = conflict_resolver::split_output_lines_for_outline(output_text).len();
|
|
build_resolved_output_conflict_markers(marker_segments, output_text, output_line_count)
|
|
}
|
|
|
|
pub(super) fn resolved_output_marker_for_line(
|
|
marker_segments: &[conflict_resolver::ConflictSegment],
|
|
output_text: &str,
|
|
output_line_ix: usize,
|
|
) -> Option<ResolvedOutputConflictMarker> {
|
|
resolved_output_markers_for_text(marker_segments, output_text)
|
|
.get(output_line_ix)
|
|
.copied()
|
|
.flatten()
|
|
}
|
|
|
|
pub(super) fn first_output_marker_line_for_conflict(
|
|
markers: &[Option<ResolvedOutputConflictMarker>],
|
|
conflict_ix: usize,
|
|
) -> Option<usize> {
|
|
markers.iter().enumerate().find_map(|(line_ix, marker)| {
|
|
marker
|
|
.as_ref()
|
|
.and_then(|m| (m.conflict_ix == conflict_ix && m.is_start).then_some(line_ix))
|
|
})
|
|
}
|
|
|
|
pub(super) fn conflict_marker_nav_entries_from_markers(
|
|
markers: &[Option<ResolvedOutputConflictMarker>],
|
|
) -> Vec<usize> {
|
|
let mut seen_conflicts = std::collections::HashSet::new();
|
|
markers
|
|
.iter()
|
|
.enumerate()
|
|
.filter_map(|(line_ix, marker)| {
|
|
marker.as_ref().and_then(|m| {
|
|
(m.is_start && seen_conflicts.insert(m.conflict_ix)).then_some(line_ix)
|
|
})
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
pub(super) fn line_index_for_offset(content: &str, offset: usize) -> usize {
|
|
content[..offset.min(content.len())].matches('\n').count()
|
|
}
|
|
|
|
pub(super) fn conflict_resolver_output_context_line(
|
|
content: &str,
|
|
cursor_offset: usize,
|
|
clicked_offset: Option<usize>,
|
|
) -> usize {
|
|
clicked_offset
|
|
.map(|offset| line_index_for_offset(content, offset))
|
|
.unwrap_or_else(|| line_index_for_offset(content, cursor_offset))
|
|
}
|
|
|
|
pub(super) fn slice_text_by_line_range(text: &str, line_range: Range<usize>) -> String {
|
|
if line_range.start >= line_range.end || text.is_empty() {
|
|
return String::new();
|
|
}
|
|
|
|
let mut line_starts = Vec::with_capacity(count_newlines(text).saturating_add(2));
|
|
line_starts.push(0usize);
|
|
for (ix, byte) in text.as_bytes().iter().enumerate() {
|
|
if *byte == b'\n' {
|
|
line_starts.push(ix.saturating_add(1));
|
|
}
|
|
}
|
|
|
|
let start_byte = line_starts
|
|
.get(line_range.start)
|
|
.copied()
|
|
.unwrap_or(text.len());
|
|
let end_byte = line_starts
|
|
.get(line_range.end)
|
|
.copied()
|
|
.unwrap_or(text.len());
|
|
if start_byte >= end_byte || start_byte >= text.len() {
|
|
return String::new();
|
|
}
|
|
text[start_byte..end_byte.min(text.len())].to_string()
|
|
}
|
|
|
|
pub(super) fn split_target_conflict_block_into_subchunks(
|
|
marker_segments: &mut Vec<conflict_resolver::ConflictSegment>,
|
|
conflict_region_indices: &mut Vec<usize>,
|
|
target_conflict_ix: usize,
|
|
) -> bool {
|
|
use gitcomet_core::conflict_session::{Subchunk, split_conflict_into_subchunks};
|
|
|
|
let Some(target_block) = marker_segments
|
|
.iter()
|
|
.filter_map(|seg| match seg {
|
|
conflict_resolver::ConflictSegment::Block(block) => Some(block),
|
|
_ => None,
|
|
})
|
|
.nth(target_conflict_ix)
|
|
.cloned()
|
|
else {
|
|
return false;
|
|
};
|
|
if target_block.resolved {
|
|
return false;
|
|
}
|
|
|
|
enum SplitMode {
|
|
Subchunks(Vec<Subchunk>),
|
|
DecisionRanges(Vec<UnresolvedDecisionRegion>),
|
|
}
|
|
let split_mode = if let Some(base) = target_block.base.as_deref() {
|
|
split_conflict_into_subchunks(base, &target_block.ours, &target_block.theirs).and_then(
|
|
|subchunks| {
|
|
let split_conflict_count = subchunks
|
|
.iter()
|
|
.filter(|subchunk| matches!(subchunk, Subchunk::Conflict { .. }))
|
|
.count();
|
|
(split_conflict_count > 1).then_some(SplitMode::Subchunks(subchunks))
|
|
},
|
|
)
|
|
} else {
|
|
None
|
|
}
|
|
.or_else(|| {
|
|
unresolved_decision_regions_for_block(&target_block)
|
|
.and_then(|regions| (regions.len() > 1).then_some(SplitMode::DecisionRanges(regions)))
|
|
});
|
|
let Some(split_mode) = split_mode else {
|
|
return false;
|
|
};
|
|
|
|
let mut next_segments = Vec::with_capacity(marker_segments.len().saturating_add(4));
|
|
let mut next_region_indices =
|
|
Vec::with_capacity(conflict_region_indices.len().saturating_add(4));
|
|
let mut seen_conflict_ix = 0usize;
|
|
for seg in marker_segments.drain(..) {
|
|
match seg {
|
|
conflict_resolver::ConflictSegment::Block(block) => {
|
|
let region_ix = conflict_region_indices
|
|
.get(seen_conflict_ix)
|
|
.copied()
|
|
.unwrap_or(seen_conflict_ix);
|
|
if seen_conflict_ix == target_conflict_ix {
|
|
match &split_mode {
|
|
SplitMode::Subchunks(subchunks) => {
|
|
for subchunk in subchunks {
|
|
match subchunk {
|
|
Subchunk::Resolved(text) => {
|
|
push_conflict_text_segment(
|
|
&mut next_segments,
|
|
text.clone(),
|
|
);
|
|
}
|
|
Subchunk::Conflict { base, ours, theirs } => {
|
|
next_segments.push(
|
|
conflict_resolver::ConflictSegment::Block(
|
|
conflict_resolver::ConflictBlock {
|
|
base: Some(base.clone()),
|
|
ours: ours.clone(),
|
|
theirs: theirs.clone(),
|
|
choice: target_block.choice,
|
|
resolved: false,
|
|
},
|
|
),
|
|
);
|
|
next_region_indices.push(region_ix);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
SplitMode::DecisionRanges(regions) => {
|
|
let (selected_text, alternate_text, choice_is_ours) =
|
|
match target_block.choice {
|
|
conflict_resolver::ConflictChoice::Ours => {
|
|
(&target_block.ours, &target_block.theirs, true)
|
|
}
|
|
conflict_resolver::ConflictChoice::Theirs => {
|
|
(&target_block.theirs, &target_block.ours, false)
|
|
}
|
|
_ => {
|
|
return false;
|
|
}
|
|
};
|
|
let selected_total_lines = source_line_count(selected_text);
|
|
let mut selected_cursor = 0usize;
|
|
for region in regions {
|
|
let prefix = slice_text_by_line_range(
|
|
selected_text,
|
|
selected_cursor..region.selected_line_range.start,
|
|
);
|
|
push_conflict_text_segment(&mut next_segments, prefix);
|
|
|
|
let selected_fragment = slice_text_by_line_range(
|
|
selected_text,
|
|
region.selected_line_range.clone(),
|
|
);
|
|
let alternate_fragment = slice_text_by_line_range(
|
|
alternate_text,
|
|
region.alternate_line_range.clone(),
|
|
);
|
|
let (ours, theirs) = if choice_is_ours {
|
|
(selected_fragment, alternate_fragment)
|
|
} else {
|
|
(alternate_fragment, selected_fragment)
|
|
};
|
|
next_segments.push(conflict_resolver::ConflictSegment::Block(
|
|
conflict_resolver::ConflictBlock {
|
|
base: None,
|
|
ours,
|
|
theirs,
|
|
choice: target_block.choice,
|
|
resolved: false,
|
|
},
|
|
));
|
|
next_region_indices.push(region_ix);
|
|
selected_cursor = region.selected_line_range.end;
|
|
}
|
|
let suffix = slice_text_by_line_range(
|
|
selected_text,
|
|
selected_cursor..selected_total_lines,
|
|
);
|
|
push_conflict_text_segment(&mut next_segments, suffix);
|
|
}
|
|
}
|
|
} else {
|
|
next_segments.push(conflict_resolver::ConflictSegment::Block(block));
|
|
next_region_indices.push(region_ix);
|
|
}
|
|
seen_conflict_ix = seen_conflict_ix.saturating_add(1);
|
|
}
|
|
conflict_resolver::ConflictSegment::Text(text) => {
|
|
push_conflict_text_segment(&mut next_segments, text);
|
|
}
|
|
}
|
|
}
|
|
|
|
*marker_segments = next_segments;
|
|
*conflict_region_indices = next_region_indices;
|
|
true
|
|
}
|
|
|
|
pub(super) fn conflict_region_index_is_unique(
|
|
conflict_region_indices: &[usize],
|
|
region_ix: usize,
|
|
) -> bool {
|
|
conflict_region_indices
|
|
.iter()
|
|
.filter(|&&ix| ix == region_ix)
|
|
.take(2)
|
|
.count()
|
|
<= 1
|
|
}
|
|
|
|
pub(super) fn conflict_block_matches_group(
|
|
block: &conflict_resolver::ConflictBlock,
|
|
region_ix: usize,
|
|
target_block: &conflict_resolver::ConflictBlock,
|
|
target_region_ix: usize,
|
|
) -> bool {
|
|
region_ix == target_region_ix
|
|
&& block.base == target_block.base
|
|
&& block.ours == target_block.ours
|
|
&& block.theirs == target_block.theirs
|
|
}
|
|
|
|
pub(super) fn conflict_group_member_indices_for_ix(
|
|
marker_segments: &[conflict_resolver::ConflictSegment],
|
|
conflict_region_indices: &[usize],
|
|
conflict_ix: usize,
|
|
) -> Vec<usize> {
|
|
let mut blocks: Vec<&conflict_resolver::ConflictBlock> = Vec::new();
|
|
// True when a block has non-empty text between it and the previous block.
|
|
let mut separated_before: Vec<bool> = Vec::new();
|
|
let mut saw_text_since_prev_block = false;
|
|
for seg in marker_segments {
|
|
match seg {
|
|
conflict_resolver::ConflictSegment::Text(text) => {
|
|
if !text.is_empty() {
|
|
saw_text_since_prev_block = true;
|
|
}
|
|
}
|
|
conflict_resolver::ConflictSegment::Block(block) => {
|
|
separated_before.push(saw_text_since_prev_block);
|
|
blocks.push(block);
|
|
saw_text_since_prev_block = false;
|
|
}
|
|
}
|
|
}
|
|
let Some(target_block) = blocks.get(conflict_ix).copied() else {
|
|
return Vec::new();
|
|
};
|
|
let target_region_ix = conflict_region_indices
|
|
.get(conflict_ix)
|
|
.copied()
|
|
.unwrap_or(conflict_ix);
|
|
|
|
let mut start = conflict_ix;
|
|
while start > 0 {
|
|
if separated_before[start] {
|
|
break;
|
|
}
|
|
let prev_ix = start - 1;
|
|
let prev_block = blocks[prev_ix];
|
|
let prev_region_ix = conflict_region_indices
|
|
.get(prev_ix)
|
|
.copied()
|
|
.unwrap_or(prev_ix);
|
|
if conflict_block_matches_group(prev_block, prev_region_ix, target_block, target_region_ix)
|
|
{
|
|
start = prev_ix;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
let mut end_exclusive = conflict_ix + 1;
|
|
while end_exclusive < blocks.len() {
|
|
let next_ix = end_exclusive;
|
|
if separated_before[next_ix] {
|
|
break;
|
|
}
|
|
let next_block = blocks[next_ix];
|
|
let next_region_ix = conflict_region_indices
|
|
.get(next_ix)
|
|
.copied()
|
|
.unwrap_or(next_ix);
|
|
if conflict_block_matches_group(next_block, next_region_ix, target_block, target_region_ix)
|
|
{
|
|
end_exclusive += 1;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
(start..end_exclusive).collect()
|
|
}
|
|
|
|
pub(super) fn conflict_group_selected_choices_for_ix(
|
|
marker_segments: &[conflict_resolver::ConflictSegment],
|
|
conflict_region_indices: &[usize],
|
|
conflict_ix: usize,
|
|
) -> Vec<conflict_resolver::ConflictChoice> {
|
|
let group_indices =
|
|
conflict_group_member_indices_for_ix(marker_segments, conflict_region_indices, conflict_ix);
|
|
if group_indices.is_empty() {
|
|
return Vec::new();
|
|
}
|
|
let blocks: Vec<&conflict_resolver::ConflictBlock> = marker_segments
|
|
.iter()
|
|
.filter_map(|seg| match seg {
|
|
conflict_resolver::ConflictSegment::Block(block) => Some(block),
|
|
_ => None,
|
|
})
|
|
.collect();
|
|
|
|
let mut has_base = false;
|
|
let mut has_ours = false;
|
|
let mut has_theirs = false;
|
|
for ix in group_indices {
|
|
let Some(block) = blocks.get(ix).copied() else {
|
|
continue;
|
|
};
|
|
if !block.resolved {
|
|
continue;
|
|
}
|
|
match block.choice {
|
|
conflict_resolver::ConflictChoice::Base => has_base = true,
|
|
conflict_resolver::ConflictChoice::Ours => has_ours = true,
|
|
conflict_resolver::ConflictChoice::Theirs => has_theirs = true,
|
|
conflict_resolver::ConflictChoice::Both => {
|
|
has_ours = true;
|
|
has_theirs = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
let mut selected = Vec::with_capacity(3);
|
|
if has_base {
|
|
selected.push(conflict_resolver::ConflictChoice::Base);
|
|
}
|
|
if has_ours {
|
|
selected.push(conflict_resolver::ConflictChoice::Ours);
|
|
}
|
|
if has_theirs {
|
|
selected.push(conflict_resolver::ConflictChoice::Theirs);
|
|
}
|
|
selected
|
|
}
|
|
|
|
pub(super) fn conflict_group_indices_for_choice(
|
|
marker_segments: &[conflict_resolver::ConflictSegment],
|
|
conflict_region_indices: &[usize],
|
|
conflict_ix: usize,
|
|
choice: conflict_resolver::ConflictChoice,
|
|
) -> Vec<usize> {
|
|
let group_indices =
|
|
conflict_group_member_indices_for_ix(marker_segments, conflict_region_indices, conflict_ix);
|
|
if group_indices.is_empty() {
|
|
return Vec::new();
|
|
}
|
|
let blocks: Vec<&conflict_resolver::ConflictBlock> = marker_segments
|
|
.iter()
|
|
.filter_map(|seg| match seg {
|
|
conflict_resolver::ConflictSegment::Block(block) => Some(block),
|
|
_ => None,
|
|
})
|
|
.collect();
|
|
|
|
group_indices
|
|
.into_iter()
|
|
.filter(|&ix| {
|
|
let Some(block) = blocks.get(ix).copied() else {
|
|
return false;
|
|
};
|
|
if !block.resolved {
|
|
return false;
|
|
}
|
|
match choice {
|
|
conflict_resolver::ConflictChoice::Base => {
|
|
block.choice == conflict_resolver::ConflictChoice::Base
|
|
}
|
|
conflict_resolver::ConflictChoice::Ours => {
|
|
matches!(
|
|
block.choice,
|
|
conflict_resolver::ConflictChoice::Ours
|
|
| conflict_resolver::ConflictChoice::Both
|
|
)
|
|
}
|
|
conflict_resolver::ConflictChoice::Theirs => {
|
|
matches!(
|
|
block.choice,
|
|
conflict_resolver::ConflictChoice::Theirs
|
|
| conflict_resolver::ConflictChoice::Both
|
|
)
|
|
}
|
|
conflict_resolver::ConflictChoice::Both => {
|
|
block.choice == conflict_resolver::ConflictChoice::Both
|
|
}
|
|
}
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
pub(super) fn should_remove_conflict_block_on_reset(
|
|
marker_segments: &[conflict_resolver::ConflictSegment],
|
|
conflict_region_indices: &[usize],
|
|
conflict_ix: usize,
|
|
) -> bool {
|
|
let group_indices =
|
|
conflict_group_member_indices_for_ix(marker_segments, conflict_region_indices, conflict_ix);
|
|
group_indices.len() > 1
|
|
}
|
|
|
|
pub(super) fn remove_conflict_block_at(
|
|
marker_segments: &mut Vec<conflict_resolver::ConflictSegment>,
|
|
conflict_region_indices: &mut Vec<usize>,
|
|
conflict_ix: usize,
|
|
) -> bool {
|
|
let mut next_segments = Vec::with_capacity(marker_segments.len());
|
|
let mut seen_conflict_ix = 0usize;
|
|
let mut removed = false;
|
|
for seg in marker_segments.drain(..) {
|
|
match seg {
|
|
conflict_resolver::ConflictSegment::Block(block) => {
|
|
if seen_conflict_ix == conflict_ix {
|
|
removed = true;
|
|
} else {
|
|
next_segments.push(conflict_resolver::ConflictSegment::Block(block));
|
|
}
|
|
seen_conflict_ix = seen_conflict_ix.saturating_add(1);
|
|
}
|
|
conflict_resolver::ConflictSegment::Text(text) => {
|
|
push_conflict_text_segment(&mut next_segments, text);
|
|
}
|
|
}
|
|
}
|
|
*marker_segments = next_segments;
|
|
if removed && conflict_ix < conflict_region_indices.len() {
|
|
conflict_region_indices.remove(conflict_ix);
|
|
}
|
|
removed
|
|
}
|
|
|
|
pub(super) fn reset_conflict_block_selection(
|
|
marker_segments: &mut Vec<conflict_resolver::ConflictSegment>,
|
|
conflict_region_indices: &mut Vec<usize>,
|
|
conflict_ix: usize,
|
|
) -> bool {
|
|
if should_remove_conflict_block_on_reset(marker_segments, conflict_region_indices, conflict_ix)
|
|
{
|
|
return remove_conflict_block_at(marker_segments, conflict_region_indices, conflict_ix);
|
|
}
|
|
|
|
let mut seen_conflict_ix = 0usize;
|
|
for seg in marker_segments.iter_mut() {
|
|
let conflict_resolver::ConflictSegment::Block(block) = seg else {
|
|
continue;
|
|
};
|
|
if seen_conflict_ix == conflict_ix {
|
|
if !block.resolved {
|
|
return false;
|
|
}
|
|
block.resolved = false;
|
|
// Unpicked state should return to the default local-side choice.
|
|
block.choice = conflict_resolver::ConflictChoice::Ours;
|
|
return true;
|
|
}
|
|
seen_conflict_ix = seen_conflict_ix.saturating_add(1);
|
|
}
|
|
false
|
|
}
|
|
|
|
pub(super) fn append_choice_after_conflict_block(
|
|
marker_segments: &mut Vec<conflict_resolver::ConflictSegment>,
|
|
conflict_region_indices: &mut Vec<usize>,
|
|
conflict_ix: usize,
|
|
choice: conflict_resolver::ConflictChoice,
|
|
) -> Option<usize> {
|
|
let target_block = marker_segments
|
|
.iter()
|
|
.filter_map(|seg| match seg {
|
|
conflict_resolver::ConflictSegment::Block(block) => Some(block),
|
|
_ => None,
|
|
})
|
|
.nth(conflict_ix)?
|
|
.clone();
|
|
let group_indices =
|
|
conflict_group_member_indices_for_ix(marker_segments, conflict_region_indices, conflict_ix);
|
|
let &group_end_ix = group_indices.last()?;
|
|
let target_region_ix = conflict_region_indices
|
|
.get(conflict_ix)
|
|
.copied()
|
|
.unwrap_or(conflict_ix);
|
|
if !target_block.resolved {
|
|
return None;
|
|
}
|
|
if matches!(choice, conflict_resolver::ConflictChoice::Base) && target_block.base.is_none() {
|
|
return None;
|
|
}
|
|
if conflict_group_selected_choices_for_ix(marker_segments, conflict_region_indices, conflict_ix)
|
|
.contains(&choice)
|
|
{
|
|
return None;
|
|
}
|
|
|
|
let mut next_segments = Vec::with_capacity(marker_segments.len().saturating_add(1));
|
|
let mut next_region_indices =
|
|
Vec::with_capacity(conflict_region_indices.len().saturating_add(1));
|
|
let mut seen_conflict_ix = 0usize;
|
|
let mut next_conflict_ix = 0usize;
|
|
let mut inserted_conflict_ix = None;
|
|
|
|
let push_appended = |next_segments: &mut Vec<conflict_resolver::ConflictSegment>,
|
|
next_region_indices: &mut Vec<usize>,
|
|
next_conflict_ix: &mut usize,
|
|
inserted_conflict_ix: &mut Option<usize>| {
|
|
if inserted_conflict_ix.is_some() {
|
|
return;
|
|
}
|
|
let mut appended = target_block.clone();
|
|
appended.choice = choice;
|
|
appended.resolved = true;
|
|
next_segments.push(conflict_resolver::ConflictSegment::Block(appended));
|
|
next_region_indices.push(target_region_ix);
|
|
*inserted_conflict_ix = Some(*next_conflict_ix);
|
|
*next_conflict_ix = next_conflict_ix.saturating_add(1);
|
|
};
|
|
|
|
for seg in marker_segments.drain(..) {
|
|
if seen_conflict_ix == group_end_ix.saturating_add(1) {
|
|
push_appended(
|
|
&mut next_segments,
|
|
&mut next_region_indices,
|
|
&mut next_conflict_ix,
|
|
&mut inserted_conflict_ix,
|
|
);
|
|
}
|
|
match seg {
|
|
conflict_resolver::ConflictSegment::Block(block) => {
|
|
let region_ix = conflict_region_indices
|
|
.get(seen_conflict_ix)
|
|
.copied()
|
|
.unwrap_or(seen_conflict_ix);
|
|
next_segments.push(conflict_resolver::ConflictSegment::Block(block));
|
|
next_region_indices.push(region_ix);
|
|
next_conflict_ix = next_conflict_ix.saturating_add(1);
|
|
seen_conflict_ix = seen_conflict_ix.saturating_add(1);
|
|
}
|
|
conflict_resolver::ConflictSegment::Text(text) => {
|
|
push_conflict_text_segment(&mut next_segments, text);
|
|
}
|
|
}
|
|
}
|
|
push_appended(
|
|
&mut next_segments,
|
|
&mut next_region_indices,
|
|
&mut next_conflict_ix,
|
|
&mut inserted_conflict_ix,
|
|
);
|
|
|
|
*marker_segments = next_segments;
|
|
*conflict_region_indices = next_region_indices;
|
|
inserted_conflict_ix
|
|
}
|
|
|
|
pub(super) fn scroll_conflict_resolved_output_to_line(
|
|
scroll_handle: &UniformListScrollHandle,
|
|
target_line_ix: usize,
|
|
line_count: usize,
|
|
) {
|
|
if line_count == 0 {
|
|
return;
|
|
}
|
|
|
|
let base_handle = scroll_handle.0.borrow().base_handle.clone();
|
|
let viewport_h = base_handle.bounds().size.height.max(px(0.0));
|
|
if viewport_h <= px(0.0) {
|
|
return;
|
|
}
|
|
|
|
let line_h = px(CONFLICT_RESOLVED_OUTPUT_ROW_HEIGHT_PX);
|
|
let total_h = line_h * line_count as f32;
|
|
let max_scroll = (total_h - viewport_h).max(px(0.0));
|
|
let target_line = target_line_ix.min(line_count.saturating_sub(1));
|
|
let target_center = line_h * target_line as f32 + line_h * 0.5;
|
|
let target_scroll_top = (target_center - viewport_h * 0.5)
|
|
.max(px(0.0))
|
|
.min(max_scroll);
|
|
let current = base_handle.offset();
|
|
base_handle.set_offset(point(current.x, -target_scroll_top));
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
pub(super) fn apply_three_way_empty_base_provenance_hints(
|
|
meta: &mut [conflict_resolver::ResolvedLineMeta],
|
|
marker_segments: &[conflict_resolver::ConflictSegment],
|
|
output_text: &str,
|
|
) {
|
|
let generated = conflict_resolver::generate_resolved_text(marker_segments);
|
|
if generated != output_text || meta.is_empty() {
|
|
return;
|
|
}
|
|
|
|
let mut block_ix = 0usize;
|
|
let mut a_line = 1u32;
|
|
let mut b_line = 1u32;
|
|
let mut c_line = 1u32;
|
|
|
|
for seg in marker_segments {
|
|
match seg {
|
|
conflict_resolver::ConflictSegment::Text(text) => {
|
|
let n = u32::try_from(source_line_count(text)).unwrap_or(0);
|
|
a_line = a_line.saturating_add(n);
|
|
b_line = b_line.saturating_add(n);
|
|
c_line = c_line.saturating_add(n);
|
|
}
|
|
conflict_resolver::ConflictSegment::Block(block) => {
|
|
let a_count =
|
|
u32::try_from(source_line_count(block.base.as_deref().unwrap_or_default()))
|
|
.unwrap_or(0);
|
|
let b_count = u32::try_from(source_line_count(&block.ours)).unwrap_or(0);
|
|
let c_count = u32::try_from(source_line_count(&block.theirs)).unwrap_or(0);
|
|
|
|
let base_empty = block.base.as_ref().is_none_or(|s| s.is_empty());
|
|
if base_empty
|
|
&& let Some(range) = output_line_range_for_conflict_block_in_text(
|
|
marker_segments,
|
|
output_text,
|
|
block_ix,
|
|
)
|
|
{
|
|
match block.choice {
|
|
conflict_resolver::ConflictChoice::Base => {}
|
|
conflict_resolver::ConflictChoice::Ours => {
|
|
let take = usize::min(
|
|
range.end.saturating_sub(range.start),
|
|
usize::try_from(b_count).unwrap_or(0),
|
|
);
|
|
for off in 0..take {
|
|
if let Some(m) = meta.get_mut(range.start + off)
|
|
&& matches!(
|
|
m.source,
|
|
conflict_resolver::ResolvedLineSource::A
|
|
| conflict_resolver::ResolvedLineSource::Manual
|
|
)
|
|
{
|
|
m.source = conflict_resolver::ResolvedLineSource::B;
|
|
m.input_line = Some(
|
|
b_line.saturating_add(u32::try_from(off).unwrap_or(0)),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
conflict_resolver::ConflictChoice::Theirs => {
|
|
let take = usize::min(
|
|
range.end.saturating_sub(range.start),
|
|
usize::try_from(c_count).unwrap_or(0),
|
|
);
|
|
for off in 0..take {
|
|
if let Some(m) = meta.get_mut(range.start + off)
|
|
&& matches!(
|
|
m.source,
|
|
conflict_resolver::ResolvedLineSource::A
|
|
| conflict_resolver::ResolvedLineSource::Manual
|
|
)
|
|
{
|
|
m.source = conflict_resolver::ResolvedLineSource::C;
|
|
m.input_line = Some(
|
|
c_line.saturating_add(u32::try_from(off).unwrap_or(0)),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
conflict_resolver::ConflictChoice::Both => {
|
|
let total = range.end.saturating_sub(range.start);
|
|
let ours_take =
|
|
usize::min(total, usize::try_from(b_count).unwrap_or(0));
|
|
for off in 0..ours_take {
|
|
if let Some(m) = meta.get_mut(range.start + off)
|
|
&& matches!(
|
|
m.source,
|
|
conflict_resolver::ResolvedLineSource::A
|
|
| conflict_resolver::ResolvedLineSource::Manual
|
|
)
|
|
{
|
|
m.source = conflict_resolver::ResolvedLineSource::B;
|
|
m.input_line = Some(
|
|
b_line.saturating_add(u32::try_from(off).unwrap_or(0)),
|
|
);
|
|
}
|
|
}
|
|
|
|
let theirs_take = total.saturating_sub(ours_take);
|
|
for off in 0..theirs_take {
|
|
if let Some(m) = meta.get_mut(range.start + ours_take + off)
|
|
&& matches!(
|
|
m.source,
|
|
conflict_resolver::ResolvedLineSource::A
|
|
| conflict_resolver::ResolvedLineSource::Manual
|
|
)
|
|
{
|
|
m.source = conflict_resolver::ResolvedLineSource::C;
|
|
m.input_line = Some(
|
|
c_line.saturating_add(u32::try_from(off).unwrap_or(0)),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
a_line = a_line.saturating_add(a_count);
|
|
b_line = b_line.saturating_add(b_count);
|
|
c_line = c_line.saturating_add(c_count);
|
|
block_ix = block_ix.saturating_add(1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(super) fn apply_conflict_choice_provenance_hints(
|
|
meta: &mut [conflict_resolver::ResolvedLineMeta],
|
|
marker_segments: &[conflict_resolver::ConflictSegment],
|
|
output_text: &str,
|
|
view_mode: ConflictResolverViewMode,
|
|
) {
|
|
let generated = conflict_resolver::generate_resolved_text(marker_segments);
|
|
if generated != output_text || meta.is_empty() {
|
|
return;
|
|
}
|
|
|
|
let assign_range = |meta: &mut [conflict_resolver::ResolvedLineMeta],
|
|
range: Range<usize>,
|
|
source: conflict_resolver::ResolvedLineSource,
|
|
start_line: u32,
|
|
line_count: u32| {
|
|
let len = range.end.saturating_sub(range.start);
|
|
for off in 0..len {
|
|
if let Some(m) = meta.get_mut(range.start + off) {
|
|
m.source = source;
|
|
let off_u32 = u32::try_from(off).unwrap_or(u32::MAX);
|
|
m.input_line = (off_u32 < line_count).then_some(start_line.saturating_add(off_u32));
|
|
}
|
|
}
|
|
};
|
|
|
|
let assign_both_range = |meta: &mut [conflict_resolver::ResolvedLineMeta],
|
|
range: Range<usize>,
|
|
first_source: conflict_resolver::ResolvedLineSource,
|
|
first_start: u32,
|
|
first_count: u32,
|
|
second_source: conflict_resolver::ResolvedLineSource,
|
|
second_start: u32,
|
|
second_count: u32| {
|
|
let len = range.end.saturating_sub(range.start);
|
|
let first_count_usize = usize::try_from(first_count).unwrap_or(0);
|
|
let first_take = len.min(first_count_usize);
|
|
assign_range(
|
|
meta,
|
|
range.start..range.start.saturating_add(first_take),
|
|
first_source,
|
|
first_start,
|
|
first_count,
|
|
);
|
|
assign_range(
|
|
meta,
|
|
range.start.saturating_add(first_take)..range.end,
|
|
second_source,
|
|
second_start,
|
|
second_count,
|
|
);
|
|
};
|
|
|
|
let mut block_ix = 0usize;
|
|
let mut a_line = 1u32;
|
|
let mut b_line = 1u32;
|
|
let mut c_line = 1u32;
|
|
|
|
for seg in marker_segments {
|
|
match seg {
|
|
conflict_resolver::ConflictSegment::Text(text) => {
|
|
let n = u32::try_from(source_line_count(text)).unwrap_or(0);
|
|
a_line = a_line.saturating_add(n);
|
|
b_line = b_line.saturating_add(n);
|
|
if view_mode == ConflictResolverViewMode::ThreeWay {
|
|
c_line = c_line.saturating_add(n);
|
|
}
|
|
}
|
|
conflict_resolver::ConflictSegment::Block(block) => {
|
|
let (a_count, b_count, c_count) = match view_mode {
|
|
ConflictResolverViewMode::ThreeWay => (
|
|
u32::try_from(source_line_count(block.base.as_deref().unwrap_or_default()))
|
|
.unwrap_or(0),
|
|
u32::try_from(source_line_count(&block.ours)).unwrap_or(0),
|
|
u32::try_from(source_line_count(&block.theirs)).unwrap_or(0),
|
|
),
|
|
ConflictResolverViewMode::TwoWayDiff => (
|
|
u32::try_from(source_line_count(&block.ours)).unwrap_or(0),
|
|
u32::try_from(source_line_count(&block.theirs)).unwrap_or(0),
|
|
0,
|
|
),
|
|
};
|
|
|
|
if let Some(range) = output_line_range_for_conflict_block_in_text(
|
|
marker_segments,
|
|
output_text,
|
|
block_ix,
|
|
) {
|
|
match (view_mode, block.choice) {
|
|
(
|
|
ConflictResolverViewMode::ThreeWay,
|
|
conflict_resolver::ConflictChoice::Base,
|
|
) => {
|
|
assign_range(
|
|
meta,
|
|
range,
|
|
conflict_resolver::ResolvedLineSource::A,
|
|
a_line,
|
|
a_count,
|
|
);
|
|
}
|
|
(
|
|
ConflictResolverViewMode::ThreeWay,
|
|
conflict_resolver::ConflictChoice::Ours,
|
|
) => {
|
|
assign_range(
|
|
meta,
|
|
range,
|
|
conflict_resolver::ResolvedLineSource::B,
|
|
b_line,
|
|
b_count,
|
|
);
|
|
}
|
|
(
|
|
ConflictResolverViewMode::ThreeWay,
|
|
conflict_resolver::ConflictChoice::Theirs,
|
|
) => {
|
|
assign_range(
|
|
meta,
|
|
range,
|
|
conflict_resolver::ResolvedLineSource::C,
|
|
c_line,
|
|
c_count,
|
|
);
|
|
}
|
|
(
|
|
ConflictResolverViewMode::ThreeWay,
|
|
conflict_resolver::ConflictChoice::Both,
|
|
) => {
|
|
assign_both_range(
|
|
meta,
|
|
range,
|
|
conflict_resolver::ResolvedLineSource::B,
|
|
b_line,
|
|
b_count,
|
|
conflict_resolver::ResolvedLineSource::C,
|
|
c_line,
|
|
c_count,
|
|
);
|
|
}
|
|
(
|
|
ConflictResolverViewMode::TwoWayDiff,
|
|
conflict_resolver::ConflictChoice::Theirs,
|
|
) => {
|
|
assign_range(
|
|
meta,
|
|
range,
|
|
conflict_resolver::ResolvedLineSource::B,
|
|
b_line,
|
|
b_count,
|
|
);
|
|
}
|
|
(
|
|
ConflictResolverViewMode::TwoWayDiff,
|
|
conflict_resolver::ConflictChoice::Both,
|
|
) => {
|
|
assign_both_range(
|
|
meta,
|
|
range,
|
|
conflict_resolver::ResolvedLineSource::A,
|
|
a_line,
|
|
a_count,
|
|
conflict_resolver::ResolvedLineSource::B,
|
|
b_line,
|
|
b_count,
|
|
);
|
|
}
|
|
// In two-way mode, Base falls back to local-side semantics.
|
|
(
|
|
ConflictResolverViewMode::TwoWayDiff,
|
|
conflict_resolver::ConflictChoice::Base,
|
|
)
|
|
| (
|
|
ConflictResolverViewMode::TwoWayDiff,
|
|
conflict_resolver::ConflictChoice::Ours,
|
|
) => {
|
|
assign_range(
|
|
meta,
|
|
range,
|
|
conflict_resolver::ResolvedLineSource::A,
|
|
a_line,
|
|
a_count,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
a_line = a_line.saturating_add(a_count);
|
|
b_line = b_line.saturating_add(b_count);
|
|
c_line = c_line.saturating_add(c_count);
|
|
block_ix = block_ix.saturating_add(1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(super) fn replacement_lines_for_conflict_block(
|
|
block: &conflict_resolver::ConflictBlock,
|
|
choice: conflict_resolver::ConflictChoice,
|
|
) -> Option<Vec<String>> {
|
|
match choice {
|
|
conflict_resolver::ConflictChoice::Base => {
|
|
Some(split_text_lines_owned(block.base.as_deref()?))
|
|
}
|
|
conflict_resolver::ConflictChoice::Ours => Some(split_text_lines_owned(&block.ours)),
|
|
conflict_resolver::ConflictChoice::Theirs => Some(split_text_lines_owned(&block.theirs)),
|
|
conflict_resolver::ConflictChoice::Both => {
|
|
let mut resolved_block = block.clone();
|
|
resolved_block.choice = conflict_resolver::ConflictChoice::Both;
|
|
resolved_block.resolved = true;
|
|
let merged = conflict_resolver::generate_resolved_text(&[
|
|
conflict_resolver::ConflictSegment::Block(resolved_block),
|
|
]);
|
|
Some(split_text_lines_owned(&merged))
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(super) fn replace_output_lines_in_range(
|
|
output: &str,
|
|
range: Range<usize>,
|
|
replacement_lines: &[String],
|
|
) -> String {
|
|
let mut lines: Vec<String> = if output.is_empty() {
|
|
Vec::new()
|
|
} else {
|
|
output.split('\n').map(|line| line.to_string()).collect()
|
|
};
|
|
let start = range.start.min(lines.len());
|
|
let end = range.end.min(lines.len()).max(start);
|
|
lines.splice(start..end, replacement_lines.iter().cloned());
|
|
lines.join("\n")
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub(super) enum ClearDiffSelectionAction {
|
|
ClearSelection,
|
|
ExitFocusedMergetool,
|
|
}
|
|
|
|
pub(super) fn clear_diff_selection_action(view_mode: GitCometViewMode) -> ClearDiffSelectionAction {
|
|
match view_mode {
|
|
GitCometViewMode::Normal => ClearDiffSelectionAction::ClearSelection,
|
|
GitCometViewMode::FocusedMergetool => ClearDiffSelectionAction::ExitFocusedMergetool,
|
|
}
|
|
}
|
|
|
|
pub(super) fn focused_mergetool_save_exit_code(
|
|
total_conflicts: usize,
|
|
resolved_conflicts: usize,
|
|
) -> i32 {
|
|
if total_conflicts == 0 || total_conflicts == resolved_conflicts {
|
|
FOCUSED_MERGETOOL_EXIT_SUCCESS
|
|
} else {
|
|
FOCUSED_MERGETOOL_EXIT_CANCELED
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
|
pub(in crate::view) enum PreparedSyntaxViewMode {
|
|
FileDiffInline,
|
|
FileDiffSplitLeft,
|
|
FileDiffSplitRight,
|
|
WorktreePreview,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
|
pub(in crate::view) struct PreparedSyntaxDocumentKey {
|
|
pub(in crate::view) repo_id: RepoId,
|
|
pub(in crate::view) target_rev: u64,
|
|
pub(in crate::view) file_path: std::path::PathBuf,
|
|
pub(in crate::view) view_mode: PreparedSyntaxViewMode,
|
|
}
|
|
|
|
pub(in crate::view) struct MainPaneView {
|
|
pub(in crate::view) store: Arc<AppStore>,
|
|
pub(super) state: Arc<AppState>,
|
|
pub(in crate::view) view_mode: GitCometViewMode,
|
|
pub(in crate::view) focused_mergetool_labels: Option<FocusedMergetoolLabels>,
|
|
pub(in crate::view) focused_mergetool_exit_code: Option<Arc<AtomicI32>>,
|
|
pub(in crate::view) theme: AppTheme,
|
|
pub(in crate::view) date_time_format: DateTimeFormat,
|
|
pub(super) _ui_model_subscription: gpui::Subscription,
|
|
pub(super) root_view: WeakEntity<GitCometView>,
|
|
pub(super) tooltip_host: WeakEntity<TooltipHost>,
|
|
pub(super) notify_fingerprint: u64,
|
|
pub(in crate::view) active_context_menu_invoker: Option<SharedString>,
|
|
|
|
pub(in crate::view) last_window_size: Size<Pixels>,
|
|
|
|
pub(in crate::view) show_whitespace: bool,
|
|
pub(in crate::view) diff_view: DiffViewMode,
|
|
pub(in crate::view) svg_diff_view_mode: SvgDiffViewMode,
|
|
pub(in crate::view) diff_word_wrap: bool,
|
|
pub(in crate::view) diff_split_ratio: f32,
|
|
pub(in crate::view) diff_split_resize: Option<DiffSplitResizeState>,
|
|
pub(in crate::view) diff_split_last_synced_y: Pixels,
|
|
pub(in crate::view) diff_horizontal_min_width: Pixels,
|
|
pub(in crate::view) diff_cache_repo_id: Option<RepoId>,
|
|
pub(in crate::view) diff_cache_rev: u64,
|
|
pub(in crate::view) diff_cache_target: Option<DiffTarget>,
|
|
pub(in crate::view) diff_cache: Vec<AnnotatedDiffLine>,
|
|
pub(in crate::view) diff_row_provider: Option<Arc<super::diff_cache::PagedPatchDiffRows>>,
|
|
pub(in crate::view) diff_split_row_provider:
|
|
Option<Arc<super::diff_cache::PagedPatchSplitRows>>,
|
|
pub(in crate::view) diff_file_for_src_ix: Vec<Option<Arc<str>>>,
|
|
pub(in crate::view) diff_language_for_src_ix: Vec<Option<rows::DiffSyntaxLanguage>>,
|
|
pub(in crate::view) diff_click_kinds: Vec<DiffClickKind>,
|
|
pub(in crate::view) diff_line_kind_for_src_ix: Vec<gitcomet_core::domain::DiffLineKind>,
|
|
pub(in crate::view) diff_hide_unified_header_for_src_ix: Vec<bool>,
|
|
pub(in crate::view) diff_header_display_cache: HashMap<usize, SharedString>,
|
|
pub(in crate::view) diff_split_cache: Vec<PatchSplitRow>,
|
|
pub(in crate::view) diff_split_cache_len: usize,
|
|
pub(in crate::view) diff_panel_focus_handle: FocusHandle,
|
|
pub(in crate::view) diff_autoscroll_pending: bool,
|
|
pub(in crate::view) diff_raw_input: Entity<components::TextInput>,
|
|
pub(in crate::view) diff_visible_indices: Vec<usize>,
|
|
pub(in crate::view) diff_visible_inline_map: Option<super::diff_cache::PatchInlineVisibleMap>,
|
|
pub(in crate::view) diff_visible_cache_len: usize,
|
|
pub(in crate::view) diff_visible_view: DiffViewMode,
|
|
pub(in crate::view) diff_visible_is_file_view: bool,
|
|
pub(in crate::view) diff_scrollbar_markers_cache: Vec<components::ScrollbarMarker>,
|
|
pub(in crate::view) diff_word_highlights: Vec<Option<Vec<Range<usize>>>>,
|
|
pub(in crate::view) diff_word_highlights_inflight: Option<u64>,
|
|
pub(in crate::view) diff_file_stats: Vec<Option<(usize, usize)>>,
|
|
pub(in crate::view) diff_text_segments_cache: Vec<Option<CachedDiffStyledText>>,
|
|
pub(in crate::view) diff_text_query_segments_cache: Vec<Option<CachedDiffStyledText>>,
|
|
pub(in crate::view) diff_text_query_cache_query: SharedString,
|
|
pub(in crate::view) diff_selection_anchor: Option<usize>,
|
|
pub(in crate::view) diff_selection_range: Option<(usize, usize)>,
|
|
pub(in crate::view) diff_text_selecting: bool,
|
|
pub(in crate::view) diff_text_anchor: Option<DiffTextPos>,
|
|
pub(in crate::view) diff_text_head: Option<DiffTextPos>,
|
|
pub(super) diff_text_autoscroll_seq: u64,
|
|
pub(super) diff_text_autoscroll_target: Option<DiffTextAutoscrollTarget>,
|
|
pub(super) diff_text_last_mouse_pos: Point<Pixels>,
|
|
pub(in crate::view) diff_suppress_clicks_remaining: u8,
|
|
pub(in crate::view) diff_text_hitboxes: HashMap<(usize, DiffTextRegion), DiffTextHitbox>,
|
|
pub(in crate::view) diff_text_layout_cache_epoch: u64,
|
|
pub(in crate::view) diff_text_layout_cache: HashMap<u64, DiffTextLayoutCacheEntry>,
|
|
pub(in crate::view) diff_hunk_picker_search_input: Option<Entity<components::TextInput>>,
|
|
pub(in crate::view) diff_search_active: bool,
|
|
pub(in crate::view) diff_search_query: SharedString,
|
|
pub(in crate::view) diff_search_matches: Vec<usize>,
|
|
pub(in crate::view) diff_search_match_ix: Option<usize>,
|
|
pub(in crate::view) diff_search_input: Entity<components::TextInput>,
|
|
pub(super) _diff_search_subscription: gpui::Subscription,
|
|
|
|
pub(in crate::view) file_diff_cache_repo_id: Option<RepoId>,
|
|
pub(in crate::view) file_diff_cache_rev: u64,
|
|
pub(in crate::view) file_diff_cache_target: Option<DiffTarget>,
|
|
pub(in crate::view) file_diff_cache_path: Option<std::path::PathBuf>,
|
|
pub(in crate::view) file_diff_cache_language: Option<rows::DiffSyntaxLanguage>,
|
|
pub(in crate::view) file_diff_cache_rows: Vec<FileDiffRow>,
|
|
pub(in crate::view) file_diff_inline_cache: Vec<AnnotatedDiffLine>,
|
|
pub(in crate::view) file_diff_inline_word_highlights: Vec<Option<Vec<Range<usize>>>>,
|
|
pub(in crate::view) file_diff_split_word_highlights_old: Vec<Option<Vec<Range<usize>>>>,
|
|
pub(in crate::view) file_diff_split_word_highlights_new: Vec<Option<Vec<Range<usize>>>>,
|
|
pub(in crate::view) file_diff_cache_seq: u64,
|
|
pub(in crate::view) file_diff_cache_inflight: Option<u64>,
|
|
pub(in crate::view) file_diff_syntax_generation: u64,
|
|
pub(in crate::view) prepared_syntax_documents:
|
|
HashMap<PreparedSyntaxDocumentKey, rows::PreparedDiffSyntaxDocument>,
|
|
|
|
pub(in crate::view) file_image_diff_cache_repo_id: Option<RepoId>,
|
|
pub(in crate::view) file_image_diff_cache_rev: u64,
|
|
pub(in crate::view) file_image_diff_cache_target: Option<DiffTarget>,
|
|
pub(in crate::view) file_image_diff_cache_path: Option<std::path::PathBuf>,
|
|
pub(in crate::view) file_image_diff_cache_old: Option<Arc<gpui::Image>>,
|
|
pub(in crate::view) file_image_diff_cache_new: Option<Arc<gpui::Image>>,
|
|
pub(in crate::view) file_image_diff_cache_old_svg_path: Option<std::path::PathBuf>,
|
|
pub(in crate::view) file_image_diff_cache_new_svg_path: Option<std::path::PathBuf>,
|
|
|
|
pub(in crate::view) worktree_preview_path: Option<std::path::PathBuf>,
|
|
pub(in crate::view) worktree_preview: Loadable<Arc<Vec<String>>>,
|
|
pub(in crate::view) worktree_preview_content_rev: u64,
|
|
pub(in crate::view) worktree_preview_segments_cache_path: Option<std::path::PathBuf>,
|
|
pub(in crate::view) worktree_preview_syntax_language: Option<rows::DiffSyntaxLanguage>,
|
|
pub(in crate::view) worktree_preview_segments_cache: HashMap<usize, CachedDiffStyledText>,
|
|
pub(in crate::view) diff_preview_is_new_file: bool,
|
|
pub(in crate::view) diff_preview_new_file_lines: Arc<Vec<String>>,
|
|
|
|
pub(in crate::view) conflict_resolver_input: Entity<components::TextInput>,
|
|
pub(super) _conflict_resolver_input_subscription: gpui::Subscription,
|
|
pub(in crate::view) conflict_resolver: ConflictResolverUiState,
|
|
pub(in crate::view) conflict_resolver_vsplit_ratio: f32,
|
|
pub(in crate::view) conflict_resolver_vsplit_resize: Option<ConflictVSplitResizeState>,
|
|
pub(in crate::view) conflict_three_way_col_ratios: [f32; 2],
|
|
pub(in crate::view) conflict_three_way_col_widths: [Pixels; 3],
|
|
pub(in crate::view) conflict_hsplit_resize: Option<ConflictHSplitResizeState>,
|
|
pub(in crate::view) conflict_diff_split_ratio: f32,
|
|
pub(in crate::view) conflict_diff_split_resize: Option<ConflictDiffSplitResizeState>,
|
|
pub(in crate::view) conflict_diff_split_col_widths: [Pixels; 2],
|
|
pub(in crate::view) conflict_canvas_rows_enabled: bool,
|
|
pub(in crate::view) conflict_diff_segments_cache_split:
|
|
HashMap<(usize, ConflictPickSide), CachedDiffStyledText>,
|
|
pub(in crate::view) conflict_diff_segments_cache_inline: HashMap<usize, CachedDiffStyledText>,
|
|
pub(in crate::view) conflict_diff_query_segments_cache_split:
|
|
HashMap<(usize, ConflictPickSide), CachedDiffStyledText>,
|
|
pub(in crate::view) conflict_diff_query_segments_cache_inline:
|
|
HashMap<usize, CachedDiffStyledText>,
|
|
pub(in crate::view) conflict_diff_query_cache_query: SharedString,
|
|
pub(in crate::view) conflict_three_way_segments_cache:
|
|
HashMap<(usize, ThreeWayColumn), CachedDiffStyledText>,
|
|
pub(in crate::view) conflict_resolved_preview_path: Option<std::path::PathBuf>,
|
|
pub(in crate::view) conflict_resolved_preview_source_hash: Option<u64>,
|
|
pub(in crate::view) conflict_resolved_preview_text: SharedString,
|
|
pub(in crate::view) conflict_resolved_preview_syntax_language: Option<rows::DiffSyntaxLanguage>,
|
|
pub(in crate::view) conflict_resolved_preview_line_count: usize,
|
|
pub(in crate::view) conflict_resolved_preview_line_starts: Vec<usize>,
|
|
pub(in crate::view) conflict_resolved_preview_segments_cache:
|
|
HashMap<usize, CachedDiffStyledText>,
|
|
|
|
pub(in crate::view) history_view: Entity<super::HistoryView>,
|
|
pub(in crate::view) diff_scroll: UniformListScrollHandle,
|
|
pub(in crate::view) diff_split_right_scroll: UniformListScrollHandle,
|
|
pub(in crate::view) conflict_resolver_diff_scroll: UniformListScrollHandle,
|
|
pub(in crate::view) conflict_resolved_preview_scroll: UniformListScrollHandle,
|
|
pub(in crate::view) worktree_preview_scroll: UniformListScrollHandle,
|
|
|
|
pub(super) path_display_cache: std::cell::RefCell<HashMap<std::path::PathBuf, SharedString>>,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub(super) enum DiffTextAutoscrollTarget {
|
|
DiffLeftOrInline,
|
|
DiffSplitRight,
|
|
WorktreePreview,
|
|
ConflictResolvedPreview,
|
|
}
|
|
|
|
pub(super) fn parse_conflict_canvas_rows_env(value: &str) -> bool {
|
|
!matches!(
|
|
value.trim().to_ascii_lowercase().as_str(),
|
|
"0" | "false" | "off" | "no"
|
|
)
|
|
}
|
|
|
|
pub(super) fn conflict_canvas_rows_enabled_from_env() -> bool {
|
|
std::env::var("GITCOMET_CONFLICT_CANVAS_ROWS")
|
|
.ok()
|
|
.is_none_or(|value| parse_conflict_canvas_rows_env(&value))
|
|
}
|