Skip to content

Commit 4d23aef

Browse files
author
Oil3
committed
refactored to fix lazy-loading
1 parent 2d11909 commit 4d23aef

File tree

7 files changed

+218
-177
lines changed

7 files changed

+218
-177
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<Bucket
3+
uuid = "52E8137F-4932-45B1-A35A-B3E43BC2D48A"
4+
type = "1"
5+
version = "2.0">
6+
</Bucket>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>SchemeUserState</key>
6+
<dict>
7+
<key>CodeColors.xcscheme_^#shared#^_</key>
8+
<dict>
9+
<key>orderHint</key>
10+
<integer>0</integer>
11+
</dict>
12+
<key>QuickCodeColorLook.xcscheme_^#shared#^_</key>
13+
<dict>
14+
<key>orderHint</key>
15+
<integer>1</integer>
16+
</dict>
17+
</dict>
18+
</dict>
19+
</plist>
Lines changed: 105 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,83 +1,122 @@
1-
// CodeContentLoader.swift
2-
31
import SwiftUI
42
import Combine
53

64
class CodeContentLoader: ObservableObject {
7-
@Published var attributedContent: AttributedString = AttributedString()
8-
@Published var isLoading = false
9-
@Published var totalLines = 0
10-
@Published var fileSize: Int64 = 0
11-
12-
private var shouldCancel = false
13-
14-
func loadFile(at url: URL, maxFileSize: Int64 = 5 * 1024 * 1024) {
15-
self.isLoading = true
16-
self.shouldCancel = false
17-
self.attributedContent = AttributedString()
5+
@Published var attributedContent: AttributedString = AttributedString()
6+
@Published var isLoading = false
7+
@Published var totalLines = 0
8+
@Published var progress: Double = 0.0
9+
@Published var errorMessage: String? = nil
1810

19-
// Check file size
20-
if let fileSize = try? url.resourceValues(forKeys: [.fileSizeKey]).fileSize {
21-
self.fileSize = Int64(fileSize)
22-
if self.fileSize > maxFileSize {
23-
DispatchQueue.main.async {
24-
self.isLoading = false
11+
private var shouldCancel = false
12+
private var cachedLines: [AttributedString] = []
13+
private var reader: LineReader?
14+
private let batchSize = 100
15+
private let preloadThreshold = 20
16+
private let maxLines = 10_000
17+
18+
func loadFile(at url: URL, maxFileSize: Int64 = 5 * 1024 * 1024) {
19+
cleanup() // Reset the loader
20+
21+
// Check file size
22+
if let fileSize = try? url.resourceValues(forKeys: [.fileSizeKey]).fileSize {
23+
if Int64(fileSize) > maxFileSize {
24+
DispatchQueue.main.async {
25+
self.errorMessage = "File size exceeds the preview limit of \(maxFileSize / 1024 / 1024) MB."
26+
self.isLoading = false
27+
}
28+
return
29+
}
30+
}
31+
32+
let fileExtension = url.pathExtension.lowercased()
33+
self.isLoading = true
34+
self.shouldCancel = false
35+
36+
DispatchQueue.global(qos: .userInitiated).async {
37+
guard let reader = LineReader(url: url) else {
38+
DispatchQueue.main.async {
39+
self.errorMessage = "Unable to open file. Unsupported format or corrupt file."
40+
self.isLoading = false
41+
}
42+
return
43+
}
44+
self.reader = reader
45+
self.processLines(fileExtension: fileExtension)
2546
}
26-
return
27-
}
2847
}
2948

30-
let fileExtension = url.pathExtension.lowercased()
31-
32-
DispatchQueue.global(qos: .userInitiated).async {
33-
guard let reader = LineReader(url: url) else {
34-
DispatchQueue.main.async {
35-
self.isLoading = false
36-
}
37-
return
38-
}
39-
40-
defer {
41-
reader.close()
42-
}
43-
44-
var lineNumber = 1
45-
46-
while let line = reader.nextLine() {
47-
if self.shouldCancel {
48-
break
49-
}
50-
51-
// Highlight the line
52-
var attributedLine = SyntaxHighlighter.highlightLine(line: line, fileExtension: fileExtension)
49+
private func processLines(fileExtension: String) {
50+
var lineNumber = 0
51+
var batch = [AttributedString]()
5352

54-
// Prepend line number
55-
// var lineNumberString = AttributedString("\(lineNumber) ")
56-
// lineNumberString.foregroundColor = .gray
57-
58-
lineNumber += 1
53+
while !shouldCancel {
54+
guard let line = reader?.nextLine() else { break }
55+
let highlightedLine = SyntaxHighlighter.highlightLine(line: line, fileExtension: fileExtension)
56+
batch.append(highlightedLine)
57+
lineNumber += 1
58+
59+
if batch.count >= batchSize {
60+
appendBatch(batch, lineNumber: lineNumber)
61+
batch.removeAll()
62+
}
63+
64+
// Stop processing if max lines are reached
65+
if lineNumber >= maxLines {
66+
DispatchQueue.main.async {
67+
self.errorMessage = "Preview truncated to \(self.maxLines) lines."
68+
self.isLoading = false
69+
}
70+
break
71+
}
72+
}
5973

60-
// Combine line number and content
61-
// lineNumberString.append(attributedLine)
62-
// Append newline character
63-
attributedLine.append(AttributedString("\n"))
74+
if !batch.isEmpty {
75+
appendBatch(batch, lineNumber: lineNumber)
76+
}
6477

6578
DispatchQueue.main.async {
66-
self.attributedContent.append(attributedLine)
67-
self.totalLines = lineNumber - 1
79+
self.isLoading = false
80+
}
81+
}
82+
83+
private func appendBatch(_ batch: [AttributedString], lineNumber: Int) {
84+
DispatchQueue.main.async {
85+
self.cachedLines.append(contentsOf: batch)
86+
self.totalLines = lineNumber
87+
self.progress = Double(lineNumber) / Double(self.maxLines)
88+
}
89+
}
90+
91+
func getLine(at index: Int) -> AttributedString {
92+
guard index >= 0 && index < cachedLines.count else {
93+
return AttributedString("")
6894
}
69-
}
70-
71-
DispatchQueue.main.async {
72-
self.isLoading = false
73-
}
95+
return cachedLines[index]
96+
}
97+
98+
func loadNextBatchIfNeeded(currentIndex: Int) {
99+
guard currentIndex >= totalLines - preloadThreshold, !isLoading else { return }
100+
processLines(fileExtension: "swift") // Replace with the actual file extension
101+
}
102+
103+
func cancelLoading() {
104+
shouldCancel = true
105+
cleanup()
106+
}
107+
108+
private func cleanup() {
109+
shouldCancel = true
110+
reader?.close()
111+
reader = nil
112+
cachedLines.removeAll()
113+
attributedContent = AttributedString()
114+
isLoading = false
115+
totalLines = 0
116+
progress = 0.0
117+
errorMessage = nil
74118
}
75-
}
76-
77-
func cancelLoading() {
78-
shouldCancel = true
79-
}
80119
}
81120
//
82-
// Copyright Almahdi Morris Quet 2024
121+
// Copyright Almahdi Morris Quet 2024-2025
83122
//
Lines changed: 36 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,47 @@
11
import Foundation
22

33
class LineReader {
4-
let encoding: String.Encoding
5-
let chunkSize: Int
6-
var fileHandle: FileHandle!
7-
var buffer: Data
8-
var atEof: Bool = false
9-
10-
private let accessQueue = DispatchQueue(label: "LineReaderAccessQueue", qos: .userInitiated, attributes: .concurrent)
11-
12-
init?(url: URL, encoding: String.Encoding = .utf8, chunkSize: Int = 4096) {
13-
guard let fileHandle = try? FileHandle(forReadingFrom: url) else {
14-
return nil
4+
let encoding: String.Encoding
5+
let chunkSize: Int
6+
var fileHandle: FileHandle!
7+
var buffer: Data
8+
var atEof: Bool = false
9+
10+
init?(url: URL, encoding: String.Encoding = .utf8, chunkSize: Int = 4096) {
11+
guard let fileHandle = try? FileHandle(forReadingFrom: url) else { return nil }
12+
self.fileHandle = fileHandle
13+
self.encoding = encoding
14+
self.chunkSize = chunkSize
15+
self.buffer = Data(capacity: chunkSize)
1516
}
16-
self.fileHandle = fileHandle
17-
self.encoding = encoding
18-
self.chunkSize = chunkSize
19-
self.buffer = Data(capacity: chunkSize)
20-
}
21-
22-
func nextLine() -> String? {
23-
return accessQueue.sync {
24-
guard fileHandle != nil else { return nil }
25-
26-
while !atEof {
27-
if let range = buffer.range(of: Data([0x0A])) { // Newline character
28-
let subData = buffer.subdata(in: 0..<range.lowerBound)
29-
let line = String(data: subData, encoding: encoding)
30-
buffer.removeSubrange(0..<range.upperBound)
31-
return line
17+
18+
func nextLine() -> String? {
19+
while !atEof {
20+
if let range = buffer.range(of: Data([0x0A])) { // Newline character
21+
let subData = buffer.subdata(in: 0..<range.lowerBound)
22+
let line = String(data: subData, encoding: encoding)
23+
buffer.removeSubrange(0..<range.upperBound)
24+
return line
25+
}
26+
let tmpData = fileHandle.readData(ofLength: chunkSize)
27+
if tmpData.count > 0 {
28+
buffer.append(tmpData)
29+
} else {
30+
atEof = true
31+
if buffer.count > 0 {
32+
let line = String(data: buffer, encoding: encoding)
33+
buffer.count = 0
34+
return line
35+
}
36+
}
3237
}
33-
let tmpData = fileHandle.readData(ofLength: chunkSize)
34-
if tmpData.count > 0 {
35-
buffer.append(tmpData)
36-
} else {
37-
atEof = true
38-
if buffer.count > 0 {
39-
let line = String(data: buffer as Data, encoding: encoding)
40-
buffer.count = 0
41-
return line
42-
}
43-
return nil
44-
}
45-
}
46-
return nil
38+
return nil
4739
}
48-
}
49-
50-
func close() {
51-
accessQueue.sync {
52-
if fileHandle != nil {
40+
41+
func close() {
5342
try? fileHandle.close()
54-
fileHandle = nil
55-
}
5643
}
57-
}
58-
59-
deinit {
60-
close()
61-
}
6244
}
6345
//
64-
// Copyright Almahdi Morris Quet 2024
46+
// Copyright Almahdi Morris Quet 2024-2025
6547
//

QuickCodeColorLook/PreviewProvider.swift

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,40 @@ class PreviewProvider: QLPreviewProvider {
88
}
99
}
1010
// ContentView.swift
11-
1211
import SwiftUI
1312

1413
struct ContentView: View {
15-
@ObservedObject var loader: CodeContentLoader
16-
@State private var fontSize: CGFloat = 12.0
17-
18-
var body: some View {
19-
ScrollView([.vertical, .horizontal]) {
20-
Text(loader.attributedContent)
21-
.font(.system(size: fontSize, weight: .regular, design: .monospaced))
22-
.textSelection(.enabled)
23-
.padding()
14+
@ObservedObject var loader: CodeContentLoader
15+
@State private var fontSize: CGFloat = 12.0
16+
17+
var body: some View {
18+
VStack {
19+
if loader.isLoading {
20+
ProgressView(value: loader.progress, total: 1.0)
21+
.padding()
22+
Text("Loading...")
23+
} else if let error = loader.errorMessage {
24+
Text(error)
25+
.foregroundColor(.red)
26+
.padding()
27+
} else {
28+
ScrollView {
29+
LazyVStack(alignment: .leading, spacing: 2) {
30+
ForEach(0..<loader.totalLines, id: \.self) { lineIndex in
31+
Text(loader.getLine(at: lineIndex))
32+
.font(.system(size: fontSize, weight: .regular, design: .monospaced))
33+
.padding(.horizontal)
34+
.onAppear {
35+
loader.loadNextBatchIfNeeded(currentIndex: lineIndex)
36+
}
37+
}
38+
}
39+
}
40+
.gesture(MagnificationGesture()
41+
.onChanged { value in
42+
self.fontSize = max(8.0, min(24.0, 12.0 * value))
43+
})
44+
}
45+
}
2446
}
25-
.gesture(MagnificationGesture()
26-
.onChanged { value in
27-
self.fontSize = max(8.0, min(24.0, 12.0 * value))
28-
})
29-
}
3047
}

0 commit comments

Comments
 (0)