import SwiftUI /// The parsed unified diff, file by file, hunk by hunk. Loads its own /// text so the screen is reachable by value from anywhere. struct DiffView: View { private let client: GitbayClient private let repo: String private let number: Int64 @State private var state: LoadState = .loading init(client: GitbayClient, repo: String, number: Int64) { self.client = client self.repo = repo self.number = number } var body: some View { ZStack { Color.clear if let diff = state.value, !diff.files.isEmpty { List { ForEach(diff.files) { file in FileDiffSection(file: file) } } .listStyle(.plain) } else if case .loaded = state { ContentUnavailableView { Label("No changes", systemImage: "plus.forwardslash.minus") } } } .overlay { LoadStateOverlay(state: state) } .navigationTitle("Diff") .navigationBarTitleDisplayMode(.inline) .task { do { let text = try await client.readText(["mr", "diff", repo, String(number)]) state = .loaded(UnifiedDiff.parse(text)) } catch { state = .from(error) } } } } private struct FileDiffSection: View { let file: UnifiedDiff.File @State private var collapsed = false var body: some View { Section { if !collapsed { if file.isBinary { Text("Binary file") .font(.caption) .foregroundStyle(.secondary) } else { ForEach(file.hunks) { hunk in HunkView(hunk: hunk) } } } } header: { Button { collapsed.toggle() } label: { HStack(spacing: 6) { Image(systemName: collapsed ? "chevron.right" : "chevron.down") .font(.caption2) Text(file.displayPath) .font(.caption.monospaced().weight(.semibold)) .lineLimit(1) .truncationMode(.head) if file.isNew { Text("new").font(.caption2).foregroundStyle(.green) } if file.isDeleted { Text("deleted").font(.caption2).foregroundStyle(.red) } Spacer() Text("+\(file.additions)").foregroundStyle(.green).font(.caption2) Text("−\(file.deletions)").foregroundStyle(.red).font(.caption2) } } .buttonStyle(.plain) .textCase(nil) } } } private struct HunkView: View { let hunk: UnifiedDiff.Hunk var body: some View { ScrollView(.horizontal) { VStack(alignment: .leading, spacing: 0) { Text(hunk.header) .font(.caption2.monospaced()) .foregroundStyle(.blue) .padding(.vertical, 2) ForEach(hunk.lines) { line in LineView(line: line) } } } .listRowInsets(EdgeInsets(top: 2, leading: 8, bottom: 2, trailing: 8)) } } private struct LineView: View { let line: UnifiedDiff.Line var body: some View { HStack(spacing: 0) { Text(line.oldNumber.map(String.init) ?? "") .frame(width: 34, alignment: .trailing) .foregroundStyle(.tertiary) Text(line.newNumber.map(String.init) ?? "") .frame(width: 34, alignment: .trailing) .foregroundStyle(.tertiary) Text(marker) .frame(width: 14) .foregroundStyle(markerColor) Text(line.text.isEmpty ? " " : line.text) .foregroundStyle(textColor) } .font(.caption2.monospaced()) .background(background) } private var marker: String { switch line.kind { case .addition: "+" case .deletion: "−" case .context: " " } } private var markerColor: Color { switch line.kind { case .addition: .green case .deletion: .red case .context: .clear } } private var textColor: Color { .primary } private var background: Color { switch line.kind { case .addition: .green.opacity(0.12) case .deletion: .red.opacity(0.12) case .context: .clear } } }