4c0f55c32d
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
69 lines
2.9 KiB
Swift
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
|
|
}
|
|
}
|