Commit 34e12c4d2c

34e12c4d2cb3fbdbee023c79aa183e055e1c4008

parent: 2790ec0678

Unsigned

cmc <hello@cleberg.net> · 2026-04-13 00:22 UTC

fix: improve inbox patch rendering and diff collapsing

Fixes: https://todo.sr.ht/~ccleberg/hutch/22
Implements: https://todo.sr.ht/~ccleberg/hutch/35

Layout: unified · split

Hutch/Views/Inbox/ThreadDetailView.swift +2 −5
@@ -266,11 +266,8 @@ private struct InboxMessageRow: View {
266266 .frame(maxWidth: .infinity, alignment: .leading)
267267 .fixedSize(horizontal: false, vertical: true)
268268 case .diff(let diff):
269 ScrollView(.horizontal) {
270 DiffView(diff: diff)
271 .textSelection(.enabled)
272 .frame(maxWidth: .infinity, alignment: .leading)
273 }
269 DiffView(diff: diff)
270 .textSelection(.enabled)
274271 }
275272 }
276273 }
Hutch/Views/Repositories/DiffView.swift +109 −5
@@ -76,16 +76,118 @@ private struct DiffBlockView: View {
7676 let lines: [String]
7777
7878 var body: some View {
79 let hunks = DiffHunk.split(lines: lines)
80 LazyVStack(alignment: .leading, spacing: 0) {
81 ForEach(hunks) { hunk in
82 DiffHunkView(hunk: hunk)
83 }
84 }
85 .font(.system(.caption, design: .monospaced))
86 .background(Color(.secondarySystemBackground))
87 }
88}
89
90private struct DiffHunk: Identifiable {
91 let id: Int
92 let header: String?
93 let lines: [String]
94 let isFileHeader: Bool
95
96 static func split(lines: [String]) -> [DiffHunk] {
97 var hunks: [DiffHunk] = []
98 var current: [String] = []
99 var hunkIndex = 0
100 var headerLines: [String] = []
101 var passedFirstHunk = false
102
103 for line in lines {
104 if line.hasPrefix("@@") {
105 if !passedFirstHunk {
106 // Collect file header lines before first hunk
107 if !current.isEmpty {
108 headerLines = current
109 hunks.append(DiffHunk(id: hunkIndex, header: nil, lines: headerLines, isFileHeader: true))
110 hunkIndex += 1
111 }
112 current = [line]
113 passedFirstHunk = true
114 } else {
115 // End previous hunk, start new one
116 if !current.isEmpty {
117 let header = current.first
118 hunks.append(DiffHunk(id: hunkIndex, header: header, lines: current, isFileHeader: false))
119 hunkIndex += 1
120 }
121 current = [line]
122 }
123 } else {
124 current.append(line)
125 }
126 }
127
128 if !current.isEmpty {
129 if passedFirstHunk {
130 let header = current.first(where: { $0.hasPrefix("@@") }) ?? current.first
131 hunks.append(DiffHunk(id: hunkIndex, header: header, lines: current, isFileHeader: false))
132 } else {
133 hunks.append(DiffHunk(id: hunkIndex, header: nil, lines: current, isFileHeader: true))
134 }
135 }
136
137 return hunks
138 }
139}
140
141private struct DiffHunkView: View {
142 let hunk: DiffHunk
143 @State private var isExpanded = true
144
145 private var isCollapsible: Bool {
146 !hunk.isFileHeader && hunk.lines.count > 1
147 }
148
149 var body: some View {
150 if isCollapsible {
151 Button {
152 withAnimation(.snappy(duration: 0.2)) {
153 isExpanded.toggle()
154 }
155 } label: {
156 HStack(spacing: 6) {
157 Image(systemName: isExpanded ? "chevron.down" : "chevron.right")
158 .font(.system(size: 8, weight: .bold, design: .monospaced))
159 .foregroundStyle(.secondary)
160 .frame(width: 10)
161
162 Text(hunk.header ?? "")
163 .foregroundStyle(.secondary)
164 .lineLimit(1)
165 }
166 .padding(.horizontal, 8)
167 .padding(.vertical, 4)
168 .frame(maxWidth: .infinity, alignment: .leading)
169 .contentShape(Rectangle())
170 }
171 .buttonStyle(.plain)
172 .background(Color(.systemBackground).opacity(0.5))
173
174 if isExpanded {
175 hunkContent(lines: hunk.lines.dropFirst().map { $0 })
176 }
177 } else {
178 hunkContent(lines: hunk.lines)
179 }
180 }
181
182 @ViewBuilder
183 private func hunkContent(lines: [String]) -> some View {
79184 ScrollView(.horizontal, showsIndicators: false) {
80 LazyVStack(alignment: .leading, spacing: 0) {
185 VStack(alignment: .leading, spacing: 0) {
81186 ForEach(Array(lines.enumerated()), id: \.offset) { _, line in
82187 DiffLineView(line: line)
83188 }
84189 }
85 .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
86190 }
87 .font(.system(.caption, design: .monospaced))
88 .background(Color(.secondarySystemBackground))
89191 }
90192}
91193
@@ -171,8 +273,10 @@ private struct DiffLineView: View {
171273
172274 var body: some View {
173275 Text(line.isEmpty ? " " : line)
174 .frame(maxWidth: .infinity, alignment: .leading)
276 .fixedSize(horizontal: true, vertical: false)
175277 .padding(.horizontal, 8)
278 .padding(.vertical, 1)
279 .frame(maxWidth: .infinity, alignment: .leading)
176280 .background(backgroundColor)
177281 .foregroundStyle(foregroundColor)
178282 .fontWeight(isHeader ? .semibold : .regular)