Files
sourcekit-lsp/Sources/SourceKitLSP/Workspace.swift
2024-09-09 16:30:13 -07:00

284 lines
11 KiB
Swift

//===----------------------------------------------------------------------===//
//
// 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 IndexStoreDB
import LanguageServerProtocol
import SKLogging
import SKOptions
import SKSupport
import SemanticIndex
import SwiftExtensions
import ToolchainRegistry
import struct TSCBasic.AbsolutePath
import struct TSCBasic.RelativePath
/// Same as `??` but allows the right-hand side of the operator to 'await'.
fileprivate func firstNonNil<T>(_ optional: T?, _ defaultValue: @autoclosure () async throws -> T) async rethrows -> T {
if let optional {
return optional
}
return try await defaultValue()
}
fileprivate func firstNonNil<T>(
_ optional: T?,
_ defaultValue: @autoclosure () async throws -> T?
) async rethrows -> T? {
if let optional {
return optional
}
return try await defaultValue()
}
/// Represents the configuration and state of a project or combination of projects being worked on
/// together.
///
/// In LSP, this represents the per-workspace state that is typically only available after the
/// "initialize" request has been made.
///
/// Typically a workspace is contained in a root directory.
package final class Workspace: Sendable {
/// The root directory of the workspace.
package let rootUri: DocumentURI?
/// Tracks dynamically registered server capabilities as well as the client's capabilities.
package let capabilityRegistry: CapabilityRegistry
/// The build system manager to use for documents in this workspace.
package let buildSystemManager: BuildSystemManager
let options: SourceKitLSPOptions
/// The source code index, if available.
///
/// Usually a checked index (retrieved using `index(checkedFor:)`) should be used instead of the unchecked index.
private let _uncheckedIndex: ThreadSafeBox<UncheckedIndex?>
package var uncheckedIndex: UncheckedIndex? {
return _uncheckedIndex.value
}
/// The index that syntactically scans the workspace for tests.
let syntacticTestIndex = SyntacticTestIndex()
/// Documents open in the SourceKitLSPServer. This may include open documents from other workspaces.
private let documentManager: DocumentManager
/// Language service for an open document, if available.
private let documentService: ThreadSafeBox<[DocumentURI: LanguageService]> = ThreadSafeBox(initialValue: [:])
/// The `SemanticIndexManager` that keeps track of whose file's index is up-to-date in the workspace and schedules
/// indexing and preparation tasks for files with out-of-date index.
///
/// `nil` if background indexing is not enabled.
let semanticIndexManager: SemanticIndexManager?
init(
documentManager: DocumentManager,
rootUri: DocumentURI?,
capabilityRegistry: CapabilityRegistry,
toolchainRegistry: ToolchainRegistry,
options: SourceKitLSPOptions,
testHooks: TestHooks,
underlyingBuildSystem: BuiltInBuildSystem?,
index uncheckedIndex: UncheckedIndex?,
indexDelegate: SourceKitIndexDelegate?,
indexTaskScheduler: TaskScheduler<AnyIndexTaskDescription>,
logMessageToIndexLog: @escaping @Sendable (_ taskID: IndexTaskID, _ message: String) -> Void,
indexTasksWereScheduled: @escaping @Sendable (Int) -> Void,
indexProgressStatusDidChange: @escaping @Sendable () -> Void
) async {
self.documentManager = documentManager
self.rootUri = rootUri
self.capabilityRegistry = capabilityRegistry
self.options = options
self._uncheckedIndex = ThreadSafeBox(initialValue: uncheckedIndex)
self.buildSystemManager = await BuildSystemManager(
buildSystem: underlyingBuildSystem,
fallbackBuildSystem: FallbackBuildSystem(options: options.fallbackBuildSystemOrDefault),
mainFilesProvider: uncheckedIndex,
toolchainRegistry: toolchainRegistry
)
if options.backgroundIndexingOrDefault, let uncheckedIndex, await buildSystemManager.supportsPreparation {
self.semanticIndexManager = SemanticIndexManager(
index: uncheckedIndex,
buildSystemManager: buildSystemManager,
updateIndexStoreTimeout: options.indexOrDefault.updateIndexStoreTimeoutOrDefault,
testHooks: testHooks.indexTestHooks,
indexTaskScheduler: indexTaskScheduler,
logMessageToIndexLog: logMessageToIndexLog,
indexTasksWereScheduled: indexTasksWereScheduled,
indexProgressStatusDidChange: indexProgressStatusDidChange
)
} else {
self.semanticIndexManager = nil
}
await indexDelegate?.addMainFileChangedCallback { [weak self] in
await self?.buildSystemManager.mainFilesChanged()
}
await underlyingBuildSystem?.addSourceFilesDidChangeCallback { [weak self] in
guard let self else {
return
}
await self.syntacticTestIndex.listOfTestFilesDidChange(self.buildSystemManager.testFiles())
}
// Trigger an initial population of `syntacticTestIndex`.
await syntacticTestIndex.listOfTestFilesDidChange(buildSystemManager.testFiles())
if let semanticIndexManager {
await semanticIndexManager.scheduleBuildGraphGenerationAndBackgroundIndexAllFiles()
}
}
/// Creates a workspace for a given root `DocumentURI`, inferring the `ExternalWorkspace` if possible.
///
/// - Parameters:
/// - url: The root directory of the workspace, which must be a valid path.
/// - clientCapabilities: The client capabilities provided during server initialization.
/// - toolchainRegistry: The toolchain registry.
convenience init(
documentManager: DocumentManager,
rootUri: DocumentURI,
capabilityRegistry: CapabilityRegistry,
buildSystem: BuiltInBuildSystem?,
toolchainRegistry: ToolchainRegistry,
options: SourceKitLSPOptions,
testHooks: TestHooks,
indexTaskScheduler: TaskScheduler<AnyIndexTaskDescription>,
logMessageToIndexLog: @escaping @Sendable (_ taskID: IndexTaskID, _ message: String) -> Void,
indexTasksWereScheduled: @Sendable @escaping (Int) -> Void,
indexProgressStatusDidChange: @Sendable @escaping () -> Void
) async throws {
var index: IndexStoreDB? = nil
var indexDelegate: SourceKitIndexDelegate? = nil
let indexOptions = options.indexOrDefault
if let storePath = await firstNonNil(
AbsolutePath(validatingOrNil: indexOptions.indexStorePath),
await buildSystem?.indexStorePath
),
let dbPath = await firstNonNil(
AbsolutePath(validatingOrNil: indexOptions.indexDatabasePath),
await buildSystem?.indexDatabasePath
),
let libPath = await toolchainRegistry.default?.libIndexStore
{
do {
let lib = try IndexStoreLibrary(dylibPath: libPath.pathString)
indexDelegate = SourceKitIndexDelegate()
let prefixMappings =
indexOptions.indexPrefixMap?.map { PathPrefixMapping(original: $0.key, replacement: $0.value) } ?? []
index = try IndexStoreDB(
storePath: storePath.pathString,
databasePath: dbPath.pathString,
library: lib,
delegate: indexDelegate,
prefixMappings: prefixMappings.map { PathMapping(original: $0.original, replacement: $0.replacement) }
)
logger.debug("Opened IndexStoreDB at \(dbPath) with store path \(storePath)")
} catch {
logger.error("Failed to open IndexStoreDB: \(error.localizedDescription)")
}
}
await self.init(
documentManager: documentManager,
rootUri: rootUri,
capabilityRegistry: capabilityRegistry,
toolchainRegistry: toolchainRegistry,
options: options,
testHooks: testHooks,
underlyingBuildSystem: buildSystem,
index: UncheckedIndex(index),
indexDelegate: indexDelegate,
indexTaskScheduler: indexTaskScheduler,
logMessageToIndexLog: logMessageToIndexLog,
indexTasksWereScheduled: indexTasksWereScheduled,
indexProgressStatusDidChange: indexProgressStatusDidChange
)
}
package static func forTesting(
toolchainRegistry: ToolchainRegistry,
options: SourceKitLSPOptions,
testHooks: TestHooks,
underlyingBuildSystem: BuiltInBuildSystem,
indexTaskScheduler: TaskScheduler<AnyIndexTaskDescription>
) async -> Workspace {
return await Workspace(
documentManager: DocumentManager(),
rootUri: nil,
capabilityRegistry: CapabilityRegistry(clientCapabilities: ClientCapabilities()),
toolchainRegistry: toolchainRegistry,
options: options,
testHooks: testHooks,
underlyingBuildSystem: underlyingBuildSystem,
index: nil,
indexDelegate: nil,
indexTaskScheduler: indexTaskScheduler,
logMessageToIndexLog: { _, _ in },
indexTasksWereScheduled: { _ in },
indexProgressStatusDidChange: {}
)
}
/// Returns a `CheckedIndex` that verifies that all the returned entries are up-to-date with the given
/// `IndexCheckLevel`.
func index(checkedFor checkLevel: IndexCheckLevel) -> CheckedIndex? {
return _uncheckedIndex.value?.checked(for: checkLevel)
}
/// Write the index to disk.
///
/// After this method is called, the workspace will no longer have an index associated with it. It should only be
/// called when SourceKit-LSP shuts down.
func closeIndex() {
_uncheckedIndex.value = nil
}
package func filesDidChange(_ events: [FileEvent]) async {
await buildSystemManager.filesDidChange(events)
await syntacticTestIndex.filesDidChange(events)
await semanticIndexManager?.filesDidChange(events)
}
func documentService(for uri: DocumentURI) -> LanguageService? {
return documentService.value[uri.primaryFile ?? uri]
}
/// Set a language service for a document uri and returns if none exists already.
/// If a language service already exists for this document, eg. because two requests start creating a language
/// service for a document and race, `newLanguageService` is dropped and the existing language service for the
/// document is returned.
func setDocumentService(for uri: DocumentURI, _ newLanguageService: any LanguageService) -> LanguageService {
return documentService.withLock { service in
if let languageService = service[uri] {
return languageService
}
service[uri] = newLanguageService
return newLanguageService
}
}
}
/// Wrapper around a workspace that isn't being retained.
struct WeakWorkspace {
weak var value: Workspace?
init(_ value: Workspace? = nil) {
self.value = value
}
}