mirror of
https://github.com/apple/sourcekit-lsp.git
synced 2026-03-02 18:23:24 +01:00
Depends on https://github.com/swiftlang/swift/pull/83378 --- Adds support for the LSP signature help request. > [!NOTE] > As of https://github.com/swiftlang/swift/pull/83378, SourceKitD still doesn't separate parameter documentation from the signature documentation and thus parameters don't have their own separate documentation. This should just work once SourceKitD implements this functionality and we'll only need to modify the tests.
1322 lines
50 KiB
Swift
1322 lines
50 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
//
|
|
// This source file is part of the Swift.org open source project
|
|
//
|
|
// Copyright (c) 2014 - 2020 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 BuildServerProtocol
|
|
import Csourcekitd
|
|
import Dispatch
|
|
import Foundation
|
|
import IndexStoreDB
|
|
package import LanguageServerProtocol
|
|
import LanguageServerProtocolExtensions
|
|
import SKLogging
|
|
package import SKOptions
|
|
import SKUtilities
|
|
import SemanticIndex
|
|
package import SourceKitD
|
|
package import SourceKitLSP
|
|
import SwiftExtensions
|
|
import SwiftParser
|
|
import SwiftParserDiagnostics
|
|
package import SwiftSyntax
|
|
package import ToolchainRegistry
|
|
|
|
#if os(Windows)
|
|
import WinSDK
|
|
#endif
|
|
|
|
fileprivate extension Range {
|
|
/// Checks if this range overlaps with the other range, counting an overlap with an empty range as a valid overlap.
|
|
/// The standard library implementation makes `1..<3.overlaps(2..<2)` return false because the second range is empty and thus the overlap is also empty.
|
|
/// This implementation over overlap considers such an inclusion of an empty range as a valid overlap.
|
|
func overlapsIncludingEmptyRanges(other: Range<Bound>) -> Bool {
|
|
switch (self.isEmpty, other.isEmpty) {
|
|
case (true, true):
|
|
return self.lowerBound == other.lowerBound
|
|
case (true, false):
|
|
return other.contains(self.lowerBound)
|
|
case (false, true):
|
|
return self.contains(other.lowerBound)
|
|
case (false, false):
|
|
return self.overlaps(other)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Explicitly excluded `DocumentURI` schemes.
|
|
private let excludedDocumentURISchemes: [String] = [
|
|
"git",
|
|
"hg",
|
|
]
|
|
|
|
/// Returns true if diagnostics should be emitted for the given document.
|
|
///
|
|
/// Some editors (like Visual Studio Code) use non-file URLs to manage source control diff bases
|
|
/// for the active document, which can lead to duplicate diagnostics in the Problems view.
|
|
/// As a workaround we explicitly exclude those URIs and don't emit diagnostics for them.
|
|
///
|
|
/// Additionally, as of Xcode 11.4, sourcekitd does not properly handle non-file URLs when
|
|
/// the `-working-directory` argument is passed since it incorrectly applies it to the input
|
|
/// argument but not the internal primary file, leading sourcekitd to believe that the input
|
|
/// file is missing.
|
|
private func diagnosticsEnabled(for document: DocumentURI) -> Bool {
|
|
guard let scheme = document.scheme else { return true }
|
|
return !excludedDocumentURISchemes.contains(scheme)
|
|
}
|
|
|
|
/// A swift compiler command derived from a `FileBuildSettingsChange`.
|
|
package struct SwiftCompileCommand: Sendable, Equatable, Hashable {
|
|
|
|
/// The compiler arguments, including working directory. This is required since sourcekitd only
|
|
/// accepts the working directory via the compiler arguments.
|
|
package let compilerArgs: [String]
|
|
|
|
/// Whether the compiler arguments are considered fallback - we withhold diagnostics for
|
|
/// fallback arguments and represent the file state differently.
|
|
package let isFallback: Bool
|
|
|
|
package init(_ settings: FileBuildSettings) {
|
|
let baseArgs = settings.compilerArguments
|
|
// Add working directory arguments if needed.
|
|
if let workingDirectory = settings.workingDirectory, !baseArgs.contains("-working-directory") {
|
|
self.compilerArgs = baseArgs + ["-working-directory", workingDirectory]
|
|
} else {
|
|
self.compilerArgs = baseArgs
|
|
}
|
|
self.isFallback = settings.isFallback
|
|
}
|
|
}
|
|
|
|
package actor SwiftLanguageService: LanguageService, Sendable {
|
|
/// The ``SourceKitLSPServer`` instance that created this `SwiftLanguageService`.
|
|
private(set) weak var sourceKitLSPServer: SourceKitLSPServer?
|
|
|
|
private let sourcekitdPath: URL
|
|
|
|
package let sourcekitd: SourceKitD
|
|
|
|
/// Path to the swift-format executable if it exists in the toolchain.
|
|
let swiftFormat: URL?
|
|
|
|
/// Queue on which notifications from sourcekitd are handled to ensure we are
|
|
/// handling them in-order.
|
|
let sourcekitdNotificationHandlingQueue = AsyncQueue<Serial>()
|
|
|
|
let capabilityRegistry: CapabilityRegistry
|
|
|
|
let hooks: Hooks
|
|
|
|
let options: SourceKitLSPOptions
|
|
|
|
/// Directory where generated Swift interfaces will be stored.
|
|
var generatedInterfacesPath: URL {
|
|
options.generatedFilesAbsolutePath.appendingPathComponent("GeneratedInterfaces")
|
|
}
|
|
|
|
/// Directory where generated Macro expansions will be stored.
|
|
var generatedMacroExpansionsPath: URL {
|
|
options.generatedFilesAbsolutePath.appendingPathComponent("GeneratedMacroExpansions")
|
|
}
|
|
|
|
/// For each edited document, the last task that was triggered to send a `PublishDiagnosticsNotification`.
|
|
///
|
|
/// This is used to cancel previous publish diagnostics tasks if an edit is made to a document.
|
|
///
|
|
/// - Note: We only clear entries from the dictionary when a document is closed. The task that the document maps to
|
|
/// might have finished. This isn't an issue since the tasks do not retain `self`.
|
|
private var inFlightPublishDiagnosticsTasks: [DocumentURI: Task<Void, Never>] = [:]
|
|
|
|
let syntaxTreeManager = SyntaxTreeManager()
|
|
|
|
/// The `semanticIndexManager` of the workspace this language service was created for.
|
|
private let semanticIndexManager: SemanticIndexManager?
|
|
|
|
nonisolated var keys: sourcekitd_api_keys { return sourcekitd.keys }
|
|
nonisolated var requests: sourcekitd_api_requests { return sourcekitd.requests }
|
|
nonisolated var values: sourcekitd_api_values { return sourcekitd.values }
|
|
|
|
/// - Important: Use `setState` to change the state, which notifies the state change handlers
|
|
private var state: LanguageServerState
|
|
|
|
private var stateChangeHandlers: [(_ oldState: LanguageServerState, _ newState: LanguageServerState) -> Void] = []
|
|
|
|
private let diagnosticReportManager: DiagnosticReportManager
|
|
|
|
/// - Note: Implicitly unwrapped optional so we can pass a reference of `self` to `MacroExpansionManager`.
|
|
private(set) var macroExpansionManager: MacroExpansionManager! {
|
|
willSet {
|
|
// Must only be set once.
|
|
precondition(macroExpansionManager == nil)
|
|
precondition(newValue != nil)
|
|
}
|
|
}
|
|
|
|
/// - Note: Implicitly unwrapped optional so we can pass a reference of `self` to `MacroExpansionManager`.
|
|
private(set) var generatedInterfaceManager: GeneratedInterfaceManager! {
|
|
willSet {
|
|
// Must only be set once.
|
|
precondition(generatedInterfaceManager == nil)
|
|
precondition(newValue != nil)
|
|
}
|
|
}
|
|
|
|
var documentManager: DocumentManager {
|
|
get throws {
|
|
guard let sourceKitLSPServer else {
|
|
throw ResponseError.unknown("Connection to the editor closed")
|
|
}
|
|
return sourceKitLSPServer.documentManager
|
|
}
|
|
}
|
|
|
|
/// The build settings that were used to open the given files.
|
|
///
|
|
/// - Note: Not all documents open in `SwiftLanguageService` are necessarily in this dictionary because files where
|
|
/// `buildSettings(for:)` returns `nil` are not included.
|
|
private var buildSettingsForOpenFiles: [DocumentURI: SwiftCompileCommand] = [:]
|
|
|
|
/// Calling `scheduleCall` on `refreshDiagnosticsAndSemanticTokensDebouncer` schedules a `DiagnosticsRefreshRequest`
|
|
/// and `WorkspaceSemanticTokensRefreshRequest` to be sent to to the client.
|
|
///
|
|
/// We debounce these calls because the `DiagnosticsRefreshRequest` is a workspace-wide request. If we discover that
|
|
/// the client should update diagnostics for file A and then discover that it should also update diagnostics for file
|
|
/// B, we don't want to send two `DiagnosticsRefreshRequest`s. Instead, the two should be unified into a single
|
|
/// request.
|
|
private let refreshDiagnosticsAndSemanticTokensDebouncer: Debouncer<Void>
|
|
|
|
/// Creates a language server for the given client using the sourcekitd dylib specified in `toolchain`.
|
|
/// `reopenDocuments` is a closure that will be called if sourcekitd crashes and the `SwiftLanguageService` asks its
|
|
/// parent server to reopen all of its documents.
|
|
/// Returns `nil` if `sourcekitd` couldn't be found.
|
|
package init(
|
|
sourceKitLSPServer: SourceKitLSPServer,
|
|
toolchain: Toolchain,
|
|
options: SourceKitLSPOptions,
|
|
hooks: Hooks,
|
|
workspace: Workspace
|
|
) async throws {
|
|
guard let sourcekitd = toolchain.sourcekitd else {
|
|
throw ResponseError.unknown(
|
|
"Cannot create SwiftLanguage service because \(toolchain.identifier) does not contain sourcekitd"
|
|
)
|
|
}
|
|
self.sourcekitdPath = sourcekitd
|
|
self.sourceKitLSPServer = sourceKitLSPServer
|
|
self.swiftFormat = toolchain.swiftFormat
|
|
let pluginPaths: PluginPaths?
|
|
if let clientPlugin = options.sourcekitdOrDefault.clientPlugin,
|
|
let servicePlugin = options.sourcekitdOrDefault.servicePlugin
|
|
{
|
|
pluginPaths = PluginPaths(
|
|
clientPlugin: URL(fileURLWithPath: clientPlugin),
|
|
servicePlugin: URL(fileURLWithPath: servicePlugin)
|
|
)
|
|
} else if let clientPlugin = toolchain.sourceKitClientPlugin, let servicePlugin = toolchain.sourceKitServicePlugin {
|
|
pluginPaths = PluginPaths(clientPlugin: clientPlugin, servicePlugin: servicePlugin)
|
|
} else {
|
|
logger.fault("Failed to find SourceKit plugin for toolchain at \(toolchain.path.path)")
|
|
pluginPaths = nil
|
|
}
|
|
self.sourcekitd = try await SourceKitD.getOrCreate(dylibPath: sourcekitd, pluginPaths: pluginPaths)
|
|
self.capabilityRegistry = workspace.capabilityRegistry
|
|
self.semanticIndexManager = workspace.semanticIndexManager
|
|
self.hooks = hooks
|
|
self.state = .connected
|
|
self.options = options
|
|
|
|
// The debounce duration of 500ms was chosen arbitrarily without scientific research.
|
|
self.refreshDiagnosticsAndSemanticTokensDebouncer = Debouncer(debounceDuration: .milliseconds(500)) {
|
|
[weak sourceKitLSPServer] in
|
|
guard let sourceKitLSPServer else {
|
|
logger.fault(
|
|
"Not sending diagnostic and semantic token refresh request to client because sourceKitLSPServer has been deallocated"
|
|
)
|
|
return
|
|
}
|
|
let clientCapabilities = await sourceKitLSPServer.capabilityRegistry?.clientCapabilities
|
|
if clientCapabilities?.workspace?.diagnostics?.refreshSupport ?? false {
|
|
_ = await orLog("Sending DiagnosticRefreshRequest to client after document dependencies updated") {
|
|
try await sourceKitLSPServer.sendRequestToClient(DiagnosticsRefreshRequest())
|
|
}
|
|
} else {
|
|
logger.debug("Not sending DiagnosticRefreshRequest because the client doesn't support it")
|
|
}
|
|
|
|
if clientCapabilities?.workspace?.semanticTokens?.refreshSupport ?? false {
|
|
_ = await orLog("Sending WorkspaceSemanticTokensRefreshRequest to client after document dependencies updated") {
|
|
try await sourceKitLSPServer.sendRequestToClient(WorkspaceSemanticTokensRefreshRequest())
|
|
}
|
|
} else {
|
|
logger.debug("Not sending WorkspaceSemanticTokensRefreshRequest because the client doesn't support it")
|
|
}
|
|
}
|
|
|
|
self.diagnosticReportManager = DiagnosticReportManager(
|
|
sourcekitd: self.sourcekitd,
|
|
options: options,
|
|
syntaxTreeManager: syntaxTreeManager,
|
|
documentManager: sourceKitLSPServer.documentManager,
|
|
clientHasDiagnosticsCodeDescriptionSupport: await capabilityRegistry.clientHasDiagnosticsCodeDescriptionSupport
|
|
)
|
|
|
|
self.macroExpansionManager = MacroExpansionManager(swiftLanguageService: self)
|
|
self.generatedInterfaceManager = GeneratedInterfaceManager(swiftLanguageService: self)
|
|
|
|
// Create sub-directories for each type of generated file
|
|
try FileManager.default.createDirectory(at: generatedInterfacesPath, withIntermediateDirectories: true)
|
|
try FileManager.default.createDirectory(at: generatedMacroExpansionsPath, withIntermediateDirectories: true)
|
|
}
|
|
|
|
/// - Important: For testing only
|
|
package func setReusedNodeCallback(_ callback: (@Sendable (_ node: Syntax) -> Void)?) async {
|
|
await self.syntaxTreeManager.setReusedNodeCallback(callback)
|
|
}
|
|
|
|
/// Returns the latest snapshot of the given URI, generating the snapshot in case the URI is a reference document.
|
|
func latestSnapshot(for uri: DocumentURI) async throws -> DocumentSnapshot {
|
|
switch try? ReferenceDocumentURL(from: uri) {
|
|
case .macroExpansion(let data):
|
|
let content = try await self.macroExpansionManager.macroExpansion(for: data)
|
|
return DocumentSnapshot(uri: uri, language: .swift, version: 0, lineTable: LineTable(content))
|
|
case .generatedInterface(let data):
|
|
return try await self.generatedInterfaceManager.snapshot(of: data)
|
|
case nil:
|
|
return try documentManager.latestSnapshot(uri)
|
|
}
|
|
}
|
|
|
|
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: document.buildSettingsFile) else {
|
|
return nil
|
|
}
|
|
let settings = await workspace.buildServerManager.buildSettingsInferredFromMainFile(
|
|
for: document.buildSettingsFile,
|
|
language: .swift,
|
|
fallbackAfterTimeout: fallbackAfterTimeout
|
|
)
|
|
|
|
guard let settings else {
|
|
return nil
|
|
}
|
|
return SwiftCompileCommand(settings)
|
|
}
|
|
|
|
func send(
|
|
sourcekitdRequest requestUid: KeyPath<sourcekitd_api_requests, sourcekitd_api_uid_t> & Sendable,
|
|
_ request: SKDRequestDictionary,
|
|
snapshot: DocumentSnapshot?
|
|
) async throws -> SKDResponseDictionary {
|
|
try await sourcekitd.send(
|
|
requestUid,
|
|
request,
|
|
timeout: options.sourcekitdRequestTimeoutOrDefault,
|
|
restartTimeout: options.semanticServiceRestartTimeoutOrDefault,
|
|
documentUrl: snapshot?.uri.arbitrarySchemeURL,
|
|
fileContents: snapshot?.text
|
|
)
|
|
}
|
|
|
|
package nonisolated func canHandle(workspace: Workspace, toolchain: Toolchain) -> Bool {
|
|
return self.sourcekitdPath == toolchain.sourcekitd
|
|
}
|
|
|
|
private func setState(_ newState: LanguageServerState) async {
|
|
let oldState = state
|
|
state = newState
|
|
for handler in stateChangeHandlers {
|
|
handler(oldState, newState)
|
|
}
|
|
|
|
guard let sourceKitLSPServer else {
|
|
return
|
|
}
|
|
switch (oldState, newState) {
|
|
case (.connected, .connectionInterrupted), (.connected, .semanticFunctionalityDisabled):
|
|
await sourceKitLSPServer.sourcekitdCrashedWorkDoneProgress.start()
|
|
case (.connectionInterrupted, .connected), (.semanticFunctionalityDisabled, .connected):
|
|
await sourceKitLSPServer.sourcekitdCrashedWorkDoneProgress.end()
|
|
// We can provide diagnostics again now. Send a diagnostic refresh request to prompt the editor to reload
|
|
// diagnostics.
|
|
await refreshDiagnosticsAndSemanticTokensDebouncer.scheduleCall()
|
|
case (.connected, .connected),
|
|
(.connectionInterrupted, .connectionInterrupted),
|
|
(.connectionInterrupted, .semanticFunctionalityDisabled),
|
|
(.semanticFunctionalityDisabled, .connectionInterrupted),
|
|
(.semanticFunctionalityDisabled, .semanticFunctionalityDisabled):
|
|
break
|
|
}
|
|
}
|
|
|
|
package func addStateChangeHandler(
|
|
handler: @Sendable @escaping (_ oldState: LanguageServerState, _ newState: LanguageServerState) -> Void
|
|
) {
|
|
self.stateChangeHandlers.append(handler)
|
|
}
|
|
}
|
|
|
|
extension SwiftLanguageService {
|
|
|
|
package func initialize(_ initialize: InitializeRequest) async throws -> InitializeResult {
|
|
await sourcekitd.addNotificationHandler(self)
|
|
|
|
return InitializeResult(
|
|
capabilities: ServerCapabilities(
|
|
textDocumentSync: .options(
|
|
TextDocumentSyncOptions(
|
|
openClose: true,
|
|
change: .incremental
|
|
)
|
|
),
|
|
hoverProvider: .bool(true),
|
|
completionProvider: CompletionOptions(
|
|
resolveProvider: true,
|
|
triggerCharacters: [".", "("]
|
|
),
|
|
signatureHelpProvider: SignatureHelpOptions(
|
|
triggerCharacters: ["(", "["],
|
|
retriggerCharacters: [",", ":"]
|
|
),
|
|
definitionProvider: nil,
|
|
implementationProvider: .bool(true),
|
|
referencesProvider: nil,
|
|
documentHighlightProvider: .bool(true),
|
|
documentSymbolProvider: .bool(true),
|
|
codeActionProvider: .value(
|
|
CodeActionServerCapabilities(
|
|
clientCapabilities: initialize.capabilities.textDocument?.codeAction,
|
|
codeActionOptions: CodeActionOptions(codeActionKinds: [.quickFix, .refactor]),
|
|
supportsCodeActions: true
|
|
)
|
|
),
|
|
codeLensProvider: CodeLensOptions(),
|
|
colorProvider: .bool(true),
|
|
foldingRangeProvider: .bool(true),
|
|
executeCommandProvider: ExecuteCommandOptions(
|
|
commands: Self.builtInCommands
|
|
),
|
|
semanticTokensProvider: SemanticTokensOptions(
|
|
legend: SemanticTokensLegend.sourceKitLSPLegend,
|
|
range: .bool(true),
|
|
full: .bool(true)
|
|
),
|
|
inlayHintProvider: .value(InlayHintOptions(resolveProvider: false)),
|
|
diagnosticProvider: DiagnosticOptions(
|
|
interFileDependencies: true,
|
|
workspaceDiagnostics: false
|
|
)
|
|
)
|
|
)
|
|
}
|
|
|
|
package func shutdown() async {
|
|
await self.sourcekitd.removeNotificationHandler(self)
|
|
}
|
|
|
|
package func canonicalDeclarationPosition(of position: Position, in uri: DocumentURI) async -> Position? {
|
|
guard let snapshot = try? documentManager.latestSnapshot(uri) else {
|
|
return nil
|
|
}
|
|
let syntaxTree = await syntaxTreeManager.syntaxTree(for: snapshot)
|
|
let decl = syntaxTree.token(at: snapshot.absolutePosition(of: position))?.findParentOfSelf(
|
|
ofType: DeclSyntax.self,
|
|
stoppingIf: { $0.is(CodeBlockSyntax.self) || $0.is(MemberBlockSyntax.self) }
|
|
)
|
|
guard let decl else {
|
|
return nil
|
|
}
|
|
return snapshot.position(of: decl.positionAfterSkippingLeadingTrivia)
|
|
}
|
|
|
|
/// Tell sourcekitd to crash itself. For testing purposes only.
|
|
package func crash() async {
|
|
_ = try? await send(sourcekitdRequest: \.crashWithExit, sourcekitd.dictionary([:]), snapshot: nil)
|
|
}
|
|
|
|
// MARK: - Build Server Integration
|
|
|
|
package func reopenDocument(_ notification: ReopenTextDocumentNotification) async {
|
|
switch try? ReferenceDocumentURL(from: notification.textDocument.uri) {
|
|
case .macroExpansion, .generatedInterface:
|
|
// Macro expansions and generated interfaces don't have document dependencies or build settings associated with
|
|
// their URI. We should thus not not receive any `ReopenDocument` notifications for them.
|
|
logger.fault("Unexpectedly received reopen document notification for reference document")
|
|
case nil:
|
|
let snapshot = orLog("Getting snapshot to re-open document") {
|
|
try documentManager.latestSnapshot(notification.textDocument.uri)
|
|
}
|
|
guard let snapshot else {
|
|
return
|
|
}
|
|
cancelInFlightPublishDiagnosticsTask(for: snapshot.uri)
|
|
await diagnosticReportManager.removeItemsFromCache(with: snapshot.uri)
|
|
|
|
let closeReq = closeDocumentSourcekitdRequest(uri: snapshot.uri)
|
|
_ = await orLog("Closing document to re-open it") {
|
|
try await self.send(sourcekitdRequest: \.editorClose, closeReq, snapshot: nil)
|
|
}
|
|
|
|
let buildSettings = await compileCommand(for: snapshot.uri, fallbackAfterTimeout: true)
|
|
let openReq = openDocumentSourcekitdRequest(
|
|
snapshot: snapshot,
|
|
compileCommand: buildSettings
|
|
)
|
|
self.buildSettingsForOpenFiles[snapshot.uri] = buildSettings
|
|
_ = await orLog("Re-opening document") {
|
|
try await self.send(sourcekitdRequest: \.editorOpen, openReq, snapshot: snapshot)
|
|
}
|
|
|
|
if await capabilityRegistry.clientSupportsPullDiagnostics(for: .swift) {
|
|
await self.refreshDiagnosticsAndSemanticTokensDebouncer.scheduleCall()
|
|
} else {
|
|
await publishDiagnosticsIfNeeded(for: snapshot.uri)
|
|
}
|
|
}
|
|
}
|
|
|
|
package func documentUpdatedBuildSettings(_ uri: DocumentURI) async {
|
|
guard (try? documentManager.openDocuments.contains(uri)) ?? false else {
|
|
return
|
|
}
|
|
let newBuildSettings = await self.compileCommand(for: uri, fallbackAfterTimeout: false)
|
|
if newBuildSettings != buildSettingsForOpenFiles[uri] {
|
|
// Close and re-open the document internally to inform sourcekitd to update the compile command. At the moment
|
|
// there's no better way to do this.
|
|
// Schedule the document re-open in the SourceKit-LSP server. This ensures that the re-open happens exclusively with
|
|
// no other request running at the same time.
|
|
sourceKitLSPServer?.handle(ReopenTextDocumentNotification(textDocument: TextDocumentIdentifier(uri)))
|
|
}
|
|
}
|
|
|
|
package func documentDependenciesUpdated(_ uris: Set<DocumentURI>) async {
|
|
let uris = uris.filter { (try? documentManager.openDocuments.contains($0)) ?? false }
|
|
guard !uris.isEmpty else {
|
|
return
|
|
}
|
|
|
|
await orLog("Sending dependencyUpdated request to sourcekitd") {
|
|
_ = try await self.send(sourcekitdRequest: \.dependencyUpdated, sourcekitd.dictionary([:]), snapshot: nil)
|
|
}
|
|
// Even after sending the `dependencyUpdated` request to sourcekitd, the code completion session has state from
|
|
// before the AST update. Close it and open a new code completion session on the next completion request.
|
|
CodeCompletionSession.close(sourcekitd: sourcekitd, uris: uris)
|
|
|
|
for uri in uris {
|
|
await macroExpansionManager.purge(primaryFile: uri)
|
|
sourceKitLSPServer?.handle(ReopenTextDocumentNotification(textDocument: TextDocumentIdentifier(uri)))
|
|
}
|
|
}
|
|
|
|
// MARK: - Text synchronization
|
|
|
|
func openDocumentSourcekitdRequest(
|
|
snapshot: DocumentSnapshot,
|
|
compileCommand: SwiftCompileCommand?
|
|
) -> SKDRequestDictionary {
|
|
return sourcekitd.dictionary([
|
|
keys.name: snapshot.uri.pseudoPath,
|
|
keys.sourceText: snapshot.text,
|
|
keys.enableSyntaxMap: 0,
|
|
keys.enableStructure: 0,
|
|
keys.enableDiagnostics: 0,
|
|
keys.syntacticOnly: 1,
|
|
keys.compilerArgs: compileCommand?.compilerArgs as [SKDRequestValue]?,
|
|
])
|
|
}
|
|
|
|
func closeDocumentSourcekitdRequest(uri: DocumentURI) -> SKDRequestDictionary {
|
|
return sourcekitd.dictionary([
|
|
keys.name: uri.pseudoPath,
|
|
keys.cancelBuilds: 0,
|
|
])
|
|
}
|
|
|
|
package func openDocument(_ notification: DidOpenTextDocumentNotification, snapshot: DocumentSnapshot) async {
|
|
switch try? ReferenceDocumentURL(from: notification.textDocument.uri) {
|
|
case .macroExpansion:
|
|
break
|
|
case .generatedInterface(let data):
|
|
await orLog("Opening generated interface") {
|
|
try await generatedInterfaceManager.open(document: data)
|
|
}
|
|
case nil:
|
|
cancelInFlightPublishDiagnosticsTask(for: notification.textDocument.uri)
|
|
await diagnosticReportManager.removeItemsFromCache(with: notification.textDocument.uri)
|
|
|
|
let buildSettings = await self.compileCommand(for: snapshot.uri, fallbackAfterTimeout: true)
|
|
buildSettingsForOpenFiles[snapshot.uri] = buildSettings
|
|
|
|
let req = openDocumentSourcekitdRequest(snapshot: snapshot, compileCommand: buildSettings)
|
|
await orLog("Opening sourcekitd document") {
|
|
_ = try await self.send(sourcekitdRequest: \.editorOpen, req, snapshot: snapshot)
|
|
}
|
|
await publishDiagnosticsIfNeeded(for: notification.textDocument.uri)
|
|
}
|
|
}
|
|
|
|
package func closeDocument(_ notification: DidCloseTextDocumentNotification) async {
|
|
cancelInFlightPublishDiagnosticsTask(for: notification.textDocument.uri)
|
|
inFlightPublishDiagnosticsTasks[notification.textDocument.uri] = nil
|
|
await diagnosticReportManager.removeItemsFromCache(with: notification.textDocument.uri)
|
|
buildSettingsForOpenFiles[notification.textDocument.uri] = nil
|
|
await syntaxTreeManager.clearSyntaxTrees(for: notification.textDocument.uri)
|
|
switch try? ReferenceDocumentURL(from: notification.textDocument.uri) {
|
|
case .macroExpansion:
|
|
break
|
|
case .generatedInterface(let data):
|
|
await generatedInterfaceManager.close(document: data)
|
|
case nil:
|
|
let req = closeDocumentSourcekitdRequest(uri: notification.textDocument.uri)
|
|
await orLog("Closing sourcekitd document") {
|
|
_ = try await self.send(sourcekitdRequest: \.editorClose, req, snapshot: nil)
|
|
}
|
|
}
|
|
}
|
|
|
|
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] {
|
|
inFlightTask.cancel()
|
|
}
|
|
}
|
|
|
|
/// If the client doesn't support pull diagnostics, compute diagnostics for the latest version of the given document
|
|
/// and send a `PublishDiagnosticsNotification` to the client for it.
|
|
private func publishDiagnosticsIfNeeded(for document: DocumentURI) async {
|
|
await withLoggingScope("publish-diagnostics") {
|
|
await publishDiagnosticsIfNeededImpl(for: document)
|
|
}
|
|
}
|
|
|
|
private func publishDiagnosticsIfNeededImpl(for document: DocumentURI) async {
|
|
guard await !capabilityRegistry.clientSupportsPullDiagnostics(for: .swift) else {
|
|
return
|
|
}
|
|
guard diagnosticsEnabled(for: document) else {
|
|
return
|
|
}
|
|
cancelInFlightPublishDiagnosticsTask(for: document)
|
|
inFlightPublishDiagnosticsTasks[document] = Task(priority: .medium) { [weak self] in
|
|
guard let self, let sourceKitLSPServer = await self.sourceKitLSPServer else {
|
|
logger.fault("Cannot produce PublishDiagnosticsNotification because sourceKitLSPServer was deallocated")
|
|
return
|
|
}
|
|
do {
|
|
// Sleep for a little bit until triggering the diagnostic generation. This effectively de-bounces diagnostic
|
|
// generation since any later edit will cancel the previous in-flight task, which will thus never go on to send
|
|
// the `DocumentDiagnosticsRequest`.
|
|
try await Task.sleep(for: sourceKitLSPServer.options.swiftPublishDiagnosticsDebounceDurationOrDefault)
|
|
} catch {
|
|
return
|
|
}
|
|
do {
|
|
let snapshot = try await self.latestSnapshot(for: document)
|
|
let buildSettings = await self.compileCommand(for: document, fallbackAfterTimeout: false)
|
|
let diagnosticReport = try await self.diagnosticReportManager.diagnosticReport(
|
|
for: snapshot,
|
|
buildSettings: buildSettings
|
|
)
|
|
let latestSnapshotID = try? await self.latestSnapshot(for: snapshot.uri).id
|
|
if latestSnapshotID != snapshot.id {
|
|
// Check that the document wasn't modified while we were getting diagnostics. This could happen because we are
|
|
// calling `publishDiagnosticsIfNeeded` outside of `messageHandlingQueue` and thus a concurrent edit is
|
|
// possible while we are waiting for the sourcekitd request to return a result.
|
|
logger.log(
|
|
"""
|
|
Document was modified while loading diagnostics. \
|
|
Loaded diagnostics for \(snapshot.id.version, privacy: .public), \
|
|
latest snapshot is \((latestSnapshotID?.version).map(String.init) ?? "<nil>", privacy: .public)
|
|
"""
|
|
)
|
|
throw CancellationError()
|
|
}
|
|
|
|
sourceKitLSPServer.sendNotificationToClient(
|
|
PublishDiagnosticsNotification(
|
|
uri: document,
|
|
diagnostics: diagnosticReport.items
|
|
)
|
|
)
|
|
} catch is CancellationError {
|
|
} catch {
|
|
logger.fault(
|
|
"""
|
|
Failed to get diagnostics
|
|
\(error.forLogging)
|
|
"""
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
package func changeDocument(
|
|
_ notification: DidChangeTextDocumentNotification,
|
|
preEditSnapshot: DocumentSnapshot,
|
|
postEditSnapshot: DocumentSnapshot,
|
|
edits: [SourceEdit]
|
|
) async {
|
|
cancelInFlightPublishDiagnosticsTask(for: notification.textDocument.uri)
|
|
|
|
let keys = self.keys
|
|
struct Edit {
|
|
let offset: Int
|
|
let length: Int
|
|
let replacement: String
|
|
}
|
|
|
|
for edit in edits {
|
|
let req = sourcekitd.dictionary([
|
|
keys.name: notification.textDocument.uri.pseudoPath,
|
|
keys.enableSyntaxMap: 0,
|
|
keys.enableStructure: 0,
|
|
keys.enableDiagnostics: 0,
|
|
keys.syntacticOnly: 1,
|
|
keys.offset: edit.range.lowerBound.utf8Offset,
|
|
keys.length: edit.range.length.utf8Length,
|
|
keys.sourceText: edit.replacement,
|
|
])
|
|
do {
|
|
_ = try await self.send(sourcekitdRequest: \.editorReplaceText, req, snapshot: nil)
|
|
} catch {
|
|
logger.fault(
|
|
"""
|
|
Failed to replace \(edit.range.lowerBound.utf8Offset):\(edit.range.upperBound.utf8Offset) by \
|
|
'\(edit.replacement)' in sourcekitd
|
|
"""
|
|
)
|
|
}
|
|
}
|
|
|
|
let concurrentEdits = ConcurrentEdits(
|
|
fromSequential: edits
|
|
)
|
|
await syntaxTreeManager.registerEdit(
|
|
preEditSnapshot: preEditSnapshot,
|
|
postEditSnapshot: postEditSnapshot,
|
|
edits: concurrentEdits
|
|
)
|
|
|
|
await publishDiagnosticsIfNeeded(for: notification.textDocument.uri)
|
|
}
|
|
|
|
// MARK: - Language features
|
|
|
|
package func definition(_ request: DefinitionRequest) async throws -> LocationsOrLocationLinksResponse? {
|
|
throw ResponseError.unknown("unsupported method")
|
|
}
|
|
|
|
package func declaration(_ request: DeclarationRequest) async throws -> LocationsOrLocationLinksResponse? {
|
|
throw ResponseError.unknown("unsupported method")
|
|
}
|
|
|
|
package func hover(_ req: HoverRequest) async throws -> HoverResponse? {
|
|
let uri = req.textDocument.uri
|
|
let position = req.position
|
|
let cursorInfoResults = try await cursorInfo(uri, position..<position, fallbackSettingsAfterTimeout: false)
|
|
.cursorInfo
|
|
|
|
let symbolDocumentations = cursorInfoResults.compactMap { (cursorInfo) -> String? in
|
|
if let documentation = cursorInfo.documentation {
|
|
var result = ""
|
|
if let annotatedDeclaration = cursorInfo.annotatedDeclaration {
|
|
let markdownDecl =
|
|
orLog("Convert XML declaration to Markdown") {
|
|
try xmlDocumentationToMarkdown(annotatedDeclaration)
|
|
} ?? annotatedDeclaration
|
|
result += "\(markdownDecl)\n"
|
|
}
|
|
result += documentation
|
|
return result
|
|
} else if let annotated: String = cursorInfo.annotatedDeclaration {
|
|
return """
|
|
\(orLog("Convert XML to Markdown") { try xmlDocumentationToMarkdown(annotated) } ?? annotated)
|
|
"""
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
if symbolDocumentations.isEmpty {
|
|
return nil
|
|
}
|
|
|
|
let joinedDocumentation: String
|
|
if let only = symbolDocumentations.only {
|
|
joinedDocumentation = only
|
|
} else {
|
|
let documentationsWithSpacing = symbolDocumentations.enumerated().map { index, documentation in
|
|
// Work around a bug in VS Code that displays a code block after a horizontal ruler without any spacing
|
|
// (the pixels of the code block literally touch the ruler) by adding an empty line into the code block.
|
|
// Only do this for subsequent results since only those are preceeded by a ruler.
|
|
let prefix = "```swift\n"
|
|
if index != 0 && documentation.starts(with: prefix) {
|
|
return prefix + "\n" + documentation.dropFirst(prefix.count)
|
|
} else {
|
|
return documentation
|
|
}
|
|
}
|
|
joinedDocumentation = """
|
|
## Multiple results
|
|
|
|
\(documentationsWithSpacing.joined(separator: "\n\n---\n\n"))
|
|
"""
|
|
}
|
|
|
|
var tokenRange: Range<Position>?
|
|
|
|
if let snapshot = try? await latestSnapshot(for: uri) {
|
|
let tree = await syntaxTreeManager.syntaxTree(for: snapshot)
|
|
if let token = tree.token(at: snapshot.absolutePosition(of: position)) {
|
|
tokenRange = snapshot.absolutePositionRange(of: token.trimmedRange)
|
|
}
|
|
}
|
|
|
|
return HoverResponse(
|
|
contents: .markupContent(MarkupContent(kind: .markdown, value: joinedDocumentation)),
|
|
range: tokenRange
|
|
)
|
|
}
|
|
|
|
package func documentColor(_ req: DocumentColorRequest) async throws -> [ColorInformation] {
|
|
let snapshot = try self.documentManager.latestSnapshot(req.textDocument.uri)
|
|
|
|
let syntaxTree = await syntaxTreeManager.syntaxTree(for: snapshot)
|
|
|
|
class ColorLiteralFinder: SyntaxVisitor {
|
|
let snapshot: DocumentSnapshot
|
|
var result: [ColorInformation] = []
|
|
|
|
init(snapshot: DocumentSnapshot) {
|
|
self.snapshot = snapshot
|
|
super.init(viewMode: .sourceAccurate)
|
|
}
|
|
|
|
override func visit(_ node: MacroExpansionExprSyntax) -> SyntaxVisitorContinueKind {
|
|
guard node.macroName.text == "colorLiteral" else {
|
|
return .visitChildren
|
|
}
|
|
func extractArgument(_ argumentName: String, from arguments: LabeledExprListSyntax) -> Double? {
|
|
for argument in arguments {
|
|
if argument.label?.text == argumentName {
|
|
if let integer = argument.expression.as(IntegerLiteralExprSyntax.self) {
|
|
return Double(integer.literal.text)
|
|
} else if let integer = argument.expression.as(FloatLiteralExprSyntax.self) {
|
|
return Double(integer.literal.text)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
guard let red = extractArgument("red", from: node.arguments),
|
|
let green = extractArgument("green", from: node.arguments),
|
|
let blue = extractArgument("blue", from: node.arguments),
|
|
let alpha = extractArgument("alpha", from: node.arguments)
|
|
else {
|
|
return .skipChildren
|
|
}
|
|
|
|
result.append(
|
|
ColorInformation(
|
|
range: snapshot.absolutePositionRange(of: node.position..<node.endPosition),
|
|
color: Color(red: red, green: green, blue: blue, alpha: alpha)
|
|
)
|
|
)
|
|
|
|
return .skipChildren
|
|
}
|
|
}
|
|
|
|
try Task.checkCancellation()
|
|
|
|
let colorLiteralFinder = ColorLiteralFinder(snapshot: snapshot)
|
|
colorLiteralFinder.walk(syntaxTree)
|
|
return colorLiteralFinder.result
|
|
}
|
|
|
|
package func colorPresentation(_ req: ColorPresentationRequest) async throws -> [ColorPresentation] {
|
|
let color = req.color
|
|
// Empty string as a label breaks VSCode color picker
|
|
let label = "Color Literal"
|
|
let newText = "#colorLiteral(red: \(color.red), green: \(color.green), blue: \(color.blue), alpha: \(color.alpha))"
|
|
let textEdit = TextEdit(range: req.range, newText: newText)
|
|
let presentation = ColorPresentation(label: label, textEdit: textEdit, additionalTextEdits: nil)
|
|
return [presentation]
|
|
}
|
|
|
|
package func documentSymbolHighlight(_ req: DocumentHighlightRequest) async throws -> [DocumentHighlight]? {
|
|
let snapshot = try await self.latestSnapshot(for: req.textDocument.uri)
|
|
|
|
let relatedIdentifiers = try await self.relatedIdentifiers(
|
|
at: req.position,
|
|
in: snapshot,
|
|
includeNonEditableBaseNames: false
|
|
)
|
|
return relatedIdentifiers.relatedIdentifiers.map {
|
|
DocumentHighlight(
|
|
range: $0.range,
|
|
kind: .read // unknown
|
|
)
|
|
}
|
|
}
|
|
|
|
package func codeAction(_ req: CodeActionRequest) async throws -> CodeActionRequestResponse? {
|
|
if (try? ReferenceDocumentURL(from: req.textDocument.uri)) != nil {
|
|
// Do not show code actions in reference documents
|
|
return nil
|
|
}
|
|
let providersAndKinds: [(provider: CodeActionProvider, kind: CodeActionKind?)] = [
|
|
(retrieveSyntaxCodeActions, nil),
|
|
(retrieveRefactorCodeActions, .refactor),
|
|
(retrieveQuickFixCodeActions, .quickFix),
|
|
]
|
|
let wantedActionKinds = req.context.only
|
|
let providers: [CodeActionProvider] = providersAndKinds.compactMap {
|
|
if let wantedActionKinds, let kind = $0.1, !wantedActionKinds.contains(kind) {
|
|
return nil
|
|
}
|
|
|
|
return $0.provider
|
|
}
|
|
let codeActionCapabilities = capabilityRegistry.clientCapabilities.textDocument?.codeAction
|
|
let codeActions = try await retrieveCodeActions(req, providers: providers)
|
|
let response = CodeActionRequestResponse(
|
|
codeActions: codeActions,
|
|
clientCapabilities: codeActionCapabilities
|
|
)
|
|
return response
|
|
}
|
|
|
|
func retrieveCodeActions(
|
|
_ req: CodeActionRequest,
|
|
providers: [CodeActionProvider]
|
|
) async throws -> [CodeAction] {
|
|
guard providers.isEmpty == false else {
|
|
return []
|
|
}
|
|
return await providers.concurrentMap { provider in
|
|
do {
|
|
return try await provider(req)
|
|
} catch {
|
|
// Ignore any providers that failed to provide refactoring actions.
|
|
return []
|
|
}
|
|
}
|
|
.flatMap { $0 }
|
|
}
|
|
|
|
func retrieveSyntaxCodeActions(_ request: CodeActionRequest) async throws -> [CodeAction] {
|
|
let uri = request.textDocument.uri
|
|
let snapshot = try documentManager.latestSnapshot(uri)
|
|
|
|
let syntaxTree = await syntaxTreeManager.syntaxTree(for: snapshot)
|
|
guard let scope = SyntaxCodeActionScope(snapshot: snapshot, syntaxTree: syntaxTree, request: request) else {
|
|
return []
|
|
}
|
|
return await allSyntaxCodeActions.concurrentMap { provider in
|
|
return provider.codeActions(in: scope)
|
|
}.flatMap { $0 }
|
|
}
|
|
|
|
func retrieveRefactorCodeActions(_ params: CodeActionRequest) async throws -> [CodeAction] {
|
|
let additionalCursorInfoParameters: ((SKDRequestDictionary) -> Void) = { skreq in
|
|
skreq.set(self.keys.retrieveRefactorActions, to: 1)
|
|
}
|
|
|
|
let cursorInfoResponse = try await cursorInfo(
|
|
params.textDocument.uri,
|
|
params.range,
|
|
fallbackSettingsAfterTimeout: true,
|
|
additionalParameters: additionalCursorInfoParameters
|
|
)
|
|
|
|
var canInlineMacro = false
|
|
|
|
var refactorActions = cursorInfoResponse.refactorActions.compactMap {
|
|
let lspCommand = $0.asCommand()
|
|
if !canInlineMacro {
|
|
canInlineMacro = $0.actionString == "source.refactoring.kind.inline.macro"
|
|
}
|
|
|
|
return CodeAction(title: $0.title, kind: .refactor, command: lspCommand)
|
|
}
|
|
|
|
if canInlineMacro {
|
|
let expandMacroCommand = ExpandMacroCommand(positionRange: params.range, textDocument: params.textDocument)
|
|
.asCommand()
|
|
|
|
refactorActions.append(CodeAction(title: expandMacroCommand.title, kind: .refactor, command: expandMacroCommand))
|
|
}
|
|
|
|
return refactorActions
|
|
}
|
|
|
|
func retrieveQuickFixCodeActions(_ params: CodeActionRequest) async throws -> [CodeAction] {
|
|
let snapshot = try await self.latestSnapshot(for: params.textDocument.uri)
|
|
let buildSettings = await self.compileCommand(for: params.textDocument.uri, fallbackAfterTimeout: true)
|
|
let diagnosticReport = try await self.diagnosticReportManager.diagnosticReport(
|
|
for: snapshot,
|
|
buildSettings: buildSettings
|
|
)
|
|
|
|
let codeActions = diagnosticReport.items.flatMap { (diag) -> [CodeAction] in
|
|
let codeActions: [CodeAction] =
|
|
(diag.codeActions ?? []) + (diag.relatedInformation?.flatMap { $0.codeActions ?? [] } ?? [])
|
|
|
|
if codeActions.isEmpty {
|
|
// The diagnostic doesn't have fix-its. Don't return anything.
|
|
return []
|
|
}
|
|
|
|
// Check if the diagnostic overlaps with the selected range.
|
|
guard params.range.overlapsIncludingEmptyRanges(other: diag.range) else {
|
|
return []
|
|
}
|
|
|
|
// Check if the set of diagnostics provided by the request contains this diagnostic.
|
|
// For this, only compare the 'basic' properties of the diagnostics, excluding related information and code actions since
|
|
// code actions are only defined in an LSP extension and might not be sent back to us.
|
|
guard
|
|
params.context.diagnostics.contains(where: { (contextDiag) -> Bool in
|
|
return contextDiag.range == diag.range && contextDiag.severity == diag.severity
|
|
&& contextDiag.code == diag.code && contextDiag.source == diag.source && contextDiag.message == diag.message
|
|
})
|
|
else {
|
|
return []
|
|
}
|
|
|
|
// Flip the attachment of diagnostic to code action instead of the code action being attached to the diagnostic
|
|
return codeActions.map({
|
|
var codeAction = $0
|
|
var diagnosticWithoutCodeActions = diag
|
|
diagnosticWithoutCodeActions.codeActions = nil
|
|
if let related = diagnosticWithoutCodeActions.relatedInformation {
|
|
diagnosticWithoutCodeActions.relatedInformation = related.map {
|
|
var withoutCodeActions = $0
|
|
withoutCodeActions.codeActions = nil
|
|
return withoutCodeActions
|
|
}
|
|
}
|
|
codeAction.diagnostics = [diagnosticWithoutCodeActions]
|
|
return codeAction
|
|
})
|
|
}
|
|
|
|
return codeActions
|
|
}
|
|
|
|
package func inlayHint(_ req: InlayHintRequest) async throws -> [InlayHint] {
|
|
let uri = req.textDocument.uri
|
|
let infos = try await variableTypeInfos(uri, req.range)
|
|
let hints = infos
|
|
.lazy
|
|
.filter { !$0.hasExplicitType }
|
|
.map { info -> InlayHint in
|
|
let position = info.range.upperBound
|
|
let label = ": \(info.printedType)"
|
|
let textEdits: [TextEdit]?
|
|
if info.canBeFollowedByTypeAnnotation {
|
|
textEdits = [TextEdit(range: position..<position, newText: label)]
|
|
} else {
|
|
textEdits = nil
|
|
}
|
|
return InlayHint(
|
|
position: position,
|
|
label: .string(label),
|
|
kind: .type,
|
|
textEdits: textEdits
|
|
)
|
|
}
|
|
|
|
return Array(hints)
|
|
}
|
|
|
|
package func codeLens(_ req: CodeLensRequest) async throws -> [CodeLens] {
|
|
let snapshot = try documentManager.latestSnapshot(req.textDocument.uri)
|
|
var targetDisplayName: String? = nil
|
|
if let workspace = await sourceKitLSPServer?.workspaceForDocument(uri: req.textDocument.uri),
|
|
let target = await workspace.buildServerManager.canonicalTarget(for: req.textDocument.uri),
|
|
let buildTarget = await workspace.buildServerManager.buildTarget(named: target)
|
|
{
|
|
targetDisplayName = buildTarget.displayName
|
|
}
|
|
return await SwiftCodeLensScanner.findCodeLenses(
|
|
in: snapshot,
|
|
syntaxTreeManager: self.syntaxTreeManager,
|
|
targetName: targetDisplayName,
|
|
supportedCommands: self.capabilityRegistry.supportedCodeLensCommands
|
|
)
|
|
}
|
|
|
|
package func documentDiagnostic(_ req: DocumentDiagnosticsRequest) async throws -> DocumentDiagnosticReport {
|
|
do {
|
|
switch try? ReferenceDocumentURL(from: req.textDocument.uri) {
|
|
case .generatedInterface:
|
|
// Generated interfaces don't have diagnostics associated with them.
|
|
return .full(RelatedFullDocumentDiagnosticReport(items: []))
|
|
case .macroExpansion, nil: break
|
|
}
|
|
|
|
await semanticIndexManager?.prepareFileForEditorFunctionality(
|
|
req.textDocument.uri.buildSettingsFile
|
|
)
|
|
let snapshot = try await self.latestSnapshot(for: req.textDocument.uri)
|
|
let buildSettings = await self.compileCommand(for: req.textDocument.uri, fallbackAfterTimeout: false)
|
|
try Task.checkCancellation()
|
|
let diagnosticReport = try await self.diagnosticReportManager.diagnosticReport(
|
|
for: snapshot,
|
|
buildSettings: buildSettings
|
|
)
|
|
return .full(diagnosticReport)
|
|
} catch {
|
|
// VS Code does not request diagnostics again for a document if the diagnostics request failed.
|
|
// Since sourcekit-lsp usually recovers from failures (e.g. after sourcekitd crashes), this is undesirable.
|
|
// Instead of returning an error, return empty results.
|
|
// Do forward cancellation because we don't want to clear diagnostics in the client if they cancel the diagnostic
|
|
// request.
|
|
if ResponseError(error) == .cancelled {
|
|
throw error
|
|
}
|
|
logger.error(
|
|
"""
|
|
Loading diagnostic failed with the following error. Returning empty diagnostics.
|
|
\(error.forLogging)
|
|
"""
|
|
)
|
|
return .full(RelatedFullDocumentDiagnosticReport(items: []))
|
|
}
|
|
}
|
|
|
|
package func indexedRename(_ request: IndexedRenameRequest) async throws -> WorkspaceEdit? {
|
|
throw ResponseError.unknown("unsupported method")
|
|
}
|
|
|
|
package func executeCommand(_ req: ExecuteCommandRequest) async throws -> LSPAny? {
|
|
if let command = req.swiftCommand(ofType: SemanticRefactorCommand.self) {
|
|
try await semanticRefactoring(command)
|
|
} else if let command = req.swiftCommand(ofType: ExpandMacroCommand.self) {
|
|
try await expandMacro(command)
|
|
} else {
|
|
throw ResponseError.unknown("unknown command \(req.command)")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
package func getReferenceDocument(_ req: GetReferenceDocumentRequest) async throws -> GetReferenceDocumentResponse {
|
|
let referenceDocumentURL = try ReferenceDocumentURL(from: req.uri)
|
|
|
|
switch referenceDocumentURL {
|
|
case let .macroExpansion(data):
|
|
return GetReferenceDocumentResponse(
|
|
content: try await macroExpansionManager.macroExpansion(for: data)
|
|
)
|
|
case .generatedInterface(let data):
|
|
return GetReferenceDocumentResponse(
|
|
content: try await generatedInterfaceManager.snapshot(of: data).text
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
extension SwiftLanguageService: SKDNotificationHandler {
|
|
package nonisolated func notification(_ notification: SKDResponse) {
|
|
sourcekitdNotificationHandlingQueue.async {
|
|
await self.notificationImpl(notification)
|
|
}
|
|
}
|
|
|
|
private func notificationImpl(_ notification: SKDResponse) async {
|
|
logger.debug(
|
|
"""
|
|
Received notification from sourcekitd
|
|
\(notification.forLogging)
|
|
"""
|
|
)
|
|
// Check if we need to update our `state` based on the contents of the notification.
|
|
if notification.value?[self.keys.notification] == self.values.semaEnabledNotification {
|
|
await self.setState(.connected)
|
|
return
|
|
}
|
|
|
|
if self.state == .connectionInterrupted {
|
|
// If we get a notification while we are restoring the connection, it means that the server has restarted.
|
|
// We still need to wait for semantic functionality to come back up.
|
|
await self.setState(.semanticFunctionalityDisabled)
|
|
|
|
// Ask our parent to re-open all of our documents.
|
|
if let sourceKitLSPServer {
|
|
await sourceKitLSPServer.reopenDocuments(for: self)
|
|
} else {
|
|
logger.fault("Cannot reopen documents because SourceKitLSPServer is no longer alive")
|
|
}
|
|
}
|
|
|
|
if notification.error == .connectionInterrupted {
|
|
await self.setState(.connectionInterrupted)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Position conversion
|
|
|
|
/// A line:column position as it is used in sourcekitd, using UTF-8 for the column index and using a one-based line and
|
|
/// column number.
|
|
struct SourceKitDPosition {
|
|
/// Line number within a document (one-based).
|
|
public var line: Int
|
|
|
|
/// UTF-8 code-unit offset from the start of a line (1-based).
|
|
public var utf8Column: Int
|
|
}
|
|
|
|
extension DocumentSnapshot {
|
|
func sourcekitdPosition(
|
|
of position: Position,
|
|
callerFile: StaticString = #fileID,
|
|
callerLine: UInt = #line
|
|
) -> SourceKitDPosition {
|
|
let utf8Column = lineTable.utf8ColumnAt(
|
|
line: position.line,
|
|
utf16Column: position.utf16index,
|
|
callerFile: callerFile,
|
|
callerLine: callerLine
|
|
)
|
|
return SourceKitDPosition(line: position.line + 1, utf8Column: utf8Column + 1)
|
|
}
|
|
}
|
|
|
|
extension sourcekitd_api_uid_t {
|
|
func isCommentKind(_ vals: sourcekitd_api_values) -> Bool {
|
|
switch self {
|
|
case vals.comment, vals.commentMarker, vals.commentURL:
|
|
return true
|
|
default:
|
|
return isDocCommentKind(vals)
|
|
}
|
|
}
|
|
|
|
func isDocCommentKind(_ vals: sourcekitd_api_values) -> Bool {
|
|
return self == vals.docComment || self == vals.docCommentField
|
|
}
|
|
|
|
func asCompletionItemKind(_ vals: sourcekitd_api_values) -> CompletionItemKind? {
|
|
switch self {
|
|
case vals.completionKindKeyword:
|
|
return .keyword
|
|
case vals.declModule:
|
|
return .module
|
|
case vals.declClass:
|
|
return .class
|
|
case vals.declStruct:
|
|
return .struct
|
|
case vals.declEnum:
|
|
return .enum
|
|
case vals.declEnumElement:
|
|
return .enumMember
|
|
case vals.declProtocol:
|
|
return .interface
|
|
case vals.declAssociatedType:
|
|
return .typeParameter
|
|
case vals.declTypeAlias:
|
|
return .typeParameter
|
|
case vals.declGenericTypeParam:
|
|
return .typeParameter
|
|
case vals.declConstructor:
|
|
return .constructor
|
|
case vals.declDestructor:
|
|
return .value
|
|
case vals.declSubscript:
|
|
return .method
|
|
case vals.declMethodStatic:
|
|
return .method
|
|
case vals.declMethodInstance:
|
|
return .method
|
|
case vals.declFunctionPrefixOperator,
|
|
vals.declFunctionPostfixOperator,
|
|
vals.declFunctionInfixOperator:
|
|
return .operator
|
|
case vals.declPrecedenceGroup:
|
|
return .value
|
|
case vals.declFunctionFree:
|
|
return .function
|
|
case vals.declVarStatic, vals.declVarClass:
|
|
return .property
|
|
case vals.declVarInstance:
|
|
return .property
|
|
case vals.declVarLocal,
|
|
vals.declVarGlobal,
|
|
vals.declVarParam:
|
|
return .variable
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func asSymbolKind(_ vals: sourcekitd_api_values) -> SymbolKind? {
|
|
switch self {
|
|
case vals.declClass, vals.refClass, vals.declActor, vals.refActor:
|
|
return .class
|
|
case vals.declMethodInstance, vals.refMethodInstance,
|
|
vals.declMethodStatic, vals.refMethodStatic,
|
|
vals.declMethodClass, vals.refMethodClass:
|
|
return .method
|
|
case vals.declVarInstance, vals.refVarInstance,
|
|
vals.declVarStatic, vals.refVarStatic,
|
|
vals.declVarClass, vals.refVarClass:
|
|
return .property
|
|
case vals.declEnum, vals.refEnum:
|
|
return .enum
|
|
case vals.declEnumElement, vals.refEnumElement:
|
|
return .enumMember
|
|
case vals.declProtocol, vals.refProtocol:
|
|
return .interface
|
|
case vals.declFunctionFree, vals.refFunctionFree:
|
|
return .function
|
|
case vals.declVarGlobal, vals.refVarGlobal,
|
|
vals.declVarLocal, vals.refVarLocal:
|
|
return .variable
|
|
case vals.declStruct, vals.refStruct:
|
|
return .struct
|
|
case vals.declGenericTypeParam, vals.refGenericTypeParam:
|
|
return .typeParameter
|
|
case vals.declExtension:
|
|
// There are no extensions in LSP, so we return something vaguely similar
|
|
return .namespace
|
|
case vals.refModule:
|
|
return .module
|
|
case vals.declConstructor, vals.refConstructor:
|
|
return .constructor
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
}
|