Merge pull request #2226 from matthewbastien/docc-cache-snapshots

[DocC Live Preview] Cache on-disk snapshots opened in sourcekitd
This commit is contained in:
Alex Hoppen
2025-08-27 23:30:42 +02:00
committed by GitHub
12 changed files with 246 additions and 178 deletions

View File

@@ -795,6 +795,28 @@ package actor BuildServerManager: QueueBasedMessageHandler {
return languageFromBuildServer ?? Language(inferredFromFileExtension: document)
}
/// Returns the language that a document should be interpreted in for background tasks where the editor doesn't
/// specify the document's language.
///
/// If the language could not be determined, this method throws an error.
package func defaultLanguageInCanonicalTarget(for document: DocumentURI) async throws -> Language {
struct UnableToInferLanguage: Error, CustomStringConvertible {
let document: DocumentURI
var description: String { "Unable to infer language for \(document)" }
}
guard let canonicalTarget = await self.canonicalTarget(for: document) else {
guard let language = Language(inferredFromFileExtension: document) else {
throw UnableToInferLanguage(document: document)
}
return language
}
guard let language = await defaultLanguage(for: document, in: canonicalTarget) else {
throw UnableToInferLanguage(document: document)
}
return language
}
/// Retrieve information about the given source file within the build server.
package func sourceFileInfo(for document: DocumentURI) async -> SourceFileInfo? {
return await orLog("Getting targets for source file") {

View File

@@ -496,14 +496,10 @@ extension ClangLanguageService {
}
package func doccDocumentation(_ req: DoccDocumentationRequest) async throws -> DoccDocumentationResponse {
guard let sourceKitLSPServer else {
throw ResponseError.unknown("Connection to the editor closed")
guard let language = openDocuments[req.textDocument.uri] else {
throw ResponseError.requestFailed("Documentation preview is not available for clang files")
}
let snapshot = try sourceKitLSPServer.documentManager.latestSnapshot(req.textDocument.uri)
throw ResponseError.requestFailed(
"Documentation preview is not available for \(snapshot.language.description) files"
)
throw ResponseError.requestFailed("Documentation preview is not available for \(language.description) files")
}
package func symbolInfo(_ req: SymbolInfoRequest) async throws -> [SymbolDetails] {

View File

@@ -23,7 +23,7 @@ enum DocCDocumentationError: LocalizedError {
case .unsupportedLanguage(let language):
return "Documentation preview is not available for \(language.description) files"
case .indexNotAvailable:
return "The index is not availble to complete the request"
return "The index is not available to complete the request"
case .symbolNotFound(let symbolName):
return "Could not find symbol \(symbolName) in the project"
}

View File

@@ -104,47 +104,35 @@ extension DocumentationLanguageService {
guard let index = workspace.index(checkedFor: .deletedFiles) else {
throw ResponseError.requestFailed(doccDocumentationError: .indexNotAvailable)
}
guard let symbolLink = DocCSymbolLink(linkString: symbolName),
let symbolOccurrence = try await index.primaryDefinitionOrDeclarationOccurrence(
ofDocCSymbolLink: symbolLink,
fetchSymbolGraph: { location in
guard let symbolWorkspace = await sourceKitLSPServer.workspaceForDocument(uri: location.documentUri) else {
throw ResponseError.internalError("Unable to find language service for \(location.documentUri)")
return try await sourceKitLSPServer.withOnDiskDocumentManager { onDiskDocumentManager in
guard let symbolLink = DocCSymbolLink(linkString: symbolName),
let symbolOccurrence = try await index.primaryDefinitionOrDeclarationOccurrence(
ofDocCSymbolLink: symbolLink,
fetchSymbolGraph: { location in
return try await sourceKitLSPServer.primaryLanguageService(
for: location.documentUri,
workspace.buildServerManager.defaultLanguageInCanonicalTarget(for: location.documentUri),
in: workspace
)
.symbolGraph(forOnDiskContentsAt: location, in: workspace, manager: onDiskDocumentManager)
}
let languageService = try await sourceKitLSPServer.primaryLanguageService(
for: location.documentUri,
.swift,
in: symbolWorkspace
)
return try await languageService.symbolGraph(
forOnDiskContentsOf: location.documentUri,
at: location
)
}
)
else {
throw ResponseError.requestFailed(doccDocumentationError: .symbolNotFound(symbolName))
}
let symbolGraph = try await sourceKitLSPServer.primaryLanguageService(
for: symbolOccurrence.location.documentUri,
workspace.buildServerManager.defaultLanguageInCanonicalTarget(for: symbolOccurrence.location.documentUri),
in: workspace
).symbolGraph(forOnDiskContentsAt: symbolOccurrence.location, in: workspace, manager: onDiskDocumentManager)
return try await documentationManager.renderDocCDocumentation(
symbolUSR: symbolOccurrence.symbol.usr,
symbolGraph: symbolGraph,
markupFile: snapshot.text,
moduleName: moduleName,
catalogURL: catalogURL
)
else {
throw ResponseError.requestFailed(doccDocumentationError: .symbolNotFound(symbolName))
}
let symbolDocumentUri = symbolOccurrence.location.documentUri
guard let symbolWorkspace = await sourceKitLSPServer.workspaceForDocument(uri: symbolDocumentUri) else {
throw ResponseError.internalError("Unable to find language service for \(symbolDocumentUri)")
}
let languageService = try await sourceKitLSPServer.primaryLanguageService(
for: symbolDocumentUri,
.swift,
in: symbolWorkspace
)
let symbolGraph = try await languageService.symbolGraph(
forOnDiskContentsOf: symbolDocumentUri,
at: symbolOccurrence.location
)
return try await documentationManager.renderDocCDocumentation(
symbolUSR: symbolOccurrence.symbol.usr,
symbolGraph: symbolGraph,
markupFile: snapshot.text,
moduleName: moduleName,
catalogURL: catalogURL
)
}
// This is a page representing the module itself.
// Create a dummy symbol graph and tell SwiftDocC to convert the module name.
@@ -175,24 +163,26 @@ extension DocumentationLanguageService {
in: workspace
).symbolGraph(for: snapshot, at: position)
// Locate the documentation extension and include it in the request if one exists
let markupExtensionFile = await orLog("Finding markup extension file for symbol \(symbolUSR)") {
try await findMarkupExtensionFile(
workspace: workspace,
documentationManager: documentationManager,
catalogURL: catalogURL,
for: symbolUSR,
fetchSymbolGraph: { location in
guard let symbolWorkspace = await sourceKitLSPServer.workspaceForDocument(uri: location.documentUri) else {
throw ResponseError.internalError("Unable to find language service for \(location.documentUri)")
let markupExtensionFile = await sourceKitLSPServer.withOnDiskDocumentManager {
[documentationManager, documentManager = try documentManager] onDiskDocumentManager in
await orLog("Finding markup extension file for symbol \(symbolUSR)") {
try await Self.findMarkupExtensionFile(
workspace: workspace,
documentationManager: documentationManager,
documentManager: documentManager,
catalogURL: catalogURL,
for: symbolUSR,
fetchSymbolGraph: { location in
try await sourceKitLSPServer.primaryLanguageService(
for: location.documentUri,
snapshot.language,
in: workspace
)
.symbolGraph(forOnDiskContentsAt: location, in: workspace, manager: onDiskDocumentManager)
}
let languageService = try await sourceKitLSPServer.primaryLanguageService(
for: location.documentUri,
.swift,
in: symbolWorkspace
)
return try await languageService.symbolGraph(forOnDiskContentsOf: location.documentUri, at: location)
}
)
)
}
}
return try await documentationManager.renderDocCDocumentation(
symbolUSR: symbolUSR,
@@ -204,9 +194,10 @@ extension DocumentationLanguageService {
)
}
private func findMarkupExtensionFile(
private static func findMarkupExtensionFile(
workspace: Workspace,
documentationManager: DocCDocumentationManager,
documentManager: DocumentManager,
catalogURL: URL?,
for symbolUSR: String,
fetchSymbolGraph: @Sendable (SymbolLocation) async throws -> String?
@@ -215,16 +206,17 @@ extension DocumentationLanguageService {
return nil
}
let catalogIndex = try await documentationManager.catalogIndex(for: catalogURL)
guard let index = workspace.index(checkedFor: .deletedFiles),
let symbolInformation = try await index.doccSymbolInformation(
ofUSR: symbolUSR,
fetchSymbolGraph: fetchSymbolGraph
),
let markupExtensionFileURL = catalogIndex.documentationExtension(for: symbolInformation)
else {
guard let index = workspace.index(checkedFor: .deletedFiles) else {
return nil
}
return try? documentManager.latestSnapshotOrDisk(
let symbolInformation = try await index.doccSymbolInformation(
ofUSR: symbolUSR,
fetchSymbolGraph: fetchSymbolGraph
)
guard let markupExtensionFileURL = catalogIndex.documentationExtension(for: symbolInformation) else {
return nil
}
return documentManager.latestSnapshotOrDisk(
DocumentURI(markupExtensionFileURL),
language: .markdown
)?.text

View File

@@ -40,7 +40,7 @@ extension CheckedIndex {
var result: [SymbolOccurrence] = []
for occurrence in topLevelSymbolOccurrences {
let info = try await doccSymbolInformation(ofUSR: occurrence.symbol.usr, fetchSymbolGraph: fetchSymbolGraph)
if let info, info.matches(symbolLink) {
if info.matches(symbolLink) {
result.append(occurrence)
}
}
@@ -60,9 +60,9 @@ extension CheckedIndex {
func doccSymbolInformation(
ofUSR usr: String,
fetchSymbolGraph: (SymbolLocation) async throws -> String?
) async throws -> DocCSymbolInformation? {
) async throws -> DocCSymbolInformation {
guard let topLevelSymbolOccurrence = primaryDefinitionOrDeclarationOccurrence(ofUSR: usr) else {
return nil
throw DocCCheckedIndexError.emptyDocCSymbolLink
}
let moduleName = topLevelSymbolOccurrence.location.moduleName
var symbols = [topLevelSymbolOccurrence]

View File

@@ -13,6 +13,7 @@ add_library(SourceKitLSP STATIC
LogMessageNotification+representingStructureUsingEmojiPrefixIfNecessary.swift
MacroExpansionReferenceDocumentURLData.swift
MessageHandlingDependencyTracker.swift
OnDiskDocumentManager.swift
ReferenceDocumentURL.swift
Rename.swift
SemanticTokensLegend+SourceKitLSPLegend.swift

View File

@@ -10,6 +10,7 @@
//
//===----------------------------------------------------------------------===//
package import BuildServerIntegration
import Foundation
package import IndexStoreDB
package import LanguageServerProtocol
@@ -148,6 +149,18 @@ package protocol LanguageService: AnyObject, Sendable {
/// Sent to close a document on the Language Server.
func closeDocument(_ notification: DidCloseTextDocumentNotification) async
/// Sent to open up a document on the Language Server whose contents are on-disk.
///
/// The snapshot will have a synthesized name and the caller is responsible for synthesizing build settings for it.
///
/// - Important: This should only be called by `OnDiskDocumentManager`.
func openOnDiskDocument(snapshot: DocumentSnapshot, buildSettings: FileBuildSettings) async throws
/// Sent to close a document that was opened by `openOnDiskDocument`.
///
/// - Important: This should only be called by `OnDiskDocumentManager`.
func closeOnDiskDocument(uri: DocumentURI) async throws
/// Re-open the given document, discarding any in-memory state and forcing an AST to be re-built after build settings
/// have been changed. This needs to be handled via a notification to ensure that no other request for this document
/// is executing at the same time.
@@ -197,8 +210,9 @@ package protocol LanguageService: AnyObject, Sendable {
/// Return the symbol graph at the given location for the contents of the document as they are on-disk (opposed to the
/// in-memory modified version of the document).
func symbolGraph(
forOnDiskContentsOf symbolDocumentUri: DocumentURI,
at location: SymbolLocation
forOnDiskContentsAt location: SymbolLocation,
in workspace: Workspace,
manager: OnDiskDocumentManager
) async throws -> String
/// Request a generated interface of a module to display in the IDE.
@@ -330,6 +344,14 @@ package extension LanguageService {
func clientInitialized(_ initialized: LanguageServerProtocol.InitializedNotification) async {}
func openOnDiskDocument(snapshot: DocumentSnapshot, buildSettings: FileBuildSettings) async throws {
throw ResponseError.unknown("\(#function) not implemented in \(Self.self)")
}
func closeOnDiskDocument(uri: DocumentURI) async throws {
throw ResponseError.unknown("\(#function) not implemented in \(Self.self)")
}
func willSaveDocument(_ notification: WillSaveTextDocumentNotification) async {}
func didSaveDocument(_ notification: DidSaveTextDocumentNotification) async {}
@@ -368,8 +390,9 @@ package extension LanguageService {
}
func symbolGraph(
forOnDiskContentsOf symbolDocumentUri: DocumentURI,
at location: SymbolLocation
forOnDiskContentsAt location: SymbolLocation,
in workspace: Workspace,
manager: OnDiskDocumentManager
) async throws -> String {
throw ResponseError.internalError("\(#function) not implemented in \(Self.self)")
}

View File

@@ -0,0 +1,95 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//
package import BuildServerIntegration
import Foundation
package import LanguageServerProtocol
import SKLogging
import SKUtilities
import SwiftExtensions
package actor OnDiskDocumentManager {
private let sourceKitLSPServer: SourceKitLSPServer
private var openSnapshots:
[DocumentURI: (snapshot: DocumentSnapshot, buildSettings: FileBuildSettings, workspace: Workspace)]
fileprivate init(sourceKitLSPServer: SourceKitLSPServer) {
self.sourceKitLSPServer = sourceKitLSPServer
openSnapshots = [:]
}
/// Opens a dummy ``DocumentSnapshot`` with contents from disk for a given ``DocumentURI`` and ``Language``.
///
/// The snapshot will remain cached until ``closeAllDocuments()`` is called.
package func open(
uri: DocumentURI,
language: Language,
in workspace: Workspace
) async throws -> (snapshot: DocumentSnapshot, buildSettings: FileBuildSettings) {
guard let fileURL = uri.fileURL else {
throw ResponseError.unknown("Cannot create snapshot with on-disk contents for non-file URI \(uri.forLogging)")
}
if let cachedSnapshot = openSnapshots[uri] {
return (cachedSnapshot.snapshot, cachedSnapshot.buildSettings)
}
let snapshot = DocumentSnapshot(
uri: try DocumentURI(filePath: "\(UUID().uuidString)/\(fileURL.filePath)", isDirectory: false),
language: language,
version: 0,
lineTable: LineTable(try String(contentsOf: fileURL, encoding: .utf8))
)
let languageService = try await sourceKitLSPServer.primaryLanguageService(for: uri, language, in: workspace)
let originalBuildSettings = await workspace.buildServerManager.buildSettingsInferredFromMainFile(
for: uri,
language: language,
fallbackAfterTimeout: false
)
guard let originalBuildSettings else {
throw ResponseError.unknown("Failed to infer build settings for \(uri)")
}
let patchedBuildSettings = originalBuildSettings.patching(newFile: snapshot.uri, originalFile: uri)
try await languageService.openOnDiskDocument(snapshot: snapshot, buildSettings: patchedBuildSettings)
openSnapshots[uri] = (snapshot, patchedBuildSettings, workspace)
return (snapshot, patchedBuildSettings)
}
/// Close all of the ``DocumentSnapshot``s that were opened by this ``OnDiskDocumentManager``.
fileprivate func closeAllDocuments() async {
for (snapshot, _, workspace) in openSnapshots.values {
await orLog("Closing snapshot from on-disk contents: \(snapshot.uri.forLogging)") {
let languageService =
try await sourceKitLSPServer.primaryLanguageService(for: snapshot.uri, snapshot.language, in: workspace)
try await languageService.closeOnDiskDocument(uri: snapshot.uri)
}
}
openSnapshots = [:]
}
}
package extension SourceKitLSPServer {
nonisolated func withOnDiskDocumentManager<T>(
_ body: (OnDiskDocumentManager) async throws -> T
) async rethrows -> T {
let manager = OnDiskDocumentManager(sourceKitLSPServer: self)
do {
let result = try await body(manager)
await manager.closeAllDocuments()
return result
} catch {
await manager.closeAllDocuments()
throw error
}
}
}

View File

@@ -21,7 +21,6 @@ add_library(SwiftLanguageService STATIC
SemanticRefactorCommand.swift
DocumentFormatting.swift
DiagnosticReportManager.swift
WithSnapshotFromDiskOpenedInSourcekitd.swift
SemanticTokens.swift
ExpandMacroCommand.swift
Diagnostic.swift

View File

@@ -294,29 +294,24 @@ package actor SwiftLanguageService: LanguageService, Sendable {
}
}
func buildSettings(for document: DocumentURI, fallbackAfterTimeout: Bool) async -> FileBuildSettings? {
let buildSettingsFile = document.buildSettingsFile
func compileCommand(for document: DocumentURI, fallbackAfterTimeout: Bool) async -> SwiftCompileCommand? {
guard let sourceKitLSPServer else {
logger.fault("Cannot retrieve build settings because SourceKitLSPServer is no longer alive")
return nil
}
guard let workspace = await sourceKitLSPServer.workspaceForDocument(uri: buildSettingsFile) else {
guard let workspace = await sourceKitLSPServer.workspaceForDocument(uri: document.buildSettingsFile) else {
return nil
}
return await workspace.buildServerManager.buildSettingsInferredFromMainFile(
for: buildSettingsFile,
let settings = await workspace.buildServerManager.buildSettingsInferredFromMainFile(
for: document.buildSettingsFile,
language: .swift,
fallbackAfterTimeout: fallbackAfterTimeout
)
}
func compileCommand(for document: DocumentURI, fallbackAfterTimeout: Bool) async -> SwiftCompileCommand? {
if let settings = await self.buildSettings(for: document, fallbackAfterTimeout: fallbackAfterTimeout) {
return SwiftCompileCommand(settings)
} else {
guard let settings else {
return nil
}
return SwiftCompileCommand(settings)
}
func send(
@@ -586,6 +581,22 @@ extension SwiftLanguageService {
}
}
package func openOnDiskDocument(snapshot: DocumentSnapshot, buildSettings: FileBuildSettings) async throws {
_ = try await send(
sourcekitdRequest: \.editorOpen,
self.openDocumentSourcekitdRequest(snapshot: snapshot, compileCommand: SwiftCompileCommand(buildSettings)),
snapshot: snapshot
)
}
package func closeOnDiskDocument(uri: DocumentURI) async throws {
_ = try await send(
sourcekitdRequest: \.editorClose,
self.closeDocumentSourcekitdRequest(uri: uri),
snapshot: nil
)
}
/// Cancels any in-flight tasks to send a `PublishedDiagnosticsNotification` after edits.
private func cancelInFlightPublishDiagnosticsTask(for document: DocumentURI) {
if let inFlightTask = inFlightPublishDiagnosticsTasks[document] {

View File

@@ -2,7 +2,7 @@
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors
// 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
@@ -10,6 +10,7 @@
//
//===----------------------------------------------------------------------===//
import BuildServerIntegration
import Foundation
package import IndexStoreDB
package import LanguageServerProtocol
@@ -19,24 +20,22 @@ import SwiftSyntax
extension SwiftLanguageService {
package func symbolGraph(
forOnDiskContentsOf symbolDocumentUri: DocumentURI,
at location: SymbolLocation
forOnDiskContentsAt location: SymbolLocation,
in workspace: Workspace,
manager: OnDiskDocumentManager
) async throws -> String {
return try await withSnapshotFromDiskOpenedInSourcekitd(
uri: symbolDocumentUri,
fallbackSettingsAfterTimeout: false
) { snapshot, compileCommand in
let symbolGraph = try await cursorInfo(
snapshot,
compileCommand: compileCommand,
Range(snapshot.position(of: location)),
includeSymbolGraph: true
).symbolGraph
guard let symbolGraph else {
throw ResponseError.internalError("Unable to retrieve symbol graph")
}
return symbolGraph
let (snapshot, buildSettings) = try await manager.open(uri: location.documentUri, language: .swift, in: workspace)
let symbolGraph = try await cursorInfo(
snapshot,
compileCommand: SwiftCompileCommand(buildSettings),
Range(snapshot.position(of: location)),
includeSymbolGraph: true
).symbolGraph
guard let symbolGraph else {
throw ResponseError.internalError("Unable to retrieve symbol graph")
}
return symbolGraph
}
package func symbolGraph(

View File

@@ -1,70 +0,0 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2018 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
//
//===----------------------------------------------------------------------===//
import BuildServerIntegration
import Foundation
import LanguageServerProtocol
import SKLogging
import SKUtilities
import SourceKitLSP
import SwiftExtensions
extension SwiftLanguageService {
/// Open a unique dummy document in sourcekitd that has the contents of the file on disk for `uri` but an arbitrary
/// URI which doesn't exist on disk. Invoke `body` with a snapshot that contains the on-disk document contents and has
/// that dummy URI as well as build settings that were inferred from `uri` but have that URI replaced with the dummy
/// URI. Close the document in sourcekit after `body` has finished.
func withSnapshotFromDiskOpenedInSourcekitd<Result: Sendable>(
uri: DocumentURI,
fallbackSettingsAfterTimeout: Bool,
body: (_ snapshot: DocumentSnapshot, _ patchedCompileCommand: SwiftCompileCommand?) async throws -> Result
) async throws -> Result {
guard let fileURL = uri.fileURL else {
throw ResponseError.unknown("Cannot create snapshot with on-disk contents for non-file URI \(uri.forLogging)")
}
let snapshot = DocumentSnapshot(
uri: try DocumentURI(filePath: "\(UUID().uuidString)/\(fileURL.filePath)", isDirectory: false),
language: .swift,
version: 0,
lineTable: LineTable(try String(contentsOf: fileURL, encoding: .utf8))
)
let patchedCompileCommand: SwiftCompileCommand? =
if let buildSettings = await self.buildSettings(
for: uri,
fallbackAfterTimeout: fallbackSettingsAfterTimeout
) {
SwiftCompileCommand(buildSettings.patching(newFile: snapshot.uri, originalFile: uri))
} else {
nil
}
_ = try await send(
sourcekitdRequest: \.editorOpen,
self.openDocumentSourcekitdRequest(snapshot: snapshot, compileCommand: patchedCompileCommand),
snapshot: snapshot
)
let result: Swift.Result<Result, Error>
do {
result = .success(try await body(snapshot, patchedCompileCommand))
} catch {
result = .failure(error)
}
await orLog("Close helper document '\(snapshot.uri)' for cursorInfoFromDisk") {
_ = try await send(
sourcekitdRequest: \.editorClose,
self.closeDocumentSourcekitdRequest(uri: snapshot.uri),
snapshot: snapshot
)
}
return try result.get()
}
}