Commit 34e12c4d2c
Unsigned
Layout: unified · split
Hutch/Views/Inbox/ThreadDetailView.swift +2 −5
| @@ -266,11 +266,8 @@ private struct InboxMessageRow: View { | ||
| 266 | 266 | .frame(maxWidth: .infinity, alignment: .leading) |
| 267 | 267 | .fixedSize(horizontal: false, vertical: true) |
| 268 | 268 | 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) | |
| 274 | 271 | } |
| 275 | 272 | } |
| 276 | 273 | } |
Hutch/Views/Repositories/DiffView.swift +109 −5
| @@ -76,16 +76,118 @@ private struct DiffBlockView: View { | ||
| 76 | 76 | let lines: [String] |
| 77 | 77 | |
| 78 | 78 | 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 | ||
| 90 | private 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 | ||
| 141 | private 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 { | |
| 79 | 184 | ScrollView(.horizontal, showsIndicators: false) { |
| 80 | LazyVStack(alignment: .leading, spacing: 0) { | |
| 185 | VStack(alignment: .leading, spacing: 0) { | |
| 81 | 186 | ForEach(Array(lines.enumerated()), id: \.offset) { _, line in |
| 82 | 187 | DiffLineView(line: line) |
| 83 | 188 | } |
| 84 | 189 | } |
| 85 | .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) | |
| 86 | 190 | } |
| 87 | .font(.system(.caption, design: .monospaced)) | |
| 88 | .background(Color(.secondarySystemBackground)) | |
| 89 | 191 | } |
| 90 | 192 | } |
| 91 | 193 | |
| @@ -171,8 +273,10 @@ private struct DiffLineView: View { | ||
| 171 | 273 | |
| 172 | 274 | var body: some View { |
| 173 | 275 | Text(line.isEmpty ? " " : line) |
| 174 | .frame(maxWidth: .infinity, alignment: .leading) | |
| 276 | .fixedSize(horizontal: true, vertical: false) | |
| 175 | 277 | .padding(.horizontal, 8) |
| 278 | .padding(.vertical, 1) | |
| 279 | .frame(maxWidth: .infinity, alignment: .leading) | |
| 176 | 280 | .background(backgroundColor) |
| 177 | 281 | .foregroundStyle(foregroundColor) |
| 178 | 282 | .fontWeight(isHeader ? .semibold : .regular) |