gitbay/Views/MRs/DiffView.swift

5411753023a5e2958f17b523903abce380941107
gitbay-ios/gitbay/Views/MRs/DiffView.swift history · blame · raw

163 lines · 4882 bytes

  1import SwiftUI
  2
  3/// The parsed unified diff, file by file, hunk by hunk. Loads its own
  4/// text so the screen is reachable by value from anywhere.
  5struct DiffView: View {
  6
  7    private let client: GitbayClient
  8    private let repo: String
  9    private let number: Int64
 10    @State private var state: LoadState<UnifiedDiff> = .loading
 11
 12    init(client: GitbayClient, repo: String, number: Int64) {
 13        self.client = client
 14        self.repo = repo
 15        self.number = number
 16    }
 17
 18    var body: some View {
 19        ZStack {
 20            Color.clear
 21            if let diff = state.value, !diff.files.isEmpty {
 22                List {
 23                    ForEach(diff.files) { file in
 24                        FileDiffSection(file: file)
 25                    }
 26                }
 27                .listStyle(.plain)
 28            } else if case .loaded = state {
 29                ContentUnavailableView {
 30                    Label("No changes", systemImage: "plus.forwardslash.minus")
 31                }
 32            }
 33        }
 34        .overlay { LoadStateOverlay(state: state) }
 35        .navigationTitle("Diff")
 36        .navigationBarTitleDisplayMode(.inline)
 37        .task {
 38            do {
 39                let text = try await client.readText(["mr", "diff", repo, String(number)])
 40                state = .loaded(UnifiedDiff.parse(text))
 41            } catch {
 42                state = .from(error)
 43            }
 44        }
 45    }
 46}
 47
 48private struct FileDiffSection: View {
 49
 50    let file: UnifiedDiff.File
 51    @State private var collapsed = false
 52
 53    var body: some View {
 54        Section {
 55            if !collapsed {
 56                if file.isBinary {
 57                    Text("Binary file")
 58                        .font(.caption)
 59                        .foregroundStyle(.secondary)
 60                } else {
 61                    ForEach(file.hunks) { hunk in
 62                        HunkView(hunk: hunk)
 63                    }
 64                }
 65            }
 66        } header: {
 67            Button {
 68                collapsed.toggle()
 69            } label: {
 70                HStack(spacing: 6) {
 71                    Image(systemName: collapsed ? "chevron.right" : "chevron.down")
 72                        .font(.caption2)
 73                    Text(file.displayPath)
 74                        .font(.caption.monospaced().weight(.semibold))
 75                        .lineLimit(1)
 76                        .truncationMode(.head)
 77                    if file.isNew {
 78                        Text("new").font(.caption2).foregroundStyle(.green)
 79                    }
 80                    if file.isDeleted {
 81                        Text("deleted").font(.caption2).foregroundStyle(.red)
 82                    }
 83                    Spacer()
 84                    Text("+\(file.additions)").foregroundStyle(.green).font(.caption2)
 85                    Text("−\(file.deletions)").foregroundStyle(.red).font(.caption2)
 86                }
 87            }
 88            .buttonStyle(.plain)
 89            .textCase(nil)
 90        }
 91    }
 92}
 93
 94private struct HunkView: View {
 95
 96    let hunk: UnifiedDiff.Hunk
 97
 98    var body: some View {
 99        ScrollView(.horizontal) {
100            VStack(alignment: .leading, spacing: 0) {
101                Text(hunk.header)
102                    .font(.caption2.monospaced())
103                    .foregroundStyle(.blue)
104                    .padding(.vertical, 2)
105                ForEach(hunk.lines) { line in
106                    LineView(line: line)
107                }
108            }
109        }
110        .listRowInsets(EdgeInsets(top: 2, leading: 8, bottom: 2, trailing: 8))
111    }
112}
113
114private struct LineView: View {
115
116    let line: UnifiedDiff.Line
117
118    var body: some View {
119        HStack(spacing: 0) {
120            Text(line.oldNumber.map(String.init) ?? "")
121                .frame(width: 34, alignment: .trailing)
122                .foregroundStyle(.tertiary)
123            Text(line.newNumber.map(String.init) ?? "")
124                .frame(width: 34, alignment: .trailing)
125                .foregroundStyle(.tertiary)
126            Text(marker)
127                .frame(width: 14)
128                .foregroundStyle(markerColor)
129            Text(line.text.isEmpty ? " " : line.text)
130                .foregroundStyle(textColor)
131        }
132        .font(.caption2.monospaced())
133        .background(background)
134    }
135
136    private var marker: String {
137        switch line.kind {
138        case .addition: "+"
139        case .deletion: "−"
140        case .context: " "
141        }
142    }
143
144    private var markerColor: Color {
145        switch line.kind {
146        case .addition: .green
147        case .deletion: .red
148        case .context: .clear
149        }
150    }
151
152    private var textColor: Color {
153        .primary
154    }
155
156    private var background: Color {
157        switch line.kind {
158        case .addition: .green.opacity(0.12)
159        case .deletion: .red.opacity(0.12)
160        case .context: .clear
161        }
162    }
163}