Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ members = [
"examples/hello_world",
"examples/input",
"examples/editor",
"examples/tig",
"examples/brush",
"examples/dock",
"examples/html",
Expand Down
83 changes: 76 additions & 7 deletions crates/component/src/diff/conflict.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,18 @@ pub(crate) enum ConflictPart {
Incoming,
}

/// One conflict region. Ranges index the file's modified-side lines, which
/// exclude the marker lines.
/// A parsed merge conflict in a working file.
///
/// Source coordinates are one-based, half-open ranges, allowing empty parts.
/// Returned by [`DiffFile::conflicts`] in source order; its slice index is the
/// index accepted by [`super::DiffState::resolve_conflict`]. Fields are private
/// and only the parser constructs these snapshots.
#[derive(Clone, Debug)]
pub(crate) struct Conflict {
pub struct DiffConflict {
lines: Range<usize>,
current_lines: Range<usize>,
base_lines: Option<Range<usize>>,
incoming_lines: Range<usize>,
current: Range<usize>,
base: Option<Range<usize>>,
incoming: Range<usize>,
Expand All @@ -41,9 +49,37 @@ pub(crate) struct Conflict {
incoming_label: SharedString,
}

impl Conflict {
impl DiffConflict {
/// One-based working-file lines, including markers, with an exclusive end.
pub fn lines(&self) -> Range<usize> {
self.lines.clone()
}
/// Current-side source lines, with an exclusive end; may be empty.
pub fn current_lines(&self) -> Range<usize> {
self.current_lines.clone()
}
/// Base source lines for diff3, with an exclusive end; may be empty.
pub fn base_lines(&self) -> Option<Range<usize>> {
self.base_lines.clone()
}
/// Incoming-side source lines, with an exclusive end; may be empty.
pub fn incoming_lines(&self) -> Range<usize> {
self.incoming_lines.clone()
}
/// The label from the opening marker.
pub fn current_label(&self) -> &SharedString {
&self.current_label
}
/// The base marker label, or None for a two-way conflict.
pub fn base_label(&self) -> Option<&SharedString> {
self.base.as_ref().map(|_| &self.base_label)
}
/// The label from the closing marker.
pub fn incoming_label(&self) -> &SharedString {
&self.incoming_label
}
/// Every line of the region, in file order.
pub(crate) fn lines(&self) -> Range<usize> {
pub(crate) fn source_lines(&self) -> Range<usize> {
self.current.start..self.incoming.end
}
pub(crate) fn part(&self, part: ConflictPart) -> Option<Range<usize>> {
Expand Down Expand Up @@ -129,7 +165,7 @@ impl DiffFile {
let mut pairs = Vec::new();
let mut conflicts = Vec::new();
// The marker-delimited part being read, with the open region's ranges.
let mut open: Option<(ConflictPart, Conflict)> = None;
let mut open: Option<(ConflictPart, DiffConflict)> = None;
for (ix, range) in line_ranges(text).into_iter().enumerate() {
let line = &text[range.clone()];
let content = line.trim_end_matches(['\n', '\r']);
Expand All @@ -151,7 +187,11 @@ impl DiffFile {
if let Some(label) = opening {
open = Some((
ConflictPart::Current,
Conflict {
DiffConflict {
lines: ix + 1..ix + 1,
current_lines: ix + 2..ix + 2,
base_lines: None,
incoming_lines: ix + 2..ix + 2,
current: next..next,
base: None,
incoming: next..next,
Expand All @@ -176,6 +216,8 @@ impl DiffFile {
match (*part, base, separator, closing) {
(ConflictPart::Current, Some(label), _, _) => {
conflict.current.end = next;
conflict.current_lines.end = ix + 1;
conflict.base_lines = Some(ix + 2..ix + 2);
conflict.base = Some(next..next);
conflict.base_label = label;
*part = ConflictPart::Base;
Expand All @@ -184,15 +226,20 @@ impl DiffFile {
(ConflictPart::Current | ConflictPart::Base, _, Some(_), _) => {
if *part == ConflictPart::Current {
conflict.current.end = next;
conflict.current_lines.end = ix + 1;
} else if let Some(base) = &mut conflict.base {
base.end = next;
conflict.base_lines.as_mut().unwrap().end = ix + 1;
}
conflict.incoming = next..next;
conflict.incoming_lines = ix + 2..ix + 2;
*part = ConflictPart::Incoming;
continue;
}
(ConflictPart::Incoming, _, _, Some(label)) => {
conflict.incoming.end = next;
conflict.incoming_lines.end = ix + 1;
conflict.lines.end = ix + 2;
conflict.incoming_label = label;
if let Some((_, conflict)) = open.take() {
conflicts.push(conflict);
Expand Down Expand Up @@ -259,6 +306,13 @@ mod tests {
.collect::<Vec<_>>();
assert_eq!(numbers, [1, 3, 5, 7]);
let conflict = &file.conflicts()[0];
assert_eq!(conflict.lines(), 2..7);
assert_eq!(conflict.current_lines(), 3..4);
assert_eq!(conflict.incoming_lines(), 5..6);
assert_eq!(conflict.base_lines(), None);
assert_eq!(conflict.base_label(), None);
assert_eq!(conflict.current_label().as_str(), "HEAD");
assert_eq!(conflict.incoming_label().as_str(), "feature");
assert_eq!(conflict.part(ConflictPart::Current), Some(1..2));
assert_eq!(conflict.part(ConflictPart::Incoming), Some(2..3));
assert_eq!(conflict.label(ConflictPart::Current).as_str(), "HEAD");
Expand All @@ -275,6 +329,11 @@ mod tests {
)
.unwrap();
let conflict = &file.conflicts()[0];
assert_eq!(conflict.lines(), 1..8);
assert_eq!(conflict.current_lines(), 2..3);
assert_eq!(conflict.base_lines(), Some(4..5));
assert_eq!(conflict.incoming_lines(), 6..7);
assert_eq!(conflict.base_label().unwrap().as_str(), "base");
assert_eq!(conflict.part(ConflictPart::Base), Some(1..2));
assert_eq!(conflict.label(ConflictPart::Base).as_str(), "base");
assert_eq!(conflict.kept(DiffConflictResolution::Both), [0..1, 2..3]);
Expand All @@ -286,6 +345,16 @@ mod tests {
let error = DiffFile::parse_conflicts("a.txt", text).err().unwrap();
assert!(error.line() > 0);
}
let empty = DiffFile::parse_conflicts(
"empty.txt",
"<<<<<<< ours\n||||||| base\n=======\n>>>>>>> theirs\n",
)
.unwrap();
let conflict = &empty.conflicts()[0];
assert_eq!(conflict.lines(), 1..5);
assert_eq!(conflict.current_lines(), 2..2);
assert_eq!(conflict.base_lines(), Some(3..3));
assert_eq!(conflict.incoming_lines(), 4..4);
// A separator outside a conflict is ordinary text.
assert!(DiffFile::parse_conflicts("a.md", "title\n=======\n").is_ok());
}
Expand Down
9 changes: 5 additions & 4 deletions crates/component/src/diff/document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::{ops::Range, sync::Arc};

use gpui::SharedString;

use super::conflict::Conflict;
use super::conflict::DiffConflict;
use smallvec::{SmallVec, smallvec};
use unicode_segmentation::UnicodeSegmentation as _;

Expand Down Expand Up @@ -377,7 +377,7 @@ struct DocumentInner {
/// The patch body in order; each side's lines map back into it.
patch_lines: Vec<PatchLine>,
patch_ixs: [Vec<usize>; 2],
conflicts: Vec<Conflict>,
conflicts: Vec<DiffConflict>,
}

/// One changed file parsed from a unified or Git diff.
Expand Down Expand Up @@ -521,10 +521,11 @@ impl DiffFile {
DiffFileStatus::Unchanged | DiffFileStatus::Conflicted
)
}
pub(crate) fn conflicts(&self) -> &[Conflict] {
/// Merge conflicts in working-file order; slice indices identify resolutions.
pub fn conflicts(&self) -> &[DiffConflict] {
&self.inner.conflicts
}
pub(crate) fn with_conflicts(mut self, conflicts: Vec<Conflict>) -> Self {
pub(crate) fn with_conflicts(mut self, conflicts: Vec<DiffConflict>) -> Self {
Arc::get_mut(&mut self.inner)
.expect("a newly built file is not shared")
.conflicts = conflicts;
Expand Down
17 changes: 15 additions & 2 deletions crates/component/src/diff/header_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ fn test_diff_builder(cx: &mut TestAppContext) {
.line_number(false)
.syntax_highlight(false)
.annotations([annotation.clone()])
.header({
.render_header({
let header_calls = header_calls.clone();
move |document: &DiffFile, _, _| {
header_calls.borrow_mut().push((
Expand All @@ -41,7 +41,7 @@ fn test_diff_builder(cx: &mut TestAppContext) {
div().child("header")
}
})
.annotation_content({
.render_annotation({
let annotation_calls = annotation_calls.clone();
move |annotation, _, _| {
assert_eq!(annotation.id(), &"review-comment".into());
Expand All @@ -60,6 +60,19 @@ fn test_diff_builder(cx: &mut TestAppContext) {
assert_eq!(diff.annotations.items[0].position(), Some(&position));
assert_eq!(diff.annotations.lines[&position], [0]);

// Existing consumers keep the original builder spellings.
let compatibility = Diff::new(&state)
.header(|_, _, _| div().child("header"))
.header_prefix(|_, _, _| div().child("prefix"))
.header_title_suffix(|_, _, _| div().child("title suffix"))
.header_suffix(|_, _, _| div().child("suffix"))
.annotation_content(|_, _, _| div().child("annotation"));
assert!(compatibility.header.content.is_some());
assert!(compatibility.header.prefix.is_some());
assert!(compatibility.header.title_suffix.is_some());
assert!(compatibility.header.suffix.is_some());
assert!(compatibility.annotation_content.is_some());

let window = cx.add_empty_window();
window.update(|window, cx| {
let render = diff.header.content.as_ref().unwrap();
Expand Down
Loading
Loading