diff options
| author | Christian Cleberg <[email protected]> | 2026-04-12 19:22:51 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-04-12 19:22:51 -0500 |
| commit | eb1a5fe236d40d1ccd0901bd458167973a878c82 (patch) | |
| tree | 42b3774878dfeec3df4ace1c75a2ce8d50a5db81 /Hutch | |
| parent | 16b56544ad5bfe39e1f58d025646988c34717033 (diff) | |
| download | hutch-eb1a5fe236d40d1ccd0901bd458167973a878c82.tar.gz hutch-eb1a5fe236d40d1ccd0901bd458167973a878c82.tar.bz2 hutch-eb1a5fe236d40d1ccd0901bd458167973a878c82.zip | |
fix: improve inbox patch rendering and diff collapsing
Fixes: https://todo.sr.ht/~ccleberg/hutch/22
Implements: https://todo.sr.ht/~ccleberg/hutch/35
Diffstat (limited to 'Hutch')
| -rw-r--r-- | Hutch/Views/Inbox/ThreadDetailView.swift | 7 | ||||
| -rw-r--r-- | Hutch/Views/Repositories/DiffView.swift | 114 |
2 files changed, 111 insertions, 10 deletions
diff --git a/Hutch/Views/Inbox/ThreadDetailView.swift b/Hutch/Views/Inbox/ThreadDetailView.swift index 03aa779..cdb0104 100644 --- a/Hutch/Views/Inbox/ThreadDetailView.swift +++ b/Hutch/Views/Inbox/ThreadDetailView.swift @@ -266,11 +266,8 @@ private struct InboxMessageRow: View { .frame(maxWidth: .infinity, alignment: .leading) .fixedSize(horizontal: false, vertical: true) case .diff(let diff): - ScrollView(.horizontal) { - DiffView(diff: diff) - .textSelection(.enabled) - .frame(maxWidth: .infinity, alignment: .leading) - } + DiffView(diff: diff) + .textSelection(.enabled) } } } diff --git a/Hutch/Views/Repositories/DiffView.swift b/Hutch/Views/Repositories/DiffView.swift index a370dfb..f105ff0 100644 --- a/Hutch/Views/Repositories/DiffView.swift +++ b/Hutch/Views/Repositories/DiffView.swift @@ -76,16 +76,118 @@ private struct DiffBlockView: View { let lines: [String] var body: some View { + let hunks = DiffHunk.split(lines: lines) + LazyVStack(alignment: .leading, spacing: 0) { + ForEach(hunks) { hunk in + DiffHunkView(hunk: hunk) + } + } + .font(.system(.caption, design: .monospaced)) + .background(Color(.secondarySystemBackground)) + } +} + +private struct DiffHunk: Identifiable { + let id: Int + let header: String? + let lines: [String] + let isFileHeader: Bool + + static func split(lines: [String]) -> [DiffHunk] { + var hunks: [DiffHunk] = [] + var current: [String] = [] + var hunkIndex = 0 + var headerLines: [String] = [] + var passedFirstHunk = false + + for line in lines { + if line.hasPrefix("@@") { + if !passedFirstHunk { + // Collect file header lines before first hunk + if !current.isEmpty { + headerLines = current + hunks.append(DiffHunk(id: hunkIndex, header: nil, lines: headerLines, isFileHeader: true)) + hunkIndex += 1 + } + current = [line] + passedFirstHunk = true + } else { + // End previous hunk, start new one + if !current.isEmpty { + let header = current.first + hunks.append(DiffHunk(id: hunkIndex, header: header, lines: current, isFileHeader: false)) + hunkIndex += 1 + } + current = [line] + } + } else { + current.append(line) + } + } + + if !current.isEmpty { + if passedFirstHunk { + let header = current.first(where: { $0.hasPrefix("@@") }) ?? current.first + hunks.append(DiffHunk(id: hunkIndex, header: header, lines: current, isFileHeader: false)) + } else { + hunks.append(DiffHunk(id: hunkIndex, header: nil, lines: current, isFileHeader: true)) + } + } + + return hunks + } +} + +private struct DiffHunkView: View { + let hunk: DiffHunk + @State private var isExpanded = true + + private var isCollapsible: Bool { + !hunk.isFileHeader && hunk.lines.count > 1 + } + + var body: some View { + if isCollapsible { + Button { + withAnimation(.snappy(duration: 0.2)) { + isExpanded.toggle() + } + } label: { + HStack(spacing: 6) { + Image(systemName: isExpanded ? "chevron.down" : "chevron.right") + .font(.system(size: 8, weight: .bold, design: .monospaced)) + .foregroundStyle(.secondary) + .frame(width: 10) + + Text(hunk.header ?? "") + .foregroundStyle(.secondary) + .lineLimit(1) + } + .padding(.horizontal, 8) + .padding(.vertical, 4) + .frame(maxWidth: .infinity, alignment: .leading) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .background(Color(.systemBackground).opacity(0.5)) + + if isExpanded { + hunkContent(lines: hunk.lines.dropFirst().map { $0 }) + } + } else { + hunkContent(lines: hunk.lines) + } + } + + @ViewBuilder + private func hunkContent(lines: [String]) -> some View { ScrollView(.horizontal, showsIndicators: false) { - LazyVStack(alignment: .leading, spacing: 0) { + VStack(alignment: .leading, spacing: 0) { ForEach(Array(lines.enumerated()), id: \.offset) { _, line in DiffLineView(line: line) } } - .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) } - .font(.system(.caption, design: .monospaced)) - .background(Color(.secondarySystemBackground)) } } @@ -171,8 +273,10 @@ private struct DiffLineView: View { var body: some View { Text(line.isEmpty ? " " : line) - .frame(maxWidth: .infinity, alignment: .leading) + .fixedSize(horizontal: true, vertical: false) .padding(.horizontal, 8) + .padding(.vertical, 1) + .frame(maxWidth: .infinity, alignment: .leading) .background(backgroundColor) .foregroundStyle(foregroundColor) .fontWeight(isHeader ? .semibold : .regular) |
