~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/knotless

git that lives in your Atmosphere account

Forked from chadtmiller.com/knotless

0
0
232 lines · 8.7 KB Raw History
import SwiftUI

/// A pull's diff: every file listed first, each file's rows asked for only as
/// it comes on screen, as the pages do.
struct FilesView: View {
    let repo: RepoRef
    let uri: String

    @Environment(Account.self) private var account
    @State private var reviewing = false

    var body: some View {
        Loading(load: { [repo, uri] in
            let thread: PullThread = try await Client.shared.query(
                "getThread", ["repo": repo.uri, "uri": uri, "filesOnly": "true"])
            return thread
        }) { thread in
            let files = thread.changes?.files ?? []
            ScrollView {
                LazyVStack(alignment: .leading, spacing: 0, pinnedViews: [.sectionHeaders]) {
                    ForEach(files) { file in
                        FileSection(repo: repo, uri: uri, file: file)
                    }
                }
            }
            .safeAreaInset(edge: .bottom) {
                if account.signedIn, thread.item.state == "open" {
                    ReviewButton { reviewing = true }.padding(.bottom, Space.s2)
                }
            }
            .sheet(isPresented: $reviewing) {
                ReviewSheet(repo: repo, uri: uri, author: thread.item.author.did) {}
            }
            .navigationSubtitleCompat(added: files.reduce(0) { $0 + $1.added }, removed: files.reduce(0) { $0 + $1.removed })
        }
        .navigationTitle("Files changed")
        .navigationBarTitleDisplayMode(.inline)
    }
}

private extension View {
    func navigationSubtitleCompat(added: Int, removed: Int) -> some View {
        toolbar {
            ToolbarItem(placement: .topBarTrailing) {
                HStack(spacing: Space.s1) {
                    Text("+\(added)").foregroundStyle(Color.success)
                    Text("-\(removed)").foregroundStyle(Color.danger)
                }
                .font(.footnote.monospacedDigit())
                .fixedSize()
            }
        }
    }
}

struct FileSection: View {
    let repo: RepoRef
    /// The pull whose rows to ask for, if they aren't in `file`.
    var uri: String?
    let file: DiffFile

    struct Line: Identifiable {
        let id: Int
        let row: DiffRow
        let text: AttributedString
    }

    @State private var open = true
    @State private var lines: [Line]?
    @State private var note: String?
    @State private var failed = false

    var body: some View {
        Section {
            if open {
                if let lines {
                    if lines.isEmpty {
                        Text(note ?? "No changes to show.").font(.footnote).foregroundStyle(.secondary).padding()
                    } else {
                        DiffLines(lines: lines)
                    }
                } else if failed {
                    VStack(alignment: .leading, spacing: Space.s2) {
                        Text("This file's diff didn't load.").foregroundStyle(.secondary)
                        Button("Try again") { failed = false }
                    }
                    .font(.footnote)
                    .padding()
                } else {
                    // A row that leaves the screen cancels this, and asks again
                    // when it comes back.
                    ProgressView().frame(maxWidth: .infinity).padding().task { await load() }
                }
            }
        } header: {
            Button { open.toggle() } label: {
                HStack(spacing: Space.s3) {
                    Oct(name: "chevron-down", size: Icon.md)
                        .rotationEffect(open ? .zero : .degrees(-90))
                        .foregroundStyle(.secondary)
                    Text(file.path).font(.subheadline.monospaced()).lineLimit(1).truncationMode(.head)
                        .foregroundStyle(.primary)
                    Spacer(minLength: 4)
                    Text("+\(file.added)").foregroundStyle(Color.success)
                    Text("-\(file.removed)").foregroundStyle(Color.danger)
                }
                .font(.footnote.monospacedDigit())
                .padding(.horizontal, Space.s4)
                .frame(minHeight: Size.fileHeader)
                .background(Color.raised)
                .overlay(alignment: .top) { Color.line.frame(height: 0.5) }
                .overlay(alignment: .bottom) { Color.line.frame(height: 0.5) }
            }
            .buttonStyle(.plain)
            .accessibilityValue(open ? "Open" : "Closed")
        }
    }

