Skip to content

Commit 55868f3

Browse files
committed
Support swift.play in textDocument/codelens request
- New `swift.play` CodeLens support that is an experimental feature while [swift play](https://github.com/apple/swift-play-experimental/) is still experimental - Add #Playground macro visitor to parse the macro expansions - File must `import Playgrounds` to record the macro expansion - The `swift-play` binary must exist in the toolchain to - TextDocumentPlayground will record the id and optionally label to match detail you get from ``` $ swift play --list Building for debugging... Found 1 Playground * Fibonacci/Fibonacci.swift:23 "Fibonacci" ``` - Add LSP extension documentation for designing pending `workspace/playground` request - Add new parsing test cases - Update CMake files Issue: #2339 #2343
1 parent f384851 commit 55868f3

File tree

14 files changed

+527
-22
lines changed

14 files changed

+527
-22
lines changed

Contributor Documentation/LSP Extensions.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -690,6 +690,52 @@ export interface PeekDocumentsResult {
690690
}
691691
```
692692
693+
## `workspace/playgrounds`
694+
695+
New request for return the list of all #Playground macro expansions in the workspace.
696+
697+
Primarily designed to allow editors to provide a list of available playgrounds in the project workspace and allow
698+
jumping to the locations where the #Playground macro was expanded.
699+
700+
The request fetches the list of all macro expansions found in the workspace, returning the location, identifier, and optional label
701+
when available for each #Playground macro expansion. If you want to keep the list of playgrounds up to date without needing to
702+
call `workspace/playgrounds` each time a document is changed, you can filter for `swift.play` CodeLens returned by the `textDocument/codelens` request.
703+
704+
SourceKit-LSP will advertise `workspace/playgrounds` in its experimental server capabilities if it supports it.
705+
706+
- params: `WorkspacePlaygroundParams`
707+
- result: `Playground[]`
708+
709+
```ts
710+
export interface WorkspacePlaygroundParams {}
711+
712+
/**
713+
* A `Playground` represents an expansion of the #Playground macro, providing the editor with the
714+
* location of the playground and identifiers to allow executing the playground through a "swift play" command.
715+
*/
716+
export interface Playground {
717+
/**
718+
* Unique identifier for the `Playground`. Client can run the playground by executing `swift play <id>`.
719+
*
720+
* This property is always present whether the `Playground` has a `label` or not.
721+
*
722+
* Follows the format output by `swift play --list`.
723+
*/
724+
id: string;
725+
726+
/**
727+
* The label that can be used as a display name for the playground. This optional property is only available
728+
* for named playgrounds. For example: `#Playground("hello") { print("Hello!) }` would have a `label` of `"hello"`.
729+
*/
730+
label?: string
731+
732+
/**
733+
* The location of the of where the #Playground macro expansion occured in the source code.
734+
*/
735+
location: Location
736+
}
737+
```
738+
693739
## `workspace/synchronize`
694740
695741
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.

Sources/LanguageServerProtocol/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ add_library(LanguageServerProtocol STATIC
122122
SupportTypes/NotebookDocument.swift
123123
SupportTypes/NotebookDocumentChangeEvent.swift
124124
SupportTypes/NotebookDocumentIdentifier.swift
125+
SupportTypes/Playground.swift
125126
SupportTypes/Position.swift
126127
SupportTypes/PositionEncoding.swift
127128
SupportTypes/ProgressToken.swift
@@ -138,6 +139,7 @@ add_library(LanguageServerProtocol STATIC
138139
SupportTypes/TextDocumentEdit.swift
139140
SupportTypes/TextDocumentIdentifier.swift
140141
SupportTypes/TextDocumentItem.swift
142+
SupportTypes/TextDocumentPlayground.swift
141143
SupportTypes/TextDocumentSaveReason.swift
142144
SupportTypes/TextEdit.swift
143145
SupportTypes/Tracing.swift

Sources/LanguageServerProtocol/SupportTypes/Location.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,11 @@ public struct Location: ResponseType, Hashable, Codable, CustomDebugStringConver
3737
public var debugDescription: String {
3838
return "\(uri):\(range.lowerBound)-\(range.upperBound)"
3939
}
40+
41+
public func encodeToLSPAny() -> LSPAny {
42+
return .dictionary([
43+
"uri": .string(uri.stringValue),
44+
"range": range.encodeToLSPAny()
45+
])
46+
}
4047
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
/// A playground item that can be used to identify playgrounds alongside a source file.
14+
public struct Playground: ResponseType, Equatable {
15+
/// Identifier for the `Playground`.
16+
///
17+
/// This identifier uniquely identifies the playground. It can be used to run an individual playground with `swift play`.
18+
public var id: String
19+
20+
/// Display name describing the playground.
21+
public var label: String?
22+
23+
/// The location of the #Playground macro expansion in the source code.
24+
public var location: Location
25+
26+
public init(
27+
id: String,
28+
label: String?,
29+
location: Location,
30+
) {
31+
self.id = id
32+
self.label = label
33+
self.location = location
34+
}
35+
36+
public func encodeToLSPAny() -> LSPAny {
37+
var dict: [String: LSPAny] = [
38+
"id": .string(id),
39+
"location": location.encodeToLSPAny()
40+
]
41+
42+
if let label {
43+
dict["label"] = .string(label)
44+
}
45+
46+
return .dictionary(dict)
47+
}
48+
}

Sources/LanguageServerProtocol/SupportTypes/SupportedCodeLensCommand.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,7 @@ public struct SupportedCodeLensCommand: Codable, Hashable, RawRepresentable, Sen
2626

2727
/// Lens to debug the application
2828
public static let debug: Self = Self(rawValue: "swift.debug")
29+
30+
/// Lens to run the playground
31+
public static let play: Self = Self(rawValue: "swift.play")
2932
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
/// A playground item that can be used to identify playground. Differs from `TextDocumentPlayground`
14+
/// by not including location which is given for `textDocument/playgrounds` request
15+
public struct TextDocumentPlayground: ResponseType, Equatable {
16+
/// Identifier for the `TextDocumentPlayground`.
17+
///
18+
/// This identifier uniquely identifies the playground. It can be used to run an individual playground with `swift play`.
19+
public var id: String
20+
21+
/// Display name describing the playground.
22+
public var label: String?
23+
24+
/// The range of the #Playground macro expansion in the given file.
25+
public var range: Range<Position>
26+
27+
public init(
28+
id: String,
29+
label: String?,
30+
range: Range<Position>
31+
) {
32+
self.id = id
33+
self.label = label
34+
self.range = range
35+
}
36+
37+
public func encodeToLSPAny() -> LSPAny {
38+
var dict: [String: LSPAny] = [
39+
"id": .string(id),
40+
"range": range.encodeToLSPAny()
41+
]
42+
43+
if let label {
44+
dict["label"] = .string(label)
45+
}
46+
47+
return .dictionary(dict)
48+
}
49+
}

Sources/SKTestSupport/SwiftPMTestProject.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ package import SKOptions
1717
package import SourceKitLSP
1818
import SwiftExtensions
1919
import TSCBasic
20-
import ToolchainRegistry
20+
package import ToolchainRegistry
2121
import XCTest
2222

2323
package class SwiftPMTestProject: MultiFileTestProject {
@@ -183,6 +183,7 @@ package class SwiftPMTestProject: MultiFileTestProject {
183183
initializationOptions: LSPAny? = nil,
184184
capabilities: ClientCapabilities = ClientCapabilities(),
185185
options: SourceKitLSPOptions? = nil,
186+
toolchainRegistry: ToolchainRegistry = .forTesting,
186187
hooks: Hooks = Hooks(),
187188
enableBackgroundIndexing: Bool = false,
188189
usePullDiagnostics: Bool = true,
@@ -224,6 +225,7 @@ package class SwiftPMTestProject: MultiFileTestProject {
224225
initializationOptions: initializationOptions,
225226
capabilities: capabilities,
226227
options: options,
228+
toolchainRegistry: toolchainRegistry,
227229
hooks: hooks,
228230
enableBackgroundIndexing: enableBackgroundIndexing,
229231
usePullDiagnostics: usePullDiagnostics,

Sources/SwiftLanguageService/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ add_library(SwiftLanguageService STATIC
2525
InlayHints.swift
2626
MacroExpansion.swift
2727
OpenInterface.swift
28+
PlaygroundDiscovery.swift
2829
RefactoringEdit.swift
2930
RefactoringResponse.swift
3031
RelatedIdentifiers.swift

Sources/SwiftLanguageService/DocumentFormatting.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import SourceKitLSP
2020
import SwiftExtensions
2121
import SwiftParser
2222
import SwiftSyntax
23+
import ToolchainRegistry
2324
import TSCExtensions
2425

2526
import struct TSCBasic.AbsolutePath
@@ -170,7 +171,7 @@ extension SwiftLanguageService {
170171
options: FormattingOptions,
171172
range: Range<Position>? = nil
172173
) async throws -> [TextEdit]? {
173-
guard let swiftFormat else {
174+
guard let swiftFormat = toolchain.swiftFormat else {
174175
throw ResponseError.unknown(
175176
"Formatting not supported because the toolchain is missing the swift-format executable"
176177
)

Sources/SwiftLanguageService/SwiftCodeLensScanner.swift

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,12 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13+
internal import BuildServerIntegration
14+
import BuildServerProtocol
1315
import LanguageServerProtocol
1416
import SourceKitLSP
1517
import SwiftSyntax
18+
import ToolchainRegistry
1619

1720
/// Scans a source file for classes or structs annotated with `@main` and returns a code lens for them.
1821
final class SwiftCodeLensScanner: SyntaxVisitor {
@@ -42,19 +45,38 @@ final class SwiftCodeLensScanner: SyntaxVisitor {
4245
/// and returns CodeLens's with Commands to run/debug the application.
4346
public static func findCodeLenses(
4447
in snapshot: DocumentSnapshot,
48+
workspace: Workspace?,
4549
syntaxTreeManager: SyntaxTreeManager,
46-
targetName: String? = nil,
47-
supportedCommands: [SupportedCodeLensCommand: String]
50+
supportedCommands: [SupportedCodeLensCommand: String],
51+
toolchain: Toolchain
4852
) async -> [CodeLens] {
49-
guard snapshot.text.contains("@main") && !supportedCommands.isEmpty else {
50-
// This is intended to filter out files that obviously do not contain an entry point.
53+
guard !supportedCommands.isEmpty else {
5154
return []
5255
}
5356

57+
var targetDisplayName: String? = nil
58+
if let workspace,
59+
let target = await workspace.buildServerManager.canonicalTarget(for: snapshot.uri),
60+
let buildTarget = await workspace.buildServerManager.buildTarget(named: target)
61+
{
62+
targetDisplayName = buildTarget.displayName
63+
}
64+
65+
var codeLenses: [CodeLens] = []
5466
let syntaxTree = await syntaxTreeManager.syntaxTree(for: snapshot)
55-
let visitor = SwiftCodeLensScanner(snapshot: snapshot, targetName: targetName, supportedCommands: supportedCommands)
56-
visitor.walk(syntaxTree)
57-
return visitor.result
67+
if snapshot.text.contains("@main") {
68+
let visitor = SwiftCodeLensScanner(snapshot: snapshot, targetName: targetDisplayName, supportedCommands: supportedCommands)
69+
visitor.walk(syntaxTree)
70+
codeLenses += visitor.result
71+
}
72+
73+
// "swift.play" CodeLens should be ignored if "swift-play" is not in the toolchain as the client has no way of running
74+
if toolchain.swiftPlay != nil, let workspace, let playCommand = supportedCommands[SupportedCodeLensCommand.play], snapshot.text.contains("#Playground") {
75+
let playgrounds = await SwiftPlaygroundsScanner.findDocumentPlaygrounds(in: syntaxTree, workspace: workspace, snapshot: snapshot)
76+
codeLenses += playgrounds.map({ p in CodeLens(range: p.range, command: Command(title: "Play \"\(p.label ?? p.id)\"", command: playCommand, arguments: [p.encodeToLSPAny()])) })
77+
}
78+
79+
return codeLenses
5880
}
5981

6082
override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {

0 commit comments

Comments
 (0)