Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion Sources/LanguageServerProtocol/SupportTypes/Location.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
/// Range within a particular document.
///
/// For a location where the document is implied, use `Position` or `Range<Position>`.
public struct Location: ResponseType, Hashable, Codable, CustomDebugStringConvertible, Comparable, Sendable {
public struct Location: ResponseType, Hashable, Codable, CustomDebugStringConvertible, Comparable, Sendable,
LSPAnyCodable
{
public static func < (lhs: Location, rhs: Location) -> Bool {
if lhs.uri != rhs.uri {
return lhs.uri.stringValue < rhs.uri.stringValue
Expand All @@ -34,7 +36,27 @@ public struct Location: ResponseType, Hashable, Codable, CustomDebugStringConver
self._range = CustomCodable<PositionRange>(wrappedValue: range)
}

public init?(fromLSPDictionary dictionary: [String: LSPAny]) {
guard
case .string(let uriString) = dictionary["uri"],
case .dictionary(let rangeDict) = dictionary["range"],
let uri = try? DocumentURI(string: uriString),
let range = Range<Position>(fromLSPDictionary: rangeDict)
else {
return nil
}
self.uri = uri
self._range = CustomCodable<PositionRange>(wrappedValue: range)
}

public var debugDescription: String {
return "\(uri):\(range.lowerBound)-\(range.upperBound)"
}

public func encodeToLSPAny() -> LSPAny {
return .dictionary([
"uri": .string(uri.stringValue),
"range": range.encodeToLSPAny(),
])
}
}
69 changes: 69 additions & 0 deletions Sources/LanguageServerProtocol/SupportTypes/Playground.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

/// 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.
///
/// **(LSP Extension)**
public struct Playground: ResponseType, Equatable, LSPAnyCodable {
/// 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`.
public var 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"`.
public var label: String?

/// The location of where the #Playground macro was used in the source code.
public var location: Location

public init(
id: String,
label: String?,
location: Location,
) {
self.id = id
self.label = label
self.location = location
}

public init?(fromLSPDictionary dictionary: [String: LSPAny]) {
guard
case .string(let id) = dictionary["id"],
case .dictionary(let locationDict) = dictionary["location"],
let location = Location(fromLSPDictionary: locationDict)
else {
return nil
}
self.id = id
self.location = location
if case .string(let label) = dictionary["label"] {
self.label = label
}
}

public func encodeToLSPAny() -> LSPAny {
var dict: [String: LSPAny] = [
"id": .string(id),
"location": location.encodeToLSPAny(),
]

if let label {
dict["label"] = .string(label)
}

return .dictionary(dict)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,7 @@ public struct SupportedCodeLensCommand: Codable, Hashable, RawRepresentable, Sen

/// Lens to debug the application
public static let debug: Self = Self(rawValue: "swift.debug")

/// Lens to run the playground
public static let play: Self = Self(rawValue: "swift.play")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

/// A `TextDocumentPlayground` item can be used to identify playground and identify it
/// to allow executing the playground through a "swift play" command. Differs from `Playground`
/// by only including the `range` instead of full `location` with the expectation being that
/// it is only returned as part of a textDocument/* request such as textDocument/codelens
public struct TextDocumentPlayground: ResponseType, Equatable, LSPAnyCodable {
/// 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`.
public var id: String
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this needs to capture either the corresponding product or target as well so that the playground can be executed in the appropriate context

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@owenv the LSP code that parses these does include the target https://github.com/swiftlang/sourcekit-lsp/pull/2340/files#diff-d76b1dc09dd52af2a88684043d44d1e5261f013c09f6137dffd4e23aebce6e56R91 but do you want the comment code to reflect the expected format?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a better explanation about id but let me know if there is more you wanted

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, so it seems like the playgrounds implementation is building a dylib composed of every target in the package and linking the runner against that. This will work in small examples, but in general it's not safe to assume all the targets in a package can be safely linked into a single image. e.g. they may have different platform requirements, conflicting static initializers, multiple copies of a binary dependency built from the same sources, etc. I think sourcekit-lsp will need to pick a single specific appropriate library product containing the playground's code, thread that through to the play command, and adjust the build of the runner accordingly

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whatever swift play --list returns is what I think we should aim to match, and then VSCode executing swift play myPlaygroundID should make sure the right one is built and run

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any further comments @owenv? Can we merge and tag a new release?


/// 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"`.
public var label: String?

/// The full range of the #Playground macro body in the given file.
public var range: Range<Position>

public init(
id: String,
label: String?,
range: Range<Position>
) {
self.id = id
self.label = label
self.range = range
}

public init?(fromLSPDictionary dictionary: [String: LSPAny]) {
guard
case .string(let id) = dictionary["id"],
case .dictionary(let rangeDict) = dictionary["range"],
let range = Range<Position>(fromLSPDictionary: rangeDict)
else {
return nil
}
self.id = id
self.range = range
if case .string(let label) = dictionary["label"] {
self.label = label
}
}

public func encodeToLSPAny() -> LSPAny {
var dict: [String: LSPAny] = [
"id": .string(id),
"range": range.encodeToLSPAny(),
]
if let label {
dict["label"] = .string(label)
}
return .dictionary(dict)
}
}
Loading