Files
sourcekit-lsp/Tests/SourceKitLSPTests/ExecuteCommandTests.swift
Alex Hoppen 90e0f3f5fa Support expansion of nested macros
The basic idea is that a `sourcekit-lsp://swift-macro-expansion` URL should have sufficient information to reconstruct the contents of that macro buffer without relying on any state in SourceKit-LSP. The benefit of not having any cross-request state in SourceKit-LSP is that an editor might can send the `workspace/getReferenceDocument` request at any time and it will succeed independent of the previous requests. Furthermore, we can always get the contents of the macro expansion to form a `DocumentSnapshot`, which can be used to provide semantic functionality inside macro expansion buffers.

To do that, the `sourcekit-lsp:` URL scheme was changed to have a parent instead of a `primary`, which is the URI of the document that the buffer was expanded from. For nested macro expansions, this will be a `sourcekit-lsp://swift-macro-expansion` URL itself.

With that parent, we can reconstruct the macro expansion chain all the way from the primary source file. To avoid sending the same expand macro request to sourcekitd all the time, we introduce `MacroExpansionManager`, which caches the last 10 macro expansions.

`SwiftLanguageService` now has a `latestSnapshot` method that returns the contents of the reference document when asked for a reference document URL and only consults the document manager for other URIs. To support semantic functionality in macro expansion buffers, we need to call that `latestSnapshot` method so we have a document snapshot of the macro expansion buffer for position conversions and pass the following to the sourcekitd requests.
```
keys.sourceFile: snapshot.uri.sourcekitdSourceFile,
keys.primaryFile: snapshot.uri.primaryFile?.pseudoPath,
```

We should consider if there’s a way to make the `latestSnapshot` method on `documentManager` less accessible so that the method which also returns snapshots for reference documents is the one being used by default.

Co-Authored-By: Lokesh T R <lokesh.t.r.official@gmail.com>
2024-08-16 14:51:05 -07:00

