Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

conflicts: whitespace parsing fixes #4944

Merged
merged 4 commits into from
Nov 23, 2024
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
18 changes: 16 additions & 2 deletions lib/src/conflicts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use std::io::Write;
use std::iter::zip;

use bstr::BString;
use bstr::ByteSlice;
use futures::stream::BoxStream;
use futures::try_join;
use futures::Stream;
Expand Down Expand Up @@ -394,7 +395,7 @@ pub fn parse_conflict(input: &[u8], num_sides: usize) -> Option<Vec<Merge<BStrin
let mut conflict_start = None;
let mut conflict_start_len = 0;
for line in input.split_inclusive(|b| *b == b'\n') {
if CONFLICT_MARKER_REGEX.is_match_at(line, 0) {
if is_conflict_marker_line(line) {
if line[0] == CONFLICT_START_LINE_CHAR {
conflict_start = Some(pos);
conflict_start_len = line.len();
Expand Down Expand Up @@ -436,7 +437,7 @@ fn parse_conflict_hunk(input: &[u8]) -> Merge<BString> {
let mut removes = vec![];
let mut adds = vec![];
for line in input.split_inclusive(|b| *b == b'\n') {
if CONFLICT_MARKER_REGEX.is_match_at(line, 0) {
if is_conflict_marker_line(line) {
match line[0] {
CONFLICT_DIFF_LINE_CHAR => {
state = State::Diff;
Expand Down Expand Up @@ -466,6 +467,12 @@ fn parse_conflict_hunk(input: &[u8]) -> Merge<BString> {
} else if let Some(rest) = line.strip_prefix(b" ") {
removes.last_mut().unwrap().extend_from_slice(rest);
adds.last_mut().unwrap().extend_from_slice(rest);
} else if line == b"\n" || line == b"\r\n" {
// Some editors strip trailing whitespace, so " \n" might become "\n". It would
// be unfortunate if this prevented the conflict from being parsed, so we add
// the empty line to the "remove" and "add" as if there was a space in front
removes.last_mut().unwrap().extend_from_slice(line);
adds.last_mut().unwrap().extend_from_slice(line);
} else {
// Doesn't look like a valid conflict
return Merge::resolved(BString::new(vec![]));
Expand All @@ -492,6 +499,13 @@ fn parse_conflict_hunk(input: &[u8]) -> Merge<BString> {
}
}

/// Check whether a line is a conflict marker. Removes trailing whitespace
/// before checking against regex to ensure it parses CRLF endings correctly.
fn is_conflict_marker_line(line: &[u8]) -> bool {
let line = line.trim_end_with(|ch| ch.is_ascii_whitespace());
CONFLICT_MARKER_REGEX.is_match_at(line, 0)
}

/// Parses conflict markers in `content` and returns an updated version of
/// `file_ids` with the new contents. If no (valid) conflict markers remain, a
/// single resolves `FileId` will be returned.
Expand Down
86 changes: 86 additions & 0 deletions lib/tests/test_conflicts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,92 @@ fn test_parse_conflict_multi_way() {
);
}

#[test]
fn test_parse_conflict_crlf_markers() {
// Conflict markers should be recognized even with CRLF
insta::assert_debug_snapshot!(
parse_conflict(
indoc! {b"
line 1\r
<<<<<<<\r
+++++++\r
left\r
-------\r
base\r
+++++++\r
right\r
>>>>>>>\r
line 5\r
"},
2
),
@r#"
Some(
[
Resolved(
"line 1\r\n",
),
Conflicted(
[
"left\r\n",
"base\r\n",
"right\r\n",
],
),
Resolved(
"line 5\r\n",
),
],
)
"#
);
}

#[test]
fn test_parse_conflict_diff_stripped_whitespace() {
// Should be able to parse conflict even if diff contains empty line (without
// even a leading space, which is sometimes stripped by text editors)
insta::assert_debug_snapshot!(
parse_conflict(
indoc! {b"
line 1
<<<<<<<
%%%%%%%
line 2

-line 3
+left
\r
line 4
+++++++
right
>>>>>>>
line 5
"},
2
),
@r#"
Some(
[
Resolved(
"line 1\n",
),
Conflicted(
[
"line 2\n\nleft\n\r\nline 4\n",
"line 2\n\nline 3\n\r\nline 4\n",
"right\n",
],
),
Resolved(
"line 5\n",
),
],
)
"#
);
}

#[test]
fn test_parse_conflict_wrong_arity() {
// Valid conflict marker but it has fewer sides than the caller expected
Expand Down