diff --git a/Sources/Diagnose/SourcekitdRequestCommand.swift b/Sources/Diagnose/SourcekitdRequestCommand.swift index c4cb8665d..14cabbad4 100644 --- a/Sources/Diagnose/SourcekitdRequestCommand.swift +++ b/Sources/Diagnose/SourcekitdRequestCommand.swift @@ -56,10 +56,9 @@ public struct SourceKitdRequestCommand: AsyncParsableCommand { let requestInfo = try RequestInfo(request: requestString) let lineTable = LineTable(requestInfo.fileContents) - if let offset = lineTable.utf8OffsetOf(line: line - 1, utf8Column: column - 1) { - print("Adjusting request offset to \(offset)") - requestString.replace(#/key.offset: [0-9]+/#, with: "key.offset: \(offset)") - } + let offset = lineTable.utf8OffsetOf(line: line - 1, utf8Column: column - 1) + print("Adjusting request offset to \(offset)") + requestString.replace(#/key.offset: [0-9]+/#, with: "key.offset: \(offset)") } let request = try requestString.cString(using: .utf8)!.withUnsafeBufferPointer { buffer in diff --git a/Sources/SKSupport/LineTable.swift b/Sources/SKSupport/LineTable.swift index 87f00e9f6..343e81f86 100644 --- a/Sources/SKSupport/LineTable.swift +++ b/Sources/SKSupport/LineTable.swift @@ -125,15 +125,50 @@ extension LineTable { } } -// MARK: - Position translation +// MARK: - Position conversion extension LineTable { // MARK: line:column <-> String.Index + /// Result of `lineSlice(at:)` + @usableFromInline + enum LineSliceResult { + /// The line index passed to `lineSlice(at:)` was negative. + case beforeFirstLine + /// The contents of the line at the index passed to `lineSlice(at:)`. + case line(Substring) + /// The line index passed to `lineSlice(at:)` was after the last line of the file + case afterLastLine + } + + /// Extracts the contents of the line at the given index. + /// + /// If `line` is out-of-bounds, logs a fault and returns either `beforeFirstLine` or `afterLastLine`. + @usableFromInline + func lineSlice(at line: Int, callerFile: StaticString, callerLine: UInt) -> LineSliceResult { + guard line >= 0 else { + logger.fault( + """ + Line \(line) is negative (\(callerFile, privacy: .public):\(callerLine, privacy: .public)) + """ + ) + return .beforeFirstLine + } + guard line < count else { + logger.fault( + """ + Line \(line) is out-of range (\(callerFile, privacy: .public):\(callerLine, privacy: .public)) + """ + ) + return .afterLastLine + } + return .line(self[line]) + } + /// Converts the given UTF-16-based `line:column`` position to a `String.Index`. /// - /// If the position does not refer to a valid position with in the source file, returns `nil` and logs a fault - /// containing the file and line of the caller (from `callerFile` and `callerLine`). + /// If the position does not refer to a valid position with in the source file, returns the closest valid position and + /// logs a fault containing the file and line of the caller (from `callerFile` and `callerLine`). /// /// - parameter line: Line number (zero-based). /// - parameter utf16Column: UTF-16 column offset (zero-based). @@ -143,34 +178,42 @@ extension LineTable { utf16Column: Int, callerFile: StaticString = #fileID, callerLine: UInt = #line - ) -> String.Index? { - guard line < count else { + ) -> String.Index { + let lineSlice: Substring + switch self.lineSlice(at: line, callerFile: callerFile, callerLine: callerLine) { + case .beforeFirstLine: + return self.content.startIndex + case .afterLastLine: + return self.content.endIndex + case .line(let line): + lineSlice = line + } + guard utf16Column >= 0 else { logger.fault( """ - Unable to get string index for \(line):\(utf16Column) (UTF-16) because line is out of range \ + Column is negative while converting \(line):\(utf16Column) (UTF-16) to String.Index \ (\(callerFile, privacy: .public):\(callerLine, privacy: .public)) """ ) - return nil + return lineSlice.startIndex } - let lineSlice = self[line] guard let index = content.utf16.index(lineSlice.startIndex, offsetBy: utf16Column, limitedBy: lineSlice.endIndex) else { logger.fault( """ - Unable to get string index for \(line):\(utf16Column) (UTF-16) because column is out of range \ + Column is past line end while converting \(line):\(utf16Column) (UTF-16) to String.Index \ (\(callerFile, privacy: .public):\(callerLine, privacy: .public)) """ ) - return nil + return lineSlice.endIndex } return index } /// Converts the given UTF-8-based `line:column`` position to a `String.Index`. /// - /// If the position does not refer to a valid position with in the source file, returns `nil` and logs a fault - /// containing the file and line of the caller (from `callerFile` and `callerLine`). + /// If the position does not refer to a valid position with in the source file, returns the closest valid position and + /// logs a fault containing the file and line of the caller (from `callerFile` and `callerLine`). /// /// - parameter line: Line number (zero-based). /// - parameter utf8Column: UTF-8 column offset (zero-based). @@ -180,35 +223,46 @@ extension LineTable { utf8Column: Int, callerFile: StaticString = #fileID, callerLine: UInt = #line - ) -> String.Index? { - guard 0 <= line, line < count else { + ) -> String.Index { + let lineSlice: Substring + switch self.lineSlice(at: line, callerFile: callerFile, callerLine: callerLine) { + case .beforeFirstLine: + return self.content.startIndex + case .afterLastLine: + return self.content.endIndex + case .line(let line): + lineSlice = line + } + + guard utf8Column >= 0 else { logger.fault( """ - Unable to get string index for \(line):\(utf8Column) (UTF-8) because line is out of range \ + Column is negative while converting \(line):\(utf8Column) (UTF-8) to String.Index. \ (\(callerFile, privacy: .public):\(callerLine, privacy: .public)) """ ) - return nil + return lineSlice.startIndex } - guard 0 <= utf8Column else { + guard let index = content.utf8.index(lineSlice.startIndex, offsetBy: utf8Column, limitedBy: lineSlice.endIndex) + else { logger.fault( """ - Unable to get string index for \(line):\(utf8Column) (UTF-8) because column is out of range \ + Column is after end of line while converting \(line):\(utf8Column) (UTF-8) to String.Index. \ (\(callerFile, privacy: .public):\(callerLine, privacy: .public)) """ ) - return nil + return lineSlice.endIndex } - let lineSlice = self[line] - return content.utf8.index(lineSlice.startIndex, offsetBy: utf8Column, limitedBy: lineSlice.endIndex) + + return index } // MARK: line:column <-> UTF-8 offset /// Converts the given UTF-16-based `line:column`` position to a UTF-8 offset within the source file. /// - /// If the position does not refer to a valid position with in the source file, returns `nil` and logs a fault - /// containing the file and line of the caller (from `callerFile` and `callerLine`). + /// If the position does not refer to a valid position with in the source file, returns the closest valid offset and + /// logs a fault containing the file and line of the caller (from `callerFile` and `callerLine`). /// /// - parameter line: Line number (zero-based). /// - parameter utf16Column: UTF-16 column offset (zero-based). @@ -218,24 +272,20 @@ extension LineTable { utf16Column: Int, callerFile: StaticString = #fileID, callerLine: UInt = #line - ) -> Int? { - guard - let stringIndex = stringIndexOf( - line: line, - utf16Column: utf16Column, - callerFile: callerFile, - callerLine: callerLine - ) - else { - return nil - } + ) -> Int { + let stringIndex = stringIndexOf( + line: line, + utf16Column: utf16Column, + callerFile: callerFile, + callerLine: callerLine + ) return content.utf8.distance(from: content.startIndex, to: stringIndex) } - /// Converts the given UTF-8-based `line:column`` position to a UTF-8 offset within the source file. + /// Converts the given UTF-8-based `line:column` position to a UTF-8 offset within the source file. /// - /// If the position does not refer to a valid position with in the source file, returns `nil` and logs a fault - /// containing the file and line of the caller (from `callerFile` and `callerLine`). + /// If the position does not refer to a valid position with in the source file, returns the closest valid offset and + /// logs a fault containing the file and line of the caller (from `callerFile` and `callerLine`). /// /// - parameter line: Line number (zero-based). /// - parameter utf8Column: UTF-8 column offset (zero-based). @@ -245,24 +295,20 @@ extension LineTable { utf8Column: Int, callerFile: StaticString = #fileID, callerLine: UInt = #line - ) -> Int? { - guard - let stringIndex = stringIndexOf( - line: line, - utf8Column: utf8Column, - callerFile: callerFile, - callerLine: callerLine - ) - else { - return nil - } + ) -> Int { + let stringIndex = stringIndexOf( + line: line, + utf8Column: utf8Column, + callerFile: callerFile, + callerLine: callerLine + ) return content.utf8.distance(from: content.startIndex, to: stringIndex) } - /// Converts the given UTF-16-based line:column position to the UTF-8 offset of that position within the source file. + /// Converts the given UTF-8 offset to a zero-based UTF-16 line:column pair. /// - /// If the position does not refer to a valid position with in the snapshot, returns `nil` and logs a fault - /// containing the file and line of the caller (from `callerFile` and `callerLine`). + /// If the position does not refer to a valid position with in the snapshot, returns the closest valid line:column + /// pair and logs a fault containing the file and line of the caller (from `callerFile` and `callerLine`). /// /// - parameter utf8Offset: UTF-8 buffer offset (zero-based). @inlinable @@ -270,47 +316,48 @@ extension LineTable { utf8Offset: Int, callerFile: StaticString = #fileID, callerLine: UInt = #line - ) -> (line: Int, utf16Column: Int)? { + ) -> (line: Int, utf16Column: Int) { + guard utf8Offset >= 0 else { + logger.fault( + """ + UTF-8 offset \(utf8Offset) is negative while converting it to UTF-16 line:column \ + (\(callerFile, privacy: .public):\(callerLine, privacy: .public)) + """ + ) + return (line: 0, utf16Column: 0) + } guard utf8Offset <= content.utf8.count else { logger.fault( """ - Unable to get line and UTF-16 column for UTF-8 offset \(utf8Offset) because offset is out of range \ + UTF-8 offset \(utf8Offset) is past the end of the file while converting it to UTF-16 line:column \ (\(callerFile, privacy: .public):\(callerLine, privacy: .public)) """ ) - return nil + return lineAndUTF16ColumnOf(content.endIndex) } return lineAndUTF16ColumnOf(content.utf8.index(content.startIndex, offsetBy: utf8Offset)) } /// Converts the given UTF-8-based line:column position to the UTF-8 offset of that position within the source file. /// - /// If the position does not refer to a valid position with in the snapshot, returns `nil` and logs a fault - /// containing the file and line of the caller (from `callerFile` and `callerLine`). + /// If the position does not refer to a valid position with in the snapshot, returns the closest valid line:colum pair + /// and logs a fault containing the file and line of the caller (from `callerFile` and `callerLine`). @inlinable func lineAndUTF8ColumnOf( utf8Offset: Int, callerFile: StaticString = #fileID, callerLine: UInt = #line - ) -> (line: Int, utf8Column: Int)? { - guard - let (line, utf16Column) = lineAndUTF16ColumnOf( - utf8Offset: utf8Offset, - callerFile: callerFile, - callerLine: callerLine - ) - else { - return nil - } - guard - let utf8Column = utf8ColumnAt( - line: line, - utf16Column: utf16Column, - callerFile: callerFile, - callerLine: callerLine - ) - else { - return nil - } + ) -> (line: Int, utf8Column: Int) { + let (line, utf16Column) = lineAndUTF16ColumnOf( + utf8Offset: utf8Offset, + callerFile: callerFile, + callerLine: callerLine + ) + let utf8Column = utf8ColumnAt( + line: line, + utf16Column: utf16Column, + callerFile: callerFile, + callerLine: callerLine + ) return (line, utf8Column) } @@ -318,8 +365,8 @@ extension LineTable { /// Returns UTF-16 column offset at UTF-8 based `line:column` position. /// - /// If the position does not refer to a valid position with in the snapshot, returns `nil` and logs a fault - /// containing the file and line of the caller (from `callerFile` and `callerLine`). + /// If the position does not refer to a valid position with in the snapshot, performs a best-effort recovery and logs + /// a fault containing the file and line of the caller (from `callerFile` and `callerLine`). /// /// - parameter line: Line number (zero-based). /// - parameter utf8Column: UTF-8 column offset (zero-based). @@ -329,21 +376,34 @@ extension LineTable { utf8Column: Int, callerFile: StaticString = #fileID, callerLine: UInt = #line - ) -> Int? { - return convertColumn( - line: line, - column: utf8Column, - indexFunction: content.utf8.index(_:offsetBy:limitedBy:), - distanceFunction: content.utf16.distance(from:to:), - callerFile: callerFile, - callerLine: callerLine - ) + ) -> Int { + let lineSlice: Substring + switch self.lineSlice(at: line, callerFile: callerFile, callerLine: callerLine) { + case .beforeFirstLine, .afterLastLine: + // This line is out-of-bounds. `lineSlice(at:)` already logged a fault. + // Recovery by assuming that UTF-8 and UTF-16 columns are similar. + return utf8Column + case .line(let line): + lineSlice = line + } + guard + let stringIndex = lineSlice.utf8.index(lineSlice.startIndex, offsetBy: utf8Column, limitedBy: lineSlice.endIndex) + else { + logger.fault( + """ + UTF-8 column is past the end of the line while getting UTF-16 column of \(line):\(utf8Column) \ + (\(callerFile, privacy: .public):\(callerLine, privacy: .public)) + """ + ) + return lineSlice.utf16.count + } + return lineSlice.utf16.distance(from: lineSlice.startIndex, to: stringIndex) } /// Returns UTF-8 column offset at UTF-16 based `line:column` position. /// - /// If the position does not refer to a valid position with in the snapshot, returns `nil` and logs a fault - /// containing the file and line of the caller (from `callerFile` and `callerLine`). + /// If the position does not refer to a valid position with in the snapshot, performs a bets-effort recovery and logs + /// a fault containing the file and line of the caller (from `callerFile` and `callerLine`). /// /// - parameter line: Line number (zero-based). /// - parameter utf16Column: UTF-16 column offset (zero-based). @@ -353,45 +413,31 @@ extension LineTable { utf16Column: Int, callerFile: StaticString = #fileID, callerLine: UInt = #line - ) -> Int? { - return convertColumn( - line: line, - column: utf16Column, - indexFunction: content.utf16.index(_:offsetBy:limitedBy:), - distanceFunction: content.utf8.distance(from:to:), - callerFile: callerFile, - callerLine: callerLine - ) - } - - @inlinable - func convertColumn( - line: Int, - column: Int, - indexFunction: (Substring.Index, Int, Substring.Index) -> Substring.Index?, - distanceFunction: (Substring.Index, Substring.Index) -> Int, - callerFile: StaticString = #fileID, - callerLine: UInt = #line - ) -> Int? { - guard line < count else { - logger.fault( - """ - Unable to convert column of \(line):\(column) because line is out of range \ - (\(callerFile, privacy: .public):\(callerLine, privacy: .public)) - """ - ) - return nil + ) -> Int { + let lineSlice: Substring + switch self.lineSlice(at: line, callerFile: callerFile, callerLine: callerLine) { + case .beforeFirstLine, .afterLastLine: + // This line is out-of-bounds. `lineSlice` already logged a fault. + // Recovery by assuming that UTF-8 and UTF-16 columns are similar. + return utf16Column + case .line(let line): + lineSlice = line } - let lineSlice = self[line] - guard let targetIndex = indexFunction(lineSlice.startIndex, column, lineSlice.endIndex) else { + guard + let stringIndex = lineSlice.utf16.index( + lineSlice.startIndex, + offsetBy: utf16Column, + limitedBy: lineSlice.endIndex + ) + else { logger.fault( """ - Unable to convert column of \(line):\(column) because column is out of range \ + UTF-16 column is past the end of the line while getting UTF-8 column of \(line):\(utf16Column) \ (\(callerFile, privacy: .public):\(callerLine, privacy: .public)) """ ) - return nil + return lineSlice.utf8.count } - return distanceFunction(lineSlice.startIndex, targetIndex) + return lineSlice.utf8.distance(from: lineSlice.startIndex, to: stringIndex) } } diff --git a/Sources/SKTestSupport/TestSourceKitLSPClient.swift b/Sources/SKTestSupport/TestSourceKitLSPClient.swift index d3ce52091..72fe9107d 100644 --- a/Sources/SKTestSupport/TestSourceKitLSPClient.swift +++ b/Sources/SKTestSupport/TestSourceKitLSPClient.swift @@ -357,9 +357,7 @@ public struct DocumentPositions { let lineTable = LineTable(textWithoutMarkers) positions = markers.mapValues { offset in - guard let (line, column) = lineTable.lineAndUTF16ColumnOf(utf8Offset: offset) else { - preconditionFailure("UTF-8 offset not within source file: \(offset)") - } + let (line, column) = lineTable.lineAndUTF16ColumnOf(utf8Offset: offset) return Position(line: line, utf16index: column) } } diff --git a/Sources/SourceKitLSP/DocumentManager.swift b/Sources/SourceKitLSP/DocumentManager.swift index 67d188eda..2cf11ef78 100644 --- a/Sources/SourceKitLSP/DocumentManager.swift +++ b/Sources/SourceKitLSP/DocumentManager.swift @@ -88,7 +88,6 @@ public final class DocumentManager { public enum Error: Swift.Error { case alreadyOpen(DocumentURI) case missingDocument(DocumentURI) - case failedToConvertPosition } let queue: DispatchQueue = DispatchQueue(label: "document-manager-queue") @@ -156,10 +155,7 @@ public final class DocumentManager { var sourceEdits: [SourceEdit] = [] for edit in edits { - guard let sourceEdit = SourceEdit(edit: edit, lineTableBeforeEdit: document.latestLineTable) else { - throw Error.failedToConvertPosition - } - sourceEdits.append(sourceEdit) + sourceEdits.append(SourceEdit(edit: edit, lineTableBeforeEdit: document.latestLineTable)) if let range = edit.range { document.latestLineTable.replace( @@ -234,20 +230,16 @@ fileprivate extension SourceEdit { /// /// Returns `nil` if the `TextDocumentContentChangeEvent` refers to line:column positions that don't exist in /// `LineTable`. - init?(edit: TextDocumentContentChangeEvent, lineTableBeforeEdit: LineTable) { + init(edit: TextDocumentContentChangeEvent, lineTableBeforeEdit: LineTable) { if let range = edit.range { - guard - let offset = lineTableBeforeEdit.utf8OffsetOf( - line: range.lowerBound.line, - utf16Column: range.lowerBound.utf16index - ), - let end = lineTableBeforeEdit.utf8OffsetOf( - line: range.upperBound.line, - utf16Column: range.upperBound.utf16index - ) - else { - return nil - } + let offset = lineTableBeforeEdit.utf8OffsetOf( + line: range.lowerBound.line, + utf16Column: range.lowerBound.utf16index + ) + let end = lineTableBeforeEdit.utf8OffsetOf( + line: range.upperBound.line, + utf16Column: range.upperBound.utf16index + ) self.init( range: AbsolutePosition(utf8Offset: offset).., callerFile: StaticString = #fileID, callerLine: UInt = #line) -> Substring? { - guard - let start = self.stringIndexOf( - line: range.lowerBound.line, - utf16Column: range.lowerBound.utf16index, - callerFile: callerFile, - callerLine: callerLine - ), - let end = self.stringIndexOf( - line: range.upperBound.line, - utf16Column: range.upperBound.utf16index, - callerFile: callerFile, - callerLine: callerLine - ) - else { - return nil - } + subscript(range: Range, callerFile: StaticString = #fileID, callerLine: UInt = #line) -> Substring { + let start = self.stringIndexOf( + line: range.lowerBound.line, + utf16Column: range.lowerBound.utf16index, + callerFile: callerFile, + callerLine: callerLine + ) + let end = self.stringIndexOf( + line: range.upperBound.line, + utf16Column: range.upperBound.utf16index, + callerFile: callerFile, + callerLine: callerLine + ) return self.content[start.. String { - guard - let position = snapshot.position(of: symbolLocation), - let offset = snapshot.utf8Offset(of: position) - else { - throw NameTranslationError.cannotComputeOffset(symbolLocation) - } let req = sourcekitd.dictionary([ keys.request: sourcekitd.requests.nameTranslation, keys.sourceFile: snapshot.uri.pseudoPath, keys.compilerArgs: await self.buildSettings(for: snapshot.uri)?.compilerArgs as [SKDRequestValue]?, - keys.offset: offset, + keys.offset: snapshot.utf8Offset(of: snapshot.position(of: symbolLocation)), keys.nameKind: sourcekitd.values.nameObjc, ]) @@ -947,10 +923,7 @@ extension SwiftLanguageService { /// also be a closing ']' for subscripts or the end of a trailing closure. private func findFunctionLikeRange(of position: Position, in snapshot: DocumentSnapshot) async -> Range? { let tree = await self.syntaxTreeManager.syntaxTree(for: snapshot) - guard let absolutePosition = snapshot.absolutePosition(of: position) else { - return nil - } - guard let token = tree.token(at: absolutePosition) else { + guard let token = tree.token(at: snapshot.absolutePosition(of: position)) else { return nil } @@ -1018,11 +991,10 @@ extension SwiftLanguageService { break } - if let startToken, let endToken, - let startPosition = snapshot.position(of: startToken.positionAfterSkippingLeadingTrivia), - let endPosition = snapshot.position(of: endToken.endPositionBeforeTrailingTrivia) - { - return startPosition.. [TextEdit] { - guard let position = snapshot.absolutePosition(of: renameLocation) else { - return [] - } + let position = snapshot.absolutePosition(of: renameLocation) let syntaxTree = await syntaxTreeManager.syntaxTree(for: snapshot) let token = syntaxTree.token(at: position) let parameterClause: FunctionParameterClauseSyntax? @@ -1192,11 +1162,8 @@ extension SwiftLanguageService { definitionLanguage: .swift ) - guard let parameterPosition = snapshot.position(of: parameter.positionAfterSkippingLeadingTrivia) else { - continue - } - let parameterRenameEdits = await orLog("Renaming parameter") { + let parameterPosition = snapshot.position(of: parameter.positionAfterSkippingLeadingTrivia) // Once we have lexical scope lookup in swift-syntax, this can be a purely syntactic rename. // We know that the parameters are variables and thus there can't be overloads that need to be resolved by the // type checker. @@ -1247,9 +1214,9 @@ extension SwiftLanguageService { // E.g. `func foo(a: Int)` becomes `func foo(_ a: Int)`. return TextEdit(range: piece.range, newText: " " + oldParameterName) } - if let original = snapshot.lineTable[piece.range], - case .named(let newParameterLabel) = newParameter, - newParameterLabel.trimmingCharacters(in: .whitespaces) == original.trimmingCharacters(in: .whitespaces) + if case .named(let newParameterLabel) = newParameter, + newParameterLabel.trimmingCharacters(in: .whitespaces) + == snapshot.lineTable[piece.range].trimmingCharacters(in: .whitespaces) { // We are changing the external parameter name to be the same one as the internal parameter name. The // internal name is thus no longer needed. Drop it. @@ -1336,17 +1303,18 @@ extension SwiftLanguageService { } return compoundRenameRange.pieces.compactMap { (piece) -> TextEdit? in if piece.kind == .baseName { - if let absolutePiecePosition = snapshot.absolutePosition(of: piece.range.lowerBound), - let firstNameToken = tree.token(at: absolutePiecePosition), + if let firstNameToken = tree.token(at: snapshot.absolutePosition(of: piece.range.lowerBound)), firstNameToken.keyPathInParent == \FunctionParameterSyntax.firstName, let parameterSyntax = firstNameToken.parent(as: FunctionParameterSyntax.self), - parameterSyntax.secondName == nil, // Should always be true because otherwise decl would be second name - let firstNameEndPos = snapshot.position(of: firstNameToken.endPositionBeforeTrailingTrivia) + parameterSyntax.secondName == nil // Should always be true because otherwise decl would be second name { // We are renaming a function parameter from inside the function body. // This should be a local rename and it shouldn't affect all the callers of the function. Introduce the new // name as a second name. - return TextEdit(range: firstNameEndPos.. [RenameLocation] { - return self.relatedIdentifiers.compactMap { - (relatedIdentifier) -> RenameLocation? in + return self.relatedIdentifiers.map { + (relatedIdentifier) -> RenameLocation in let position = relatedIdentifier.range.lowerBound - guard let utf8Column = snapshot.lineTable.utf8ColumnAt(line: position.line, utf16Column: position.utf16index) - else { - return nil - } + let utf8Column = snapshot.lineTable.utf8ColumnAt(line: position.line, utf16Column: position.utf16index) return RenameLocation(line: position.line + 1, utf8Column: utf8Column + 1, usage: relatedIdentifier.usage) } } diff --git a/Sources/SourceKitLSP/Swift/AdjustPositionToStartOfIdentifier.swift b/Sources/SourceKitLSP/Swift/AdjustPositionToStartOfIdentifier.swift index 209f6a748..f77e6ec4c 100644 --- a/Sources/SourceKitLSP/Swift/AdjustPositionToStartOfIdentifier.swift +++ b/Sources/SourceKitLSP/Swift/AdjustPositionToStartOfIdentifier.swift @@ -53,10 +53,7 @@ extension SwiftLanguageService { in snapshot: DocumentSnapshot ) async -> Position { let tree = await self.syntaxTreeManager.syntaxTree(for: snapshot) - guard let swiftSyntaxPosition = snapshot.absolutePosition(of: position) else { - return position - } - let visitor = StartOfIdentifierFinder(position: swiftSyntaxPosition) + let visitor = StartOfIdentifierFinder(position: snapshot.absolutePosition(of: position)) visitor.walk(tree) if let resolvedPosition = visitor.resolvedPosition { return snapshot.position(of: resolvedPosition) ?? position diff --git a/Sources/SourceKitLSP/Swift/CodeCompletion.swift b/Sources/SourceKitLSP/Swift/CodeCompletion.swift index 3d913a2e4..fae12340c 100644 --- a/Sources/SourceKitLSP/Swift/CodeCompletion.swift +++ b/Sources/SourceKitLSP/Swift/CodeCompletion.swift @@ -21,21 +21,11 @@ extension SwiftLanguageService { let snapshot = try documentManager.latestSnapshot(req.textDocument.uri) let completionPos = await adjustPositionToStartOfIdentifier(req.position, in: snapshot) - - guard let offset = snapshot.utf8Offset(of: completionPos) else { - return CompletionList(isIncomplete: true, items: []) - } + let offset = snapshot.utf8Offset(of: completionPos) + let filterText = String(snapshot.text[snapshot.indexOf(utf8Offset: offset).. TextEdit? { + ) -> TextEdit { let textEditRangeStart: Position // Compute the TextEdit @@ -456,15 +440,7 @@ class CodeCompletionSession { assert(completionPos.line == requestPosition.line) // Construct a string index for the edit range start by subtracting the UTF-8 code units to erase from the completion position. let line = snapshot.lineTable[completionPos.line] - guard - let completionPosStringIndex = snapshot.lineTable.stringIndexOf( - line: completionPos.line, - utf16Column: completionPos.utf16index - ) - else { - return nil - } - let deletionStartStringIndex = line.utf8.index(completionPosStringIndex, offsetBy: -utf8CodeUnitsToErase) + let deletionStartStringIndex = line.utf8.index(snapshot.index(of: completionPos), offsetBy: -utf8CodeUnitsToErase) // Compute the UTF-16 offset of the deletion start range. If the start lies in a previous line, this will be negative let deletionStartUtf16Offset = line.utf16.distance(from: line.startIndex, to: deletionStartStringIndex) diff --git a/Sources/SourceKitLSP/Swift/CursorInfo.swift b/Sources/SourceKitLSP/Swift/CursorInfo.swift index a76f43677..e0f75b1d7 100644 --- a/Sources/SourceKitLSP/Swift/CursorInfo.swift +++ b/Sources/SourceKitLSP/Swift/CursorInfo.swift @@ -132,9 +132,7 @@ extension SwiftLanguageService { ) async throws -> (cursorInfo: [CursorInfo], refactorActions: [SemanticRefactorCommand]) { let snapshot = try documentManager.latestSnapshot(uri) - guard let offsetRange = snapshot.utf8OffsetRange(of: range) else { - throw CursorInfoError.invalidRange(range) - } + let offsetRange = snapshot.utf8OffsetRange(of: range) let keys = self.keys diff --git a/Sources/SourceKitLSP/Swift/Diagnostic.swift b/Sources/SourceKitLSP/Swift/Diagnostic.swift index eb0880604..b47e28d5b 100644 --- a/Sources/SourceKitLSP/Swift/Diagnostic.swift +++ b/Sources/SourceKitLSP/Swift/Diagnostic.swift @@ -61,12 +61,7 @@ extension CodeAction { init?(_ fixIt: FixIt, in snapshot: DocumentSnapshot) { var textEdits = [TextEdit]() for edit in fixIt.edits { - guard let startPosition = snapshot.position(of: edit.range.lowerBound), - let endPosition = snapshot.position(of: edit.range.upperBound) - else { - continue - } - textEdits.append(TextEdit(range: startPosition.. 0 || !replacement.isEmpty { // Snippets are only suppored in code completion. @@ -136,6 +129,8 @@ extension TextEdit { return nil } + let position = snapshot.positionOf(utf8Offset: utf8Offset) + let endPosition = snapshot.positionOf(utf8Offset: utf8Offset + length) self.init(range: position.. 0, utf8Column > 0 { - range = snapshot.positionOf(zeroBasedLine: line - 1, utf8Column: utf8Column - 1).map(Range.init) + range = Range(snapshot.positionOf(zeroBasedLine: line - 1, utf8Column: utf8Column - 1)) } else if let utf8Offset: Int = diag[keys.offset] { - range = snapshot.positionOf(utf8Offset: utf8Offset).map(Range.init) + range = Range(snapshot.positionOf(utf8Offset: utf8Offset)) } // If the diagnostic has a range associated with it that starts at the same location as the diagnostics position, use it to retrieve a proper range for the diagnostic, instead of just reporting a zero-length range. (diag[keys.ranges] as SKDResponseArray?)?.forEach { index, skRange in - if let utf8Offset: Int = skRange[keys.offset], - let start = snapshot.positionOf(utf8Offset: utf8Offset), - start == range?.lowerBound, - let length: Int = skRange[keys.length], - let end = snapshot.positionOf(utf8Offset: utf8Offset + length) - { - range = start.. [TextE // Map the offset-based edits to line-column based edits to be consumed by LSP return concurrentEdits.edits.compactMap { (edit) -> TextEdit? in - guard let (startLine, startColumn) = original.lineTable.lineAndUTF16ColumnOf(utf8Offset: edit.offset) else { - return nil - } - guard let (endLine, endColumn) = original.lineTable.lineAndUTF16ColumnOf(utf8Offset: edit.endOffset) else { - return nil - } + let (startLine, startColumn) = original.lineTable.lineAndUTF16ColumnOf(utf8Offset: edit.offset) + let (endLine, endColumn) = original.lineTable.lineAndUTF16ColumnOf(utf8Offset: edit.endOffset) guard let newText = String(bytes: edit.replacement, encoding: .utf8) else { logger.fault("Failed to get String from UTF-8 bytes \(edit.replacement)") return nil diff --git a/Sources/SourceKitLSP/Swift/DocumentSymbols.swift b/Sources/SourceKitLSP/Swift/DocumentSymbols.swift index 24454b797..0fe06cfc6 100644 --- a/Sources/SourceKitLSP/Swift/DocumentSymbols.swift +++ b/Sources/SourceKitLSP/Swift/DocumentSymbols.swift @@ -74,13 +74,8 @@ fileprivate final class DocumentSymbolsFinder: SyntaxAnyVisitor { if !self.range.overlaps(range) { return .skipChildren } - guard let rangeLowerBound = snapshot.position(of: range.lowerBound), - let rangeUpperBound = snapshot.position(of: range.upperBound), - let selectionLowerBound = snapshot.position(of: selection.lowerBound), - let selectionUpperBound = snapshot.position(of: selection.upperBound) - else { - return .skipChildren - } + let positionRange = snapshot.range(of: range) + let selectionPositionRange = snapshot.range(of: selection) // Record MARK comments on the node's leading and trailing trivia in `result` not as a child of `node`. visit(node.leadingTrivia, position: node.position) @@ -94,8 +89,8 @@ fileprivate final class DocumentSymbolsFinder: SyntaxAnyVisitor { DocumentSymbol( name: name, kind: symbolKind, - range: rangeLowerBound.. RelatedIdentifiersResponse { - guard let offset = snapshot.utf8Offset(of: position) else { - throw ResponseError.unknown("invalid position \(position)") - } - let skreq = sourcekitd.dictionary([ keys.request: requests.relatedIdents, keys.cancelOnSubsequentRequest: 0, - keys.offset: offset, + keys.offset: snapshot.utf8Offset(of: position), keys.sourceFile: snapshot.uri.pseudoPath, keys.includeNonEditableBaseNames: includeNonEditableBaseNames ? 1 : 0, keys.compilerArgs: await self.buildSettings(for: snapshot.uri)?.compilerArgs as [SKDRequestValue]?, @@ -90,17 +86,14 @@ extension SwiftLanguageService { var relatedIdentifiers: [RelatedIdentifier] = [] results.forEach { _, value in - if let offset: Int = value[keys.offset], - let start: Position = snapshot.positionOf(utf8Offset: offset), - let length: Int = value[keys.length], - let end: Position = snapshot.positionOf(utf8Offset: offset + length) - { - let usage = RenameLocation.Usage(value[keys.nameType], values) ?? .unknown - relatedIdentifiers.append( - RelatedIdentifier(range: start..) - - /// The given position failed to convert to UTF-8. - case failedToRetrieveOffset(Range) - /// The underlying sourcekitd request failed with the given error. case responseError(ResponseError) @@ -100,10 +94,6 @@ enum SemanticRefactoringError: Error { extension SemanticRefactoringError: CustomStringConvertible { var description: String { switch self { - case .invalidRange(let range): - return "failed to refactor due to invalid range: \(range)" - case .failedToRetrieveOffset(let range): - return "Failed to convert range to UTF-8 offset: \(range)" case .responseError(let error): return "\(error)" case .noEditsNeeded(let url): @@ -128,14 +118,9 @@ extension SwiftLanguageService { let uri = refactorCommand.textDocument.uri let snapshot = try self.documentManager.latestSnapshot(uri) - guard let offsetRange = snapshot.utf8OffsetRange(of: refactorCommand.positionRange) else { - throw SemanticRefactoringError.failedToRetrieveOffset(refactorCommand.positionRange) - } let line = refactorCommand.positionRange.lowerBound.line let utf16Column = refactorCommand.positionRange.lowerBound.utf16index - guard let utf8Column = snapshot.lineTable.utf8ColumnAt(line: line, utf16Column: utf16Column) else { - throw SemanticRefactoringError.invalidRange(refactorCommand.positionRange) - } + let utf8Column = snapshot.lineTable.utf8ColumnAt(line: line, utf16Column: utf16Column) let skreq = sourcekitd.dictionary([ keys.request: self.requests.semanticRefactoring, @@ -146,7 +131,7 @@ extension SwiftLanguageService { // LSP is zero based, but this request is 1 based. keys.line: line + 1, keys.column: utf8Column + 1, - keys.length: offsetRange.count, + keys.length: snapshot.utf8OffsetRange(of: refactorCommand.positionRange).count, keys.actionUID: self.sourcekitd.api.uid_get_from_cstr(refactorCommand.actionString)!, keys.compilerArgs: await self.buildSettings(for: snapshot.uri)?.compilerArgs as [SKDRequestValue]?, ]) diff --git a/Sources/SourceKitLSP/Swift/SemanticTokens.swift b/Sources/SourceKitLSP/Swift/SemanticTokens.swift index ccf8acc73..47782f9b6 100644 --- a/Sources/SourceKitLSP/Swift/SemanticTokens.swift +++ b/Sources/SourceKitLSP/Swift/SemanticTokens.swift @@ -107,14 +107,7 @@ extension SyntaxClassifiedRange { return SyntaxHighlightingTokens(tokens: []) } - guard - let start: Position = snapshot.positionOf(utf8Offset: self.offset), - let end: Position = snapshot.positionOf(utf8Offset: self.endOffset) - else { - return SyntaxHighlightingTokens(tokens: []) - } - - let multiLineRange = start.. String.Index? { + /// If the offset is out-of-bounds of the snapshot, returns the closest valid index and logs a fault containing the + /// file and line of the caller (from `callerFile` and `callerLine`). + func indexOf(utf8Offset: Int, callerFile: StaticString = #fileID, callerLine: UInt = #line) -> String.Index { + guard utf8Offset >= 0 else { + logger.fault( + """ + UTF-8 offset \(utf8Offset) is negative while converting it to String.Index \ + (\(callerFile, privacy: .public):\(callerLine, privacy: .public)) + """ + ) + return text.startIndex + } guard let index = text.utf8.index(text.startIndex, offsetBy: utf8Offset, limitedBy: text.endIndex) else { logger.fault( """ - Unable to get String index for UTF-8 offset \(utf8Offset) because offset is out of range \ + UTF-8 offset \(utf8Offset) is past end of file while converting it to String.Index \ (\(callerFile, privacy: .public):\(callerLine, privacy: .public)) """ ) - return nil + return text.endIndex } return index } - // MARK: Position <-> Raw UTF-8 + // MARK: Position <-> Raw UTF-8 offset /// Converts the given UTF-16-based line:column position to the UTF-8 offset of that position within the source file. /// - /// If `position` does not refer to a valid position with in the snapshot, returns `nil` and logs a fault - /// containing the file and line of the caller (from `callerFile` and `callerLine`). - func utf8Offset(of position: Position, callerFile: StaticString = #fileID, callerLine: UInt = #line) -> Int? { + /// If `position` does not refer to a valid position with in the snapshot, returns the offset of the closest valid + /// position and logs a fault containing the file and line of the caller (from `callerFile` and `callerLine`). + func utf8Offset(of position: Position, callerFile: StaticString = #fileID, callerLine: UInt = #line) -> Int { return lineTable.utf8OffsetOf( line: position.line, utf16Column: position.utf16index, @@ -970,75 +973,78 @@ extension DocumentSnapshot { ) } - // MARK: Position <-> String.Index - - /// Converts the given UTF-16-based `line:column`` position to a `String.Index`. + /// Converts the given UTF-8 offset to a UTF-16-based line:column position. /// - /// If `position` does not refer to a valid position with in the snapshot, returns `nil` and logs a fault - /// containing the file and line of the caller (from `callerFile` and `callerLine`). - func index(of position: Position, callerFile: StaticString = #fileID, callerLine: UInt = #line) -> String.Index? { - return lineTable.stringIndexOf( - line: position.line, - utf16Column: position.utf16index, + /// If the offset is after the end of the snapshot, returns `nil` and logs a fault containing the file and line of + /// the caller (from `callerFile` and `callerLine`). + func positionOf(utf8Offset: Int, callerFile: StaticString = #fileID, callerLine: UInt = #line) -> Position { + let (line, utf16Column) = lineTable.lineAndUTF16ColumnOf( + utf8Offset: utf8Offset, callerFile: callerFile, callerLine: callerLine ) + return Position(line: line, utf16index: utf16Column) } /// Converts the given UTF-16 based line:column range to a UTF-8 based offset range. /// - /// If either the lower or upper bound of `range` do not refer to valid positions with in the snapshot, returns - /// `nil` and logs a fault containing the file and line of the caller (from `callerFile` and `callerLine`). + /// If the bounds of the range do not refer to a valid positions with in the snapshot, this function adjusts them to + /// the closest valid positions and logs a fault containing the file and line of the caller (from `callerFile` and + /// `callerLine`). func utf8OffsetRange( of range: Range, callerFile: StaticString = #fileID, callerLine: UInt = #line - ) -> Range? { - guard let startOffset = utf8Offset(of: range.lowerBound, callerFile: callerFile, callerLine: callerLine), - let endOffset = utf8Offset(of: range.upperBound, callerFile: callerFile, callerLine: callerLine) - else { - return nil - } + ) -> Range { + let startOffset = utf8Offset(of: range.lowerBound, callerFile: callerFile, callerLine: callerLine) + let endOffset = utf8Offset(of: range.upperBound, callerFile: callerFile, callerLine: callerLine) return startOffset.. String.Index + + /// Converts the given UTF-16-based `line:column` position to a `String.Index`. /// - /// If the offset is after the end of the snapshot, returns `nil` and logs a fault containing the file and line of - /// the caller (from `callerFile` and `callerLine`). - func positionOf(utf8Offset: Int, callerFile: StaticString = #fileID, callerLine: UInt = #line) -> Position? { - guard - let (line, utf16Column) = lineTable.lineAndUTF16ColumnOf( - utf8Offset: utf8Offset, - callerFile: callerFile, - callerLine: callerLine - ) - else { - return nil - } - return Position(line: line, utf16index: utf16Column) + /// If `position` does not refer to a valid position with in the snapshot, returns the index of the closest valid + /// position and logs a fault containing the file and line of the caller (from `callerFile` and `callerLine`). + func index(of position: Position, callerFile: StaticString = #fileID, callerLine: UInt = #line) -> String.Index { + return lineTable.stringIndexOf( + line: position.line, + utf16Column: position.utf16index, + callerFile: callerFile, + callerLine: callerLine + ) + } + + /// Converts the given UTF-16-based `line:column` range to a `String.Index` range. + /// + /// If the bounds of the range do not refer to a valid positions with in the snapshot, this function adjusts them to + /// the closest valid positions and logs a fault containing the file and line of the caller (from `callerFile` and + /// `callerLine`). + func indexRange( + of range: Range, + callerFile: StaticString = #fileID, + callerLine: UInt = #line + ) -> Range { + return self.index(of: range.lowerBound).. Position? { - guard - let utf16Column = lineTable.utf16ColumnAt( - line: zeroBasedLine, - utf8Column: utf8Column, - callerFile: callerFile, - callerLine: callerLine - ) - else { - return nil - } + ) -> Position { + let utf16Column = lineTable.utf16ColumnAt( + line: zeroBasedLine, + utf8Column: utf8Column, + callerFile: callerFile, + callerLine: callerLine + ) return Position(line: zeroBasedLine, utf16index: utf16Column) } @@ -1046,61 +1052,56 @@ extension DocumentSnapshot { /// Converts the given UTF-8-offset-based `AbsolutePosition` to a UTF-16-based line:column. /// - /// If the `AbsolutePosition` out of bounds of the source file, returns `nil` and logs a fault containing the file and - /// line of the caller (from `callerFile` and `callerLine`). + /// If the `AbsolutePosition` out of bounds of the source file, returns the closest valid position and logs a fault + /// containing the file and line of the caller (from `callerFile` and `callerLine`). func position( of position: AbsolutePosition, callerFile: StaticString = #fileID, callerLine: UInt = #line - ) -> Position? { + ) -> Position { return positionOf(utf8Offset: position.utf8Offset, callerFile: callerFile, callerLine: callerLine) } /// Converts the given UTF-16-based line:column `Position` to a UTF-8-offset-based `AbsolutePosition`. /// - /// If the UTF-16 based line:column pair does not refer to a valid position within the snapshot, returns `nil` and - /// logs a fault containing the file and line of the caller (from `callerFile` and `callerLine`). + /// If the UTF-16 based line:column pair does not refer to a valid position within the snapshot, returns the closest + /// valid position and logs a fault containing the file and line of the caller (from `callerFile` and `callerLine`). func absolutePosition( of position: Position, callerFile: StaticString = #fileID, callerLine: UInt = #line - ) -> AbsolutePosition? { - guard let offset = utf8Offset(of: position, callerFile: callerFile, callerLine: callerLine) else { - return nil - } + ) -> AbsolutePosition { + let offset = utf8Offset(of: position, callerFile: callerFile, callerLine: callerLine) return AbsolutePosition(utf8Offset: offset) } /// Converts the lower and upper bound of the given UTF-8-offset-based `AbsolutePosition` range to a UTF-16-based /// line:column range for use in LSP. /// - /// If either the lower or the upper bound of the range is out of bounds of the source file, returns `nil` and logs - /// a fault containing the file and line of the caller (from `callerFile` and `callerLine`). + /// If the bounds of the range do not refer to a valid positions with in the snapshot, this function adjusts them to + /// the closest valid positions and logs a fault containing the file and line of the caller (from `callerFile` and + /// `callerLine`). func range( of range: Range, callerFile: StaticString = #fileID, callerLine: UInt = #line - ) -> Range? { - guard let lowerBound = self.position(of: range.lowerBound, callerFile: callerFile, callerLine: callerLine), - let upperBound = self.position(of: range.upperBound, callerFile: callerFile, callerLine: callerLine) - else { - return nil - } + ) -> Range { + let lowerBound = self.position(of: range.lowerBound, callerFile: callerFile, callerLine: callerLine) + let upperBound = self.position(of: range.upperBound, callerFile: callerFile, callerLine: callerLine) return lowerBound.., callerFile: StaticString = #fileID, callerLine: UInt = #line - ) -> ByteSourceRange? { - guard let utf8OffsetRange = utf8OffsetRange(of: range, callerFile: callerFile, callerLine: callerLine) else { - return nil - } + ) -> ByteSourceRange { + let utf8OffsetRange = utf8OffsetRange(of: range, callerFile: callerFile, callerLine: callerLine) return ByteSourceRange(offset: utf8OffsetRange.startIndex, length: utf8OffsetRange.count) } @@ -1108,13 +1109,13 @@ extension DocumentSnapshot { /// Converts the given UTF-8-based line:column `RenamedLocation` to a UTF-16-based line:column `Position`. /// - /// If the UTF-8 based line:column pair does not refer to a valid position within the snapshot, returns `nil` and - /// logs a fault containing the file and line of the caller (from `callerFile` and `callerLine`). + /// If the UTF-8 based line:column pair does not refer to a valid position within the snapshot, returns the closest + /// valid position and logs a fault containing the file and line of the caller (from `callerFile` and `callerLine`). func position( of renameLocation: RenameLocation, callerFile: StaticString = #fileID, callerLine: UInt = #line - ) -> Position? { + ) -> Position { return positionOf( zeroBasedLine: renameLocation.line - 1, utf8Column: renameLocation.utf8Column - 1, @@ -1127,13 +1128,13 @@ extension DocumentSnapshot { /// Converts the given UTF-8-offset-based `SymbolLocation` to a UTF-16-based line:column `Position`. /// - /// If the UTF-8 offset is out-of-bounds of the snapshot, returns `nil` and logs a fault containing the file and line - /// of the caller (from `callerFile` and `callerLine`). + /// If the UTF-8 offset is out-of-bounds of the snapshot, returns the closest valid position and logs a fault + /// containing the file and line of the caller (from `callerFile` and `callerLine`). func position( of symbolLocation: SymbolLocation, callerFile: StaticString = #fileID, callerLine: UInt = #line - ) -> Position? { + ) -> Position { return positionOf( zeroBasedLine: symbolLocation.line - 1, utf8Column: symbolLocation.utf8Column - 1, @@ -1146,23 +1147,20 @@ extension DocumentSnapshot { /// Converts the given UTF-8-based line:column `RenamedLocation` to a UTF-8-offset-based `AbsolutePosition`. /// - /// If the UTF-8 based line:column pair does not refer to a valid position within the snapshot, returns `nil` and - /// logs a fault containing the file and line of the caller (from `callerFile` and `callerLine`). + /// If the UTF-8 based line:column pair does not refer to a valid position within the snapshot, returns the offset of + /// the closest valid position and logs a fault containing the file and line of the caller (from `callerFile` and + /// `callerLine`). func absolutePosition( of renameLocation: RenameLocation, callerFile: StaticString = #fileID, callerLine: UInt = #line - ) -> AbsolutePosition? { - guard - let utf8Offset = lineTable.utf8OffsetOf( - line: renameLocation.line - 1, - utf8Column: renameLocation.utf8Column - 1, - callerFile: callerFile, - callerLine: callerLine - ) - else { - return nil - } + ) -> AbsolutePosition { + let utf8Offset = lineTable.utf8OffsetOf( + line: renameLocation.line - 1, + utf8Column: renameLocation.utf8Column - 1, + callerFile: callerFile, + callerLine: callerLine + ) return AbsolutePosition(utf8Offset: utf8Offset) } } diff --git a/Sources/SourceKitLSP/Swift/SyntaxHighlightingTokenParser.swift b/Sources/SourceKitLSP/Swift/SyntaxHighlightingTokenParser.swift index 1b05cb57e..6733a7777 100644 --- a/Sources/SourceKitLSP/Swift/SyntaxHighlightingTokenParser.swift +++ b/Sources/SourceKitLSP/Swift/SyntaxHighlightingTokenParser.swift @@ -31,7 +31,6 @@ struct SyntaxHighlightingTokenParser { if let offset: Int = response[keys.offset], var length: Int = response[keys.length], - let start: Position = snapshot.positionOf(utf8Offset: offset), let skKind: sourcekitd_api_uid_t = response[keys.kind], case (let kind, var modifiers)? = parseKindAndModifiers(skKind) { @@ -39,7 +38,7 @@ struct SyntaxHighlightingTokenParser { // If the name is escaped in backticks, we need to add two characters to the // length for the backticks. if modifiers.contains(.declaration), - let index = snapshot.indexOf(utf8Offset: offset), snapshot.text[index] == "`" + snapshot.text[snapshot.indexOf(utf8Offset: offset)] == "`" { length += 2 } @@ -48,17 +47,15 @@ struct SyntaxHighlightingTokenParser { modifiers.insert(.defaultLibrary) } - if let end: Position = snapshot.positionOf(utf8Offset: offset + length) { - let multiLineRange = start.. { return [self] } - guard let startIndex = snapshot.index(of: lowerBound), - let endIndex = snapshot.index(of: upperBound) - else { - logger.fault("Range \(self) reaches outside of the document") - return [self] - } - - let text = snapshot.text[startIndex..