Files
sourcekit-lsp/Sources/SwiftLanguageService/SwiftLanguageService.swift
2025-08-14 11:12:31 +02:00

1315 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 { return nil }
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 buildSettings(for document: DocumentURI, fallbackAfterTimeout: Bool) async -> FileBuildSettings? {
let buildSettingsFile = document.buildSettingsFile
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: buildSettingsFile) else {
return nil
}
return await workspace.buildServerManager.buildSettingsInferredFromMainFile(
for: buildSettingsFile,
language: .swift,
fallbackAfterTimeout: fallbackAfterTimeout
)
}
func compileCommand(for document: DocumentURI, fallbackAfterTimeout: Bool) async -> SwiftCompileCommand? {
if let settings = await self.buildSettings(for: document, fallbackAfterTimeout: fallbackAfterTimeout) {
return SwiftCompileCommand(settings)
} else {
return nil
}
}
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: [".", "("]
),
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 clientInitialized(_: InitializedNotification) {
// Nothing to do.
}
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)
}
}
}
/// 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)
}
package func willSaveDocument(_ notification: WillSaveTextDocumentNotification) {
}
package func didSaveDocument(_ notification: DidSaveTextDocumentNotification) {
}
// 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
}
}
}