Skip to content
This repository was archived by the owner on Jun 18, 2025. It is now read-only.

Commit cd124aa

Browse files
committed
Implement cache for SourceEditor
1 parent 3e8ff22 commit cd124aa

1 file changed

Lines changed: 59 additions & 26 deletions

File tree

Tool/Sources/XcodeInspector/SourceEditor.swift

Lines changed: 59 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import AppKit
22
import AsyncExtensions
33
import AXNotificationStream
44
import Foundation
5+
import Logger
56
import SuggestionModel
67

78
/// Representing a source editor inside Xcode.
@@ -24,43 +25,25 @@ public class SourceEditor {
2425
var observeAXNotificationsTask: Task<Void, Never>?
2526
public let axNotifications = AsyncPassthroughSubject<AXNotification>()
2627

28+
/// To prevent expensive calculations in ``getContent()``.
29+
private let cache = Cache()
2730

28-
private var cachedContent: String?
29-
private var cachedLines = [String]()
30-
3131
/// Get the content of the source editor.
3232
///
3333
/// - note: This method is expensive.
3434
public func getContent() -> Content {
3535
let content = element.value
36-
37-
let split = if element.hashValue == cachedContent?.hashValue {
38-
cachedLines
39-
} else {
40-
content.breakLines(appendLineBreakToLastLine: false)
41-
}
42-
43-
cachedContent = content
44-
cachedLines = split
45-
36+
let selectionRange = element.selectedTextRange
37+
let (lines, selections) = cache.get(content: content, selectedTextRange: selectionRange)
38+
4639
let lineAnnotationElements = element.children.filter { $0.identifier == "Line Annotation" }
4740
let lineAnnotations = lineAnnotationElements.map(\.description)
4841

49-
if let selectionRange = element.selectedTextRange {
50-
let range = Self.convertRangeToCursorRange(selectionRange, in: split)
51-
return .init(
52-
content: content,
53-
lines: split,
54-
selections: [range],
55-
cursorPosition: range.start,
56-
lineAnnotations: lineAnnotations
57-
)
58-
}
5942
return .init(
6043
content: content,
61-
lines: split,
62-
selections: [],
63-
cursorPosition: .outOfScope,
44+
lines: lines,
45+
selections: selections,
46+
cursorPosition: selections.first?.start ?? .outOfScope,
6447
lineAnnotations: lineAnnotations
6548
)
6649
}
@@ -129,6 +112,56 @@ public class SourceEditor {
129112
}
130113
}
131114

115+
extension SourceEditor {
116+
final class Cache {
117+
static let queue = DispatchQueue(label: "SourceEditor.Cache")
118+
119+
private var sourceContent: String?
120+
private var cachedLines = [String]()
121+
private var sourceSelectedTextRange: ClosedRange<Int>?
122+
private var cachedSelections = [CursorRange]()
123+
124+
func get(content: String, selectedTextRange: ClosedRange<Int>?) -> (
125+
lines: [String],
126+
selections: [CursorRange]
127+
) {
128+
Self.queue.sync {
129+
let contentMatch = content.hashValue == sourceContent?.hashValue
130+
let selectedRangeMatch = selectedTextRange == sourceSelectedTextRange
131+
let lines: [String] = {
132+
if contentMatch {
133+
Logger.service.debug("Cache Hit: Lines")
134+
return cachedLines
135+
}
136+
Logger.service.debug("Cache Missed: Lines")
137+
return content.breakLines(appendLineBreakToLastLine: false)
138+
}()
139+
let selections: [CursorRange] = {
140+
if contentMatch, selectedRangeMatch {
141+
Logger.service.debug("Cache Hit: Selections")
142+
return cachedSelections
143+
}
144+
Logger.service.debug("Cache Missed: Selections")
145+
if let selectedTextRange {
146+
return [SourceEditor.convertRangeToCursorRange(
147+
selectedTextRange,
148+
in: lines
149+
)]
150+
}
151+
return []
152+
}()
153+
154+
sourceContent = content
155+
cachedLines = lines
156+
sourceSelectedTextRange = selectedTextRange
157+
cachedSelections = selections
158+
159+
return (lines, selections)
160+
}
161+
}
162+
}
163+
}
164+
132165
// MARK: - Helpers
133166

134167
public extension SourceEditor {

0 commit comments

Comments
 (0)