192 lines
6.2 KiB
Swift
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2019 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
//
//===----------------------------------------------------------------------===//
import LanguageServerProtocol
import SKOptions
import SKTestSupport
@_spi(Testing) import SourceKitLSP
import SwiftExtensions
import XCTest
final class ExecuteCommandTests: XCTestCase {
func testLocationSemanticRefactoring() async throws {
let testClient = try await TestSourceKitLSPClient()
let uri = DocumentURI(for: .swift)
let positions = testClient.openDocument(
"""
func foo() {
1"hello2"3
}
""",
uri: uri
)
let args = SemanticRefactorCommand(
title: "Localize String",
actionString: "source.refactoring.kind.localize.string",
positionRange: Range(positions["2"]),
textDocument: TextDocumentIdentifier(uri)
)
let metadata = SourceKitLSPCommandMetadata(textDocument: TextDocumentIdentifier(uri))
var command = args.asCommand()
command.arguments?.append(metadata.encodeToLSPAny())
let request = ExecuteCommandRequest(command: command.command, arguments: command.arguments)
let expectation = self.expectation(description: "Handle ApplyEditRequest")
let applyEditTitle = ThreadSafeBox<String?>(initialValue: nil)
let applyEditWorkspaceEdit = ThreadSafeBox<WorkspaceEdit?>(initialValue: nil)
testClient.handleSingleRequest { (req: ApplyEditRequest) -> ApplyEditResponse in
applyEditTitle.value = req.label
applyEditWorkspaceEdit.value = req.edit
expectation.fulfill()
return ApplyEditResponse(applied: true, failureReason: nil)
}
let _ = try await testClient.send(request)
try await fulfillmentOfOrThrow([expectation])
let label = try XCTUnwrap(applyEditTitle.value)
let edit = try XCTUnwrap(applyEditWorkspaceEdit.value)
XCTAssertEqual(label, "Localize String")
XCTAssertEqual(
edit,
WorkspaceEdit(changes: [
uri: [
TextEdit(
range: Range(positions["1"]),
newText: "NSLocalizedString("
),
TextEdit(
range: Range(positions["3"]),
newText: ", comment: \"\")"
),
]
])
)
}
func testRangeSemanticRefactoring() async throws {
let testClient = try await TestSourceKitLSPClient()
let uri = DocumentURI(for: .swift)
let positions = testClient.openDocument(
"""
func foo() -> String {
1⃣var a = "hello"
return a2
}
""",
uri: uri
)
let args = SemanticRefactorCommand(
title: "Extract Method",
actionString: "source.refactoring.kind.extract.function",
positionRange: positions["1"]..<positions["2"],
textDocument: TextDocumentIdentifier(uri)
)
let metadata = SourceKitLSPCommandMetadata(textDocument: TextDocumentIdentifier(uri))
var command = args.asCommand()
command.arguments?.append(metadata.encodeToLSPAny())
let request = ExecuteCommandRequest(command: command.command, arguments: command.arguments)
let expectation = self.expectation(description: "Handle ApplyEditRequest")
let applyEditTitle = ThreadSafeBox<String?>(initialValue: nil)
let applyEditWorkspaceEdit = ThreadSafeBox<WorkspaceEdit?>(initialValue: nil)
testClient.handleSingleRequest { (req: ApplyEditRequest) -> ApplyEditResponse in
applyEditTitle.value = req.label
applyEditWorkspaceEdit.value = req.edit
expectation.fulfill()
return ApplyEditResponse(applied: true, failureReason: nil)
}
let _ = try await testClient.send(request)
try await fulfillmentOfOrThrow([expectation])
let label = try XCTUnwrap(applyEditTitle.value)
let edit = try XCTUnwrap(applyEditWorkspaceEdit.value)
XCTAssertEqual(label, "Extract Method")
XCTAssertEqual(
edit,
WorkspaceEdit(changes: [
uri: [
TextEdit(
range: Range(Position(line: 0, utf16index: 0)),
newText:
"""
fileprivate func extractedFunc() -> String {
var a = "hello"
return a
}
"""
),
TextEdit(
range: positions["1"]..<positions["2"],
newText: "return extractedFunc()"
),
]
])
)
}
func testLSPCommandMetadataRetrieval() {
var req = ExecuteCommandRequest(command: "", arguments: nil)
XCTAssertNil(req.metadata)
req.arguments = [1, 2, ""]
XCTAssertNil(req.metadata)
let url = URL(fileURLWithPath: "/a.swift")
let textDocument = TextDocumentIdentifier(url)
let metadata = SourceKitLSPCommandMetadata(textDocument: textDocument)
req.arguments = [metadata.encodeToLSPAny(), 1, 2, ""]
XCTAssertNil(req.metadata)
req.arguments = [1, 2, "", [metadata.encodeToLSPAny()]]
XCTAssertNil(req.metadata)
req.arguments = [1, 2, "", metadata.encodeToLSPAny()]
XCTAssertEqual(req.metadata, metadata)
req.arguments = [metadata.encodeToLSPAny()]
XCTAssertEqual(req.metadata, metadata)
}
func testLSPCommandMetadataRemoval() {
var req = ExecuteCommandRequest(command: "", arguments: nil)
XCTAssertNil(req.argumentsWithoutSourceKitMetadata)
req.arguments = [1, 2, ""]
XCTAssertEqual(req.arguments, req.argumentsWithoutSourceKitMetadata)
let url = URL(fileURLWithPath: "/a.swift")
let textDocument = TextDocumentIdentifier(url)
let metadata = SourceKitLSPCommandMetadata(textDocument: textDocument)
req.arguments = [metadata.encodeToLSPAny(), 1, 2, ""]
XCTAssertEqual(req.arguments, req.argumentsWithoutSourceKitMetadata)
req.arguments = [1, 2, "", [metadata.encodeToLSPAny()]]
XCTAssertEqual(req.arguments, req.argumentsWithoutSourceKitMetadata)
req.arguments = [1, 2, "", metadata.encodeToLSPAny()]
XCTAssertEqual([1, 2, ""], req.argumentsWithoutSourceKitMetadata)
}
}