import SwiftUI struct TreeView: View { let repo: RepoRef let ref: String? let path: String var body: some View { Loading(load: { [repo, ref, path] in let tree: Tree = try await Client.shared.query( "getTree", ["repo": repo.uri, "ref": ref, "path": path.isEmpty ? nil : path]) return tree.entries ?? [] }) { entries in if entries.isEmpty { ContentUnavailableView("No files here", systemImage: "folder") } else { List { ForEach(entries.sorted { ($0.kind == "tree" ? 0 : 1, $0.name) < ($1.kind == "tree" ? 0 : 1, $1.name) }) { entry in let full = path.isEmpty ? entry.name : path + "/" + entry.name NavigationLink(value: entry.kind == "tree" ? Route.tree(repo, ref: ref, path: full) : Route.file(repo, ref: ref, path: full)) { SwiftUI.Label { Text(entry.name) } icon: { Oct(name: entry.kind == "tree" ? "file-directory-fill" : "file") .foregroundStyle(entry.kind == "tree" ? Color.accent : Color.neutral) } } }.listRowBackground(Color.page) } .listStyle(.plain) } } .navigationTitle(path.isEmpty ? repo.name : (path.split(separator: "/").last.map(String.init) ?? path)) .navigationBarTitleDisplayMode(.inline) } } struct FileView: View { let repo: RepoRef let ref: String? let path: String var body: some View { Loading(load: { [repo, ref, path] in let file: FileAnswer = try await Client.shared.query( "getFile", ["repo": repo.uri, "ref": ref, "path": path]) return file }) { file in if let text = file.text { let lines = text.hasSuffix("\n") ? String(text.dropLast()).components(separatedBy: "\n") : text.components(separatedBy: "\n") CodeLines(lines: lines, painted: Highlight.paint(lines: lines, ranges: file.ranges)) } else { ContentUnavailableView("This file can't be shown here", systemImage: "doc") } } .navigationTitle(path.split(separator: "/").last.map(String.init) ?? path) .navigationBarTitleDisplayMode(.inline) } } /// A file's lines, each on one row whatever its length. The code scrolls sideways /// under numbers that stay put. private struct CodeLines: View { let lines: [String] let painted: [AttributedString] /// Every row the same height, so the two columns line up whatever a line holds. @ScaledMetric(relativeTo: .caption) private var rowHeight = 17 var body: some View { ScrollView { HStack(alignment: .top, spacing: Space.s2) { column(widest: "\(painted.count)", alignment: .trailing) { i in Text("\(i + 1)").foregroundStyle(.secondary) } .padding(.leading, Space.s2) ScrollView(.horizontal) { column(widest: lines.max { $0.count < $1.count } ?? "", alignment: .leading) { i in Text(painted[i]) } .padding(.trailing, Space.s4) } } .padding(.vertical, Space.s2) } } /// Rows made as they come into view. The widest, drawn with no height, holds /// the column at its full width from the start, so it doesn't jump on scrolling. private func column(widest: String, alignment: HorizontalAlignment, @ViewBuilder row: @escaping (Int) -> Text) -> some View { VStack(alignment: alignment, spacing: 0) { Text(widest).frame(height: 0).hidden().accessibilityHidden(true) LazyVStack(alignment: alignment, spacing: 0) { ForEach(painted.indices, id: \.self) { i in row(i).lineLimit(1).fixedSize().frame(height: rowHeight) } } } .font(.caption.monospaced()) .fixedSize(horizontal: true, vertical: false) } }