Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle Markdown and Tutorial files in textDocument/doccDocumentation #1959

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
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
27 changes: 26 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,25 @@ var targets: [Target] = [
swiftSettings: globalSwiftSettings
),

// MARK: DocCDocumentation

.target(
name: "DocCDocumentation",
dependencies: [
"BuildServerProtocol",
"BuildSystemIntegration",
"LanguageServerProtocol",
"SemanticIndex",
"SKLogging",
"SwiftExtensions",
.product(name: "IndexStoreDB", package: "indexstore-db"),
.product(name: "SwiftDocC", package: "swift-docc"),
.product(name: "SymbolKit", package: "swift-docc-symbolkit"),
],
exclude: ["CMakeLists.txt"],
swiftSettings: globalSwiftSettings
),

// MARK: InProcessClient

.target(
Expand Down Expand Up @@ -474,6 +493,7 @@ var targets: [Target] = [
dependencies: [
"BuildServerProtocol",
"BuildSystemIntegration",
"DocCDocumentation",
"LanguageServerProtocol",
"LanguageServerProtocolExtensions",
"LanguageServerProtocolJSONRPC",
Expand All @@ -485,10 +505,11 @@ var targets: [Target] = [
"SwiftExtensions",
"ToolchainRegistry",
"TSCExtensions",
.product(name: "SwiftDocC", package: "swift-docc"),
.product(name: "IndexStoreDB", package: "indexstore-db"),
.product(name: "Crypto", package: "swift-crypto"),
.product(name: "Markdown", package: "swift-markdown"),
.product(name: "SwiftToolsSupport-auto", package: "swift-tools-support-core"),
.product(name: "SymbolKit", package: "swift-docc-symbolkit"),
]
+ swiftPMDependency([
.product(name: "SwiftPM-auto", package: "swift-package-manager")
Expand Down Expand Up @@ -770,6 +791,8 @@ var dependencies: [Package.Dependency] {
return [
.package(path: "../indexstore-db"),
.package(path: "../swift-docc"),
.package(path: "../swift-docc-symbolkit"),
.package(path: "../swift-markdown"),
.package(path: "../swift-tools-support-core"),
.package(path: "../swift-argument-parser"),
.package(path: "../swift-syntax"),
Expand All @@ -781,6 +804,8 @@ var dependencies: [Package.Dependency] {
return [
.package(url: "https://github.com/swiftlang/indexstore-db.git", branch: relatedDependenciesBranch),
.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/apple/swift-tools-support-core.git", branch: relatedDependenciesBranch),
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.4.0"),
.package(url: "https://github.com/swiftlang/swift-syntax.git", branch: relatedDependenciesBranch),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,26 @@ public struct SourceItemDataKind: RawRepresentable, Codable, Hashable, Sendable
}

/// **(BSP Extension)**

public enum SourceKitSourceItemKind: String, Codable {
/// A source file that belongs to the target
case source = "source"

/// A header file that is clearly associated with one target.
///
/// For example header files in SwiftPM projects are always associated to one target and SwiftPM can provide build
/// settings for that header file.
///
/// In general, build systems don't need to list all header files in the `buildTarget/sources` request: Semantic
/// functionality for header files is usually provided by finding a main file that includes the header file and
/// inferring build settings from it. Listing header files in `buildTarget/sources` allows SourceKit-LSP to provide
/// semantic functionality for header files if they haven't been included by any main file.
case header = "header"

/// A SwiftDocC documentation catalog usually ending in the ".docc" extension.
case doccCatalog = "doccCatalog"
}

public struct SourceKitSourceItemData: LSPAnyCodable, Codable {
/// The language of the source file. If `nil`, the language is inferred from the file extension.
public var language: Language?
Expand All @@ -132,7 +152,14 @@ public struct SourceKitSourceItemData: LSPAnyCodable, Codable {
/// functionality for header files is usually provided by finding a main file that includes the header file and
/// inferring build settings from it. Listing header files in `buildTarget/sources` allows SourceKit-LSP to provide
/// semantic functionality for header files if they haven't been included by any main file.
public var isHeader: Bool?
public var isHeader: Bool? {
guard let kind else {
return nil
}
return kind == .header
}

public var kind: SourceKitSourceItemKind?

/// The output path that is used during indexing for this file, ie. the `-index-unit-output-path`, if it is specified
/// in the compiler arguments or the file that is passed as `-o`, if `-index-unit-output-path` is not specified.
Expand All @@ -144,18 +171,22 @@ public struct SourceKitSourceItemData: LSPAnyCodable, Codable {
/// `outputPathsProvider: true` in `SourceKitInitializeBuildResponseData`.
public var outputPath: String?

public init(language: Language? = nil, isHeader: Bool? = nil, outputPath: String? = nil) {
public init(language: Language? = nil, kind: SourceKitSourceItemKind? = nil, outputPath: String? = nil) {
self.language = language
self.isHeader = isHeader
self.kind = kind
self.outputPath = outputPath
}

public init?(fromLSPDictionary dictionary: [String: LanguageServerProtocol.LSPAny]) {
if case .string(let language) = dictionary[CodingKeys.language.stringValue] {
self.language = Language(rawValue: language)
}
if case .bool(let isHeader) = dictionary[CodingKeys.isHeader.stringValue] {
self.isHeader = isHeader
if case .string(let rawKind) = dictionary[CodingKeys.kind.stringValue] {
self.kind = SourceKitSourceItemKind(rawValue: rawKind)
}
// Backwards compatibility for isHeader
if case .bool(let isHeader) = dictionary["isHeader"], isHeader {
self.kind = .header
}
if case .string(let outputFilePath) = dictionary[CodingKeys.outputPath.stringValue] {
self.outputPath = outputFilePath
Expand All @@ -167,8 +198,12 @@ public struct SourceKitSourceItemData: LSPAnyCodable, Codable {
if let language {
result[CodingKeys.language.stringValue] = .string(language.rawValue)
}
if let kind {
result[CodingKeys.kind.stringValue] = .string(kind.rawValue)
}
// Backwards compatibility for isHeader
if let isHeader {
result[CodingKeys.isHeader.stringValue] = .bool(isHeader)
result["isHeader"] = .bool(isHeader)
}
if let outputPath {
result[CodingKeys.outputPath.stringValue] = .string(outputPath)
Expand Down
12 changes: 10 additions & 2 deletions Sources/BuildSystemIntegration/FileBuildSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,15 @@ package struct FileBuildSettings: Equatable, Sendable {
///
/// This patches the arguments by searching for the argument corresponding to
/// `originalFile` and replacing it.
func patching(newFile: DocumentURI, originalFile: DocumentURI) -> FileBuildSettings {
package func patching(newFile: DocumentURI, originalFile: DocumentURI) -> FileBuildSettings {
return patching(newFile: newFile.pseudoPath, originalFile: originalFile)
}

/// Return arguments suitable for use by `newFile`.
///
/// This patches the arguments by searching for the argument corresponding to
/// `originalFile` and replacing it.
package func patching(newFile: String, originalFile: DocumentURI) -> FileBuildSettings {
var arguments = self.compilerArguments
// URL.lastPathComponent is only set for file URLs but we want to also infer a file extension for non-file URLs like
// untitled:file.cpp
Expand All @@ -66,7 +74,7 @@ package struct FileBuildSettings: Equatable, Sendable {
// the file system.
$0.hasSuffix(basename) && originalFile.pseudoPath.hasSuffix($0)
}) {
arguments[index] = newFile.pseudoPath
arguments[index] = newFile
// The `-x<lang>` flag needs to be before the possible `-c <header file>`
// argument in order for Clang to respect it. If there is a pre-existing `-x`
// flag though, Clang will honor that one instead since it comes after.
Expand Down
23 changes: 15 additions & 8 deletions Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -593,16 +593,23 @@ package actor SwiftPMBuildSystem: BuiltInBuildSystem {
kind: .file,
generated: false,
dataKind: .sourceKit,
data: SourceKitSourceItemData(isHeader: true).encodeToLSPAny()
)
}
sources += (swiftPMTarget.resources + swiftPMTarget.ignored + swiftPMTarget.others).map {
SourceItem(
uri: DocumentURI($0),
kind: $0.isDirectory ? .directory : .file,
generated: false
data: SourceKitSourceItemData(kind: .header).encodeToLSPAny()
)
}
sources += (swiftPMTarget.resources + swiftPMTarget.ignored + swiftPMTarget.others)
.map { (url: URL) -> SourceItem in
var data: SourceKitSourceItemData? = nil
if url.isDirectory, url.pathExtension == "docc" {
data = SourceKitSourceItemData(kind: .doccCatalog)
}
return SourceItem(
uri: DocumentURI(url),
kind: url.isDirectory ? .directory : .file,
generated: false,
dataKind: data != nil ? .sourceKit : nil,
data: data?.encodeToLSPAny()
)
}
result.append(SourcesItem(target: target, sources: sources))
}
return BuildTargetSourcesResponse(items: result)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 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
//
//===----------------------------------------------------------------------===//

package import BuildServerProtocol
package import BuildSystemIntegration
package import Foundation
import LanguageServerProtocol

package extension BuildSystemManager {
/// Retrieves the name of the Swift module for a given target.
///
/// - Parameter target: The build target identifier
/// - Returns: The name of the Swift module or nil if it could not be determined
func moduleName(for target: BuildTargetIdentifier) async -> String? {
let sourceFiles = (try? await sourceFiles(in: [target]).flatMap(\.sources)) ?? []
for sourceFile in sourceFiles {
let language = await defaultLanguage(for: sourceFile.uri, in: target)
guard language == .swift else {
continue
}
if let moduleName = await moduleName(for: sourceFile.uri, in: target) {
return moduleName
}
}
return nil
}

/// Finds the SwiftDocC documentation catalog associated with a target, if any.
///
/// - Parameter target: The build target identifier
/// - Returns: The URL of the documentation catalog or nil if one could not be found
func doccCatalog(for target: BuildTargetIdentifier) async -> URL? {
let sourceFiles = (try? await sourceFiles(in: [target]).flatMap(\.sources)) ?? []
let catalogURLs = sourceFiles.compactMap { sourceItem -> URL? in
guard sourceItem.dataKind == .sourceKit,
let data = SourceKitSourceItemData(fromLSPAny: sourceItem.data),
data.kind == .doccCatalog
else {
return nil
}
return sourceItem.uri.fileURL
}.sorted(by: { $0.absoluteString >= $1.absoluteString })
return catalogURLs.first
}
}
Loading