Files
alexandrev-tibco 4c0f55c32d Say what the difference actually is, and expand from either side
A changed line only said 'changed'. Now the engine classifies it and the view
colours it accordingly: whitespace-only (teal), comment-only (indigo) or real
content (orange), with a legend counting each kind present in the file. Comment
detection uses the syntax inferred from the extension and only claims a comment
change when the code either side of it is identical; an unknown file type claims
nothing, since hiding a real change is worse than showing a cosmetic one.

Within a changed line the differing characters are now highlighted rather than
tinting the whole row, so a 21 -> 18 reads as a one-character edit instead of
looking like a rewritten line.

The tree no longer uses List's outline, which only ever puts a disclosure
triangle on the leading column: rows are flattened with their own expansion
state, so both sides carry a chevron and the tree can be driven from whichever
side you're reading. Expand all / collapse all added to the context menu.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UfhDYRLTpGJSKGP1m3LVJN
2026-07-28 10:08:54 +02:00

69 lines
2.9 KiB
Swift

import Foundation
/// How a file marks comments, so a change can be told apart from a change in
/// what the code actually does.
public struct CommentSyntax: Sendable, Equatable {
public let linePrefixes: [String]
public let blockOpen: String?
public let blockClose: String?
public init(linePrefixes: [String], blockOpen: String? = nil, blockClose: String? = nil) {
self.linePrefixes = linePrefixes
self.blockOpen = blockOpen
self.blockClose = blockClose
}
public static let cLike = CommentSyntax(linePrefixes: ["//"], blockOpen: "/*", blockClose: "*/")
public static let hash = CommentSyntax(linePrefixes: ["#"])
public static let sql = CommentSyntax(linePrefixes: ["--"], blockOpen: "/*", blockClose: "*/")
public static let markup = CommentSyntax(linePrefixes: [], blockOpen: "<!--", blockClose: "-->")
/// Best guess from the file extension. Unknown types get nil, and then a
/// change is never reported as comment-only better to under-claim than to
/// hide a real difference.
public static func forPath(_ path: String) -> CommentSyntax? {
switch (path as NSString).pathExtension.lowercased() {
case "swift", "java", "js", "ts", "jsx", "tsx", "go", "c", "h", "cpp", "hpp",
"cs", "kt", "scala", "rs", "css", "json5", "gradle", "groovy":
return .cLike
case "py", "rb", "sh", "bash", "zsh", "yml", "yaml", "properties", "conf",
"cfg", "ini", "toml", "dockerfile", "makefile", "pl", "r":
return .hash
case "sql":
return .sql
case "xml", "html", "htm", "xsd", "xsl", "xslt", "wsdl", "svg", "vue":
return .markup
default:
return nil
}
}
/// The line with its comments removed. Block comments are handled only when
/// they open and close on the same line a multi-line block would need real
/// parsing, and guessing there risks calling a code change a comment.
func stripping(_ line: String) -> String {
var result = line
if let open = blockOpen, let close = blockClose {
while let start = result.range(of: open),
let end = result.range(of: close, range: start.upperBound..<result.endIndex) {
result.removeSubrange(start.lowerBound..<end.upperBound)
}
}
for prefix in linePrefixes {
if let start = result.range(of: prefix) {
result = String(result[result.startIndex..<start.lowerBound])
break
}
}
return result
}
/// True when the line has nothing but a comment (or nothing at all).
func isEntirelyComment(_ line: String) -> Bool {
let trimmed = line.trimmingCharacters(in: .whitespaces)
guard !trimmed.isEmpty else { return false }
return stripping(line).trimmingCharacters(in: .whitespaces).isEmpty
}
}