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() 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.. EditorDirtyLineInvalidationPlan { guard previousText != nil else { return EditorDirtyLineInvalidationPlan( reason: .initial, isFullRender: true, dirtyLineIndexes: Array(0..() let reason: EditorRenderReason let changedRange: NSRange? if let edit { reason = .sourceChange changedRange = edit.insertedRangeInEditedDocument dirtyLineIndexes.formUnion( currentLineIndex.lineIndexesAffected( by: changedRange ?? NSRange(location: 0, length: 0) ) ) } else if previousActiveLineIndex != currentActiveLineIndex { reason = .activeLineChange changedRange = nil } else { reason = .viewUpdate changedRange = nil } if let previousActiveLineIndex, previousActiveLineIndex != currentActiveLineIndex { dirtyLineIndexes.insert(previousActiveLineIndex) dirtyLineIndexes.insert(currentActiveLineIndex) } let validLineIndexes = dirtyLineIndexes .filter { (0.. 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 { 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 } }