Skip to content

Commit 1301981

Browse files
committed
Unified (git) diff highlighting: -/+ red/green with tint, @@ cyan, headers bold; isUnifiedDiffLine for callers
1 parent bf2ad90 commit 1301981

2 files changed

Lines changed: 125 additions & 0 deletions

File tree

‎Sources/AgentColorSyntax/CodeBlockSyntax+Diff.swift‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ extension CodeBlockHighlighter {
1414
/// Highlight a diff code block with red/green backgrounds for removed/added lines,
1515
/// and line numbers for context. Format: "LINE_NUM -\tcode" or "LINE_NUM +\tcode" or "LINE_NUM\tcode"
1616
public static func highlightDiffBlock(code: String, font: NSFont) -> NSAttributedString {
17+
if looksLikeUnifiedDiff(code) { return highlightUnifiedDiff(code: code, font: font) }
1718
let text = CodeBlockTheme.text
1819
let result = NSMutableAttributedString(string: code, attributes: [
1920
.font: font, .foregroundColor: text
@@ -81,4 +82,64 @@ extension CodeBlockHighlighter {
8182

8283
return result
8384
}
85+
86+
// MARK: - Unified (git) diff
87+
88+
/// Header lines that can appear inside a git diff between hunks.
89+
private static let unifiedHeaderPrefixes = [
90+
"diff --git ", "index ", "--- ", "+++ ", "new file mode", "deleted file mode",
91+
"old mode", "new mode", "similarity index", "rename from", "rename to", "Binary files"
92+
]
93+
94+
/// True for `git diff` / `diff -u` output: a `diff --git` header, or `---`/`+++` file headers plus an `@@` hunk.
95+
public static func looksLikeUnifiedDiff(_ code: String) -> Bool {
96+
let t = code.drop(while: { $0 == "\n" || $0 == " " })
97+
if t.hasPrefix("diff --git ") { return true }
98+
return code.contains("\n@@ ") && code.contains("--- ") && code.contains("+++ ")
99+
}
100+
101+
/// True if `line` can belong to a unified diff (header, hunk, context, add, remove, "\ No newline").
102+
public static func isUnifiedDiffLine(_ line: Substring) -> Bool {
103+
guard let c = line.first else { return false }
104+
if c == " " || c == "+" || c == "-" || c == "\\" { return true }
105+
if line.hasPrefix("@@") { return true }
106+
return unifiedHeaderPrefixes.contains(where: { line.hasPrefix($0) })
107+
}
108+
109+
/// git-style coloring: file headers bold, `@@` hunks cyan, `-` lines red, `+` lines green (with tinted background).
110+
public static func highlightUnifiedDiff(code: String, font: NSFont) -> NSAttributedString {
111+
let isDark = CodeBlockTheme.isDark
112+
let result = NSMutableAttributedString(string: code, attributes: [
113+
.font: font, .foregroundColor: CodeBlockTheme.text
114+
])
115+
let bold = NSFont.monospacedSystemFont(ofSize: font.pointSize, weight: .bold)
116+
let removedBg = isDark ? NSColor(red: 0.4, green: 0.1, blue: 0.1, alpha: 0.6) : NSColor(red: 1.0, green: 0.88, blue: 0.88, alpha: 1)
117+
let addedBg = isDark ? NSColor(red: 0.1, green: 0.3, blue: 0.1, alpha: 0.6) : NSColor(red: 0.88, green: 1.0, blue: 0.88, alpha: 1)
118+
let removedText = isDark ? NSColor(red: 1.0, green: 0.55, blue: 0.55, alpha: 1) : NSColor(red: 0.7, green: 0.0, blue: 0.0, alpha: 1)
119+
let addedText = isDark ? NSColor(red: 0.55, green: 0.95, blue: 0.55, alpha: 1) : NSColor(red: 0.0, green: 0.5, blue: 0.0, alpha: 1)
120+
let hunk = isDark ? NSColor(red: 0.4, green: 0.85, blue: 0.85, alpha: 1) : NSColor(red: 0.0, green: 0.5, blue: 0.5, alpha: 1)
121+
let meta = isDark ? NSColor(red: 0.6, green: 0.6, blue: 0.65, alpha: 1) : NSColor(red: 0.4, green: 0.4, blue: 0.45, alpha: 1)
122+
123+
let ns = code as NSString
124+
var loc = 0
125+
while loc < ns.length {
126+
let lr = ns.lineRange(for: NSRange(location: loc, length: 0))
127+
var body = lr
128+
if body.length > 0, ns.character(at: NSMaxRange(body) - 1) == 10 { body.length -= 1 }
129+
let line = ns.substring(with: body)
130+
if line.hasPrefix("diff --git ") || line.hasPrefix("--- ") || line.hasPrefix("+++ ") {
131+
result.addAttributes([.foregroundColor: CodeBlockTheme.text, .font: bold], range: body)
132+
} else if line.hasPrefix("@@") {
133+
result.addAttribute(.foregroundColor, value: hunk, range: body)
134+
} else if line.hasPrefix("-") {
135+
result.addAttributes([.foregroundColor: removedText, .backgroundColor: removedBg], range: body)
136+
} else if line.hasPrefix("+") {
137+
result.addAttributes([.foregroundColor: addedText, .backgroundColor: addedBg], range: body)
138+
} else if unifiedHeaderPrefixes.contains(where: { line.hasPrefix($0) }) || line.hasPrefix("\\") {
139+
result.addAttribute(.foregroundColor, value: meta, range: body)
140+
}
141+
loc = NSMaxRange(lr)
142+
}
143+
return result
144+
}
84145
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import Testing
2+
import AppKit
3+
@testable import AgentColorSyntax
4+
5+
@Suite("Unified diff highlighting")
6+
struct UnifiedDiffTests {
7+
private let font = NSFont.monospacedSystemFont(ofSize: 12, weight: .regular)
8+
9+
private let gitDiff = """
10+
diff --git a/Package.resolved b/Package.resolved
11+
index 9d8b2ce4..be58c69d 100644
12+
--- a/Package.resolved
13+
+++ b/Package.resolved
14+
@@ -24,8 +24,8 @@
15+
"kind" : "remoteSourceControl",
16+
- "revision" : "5a5e994ad75768bd3593708016c3af19ef3edb12",
17+
- "version" : "1.2.8"
18+
+ "revision" : "bf2ad909ca6032adec835f6cfb1ecfc5b66d36a3",
19+
+ "version" : "1.2.9"
20+
}
21+
"""
22+
23+
private func attr(_ s: NSAttributedString, _ key: NSAttributedString.Key, at text: String) -> Any? {
24+
let loc = (s.string as NSString).range(of: text).location
25+
guard loc != NSNotFound else { return nil }
26+
return s.attribute(key, at: loc, effectiveRange: nil)
27+
}
28+
29+
@Test("git diff output is detected; line-numbered D1F diffs are not")
30+
func detection() {
31+
#expect(CodeBlockHighlighter.looksLikeUnifiedDiff(gitDiff))
32+
#expect(!CodeBlockHighlighter.looksLikeUnifiedDiff("12 -\told\n12 +\tnew"))
33+
}
34+
35+
@Test("- lines red, + lines green, context plain, @@ distinct, headers bold")
36+
func colors() {
37+
let s = CodeBlockHighlighter.highlight(code: gitDiff, language: "diff", font: font)
38+
let removed = attr(s, .foregroundColor, at: "- \"version\" : \"1.2.8\"") as? NSColor
39+
let added = attr(s, .foregroundColor, at: "+ \"version\" : \"1.2.9\"") as? NSColor
40+
let context = attr(s, .foregroundColor, at: "\"kind\"") as? NSColor
41+
let hunk = attr(s, .foregroundColor, at: "@@ -24") as? NSColor
42+
#expect(removed != nil && added != nil && removed != added)
43+
#expect(context == CodeBlockTheme.text)
44+
#expect(removed != context && added != context)
45+
#expect(hunk != context && hunk != removed && hunk != added)
46+
#expect(attr(s, .backgroundColor, at: "- \"version\"") != nil)
47+
#expect(attr(s, .backgroundColor, at: "\"kind\"") == nil)
48+
let bold = NSFont.monospacedSystemFont(ofSize: 12, weight: .bold)
49+
#expect(attr(s, .font, at: "--- a/") as? NSFont == bold)
50+
#expect(attr(s, .font, at: "+++ b/") as? NSFont == bold)
51+
// The "--- a/" header must not get removed-line red.
52+
#expect(attr(s, .foregroundColor, at: "--- a/") as? NSColor != removed)
53+
}
54+
55+
@Test("isUnifiedDiffLine accepts diff lines and rejects log lines")
56+
func lineClassifier() {
57+
#expect(CodeBlockHighlighter.isUnifiedDiffLine("+x"))
58+
#expect(CodeBlockHighlighter.isUnifiedDiffLine(" ctx"))
59+
#expect(CodeBlockHighlighter.isUnifiedDiffLine("@@ -1 +1 @@"))
60+
#expect(CodeBlockHighlighter.isUnifiedDiffLine("index abc..def 100644"))
61+
#expect(!CodeBlockHighlighter.isUnifiedDiffLine("[18:52:01] 🔧 xcode(build)"))
62+
#expect(!CodeBlockHighlighter.isUnifiedDiffLine(""))
63+
}
64+
}

0 commit comments

Comments
 (0)