diff --git a/Sources/DocCDocumentation/IndexStoreDB+Extensions.swift b/Sources/DocCDocumentation/IndexStoreDB+Extensions.swift index 6a4eba865..e3a9ffc04 100644 --- a/Sources/DocCDocumentation/IndexStoreDB+Extensions.swift +++ b/Sources/DocCDocumentation/IndexStoreDB+Extensions.swift @@ -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 { package 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] diff --git a/Sources/SourceKitLSP/CMakeLists.txt b/Sources/SourceKitLSP/CMakeLists.txt index 23026f4b2..2433d71d9 100644 --- a/Sources/SourceKitLSP/CMakeLists.txt +++ b/Sources/SourceKitLSP/CMakeLists.txt @@ -52,6 +52,7 @@ target_sources(SourceKitLSP PRIVATE Swift/GeneratedInterfaceManager.swift Swift/MacroExpansion.swift Swift/MacroExpansionReferenceDocumentURLData.swift + Swift/OnDiskDocumentManager.swift Swift/OpenInterface.swift Swift/RefactoringEdit.swift Swift/RefactoringResponse.swift @@ -73,7 +74,6 @@ target_sources(SourceKitLSP PRIVATE Swift/SyntaxHighlightingTokens.swift Swift/SyntaxTreeManager.swift Swift/VariableTypeInfo.swift - Swift/WithSnapshotFromDiskOpenedInSourcekitd.swift ) set_target_properties(SourceKitLSP PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) diff --git a/Sources/SourceKitLSP/Documentation/DoccDocumentationHandler.swift b/Sources/SourceKitLSP/Documentation/DoccDocumentationHandler.swift index 7440221f4..01c471fac 100644 --- a/Sources/SourceKitLSP/Documentation/DoccDocumentationHandler.swift +++ b/Sources/SourceKitLSP/Documentation/DoccDocumentationHandler.swift @@ -58,62 +58,26 @@ 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 = try await workspaceForDocument(uri: location.documentUri), - let languageService = try await languageService(for: location.documentUri, .swift, in: symbolWorkspace) - as? SwiftLanguageService - else { - throw ResponseError.internalError("Unable to find Swift language service for \(location.documentUri)") - } - return try await languageService.withSnapshotFromDiskOpenedInSourcekitd( - uri: location.documentUri, - fallbackSettingsAfterTimeout: false - ) { (snapshot, compileCommand) in - let (_, _, symbolGraph) = try await languageService.cursorInfo( - snapshot, - compileCommand: compileCommand, - Range(snapshot.position(of: location)), - includeSymbolGraph: true - ) - return symbolGraph - } - } + return try await sourceKitLSPServer.withOnDiskDocumentManager { onDiskDocumentManager in + guard let symbolLink = DocCSymbolLink(linkString: symbolName), + let symbolOccurrence = try await index.primaryDefinitionOrDeclarationOccurrence( + ofDocCSymbolLink: symbolLink, + fetchSymbolGraph: onDiskDocumentManager.fetchSymbolGraph(at:) + ) + else { + throw ResponseError.requestFailed(doccDocumentationError: .symbolNotFound(symbolName)) + } + guard let symbolGraph = try await onDiskDocumentManager.fetchSymbolGraph(at: symbolOccurrence.location) else { + throw ResponseError.internalError("Unable to retrieve symbol graph for \(symbolOccurrence.symbol.name)") + } + 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 = try await workspaceForDocument(uri: symbolDocumentUri), - let languageService = try await languageService(for: symbolDocumentUri, .swift, in: symbolWorkspace) - as? SwiftLanguageService - else { - throw ResponseError.internalError("Unable to find Swift language service for \(symbolDocumentUri)") - } - let symbolGraph = try await languageService.withSnapshotFromDiskOpenedInSourcekitd( - uri: symbolDocumentUri, - fallbackSettingsAfterTimeout: false - ) { snapshot, compileCommand in - try await languageService.cursorInfo( - snapshot, - compileCommand: compileCommand, - Range(snapshot.position(of: symbolOccurrence.location)), - includeSymbolGraph: true - ).symbolGraph - } - guard let symbolGraph else { - throw ResponseError.internalError("Unable to retrieve symbol graph for \(symbolOccurrence.symbol.name)") - } - 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. diff --git a/Sources/SourceKitLSP/Swift/DoccDocumentation.swift b/Sources/SourceKitLSP/Swift/DoccDocumentation.swift index 21857e919..d7f3c28f7 100644 --- a/Sources/SourceKitLSP/Swift/DoccDocumentation.swift +++ b/Sources/SourceKitLSP/Swift/DoccDocumentation.swift @@ -69,27 +69,16 @@ extension SwiftLanguageService { throw ResponseError.internalError("Unable to retrieve symbol graph for the document") } // 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: { symbolLocation in - try await withSnapshotFromDiskOpenedInSourcekitd( - uri: symbolLocation.documentUri, - fallbackSettingsAfterTimeout: false - ) { (snapshot, compileCommand) in - let (_, _, symbolGraph) = try await self.cursorInfo( - snapshot, - compileCommand: compileCommand, - Range(snapshot.position(of: symbolLocation)), - includeSymbolGraph: true - ) - return symbolGraph - } - } - ) + let markupExtensionFile = await sourceKitLSPServer.withOnDiskDocumentManager { onDiskDocumentManager in + await orLog("Finding markup extension file for symbol \(symbolUSR)") { + try await findMarkupExtensionFile( + workspace: workspace, + documentationManager: documentationManager, + catalogURL: catalogURL, + for: symbolUSR, + fetchSymbolGraph: onDiskDocumentManager.fetchSymbolGraph(at:) + ) + } } return try await documentationManager.renderDocCDocumentation( symbolUSR: symbolUSR, @@ -113,11 +102,12 @@ extension SwiftLanguageService { } 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) + let markupExtensionFileURL = try await catalogIndex.documentationExtension( + for: index.doccSymbolInformation( + ofUSR: symbolUSR, + fetchSymbolGraph: fetchSymbolGraph + ) + ) else { return nil } diff --git a/Sources/SourceKitLSP/Swift/OnDiskDocumentManager.swift b/Sources/SourceKitLSP/Swift/OnDiskDocumentManager.swift new file mode 100644 index 000000000..635ee54d7 --- /dev/null +++ b/Sources/SourceKitLSP/Swift/OnDiskDocumentManager.swift @@ -0,0 +1,156 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +import BuildServerIntegration +import Foundation +import IndexStoreDB +import LanguageServerProtocol +import SKLogging +import SKUtilities +import SwiftExtensions + +/// Caches document snapshots from disk opened in sourcekitd. +/// +/// Used by `textDocument/doccDocumentation` requests to retrieve symbol graphs for files that are not currently +/// open in the editor. This allows for retrieving multiple symbol graphs from the same file without having +/// to re-open and parse the syntax tree every time. +actor OnDiskDocumentManager: Sendable { + private weak var sourceKitLSPServer: SourceKitLSPServer? + private var openSnapshots: [DocumentURI: (snapshot: DocumentSnapshot, patchedCompileCommand: SwiftCompileCommand?)] + + fileprivate init(sourceKitLSPServer: SourceKitLSPServer) { + self.sourceKitLSPServer = sourceKitLSPServer + self.openSnapshots = [:] + } + + /// Retrieve the symbol graph at a particular ``SymbolLocation``. + /// + /// A unique dummy document will be opened in sourcekitd to fulfill the request. + /// + /// - Parameter symbolLocation: The location of the symbol to fetch the symbol graph for. + func fetchSymbolGraph(at symbolLocation: SymbolLocation) async throws -> String? { + let (snapshot, patchedCompileCommand) = try await openDocumentSnapshot(for: symbolLocation.documentUri) + let swiftLanguageService = try await swiftLanguageService(for: symbolLocation.documentUri) + return try await swiftLanguageService.cursorInfo( + snapshot, + compileCommand: patchedCompileCommand, + Range(snapshot.position(of: symbolLocation)), + includeSymbolGraph: true + ).symbolGraph + } + + /// 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. + /// + /// The document will be retained until ``closeAllDocuments()`` is called. This will avoid parsing the same + /// document multiple times if more than one symbol needs to be looked up. + /// + /// - Parameter uri: The ``DocumentURI`` that will be opened. + func openDocumentSnapshot( + for uri: DocumentURI + ) async throws -> (snapshot: DocumentSnapshot, patchedCompileCommand: SwiftCompileCommand?) { + if let cachedSnapshot = openSnapshots[uri] { + return cachedSnapshot + } + let languageService = try await swiftLanguageService(for: uri) + let snapshot = try await languageService.openSnapshotFromDiskOpenedInSourcekitd( + uri: uri, + fallbackSettingsAfterTimeout: false + ) + openSnapshots[uri] = snapshot + return snapshot + } + + /// Closes all document snapshots that were opened by this ``OnDiskDocumentManager``. + func closeAllDocuments() async { + for (snapshot, _) in openSnapshots.values { + await orLog("Close helper document \(snapshot.uri.forLogging)") { + let languageService = try await swiftLanguageService(for: snapshot.uri) + await languageService.closeSnapshotFromDiskOpenedInSourcekitd(snapshot: snapshot) + } + } + openSnapshots = [:] + } + + private func swiftLanguageService(for uri: DocumentURI) async throws -> SwiftLanguageService { + guard let sourceKitLSPServer else { + throw ResponseError.internalError("SourceKit-LSP is shutting down") + } + guard let workspace = await sourceKitLSPServer.workspaceForDocument(uri: uri), + let languageService = await sourceKitLSPServer.languageService(for: uri, .swift, in: workspace), + let swiftLanguageService = languageService as? SwiftLanguageService + else { + throw ResponseError.internalError("Unable to find SwiftLanguageService for \(uri)") + } + return swiftLanguageService + } +} + +extension SourceKitLSPServer { + nonisolated(nonsending) func withOnDiskDocumentManager( + _ body: (OnDiskDocumentManager) async throws -> T + ) async rethrows -> T { + let onDiskDocumentManager = OnDiskDocumentManager(sourceKitLSPServer: self) + do { + let result = try await body(onDiskDocumentManager) + await onDiskDocumentManager.closeAllDocuments() + return result + } catch { + await onDiskDocumentManager.closeAllDocuments() + throw error + } + } +} + +fileprivate extension SwiftLanguageService { + func openSnapshotFromDiskOpenedInSourcekitd( + uri: DocumentURI, + fallbackSettingsAfterTimeout: Bool, + ) async throws -> (snapshot: DocumentSnapshot, patchedCompileCommand: SwiftCompileCommand?) { + 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 + ) + + return (snapshot, patchedCompileCommand) + } + + func closeSnapshotFromDiskOpenedInSourcekitd(snapshot: DocumentSnapshot) async { + await orLog("Close helper document '\(snapshot.uri)'") { + _ = try await send( + sourcekitdRequest: \.editorClose, + self.closeDocumentSourcekitdRequest(uri: snapshot.uri), + snapshot: snapshot + ) + } + } +} diff --git a/Sources/SourceKitLSP/Swift/WithSnapshotFromDiskOpenedInSourcekitd.swift b/Sources/SourceKitLSP/Swift/WithSnapshotFromDiskOpenedInSourcekitd.swift deleted file mode 100644 index 305c7c593..000000000 --- a/Sources/SourceKitLSP/Swift/WithSnapshotFromDiskOpenedInSourcekitd.swift +++ /dev/null @@ -1,69 +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 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( - 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 - 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() - } -}