Files
sourcekit-lsp/Sources/SwiftLanguageService/SwiftPlaygroundsScanner.swift
2025-11-10 11:14:25 -05:00

102 lines
3.3 KiB
Swift

//===----------------------------------------------------------------------===//
//
// 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
}
}