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 c19ae3313..79b8319f3 100644 --- a/Sources/SourceKitLSP/CMakeLists.txt +++ b/Sources/SourceKitLSP/CMakeLists.txt @@ -68,6 +68,7 @@ target_sources(SourceKitLSP PRIVATE Swift/SwiftCommand.swift Swift/SwiftLanguageService.swift Swift/SwiftTestingScanner.swift + Swift/SymbolGraphCache.swift Swift/SymbolInfo.swift Swift/SyntacticSwiftXCTestScanner.swift Swift/SyntacticTestIndex.swift @@ -76,7 +77,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 5d5f15723..c9b16be82 100644 --- a/Sources/SourceKitLSP/Documentation/DoccDocumentationHandler.swift +++ b/Sources/SourceKitLSP/Documentation/DoccDocumentationHandler.swift @@ -58,53 +58,16 @@ extension DocumentationLanguageService { guard let index = workspace.index(checkedFor: .deletedFiles) else { throw ResponseError.requestFailed(doccDocumentationError: .indexNotAvailable) } + let symbolGraphCache = SymbolGraphCache(sourceKitLSPServer: sourceKitLSPServer) 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 - } - } + fetchSymbolGraph: symbolGraphCache.fetchSymbolGraph(at:) ) 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 { + guard let symbolGraph = try await symbolGraphCache.fetchSymbolGraph(at: symbolOccurrence.location) else { throw ResponseError.internalError("Unable to retrieve symbol graph for \(symbolOccurrence.symbol.name)") } return try await documentationManager.renderDocCDocumentation( diff --git a/Sources/SourceKitLSP/Swift/DoccDocumentation.swift b/Sources/SourceKitLSP/Swift/DoccDocumentation.swift index 8424c25ca..a9b194581 100644 --- a/Sources/SourceKitLSP/Swift/DoccDocumentation.swift +++ b/Sources/SourceKitLSP/Swift/DoccDocumentation.swift @@ -70,25 +70,13 @@ extension SwiftLanguageService { } // 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( + let symbolGraphCache = SymbolGraphCache(sourceKitLSPServer: sourceKitLSPServer) + return 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 - } - } + fetchSymbolGraph: symbolGraphCache.fetchSymbolGraph(at:) ) } return try await documentationManager.renderDocCDocumentation( @@ -113,11 +101,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/SymbolGraphCache.swift b/Sources/SourceKitLSP/Swift/SymbolGraphCache.swift new file mode 100644 index 000000000..73f337a21 --- /dev/null +++ b/Sources/SourceKitLSP/Swift/SymbolGraphCache.swift @@ -0,0 +1,134 @@ +//===----------------------------------------------------------------------===// +// +// 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 BuildSystemIntegration +import Foundation +import IndexStoreDB +import LanguageServerProtocol +import SKLogging +import SKUtilities +import SwiftExtensions + +/// A cache of symbol graphs and their associated snapshots opened in sourcekitd. Any opened documents will be +/// closed when the cache is de-initialized. +/// +/// 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 SymbolGraphCache: Sendable { + private weak var sourceKitLSPServer: SourceKitLSPServer? + private var openSnapshots: [DocumentURI: (snapshot: DocumentSnapshot, patchedCompileCommand: SwiftCompileCommand?)] + + init(sourceKitLSPServer: SourceKitLSPServer) { + self.sourceKitLSPServer = sourceKitLSPServer + self.openSnapshots = [:] + } + + /// 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. Return the symbol graph from sourcekitd. + /// + /// The document will be retained until ``DocCSymbolGraphCache`` is de-initialized. This will avoid parsing the same + /// document multiple times if more than one symbol needs to be looked up. + /// + /// - Parameter symbolLocation: The location of a symbol to find the symbol graph for. + /// - Returns: The symbol graph for this location, if any. + func fetchSymbolGraph(at symbolLocation: SymbolLocation) async throws -> String? { + let swiftLanguageService = try await swiftLanguageService(for: symbolLocation.documentUri) + let (snapshot, patchedCompileCommand) = try await swiftLanguageService.openSnapshotFromDiskOpenedInSourcekitd( + uri: symbolLocation.documentUri, + fallbackSettingsAfterTimeout: false + ) + return try await swiftLanguageService.cursorInfo( + snapshot, + compileCommand: patchedCompileCommand, + Range(snapshot.position(of: symbolLocation)), + includeSymbolGraph: true + ).symbolGraph + } + + 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 + } + + deinit { + guard let sourceKitLSPServer else { + return + } + + let documentsToClose = openSnapshots.values + Task { + for (snapshot, _) in documentsToClose { + guard let workspace = await sourceKitLSPServer.workspaceForDocument(uri: snapshot.uri), + let languageService = await sourceKitLSPServer.languageService(for: snapshot.uri, .swift, in: workspace), + let swiftLanguageService = languageService as? SwiftLanguageService + else { + logger.log("Unable to find SwiftLanguageService to close helper document \(snapshot.uri.forLogging)") + return + } + await swiftLanguageService.closeSnapshotFromDiskOpenedInSourcekitd(snapshot: snapshot) + } + } + } +} + +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 27918784b..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 BuildSystemIntegration -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() - } -}