johnandersen777.bsky.social/knotless
git that lives in your Atmosphere account
Forked from chadtmiller.com/knotless
0
0
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) ?? ""
}
}