T19 round 2 fixes

- regions read their lines through TextMerge's accessors; conflicts and resolve
  split with similar's tokenize_lines, so a lone return ends a line everywhere
- the merged ending and line breaks know a lone return
- taking both sides separates them only when both hold text
- merge views split rows the same way
This commit is contained in:
ZacharyZhang-NY committed 2026-09-26 02:28:13 -04:00
1 parent 3abcd5cc87
commit b2c587546e
3 files changed
+48 -19

No files matched your search

+2 -1
View File
@@ -243,4 +243,5 @@ Per-component lines live in `tasks/`. Tags there:
| T18 | 2 | FAIL | a folder it could not enter still started the shell elsewhere; links read cluster cells as spaces |
| T18 | 3 | PASS | |
| T19 | 1 | FAIL | a merge rewrote line endings and added a last newline; the split view numbered its right side from the old file; an empty side started its hunk a line late; the subject counter covered long subjects; clipped code rows had no line height |
| T19 | 2 | — | pending review |
| T19 | 2 | FAIL | a lone return split lines for similar but not for regions, so a change was lost; taking both sides added a newline to an empty side |
| T19 | 3 | — | pending review |
+6 -2
View File
@@ -5,6 +5,8 @@ use gpui::{
SharedString, Styled, Window, div, prelude::*, relative,
};
use similar::DiffableStr;
use super::merging::{Region, RegionKind, Take, conflicts, resolve, result};
use crate::{
buttons::{Button, ButtonVariant},
@@ -238,7 +240,8 @@ impl RenderOnce for ThreeWayMerge {
.font_family(theme.mono_family.clone())
.text_size(theme.text_size(TextSize::Xs))
.text_color(colors.fg)
.children(merged.lines().map(|line| {
.children(merged.tokenize_lines().into_iter().map(|line| {
let line = line.trim_end_matches(['\r', '\n']);
let marker = ["<<<<<<<", "=======", ">>>>>>>"]
.iter()
.any(|mark| line.starts_with(mark));
@@ -298,7 +301,8 @@ impl RenderOnce for ConflictResolver {
});
let (current, incoming) = (colors.success.opacity(0.1), colors.info.opacity(0.1));
let mut rows: Vec<AnyElement> = Vec::new();
for (ix, line) in self.text.lines().enumerate() {
for (ix, line) in self.text.tokenize_lines().into_iter().enumerate() {
let line = line.trim_end_matches(['\r', '\n']);
if let Some(at) = found.iter().position(|conflict| conflict.start == ix) {
rows.push(
takes(
+40 -16
View File
@@ -1,4 +1,4 @@
use similar::{MergeResolution, TextMerge};
use similar::{DiffableStr, MergeResolution, TextMerge};
/// Which side a conflict takes.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -28,12 +28,17 @@ pub struct Region {
pub theirs: Vec<String>,
}
/// Lines with their own endings, so a merge gives back each byte it keeps.
fn lines(text: &str, range: std::ops::Range<usize>) -> Vec<String> {
text.split_inclusive('\n')
.skip(range.start)
.take(range.len())
.map(str::to_string)
/// A region's lines as similar split them, each with its ending.
fn lines<'a>(
range: std::ops::Range<usize>,
line: impl Fn(usize) -> Option<&'a str>,
) -> Vec<String> {
range
.map(|ix| {
line(ix)
.expect("a region's lines are in its text")
.to_string()
})
.collect()
}
@@ -42,23 +47,25 @@ fn ending(regions: &[Region]) -> &'static str {
let line = regions
.iter()
.flat_map(|region| region.base.iter().chain(&region.ours).chain(&region.theirs))
.find(|line| line.ends_with('\n'));
.find(|line| line.as_str().ends_with_newline());
match line {
Some(line) if line.ends_with("\r\n") => "\r\n",
Some(line) if line.ends_with('\r') => "\r",
_ => "\n",
}
}
/// Ends the last line when it has no ending, so what follows starts a line.
fn break_line(out: &mut String, ending: &str) {
if !out.is_empty() && !out.ends_with('\n') {
if !out.is_empty() && !out.as_str().ends_with_newline() {
out.push_str(ending);
}
}
/// `ours` and `theirs` merged against `base`, region by region.
pub fn regions(base: &str, ours: &str, theirs: &str) -> Vec<Region> {
TextMerge::from_lines(base, ours, theirs)
let merge = TextMerge::from_lines(base, ours, theirs);
merge
.regions()
.iter()
.map(|region| Region {
@@ -70,9 +77,9 @@ pub fn regions(base: &str, ours: &str, theirs: &str) -> Vec<Region> {
MergeResolution::Conflict => RegionKind::Conflict,
other => unreachable!("a merge resolution this crate does not know: {other:?}"),
},
base: lines(base, region.base_range()),
ours: lines(ours, region.ours_range()),
theirs: lines(theirs, region.theirs_range()),
base: lines(region.base_range(), |ix| merge.base_line(ix)),
ours: lines(region.ours_range(), |ix| merge.ours_line(ix)),
theirs: lines(region.theirs_range(), |ix| merge.theirs_line(ix)),
})
.collect()
}
@@ -94,7 +101,9 @@ pub fn result(regions: &[Region], takes: &[Option<Take>]) -> (String, usize) {
Some(Take::Theirs) => out.push_str(&theirs),
Some(Take::Both) => {
out.push_str(&ours);
break_line(&mut out, ending);
if !theirs.is_empty() {
break_line(&mut out, ending);
}
out.push_str(&theirs);
}
None => {
@@ -129,7 +138,7 @@ pub struct Conflict {
pub fn conflicts(text: &str) -> Vec<Conflict> {
let mut found = Vec::new();
let (mut start, mut middle) = (None, None);
for (ix, line) in text.lines().enumerate() {
for (ix, line) in text.tokenize_lines().into_iter().enumerate() {
if line.starts_with("<<<<<<<") {
(start, middle) = (Some(ix), None);
} else if line.starts_with("=======") && start.is_some() {
@@ -150,7 +159,7 @@ pub fn conflicts(text: &str) -> Vec<Conflict> {
/// `text` with conflict `ix` settled as taken; every other byte stays.
pub fn resolve(text: &str, ix: usize, take: Take) -> String {
let conflict = conflicts(text)[ix];
let lines: Vec<&str> = text.split_inclusive('\n').collect();
let lines = text.tokenize_lines();
let ours = lines[conflict.start + 1..conflict.middle].concat();
let theirs = lines[conflict.middle + 1..conflict.end].concat();
let kept = match take {
@@ -204,6 +213,21 @@ mod tests {
assert_eq!(resolve(marked, 0, Take::Ours), "a\r\nours\r\nz");
}
#[test]
fn a_bare_return_ends_a_line_as_it_does_for_similar() {
let changed = regions("a\rb\n", "a\rb\n", "a\rB\n");
assert_eq!(result(&changed, &[]).0, "a\rB\n");
let held = result(&regions("a\r", "b\r", "c\r"), &[]).0;
assert_eq!(held, "<<<<<<< ours\rb\r=======\rc\r>>>>>>> theirs\r");
assert_eq!(resolve(&held, 0, Take::Theirs), "c\r");
}
#[test]
fn taking_both_adds_no_line_to_an_empty_side() {
assert_eq!(result(&regions("a", "b", ""), &[Some(Take::Both)]).0, "b");
assert_eq!(result(&regions("a", "", "c"), &[Some(Take::Both)]).0, "c");
}
#[test]
fn markers_resolve_to_one_side_or_both() {
let marked = "a\n<<<<<<< HEAD\nours\n=======\ntheirs\n>>>>>>> topic\nz\n";