summaryrefslogtreecommitdiff
path: root/Hutch/Views/Repositories/DiffView.swift
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-03-19 01:53:46 -0500
committerChristian Cleberg <[email protected]>2026-03-19 01:53:46 -0500
commit1e3c748119c6e9eec27f02146f17ea0302ff648a (patch)
tree6a5a7c6d22a9022b1b3daa3710768d12981ca0ed /Hutch/Views/Repositories/DiffView.swift
parent441b69afae30ee6b662be38004fd7b5de1e47302 (diff)
downloadhutch-1e3c748119c6e9eec27f02146f17ea0302ff648a.tar.gz
hutch-1e3c748119c6e9eec27f02146f17ea0302ff648a.tar.bz2
hutch-1e3c748119c6e9eec27f02146f17ea0302ff648a.zip
feat: add inbox threads and move builds under more
Diffstat (limited to 'Hutch/Views/Repositories/DiffView.swift')
-rw-r--r--Hutch/Views/Repositories/DiffView.swift147
1 files changed, 141 insertions, 6 deletions
diff --git a/Hutch/Views/Repositories/DiffView.swift b/Hutch/Views/Repositories/DiffView.swift
index 4380e22..4b8e512 100644
--- a/Hutch/Views/Repositories/DiffView.swift
+++ b/Hutch/Views/Repositories/DiffView.swift
@@ -9,8 +9,73 @@ struct DiffView: View {
let diff: String
var body: some View {
- let lines = normalizedDiff.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)
+
+ 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)
@@ -19,13 +84,83 @@ struct DiffView: View {
.font(.system(.caption, design: .monospaced))
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color(.secondarySystemBackground))
- .clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
}
+}
- private var normalizedDiff: String {
- diff
- .replacingOccurrences(of: "\r\n", with: "\n")
- .replacingOccurrences(of: "\r", with: "\n")
+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
}
}