Skip to content

Commit f8537b3

Browse files
cache snapshots opened in sourcekitd by textDocument/doccDocumentation requests
1 parent 1ab9e2e commit f8537b3

File tree

5 files changed

+149
-132
lines changed

5 files changed

+149
-132
lines changed

Sources/DocCDocumentation/IndexStoreDB+Extensions.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ extension CheckedIndex {
4040
var result: [SymbolOccurrence] = []
4141
for occurrence in topLevelSymbolOccurrences {
4242
let info = try await doccSymbolInformation(ofUSR: occurrence.symbol.usr, fetchSymbolGraph: fetchSymbolGraph)
43-
if let info, info.matches(symbolLink) {
43+
if info.matches(symbolLink) {
4444
result.append(occurrence)
4545
}
4646
}
@@ -60,9 +60,9 @@ extension CheckedIndex {
6060
package func doccSymbolInformation(
6161
ofUSR usr: String,
6262
fetchSymbolGraph: (SymbolLocation) async throws -> String?
63-
) async throws -> DocCSymbolInformation? {
63+
) async throws -> DocCSymbolInformation {
6464
guard let topLevelSymbolOccurrence = primaryDefinitionOrDeclarationOccurrence(ofUSR: usr) else {
65-
return nil
65+
throw DocCCheckedIndexError.emptyDocCSymbolLink
6666
}
6767
let moduleName = topLevelSymbolOccurrence.location.moduleName
6868
var symbols = [topLevelSymbolOccurrence]

Sources/SourceKitLSP/Documentation/DoccDocumentationHandler.swift

Lines changed: 3 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -58,53 +58,16 @@ extension DocumentationLanguageService {
5858
guard let index = workspace.index(checkedFor: .deletedFiles) else {
5959
throw ResponseError.requestFailed(doccDocumentationError: .indexNotAvailable)
6060
}
61+
let symbolGraphCache = SymbolGraphCache(sourceKitLSPServer: sourceKitLSPServer)
6162
guard let symbolLink = DocCSymbolLink(linkString: symbolName),
6263
let symbolOccurrence = try await index.primaryDefinitionOrDeclarationOccurrence(
6364
ofDocCSymbolLink: symbolLink,
64-
fetchSymbolGraph: { location in
65-
guard let symbolWorkspace = try await workspaceForDocument(uri: location.documentUri),
66-
let languageService = try await languageService(for: location.documentUri, .swift, in: symbolWorkspace)
67-
as? SwiftLanguageService
68-
else {
69-
throw ResponseError.internalError("Unable to find Swift language service for \(location.documentUri)")
70-
}
71-
return try await languageService.withSnapshotFromDiskOpenedInSourcekitd(
72-
uri: location.documentUri,
73-
fallbackSettingsAfterTimeout: false
74-
) { (snapshot, compileCommand) in
75-
let (_, _, symbolGraph) = try await languageService.cursorInfo(
76-
snapshot,
77-
compileCommand: compileCommand,
78-
Range(snapshot.position(of: location)),
79-
includeSymbolGraph: true
80-
)
81-
return symbolGraph
82-
}
83-
}
65+
fetchSymbolGraph: symbolGraphCache.fetchSymbolGraph(at:)
8466
)
8567
else {
8668
throw ResponseError.requestFailed(doccDocumentationError: .symbolNotFound(symbolName))
8769
}
88-
let symbolDocumentUri = symbolOccurrence.location.documentUri
89-
guard
90-
let symbolWorkspace = try await workspaceForDocument(uri: symbolDocumentUri),
91-
let languageService = try await languageService(for: symbolDocumentUri, .swift, in: symbolWorkspace)
92-
as? SwiftLanguageService
93-
else {
94-
throw ResponseError.internalError("Unable to find Swift language service for \(symbolDocumentUri)")
95-
}
96-
let symbolGraph = try await languageService.withSnapshotFromDiskOpenedInSourcekitd(
97-
uri: symbolDocumentUri,
98-
fallbackSettingsAfterTimeout: false
99-
) { snapshot, compileCommand in
100-
try await languageService.cursorInfo(
101-
snapshot,
102-
compileCommand: compileCommand,
103-
Range(snapshot.position(of: symbolOccurrence.location)),
104-
includeSymbolGraph: true
105-
).symbolGraph
106-
}
107-
guard let symbolGraph else {
70+
guard let symbolGraph = try await symbolGraphCache.fetchSymbolGraph(at: symbolOccurrence.location) else {
10871
throw ResponseError.internalError("Unable to retrieve symbol graph for \(symbolOccurrence.symbol.name)")
10972
}
11073
return try await documentationManager.renderDocCDocumentation(

Sources/SourceKitLSP/Swift/DoccDocumentation.swift

Lines changed: 9 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -70,25 +70,13 @@ extension SwiftLanguageService {
7070
}
7171
// Locate the documentation extension and include it in the request if one exists
7272
let markupExtensionFile = await orLog("Finding markup extension file for symbol \(symbolUSR)") {
73-
try await findMarkupExtensionFile(
73+
let symbolGraphCache = SymbolGraphCache(sourceKitLSPServer: sourceKitLSPServer)
74+
return try await findMarkupExtensionFile(
7475
workspace: workspace,
7576
documentationManager: documentationManager,
7677
catalogURL: catalogURL,
7778
for: symbolUSR,
78-
fetchSymbolGraph: { symbolLocation in
79-
try await withSnapshotFromDiskOpenedInSourcekitd(
80-
uri: symbolLocation.documentUri,
81-
fallbackSettingsAfterTimeout: false
82-
) { (snapshot, compileCommand) in
83-
let (_, _, symbolGraph) = try await self.cursorInfo(
84-
snapshot,
85-
compileCommand: compileCommand,
86-
Range(snapshot.position(of: symbolLocation)),
87-
includeSymbolGraph: true
88-
)
89-
return symbolGraph
90-
}
91-
}
79+
fetchSymbolGraph: symbolGraphCache.fetchSymbolGraph(at:)
9280
)
9381
}
9482
return try await documentationManager.renderDocCDocumentation(
@@ -113,11 +101,12 @@ extension SwiftLanguageService {
113101
}
114102
let catalogIndex = try await documentationManager.catalogIndex(for: catalogURL)
115103
guard let index = workspace.index(checkedFor: .deletedFiles),
116-
let symbolInformation = try await index.doccSymbolInformation(
117-
ofUSR: symbolUSR,
118-
fetchSymbolGraph: fetchSymbolGraph
119-
),
120-
let markupExtensionFileURL = catalogIndex.documentationExtension(for: symbolInformation)
104+
let markupExtensionFileURL = try await catalogIndex.documentationExtension(
105+
for: index.doccSymbolInformation(
106+
ofUSR: symbolUSR,
107+
fetchSymbolGraph: fetchSymbolGraph
108+
)
109+
)
121110
else {
122111
return nil
123112
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import BuildServerIntegration
14+
import Foundation
15+
import IndexStoreDB
16+
import LanguageServerProtocol
17+
import SKLogging
18+
import SKUtilities
19+
import SwiftExtensions
20+
21+
/// A cache of symbol graphs and their associated snapshots opened in sourcekitd. Any opened documents will be
22+
/// closed when the cache is de-initialized.
23+
///
24+
/// Used by `textDocument/doccDocumentation` requests to retrieve symbol graphs for files that are not currently
25+
/// open in the editor. This allows for retrieving multiple symbol graphs from the same file without having
26+
/// to re-open and parse the syntax tree every time.
27+
actor SymbolGraphCache: Sendable {
28+
private weak var sourceKitLSPServer: SourceKitLSPServer?
29+
private var openSnapshots: [DocumentURI: (snapshot: DocumentSnapshot, patchedCompileCommand: SwiftCompileCommand?)]
30+
31+
init(sourceKitLSPServer: SourceKitLSPServer) {
32+
self.sourceKitLSPServer = sourceKitLSPServer
33+
self.openSnapshots = [:]
34+
}
35+
36+
/// Open a unique dummy document in sourcekitd that has the contents of the file on disk for uri, but an arbitrary
37+
/// URI which doesn't exist on disk. Return the symbol graph from sourcekitd.
38+
///
39+
/// The document will be retained until ``DocCSymbolGraphCache`` is de-initialized. This will avoid parsing the same
40+
/// document multiple times if more than one symbol needs to be looked up.
41+
///
42+
/// - Parameter symbolLocation: The location of a symbol to find the symbol graph for.
43+
/// - Returns: The symbol graph for this location, if any.
44+
func fetchSymbolGraph(at symbolLocation: SymbolLocation) async throws -> String? {
45+
let swiftLanguageService = try await swiftLanguageService(for: symbolLocation.documentUri)
46+
let (snapshot, patchedCompileCommand) = try await swiftLanguageService.openSnapshotFromDiskOpenedInSourcekitd(
47+
uri: symbolLocation.documentUri,
48+
fallbackSettingsAfterTimeout: false
49+
)
50+
return try await swiftLanguageService.cursorInfo(
51+
snapshot,
52+
compileCommand: patchedCompileCommand,
53+
Range(snapshot.position(of: symbolLocation)),
54+
includeSymbolGraph: true
55+
).symbolGraph
56+
}
57+
58+
private func swiftLanguageService(for uri: DocumentURI) async throws -> SwiftLanguageService {
59+
guard let sourceKitLSPServer else {
60+
throw ResponseError.internalError("SourceKit-LSP is shutting down")
61+
}
62+
guard let workspace = await sourceKitLSPServer.workspaceForDocument(uri: uri),
63+
let languageService = await sourceKitLSPServer.languageService(for: uri, .swift, in: workspace),
64+
let swiftLanguageService = languageService as? SwiftLanguageService
65+
else {
66+
throw ResponseError.internalError("Unable to find SwiftLanguageService for \(uri)")
67+
}
68+
return swiftLanguageService
69+
}
70+
71+
deinit {
72+
guard let sourceKitLSPServer else {
73+
return
74+
}
75+
76+
let documentsToClose = openSnapshots.values
77+
Task {
78+
for (snapshot, _) in documentsToClose {
79+
guard let workspace = await sourceKitLSPServer.workspaceForDocument(uri: snapshot.uri),
80+
let languageService = await sourceKitLSPServer.languageService(for: snapshot.uri, .swift, in: workspace),
81+
let swiftLanguageService = languageService as? SwiftLanguageService
82+
else {
83+
logger.log("Unable to find SwiftLanguageService to close helper document \(snapshot.uri.forLogging)")
84+
return
85+
}
86+
await swiftLanguageService.closeSnapshotFromDiskOpenedInSourcekitd(snapshot: snapshot)
87+
}
88+
}
89+
}
90+
}
91+
92+
fileprivate extension SwiftLanguageService {
93+
func openSnapshotFromDiskOpenedInSourcekitd(
94+
uri: DocumentURI,
95+
fallbackSettingsAfterTimeout: Bool,
96+
) async throws -> (snapshot: DocumentSnapshot, patchedCompileCommand: SwiftCompileCommand?) {
97+
guard let fileURL = uri.fileURL else {
98+
throw ResponseError.unknown("Cannot create snapshot with on-disk contents for non-file URI \(uri.forLogging)")
99+
}
100+
let snapshot = DocumentSnapshot(
101+
uri: try DocumentURI(filePath: "\(UUID().uuidString)/\(fileURL.filePath)", isDirectory: false),
102+
language: .swift,
103+
version: 0,
104+
lineTable: LineTable(try String(contentsOf: fileURL, encoding: .utf8))
105+
)
106+
let patchedCompileCommand: SwiftCompileCommand? =
107+
if let buildSettings = await self.buildSettings(
108+
for: uri,
109+
fallbackAfterTimeout: fallbackSettingsAfterTimeout
110+
) {
111+
SwiftCompileCommand(buildSettings.patching(newFile: snapshot.uri, originalFile: uri))
112+
} else {
113+
nil
114+
}
115+
116+
_ = try await send(
117+
sourcekitdRequest: \.editorOpen,
118+
self.openDocumentSourcekitdRequest(snapshot: snapshot, compileCommand: patchedCompileCommand),
119+
snapshot: snapshot
120+
)
121+
122+
return (snapshot, patchedCompileCommand)
123+
}
124+
125+
func closeSnapshotFromDiskOpenedInSourcekitd(snapshot: DocumentSnapshot) async {
126+
await orLog("Close helper document '\(snapshot.uri)'") {
127+
_ = try await send(
128+
sourcekitdRequest: \.editorClose,
129+
self.closeDocumentSourcekitdRequest(uri: snapshot.uri),
130+
snapshot: snapshot
131+
)
132+
}
133+
}
134+
}

Sources/SourceKitLSP/Swift/WithSnapshotFromDiskOpenedInSourcekitd.swift

Lines changed: 0 additions & 69 deletions
This file was deleted.

0 commit comments

Comments
 (0)