    private func load() async {
        var source = file
        if source.rows.isEmpty, source.note == nil, let uri {
            do {
                let answer: DiffAnswer = try await Client.shared.query(
                    "getPullDiff", ["repo": repo.uri, "uri": uri, "paths": file.path])
                guard let found = answer.files.first else {
                    failed = true
                    return
                }
                source = found
            } catch is CancellationError {
                return
            } catch let error as URLError where error.code == .cancelled {
                return
            } catch {
                failed = true
                return
            }
        }
        note = source.note
        let rows = source.rows
        let code = rows.filter(\.line).map(\.text)
        let painted = Highlight.paint(lines: code, ranges: source.ranges)
        var next = 0
        var out: [Line] = []
        out.reserveCapacity(rows.count)
        for (i, row) in rows.enumerated() {
            if row.line {
                out.append(Line(id: i, row: row, text: painted[next]))
                next += 1
            } else {
                out.append(Line(id: i, row: row, text: AttributedString(row.text)))
            }
        }
        lines = out
    }
}

/// A file's diff, each line on one row whatever its length, its indent kept. The
/// code scrolls sideways under numbers that stay put, as a file's lines do.
///
/// Whole columns, not lazy ones: a lazy stack re-estimating a file's rows as they
/// arrive leaves a gap above them.
private struct DiffLines: View {
    let lines: [FileSection.Line]

    /// Every row a set height, so the two columns line up whatever a line holds.
    @ScaledMetric(relativeTo: .caption) private var codeHeight = 20
    @ScaledMetric(relativeTo: .caption) private var hunkHeight = 28
    @State private var width: CGFloat = 0

    var body: some View {
        HStack(alignment: .top, spacing: 0) {
            VStack(spacing: 0) {
                ForEach(lines) { line in
                    HStack(spacing: Space.s1_5) {
                        Text(number(line.row)).frame(width: 34, alignment: .trailing).foregroundStyle(.secondary)
                        Text(marker(line.row)).foregroundStyle(line.row.kind == "a" ? Color.success : Color.danger)
                            .accessibilityHidden(true)
                    }
                    .padding(.horizontal, Space.s1_5)
                    .frame(height: height(line.row))
                    .background(tint(line.row))
                }
            }
            .fixedSize(horizontal: true, vertical: false)
            ScrollView(.horizontal) {
                VStack(alignment: .leading, spacing: 0) {
                    ForEach(lines) { line in
                        Text(line.text)
                            .foregroundStyle(line.row.line ? Color.fg : Color.accent)
                            .lineLimit(1)
                            .fixedSize()
                            .padding(.trailing, Space.s4)
                            // The tint reaches the screen's edge on a short
                            // line, and the widest line's end on a long one.
                            .frame(minWidth: width, maxWidth: .infinity, minHeight: height(line.row), maxHeight: height(line.row), alignment: .leading)
                            .background(tint(line.row))
                    }
                }
                .fixedSize(horizontal: true, vertical: false)
            }
            .onGeometryChange(for: CGFloat.self, of: \.size.width) { width = $0 }
        }
        .font(.caption.monospaced())
    }

    private func height(_ row: DiffRow) -> CGFloat {
        row.line ? codeHeight : hunkHeight
    }

    private func tint(_ row: DiffRow) -> Color {
        switch row.kind {
        case "a": Color.success.opacity(0.14)
        case "d": Color.danger.opacity(0.12)
        case "h": Color.accent.opacity(0.12)
        default: .clear
        }
    }

    private func marker(_ row: DiffRow) -> String {
        switch row.kind {
        case "a": "+"
        case "d": "-"
        default: " "
        }
    }

    private func number(_ row: DiffRow) -> String {
        guard row.line else { return "" }
        return (row.kind == "d" ? row.old : row.new ?? row.old).map(String.init) ?? ""
    }
}