Files
sourcekit-lsp/Sources/SourceKitLSP/MessageHandlingDependencyTracker.swift
2025-12-08 15:16:36 -05:00

266 lines
11 KiB
Swift

//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2024 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
//
//===----------------------------------------------------------------------===//
@_spi(SourceKitLSP) package import LanguageServerProtocol
@_spi(SourceKitLSP) import LanguageServerProtocolExtensions
@_spi(SourceKitLSP) import LanguageServerProtocolTransport
@_spi(SourceKitLSP) import SKLogging
import SwiftExtensions
@_spi(SourceKitLSP) import ToolsProtocolsSwiftExtensions
/// A lightweight way of describing tasks that are created from handling LSP
/// requests or notifications for the purpose of dependency tracking.
package enum MessageHandlingDependencyTracker: QueueBasedMessageHandlerDependencyTracker {
/// A task that changes the global configuration of sourcekit-lsp in any way.
///
/// No other tasks must execute simultaneously with this task since they
/// might be relying on this task to take effect.
case globalConfigurationChange
/// A request that depends on the state of all documents.
///
/// These requests wait for `documentUpdate` tasks for all documents to finish before being executed.
///
/// Requests that only read the semantic index and are not affected by changes to the in-memory file contents should
/// `freestanding` requests.
case workspaceRequest
/// Changes the contents of the document with the given URI.
///
/// Any other updates or requests to this document must wait for the
/// document update to finish before being executed
case documentUpdate(DocumentURI)
/// A request that concerns one document.
///
/// Any updates to this document must be processed before the document
/// request can be handled. Multiple requests to the same document can be
/// handled simultaneously.
case documentRequest(DocumentURI)
/// A request that doesn't have any dependencies other than global
/// configuration changes.
case freestanding
/// Whether this request needs to finish before `other` can start executing.
package func isDependency(of other: MessageHandlingDependencyTracker) -> Bool {
switch (self, other) {
// globalConfigurationChange
case (.globalConfigurationChange, _): return true
case (_, .globalConfigurationChange): return true
// workspaceRequest
case (.workspaceRequest, .workspaceRequest): return false
case (.documentUpdate, .workspaceRequest): return true
case (.workspaceRequest, .documentUpdate): return true
case (.workspaceRequest, .documentRequest): return false
case (.documentRequest, .workspaceRequest): return false
// documentUpdate
case (.documentUpdate(let selfUri), .documentUpdate(let otherUri)):
return selfUri == otherUri
case (.documentUpdate(let selfUri), .documentRequest(let otherUri)):
return selfUri.buildSettingsFile == otherUri.buildSettingsFile
case (.documentRequest(let selfUri), .documentUpdate(let otherUri)):
return selfUri.buildSettingsFile == otherUri.buildSettingsFile
// documentRequest
case (.documentRequest, .documentRequest):
return false
// freestanding
case (.freestanding, _):
return false
case (_, .freestanding):
return false
}
}
package init(_ notification: some NotificationType) {
switch notification {
case is CancelRequestNotification:
self = .freestanding
case is CancelWorkDoneProgressNotification:
self = .freestanding
case is DidChangeActiveDocumentNotification:
// The notification doesn't change behavior in an observable way, so we can treat it as freestanding.
self = .freestanding
case is DidChangeConfigurationNotification:
self = .globalConfigurationChange
case let notification as DidChangeNotebookDocumentNotification:
self = .documentUpdate(notification.notebookDocument.uri)
case let notification as DidChangeTextDocumentNotification:
self = .documentUpdate(notification.textDocument.uri)
case is DidChangeWatchedFilesNotification:
// Technically, the watched files notification can change the response of any other request (eg. because a target
// needs to be re-prepared). But treating it as a `globalConfiguration` inserts a lot of barriers in request
// handling and significantly prevents parallelism. Since many editors batch file change notifications already,
// they might have delayed the file change notification even more, which is equivalent to handling the
// notification a little later inside SourceKit-LSP. Thus, treating it as `freestanding` should be acceptable.
self = .freestanding
case is DidChangeWorkspaceFoldersNotification:
self = .globalConfigurationChange
case let notification as DidCloseNotebookDocumentNotification:
self = .documentUpdate(notification.notebookDocument.uri)
case let notification as DidCloseTextDocumentNotification:
self = .documentUpdate(notification.textDocument.uri)
case is DidCreateFilesNotification:
self = .freestanding
case is DidDeleteFilesNotification:
self = .freestanding
case let notification as DidOpenNotebookDocumentNotification:
self = .documentUpdate(notification.notebookDocument.uri)
case let notification as DidOpenTextDocumentNotification:
self = .documentUpdate(notification.textDocument.uri)
case is DidRenameFilesNotification:
self = .freestanding
case let notification as DidSaveNotebookDocumentNotification:
self = .documentUpdate(notification.notebookDocument.uri)
case let notification as DidSaveTextDocumentNotification:
self = .documentUpdate(notification.textDocument.uri)
case is ExitNotification:
self = .globalConfigurationChange
case is InitializedNotification:
self = .globalConfigurationChange
case is LogMessageNotification:
self = .freestanding
case is LogTraceNotification:
self = .freestanding
case is PublishDiagnosticsNotification:
self = .freestanding
case let notification as ReopenTextDocumentNotification:
self = .documentUpdate(notification.textDocument.uri)
case is SetTraceNotification:
// `$/setTrace` changes a global configuration setting but it doesn't affect the result of any other request. To
// avoid blocking other requests on a `$/setTrace` notification the client might send during launch, we treat it
// as a freestanding message.
// Also, we don't do anything with this notification at the moment, so it doesn't matter.
self = .freestanding
case is ShowMessageNotification:
self = .freestanding
case let notification as WillSaveTextDocumentNotification:
self = .documentUpdate(notification.textDocument.uri)
case is WorkDoneProgress:
self = .freestanding
default:
logger.error(
"""
Unknown notification \(type(of: notification)). Treating as a freestanding notification. \
This might lead to out-of-order request handling
"""
)
self = .freestanding
}
}
package init(_ request: some RequestType) {
switch request {
case is ApplyEditRequest:
self = .freestanding
case is CallHierarchyIncomingCallsRequest:
self = .freestanding
case is CallHierarchyOutgoingCallsRequest:
self = .freestanding
case is CodeActionResolveRequest:
self = .freestanding
case is CodeLensRefreshRequest:
self = .freestanding
case is CodeLensResolveRequest:
self = .freestanding
case is CompletionItemResolveRequest:
self = .freestanding
case is CreateWorkDoneProgressRequest:
self = .freestanding
case is DiagnosticsRefreshRequest:
self = .freestanding
case is DocumentLinkResolveRequest:
self = .freestanding
case let request as ExecuteCommandRequest:
if let uri = request.textDocument?.uri {
self = .documentRequest(uri)
} else {
self = .freestanding
}
case let request as GetReferenceDocumentRequest:
self = .documentRequest(request.uri)
case is InitializeRequest:
self = .globalConfigurationChange
case is InlayHintRefreshRequest:
self = .freestanding
case is InlayHintResolveRequest:
self = .freestanding
case is InlineValueRefreshRequest:
self = .freestanding
case is IsIndexingRequest:
self = .freestanding
case is OutputPathsRequest:
self = .freestanding
case is RenameRequest:
// Rename might touch multiple files. Make it a global configuration change so that edits to all files that might
// be affected have been processed.
self = .globalConfigurationChange
case is RegisterCapabilityRequest:
self = .globalConfigurationChange
case is SetOptionsRequest:
// The request does not modify any global state in an observable way, so we can treat it as a freestanding
// request.
self = .freestanding
case is ShowMessageRequest:
self = .freestanding
case is ShutdownRequest:
self = .globalConfigurationChange
case is SourceKitOptionsRequest:
self = .freestanding
case is SynchronizeRequest:
self = .globalConfigurationChange
case is TriggerReindexRequest:
self = .globalConfigurationChange
case is TypeHierarchySubtypesRequest:
self = .freestanding
case is TypeHierarchySupertypesRequest:
self = .freestanding
case is UnregisterCapabilityRequest:
self = .globalConfigurationChange
case is WillCreateFilesRequest:
self = .freestanding
case is WillDeleteFilesRequest:
self = .freestanding
case is WillRenameFilesRequest:
self = .freestanding
case is WorkspaceDiagnosticsRequest:
self = .freestanding
case is WorkspaceFoldersRequest:
self = .freestanding
case is WorkspaceSemanticTokensRefreshRequest:
self = .freestanding
case is WorkspaceSymbolResolveRequest:
self = .freestanding
case is WorkspaceSymbolsRequest:
self = .freestanding
case is WorkspaceTestsRequest:
self = .workspaceRequest
case is WorkspacePlaygroundsRequest:
self = .workspaceRequest
case let request as any TextDocumentRequest:
self = .documentRequest(request.textDocument.uri)
default:
logger.error(
"""
Unknown request \(type(of: request)). Treating as a freestanding request. \
This might lead to out-of-order request handling
"""
)
self = .freestanding
}
}
}