140 lines
4.5 KiB
Swift
140 lines
4.5 KiB
Swift
import Foundation
|
|
|
|
public struct EditorDirtyLineInvalidationPlan: Hashable, Sendable {
|
|
public var reason: EditorRenderReason
|
|
public var isFullRender: Bool
|
|
public var dirtyLineIndexes: [Int]
|
|
public var changedRange: NSRange?
|
|
|
|
public init(
|
|
reason: EditorRenderReason,
|
|
isFullRender: Bool,
|
|
dirtyLineIndexes: [Int],
|
|
changedRange: NSRange? = nil
|
|
) {
|
|
self.reason = reason
|
|
self.isFullRender = isFullRender
|
|
self.dirtyLineIndexes = dirtyLineIndexes
|
|
self.changedRange = changedRange
|
|
}
|
|
|
|
public var dirtyLineCount: Int {
|
|
dirtyLineIndexes.count
|
|
}
|
|
|
|
public var requiresStyling: Bool {
|
|
isFullRender || !dirtyLineIndexes.isEmpty
|
|
}
|
|
}
|
|
|
|
public enum EditorDirtyLineInvalidator {
|
|
public static func plan(
|
|
previousText: String?,
|
|
currentText: String,
|
|
previousActiveLineIndex: Int?,
|
|
currentActiveLineIndex: Int
|
|
) -> EditorDirtyLineInvalidationPlan {
|
|
let currentLines = EditorActiveLineTracker.lines(
|
|
from: currentText,
|
|
activeLineIndex: currentActiveLineIndex
|
|
)
|
|
|
|
guard let previousText else {
|
|
return EditorDirtyLineInvalidationPlan(
|
|
reason: .initial,
|
|
isFullRender: true,
|
|
dirtyLineIndexes: currentLines.map(\.index)
|
|
)
|
|
}
|
|
|
|
var dirtyLineIndexes = Set<Int>()
|
|
let reason: EditorRenderReason
|
|
var changedRange: NSRange?
|
|
|
|
if previousText != currentText {
|
|
reason = .sourceChange
|
|
changedRange = changedUTF16Range(from: previousText, to: currentText)
|
|
dirtyLineIndexes.formUnion(
|
|
lineIndexesAffected(
|
|
by: changedRange ?? NSRange(location: 0, length: currentText.utf16.count),
|
|
in: currentText,
|
|
lineCount: currentLines.count
|
|
)
|
|
)
|
|
} else if previousActiveLineIndex != currentActiveLineIndex {
|
|
reason = .activeLineChange
|
|
} else {
|
|
reason = .viewUpdate
|
|
}
|
|
|
|
if let previousActiveLineIndex,
|
|
previousActiveLineIndex != currentActiveLineIndex {
|
|
dirtyLineIndexes.insert(previousActiveLineIndex)
|
|
dirtyLineIndexes.insert(currentActiveLineIndex)
|
|
}
|
|
|
|
let validLineIndexes = dirtyLineIndexes
|
|
.filter { (0..<currentLines.count).contains($0) }
|
|
.sorted()
|
|
|
|
return EditorDirtyLineInvalidationPlan(
|
|
reason: reason,
|
|
isFullRender: false,
|
|
dirtyLineIndexes: validLineIndexes,
|
|
changedRange: changedRange
|
|
)
|
|
}
|
|
|
|
private static func changedUTF16Range(from oldText: String, to newText: String) -> NSRange {
|
|
let old = oldText as NSString
|
|
let new = newText as NSString
|
|
let commonLength = min(old.length, new.length)
|
|
|
|
var prefixLength = 0
|
|
while prefixLength < commonLength,
|
|
old.character(at: prefixLength) == new.character(at: prefixLength) {
|
|
prefixLength += 1
|
|
}
|
|
|
|
var oldSuffixStart = old.length
|
|
var newSuffixStart = new.length
|
|
while oldSuffixStart > prefixLength,
|
|
newSuffixStart > prefixLength,
|
|
old.character(at: oldSuffixStart - 1) == new.character(at: newSuffixStart - 1) {
|
|
oldSuffixStart -= 1
|
|
newSuffixStart -= 1
|
|
}
|
|
|
|
return NSRange(location: prefixLength, length: max(0, newSuffixStart - prefixLength))
|
|
}
|
|
|
|
private static func lineIndexesAffected(
|
|
by changedRange: NSRange,
|
|
in source: String,
|
|
lineCount: Int
|
|
) -> Set<Int> {
|
|
guard lineCount > 0 else { return [] }
|
|
|
|
let sourceLength = source.utf16.count
|
|
let startLocation = max(0, min(changedRange.location, sourceLength))
|
|
let endLocation: Int
|
|
if changedRange.length == 0 {
|
|
endLocation = startLocation
|
|
} else {
|
|
endLocation = max(startLocation, min(changedRange.upperBound - 1, sourceLength))
|
|
}
|
|
|
|
let startLine = EditorActiveLineTracker.lineIndex(containing: startLocation, in: source)
|
|
let endLine = EditorActiveLineTracker.lineIndex(containing: endLocation, in: source)
|
|
let lowerBound = max(0, min(startLine, endLine) - 1)
|
|
let upperBound = min(lineCount - 1, max(startLine, endLine) + 1)
|
|
|
|
return Set(lowerBound...upperBound)
|
|
}
|
|
}
|
|
|
|
private extension NSRange {
|
|
var upperBound: Int {
|
|
location + length
|
|
}
|
|
}
|