Skip to content
Open
Show file tree
Hide file tree
Changes from all 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(),
])
}
}
72 changes: 72 additions & 0 deletions Sources/LanguageServerProtocol/SupportTypes/Playground.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
//===----------------------------------------------------------------------===//
//
// 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 {
Copy link
Member

Choose a reason for hiding this comment

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

This isn’t used anywhere yet, right? Do you want to introduce the workspace/playgrounds request in this PR already or in a follow-up PR? I’m fine with either.

/// Unique identifier for the `Playground` with the format `<target>/<filename>:<line>:[column]` where `target`
/// corresponds to the Swift package's target where the playground is defined, `filename` is the basename of the file
/// (not entire relative path), and `column` is optional only required if multiple playgrounds are defined on the same
/// line. 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)
Comment on lines +48 to +49
Copy link
Member

Choose a reason for hiding this comment

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

This can be shortened slightly.

Suggested change
case .dictionary(let locationDict) = dictionary["location"],
let location = Location(fromLSPDictionary: locationDict)
let location = Location(fromLSPAny: dictionary["location"])

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,70 @@
//===----------------------------------------------------------------------===//
//
// 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` with the format `<target>/<filename>:<line>:[column]` where `target`
/// corresponds to the Swift package's target where the playground is defined, `filename` is the basename of the file
/// (not entire relative path), and `column` is optional only required if multiple playgrounds are defined on the same
/// line. 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
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
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


/// 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)
Comment on lines +48 to +49
Copy link
Member

Choose a reason for hiding this comment

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

Same here, we can use the init(fromLSPAny:) convenience initializer.

Suggested change
case .dictionary(let rangeDict) = dictionary["range"],
let range = Range<Position>(fromLSPDictionary: rangeDict)
let range = Range<Position>(fromLSPAny: dictionary["range"])

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)
}
}