161 lines
4.8 KiB
Swift
161 lines
4.8 KiB
Swift
|
|
import Foundation
|
||
|
|
|
||
|
|
public enum LineEndingStrategy: String, Hashable, Sendable {
|
||
|
|
case lf
|
||
|
|
case crlf
|
||
|
|
case cr
|
||
|
|
case none
|
||
|
|
|
||
|
|
public var utf16Length: Int {
|
||
|
|
switch self {
|
||
|
|
case .lf, .cr:
|
||
|
|
return 1
|
||
|
|
case .crlf:
|
||
|
|
return 2
|
||
|
|
case .none:
|
||
|
|
return 0
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public struct DocumentLineBoundary: Hashable, Sendable {
|
||
|
|
public var index: Int
|
||
|
|
public var contentRange: NSRange
|
||
|
|
public var lineEnding: LineEndingStrategy
|
||
|
|
|
||
|
|
public init(index: Int, contentRange: NSRange, lineEnding: LineEndingStrategy) {
|
||
|
|
self.index = index
|
||
|
|
self.contentRange = contentRange
|
||
|
|
self.lineEnding = lineEnding
|
||
|
|
}
|
||
|
|
|
||
|
|
public var lineEndingRange: NSRange {
|
||
|
|
NSRange(location: contentRange.upperBound, length: lineEnding.utf16Length)
|
||
|
|
}
|
||
|
|
|
||
|
|
public var nextLineLocation: Int {
|
||
|
|
contentRange.upperBound + lineEnding.utf16Length
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public struct DocumentLineIndex: Hashable, Sendable {
|
||
|
|
public var source: String
|
||
|
|
public var boundaries: [DocumentLineBoundary]
|
||
|
|
|
||
|
|
public init(source: String) {
|
||
|
|
self.source = source
|
||
|
|
self.boundaries = Self.scanBoundaries(in: source)
|
||
|
|
}
|
||
|
|
|
||
|
|
public func lineIndex(containing location: Int) -> Int {
|
||
|
|
guard !boundaries.isEmpty else { return 0 }
|
||
|
|
|
||
|
|
let clampedLocation = max(0, min(location, source.utf16.count))
|
||
|
|
for boundary in boundaries.dropLast() {
|
||
|
|
if clampedLocation < boundary.nextLineLocation {
|
||
|
|
return boundary.index
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return boundaries[boundaries.count - 1].index
|
||
|
|
}
|
||
|
|
|
||
|
|
public func editorLines(activeLineIndex: Int) -> [EditorLine] {
|
||
|
|
let nsSource = source as NSString
|
||
|
|
return boundaries.map { boundary in
|
||
|
|
EditorLine(
|
||
|
|
index: boundary.index,
|
||
|
|
source: nsSource.substring(with: boundary.contentRange),
|
||
|
|
range: boundary.contentRange,
|
||
|
|
mode: boundary.index == activeLineIndex ? .source : .rendered
|
||
|
|
)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private static func scanBoundaries(in source: String) -> [DocumentLineBoundary] {
|
||
|
|
let nsSource = source as NSString
|
||
|
|
let sourceLength = nsSource.length
|
||
|
|
guard sourceLength > 0 else {
|
||
|
|
return [
|
||
|
|
DocumentLineBoundary(
|
||
|
|
index: 0,
|
||
|
|
contentRange: NSRange(location: 0, length: 0),
|
||
|
|
lineEnding: .none
|
||
|
|
)
|
||
|
|
]
|
||
|
|
}
|
||
|
|
|
||
|
|
var boundaries: [DocumentLineBoundary] = []
|
||
|
|
var lineStart = 0
|
||
|
|
var lineIndex = 0
|
||
|
|
var endedWithLineEnding = false
|
||
|
|
|
||
|
|
while lineStart < sourceLength {
|
||
|
|
var cursor = lineStart
|
||
|
|
while cursor < sourceLength,
|
||
|
|
!isLineEndingStart(nsSource.character(at: cursor)) {
|
||
|
|
cursor += 1
|
||
|
|
}
|
||
|
|
|
||
|
|
let contentRange = NSRange(location: lineStart, length: cursor - lineStart)
|
||
|
|
if cursor < sourceLength {
|
||
|
|
let lineEnding = lineEndingStrategy(at: cursor, in: nsSource)
|
||
|
|
boundaries.append(DocumentLineBoundary(
|
||
|
|
index: lineIndex,
|
||
|
|
contentRange: contentRange,
|
||
|
|
lineEnding: lineEnding
|
||
|
|
))
|
||
|
|
cursor += lineEnding.utf16Length
|
||
|
|
lineStart = cursor
|
||
|
|
endedWithLineEnding = cursor == sourceLength
|
||
|
|
} else {
|
||
|
|
boundaries.append(DocumentLineBoundary(
|
||
|
|
index: lineIndex,
|
||
|
|
contentRange: contentRange,
|
||
|
|
lineEnding: .none
|
||
|
|
))
|
||
|
|
lineStart = sourceLength
|
||
|
|
endedWithLineEnding = false
|
||
|
|
}
|
||
|
|
|
||
|
|
lineIndex += 1
|
||
|
|
}
|
||
|
|
|
||
|
|
if endedWithLineEnding {
|
||
|
|
boundaries.append(DocumentLineBoundary(
|
||
|
|
index: lineIndex,
|
||
|
|
contentRange: NSRange(location: sourceLength, length: 0),
|
||
|
|
lineEnding: .none
|
||
|
|
))
|
||
|
|
}
|
||
|
|
|
||
|
|
return boundaries
|
||
|
|
}
|
||
|
|
|
||
|
|
private static func lineEndingStrategy(at location: Int, in source: NSString) -> LineEndingStrategy {
|
||
|
|
let character = source.character(at: location)
|
||
|
|
if character == carriageReturnUTF16 {
|
||
|
|
let nextLocation = location + 1
|
||
|
|
if nextLocation < source.length,
|
||
|
|
source.character(at: nextLocation) == lineFeedUTF16 {
|
||
|
|
return .crlf
|
||
|
|
}
|
||
|
|
return .cr
|
||
|
|
}
|
||
|
|
return .lf
|
||
|
|
}
|
||
|
|
|
||
|
|
private static func isLineEndingStart(_ character: unichar) -> Bool {
|
||
|
|
character == lineFeedUTF16 || character == carriageReturnUTF16
|
||
|
|
}
|
||
|
|
|
||
|
|
private static let lineFeedUTF16: unichar = 10
|
||
|
|
private static let carriageReturnUTF16: unichar = 13
|
||
|
|
}
|
||
|
|
|
||
|
|
private extension NSRange {
|
||
|
|
var upperBound: Int {
|
||
|
|
location + length
|
||
|
|
}
|
||
|
|
}
|