diff options
| author | Christian Cleberg <[email protected]> | 2026-03-19 01:53:46 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-03-19 01:53:46 -0500 |
| commit | 1e3c748119c6e9eec27f02146f17ea0302ff648a (patch) | |
| tree | 6a5a7c6d22a9022b1b3daa3710768d12981ca0ed /Hutch/Views/Repositories/DiffView.swift | |
| parent | 441b69afae30ee6b662be38004fd7b5de1e47302 (diff) | |
| download | hutch-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.swift | 147 |
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 } } |
