summaryrefslogtreecommitdiff
path: root/Hutch/Views/Repositories/FileTreeView.swift
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-04-01 13:50:09 -0500
committerChristian Cleberg <[email protected]>2026-04-01 13:50:09 -0500
commite84e5543be9be548784859f4b082b9d7a3122f15 (patch)
tree0aa941558514adc1be7cdf5140c57bb3781ee1ad /Hutch/Views/Repositories/FileTreeView.swift
parent109b0ea7e859de2d8b5a4dc22bf0472fd3b08eaa (diff)
downloadhutch-e84e5543be9be548784859f4b082b9d7a3122f15.tar.gz
hutch-e84e5543be9be548784859f4b082b9d7a3122f15.tar.bz2
hutch-e84e5543be9be548784859f4b082b9d7a3122f15.zip
Rewrite file viewer with pinned gutter, syntax highlighting, and toolbar
Replace the SwiftUI-only CodeFileTextView with a UIViewRepresentable backed by CodeFileUIView. The previous approach could not support a fixed line number gutter while independently scrolling code content horizontally. Changes: - Gutter is pinned outside the horizontal scroll view and never scrolls left - Vertical scroll is handled by a single outer UIScrollView shared by both the gutter and code content, keeping them in sync - Line numbers are right-aligned in a fixed-width gutter calculated from the total line count - Syntax highlighting via Splash for .swift files, with plain monospaced rendering as fallback for unsupported file types - Wrap toggle persisted via AppStorage (wrapRepositoryFileLines) - Bottom toolbar replaces the floating Share button, adding Copy All and Wrap toggle actions - Copy All shows a brief checkmark confirmation that resets after 2 seconds - Font fixed at SFMono-Regular 12pt, not scaled with Dynamic Type Implements: https://todo.sr.ht/~ccleberg/Hutch/7
Diffstat (limited to 'Hutch/Views/Repositories/FileTreeView.swift')
-rw-r--r--Hutch/Views/Repositories/FileTreeView.swift566
1 files changed, 516 insertions, 50 deletions
diff --git a/Hutch/Views/Repositories/FileTreeView.swift b/Hutch/Views/Repositories/FileTreeView.swift
index 830fc36..8bedb73 100644
--- a/Hutch/Views/Repositories/FileTreeView.swift
+++ b/Hutch/Views/Repositories/FileTreeView.swift
@@ -1,4 +1,8 @@
+import Splash
import SwiftUI
+import UIKit
+
+private typealias ViewColor = SwiftUI.Color
struct FileTreeView: View {
let repository: RepositorySummary
@@ -9,7 +13,7 @@ struct FileTreeView: View {
var body: some View {
Group {
if let viewModel {
- FileTreeContentView(repository: repository, viewModel: viewModel)
+ FileTreeContentView(viewModel: viewModel)
} else {
SRHTLoadingStateView(message: "Loading files…")
}
@@ -33,10 +37,14 @@ struct FileTreeView: View {
// MARK: - Content View
private struct FileTreeContentView: View {
- let repository: RepositorySummary
let viewModel: FileTreeViewModel
+ @AppStorage(AppStorageKeys.wrapRepositoryFileLines) private var wrapRepositoryFileLines = false
@State private var showRefPicker = false
+ @State private var showFileShareSheet = false
+ @State private var showShareUnavailableAlert = false
+ @State private var didCopyFileContents = false
+ @State private var copyResetTask: Task<Void, Never>?
var body: some View {
VStack(spacing: 0) {
@@ -64,20 +72,11 @@ private struct FileTreeContentView: View {
get: { viewModel.error },
set: { viewModel.error = $0 }
))
- .refreshable {
- await viewModel.loadRootTree()
+ .onChange(of: viewModel.viewingEntry?.name) { _, _ in
+ resetCopyConfirmation()
}
}
- private var shareURL: URL? {
- guard let viewingEntry = viewModel.viewingEntry else { return nil }
- return SRHTWebURL.file(
- repository: repository,
- revspec: viewModel.revspec,
- path: currentFilePath(for: viewingEntry)
- )
- }
-
private var revspecLabel: String {
let revspec = viewModel.revspec
if revspec == "HEAD" {
@@ -138,13 +137,6 @@ private struct FileTreeContentView: View {
.background(.bar)
}
- private func currentFilePath(for entry: TreeEntry) -> String {
- let directoryComponents = viewModel.navStack
- .dropFirst()
- .map(\.name)
- return (directoryComponents + [entry.name]).joined(separator: "/")
- }
-
// MARK: - Content Area
@ViewBuilder
@@ -178,6 +170,33 @@ private struct FileTreeContentView: View {
}
}
+ private func shareFileContents(_ text: String) {
+ if text.isEmpty {
+ showShareUnavailableAlert = true
+ } else {
+ showFileShareSheet = true
+ }
+ }
+
+ private func copyFileContents(_ text: String) {
+ UIPasteboard.general.string = text
+ didCopyFileContents = true
+ copyResetTask?.cancel()
+ copyResetTask = Task {
+ try? await Task.sleep(for: .seconds(2))
+ guard !Task.isCancelled else { return }
+ await MainActor.run {
+ didCopyFileContents = false
+ }
+ }
+ }
+
+ private func resetCopyConfirmation() {
+ copyResetTask?.cancel()
+ copyResetTask = nil
+ didCopyFileContents = false
+ }
+
// MARK: - File Content View
@ViewBuilder
@@ -216,36 +235,34 @@ private struct FileTreeContentView: View {
}
}
.listStyle(.plain)
+ .refreshable {
+ await viewModel.loadRootTree()
+ }
}
// MARK: - Text Blob
@ViewBuilder
private func textBlobView(_ entry: TreeEntry, blob: GitTextBlob) -> some View {
+ let text = blob.text ?? ""
+
VStack(spacing: 0) {
- HStack {
- Spacer()
- SRHTShareButton(url: shareURL, target: .file) {
- Label("Share File", systemImage: "square.and.arrow.up")
- }
- .buttonStyle(.bordered)
- }
- .padding(.horizontal)
- .padding(.top, 12)
-
- GeometryReader { geometry in
- ScrollView([.vertical, .horizontal]) {
- Text(blob.text ?? "")
- .font(.system(.body, design: .monospaced))
- .multilineTextAlignment(.leading)
- .fixedSize(horizontal: true, vertical: false)
- .frame(minWidth: geometry.size.width,
- minHeight: geometry.size.height,
- alignment: .topLeading)
- .padding()
- }
- .frame(width: geometry.size.width, height: geometry.size.height)
- }
+ CodeFileTextView(
+ text: text,
+ fileName: entry.name,
+ wrapLines: wrapRepositoryFileLines
+ )
+ }
+ .safeAreaInset(edge: .bottom, spacing: 0) {
+ fileActionToolbar(text: text)
+ }
+ .sheet(isPresented: $showFileShareSheet) {
+ FileContentShareSheet(activityItems: [text])
+ }
+ .alert("Share Unavailable", isPresented: $showShareUnavailableAlert) {
+ Button("OK", role: .cancel) {}
+ } message: {
+ Text(SRHTShareTarget.file.fallbackMessage)
}
}
@@ -273,11 +290,6 @@ private struct FileTreeContentView: View {
.font(.subheadline)
.foregroundStyle(.tertiary)
- SRHTShareButton(url: shareURL, target: .file) {
- Label("Share File", systemImage: "square.and.arrow.up")
- }
- .buttonStyle(.bordered)
-
if let content = blob.content, let url = URL(string: content) {
Link(destination: url) {
Label("Open in Safari", systemImage: "safari")
@@ -303,6 +315,460 @@ private struct FileTreeContentView: View {
formatter.countStyle = .file
return formatter.string(fromByteCount: Int64(bytes))
}
+
+ private func fileActionToolbar(text: String) -> some View {
+ HStack(spacing: 0) {
+ toolbarButton(
+ title: "Share",
+ systemImage: "square.and.arrow.up"
+ ) {
+ shareFileContents(text)
+ }
+
+ toolbarButton(
+ title: didCopyFileContents ? "Copied" : "Copy All",
+ systemImage: didCopyFileContents ? "checkmark" : "doc.on.doc"
+ ) {
+ copyFileContents(text)
+ }
+
+ toolbarButton(
+ title: wrapRepositoryFileLines ? "Wrap On" : "Wrap Off",
+ systemImage: "text.word.spacing"
+ ) {
+ wrapRepositoryFileLines.toggle()
+ }
+ }
+ .padding(.horizontal, 8)
+ .padding(.top, 10)
+ .padding(.bottom, 8)
+ .background(.bar)
+ .overlay(alignment: .top) {
+ Divider()
+ }
+ }
+
+ private func toolbarButton(
+ title: String,
+ systemImage: String,
+ action: @escaping () -> Void
+ ) -> some View {
+ Button(action: action) {
+ VStack(spacing: 4) {
+ Image(systemName: systemImage)
+ .font(.system(size: 17, weight: .semibold))
+ Text(title)
+ .font(.caption2)
+ .lineLimit(1)
+ }
+ .frame(maxWidth: .infinity)
+ .contentShape(Rectangle())
+ }
+ .buttonStyle(.plain)
+ .foregroundStyle(.primary)
+ }
+}
+
+private struct CodeFileTextView: UIViewRepresentable {
+ let text: String
+ let fileName: String
+ let wrapLines: Bool
+
+ private let font = UIFont(name: "SFMono-Regular", size: 12)
+ ?? UIFont.monospacedSystemFont(ofSize: 12, weight: .regular)
+
+ func makeUIView(context _: Context) -> CodeFileUIView {
+ CodeFileUIView(
+ text: text,
+ fileName: fileName,
+ font: font,
+ wrapLines: wrapLines
+ )
+ }
+
+ func updateUIView(_ uiView: CodeFileUIView, context _: Context) {
+ uiView.updateContent(
+ text: text,
+ fileName: fileName,
+ font: font,
+ wrapLines: wrapLines
+ )
+ }
+}
+
+private struct FileContentShareSheet: UIViewControllerRepresentable {
+ let activityItems: [Any]
+
+ func makeUIViewController(context _: Context) -> UIActivityViewController {
+ UIActivityViewController(activityItems: activityItems, applicationActivities: nil)
+ }
+
+ func updateUIViewController(_ : UIActivityViewController, context _: Context) {}
+}
+
+private final class CodeFileUIView: UIView {
+ private enum Layout {
+ static let verticalPadding: CGFloat = 12
+ static let gutterLeadingPadding: CGFloat = 12
+ static let gutterTrailingPadding: CGFloat = 8
+ }
+
+ private final class LineRow {
+ let gutterRow = UIView()
+ let gutterLabel = UILabel()
+ let codeRow = UIView()
+ let codeLabel = UILabel()
+ let gutterHeightConstraint: NSLayoutConstraint
+ let codeHeightConstraint: NSLayoutConstraint
+
+ init() {
+ gutterRow.translatesAutoresizingMaskIntoConstraints = false
+ gutterLabel.translatesAutoresizingMaskIntoConstraints = false
+ codeRow.translatesAutoresizingMaskIntoConstraints = false
+ codeLabel.translatesAutoresizingMaskIntoConstraints = false
+
+ gutterHeightConstraint = gutterRow.heightAnchor.constraint(equalToConstant: 0)
+ codeHeightConstraint = codeRow.heightAnchor.constraint(equalToConstant: 0)
+ }
+ }
+
+ private let outerScrollView = UIScrollView()
+ private let contentView = UIView()
+ private let gutterContainerView = UIView()
+ private let gutterStackView = UIStackView()
+ private let horizontalScrollView = UIScrollView()
+ private let codeContainerView = UIView()
+ private let codeStackView = UIStackView()
+
+ private var codeContainerWidthConstraint: NSLayoutConstraint?
+ private var gutterWidthConstraint: NSLayoutConstraint?
+
+ private var rows: [LineRow] = []
+ private var currentText = ""
+ private var currentFileName = ""
+ private var currentFont: UIFont
+ private var wrapLines: Bool
+ private var needsLineLayoutUpdate = true
+ private var lastMeasuredCodeWidth: CGFloat = 0
+
+ init(text: String, fileName: String, font: UIFont, wrapLines: Bool) {
+ self.currentFont = font
+ self.wrapLines = wrapLines
+ super.init(frame: .zero)
+ setupViews()
+ updateContent(text: text, fileName: fileName, font: font, wrapLines: wrapLines)
+ }
+
+ @available(*, unavailable)
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func layoutSubviews() {
+ super.layoutSubviews()
+ updateLineLayoutsIfNeeded()
+ }
+
+ func updateContent(text: String, fileName: String, font: UIFont, wrapLines: Bool) {
+ let contentChanged = text != currentText || fileName != currentFileName || font != currentFont
+ let wrapChanged = wrapLines != self.wrapLines
+
+ currentText = text
+ currentFileName = fileName
+ currentFont = font
+ self.wrapLines = wrapLines
+
+ if contentChanged {
+ rebuildRows()
+ }
+
+ if contentChanged || wrapChanged {
+ updateWrapConfiguration(resetHorizontalOffset: wrapChanged)
+ needsLineLayoutUpdate = true
+ setNeedsLayout()
+ layoutIfNeeded()
+ }
+ }
+
+ private func setupViews() {
+ backgroundColor = .systemBackground
+
+ outerScrollView.translatesAutoresizingMaskIntoConstraints = false
+ contentView.translatesAutoresizingMaskIntoConstraints = false
+ gutterContainerView.translatesAutoresizingMaskIntoConstraints = false
+ gutterStackView.translatesAutoresizingMaskIntoConstraints = false
+ horizontalScrollView.translatesAutoresizingMaskIntoConstraints = false
+ codeContainerView.translatesAutoresizingMaskIntoConstraints = false
+ codeStackView.translatesAutoresizingMaskIntoConstraints = false
+
+ gutterStackView.axis = .vertical
+ gutterStackView.alignment = .fill
+ gutterStackView.distribution = .fill
+ gutterStackView.spacing = 0
+
+ codeStackView.axis = .vertical
+ codeStackView.alignment = .fill
+ codeStackView.distribution = .fill
+ codeStackView.spacing = 0
+
+ outerScrollView.alwaysBounceVertical = true
+ horizontalScrollView.alwaysBounceVertical = false
+ horizontalScrollView.showsVerticalScrollIndicator = false
+
+ addSubview(outerScrollView)
+ outerScrollView.addSubview(contentView)
+ contentView.addSubview(gutterContainerView)
+ contentView.addSubview(horizontalScrollView)
+ gutterContainerView.addSubview(gutterStackView)
+ horizontalScrollView.addSubview(codeContainerView)
+ codeContainerView.addSubview(codeStackView)
+
+ codeContainerWidthConstraint = codeContainerView.widthAnchor.constraint(equalTo: horizontalScrollView.frameLayoutGuide.widthAnchor)
+ gutterWidthConstraint = gutterContainerView.widthAnchor.constraint(equalToConstant: 0)
+
+ NSLayoutConstraint.activate([
+ outerScrollView.leadingAnchor.constraint(equalTo: leadingAnchor),
+ outerScrollView.trailingAnchor.constraint(equalTo: trailingAnchor),
+ outerScrollView.topAnchor.constraint(equalTo: topAnchor),
+ outerScrollView.bottomAnchor.constraint(equalTo: bottomAnchor),
+
+ contentView.leadingAnchor.constraint(equalTo: outerScrollView.contentLayoutGuide.leadingAnchor),
+ contentView.trailingAnchor.constraint(equalTo: outerScrollView.contentLayoutGuide.trailingAnchor),
+ contentView.topAnchor.constraint(equalTo: outerScrollView.contentLayoutGuide.topAnchor),
+ contentView.bottomAnchor.constraint(equalTo: outerScrollView.contentLayoutGuide.bottomAnchor),
+ contentView.widthAnchor.constraint(equalTo: outerScrollView.frameLayoutGuide.widthAnchor),
+
+ gutterContainerView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
+ gutterContainerView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: Layout.verticalPadding),
+ gutterContainerView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -Layout.verticalPadding),
+
+ horizontalScrollView.leadingAnchor.constraint(equalTo: gutterContainerView.trailingAnchor),
+ horizontalScrollView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
+ horizontalScrollView.topAnchor.constraint(equalTo: gutterContainerView.topAnchor),
+ horizontalScrollView.bottomAnchor.constraint(equalTo: gutterContainerView.bottomAnchor),
+
+ gutterStackView.leadingAnchor.constraint(equalTo: gutterContainerView.leadingAnchor, constant: Layout.gutterLeadingPadding),
+ gutterStackView.trailingAnchor.constraint(equalTo: gutterContainerView.trailingAnchor, constant: -Layout.gutterTrailingPadding),
+ gutterStackView.topAnchor.constraint(equalTo: gutterContainerView.topAnchor),
+ gutterStackView.bottomAnchor.constraint(equalTo: gutterContainerView.bottomAnchor),
+
+ codeContainerView.leadingAnchor.constraint(equalTo: horizontalScrollView.contentLayoutGuide.leadingAnchor),
+ codeContainerView.trailingAnchor.constraint(equalTo: horizontalScrollView.contentLayoutGuide.trailingAnchor),
+ codeContainerView.topAnchor.constraint(equalTo: horizontalScrollView.contentLayoutGuide.topAnchor),
+ codeContainerView.bottomAnchor.constraint(equalTo: horizontalScrollView.contentLayoutGuide.bottomAnchor),
+ codeContainerView.heightAnchor.constraint(equalTo: horizontalScrollView.frameLayoutGuide.heightAnchor),
+
+ codeStackView.leadingAnchor.constraint(equalTo: codeContainerView.leadingAnchor),
+ codeStackView.trailingAnchor.constraint(equalTo: codeContainerView.trailingAnchor),
+ codeStackView.topAnchor.constraint(equalTo: codeContainerView.topAnchor),
+ codeStackView.bottomAnchor.constraint(equalTo: codeContainerView.bottomAnchor)
+ ])
+
+ gutterWidthConstraint?.isActive = true
+ }
+
+ private func rebuildRows() {
+ rows.forEach { row in
+ gutterStackView.removeArrangedSubview(row.gutterRow)
+ row.gutterRow.removeFromSuperview()
+ codeStackView.removeArrangedSubview(row.codeRow)
+ row.codeRow.removeFromSuperview()
+ }
+ rows.removeAll()
+
+ let attributedText = CodeSyntaxHighlighter.attributedText(
+ for: currentText,
+ fileName: currentFileName,
+ font: currentFont
+ )
+ let lines = makeLines(from: attributedText, font: currentFont)
+
+ gutterWidthConstraint?.constant = Self.gutterWidth(lineCount: lines.count, font: currentFont)
+
+ for line in lines {
+ let row = makeRow(for: line)
+ rows.append(row)
+ gutterStackView.addArrangedSubview(row.gutterRow)
+ codeStackView.addArrangedSubview(row.codeRow)
+ }
+ }
+
+ private func makeRow(for line: CodeFileLineData) -> LineRow {
+ let row = LineRow()
+
+ row.gutterLabel.font = currentFont
+ row.gutterLabel.textColor = .secondaryLabel
+ row.gutterLabel.textAlignment = .right
+ row.gutterLabel.text = line.number
+
+ row.codeLabel.attributedText = line.text
+ row.codeLabel.font = currentFont
+
+ row.gutterRow.addSubview(row.gutterLabel)
+ row.codeRow.addSubview(row.codeLabel)
+
+ NSLayoutConstraint.activate([
+ row.gutterHeightConstraint,
+ row.codeHeightConstraint,
+
+ row.gutterLabel.leadingAnchor.constraint(equalTo: row.gutterRow.leadingAnchor),
+ row.gutterLabel.trailingAnchor.constraint(equalTo: row.gutterRow.trailingAnchor),
+ row.gutterLabel.topAnchor.constraint(equalTo: row.gutterRow.topAnchor),
+ row.gutterLabel.bottomAnchor.constraint(lessThanOrEqualTo: row.gutterRow.bottomAnchor),
+
+ row.codeLabel.leadingAnchor.constraint(equalTo: row.codeRow.leadingAnchor),
+ row.codeLabel.trailingAnchor.constraint(equalTo: row.codeRow.trailingAnchor),
+ row.codeLabel.topAnchor.constraint(equalTo: row.codeRow.topAnchor),
+ row.codeLabel.bottomAnchor.constraint(lessThanOrEqualTo: row.codeRow.bottomAnchor)
+ ])
+
+ return row
+ }
+
+ private func updateWrapConfiguration(resetHorizontalOffset: Bool) {
+ horizontalScrollView.alwaysBounceHorizontal = !wrapLines
+ horizontalScrollView.isScrollEnabled = true
+
+ if wrapLines {
+ codeContainerWidthConstraint?.isActive = true
+ } else {
+ codeContainerWidthConstraint?.isActive = false
+ }
+
+ for row in rows {
+ row.codeLabel.numberOfLines = wrapLines ? 0 : 1
+ row.codeLabel.lineBreakMode = wrapLines ? .byWordWrapping : .byClipping
+ }
+
+ if resetHorizontalOffset {
+ horizontalScrollView.setContentOffset(.zero, animated: false)
+ }
+ }
+
+ private func updateLineLayoutsIfNeeded() {
+ let availableWidth = max(horizontalScrollView.bounds.width, 0)
+ let shouldUpdateForWidth = wrapLines && abs(availableWidth - lastMeasuredCodeWidth) > 0.5
+
+ guard needsLineLayoutUpdate || shouldUpdateForWidth else { return }
+
+ lastMeasuredCodeWidth = availableWidth
+ let measurementWidth = wrapLines ? max(availableWidth, 1) : CGFloat.greatestFiniteMagnitude
+
+ for row in rows {
+ row.codeLabel.preferredMaxLayoutWidth = wrapLines ? measurementWidth : 0
+ let measuredSize = row.codeLabel.sizeThatFits(
+ CGSize(width: measurementWidth, height: CGFloat.greatestFiniteMagnitude)
+ )
+ let rowHeight = max(ceil(measuredSize.height), ceil(currentFont.lineHeight))
+ row.gutterHeightConstraint.constant = rowHeight
+ row.codeHeightConstraint.constant = rowHeight
+ }
+
+ needsLineLayoutUpdate = false
+ }
+
+ private func makeLines(from attributedText: NSAttributedString, font: UIFont) -> [CodeFileLineData] {
+ let string = attributedText.string as NSString
+ var lines: [CodeFileLineData] = []
+ var currentLocation = 0
+ var lineNumber = 1
+
+ while currentLocation < attributedText.length {
+ let searchRange = NSRange(location: currentLocation, length: attributedText.length - currentLocation)
+ let newlineRange = string.range(of: "\n", options: [], range: searchRange)
+ let lineRange: NSRange
+
+ if newlineRange.location == NSNotFound {
+ lineRange = searchRange
+ currentLocation = attributedText.length
+ } else {
+ lineRange = NSRange(location: currentLocation, length: newlineRange.location - currentLocation)
+ currentLocation = newlineRange.location + newlineRange.length
+ }
+
+ lines.append(CodeFileLineData(
+ number: String(lineNumber),
+ text: attributedText.attributedSubstring(from: lineRange)
+ ))
+ lineNumber += 1
+ }
+
+ if attributedText.length == 0 || string.hasSuffix("\n") {
+ lines.append(CodeFileLineData(
+ number: String(lineNumber),
+ text: NSAttributedString(string: "", attributes: [.font: font])
+ ))
+ }
+
+ return lines
+ }
+
+ private static func gutterWidth(lineCount: Int, font: UIFont) -> CGFloat {
+ let digits = String(max(lineCount, 1)).count
+ let sample = String(repeating: "8", count: digits)
+ let numberWidth = ceil((sample as NSString).size(withAttributes: [.font: font]).width)
+ return Layout.gutterLeadingPadding + numberWidth + Layout.gutterTrailingPadding
+ }
+}
+
+private struct CodeFileLineData {
+ let number: String
+ let text: NSAttributedString
+}
+
+private enum CodeSyntaxHighlighter {
+ static func attributedText(for text: String, fileName: String, font: UIFont) -> NSAttributedString {
+ guard supportsSplashHighlighting(fileName: fileName) else {
+ return plainText(text, font: font)
+ }
+
+ var splashFont = Font(size: Double(font.pointSize))
+ splashFont.resource = .preloaded(font)
+
+ let theme = Theme(
+ font: splashFont,
+ plainTextColor: .label,
+ tokenColors: [
+ .keyword: .systemPink,
+ .string: .systemRed,
+ .type: .systemTeal,
+ .call: .systemBlue,
+ .number: .systemPurple,
+ .comment: .secondaryLabel,
+ .property: .systemGreen,
+ .dotAccess: .systemIndigo,
+ .preprocessing: .systemOrange
+ ],
+ backgroundColor: .clear
+ )
+
+ let highlighted = SyntaxHighlighter(
+ format: AttributedStringOutputFormat(theme: theme)
+ ).highlight(text)
+
+ return NSMutableAttributedString(attributedString: highlighted)
+ }
+
+ private static func plainText(_ text: String, font: UIFont) -> NSAttributedString {
+ NSAttributedString(
+ string: text,
+ attributes: [
+ .font: font,
+ .foregroundColor: UIColor.label
+ ]
+ )
+ }
+
+ private static func supportsSplashHighlighting(fileName: String) -> Bool {
+ let supportedExtensions: Set<String> = [
+ "swift",
+ "swiftinterface",
+ "playground"
+ ]
+ return supportedExtensions.contains(
+ (fileName as NSString).pathExtension.lowercased()
+ )
+ }
}
// MARK: - Tree Entry Row
@@ -329,7 +795,7 @@ private struct TreeEntryRow: View {
}
}
- private var iconColor: Color {
+ private var iconColor: ViewColor {
switch entry.object {
case .tree: .blue
case .unknown: .orange
@@ -424,7 +890,7 @@ private struct RefPickerSheet: View {
}
}
- private func refRow(title: String, systemImage: String, color: Color, isSelected: Bool) -> some View {
+ private func refRow(title: String, systemImage: String, color: ViewColor, isSelected: Bool) -> some View {
HStack(spacing: 12) {
Image(systemName: systemImage)
.foregroundStyle(color)