Skip to content

Commit 0adaf25

Browse files
more robust searching for DocC catalogs
1 parent a8eae3c commit 0adaf25

File tree

8 files changed

+139
-89
lines changed

8 files changed

+139
-89
lines changed

Sources/BuildServerProtocol/Messages/BuildTargetSourcesRequest.swift

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,26 @@ public struct SourceItemDataKind: RawRepresentable, Codable, Hashable, Sendable
119119
}
120120

121121
/// **(BSP Extension)**
122+
123+
public enum SourceKitSourceItemKind: String, Codable {
124+
/// A source file that belongs to the target
125+
case source = "source"
126+
127+
/// A header file that is clearly associated with one target.
128+
///
129+
/// For example header files in SwiftPM projects are always associated to one target and SwiftPM can provide build
130+
/// settings for that header file.
131+
///
132+
/// In general, build systems don't need to list all header files in the `buildTarget/sources` request: Semantic
133+
/// functionality for header files is usually provided by finding a main file that includes the header file and
134+
/// inferring build settings from it. Listing header files in `buildTarget/sources` allows SourceKit-LSP to provide
135+
/// semantic functionality for header files if they haven't been included by any main file.
136+
case header = "header"
137+
138+
/// A SwiftDocC documentation catalog usually ending in the ".docc" extension.
139+
case doccCatalog = "doccCatalog"
140+
}
141+
122142
public struct SourceKitSourceItemData: LSPAnyCodable, Codable {
123143
/// The language of the source file. If `nil`, the language is inferred from the file extension.
124144
public var language: Language?
@@ -132,7 +152,14 @@ public struct SourceKitSourceItemData: LSPAnyCodable, Codable {
132152
/// functionality for header files is usually provided by finding a main file that includes the header file and
133153
/// inferring build settings from it. Listing header files in `buildTarget/sources` allows SourceKit-LSP to provide
134154
/// semantic functionality for header files if they haven't been included by any main file.
135-
public var isHeader: Bool?
155+
public var isHeader: Bool? {
156+
guard let kind else {
157+
return nil
158+
}
159+
return kind == .header
160+
}
161+
162+
public var kind: SourceKitSourceItemKind?
136163

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

147-
public init(language: Language? = nil, isHeader: Bool? = nil, outputPath: String? = nil) {
174+
public init(language: Language? = nil, kind: SourceKitSourceItemKind? = nil, outputPath: String? = nil) {
148175
self.language = language
149-
self.isHeader = isHeader
176+
self.kind = kind
150177
self.outputPath = outputPath
151178
}
152179

153180
public init?(fromLSPDictionary dictionary: [String: LanguageServerProtocol.LSPAny]) {
154181
if case .string(let language) = dictionary[CodingKeys.language.stringValue] {
155182
self.language = Language(rawValue: language)
156183
}
157-
if case .bool(let isHeader) = dictionary[CodingKeys.isHeader.stringValue] {
158-
self.isHeader = isHeader
184+
if case .string(let rawKind) = dictionary[CodingKeys.kind.stringValue] {
185+
self.kind = SourceKitSourceItemKind(rawValue: rawKind)
186+
}
187+
// Backwards compatibility for isHeader
188+
if case .bool(let isHeader) = dictionary["isHeader"], isHeader {
189+
self.kind = .header
159190
}
160191
if case .string(let outputFilePath) = dictionary[CodingKeys.outputPath.stringValue] {
161192
self.outputPath = outputFilePath
@@ -167,8 +198,12 @@ public struct SourceKitSourceItemData: LSPAnyCodable, Codable {
167198
if let language {
168199
result[CodingKeys.language.stringValue] = .string(language.rawValue)
169200
}
201+
if let kind {
202+
result[CodingKeys.kind.stringValue] = .string(kind.rawValue)
203+
}
204+
// Backwards compatibility for isHeader
170205
if let isHeader {
171-
result[CodingKeys.isHeader.stringValue] = .bool(isHeader)
206+
result["isHeader"] = .bool(isHeader)
172207
}
173208
if let outputPath {
174209
result[CodingKeys.outputPath.stringValue] = .string(outputPath)

Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -593,16 +593,23 @@ package actor SwiftPMBuildSystem: BuiltInBuildSystem {
593593
kind: .file,
594594
generated: false,
595595
dataKind: .sourceKit,
596-
data: SourceKitSourceItemData(isHeader: true).encodeToLSPAny()
597-
)
598-
}
599-
sources += (swiftPMTarget.resources + swiftPMTarget.ignored + swiftPMTarget.others).map {
600-
SourceItem(
601-
uri: DocumentURI($0),
602-
kind: $0.isDirectory ? .directory : .file,
603-
generated: false
596+
data: SourceKitSourceItemData(kind: .header).encodeToLSPAny()
604597
)
605598
}
599+
sources += (swiftPMTarget.resources + swiftPMTarget.ignored + swiftPMTarget.others)
600+
.map { (url: URL) -> SourceItem in
601+
var data: SourceKitSourceItemData? = nil
602+
if url.isDirectory, url.pathExtension == "docc" {
603+
data = SourceKitSourceItemData(kind: .doccCatalog)
604+
}
605+
return SourceItem(
606+
uri: DocumentURI(url),
607+
kind: url.isDirectory ? .directory : .file,
608+
generated: false,
609+
dataKind: data != nil ? .sourceKit : nil,
610+
data: data?.encodeToLSPAny()
611+
)
612+
}
606613
result.append(SourcesItem(target: target, sources: sources))
607614
}
608615
return BuildTargetSourcesResponse(items: result)

Sources/DocCDocumentation/BuildSystemIntegrationExtensions.swift

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -38,21 +38,15 @@ package extension BuildSystemManager {
3838
return nil
3939
}
4040
let sourceFiles = (try? await sourceFiles(in: [target]).flatMap(\.sources)) ?? []
41-
return sourceFiles.compactMap(\.uri.fileURL?.doccCatalogURL).first
42-
}
43-
}
44-
45-
package extension URL {
46-
var doccCatalogURL: URL? {
47-
var pathComponents = self.pathComponents
48-
var result = self
49-
while let lastPathComponent = pathComponents.last {
50-
if lastPathComponent.hasSuffix(".docc") {
51-
return result
41+
let catalogURLs = sourceFiles.compactMap { sourceItem -> URL? in
42+
guard sourceItem.dataKind == .sourceKit,
43+
let data = SourceKitSourceItemData(fromLSPAny: sourceItem.data),
44+
data.kind == .doccCatalog
45+
else {
46+
return nil
5247
}
53-
pathComponents.removeLast()
54-
result.deleteLastPathComponent()
55-
}
56-
return nil
48+
return sourceItem.uri.fileURL
49+
}.sorted(by: { $0.absoluteString >= $1.absoluteString })
50+
return catalogURLs.first
5751
}
5852
}

Sources/DocCDocumentation/DocCDocumentationManager.swift

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import BuildServerProtocol
2-
import BuildSystemIntegration
2+
package import BuildSystemIntegration
33
package import Foundation
44
package import IndexStoreDB
55
package import LanguageServerProtocol
@@ -11,7 +11,9 @@ package struct DocCDocumentationManager: Sendable {
1111
private let referenceResolutionService: DocCReferenceResolutionService
1212
private let catalogIndexManager: DocCCatalogIndexManager
1313

14-
package init() {
14+
private let buildSystemManager: BuildSystemManager
15+
16+
package init(buildSystemManager: BuildSystemManager) {
1517
let symbolResolutionServer = DocumentationServer(qualityOfService: .unspecified)
1618
doccServer = DocCServer(
1719
peer: symbolResolutionServer,
@@ -20,16 +22,16 @@ package struct DocCDocumentationManager: Sendable {
2022
catalogIndexManager = DocCCatalogIndexManager(server: doccServer)
2123
referenceResolutionService = DocCReferenceResolutionService()
2224
symbolResolutionServer.register(service: referenceResolutionService)
25+
self.buildSystemManager = buildSystemManager
2326
}
2427

2528
package func filesDidChange(_ events: [FileEvent]) async {
26-
let affectedCatalogURLs = events.reduce(into: Set<URL>()) { affectedCatalogURLs, event in
27-
guard let catalogURL = event.uri.fileURL?.doccCatalogURL else {
28-
return
29+
for event in events {
30+
guard let catalogURL = await buildSystemManager.doccCatalog(for: event.uri) else {
31+
continue
2932
}
30-
affectedCatalogURLs.insert(catalogURL)
33+
await catalogIndexManager.invalidate(catalogURL)
3134
}
32-
await catalogIndexManager.invalidate(catalogURLs: affectedCatalogURLs)
3335
}
3436

3537
package func catalogIndex(for catalogURL: URL) async throws(DocCIndexError) -> DocCCatalogIndex {

Sources/SourceKitLSP/Documentation/DoccDocumentationHandler.swift

Lines changed: 47 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ extension DocumentationLanguageService {
2525
guard let sourceKitLSPServer else {
2626
throw ResponseError.internalError("SourceKit-LSP is shutting down")
2727
}
28-
let documentationManager = sourceKitLSPServer.documentationManager
28+
guard let workspace = await sourceKitLSPServer.workspaceForDocument(uri: req.textDocument.uri) else {
29+
throw ResponseError.workspaceNotOpen(req.textDocument.uri)
30+
}
31+
let documentationManager = workspace.doccDocumentationManager
2932
let snapshot = try documentManager.latestSnapshot(req.textDocument.uri)
3033
guard let workspace = await sourceKitLSPServer.workspaceForDocument(uri: req.textDocument.uri) else {
3134
throw ResponseError.workspaceNotOpen(req.textDocument.uri)
@@ -42,7 +45,7 @@ extension DocumentationLanguageService {
4245
)
4346
case .markdown:
4447
if case let .symbol(symbolName) = MarkdownTitleFinder.find(parsing: snapshot.text) {
45-
if let moduleName = moduleName, symbolName == moduleName {
48+
if let moduleName, symbolName == moduleName {
4649
// This is a page representing the module itself.
4750
// Create a dummy symbol graph and tell SwiftDocC to convert the module name.
4851
let emptySymbolGraph = String(
@@ -66,53 +69,52 @@ extension DocumentationLanguageService {
6669
moduleName: moduleName,
6770
catalogURL: catalogURL
6871
)
69-
} else {
70-
// This is a symbol extension page. Find the symbol so that we can include it in the request.
71-
guard let index = workspace.index(checkedFor: .deletedFiles) else {
72-
throw ResponseError.requestFailed(doccDocumentationError: .indexNotAvailable)
73-
}
74-
guard let symbolLink = await documentationManager.symbolLink(string: symbolName),
75-
let symbolOccurrence = await documentationManager.primaryDefinitionOrDeclarationOccurrence(
76-
ofDocCSymbolLink: symbolLink,
77-
in: index
78-
)
79-
else {
80-
throw ResponseError.requestFailed(doccDocumentationError: .symbolNotFound(symbolName))
81-
}
82-
guard
83-
let symbolWorkspace = try await workspaceForDocument(uri: symbolOccurrence.location.documentUri),
84-
let languageService = try await languageService(
85-
for: symbolOccurrence.location.documentUri,
86-
.swift,
87-
in: symbolWorkspace
88-
) as? SwiftLanguageService,
89-
let symbolSnapshot = try documentManager.latestSnapshotOrDisk(
90-
symbolOccurrence.location.documentUri,
91-
language: .swift
92-
)
93-
else {
94-
throw ResponseError.internalError(
95-
"Unable to find Swift language service for \(symbolOccurrence.location.documentUri)"
96-
)
97-
}
98-
let position = symbolSnapshot.position(of: symbolOccurrence.location)
99-
let cursorInfo = try await languageService.cursorInfo(
72+
}
73+
// This is a symbol extension page. Find the symbol so that we can include it in the request.
74+
guard let index = workspace.index(checkedFor: .deletedFiles) else {
75+
throw ResponseError.requestFailed(doccDocumentationError: .indexNotAvailable)
76+
}
77+
guard let symbolLink = await documentationManager.symbolLink(string: symbolName),
78+
let symbolOccurrence = await documentationManager.primaryDefinitionOrDeclarationOccurrence(
79+
ofDocCSymbolLink: symbolLink,
80+
in: index
81+
)
82+
else {
83+
throw ResponseError.requestFailed(doccDocumentationError: .symbolNotFound(symbolName))
84+
}
85+
guard
86+
let symbolWorkspace = try await workspaceForDocument(uri: symbolOccurrence.location.documentUri),
87+
let languageService = try await languageService(
88+
for: symbolOccurrence.location.documentUri,
89+
.swift,
90+
in: symbolWorkspace
91+
) as? SwiftLanguageService,
92+
let symbolSnapshot = try documentManager.latestSnapshotOrDisk(
10093
symbolOccurrence.location.documentUri,
101-
position..<position,
102-
includeSymbolGraph: true,
103-
fallbackSettingsAfterTimeout: false
94+
language: .swift
10495
)
105-
guard let symbolGraph = cursorInfo.symbolGraph else {
106-
throw ResponseError.internalError("Unable to retrieve symbol graph for \(symbolOccurrence.symbol.name)")
107-
}
108-
return try await documentationManager.renderDocCDocumentation(
109-
symbolUSR: symbolOccurrence.symbol.usr,
110-
symbolGraph: symbolGraph,
111-
markupFile: snapshot.text,
112-
moduleName: moduleName,
113-
catalogURL: catalogURL
96+
else {
97+
throw ResponseError.internalError(
98+
"Unable to find Swift language service for \(symbolOccurrence.location.documentUri)"
11499
)
115100
}
101+
let position = symbolSnapshot.position(of: symbolOccurrence.location)
102+
let cursorInfo = try await languageService.cursorInfo(
103+
symbolOccurrence.location.documentUri,
104+
position..<position,
105+
includeSymbolGraph: true,
106+
fallbackSettingsAfterTimeout: false
107+
)
108+
guard let symbolGraph = cursorInfo.symbolGraph else {
109+
throw ResponseError.internalError("Unable to retrieve symbol graph for \(symbolOccurrence.symbol.name)")
110+
}
111+
return try await documentationManager.renderDocCDocumentation(
112+
symbolUSR: symbolOccurrence.symbol.usr,
113+
symbolGraph: symbolGraph,
114+
markupFile: snapshot.text,
115+
moduleName: moduleName,
116+
catalogURL: catalogURL
117+
)
116118
}
117119
// This is an article that can be rendered on its own
118120
return try await documentationManager.renderDocCDocumentation(

Sources/SourceKitLSP/SourceKitLSPServer.swift

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,6 @@ package actor SourceKitLSPServer {
7979

8080
package let documentManager = DocumentManager()
8181

82-
#if canImport(DocCDocumentation)
83-
package let documentationManager = DocCDocumentationManager()
84-
#endif
85-
8682
/// The `TaskScheduler` that schedules all background indexing tasks.
8783
///
8884
/// Shared process-wide to ensure the scheduled index operations across multiple workspaces don't exceed the maximum
@@ -1476,9 +1472,6 @@ extension SourceKitLSPServer {
14761472
// (e.g. Package.swift doesn't have build settings but affects build
14771473
// settings). Inform the build system about all file changes.
14781474
await workspaces.concurrentForEach { await $0.filesDidChange(notification.changes) }
1479-
#if canImport(DocCDocumentation)
1480-
await documentationManager.filesDidChange(notification.changes)
1481-
#endif
14821475
}
14831476

14841477
func setBackgroundIndexingPaused(_ request: SetOptionsRequest) async throws -> VoidResponse {

Sources/SourceKitLSP/Swift/DoccDocumentation.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@ extension SwiftLanguageService {
2323
guard let sourceKitLSPServer else {
2424
throw ResponseError.internalError("SourceKit-LSP is shutting down")
2525
}
26-
let documentationManager = sourceKitLSPServer.documentationManager
26+
guard let workspace = await sourceKitLSPServer.workspaceForDocument(uri: req.textDocument.uri) else {
27+
throw ResponseError.workspaceNotOpen(req.textDocument.uri)
28+
}
29+
let documentationManager = workspace.doccDocumentationManager
2730
guard let position = req.position else {
2831
throw ResponseError.invalidParams("A position must be provided for Swift files")
2932
}

Sources/SourceKitLSP/Workspace.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ import ToolchainRegistry
2626
import struct TSCBasic.AbsolutePath
2727
import struct TSCBasic.RelativePath
2828

29+
#if canImport(DocCDocumentation)
30+
package import DocCDocumentation
31+
#endif
32+
2933
/// Actor that caches realpaths for `sourceFilesWithSameRealpath`.
3034
fileprivate actor SourceFilesWithSameRealpathInferrer {
3135
private let buildSystemManager: BuildSystemManager
@@ -97,6 +101,10 @@ package final class Workspace: Sendable, BuildSystemManagerDelegate {
97101
/// The build system manager to use for documents in this workspace.
98102
package let buildSystemManager: BuildSystemManager
99103

104+
#if canImport(DocCDocumentation)
105+
package let doccDocumentationManager: DocCDocumentationManager
106+
#endif
107+
100108
private let sourceFilesWithSameRealpathInferrer: SourceFilesWithSameRealpathInferrer
101109

102110
let options: SourceKitLSPOptions
@@ -145,6 +153,9 @@ package final class Workspace: Sendable, BuildSystemManagerDelegate {
145153
self.options = options
146154
self._uncheckedIndex = ThreadSafeBox(initialValue: uncheckedIndex)
147155
self.buildSystemManager = buildSystemManager
156+
#if canImport(DocCDocumentation)
157+
self.doccDocumentationManager = DocCDocumentationManager(buildSystemManager: buildSystemManager)
158+
#endif
148159
self.sourceFilesWithSameRealpathInferrer = SourceFilesWithSameRealpathInferrer(
149160
buildSystemManager: buildSystemManager
150161
)
@@ -392,6 +403,9 @@ package final class Workspace: Sendable, BuildSystemManagerDelegate {
392403

393404
// Notify all clients about the reported and inferred edits.
394405
await buildSystemManager.filesDidChange(events)
406+
#if canImport(DocCDocumentation)
407+
await doccDocumentationManager.filesDidChange(events)
408+
#endif
395409

396410
async let updateSyntacticIndex: Void = await syntacticTestIndex.filesDidChange(events)
397411
async let updateSemanticIndex: Void? = await semanticIndexManager?.filesDidChange(events)

0 commit comments

Comments
 (0)