mirror of
https://github.com/apple/sourcekit-lsp.git
synced 2026-03-02 18:23:24 +01:00
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>
192 lines
6.2 KiB
Swift
192 lines
6.2 KiB
Swift
//===----------------------------------------------------------------------===//
|
||
//
|
||
// 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)
|
||
}
|
||
}
|