summaryrefslogtreecommitdiff
path: root/Hutch/Views/Repositories/DiffView.swift
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-03-19 15:18:38 -0500
committerChristian Cleberg <[email protected]>2026-03-19 15:18:38 -0500
commit659c76df9ff3926cb68886c16167808c23bd8f75 (patch)
tree572bef546fcca19ad37bc1aa7cfb153eb2bf8eb7 /Hutch/Views/Repositories/DiffView.swift
parented48e72520c51e4635cd3840dbc569bdd74c950f (diff)
parent6ba4e967d5dfb5d3c7bb97a0f2662f3180595563 (diff)
downloadhutch-2.tar.gz
hutch-2.tar.bz2
hutch-2.zip
v2.0: Merge branch 'dev'v2
Diffstat (limited to 'Hutch/Views/Repositories/DiffView.swift')
-rw-r--r--Hutch/Views/Repositories/DiffView.swift157
1 files changed, 150 insertions, 7 deletions
diff --git a/Hutch/Views/Repositories/DiffView.swift b/Hutch/Views/Repositories/DiffView.swift
index b1db464..4b8e512 100644
--- a/Hutch/Views/Repositories/DiffView.swift
+++ b/Hutch/Views/Repositories/DiffView.swift
@@ -9,14 +9,158 @@ struct DiffView: View {
let diff: String
var body: some View {
- let lines = diff.components(separatedBy: "\n")
+ VStack(alignment: .leading, spacing: 12) {
+ ForEach(fileSections) { section in
+ DiffFileSectionView(section: section)
+ }
+ }
+ }
+
+ private var fileSections: [DiffFileSection] {
+ DiffFileSection.parse(from: normalizedDiff)
+ }
+
+ private var normalizedDiff: String {
+ diff
+ .replacingOccurrences(of: "\r\n", with: "\n")
+ .replacingOccurrences(of: "\r", with: "\n")
+ }
+}
+
+private struct DiffFileSectionView: View {
+ let section: DiffFileSection
+ @State private var isExpanded = true
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 0) {
+ Button {
+ isExpanded.toggle()
+ } label: {
+ HStack(spacing: 10) {
+ Image(systemName: isExpanded ? "chevron.down" : "chevron.right")
+ .font(.caption.weight(.semibold))
+ .foregroundStyle(.secondary)
+ .frame(width: 12)
+
+ Text(section.filename)
+ .font(.subheadline.weight(.semibold))
+ .foregroundStyle(.primary)
+ .lineLimit(1)
- LazyVStack(alignment: .leading, spacing: 0) {
+ Spacer(minLength: 8)
+
+ Text(section.changeSummary)
+ .font(.caption.weight(.medium))
+ .foregroundStyle(.secondary)
+ }
+ .padding(.horizontal, 10)
+ .padding(.vertical, 8)
+ .contentShape(Rectangle())
+ }
+ .buttonStyle(.plain)
+ .background(Color(.tertiarySystemBackground))
+
+ if isExpanded {
+ DiffBlockView(lines: section.lines)
+ }
+ }
+ .clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
+ .overlay {
+ RoundedRectangle(cornerRadius: 8, style: .continuous)
+ .strokeBorder(Color.primary.opacity(0.06))
+ }
+ }
+}
+
+private struct DiffBlockView: View {
+ let lines: [String]
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 0) {
ForEach(Array(lines.enumerated()), id: \.offset) { _, line in
DiffLineView(line: line)
}
}
- .font(.caption.monospaced())
+ .font(.system(.caption, design: .monospaced))
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .background(Color(.secondarySystemBackground))
+ }
+}
+
+private struct DiffFileSection: Identifiable {
+ let id: String
+ let filename: String
+ let lines: [String]
+ let additions: Int
+ let deletions: Int
+
+ var changeSummary: String {
+ "+\(additions) -\(deletions)"
+ }
+
+ static func parse(from diff: String) -> [DiffFileSection] {
+ let lines = diff.components(separatedBy: "\n")
+ guard !lines.isEmpty else { return [] }
+
+ let boundaries = lines.enumerated().compactMap { index, line in
+ line.hasPrefix("diff --git ") ? index : nil
+ }
+
+ guard !boundaries.isEmpty else {
+ let section = makeSection(lines: lines, fallbackIndex: 0)
+ return section.lines.isEmpty ? [] : [section]
+ }
+
+ var sections: [DiffFileSection] = []
+ for (position, startIndex) in boundaries.enumerated() {
+ let endIndex = position + 1 < boundaries.count ? boundaries[position + 1] : lines.count
+ let sectionLines = Array(lines[startIndex..<endIndex])
+ let section = makeSection(lines: sectionLines, fallbackIndex: position)
+ if !section.lines.isEmpty {
+ sections.append(section)
+ }
+ }
+ return sections
+ }
+
+ private static func makeSection(lines: [String], fallbackIndex: Int) -> DiffFileSection {
+ let filename = fileName(from: lines) ?? "File \(fallbackIndex + 1)"
+ let additions = lines.filter { $0.hasPrefix("+") && !$0.hasPrefix("+++") }.count
+ let deletions = lines.filter { $0.hasPrefix("-") && !$0.hasPrefix("---") }.count
+ return DiffFileSection(
+ id: "\(fallbackIndex)-\(filename)",
+ filename: filename,
+ lines: lines,
+ additions: additions,
+ deletions: deletions
+ )
+ }
+
+ private static func fileName(from lines: [String]) -> String? {
+ if let diffHeader = lines.first(where: { $0.hasPrefix("diff --git ") }) {
+ let parts = diffHeader.split(separator: " ")
+ if let rhs = parts.last, rhs.hasPrefix("b/") {
+ return String(rhs.dropFirst(2))
+ }
+ }
+
+ if let plusHeader = lines.first(where: { $0.hasPrefix("+++ ") }) {
+ let path = String(plusHeader.dropFirst(4))
+ if path.hasPrefix("b/") {
+ return String(path.dropFirst(2))
+ }
+ return path
+ }
+
+ if let minusHeader = lines.first(where: { $0.hasPrefix("--- ") }) {
+ let path = String(minusHeader.dropFirst(4))
+ if path.hasPrefix("a/") {
+ return String(path.dropFirst(2))
+ }
+ return path
+ }
+
+ return nil
}
}
@@ -27,7 +171,6 @@ private struct DiffLineView: View {
Text(line.isEmpty ? " " : line)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 8)
- .padding(.vertical, 1)
.background(backgroundColor)
.foregroundStyle(foregroundColor)
.fontWeight(isHeader ? .semibold : .regular)
@@ -47,9 +190,9 @@ private struct DiffLineView: View {
switch kind {
case .added: .green.opacity(0.15)
case .removed: .red.opacity(0.15)
- case .hunk: .gray.opacity(0.12)
- case .fileHeader: .gray.opacity(0.08)
- case .meta: .gray.opacity(0.05)
+ case .hunk: .clear
+ case .fileHeader: .clear
+ case .meta: .clear
case .context: .clear
}
}