Skip to content

Commit fdb88ae

Browse files
committed
fix(editor): split CRLF files into lines in the conflict diff
Claude-Session: https://claude.ai/code/session_01WKxR4tvL8bPU4NfsULyYNA
1 parent 6a62ca4 commit fdb88ae

3 files changed

Lines changed: 83 additions & 3 deletions

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
//
2+
// FileConflictDiff.swift
3+
// TablePro
4+
//
5+
6+
import Foundation
7+
8+
/// Line splitting for the file-conflict sheet. This deliberately does not reuse
9+
/// `SqlNormalizer`, which trims boundary whitespace and rewrites line endings:
10+
/// a conflict sheet compares file bytes, so leading or trailing blank lines are a
11+
/// real difference the user needs to see.
12+
enum FileConflictDiff {
13+
/// Swift treats "\r\n" as one Character, so splitting on "\n" alone leaves a
14+
/// CRLF file as a single line. Line endings are folded before splitting, then
15+
/// blank lines are kept.
16+
static func lines(_ content: String) -> [String] {
17+
content
18+
.replacingOccurrences(of: "\r\n", with: "\n")
19+
.replacingOccurrences(of: "\r", with: "\n")
20+
.split(separator: "\n", omittingEmptySubsequences: false)
21+
.map(String.init)
22+
}
23+
24+
static func pairs(mine: String, disk: String) -> [DiffPair] {
25+
DiffComputer.computeSplit(before: lines(mine), after: lines(disk))
26+
}
27+
}

TablePro/Views/Sidebar/FileConflictDiffSheet.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,7 @@ internal struct FileConflictDiffSheet: View {
1616
@Environment(\.dismiss) private var dismiss
1717

1818
private var diffLines: [DiffPair] {
19-
let mineLines = mineContent.split(separator: "\n", omittingEmptySubsequences: false).map(String.init)
20-
let diskLines = diskContent.split(separator: "\n", omittingEmptySubsequences: false).map(String.init)
21-
return DiffComputer.computeSplit(before: mineLines, after: diskLines)
19+
FileConflictDiff.pairs(mine: mineContent, disk: diskContent)
2220
}
2321

2422
var body: some View {
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
//
2+
// FileConflictDiffTests.swift
3+
// TableProTests
4+
//
5+
6+
import Foundation
7+
@testable import TablePro
8+
import Testing
9+
10+
@Suite("FileConflictDiff")
11+
struct FileConflictDiffTests {
12+
@Test("identical content produces only unchanged pairs")
13+
func identicalContentIsUnchanged() {
14+
let pairs = FileConflictDiff.pairs(mine: "a\nb", disk: "a\nb")
15+
16+
#expect(pairs.count == 2)
17+
#expect(pairs.allSatisfy { $0.kind == .unchanged })
18+
}
19+
20+
@Test("a replaced line is reported as changed with both sides")
21+
func replacedLineIsChanged() {
22+
let pairs = FileConflictDiff.pairs(mine: "a\nb", disk: "a\nc")
23+
24+
#expect(pairs.contains(DiffPair(before: "b", after: "c", kind: .changed)))
25+
}
26+
27+
@Test("conflict lines keep boundary whitespace that SQL normalization would trim")
28+
func conflictLinesKeepBoundaryWhitespace() {
29+
let content = "\nSELECT 1\n"
30+
31+
#expect(FileConflictDiff.lines(content) == ["", "SELECT 1", ""])
32+
#expect(SqlNormalizer.lines(content) == ["SELECT 1"])
33+
}
34+
35+
@Test("a file that gained a trailing blank line reads as a real difference")
36+
func trailingBlankLineIsADifference() {
37+
let pairs = FileConflictDiff.pairs(mine: "a", disk: "a\n")
38+
39+
#expect(pairs.contains { $0.kind != .unchanged })
40+
}
41+
42+
@Test("a CRLF file splits into lines instead of collapsing into one")
43+
func crlfSplitsIntoLines() {
44+
#expect(FileConflictDiff.lines("a\r\nb") == ["a", "b"])
45+
#expect(FileConflictDiff.lines("a\rb") == ["a", "b"])
46+
}
47+
48+
@Test("a CRLF file diffs line by line against its LF twin")
49+
func crlfDiffsLineByLine() {
50+
let pairs = FileConflictDiff.pairs(mine: "a\r\nb\r\nc", disk: "a\nx\nc")
51+
52+
#expect(pairs.count == 3)
53+
#expect(pairs.contains(DiffPair(before: "b", after: "x", kind: .changed)))
54+
}
55+
}

0 commit comments

Comments
 (0)