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 } }