diff --git a/Sources/SKUtilities/LineTable.swift b/Sources/SKUtilities/LineTable.swift index 2d9abd05d..bd6ea4a39 100644 --- a/Sources/SKUtilities/LineTable.swift +++ b/Sources/SKUtilities/LineTable.swift @@ -115,45 +115,45 @@ extension LineTable { self.replace(fromLine: fromLine, utf16Offset: fromOff, toLine: toLine, utf16Offset: toOff, with: replacement) } - /// Replace the line table's `content` in the given range and update the line data. - /// - /// - parameter fromLine: Starting line number (zero-based). - /// - parameter fromOff: Starting UTF-8 column offset (zero-based). - /// - parameter toLine: Ending line number (zero-based). - /// - parameter toOff: Ending UTF-8 column offset (zero-based). - /// - parameter replacement: The new text for the given range. - @inlinable - mutating package func replace( - fromLine: Int, - utf8Offset fromOff: Int, - toLine: Int, - utf8Offset toOff: Int, - with replacement: String - ) { - let start = content.utf8.index(impl[fromLine], offsetBy: fromOff) - let end = content.utf8.index(impl[toLine], offsetBy: toOff) - - var newText = self.content - newText.replaceSubrange(start..= 0, length >= 0, + let start = utf8.index(utf8.startIndex, offsetBy: fromOff, limitedBy: utf8.endIndex), + let end = utf8.index(start, offsetBy: length, limitedBy: utf8.endIndex) + else { + throw OutOfBoundsError( + utf8Range: (lower: fromOff, upper: fromOff + length), + utf8Bounds: (lower: 0, upper: utf8.count) + ) + } var newText = self.content newText.replaceSubrange(start.. Int {} + } + + """ + var fullText = typeWithMethod + + try await sourcekitd.editDocument(path, fromOffset: 0, length: 0, newContents: typeWithMethod) + + let completion = """ + S. + """ + fullText += completion + + try await sourcekitd.editDocument(path, fromOffset: typeWithMethod.utf8.count, length: 0, newContents: completion) + + func testCompletion(file: StaticString = #filePath, line: UInt = #line) async throws { + let result = try await sourcekitd.completeOpen( + path: path, + position: Position(line: 3, utf16index: 2), + filter: "foo", + flags: [] + ) + XCTAssertGreaterThan(result.unfilteredResultCount, 1, file: file, line: line) + XCTAssertEqual(result.items.count, 1, file: file, line: line) + } + try await testCompletion() + + // Bogus edits are ignored (negative offsets crash SourceKit itself so we don't test them here). + await assertThrowsError( + try await sourcekitd.editDocument(path, fromOffset: 0, length: 99999, newContents: "") + ) + await assertThrowsError( + try await sourcekitd.editDocument(path, fromOffset: 99999, length: 1, newContents: "") + ) + await assertThrowsError( + try await sourcekitd.editDocument(path, fromOffset: 99999, length: 0, newContents: "unrelated") + ) + // SourceKit doesn't throw an error for a no-op edit. + try await sourcekitd.editDocument(path, fromOffset: 99999, length: 0, newContents: "") + + try await sourcekitd.editDocument(path, fromOffset: 0, length: 0, newContents: "") + try await sourcekitd.editDocument(path, fromOffset: fullText.utf8.count, length: 0, newContents: "") + + try await testCompletion() + + let badCompletion = """ + X. + """ + fullText = fullText.dropLast(2) + badCompletion + + try await sourcekitd.editDocument(path, fromOffset: fullText.utf8.count - 2, length: 2, newContents: badCompletion) + + let result = try await sourcekitd.completeOpen( + path: path, + position: Position(line: 3, utf16index: 2), + filter: "foo", + flags: [] + ) + XCTAssertEqual(result.unfilteredResultCount, 0) + XCTAssertEqual(result.items.count, 0) + } + func testDocumentation() async throws { try await SkipUnless.sourcekitdSupportsPlugin() let sourcekitd = try await getSourceKitD()