mirror of
https://github.com/apple/sourcekit-lsp.git
synced 2026-10-09 06:49:00 +02:00
190 lines
7.2 KiB
Swift
190 lines
7.2 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
//
|
|
// This source file is part of the Swift.org open source project
|
|
//
|
|
// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
|
|
// Licensed under Apache License v2.0 with Runtime Library Exception
|
|
//
|
|
// See https://swift.org/LICENSE.txt for license information
|
|
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
@_spi(SourceKitLSP) import LanguageServerProtocol
|
|
import SKTestSupport
|
|
import SKUtilities
|
|
import XCTest
|
|
|
|
internal func apply(edits: [TextEdit], to source: String) -> String {
|
|
var lineTable = LineTable(source)
|
|
let edits = edits.sorted(by: { $0.range.lowerBound < $1.range.lowerBound })
|
|
for edit in edits.reversed() {
|
|
lineTable.replace(
|
|
fromLine: edit.range.lowerBound.line,
|
|
utf16Offset: edit.range.lowerBound.utf16index,
|
|
toLine: edit.range.upperBound.line,
|
|
utf16Offset: edit.range.upperBound.utf16index,
|
|
with: edit.newText
|
|
)
|
|
}
|
|
return lineTable.content
|
|
}
|
|
|
|
/// Perform a rename request at every location marker in `markedSource`, renaming it to `newName`.
|
|
/// Test that applying the edits returned from the requests always result in `expected`.
|
|
func assertSingleFileRename(
|
|
_ markedSource: String,
|
|
language: Language = .swift,
|
|
newName: String,
|
|
expectedPrepareRenamePlaceholder: String,
|
|
expected: String,
|
|
testName: String = #function,
|
|
file: StaticString = #filePath,
|
|
line: UInt = #line
|
|
) async throws {
|
|
let testClient = try await TestSourceKitLSPClient()
|
|
let uri = DocumentURI(for: language, testName: testName)
|
|
let positions = testClient.openDocument(markedSource, uri: uri, language: language)
|
|
guard !positions.allMarkers.isEmpty else {
|
|
XCTFail("Test case did not contain any markers at which to invoke the rename", file: file, line: line)
|
|
return
|
|
}
|
|
for marker in positions.allMarkers {
|
|
let position = positions[marker]
|
|
let prepareRenameResponse = try await testClient.send(
|
|
PrepareRenameRequest(textDocument: TextDocumentIdentifier(uri), position: position)
|
|
)
|
|
if let prepareRenameResponse {
|
|
XCTAssertEqual(
|
|
prepareRenameResponse.placeholder,
|
|
expectedPrepareRenamePlaceholder,
|
|
"Prepare rename placeholder does not match while performing rename at \(marker)",
|
|
file: file,
|
|
line: line
|
|
)
|
|
// VS Code considers the upper bound of a range as part of the identifier so both `contains` and equality in
|
|
// `upperBound` are fine.
|
|
XCTAssert(
|
|
prepareRenameResponse.range.contains(position) || prepareRenameResponse.range.upperBound == position,
|
|
"Prepare rename range \(prepareRenameResponse.range) does not contain rename position \(position)",
|
|
file: file,
|
|
line: line
|
|
)
|
|
} else {
|
|
XCTFail("Expected non-nil prepareRename response", file: file, line: line)
|
|
}
|
|
|
|
let response = try await testClient.send(
|
|
RenameRequest(
|
|
textDocument: TextDocumentIdentifier(uri),
|
|
position: positions[marker],
|
|
newName: newName
|
|
)
|
|
)
|
|
let edits = try XCTUnwrap(response?.changes?[uri], "while performing rename at \(marker)", file: file, line: line)
|
|
let source = extractMarkers(markedSource).textWithoutMarkers
|
|
let renamed = apply(edits: edits, to: source)
|
|
XCTAssertEqual(renamed, expected, "while performing rename at \(marker)", file: file, line: line)
|
|
}
|
|
}
|
|
|
|
/// Assert that applying changes to `originalFiles` results in `expected`.
|
|
///
|
|
/// Upon failure, `message` is added to the XCTest failure messages to provide context which rename failed.
|
|
func assertRenamedSourceMatches(
|
|
originalFiles: [RelativeFileLocation: String],
|
|
changes: [DocumentURI: [TextEdit]],
|
|
expected: [RelativeFileLocation: String],
|
|
in ws: MultiFileTestProject,
|
|
message: String,
|
|
testName: String = #function,
|
|
file: StaticString = #filePath,
|
|
line: UInt = #line
|
|
) throws {
|
|
for (expectedFileLocation, expectedRenamed) in expected {
|
|
let originalMarkedSource = try XCTUnwrap(
|
|
originalFiles[expectedFileLocation],
|
|
"No original source for \(expectedFileLocation.fileName) specified; \(message)",
|
|
file: file,
|
|
line: line
|
|
)
|
|
let originalSource = extractMarkers(originalMarkedSource).textWithoutMarkers
|
|
let edits = changes[try ws.uri(for: expectedFileLocation.fileName)] ?? []
|
|
let renamed = apply(edits: edits, to: originalSource)
|
|
XCTAssertEqual(
|
|
renamed,
|
|
expectedRenamed,
|
|
"applying edits did not match expected renamed source for \(expectedFileLocation.fileName); \(message)",
|
|
file: file,
|
|
line: line
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Perform a rename request at every location marker except 0️⃣ in `files`, renaming it to `newName`. The location
|
|
/// marker 0️⃣ is intended to be used as an anchor for `preRenameActions`.
|
|
///
|
|
/// Test that applying the edits returned from the requests always result in `expected`.
|
|
///
|
|
/// `preRenameActions` is executed after opening the workspace but before performing the rename. This allows a workspace
|
|
/// to be placed in a state where there are in-memory changes that haven't been written to disk yet.
|
|
func assertMultiFileRename(
|
|
files: [RelativeFileLocation: String],
|
|
headerFileLanguage: Language? = nil,
|
|
newName: String,
|
|
expectedPrepareRenamePlaceholder: String,
|
|
expected: [RelativeFileLocation: String],
|
|
manifest: String = SwiftPMTestProject.defaultPackageManifest,
|
|
preRenameActions: (SwiftPMTestProject) throws -> Void = { _ in },
|
|
testName: String = #function,
|
|
file: StaticString = #filePath,
|
|
line: UInt = #line
|
|
) async throws {
|
|
let project = try await SwiftPMTestProject(
|
|
files: files,
|
|
manifest: manifest,
|
|
enableBackgroundIndexing: true,
|
|
testName: testName
|
|
)
|
|
try preRenameActions(project)
|
|
for (fileLocation, markedSource) in files.sorted(by: { $0.key.fileName < $1.key.fileName }) {
|
|
let markers = extractMarkers(markedSource).markers.keys.sorted().filter { $0 != "0️⃣" }
|
|
if markers.isEmpty {
|
|
continue
|
|
}
|
|
let (uri, positions) = try project.openDocument(
|
|
fileLocation.fileName,
|
|
language: fileLocation.fileName.hasSuffix(".h") ? headerFileLanguage : nil
|
|
)
|
|
defer {
|
|
project.testClient.send(DidCloseTextDocumentNotification(textDocument: TextDocumentIdentifier(uri)))
|
|
}
|
|
for marker in markers {
|
|
let prepareRenameResponse = try await project.testClient.send(
|
|
PrepareRenameRequest(textDocument: TextDocumentIdentifier(uri), position: positions[marker])
|
|
)
|
|
XCTAssertEqual(
|
|
prepareRenameResponse?.placeholder,
|
|
expectedPrepareRenamePlaceholder,
|
|
"Prepare rename placeholder does not match while performing rename at \(marker)",
|
|
file: file,
|
|
line: line
|
|
)
|
|
|
|
let response = try await project.testClient.send(
|
|
RenameRequest(textDocument: TextDocumentIdentifier(uri), position: positions[marker], newName: newName)
|
|
)
|
|
let changes = try XCTUnwrap(response?.changes, "Did not receive any edits", file: file, line: line)
|
|
try assertRenamedSourceMatches(
|
|
originalFiles: files,
|
|
changes: changes,
|
|
expected: expected,
|
|
in: project,
|
|
message: "while performing rename at \(marker)",
|
|
file: file,
|
|
line: line
|
|
)
|
|
}
|
|
}
|
|
}
|