Merge pull request #2340 from award999/document-playgrounds

Parse #Playground macro expansions and include them in CodeLens request
This commit is contained in:
Alex Hoppen
2025-11-12 08:49:30 +01:00
committed by GitHub
10 changed files with 618 additions and 24 deletions

View File

@@ -690,6 +690,52 @@ export interface PeekDocumentsResult {
}
```
## `workspace/playgrounds`
New request for returning the list of all #Playground macros in the workspace.
Primarily designed to allow editors to provide a list of available playgrounds in the project workspace and allow
jumping to the locations where the #Playground macro was expanded.
The request fetches the list of all macros found in the workspace, returning the location, identifier, and optional label
when available for each #Playground macro expansion. If you want to keep the list of playgrounds up to date without needing to
call `workspace/playgrounds` each time a document is changed, you can filter for `swift.play` CodeLens returned by the `textDocument/codelens` request.
SourceKit-LSP will advertise `workspace/playgrounds` in its experimental server capabilities if it supports it.
- params: `WorkspacePlaygroundParams`
- result: `Playground[]`
```ts
export interface WorkspacePlaygroundParams {}
/**
* A `Playground` represents a usage of the #Playground macro, providing the editor with the
* location of the playground and identifiers to allow executing the playground through a "swift play" command.
*/
export interface Playground {
/**
* Unique identifier for the `Playground`. Client can run the playground by executing `swift play <id>`.
*
* This property is always present whether the `Playground` has a `label` or not.
*
* Follows the format output by `swift play --list`.
*/
id: string;
/**
* The label that can be used as a display name for the playground. This optional property is only available
* for named playgrounds. For example: `#Playground("hello") { print("Hello!) }` would have a `label` of `"hello"`.
*/
label?: string
/**
* The location of where the #Playground macro was used in the source code.
*/
location: Location
}
```
## `workspace/synchronize`
Request from the client to the server to wait for SourceKit-LSP to handle all ongoing requests and, optionally, wait for background activity to finish.

View File

@@ -800,7 +800,7 @@ var dependencies: [Package.Dependency] {
.package(url: "https://github.com/swiftlang/swift-docc.git", branch: relatedDependenciesBranch),
.package(url: "https://github.com/swiftlang/swift-docc-symbolkit.git", branch: relatedDependenciesBranch),
.package(url: "https://github.com/swiftlang/swift-markdown.git", branch: relatedDependenciesBranch),
.package(url: "https://github.com/swiftlang/swift-tools-protocols.git", exact: "0.0.8"),
.package(url: "https://github.com/swiftlang/swift-tools-protocols.git", exact: "0.0.9"),
.package(url: "https://github.com/swiftlang/swift-tools-support-core.git", branch: relatedDependenciesBranch),
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.1"),
.package(url: "https://github.com/swiftlang/swift-syntax.git", branch: relatedDependenciesBranch),

View File

@@ -17,7 +17,7 @@ package import SKOptions
package import SourceKitLSP
import SwiftExtensions
import TSCBasic
import ToolchainRegistry
package import ToolchainRegistry
@_spi(SourceKitLSP) import ToolsProtocolsSwiftExtensions
import XCTest
@@ -184,6 +184,7 @@ package class SwiftPMTestProject: MultiFileTestProject {
initializationOptions: LSPAny? = nil,
capabilities: ClientCapabilities = ClientCapabilities(),
options: SourceKitLSPOptions? = nil,
toolchainRegistry: ToolchainRegistry = .forTesting,
hooks: Hooks = Hooks(),
enableBackgroundIndexing: Bool = false,
usePullDiagnostics: Bool = true,
@@ -225,6 +226,7 @@ package class SwiftPMTestProject: MultiFileTestProject {
initializationOptions: initializationOptions,
capabilities: capabilities,
options: options,
toolchainRegistry: toolchainRegistry,
hooks: hooks,
enableBackgroundIndexing: enableBackgroundIndexing,
usePullDiagnostics: usePullDiagnostics,

View File

@@ -25,6 +25,7 @@ add_library(SwiftLanguageService STATIC
InlayHints.swift
MacroExpansion.swift
OpenInterface.swift
SwiftPlaygroundsScanner.swift
RefactoringEdit.swift
RefactoringResponse.swift
RelatedIdentifiers.swift

View File

@@ -21,6 +21,7 @@ import SwiftExtensions
import SwiftParser
import SwiftSyntax
import TSCExtensions
import ToolchainRegistry
@_spi(SourceKitLSP) import ToolsProtocolsSwiftExtensions
import struct TSCBasic.AbsolutePath
@@ -171,7 +172,7 @@ extension SwiftLanguageService {
options: FormattingOptions,
range: Range<Position>? = nil
) async throws -> [TextEdit]? {
guard let swiftFormat else {
guard let swiftFormat = toolchain.swiftFormat else {
throw ResponseError.unknown(
"Formatting not supported because the toolchain is missing the swift-format executable"
)

View File

@@ -10,9 +10,12 @@
//
//===----------------------------------------------------------------------===//
internal import BuildServerIntegration
import BuildServerProtocol
@_spi(SourceKitLSP) import LanguageServerProtocol
import SourceKitLSP
import SwiftSyntax
import ToolchainRegistry
/// Scans a source file for classes or structs annotated with `@main` and returns a code lens for them.
final class SwiftCodeLensScanner: SyntaxVisitor {
@@ -42,19 +45,57 @@ final class SwiftCodeLensScanner: SyntaxVisitor {
/// and returns CodeLens's with Commands to run/debug the application.
public static func findCodeLenses(
in snapshot: DocumentSnapshot,
workspace: Workspace?,
syntaxTreeManager: SyntaxTreeManager,
targetName: String? = nil,
supportedCommands: [SupportedCodeLensCommand: String]
supportedCommands: [SupportedCodeLensCommand: String],
toolchain: Toolchain
) async -> [CodeLens] {
guard snapshot.text.contains("@main") && !supportedCommands.isEmpty else {
// This is intended to filter out files that obviously do not contain an entry point.
guard !supportedCommands.isEmpty else {
return []
}
var targetDisplayName: String? = nil
if let workspace,
let target = await workspace.buildServerManager.canonicalTarget(for: snapshot.uri),
let buildTarget = await workspace.buildServerManager.buildTarget(named: target)
{
targetDisplayName = buildTarget.displayName
}
var codeLenses: [CodeLens] = []
let syntaxTree = await syntaxTreeManager.syntaxTree(for: snapshot)
let visitor = SwiftCodeLensScanner(snapshot: snapshot, targetName: targetName, supportedCommands: supportedCommands)
visitor.walk(syntaxTree)
return visitor.result
if snapshot.text.contains("@main") {
let visitor = SwiftCodeLensScanner(
snapshot: snapshot,
targetName: targetDisplayName,
supportedCommands: supportedCommands
)
visitor.walk(syntaxTree)
codeLenses += visitor.result
}
// "swift.play" CodeLens should be ignored if "swift-play" is not in the toolchain as the client has no way of running
if toolchain.swiftPlay != nil, let workspace, let playCommand = supportedCommands[SupportedCodeLensCommand.play],
snapshot.text.contains("#Playground")
{
let playgrounds = await SwiftPlaygroundsScanner.findDocumentPlaygrounds(
in: syntaxTree,
workspace: workspace,
snapshot: snapshot
)
codeLenses += playgrounds.map({
CodeLens(
range: $0.range,
command: Command(
title: "Play \"\($0.label ?? $0.id)\"",
command: playCommand,
arguments: [$0.encodeToLSPAny()]
)
)
})
}
return codeLenses
}
override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {

View File

@@ -107,7 +107,7 @@ package actor SwiftLanguageService: LanguageService, Sendable {
package let sourcekitd: SourceKitD
/// Path to the swift-format executable if it exists in the toolchain.
let swiftFormat: URL?
let toolchain: Toolchain
/// Queue on which notifications from sourcekitd are handled to ensure we are
/// handling them in-order.
@@ -213,7 +213,7 @@ package actor SwiftLanguageService: LanguageService, Sendable {
}
self.sourcekitdPath = sourcekitd
self.sourceKitLSPServer = sourceKitLSPServer
self.swiftFormat = toolchain.swiftFormat
self.toolchain = toolchain
let pluginPaths: PluginPaths?
if let clientPlugin = options.sourcekitdOrDefault.clientPlugin,
let servicePlugin = options.sourcekitdOrDefault.servicePlugin
@@ -1032,18 +1032,13 @@ extension SwiftLanguageService {
package func codeLens(_ req: CodeLensRequest) async throws -> [CodeLens] {
let snapshot = try documentManager.latestSnapshot(req.textDocument.uri)
var targetDisplayName: String? = nil
if let workspace = await sourceKitLSPServer?.workspaceForDocument(uri: req.textDocument.uri),
let target = await workspace.buildServerManager.canonicalTarget(for: req.textDocument.uri),
let buildTarget = await workspace.buildServerManager.buildTarget(named: target)
{
targetDisplayName = buildTarget.displayName
}
let workspace = await sourceKitLSPServer?.workspaceForDocument(uri: req.textDocument.uri)
return await SwiftCodeLensScanner.findCodeLenses(
in: snapshot,
workspace: workspace,
syntaxTreeManager: self.syntaxTreeManager,
targetName: targetDisplayName,
supportedCommands: self.capabilityRegistry.supportedCodeLensCommands
supportedCommands: self.capabilityRegistry.supportedCodeLensCommands,
toolchain: toolchain
)
}

View File

@@ -0,0 +1,101 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2025 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
//
//===----------------------------------------------------------------------===//
internal import BuildServerIntegration
import Foundation
@_spi(SourceKitLSP) import LanguageServerProtocol
import SKLogging
import SourceKitLSP
import SwiftParser
import SwiftSyntax
// MARK: - SwiftPlaygroundsScanner
final class SwiftPlaygroundsScanner: SyntaxVisitor {
/// The base ID to use to generate IDs for any playgrounds found in this file.
private let baseID: String
/// The snapshot of the document for which we are getting playgrounds.
private let snapshot: DocumentSnapshot
/// Accumulating the result in here.
private var result: [TextDocumentPlayground] = []
/// Keep track of if "Playgrounds" has been imported
private var isPlaygroundImported: Bool = false
private init(baseID: String, snapshot: DocumentSnapshot) {
self.baseID = baseID
self.snapshot = snapshot
super.init(viewMode: .sourceAccurate)
}
/// Designated entry point for `SwiftPlaygroundsScanner`.
static func findDocumentPlaygrounds(
in node: some SyntaxProtocol,
workspace: Workspace,
snapshot: DocumentSnapshot
) async -> [TextDocumentPlayground] {
guard let canonicalTarget = await workspace.buildServerManager.canonicalTarget(for: snapshot.uri),
let moduleName = await workspace.buildServerManager.moduleName(for: snapshot.uri, in: canonicalTarget),
let baseName = snapshot.uri.fileURL?.lastPathComponent
else {
return []
}
let visitor = SwiftPlaygroundsScanner(baseID: "\(moduleName)/\(baseName)", snapshot: snapshot)
visitor.walk(node)
return visitor.isPlaygroundImported ? visitor.result : []
}
/// Add a playground location with the given parameters to the `result` array.
private func record(
id: String,
label: String?,
range: Range<AbsolutePosition>
) {
let positionRange = snapshot.absolutePositionRange(of: range)
result.append(
TextDocumentPlayground(
id: id,
label: label,
range: positionRange,
)
)
}
override func visit(_ node: ImportPathComponentSyntax) -> SyntaxVisitorContinueKind {
if node.name.text == "Playgrounds" {
isPlaygroundImported = true
}
return .skipChildren
}
override func visit(_ node: MacroExpansionExprSyntax) -> SyntaxVisitorContinueKind {
guard node.macroName.text == "Playground" else {
return .skipChildren
}
let startPosition = snapshot.sourcekitdPosition(of: snapshot.position(of: node.positionAfterSkippingLeadingTrivia))
let stringLiteral = node.arguments.first?.expression.as(StringLiteralExprSyntax.self)
let playgroundLabel = stringLiteral?.representedLiteralValue
let playgroundID = "\(baseID):\(startPosition.line):\(startPosition.utf8Column)"
record(
id: playgroundID,
label: playgroundLabel,
range: node.trimmedRange
)
return .skipChildren
}
}

View File

@@ -89,6 +89,9 @@ public final class Toolchain: Sendable {
/// The path to the swift-format executable, if available.
package let swiftFormat: URL?
/// The path to the swift-play executable, if available.
package let swiftPlay: URL?
/// The path to the clangd language server if available.
package let clangd: URL?
@@ -203,6 +206,7 @@ public final class Toolchain: Sendable {
swift: URL? = nil,
swiftc: URL? = nil,
swiftFormat: URL? = nil,
swiftPlay: URL? = nil,
clangd: URL? = nil,
sourcekitd: URL? = nil,
sourceKitClientPlugin: URL? = nil,
@@ -216,6 +220,7 @@ public final class Toolchain: Sendable {
self.swift = swift
self.swiftc = swiftc
self.swiftFormat = swiftFormat
self.swiftPlay = swiftPlay
self.clangd = clangd
self.sourcekitd = sourcekitd
self.sourceKitClientPlugin = sourceKitClientPlugin
@@ -240,7 +245,9 @@ public final class Toolchain: Sendable {
}
}
return isSuperset(for: \.clang) && isSuperset(for: \.swift) && isSuperset(for: \.swiftc)
&& isSuperset(for: \.clangd) && isSuperset(for: \.sourcekitd) && isSuperset(for: \.libIndexStore)
&& isSuperset(for: \.swiftPlay) && isSuperset(for: \.swiftFormat) && isSuperset(for: \.sourceKitClientPlugin)
&& isSuperset(for: \.sourceKitServicePlugin) && isSuperset(for: \.clangd) && isSuperset(for: \.sourcekitd)
&& isSuperset(for: \.libIndexStore)
}
/// Same as `isSuperset` but returns `false` if both toolchains have the same set of tools.
@@ -278,6 +285,7 @@ public final class Toolchain: Sendable {
var swift: URL? = nil
var swiftc: URL? = nil
var swiftFormat: URL? = nil
var swiftPlay: URL? = nil
var sourcekitd: URL? = nil
var sourceKitClientPlugin: URL? = nil
var sourceKitServicePlugin: URL? = nil
@@ -337,6 +345,12 @@ public final class Toolchain: Sendable {
foundAny = true
}
let swiftPlayPath = binPath.appending(component: "swift-play\(execExt)")
if FileManager.default.isExecutableFile(atPath: swiftPlayPath.path) {
swiftPlay = swiftPlayPath
foundAny = true
}
// If 'currentPlatform' is nil it's most likely an unknown linux flavor.
let dylibExtension: String
if let dynamicLibraryExtension = Platform.current?.dynamicLibraryExtension {
@@ -407,6 +421,7 @@ public final class Toolchain: Sendable {
swift: swift,
swiftc: swiftc,
swiftFormat: swiftFormat,
swiftPlay: swiftPlay,
clangd: clangd,
sourcekitd: sourcekitd,
sourceKitClientPlugin: sourceKitClientPlugin,

View File

@@ -13,9 +13,58 @@
@_spi(SourceKitLSP) import LanguageServerProtocol
import SKLogging
import SKTestSupport
import ToolchainRegistry
import XCTest
fileprivate extension Toolchain {
#if compiler(>=6.4)
#warning(
"Once we require swift-play in the toolchain that's used to test SourceKit-LSP, we can just use `forTesting`"
)
#endif
static var forTestingWithSwiftPlay: Toolchain {
get async throws {
let toolchain = try await unwrap(ToolchainRegistry.forTesting.default)
return Toolchain(
identifier: "\(toolchain.identifier)-swift-swift",
displayName: "\(toolchain.identifier) with swift-play",
path: toolchain.path,
clang: toolchain.clang,
swift: toolchain.swift,
swiftc: toolchain.swiftc,
swiftPlay: URL(fileURLWithPath: "/dummy/usr/bin/swift-play"),
clangd: toolchain.clangd,
sourcekitd: toolchain.sourcekitd,
sourceKitClientPlugin: toolchain.sourceKitClientPlugin,
sourceKitServicePlugin: toolchain.sourceKitServicePlugin,
libIndexStore: toolchain.libIndexStore
)
}
}
static var forTestingWithoutSwiftPlay: Toolchain {
get async throws {
let toolchain = try await unwrap(ToolchainRegistry.forTesting.default)
return Toolchain(
identifier: "\(toolchain.identifier)-no-swift-swift",
displayName: "\(toolchain.identifier) without swift-play",
path: toolchain.path,
clang: toolchain.clang,
swift: toolchain.swift,
swiftc: toolchain.swiftc,
swiftPlay: nil,
clangd: toolchain.clangd,
sourcekitd: toolchain.sourcekitd,
sourceKitClientPlugin: toolchain.sourceKitClientPlugin,
sourceKitServicePlugin: toolchain.sourceKitServicePlugin,
libIndexStore: toolchain.libIndexStore
)
}
}
}
final class CodeLensTests: SourceKitLSPTestCase {
func testNoLenses() async throws {
var codeLensCapabilities = TextDocumentClientCapabilities.CodeLens()
codeLensCapabilities.supportedCommands = [
@@ -44,15 +93,26 @@ final class CodeLensTests: SourceKitLSPTestCase {
}
func testNoClientCodeLenses() async throws {
let toolchainRegistry = ToolchainRegistry(toolchains: [try await Toolchain.forTestingWithSwiftPlay])
let project = try await SwiftPMTestProject(
files: [
"Test.swift": """
import Playgrounds
@main
struct MyApp {
public static func main() {}
}
#Playground {
print("Hello Playground!")
}
#Playground("named") {
print("Hello named Playground!")
}
"""
]
],
toolchainRegistry: toolchainRegistry
)
let (uri, _) = try project.openDocument("Test.swift")
@@ -69,16 +129,178 @@ final class CodeLensTests: SourceKitLSPTestCase {
codeLensCapabilities.supportedCommands = [
SupportedCodeLensCommand.run: "swift.run",
SupportedCodeLensCommand.debug: "swift.debug",
SupportedCodeLensCommand.play: "swift.play",
]
let capabilities = ClientCapabilities(textDocument: TextDocumentClientCapabilities(codeLens: codeLensCapabilities))
let toolchainRegistry = ToolchainRegistry(toolchains: [try await Toolchain.forTestingWithSwiftPlay])
let project = try await SwiftPMTestProject(
files: [
"Sources/MyApp/Test.swift": """
import Playgrounds
1⃣@main2
struct MyApp {
public static func main() {}
}
3⃣#Playground {
print("Hello Playground!")
}4
5⃣#Playground("named") {
print("Hello named Playground!")
}6
"""
],
manifest: """
// swift-tools-version: 5.7
import PackageDescription
let package = Package(
name: "MyApp",
targets: [.executableTarget(name: "MyApp")]
)
""",
capabilities: capabilities,
toolchainRegistry: toolchainRegistry
)
let (uri, positions) = try project.openDocument("Test.swift")
let response = try await project.testClient.send(
CodeLensRequest(textDocument: TextDocumentIdentifier(uri))
)
XCTAssertEqual(
response,
[
CodeLens(
range: positions["1"]..<positions["2"],
command: Command(title: "Run MyApp", command: "swift.run", arguments: [.string("MyApp")])
),
CodeLens(
range: positions["1"]..<positions["2"],
command: Command(title: "Debug MyApp", command: "swift.debug", arguments: [.string("MyApp")])
),
CodeLens(
range: positions["3"]..<positions["4"],
command: Command(
title: "Play \"MyApp/Test.swift:7:1\"",
command: "swift.play",
arguments: [
TextDocumentPlayground(
id: "MyApp/Test.swift:7:1",
label: nil,
range: positions["3"]..<positions["4"],
).encodeToLSPAny()
]
)
),
CodeLens(
range: positions["5"]..<positions["6"],
command: Command(
title: "Play \"named\"",
command: "swift.play",
arguments: [
TextDocumentPlayground(
id: "MyApp/Test.swift:11:1",
label: "named",
range: positions["5"]..<positions["6"],
).encodeToLSPAny()
]
)
),
]
)
}
func testMultiplePlaygroundCodeLensOnLine() async throws {
var codeLensCapabilities = TextDocumentClientCapabilities.CodeLens()
codeLensCapabilities.supportedCommands = [
SupportedCodeLensCommand.play: "swift.play"
]
let capabilities = ClientCapabilities(textDocument: TextDocumentClientCapabilities(codeLens: codeLensCapabilities))
let toolchainRegistry = ToolchainRegistry(toolchains: [try await Toolchain.forTestingWithSwiftPlay])
let project = try await SwiftPMTestProject(
files: [
"Sources/MyLibrary/Test.swift": """
import Playgrounds
1⃣#Playground { print("Hello Playground!") }2⃣; 3⃣#Playground { print("Hello Again!") }4
"""
],
capabilities: capabilities,
toolchainRegistry: toolchainRegistry
)
let (uri, positions) = try project.openDocument("Test.swift")
let response = try await project.testClient.send(
CodeLensRequest(textDocument: TextDocumentIdentifier(uri))
)
XCTAssertEqual(
response,
[
CodeLens(
range: positions["1"]..<positions["2"],
command: Command(
title: #"Play "MyLibrary/Test.swift:2:1""#,
command: "swift.play",
arguments: [
TextDocumentPlayground(
id: "MyLibrary/Test.swift:2:1",
label: nil,
range: positions["1"]..<positions["2"],
).encodeToLSPAny()
]
)
),
CodeLens(
range: positions["3"]..<positions["4"],
command: Command(
title: "Play \"MyLibrary/Test.swift:2:46\"",
command: "swift.play",
arguments: [
TextDocumentPlayground(
id: "MyLibrary/Test.swift:2:46",
label: nil,
range: positions["3"]..<positions["4"],
).encodeToLSPAny()
]
)
),
]
)
}
func testCodeLensRequestSwiftPlayMissing() async throws {
var codeLensCapabilities = TextDocumentClientCapabilities.CodeLens()
codeLensCapabilities.supportedCommands = [
SupportedCodeLensCommand.run: "swift.run",
SupportedCodeLensCommand.debug: "swift.debug",
SupportedCodeLensCommand.play: "swift.play",
]
let capabilities = ClientCapabilities(textDocument: TextDocumentClientCapabilities(codeLens: codeLensCapabilities))
let toolchainRegistry = ToolchainRegistry(toolchains: [try await Toolchain.forTestingWithoutSwiftPlay])
let project = try await SwiftPMTestProject(
files: [
"Sources/MyApp/Test.swift": """
import Playgrounds
1⃣@main2
struct MyApp {
public static func main() {}
}
#Playground {
print("Hello Playground!")
}
#Playground("named") {
print("Hello named Playground!")
}
"""
],
manifest: """
@@ -91,7 +313,8 @@ final class CodeLensTests: SourceKitLSPTestCase {
targets: [.executableTarget(name: "MyApp")]
)
""",
capabilities: capabilities
capabilities: capabilities,
toolchainRegistry: toolchainRegistry
)
let (uri, positions) = try project.openDocument("Test.swift")
@@ -114,4 +337,173 @@ final class CodeLensTests: SourceKitLSPTestCase {
]
)
}
func testNoImportPlaygrounds() async throws {
var codeLensCapabilities = TextDocumentClientCapabilities.CodeLens()
codeLensCapabilities.supportedCommands = [
SupportedCodeLensCommand.play: "swift.play"
]
let capabilities = ClientCapabilities(textDocument: TextDocumentClientCapabilities(codeLens: codeLensCapabilities))
let toolchainRegistry = ToolchainRegistry(toolchains: [try await Toolchain.forTestingWithSwiftPlay])
let project = try await SwiftPMTestProject(
files: [
"Sources/MyLibrary/Test.swift": """
public func foo() -> String {
"bar"
}
#Playground("foo") {
print(foo())
}
#Playground {
print(foo())
}
public func bar(_ i: Int, _ j: Int) -> Int {
i + j
}
#Playground("bar") {
var i = bar(1, 2)
i = i + 1
print(i)
}
"""
],
capabilities: capabilities,
toolchainRegistry: toolchainRegistry
)
let (uri, _) = try project.openDocument("Test.swift")
let response = try await project.testClient.send(
CodeLensRequest(textDocument: TextDocumentIdentifier(uri))
)
XCTAssertEqual(response, [])
}
func testCodeLensRequestNoPlaygrounds() async throws {
var codeLensCapabilities = TextDocumentClientCapabilities.CodeLens()
codeLensCapabilities.supportedCommands = [
SupportedCodeLensCommand.play: "swift.play"
]
let capabilities = ClientCapabilities(textDocument: TextDocumentClientCapabilities(codeLens: codeLensCapabilities))
let toolchainRegistry = ToolchainRegistry(toolchains: [try await Toolchain.forTestingWithSwiftPlay])
let project = try await SwiftPMTestProject(
files: [
"Sources/MyLibrary/Test.swift": """
import Playgrounds
public func Playground(_ i: Int, _ j: Int) -> Int {
i + j
}
@Playground
struct MyPlayground {
public var playground: String = ""
}
"""
],
capabilities: capabilities,
toolchainRegistry: toolchainRegistry
)
let (uri, _) = try project.openDocument("Test.swift")
let response = try await project.testClient.send(
CodeLensRequest(textDocument: TextDocumentIdentifier(uri))
)
XCTAssertEqual(response, [])
}
func testEmojiPlaygroundName() async throws {
var codeLensCapabilities = TextDocumentClientCapabilities.CodeLens()
codeLensCapabilities.supportedCommands = [
SupportedCodeLensCommand.play: "swift.play"
]
let capabilities = ClientCapabilities(textDocument: TextDocumentClientCapabilities(codeLens: codeLensCapabilities))
let toolchainRegistry = ToolchainRegistry(toolchains: [try await Toolchain.forTestingWithSwiftPlay])
let project = try await SwiftPMTestProject(
files: [
"Sources/MyLibrary/Test.swift": """
import Playgrounds
1⃣#Playground("🧑🧑🧒🧒") { print("Hello Playground!") }2
"""
],
capabilities: capabilities,
toolchainRegistry: toolchainRegistry
)
let (uri, positions) = try project.openDocument("Test.swift")
let response = try await project.testClient.send(
CodeLensRequest(textDocument: TextDocumentIdentifier(uri))
)
XCTAssertEqual(
response,
[
CodeLens(
range: positions["1"]..<positions["2"],
command: Command(
title: #"Play "🧑🧑🧒🧒""#,
command: "swift.play",
arguments: [
TextDocumentPlayground(
id: "MyLibrary/Test.swift:2:1",
label: "🧑‍🧑‍🧒‍🧒",
range: positions["1"]..<positions["2"],
).encodeToLSPAny()
]
)
)
]
)
}
func testUtf8PlaygroundOffset() async throws {
var codeLensCapabilities = TextDocumentClientCapabilities.CodeLens()
codeLensCapabilities.supportedCommands = [
SupportedCodeLensCommand.play: "swift.play"
]
let capabilities = ClientCapabilities(textDocument: TextDocumentClientCapabilities(codeLens: codeLensCapabilities))
let toolchainRegistry = ToolchainRegistry(toolchains: [try await Toolchain.forTestingWithSwiftPlay])
let project = try await SwiftPMTestProject(
files: [
"Sources/MyLibrary/Test.swift": """
import Playgrounds
/* 🧑‍🧑‍🧒‍🧒 */ 1⃣#Playground { print("Hello Playground!") }2
"""
],
capabilities: capabilities,
toolchainRegistry: toolchainRegistry
)
let (uri, positions) = try project.openDocument("Test.swift")
let response = try await project.testClient.send(
CodeLensRequest(textDocument: TextDocumentIdentifier(uri))
)
XCTAssertEqual(
response,
[
CodeLens(
range: positions["1"]..<positions["2"],
command: Command(
title: #"Play "MyLibrary/Test.swift:2:33""#,
command: "swift.play",
arguments: [
TextDocumentPlayground(
id: "MyLibrary/Test.swift:2:33",
label: nil,
range: positions["1"]..<positions["2"],
).encodeToLSPAny()
]
)
)
]
)
}
}