Skip to content

Commit 08815a0

Browse files
cache snapshots opened in sourcekitd by textDocument/doccDocumentation requests
1 parent 5f1aab8 commit 08815a0

10 files changed

+208
-159
lines changed

Sources/ClangLanguageService/ClangLanguageService.swift

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,14 @@ extension ClangLanguageService {
421421
clangd.send(notification)
422422
}
423423

424+
package func openDocumentOnDisk(snapshot: DocumentSnapshot, originalFile: DocumentURI) async throws {
425+
// clangd doesn't support on-disk documents
426+
}
427+
428+
package func closeDocumentOnDisk(snapshot: DocumentSnapshot) async {
429+
// clangd doesn't support on-disk documents
430+
}
431+
424432
package func reopenDocument(_ notification: ReopenTextDocumentNotification) {}
425433

426434
package func changeDocument(
@@ -512,9 +520,9 @@ extension ClangLanguageService {
512520
return try await forwardRequestToClangd(req)
513521
}
514522

515-
package func symbolGraph(
516-
forOnDiskContentsOf symbolDocumentUri: DocumentURI,
517-
at location: SymbolLocation
523+
package func symbolGraphForDocumentOnDisk(
524+
at location: SymbolLocation,
525+
manager: OnDiskDocumentManager
518526
) async throws -> String? {
519527
return nil
520528
}

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: 25 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -58,42 +58,34 @@ extension DocumentationLanguageService {
5858
guard let index = workspace.index(checkedFor: .deletedFiles) else {
5959
throw ResponseError.requestFailed(doccDocumentationError: .indexNotAvailable)
6060
}
61-
guard let symbolLink = DocCSymbolLink(linkString: symbolName),
62-
let symbolOccurrence = try await index.primaryDefinitionOrDeclarationOccurrence(
63-
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-
else {
68-
throw ResponseError.internalError("Unable to find Swift language service for \(location.documentUri)")
61+
return try await sourceKitLSPServer.withOnDiskDocumentManager { onDiskDocumentManager in
62+
guard let symbolLink = DocCSymbolLink(linkString: symbolName),
63+
let symbolOccurrence = try await index.primaryDefinitionOrDeclarationOccurrence(
64+
ofDocCSymbolLink: symbolLink,
65+
fetchSymbolGraph: {
66+
try await onDiskDocumentManager.languageService(for: $0.documentUri, .swift)
67+
.symbolGraphForDocumentOnDisk(at: $0, manager: onDiskDocumentManager)
6968
}
70-
return try await languageService.symbolGraph(forOnDiskContentsOf: location.documentUri, at: location)
71-
}
69+
)
70+
else {
71+
throw ResponseError.requestFailed(doccDocumentationError: .symbolNotFound(symbolName))
72+
}
73+
guard
74+
let symbolGraph = try await onDiskDocumentManager.languageService(
75+
for: symbolOccurrence.location.documentUri,
76+
.swift
77+
).symbolGraphForDocumentOnDisk(at: symbolOccurrence.location, manager: onDiskDocumentManager)
78+
else {
79+
throw ResponseError.internalError("Unable to retrieve symbol graph for \(symbolOccurrence.symbol.name)")
80+
}
81+
return try await documentationManager.renderDocCDocumentation(
82+
symbolUSR: symbolOccurrence.symbol.usr,
83+
symbolGraph: symbolGraph,
84+
markupFile: snapshot.text,
85+
moduleName: moduleName,
86+
catalogURL: catalogURL
7287
)
73-
else {
74-
throw ResponseError.requestFailed(doccDocumentationError: .symbolNotFound(symbolName))
7588
}
76-
let symbolDocumentUri = symbolOccurrence.location.documentUri
77-
guard
78-
let symbolWorkspace = try await workspaceForDocument(uri: symbolDocumentUri),
79-
let languageService = try await languageService(for: symbolDocumentUri, .swift, in: symbolWorkspace)
80-
else {
81-
throw ResponseError.internalError("Unable to find Swift language service for \(symbolDocumentUri)")
82-
}
83-
let symbolGraph = try await languageService.symbolGraph(
84-
forOnDiskContentsOf: symbolDocumentUri,
85-
at: symbolOccurrence.location
86-
)
87-
guard let symbolGraph else {
88-
throw ResponseError.internalError("Unable to retrieve symbol graph for \(symbolOccurrence.symbol.name)")
89-
}
90-
return try await documentationManager.renderDocCDocumentation(
91-
symbolUSR: symbolOccurrence.symbol.usr,
92-
symbolGraph: symbolGraph,
93-
markupFile: snapshot.text,
94-
moduleName: moduleName,
95-
catalogURL: catalogURL
96-
)
9789
}
9890
// This is a page representing the module itself.
9991
// Create a dummy symbol graph and tell SwiftDocC to convert the module name.

Sources/SourceKitLSP/Documentation/DocumentationLanguageService.swift

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,14 @@ package actor DocumentationLanguageService: LanguageService, Sendable {
9898
// The DocumentationLanguageService does not do anything with document events
9999
}
100100

101+
package func openDocumentOnDisk(snapshot: DocumentSnapshot, originalFile: DocumentURI) async throws {
102+
// The DocumentationLanguageService does not do anything with document events
103+
}
104+
105+
package func closeDocumentOnDisk(snapshot: DocumentSnapshot) async {
106+
// The DocumentationLanguageService does not do anything with document events
107+
}
108+
101109
package func reopenDocument(_ notification: ReopenTextDocumentNotification) async {
102110
// The DocumentationLanguageService does not do anything with document events
103111
}
@@ -143,11 +151,11 @@ package actor DocumentationLanguageService: LanguageService, Sendable {
143151
[]
144152
}
145153

146-
package func symbolGraph(
147-
forOnDiskContentsOf symbolDocumentUri: DocumentURI,
148-
at location: SymbolLocation
154+
package func symbolGraphForDocumentOnDisk(
155+
at location: SymbolLocation,
156+
manager: OnDiskDocumentManager
149157
) async throws -> String? {
150-
return nil
158+
nil
151159
}
152160

153161
package func openGeneratedInterface(

Sources/SourceKitLSP/LanguageService.swift

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,12 @@ package protocol LanguageService: AnyObject, Sendable {
144144
/// Sent to close a document on the Language Server.
145145
func closeDocument(_ notification: DidCloseTextDocumentNotification) async
146146

147+
/// Sent to open up a document on the Language Server whose contents are on-disk.
148+
func openDocumentOnDisk(snapshot: DocumentSnapshot, originalFile: DocumentURI) async throws
149+
150+
/// Sent to close a document on the Language Server whose contents are on-disk.
151+
func closeDocumentOnDisk(snapshot: DocumentSnapshot) async
152+
147153
/// Re-open the given document, discarding any in-memory state and forcing an AST to be re-built after build settings
148154
/// have been changed. This needs to be handled via a notification to ensure that no other request for this document
149155
/// is executing at the same time.
@@ -183,10 +189,7 @@ package protocol LanguageService: AnyObject, Sendable {
183189

184190
/// Return the symbol graph at the given location for the contents of the document as they are on-disk (opposed to the
185191
/// in-memory modified version of the document).
186-
func symbolGraph(
187-
forOnDiskContentsOf symbolDocumentUri: DocumentURI,
188-
at location: SymbolLocation
189-
) async throws -> String?
192+
func symbolGraphForDocumentOnDisk(at location: SymbolLocation, manager: OnDiskDocumentManager) async throws -> String?
190193

191194
/// Request a generated interface of a module to display in the IDE.
192195
///
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import Foundation
2+
package import LanguageServerProtocol
3+
import SKLogging
4+
import SKUtilities
5+
import SwiftExtensions
6+
7+
package actor OnDiskDocumentManager {
8+
private weak var sourceKitLSPServer: SourceKitLSPServer?
9+
private var openSnapshots: [DocumentURI: DocumentSnapshot]
10+
11+
fileprivate init(sourceKitLSPServer: SourceKitLSPServer) {
12+
self.sourceKitLSPServer = sourceKitLSPServer
13+
openSnapshots = [:]
14+
}
15+
16+
/// Retrieves the ``LanguageService`` for a given ``DocumentURI`` and ``Language``.
17+
package func languageService(for uri: DocumentURI, _ language: Language) async throws -> LanguageService {
18+
guard let sourceKitLSPServer,
19+
let workspace = await sourceKitLSPServer.workspaceForDocument(uri: uri),
20+
let languageService = await sourceKitLSPServer.languageService(for: uri, language, in: workspace)
21+
else {
22+
throw ResponseError.unknown("Unable to find language service for URI: \(uri)")
23+
}
24+
return languageService
25+
}
26+
27+
/// Opens a dummy ``DocumentSnapshot`` with contents from disk for a given ``DocumentURI`` and ``Language``.
28+
///
29+
/// The snapshot will remain cached until ``closeAllDocuments()`` is called.
30+
package func open(uri: DocumentURI, language: Language) async throws -> DocumentSnapshot {
31+
guard let fileURL = uri.fileURL else {
32+
throw ResponseError.unknown("Cannot create snapshot with on-disk contents for non-file URI \(uri.forLogging)")
33+
}
34+
35+
if let cachedSnapshot = openSnapshots[uri] {
36+
return cachedSnapshot
37+
}
38+
39+
let snapshot = DocumentSnapshot(
40+
uri: try DocumentURI(filePath: "\(UUID().uuidString)/\(fileURL.filePath)", isDirectory: false),
41+
language: language,
42+
version: 0,
43+
lineTable: LineTable(try String(contentsOf: fileURL, encoding: .utf8))
44+
)
45+
try await languageService(for: uri, language).openDocumentOnDisk(snapshot: snapshot, originalFile: uri)
46+
openSnapshots[uri] = snapshot
47+
return snapshot
48+
}
49+
50+
/// Close all of the ``DocumentSnapshot``s that were opened by this ``OnDiskDocumentManager``.
51+
package func closeAllDocuments() async {
52+
for snapshot in openSnapshots.values {
53+
await orLog("Closing snapshot from on-disk contents: \(snapshot.uri.forLogging)") {
54+
try await languageService(for: snapshot.uri, snapshot.language).closeDocumentOnDisk(snapshot: snapshot)
55+
}
56+
}
57+
openSnapshots = [:]
58+
}
59+
}
60+
61+
package extension SourceKitLSPServer {
62+
nonisolated(nonsending) func withOnDiskDocumentManager<T>(
63+
_ body: (OnDiskDocumentManager) async throws -> T
64+
) async rethrows -> T {
65+
let manager = OnDiskDocumentManager(sourceKitLSPServer: self)
66+
do {
67+
let result = try await body(manager)
68+
await manager.closeAllDocuments()
69+
return result
70+
} catch {
71+
await manager.closeAllDocuments()
72+
throw error
73+
}
74+
}
75+
}

Sources/SwiftLanguageService/DoccDocumentation.swift

Lines changed: 19 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ extension SwiftLanguageService {
3535
throw ResponseError.invalidParams("A position must be provided for Swift files")
3636
}
3737
let snapshot = try documentManager.latestSnapshot(req.textDocument.uri)
38+
3839
var moduleName: String? = nil
3940
var catalogURL: URL? = nil
4041
if let target = await workspace.buildServerManager.canonicalTarget(for: req.textDocument.uri) {
@@ -70,27 +71,19 @@ extension SwiftLanguageService {
7071
throw ResponseError.internalError("Unable to retrieve symbol graph for the document")
7172
}
7273
// Locate the documentation extension and include it in the request if one exists
73-
let markupExtensionFile = await orLog("Finding markup extension file for symbol \(symbolUSR)") {
74-
try await findMarkupExtensionFile(
75-
workspace: workspace,
76-
documentationManager: documentationManager,
77-
catalogURL: catalogURL,
78-
for: symbolUSR,
79-
fetchSymbolGraph: { symbolLocation in
80-
try await withSnapshotFromDiskOpenedInSourcekitd(
81-
uri: symbolLocation.documentUri,
82-
fallbackSettingsAfterTimeout: false
83-
) { (snapshot, compileCommand) in
84-
let (_, _, symbolGraph) = try await self.cursorInfo(
85-
snapshot,
86-
compileCommand: compileCommand,
87-
Range(snapshot.position(of: symbolLocation)),
88-
includeSymbolGraph: true
89-
)
90-
return symbolGraph
74+
let markupExtensionFile = await sourceKitLSPServer.withOnDiskDocumentManager { onDiskDocumentManager in
75+
await orLog("Finding markup extension file for symbol \(symbolUSR)") {
76+
try await findMarkupExtensionFile(
77+
workspace: workspace,
78+
documentationManager: documentationManager,
79+
catalogURL: catalogURL,
80+
for: symbolUSR,
81+
fetchSymbolGraph: {
82+
try await onDiskDocumentManager.languageService(for: $0.documentUri, .swift)
83+
.symbolGraphForDocumentOnDisk(at: $0, manager: onDiskDocumentManager)
9184
}
92-
}
93-
)
85+
)
86+
}
9487
}
9588
return try await documentationManager.renderDocCDocumentation(
9689
symbolUSR: symbolUSR,
@@ -114,11 +107,12 @@ extension SwiftLanguageService {
114107
}
115108
let catalogIndex = try await documentationManager.catalogIndex(for: catalogURL)
116109
guard let index = workspace.index(checkedFor: .deletedFiles),
117-
let symbolInformation = try await index.doccSymbolInformation(
118-
ofUSR: symbolUSR,
119-
fetchSymbolGraph: fetchSymbolGraph
120-
),
121-
let markupExtensionFileURL = catalogIndex.documentationExtension(for: symbolInformation)
110+
let markupExtensionFileURL = try await catalogIndex.documentationExtension(
111+
for: index.doccSymbolInformation(
112+
ofUSR: symbolUSR,
113+
fetchSymbolGraph: fetchSymbolGraph
114+
)
115+
)
122116
else {
123117
return nil
124118
}

Sources/SwiftLanguageService/SwiftLanguageService.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,34 @@ extension SwiftLanguageService {
586586
}
587587
}
588588

589+
package func openDocumentOnDisk(snapshot: DocumentSnapshot, originalFile: DocumentURI) async throws {
590+
let patchedCompileCommand: SwiftCompileCommand? =
591+
if let buildSettings = await self.buildSettings(
592+
for: originalFile,
593+
fallbackAfterTimeout: false
594+
) {
595+
SwiftCompileCommand(buildSettings.patching(newFile: snapshot.uri, originalFile: originalFile))
596+
} else {
597+
nil
598+
}
599+
600+
_ = try await send(
601+
sourcekitdRequest: \.editorOpen,
602+
self.openDocumentSourcekitdRequest(snapshot: snapshot, compileCommand: patchedCompileCommand),
603+
snapshot: snapshot
604+
)
605+
}
606+
607+
package func closeDocumentOnDisk(snapshot: DocumentSnapshot) async {
608+
await orLog("Close on-disk document in sourcekitd '\(snapshot.uri)'") {
609+
_ = try await send(
610+
sourcekitdRequest: \.editorClose,
611+
self.closeDocumentSourcekitdRequest(uri: snapshot.uri),
612+
snapshot: snapshot
613+
)
614+
}
615+
}
616+
589617
/// Cancels any in-flight tasks to send a `PublishedDiagnosticsNotification` after edits.
590618
private func cancelInFlightPublishDiagnosticsTask(for document: DocumentURI) {
591619
if let inFlightTask = inFlightPublishDiagnosticsTasks[document] {

0 commit comments

Comments
 (0)