mirror of
https://github.com/apple/sourcekit-lsp.git
synced 2026-03-02 18:23:24 +01:00
Merge pull request #2340 from award999/document-playgrounds
Parse #Playground macro expansions and include them in CodeLens request
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -25,6 +25,7 @@ add_library(SwiftLanguageService STATIC
|
||||
InlayHints.swift
|
||||
MacroExpansion.swift
|
||||
OpenInterface.swift
|
||||
SwiftPlaygroundsScanner.swift
|
||||
RefactoringEdit.swift
|
||||
RefactoringResponse.swift
|
||||
RelatedIdentifiers.swift
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
101
Sources/SwiftLanguageService/SwiftPlaygroundsScanner.swift
Normal file
101
Sources/SwiftLanguageService/SwiftPlaygroundsScanner.swift
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
]
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user