@@ -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}
0 commit comments