diff --git a/src/TestExplorer/LSPTestDiscovery.ts b/src/TestExplorer/LSPTestDiscovery.ts index 439c3f807..fa2ebf88f 100644 --- a/src/TestExplorer/LSPTestDiscovery.ts +++ b/src/TestExplorer/LSPTestDiscovery.ts @@ -16,29 +16,12 @@ import * as vscode from "vscode"; import * as TestDiscovery from "./TestDiscovery"; import { LSPTestItem, - textDocumentTestsRequest, - workspaceTestsRequest, -} from "../sourcekit-lsp/lspExtensions"; -import { InitializeResult, RequestType } from "vscode-languageclient/node"; + TextDocumentTestsRequest, + WorkspaceTestsRequest, +} from "../sourcekit-lsp/extensions"; import { SwiftPackage, TargetType } from "../SwiftPackage"; -import { Converter } from "vscode-languageclient/lib/common/protocolConverter"; - -interface ILanguageClient { - get initializeResult(): InitializeResult | undefined; - get protocol2CodeConverter(): Converter; - - sendRequest( - type: RequestType, - params: P, - token?: vscode.CancellationToken - ): Promise; -} - -interface ILanguageClientManager { - useLanguageClient(process: { - (client: ILanguageClient, cancellationToken: vscode.CancellationToken): Promise; - }): Promise; -} +import { LanguageClientManager } from "../sourcekit-lsp/LanguageClientManager"; +import { LanguageClient } from "vscode-languageclient/node"; /** * Used to augment test discovery via `swift test --list-tests`. @@ -49,7 +32,7 @@ interface ILanguageClientManager { * these results. */ export class LSPTestDiscovery { - constructor(private languageClient: ILanguageClientManager) {} + constructor(private languageClient: LanguageClientManager) {} /** * Return a list of tests in the supplied document. @@ -62,15 +45,15 @@ export class LSPTestDiscovery { return await this.languageClient.useLanguageClient(async (client, token) => { // Only use the lsp for this request if it supports the // textDocument/tests method, and is at least version 2. - if (this.checkExperimentalCapability(client, textDocumentTestsRequest.method, 2)) { + if (this.checkExperimentalCapability(client, TextDocumentTestsRequest.method, 2)) { const testsInDocument = await client.sendRequest( - textDocumentTestsRequest, + TextDocumentTestsRequest.type, { textDocument: { uri: document.toString() } }, token ); return this.transformToTestClass(client, swiftPackage, testsInDocument); } else { - throw new Error(`${textDocumentTestsRequest.method} requests not supported`); + throw new Error(`${TextDocumentTestsRequest.method} requests not supported`); } }); } @@ -83,11 +66,11 @@ export class LSPTestDiscovery { return await this.languageClient.useLanguageClient(async (client, token) => { // Only use the lsp for this request if it supports the // workspace/tests method, and is at least version 2. - if (this.checkExperimentalCapability(client, workspaceTestsRequest.method, 2)) { - const tests = await client.sendRequest(workspaceTestsRequest, {}, token); + if (this.checkExperimentalCapability(client, WorkspaceTestsRequest.method, 2)) { + const tests = await client.sendRequest(WorkspaceTestsRequest.type, token); return this.transformToTestClass(client, swiftPackage, tests); } else { - throw new Error(`${workspaceTestsRequest.method} requests not supported`); + throw new Error(`${WorkspaceTestsRequest.method} requests not supported`); } }); } @@ -97,7 +80,7 @@ export class LSPTestDiscovery { * above the supplied `minVersion`. */ private checkExperimentalCapability( - client: ILanguageClient, + client: LanguageClient, method: string, minVersion: number ) { @@ -114,7 +97,7 @@ export class LSPTestDiscovery { * updating the format of the location. */ private transformToTestClass( - client: ILanguageClient, + client: LanguageClient, swiftPackage: SwiftPackage, input: LSPTestItem[] ): TestDiscovery.TestClass[] { diff --git a/src/TestExplorer/SPMTestDiscovery.ts b/src/TestExplorer/SPMTestDiscovery.ts index ce5a7323e..8616e1910 100644 --- a/src/TestExplorer/SPMTestDiscovery.ts +++ b/src/TestExplorer/SPMTestDiscovery.ts @@ -12,7 +12,7 @@ // //===----------------------------------------------------------------------===// -import { TestStyle } from "../sourcekit-lsp/lspExtensions"; +import { TestStyle } from "../sourcekit-lsp/extensions"; import { TestClass } from "./TestDiscovery"; /* diff --git a/src/TestExplorer/TestDiscovery.ts b/src/TestExplorer/TestDiscovery.ts index 5da5d7dcd..c0017897b 100644 --- a/src/TestExplorer/TestDiscovery.ts +++ b/src/TestExplorer/TestDiscovery.ts @@ -14,7 +14,7 @@ import * as vscode from "vscode"; import { SwiftPackage, TargetType } from "../SwiftPackage"; -import { LSPTestItem } from "../sourcekit-lsp/lspExtensions"; +import { LSPTestItem } from "../sourcekit-lsp/extensions"; import { reduceTestItemChildren } from "./TestUtils"; /** Test class definition */ diff --git a/src/commands/reindexProject.ts b/src/commands/reindexProject.ts index ffb34e6f4..95242c7fd 100644 --- a/src/commands/reindexProject.ts +++ b/src/commands/reindexProject.ts @@ -14,7 +14,7 @@ import * as vscode from "vscode"; import { WorkspaceContext } from "../WorkspaceContext"; -import { reindexProjectRequest } from "../sourcekit-lsp/lspExtensions"; +import { ReIndexProjectRequest } from "../sourcekit-lsp/extensions"; /** * Request that the SourceKit-LSP server reindexes the workspace. @@ -22,7 +22,7 @@ import { reindexProjectRequest } from "../sourcekit-lsp/lspExtensions"; export function reindexProject(workspaceContext: WorkspaceContext): Promise { return workspaceContext.languageClientManager.useLanguageClient(async (client, token) => { try { - await client.sendRequest(reindexProjectRequest, {}, token); + await client.sendRequest(ReIndexProjectRequest.type, token); const result = await vscode.window.showWarningMessage( "Re-indexing a project should never be necessary and indicates a bug in SourceKit-LSP. Please file an issue describing which symbol was out-of-date and how you got into the state.", "Report Issue", diff --git a/src/sourcekit-lsp/LanguageClientFactory.ts b/src/sourcekit-lsp/LanguageClientFactory.ts new file mode 100644 index 000000000..ef22cbcf9 --- /dev/null +++ b/src/sourcekit-lsp/LanguageClientFactory.ts @@ -0,0 +1,41 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import { LanguageClient, LanguageClientOptions, ServerOptions } from "vscode-languageclient/node"; + +/** + * Used to create a {@link LanguageClient} for use in VS Code. + * + * This is primarily used to make unit testing easier so that we don't have to + * mock out a constructor in the `vscode-languageclient` module. + */ +export class LanguageClientFactory { + /** + * Create a new {@link LanguageClient} for use in VS Code. + * + * @param name the human-readable name for the client + * @param id the identifier for the client (used in settings) + * @param serverOptions the {@link ServerOptions} + * @param clientOptions the {@link LanguageClientOptions} + * @returns the newly created {@link LanguageClient} + */ + createLanguageClient( + name: string, + id: string, + serverOptions: ServerOptions, + clientOptions: LanguageClientOptions + ): LanguageClient { + return new LanguageClient(name, id, serverOptions, clientOptions); + } +} diff --git a/src/sourcekit-lsp/LanguageClientManager.ts b/src/sourcekit-lsp/LanguageClientManager.ts index 239062740..2d20815b0 100644 --- a/src/sourcekit-lsp/LanguageClientManager.ts +++ b/src/sourcekit-lsp/LanguageClientManager.ts @@ -14,7 +14,20 @@ import * as vscode from "vscode"; import * as path from "path"; -import * as langclient from "vscode-languageclient/node"; +import { + CloseAction, + CloseHandlerResult, + DidChangeWorkspaceFoldersNotification, + ErrorAction, + ErrorHandler, + ErrorHandlerResult, + LanguageClientOptions, + Message, + MessageType, + RevealOutputChannelOn, + State, + vsdiag, +} from "vscode-languageclient"; import configuration from "../configuration"; import { swiftRuntimeEnv } from "../utilities/utilities"; import { isPathInsidePath } from "../utilities/filesystem"; @@ -23,7 +36,7 @@ import { FolderOperation, WorkspaceContext } from "../WorkspaceContext"; import { activateLegacyInlayHints } from "./inlayHints"; import { activatePeekDocuments } from "./peekDocuments"; import { FolderContext } from "../FolderContext"; -import { LanguageClient } from "vscode-languageclient/node"; +import { Executable, LanguageClient, ServerOptions } from "vscode-languageclient/node"; import { ArgumentFilter, BuildFlags } from "../toolchain/BuildFlags"; import { DiagnosticsManager } from "../DiagnosticsManager"; import { LSPLogger, LSPOutputChannel } from "./LSPOutputChannel"; @@ -31,15 +44,14 @@ import { SwiftOutputChannel } from "../ui/SwiftOutputChannel"; import { promptForDiagnostics } from "../commands/captureDiagnostics"; import { activateGetReferenceDocument } from "./getReferenceDocument"; import { uriConverters } from "./uriConverters"; +import { LanguageClientFactory } from "./LanguageClientFactory"; +import { SourceKitLogMessageNotification, SourceKitLogMessageParams } from "./extensions"; -interface SourceKitLogMessageParams extends langclient.LogMessageParams { - logName?: string; -} - -/** Manages the creation and destruction of Language clients as we move between +/** + * Manages the creation and destruction of Language clients as we move between * workspace folders */ -export class LanguageClientManager { +export class LanguageClientManager implements vscode.Disposable { // known log names static indexingLogName = "SourceKit-LSP: Indexing"; @@ -110,7 +122,7 @@ export class LanguageClientManager { * undefined means not setup * null means in the process of restarting */ - private languageClient: langclient.LanguageClient | null | undefined; + private languageClient: LanguageClient | null | undefined; private cancellationToken?: vscode.CancellationTokenSource; private legacyInlayHints?: vscode.Disposable; private peekDocuments?: vscode.Disposable; @@ -130,14 +142,17 @@ export class LanguageClientManager { public subFolderWorkspaces: vscode.Uri[]; private namedOutputChannels: Map = new Map(); /** Get the current state of the underlying LanguageClient */ - public get state(): langclient.State { + public get state(): State { if (!this.languageClient) { - return langclient.State.Stopped; + return State.Stopped; } return this.languageClient.state; } - constructor(public workspaceContext: WorkspaceContext) { + constructor( + public workspaceContext: WorkspaceContext, + private languageClientFactory: LanguageClientFactory = new LanguageClientFactory() + ) { this.namedOutputChannels.set( LanguageClientManager.indexingLogName, new LSPOutputChannel(LanguageClientManager.indexingLogName, false, true) @@ -196,7 +211,7 @@ export class LanguageClientManager { // Enabling/Disabling sourcekit-lsp shows a special notification if (event.affectsConfiguration("swift.sourcekit-lsp.disable")) { if (configuration.lsp.disable) { - if (this.state === langclient.State.Stopped) { + if (this.state === State.Stopped) { // Language client is already stopped return; } @@ -204,7 +219,7 @@ export class LanguageClientManager { "You have disabled the Swift language server, but it is still running. Would you like to stop it now?"; restartLSPButton = "Stop Language Server"; } else { - if (this.state !== langclient.State.Stopped) { + if (this.state !== State.Stopped) { // Langauge client is already running return; } @@ -212,7 +227,7 @@ export class LanguageClientManager { "You have enabled the Swift language server. Would you like to start it now?"; restartLSPButton = "Start Language Server"; } - } else if (configuration.lsp.disable && this.state === langclient.State.Stopped) { + } else if (configuration.lsp.disable && this.state === State.Stopped) { // Ignore configuration changes if SourceKit-LSP is disabled return; } @@ -248,7 +263,7 @@ export class LanguageClientManager { // The language client stops asnyhronously, so we need to wait for it to stop // instead of doing it in dispose, which must be synchronous. async stop() { - if (this.languageClient && this.languageClient.state === langclient.State.Running) { + if (this.languageClient && this.languageClient.state === State.Running) { await this.languageClient.dispose(); } } @@ -271,10 +286,7 @@ export class LanguageClientManager { * @returns result of process */ async useLanguageClient(process: { - ( - client: langclient.LanguageClient, - cancellationToken: vscode.CancellationToken - ): Promise; + (client: LanguageClient, cancellationToken: vscode.CancellationToken): Promise; }): Promise { if (!this.languageClient || !this.clientReadyPromise) { throw new Error(LanguageClientError.LanguageClientUnavailable); @@ -310,7 +322,7 @@ export class LanguageClientManager { uri: client.code2ProtocolConverter.asUri(uri), name: FolderContext.uriName(uri), }; - client.sendNotification(langclient.DidChangeWorkspaceFoldersNotification.type, { + client.sendNotification(DidChangeWorkspaceFoldersNotification.type, { event: { added: [workspaceFolder], removed: [] }, }); }); @@ -327,7 +339,7 @@ export class LanguageClientManager { uri: client.code2ProtocolConverter.asUri(uri), name: FolderContext.uriName(uri), }; - client.sendNotification(langclient.DidChangeWorkspaceFoldersNotification.type, { + client.sendNotification(DidChangeWorkspaceFoldersNotification.type, { event: { added: [], removed: [workspaceFolder] }, }); }); @@ -340,7 +352,7 @@ export class LanguageClientManager { uri: client.code2ProtocolConverter.asUri(uri), name: FolderContext.uriName(uri), }; - client.sendNotification(langclient.DidChangeWorkspaceFoldersNotification.type, { + client.sendNotification(DidChangeWorkspaceFoldersNotification.type, { event: { added: [workspaceFolder], removed: [] }, }); } @@ -453,7 +465,7 @@ export class LanguageClientManager { } private createLSPClient(folder?: vscode.Uri): { - client: langclient.LanguageClient; + client: LanguageClient; errorHandler: SourceKitLSPErrorHandler; } { const toolchainSourceKitLSP = @@ -471,7 +483,7 @@ export class LanguageClientManager { ), ]; - const sourcekit: langclient.Executable = { + const sourcekit: Executable = { command: serverPath, args: lspConfig.serverArguments.concat(sdkArguments), options: { @@ -499,16 +511,16 @@ export class LanguageClientManager { }; } - const serverOptions: langclient.ServerOptions = sourcekit; + const serverOptions: ServerOptions = sourcekit; let workspaceFolder = undefined; if (folder) { workspaceFolder = { uri: folder, name: FolderContext.uriName(folder), index: 0 }; } const errorHandler = new SourceKitLSPErrorHandler(5); - const clientOptions: langclient.LanguageClientOptions = { + const clientOptions: LanguageClientOptions = { documentSelector: LanguageClientManager.documentSelector, - revealOutputChannelOn: langclient.RevealOutputChannelOn.Never, + revealOutputChannelOn: RevealOutputChannelOn.Never, workspaceFolder: workspaceFolder, outputChannel: new SwiftOutputChannel("SourceKit Language Server", false), middleware: { @@ -545,7 +557,7 @@ export class LanguageClientManager { }, provideDiagnostics: async (uri, previousResultId, token, next) => { const result = await next(uri, previousResultId, token); - if (result?.kind === langclient.vsdiag.DocumentDiagnosticReportKind.unChanged) { + if (result?.kind === vsdiag.DocumentDiagnosticReportKind.unChanged) { return undefined; } const document = uri as vscode.TextDocument; @@ -590,7 +602,7 @@ export class LanguageClientManager { }; return { - client: new langclient.LanguageClient( + client: this.languageClientFactory.createLanguageClient( "swift.sourcekit-lsp", "SourceKit Language Server", serverOptions, @@ -622,21 +634,14 @@ export class LanguageClientManager { } return options; } - /* eslint-enable @typescript-eslint/no-explicit-any */ - private async startClient( - client: langclient.LanguageClient, - errorHandler: SourceKitLSPErrorHandler - ) { + private async startClient(client: LanguageClient, errorHandler: SourceKitLSPErrorHandler) { client.onDidChangeState(e => { // if state is now running add in any sub-folder workspaces that // we have cached. If this is the first time we are starting then // we won't have any sub folder workspaces, but if the server crashed // or we forced a restart then we need to do this - if ( - e.oldState === langclient.State.Starting && - e.newState === langclient.State.Running - ) { + if (e.oldState === State.Starting && e.newState === State.Running) { this.addSubFolderWorkspaces(client); } }); @@ -650,7 +655,7 @@ export class LanguageClientManager { this.workspaceContext.outputChannel.log(`SourceKit-LSP setup`); } - client.onNotification(langclient.LogMessageNotification.type, params => { + client.onNotification(SourceKitLogMessageNotification.type, params => { this.logMessage(client, params as SourceKitLogMessageParams); }); @@ -683,7 +688,7 @@ export class LanguageClientManager { return this.clientReadyPromise; } - private logMessage(client: langclient.LanguageClient, params: SourceKitLogMessageParams) { + private logMessage(client: LanguageClient, params: SourceKitLogMessageParams) { let logger: LSPLogger = client; if (params.logName) { const outputChannel = @@ -693,19 +698,19 @@ export class LanguageClientManager { logger = outputChannel; } switch (params.type) { - case langclient.MessageType.Info: + case MessageType.Info: logger.info(params.message); break; - case langclient.MessageType.Debug: + case MessageType.Debug: logger.debug(params.message); break; - case langclient.MessageType.Warning: + case MessageType.Warning: logger.warn(params.message); break; - case langclient.MessageType.Error: + case MessageType.Error: logger.error(params.message); break; - case langclient.MessageType.Log: + case MessageType.Log: logger.info(params.message); break; } @@ -717,7 +722,7 @@ export class LanguageClientManager { * an error message that asks if you want to restart the sourcekit-lsp server again * after so many crashes */ -export class SourceKitLSPErrorHandler implements langclient.ErrorHandler { +export class SourceKitLSPErrorHandler implements ErrorHandler { private restarts: number[]; private enabled: boolean = false; @@ -740,32 +745,32 @@ export class SourceKitLSPErrorHandler implements langclient.ErrorHandler { */ error( _error: Error, - _message: langclient.Message | undefined, + _message: Message | undefined, count: number | undefined - ): langclient.ErrorHandlerResult | Promise { + ): ErrorHandlerResult | Promise { if (count && count <= 3) { - return { action: langclient.ErrorAction.Continue }; + return { action: ErrorAction.Continue }; } - return { action: langclient.ErrorAction.Shutdown }; + return { action: ErrorAction.Shutdown }; } /** * The connection to the server got closed. */ - closed(): langclient.CloseHandlerResult | Promise { + closed(): CloseHandlerResult | Promise { if (!this.enabled) { return { - action: langclient.CloseAction.DoNotRestart, + action: CloseAction.DoNotRestart, handled: true, }; } this.restarts.push(Date.now()); if (this.restarts.length <= this.maxRestartCount) { - return { action: langclient.CloseAction.Restart }; + return { action: CloseAction.Restart }; } else { const diff = this.restarts[this.restarts.length - 1] - this.restarts[0]; if (diff <= 3 * 60 * 1000) { - return new Promise(resolve => { + return new Promise(resolve => { vscode.window .showErrorMessage( `The SourceKit-LSP server crashed ${ @@ -777,15 +782,15 @@ export class SourceKitLSPErrorHandler implements langclient.ErrorHandler { .then(result => { if (result === "Yes") { this.restarts = []; - resolve({ action: langclient.CloseAction.Restart }); + resolve({ action: CloseAction.Restart }); } else { - resolve({ action: langclient.CloseAction.DoNotRestart }); + resolve({ action: CloseAction.DoNotRestart }); } }); }); } else { this.restarts.shift(); - return { action: langclient.CloseAction.Restart }; + return { action: CloseAction.Restart }; } } } diff --git a/src/sourcekit-lsp/extensions/GetReferenceDocumentRequest.ts b/src/sourcekit-lsp/extensions/GetReferenceDocumentRequest.ts new file mode 100644 index 000000000..eff0c4b9a --- /dev/null +++ b/src/sourcekit-lsp/extensions/GetReferenceDocumentRequest.ts @@ -0,0 +1,55 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// We use namespaces to store request information just like vscode-languageclient +/* eslint-disable @typescript-eslint/no-namespace */ + +import { DocumentUri, MessageDirection, RequestType } from "vscode-languageclient"; + +/** Parameters used to make a {@link GetReferenceDocumentRequest}. */ +export interface GetReferenceDocumentParams { + /** The {@link DocumentUri} of the custom scheme url for which content is required. */ + uri: DocumentUri; +} + +/** Response containing `content` of a {@link GetReferenceDocumentRequest}. */ +export interface GetReferenceDocumentResult { + content: string; +} + +/** + * Request from the client to the server asking for contents of a URI having a custom scheme **(LSP Extension)** + * For example: "sourcekit-lsp:" + * + * - Parameters: + * - uri: The `DocumentUri` of the custom scheme url for which content is required + * + * - Returns: `GetReferenceDocumentResponse` which contains the `content` to be displayed. + * + * ### LSP Extension + * + * This request is an extension to LSP supported by SourceKit-LSP. + * + * Enable the experimental client capability `"workspace/getReferenceDocument"` so that the server responds with + * reference document URLs for certain requests or commands whenever possible. + */ +export namespace GetReferenceDocumentRequest { + export const method = "workspace/getReferenceDocument" as const; + export const messageDirection: MessageDirection = MessageDirection.clientToServer; + export const type = new RequestType< + GetReferenceDocumentParams, + GetReferenceDocumentResult, + never + >(method); +} diff --git a/src/sourcekit-lsp/extensions/GetTestsRequest.ts b/src/sourcekit-lsp/extensions/GetTestsRequest.ts new file mode 100644 index 000000000..21da8317e --- /dev/null +++ b/src/sourcekit-lsp/extensions/GetTestsRequest.ts @@ -0,0 +1,114 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// We use namespaces to store request information just like vscode-languageclient +/* eslint-disable @typescript-eslint/no-namespace */ + +import { + Location, + TextDocumentIdentifier, + MessageDirection, + RequestType0, + RequestType, +} from "vscode-languageclient"; + +/** Test styles where test-target represents a test target that contains tests. */ +export type TestStyle = "XCTest" | "swift-testing" | "test-target"; + +/** Represents a single test returned from a {@link WorkspaceTestsRequest} or {@link TextDocumentTestsRequest}. */ +export interface LSPTestItem { + /** + * This identifier uniquely identifies the test case or test suite. It can be used to run an individual test (suite). + */ + id: string; + + /** + * Display name describing the test. + */ + label: string; + + /** + * Optional description that appears next to the label. + */ + description?: string; + + /** + * A string that should be used when comparing this item with other items. + * + * When `undefined` the `label` is used. + */ + sortText?: string; + + /** + * Whether the test is disabled. + */ + disabled: boolean; + + /** + * The type of test, eg. the testing framework that was used to declare the test. + */ + style: TestStyle; + + /** + * The location of the test item in the source code. + */ + location: Location; + + /** + * The children of this test item. + * + * For a test suite, this may contain the individual test cases or nested suites. + */ + children: LSPTestItem[]; + + /** + * Tags associated with this test item. + */ + tags: { id: string }[]; +} + +/** + * A request that returns symbols for all the test classes and test methods within the current workspace. + * + * ### LSP Extension + * + * This request is an extension to LSP supported by SourceKit-LSP. + * + * It requires the experimental client capability `"workspace/tests"` to use. + */ +export namespace WorkspaceTestsRequest { + export const method = "workspace/tests" as const; + export const messageDirection: MessageDirection = MessageDirection.clientToServer; + export const type = new RequestType0(method); +} + +/** Parameters used to make a {@link TextDocumentTestsRequest}. */ +export interface TextDocumentTestsParams { + textDocument: TextDocumentIdentifier; +} + +/** + * A request that returns symbols for all the test classes and test methods within a file. + * + * ### LSP Extension + * + * This request is an extension to LSP supported by SourceKit-LSP. + * + * It requires the experimental client capability `"textDocument/tests"` to use. + */ +export namespace TextDocumentTestsRequest { + export const method = "textDocument/tests" as const; + export const messageDirection: MessageDirection = MessageDirection.clientToServer; + export const type = new RequestType(method); +} diff --git a/src/sourcekit-lsp/extensions/LegacyInlayHintRequest.ts b/src/sourcekit-lsp/extensions/LegacyInlayHintRequest.ts new file mode 100644 index 000000000..1c23386b6 --- /dev/null +++ b/src/sourcekit-lsp/extensions/LegacyInlayHintRequest.ts @@ -0,0 +1,72 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// We use namespaces to store request information just like vscode-languageclient +/* eslint-disable @typescript-eslint/no-namespace */ + +import { + TextDocumentIdentifier, + Range, + Position, + MessageDirection, + RequestType, +} from "vscode-languageclient"; + +/** Parameters used to make a {@link LegacyInlayHintRequest}. */ +export interface LegacyInlayHintsParams { + /** + * The text document. + */ + textDocument: TextDocumentIdentifier; + + /** + * If set, the reange for which inlay hints are + * requested. If unset, hints for the entire document + * are returned. + */ + range?: Range; + + /** + * The categories of inlay hints that are requested. + * If unset, all categories are returned. + */ + only?: string[]; +} + +/** Inlay Hint (pre Swift 5.6) */ +export interface LegacyInlayHint { + /** + * The position within the code that this hint is + * attached to. + */ + position: Position; + + /** + * The hint's kind, used for more flexible client-side + * styling of the hint. + */ + category?: string; + + /** + * The hint's rendered label. + */ + label: string; +} + +/** Inlay Hints (pre Swift 5.6) */ +export namespace LegacyInlayHintRequest { + export const method = "sourcekit-lsp/inlayHints" as const; + export const messageDirection: MessageDirection = MessageDirection.clientToServer; + export const type = new RequestType(method); +} diff --git a/src/sourcekit-lsp/extensions/PeekDocumentsRequest.ts b/src/sourcekit-lsp/extensions/PeekDocumentsRequest.ts new file mode 100644 index 000000000..ea1302b40 --- /dev/null +++ b/src/sourcekit-lsp/extensions/PeekDocumentsRequest.ts @@ -0,0 +1,68 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// We use namespaces to store request information just like vscode-languageclient +/* eslint-disable @typescript-eslint/no-namespace */ + +import { DocumentUri, Position, MessageDirection, RequestType } from "vscode-languageclient"; + +/** Parameters used to make a {@link PeekDocumentsRequest}. */ +export interface PeekDocumentsParams { + /** + * The `DocumentUri` of the text document in which to show the "peeked" editor + */ + uri: DocumentUri; + + /** + * The `Position` in the given text document in which to show the "peeked editor" + */ + position: Position; + + /** + * An array `DocumentUri` of the documents to appear inside the "peeked" editor + */ + locations: DocumentUri[]; +} + +/** Response to indicate the `success` of the {@link PeekDocumentsRequest}. */ +export interface PeekDocumentsResponse { + success: boolean; +} + +/** + * Request from the server to the client to show the given documents in a "peeked" editor **(LSP Extension)** + * + * This request is handled by the client to show the given documents in a + * "peeked" editor (i.e. inline with / inside the editor canvas). This is + * similar to VS Code's built-in "editor.action.peekLocations" command. + * + * - Parameters: + * - uri: The {@link DocumentUri} of the text document in which to show the "peeked" editor + * - position: The {@link Position} in the given text document in which to show the "peeked editor" + * - locations: The {@link DocumentUri} of documents to appear inside the "peeked" editor + * + * - Returns: {@link PeekDocumentsResponse} which indicates the `success` of the request. + * + * ### LSP Extension + * + * This request is an extension to LSP supported by SourceKit-LSP. + * + * It requires the experimental client capability `"workspace/peekDocuments"` to use. + * It also needs the client to handle the request and present the "peeked" editor. + */ +export namespace PeekDocumentsRequest { + export const method = "workspace/peekDocuments" as const; + export const messageDirection: MessageDirection = MessageDirection.clientToServer; + export const type = new RequestType(method); +} diff --git a/src/sourcekit-lsp/extensions/ReIndexProjectRequest.ts b/src/sourcekit-lsp/extensions/ReIndexProjectRequest.ts new file mode 100644 index 000000000..184ef12ab --- /dev/null +++ b/src/sourcekit-lsp/extensions/ReIndexProjectRequest.ts @@ -0,0 +1,35 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// We use namespaces to store request information just like vscode-languageclient +/* eslint-disable @typescript-eslint/no-namespace */ + +import { MessageDirection, RequestType0 } from "vscode-languageclient"; + +/** + * Re-index all files open in the SourceKit-LSP server. + * + * Users should not need to rely on this request. The index should always be updated automatically in the background. + * Having to invoke this request means there is a bug in SourceKit-LSP's automatic re-indexing. It does, however, offer + * a workaround to re-index files when such a bug occurs where otherwise there would be no workaround. + * + * ### LSP Extension + * + * This request is an extension to LSP supported by SourceKit-LSP. + */ +export namespace ReIndexProjectRequest { + export const method = "workspace/triggerReindex" as const; + export const messageDirection: MessageDirection = MessageDirection.clientToServer; + export const type = new RequestType0("workspace/triggerReindex"); +} diff --git a/src/sourcekit-lsp/extensions/SourceKitLogMessageNotification.ts b/src/sourcekit-lsp/extensions/SourceKitLogMessageNotification.ts new file mode 100644 index 000000000..f81412817 --- /dev/null +++ b/src/sourcekit-lsp/extensions/SourceKitLogMessageNotification.ts @@ -0,0 +1,38 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// We use namespaces to store request information just like vscode-languageclient +/* eslint-disable @typescript-eslint/no-namespace */ + +import { LogMessageParams, MessageDirection, NotificationType } from "vscode-languageclient"; + +/** Parameters sent in a {@link SourceKitLogMessageNotification}. */ +export interface SourceKitLogMessageParams extends LogMessageParams { + logName?: string; +} + +/** + * The log message notification is sent from the server to the client to ask the client to + * log a particular message. + * + * ### LSP Extension + * + * This notification has the same behaviour as the `window/logMessage` notification built + * into the LSP. However, SourceKit-LSP adds extra information to the parameters. + */ +export namespace SourceKitLogMessageNotification { + export const method = "window/logMessage" as const; + export const messageDirection: MessageDirection = MessageDirection.serverToClient; + export const type = new NotificationType(method); +} diff --git a/src/sourcekit-lsp/extensions/SymbolInfoRequest.ts b/src/sourcekit-lsp/extensions/SymbolInfoRequest.ts new file mode 100644 index 000000000..0fdcbbf96 --- /dev/null +++ b/src/sourcekit-lsp/extensions/SymbolInfoRequest.ts @@ -0,0 +1,156 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// We use namespaces to store request information just like vscode-languageclient +/* eslint-disable @typescript-eslint/no-namespace */ + +import { + TextDocumentIdentifier, + Position, + Location, + SymbolKind, + MessageDirection, + RequestType, +} from "vscode-languageclient"; + +/** Parameters used to make a {@link SymbolInfoRequest}. */ +export interface SymbolInfoParams { + /** The document in which to lookup the symbol location. */ + textDocument: TextDocumentIdentifier; + + /** The document location at which to lookup symbol information. */ + position: Position; +} + +/** Information about which module a symbol is defined in. */ +export interface ModuleInfo { + /** The name of the module in which the symbol is defined. */ + moduleName: string; + + /** If the symbol is defined within a subgroup of a module, the name of the group. */ + groupName?: string; +} + +/** Detailed information about a symbol, such as the response to a {@link SymbolInfoRequest}. */ +export interface SymbolDetails { + /** The name of the symbol, if any. */ + name?: string; + + /** + * The name of the containing type for the symbol, if any. + * + * For example, in the following snippet, the `containerName` of `foo()` is `C`. + * + * ```c++ + * class C { + * void foo() {} + * } + * ``` + */ + containerName?: string; + + /** The USR of the symbol, if any. */ + usr?: string; + + /** + * Best known declaration or definition location without global knowledge. + * + * For a local or private variable, this is generally the canonical definition location - + * appropriate as a response to a `textDocument/definition` request. For global symbols this is + * the best known location within a single compilation unit. For example, in C++ this might be + * the declaration location from a header as opposed to the definition in some other + * translation unit. + * */ + bestLocalDeclaration?: Location; + + /** The kind of the symbol */ + kind?: SymbolKind; + + /** + * Whether the symbol is a dynamic call for which it isn't known which method will be invoked at runtime. This is + * the case for protocol methods and class functions. + * + * Optional because `clangd` does not return whether a symbol is dynamic. + */ + isDynamic?: boolean; + + /** + * Whether this symbol is defined in the SDK or standard library. + * + * This property only applies to Swift symbols. + */ + isSystem?: boolean; + + /** + * If the symbol is dynamic, the USRs of the types that might be called. + * + * This is relevant in the following cases: + * ```swift + * class A { + * func doThing() {} + * } + * class B: A {} + * class C: B { + * override func doThing() {} + * } + * class D: A { + * override func doThing() {} + * } + * func test(value: B) { + * value.doThing() + * } + * ``` + * + * The USR of the called function in `value.doThing` is `A.doThing` (or its + * mangled form) but it can never call `D.doThing`. In this case, the + * receiver USR would be `B`, indicating that only overrides of subtypes in + * `B` may be called dynamically. + */ + receiverUsrs?: string[]; + + /** + * If the symbol is defined in a module that doesn't have source information associated with it, the name and group + * and group name that defines this symbol. + * + * This property only applies to Swift symbols. + */ + systemModule?: ModuleInfo; +} + +/** + * Request for semantic information about the symbol at a given location **(LSP Extension)**. + * + * This request looks up the symbol (if any) at a given text document location and returns + * SymbolDetails for that location, including information such as the symbol's USR. The symbolInfo + * request is not primarily designed for editors, but instead as an implementation detail of how + * one LSP implementation (e.g. SourceKit) gets information from another (e.g. clangd) to use in + * performing index queries or otherwise implementing the higher level requests such as definition. + * + * - Parameters: + * - textDocument: The document in which to lookup the symbol location. + * - position: The document location at which to lookup symbol information. + * + * - Returns: `[SymbolDetails]` for the given location, which may have multiple elements if there are + * multiple references, or no elements if there is no symbol at the given location. + * + * ### LSP Extension + * + * This request is an extension to LSP supported by SourceKit-LSP and clangd. It does *not* require + * any additional client or server capabilities to use. + */ +export namespace SymbolInfoRequest { + export const method = "textDocument/documentSymbol" as const; + export const messageDirection: MessageDirection = MessageDirection.clientToServer; + export const type = new RequestType(method); +} diff --git a/src/sourcekit-lsp/extensions/index.ts b/src/sourcekit-lsp/extensions/index.ts new file mode 100644 index 000000000..93553f663 --- /dev/null +++ b/src/sourcekit-lsp/extensions/index.ts @@ -0,0 +1,23 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// Definitions for non-standard requests used by sourcekit-lsp + +export * from "./GetReferenceDocumentRequest"; +export * from "./GetTestsRequest"; +export * from "./LegacyInlayHintRequest"; +export * from "./PeekDocumentsRequest"; +export * from "./ReIndexProjectRequest"; +export * from "./SourceKitLogMessageNotification"; +export * from "./SymbolInfoRequest"; diff --git a/src/sourcekit-lsp/getReferenceDocument.ts b/src/sourcekit-lsp/getReferenceDocument.ts index b913a18cf..7a29b98be 100644 --- a/src/sourcekit-lsp/getReferenceDocument.ts +++ b/src/sourcekit-lsp/getReferenceDocument.ts @@ -14,7 +14,7 @@ import * as vscode from "vscode"; import * as langclient from "vscode-languageclient/node"; -import { GetReferenceDocumentParams, GetReferenceDocumentRequest } from "./lspExtensions"; +import { GetReferenceDocumentParams, GetReferenceDocumentRequest } from "./extensions"; export function activateGetReferenceDocument(client: langclient.LanguageClient): vscode.Disposable { const getReferenceDocument = vscode.workspace.registerTextDocumentContentProvider( @@ -25,7 +25,11 @@ export function activateGetReferenceDocument(client: langclient.LanguageClient): uri: client.code2ProtocolConverter.asUri(uri), }; - const result = await client.sendRequest(GetReferenceDocumentRequest, params, token); + const result = await client.sendRequest( + GetReferenceDocumentRequest.type, + params, + token + ); if (result) { return result.content; diff --git a/src/sourcekit-lsp/inlayHints.ts b/src/sourcekit-lsp/inlayHints.ts index 400329ee1..c73eebf42 100644 --- a/src/sourcekit-lsp/inlayHints.ts +++ b/src/sourcekit-lsp/inlayHints.ts @@ -16,7 +16,7 @@ import * as vscode from "vscode"; import * as langclient from "vscode-languageclient/node"; import configuration from "../configuration"; import { LanguageClientManager } from "./LanguageClientManager"; -import { legacyInlayHintsRequest } from "./lspExtensions"; +import { LegacyInlayHintRequest } from "./extensions"; /** Provide Inlay Hints using sourcekit-lsp */ class SwiftLegacyInlayHintsProvider implements vscode.InlayHintsProvider { @@ -37,7 +37,7 @@ class SwiftLegacyInlayHintsProvider implements vscode.InlayHintsProvider { textDocument: this.client.code2ProtocolConverter.asTextDocumentIdentifier(document), range: { start: range.start, end: range.end }, }; - const result = this.client.sendRequest(legacyInlayHintsRequest, params, token); + const result = this.client.sendRequest(LegacyInlayHintRequest.type, params, token); return result.then( hints => { return hints.map(hint => { diff --git a/src/sourcekit-lsp/lspExtensions.ts b/src/sourcekit-lsp/lspExtensions.ts deleted file mode 100644 index af5cda62a..000000000 --- a/src/sourcekit-lsp/lspExtensions.ts +++ /dev/null @@ -1,205 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the VS Code Swift open source project -// -// Copyright (c) 2021-2024 the VS Code Swift project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of VS Code Swift project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import * as ls from "vscode-languageserver-protocol"; -import * as langclient from "vscode-languageclient/node"; -import * as vscode from "vscode"; - -// Definitions for non-standard requests used by sourcekit-lsp - -// Peek Documents -export interface PeekDocumentsParams { - /** - * The `DocumentUri` of the text document in which to show the "peeked" editor - */ - uri: langclient.DocumentUri; - - /** - * The `Position` in the given text document in which to show the "peeked editor" - */ - position: vscode.Position; - - /** - * An array `DocumentUri` of the documents to appear inside the "peeked" editor - */ - locations: langclient.DocumentUri[]; -} - -/** - * Response to indicate the `success` of the `PeekDocumentsRequest` - */ -export interface PeekDocumentsResult { - success: boolean; -} - -/** - * Request from the server to the client to show the given documents in a "peeked" editor. - * - * This request is handled by the client to show the given documents in a "peeked" editor (i.e. inline with / inside the editor canvas). - * - * It requires the experimental client capability `"workspace/peekDocuments"` to use. - */ -export const PeekDocumentsRequest = new langclient.RequestType< - PeekDocumentsParams, - PeekDocumentsResult, - unknown ->("workspace/peekDocuments"); - -// Get Reference Document -export interface GetReferenceDocumentParams { - /** - * The `DocumentUri` of the custom scheme url for which content is required - */ - uri: langclient.DocumentUri; -} - -/** - * Response containing `content` of `GetReferenceDocumentRequest` - */ -export interface GetReferenceDocumentResult { - content: string; -} - -/** - * Request from the client to the server asking for contents of a URI having a custom scheme - * For example: "sourcekit-lsp:" - */ -export const GetReferenceDocumentRequest = new langclient.RequestType< - GetReferenceDocumentParams, - GetReferenceDocumentResult, - unknown ->("workspace/getReferenceDocument"); - -// Inlay Hints (pre Swift 5.6) -export interface LegacyInlayHintsParams { - /** - * The text document. - */ - textDocument: langclient.TextDocumentIdentifier; - - /** - * If set, the reange for which inlay hints are - * requested. If unset, hints for the entire document - * are returned. - */ - range?: langclient.Range; - - /** - * The categories of inlay hints that are requested. - * If unset, all categories are returned. - */ - only?: string[]; -} - -export interface LegacyInlayHint { - /** - * The position within the code that this hint is - * attached to. - */ - position: langclient.Position; - - /** - * The hint's kind, used for more flexible client-side - * styling of the hint. - */ - category?: string; - - /** - * The hint's rendered label. - */ - label: string; -} - -export const legacyInlayHintsRequest = new langclient.RequestType< - LegacyInlayHintsParams, - LegacyInlayHint[], - unknown ->("sourcekit-lsp/inlayHints"); - -// Test styles where test-target represents a test target that contains tests -export type TestStyle = "XCTest" | "swift-testing" | "test-target"; - -// Listing tests -export interface LSPTestItem { - /** - * This identifier uniquely identifies the test case or test suite. It can be used to run an individual test (suite). - */ - id: string; - - /** - * Display name describing the test. - */ - label: string; - - /** - * Optional description that appears next to the label. - */ - description?: string; - - /** - * A string that should be used when comparing this item with other items. - * - * When `undefined` the `label` is used. - */ - sortText?: string; - - /** - * Whether the test is disabled. - */ - disabled: boolean; - - /** - * The type of test, eg. the testing framework that was used to declare the test. - */ - style: TestStyle; - - /** - * The location of the test item in the source code. - */ - location: ls.Location; - - /** - * The children of this test item. - * - * For a test suite, this may contain the individual test cases or nested suites. - */ - children: LSPTestItem[]; - - /** - * Tags associated with this test item. - */ - tags: { id: string }[]; -} - -export const workspaceTestsRequest = new langclient.RequestType< - Record, - LSPTestItem[], - unknown ->("workspace/tests"); - -interface DocumentTestsParams { - textDocument: { - uri: ls.URI; - }; -} - -export const textDocumentTestsRequest = new langclient.RequestType< - DocumentTestsParams, - LSPTestItem[], - unknown ->("textDocument/tests"); - -export const reindexProjectRequest = new langclient.RequestType( - "workspace/triggerReindex" -); diff --git a/src/sourcekit-lsp/peekDocuments.ts b/src/sourcekit-lsp/peekDocuments.ts index 26b818176..51bc5a99b 100644 --- a/src/sourcekit-lsp/peekDocuments.ts +++ b/src/sourcekit-lsp/peekDocuments.ts @@ -14,7 +14,7 @@ import * as vscode from "vscode"; import * as langclient from "vscode-languageclient/node"; -import { PeekDocumentsParams, PeekDocumentsRequest } from "./lspExtensions"; +import { PeekDocumentsParams, PeekDocumentsRequest } from "./extensions"; /** * Opens a peeked editor in `uri` at `position` having contents from `locations`. diff --git a/test/integration-tests/testexplorer/LSPTestDiscovery.test.ts b/test/integration-tests/testexplorer/LSPTestDiscovery.test.ts index 88625afd9..48a60ccc4 100644 --- a/test/integration-tests/testexplorer/LSPTestDiscovery.test.ts +++ b/test/integration-tests/testexplorer/LSPTestDiscovery.test.ts @@ -14,34 +14,34 @@ import * as assert from "assert"; import * as vscode from "vscode"; -import * as ls from "vscode-languageserver-protocol"; -import * as p2c from "vscode-languageclient/lib/common/protocolConverter"; import { beforeEach } from "mocha"; -import { InitializeResult, RequestType } from "vscode-languageclient"; +import { + LanguageClient, + MessageSignature, + RequestType0, + RequestType, + Location, + Range, + Position, +} from "vscode-languageclient/node"; +import * as p2c from "vscode-languageclient/lib/common/protocolConverter"; import { LSPTestDiscovery } from "../../../src/TestExplorer/LSPTestDiscovery"; import { SwiftPackage, Target, TargetType } from "../../../src/SwiftPackage"; import { TestClass } from "../../../src/TestExplorer/TestDiscovery"; import { SwiftToolchain } from "../../../src/toolchain/toolchain"; import { LSPTestItem, - textDocumentTestsRequest, - workspaceTestsRequest, -} from "../../../src/sourcekit-lsp/lspExtensions"; + TextDocumentTestsRequest, + WorkspaceTestsRequest, +} from "../../../src/sourcekit-lsp/extensions"; +import { instance, mockFn, mockObject } from "../../MockUtils"; +import { LanguageClientManager } from "../../../src/sourcekit-lsp/LanguageClientManager"; class TestLanguageClient { private responses = new Map(); private responseVersions = new Map(); - - setResponse(type: RequestType, response: R) { - this.responses.set(type.method, response); - } - - setResponseVersion(type: RequestType, version: number) { - this.responseVersions.set(type.method, version); - } - - get initializeResult(): InitializeResult | undefined { - return { + private client = mockObject({ + initializeResult: { capabilities: { experimental: { "textDocument/tests": { @@ -52,15 +52,30 @@ class TestLanguageClient { }, }, }, - }; + }, + protocol2CodeConverter: p2c.createConverter(undefined, true, true), + sendRequest: mockFn(s => + s.callsFake((type: MessageSignature): Promise => { + const response = this.responses.get(type.method); + return response + ? Promise.resolve(response) + : Promise.reject("Method not implemented"); + }) + ), + }); + + public get languageClient(): LanguageClient { + return instance(this.client); } - get protocol2CodeConverter(): p2c.Converter { - return p2c.createConverter(undefined, true, true); + + setResponse(type: RequestType0, response: R): void; + setResponse(type: RequestType, response: R): void; + setResponse(type: MessageSignature, response: unknown) { + this.responses.set(type.method, response); } - sendRequest(type: RequestType): Promise { - const response = this.responses.get(type.method) as R | undefined; - return response ? Promise.resolve(response) : Promise.reject("Method not implemented"); + setResponseVersion(type: MessageSignature, version: number) { + this.responseVersions.set(type.method, version); } } @@ -70,27 +85,37 @@ suite("LSPTestDiscovery Suite", () => { let pkg: SwiftPackage; const file = vscode.Uri.file("file:///some/file.swift"); - beforeEach(async () => { + beforeEach(async function () { + this.timeout(10000000); pkg = await SwiftPackage.create(file, await SwiftToolchain.create()); client = new TestLanguageClient(); - discoverer = new LSPTestDiscovery({ - useLanguageClient(process) { - return process(client, new vscode.CancellationTokenSource().token); - }, - }); + discoverer = new LSPTestDiscovery( + instance( + mockObject({ + useLanguageClient: mockFn(s => + s.callsFake(process => { + return process( + client.languageClient, + new vscode.CancellationTokenSource().token + ); + }) + ), + }) + ) + ); }); suite("Empty responses", () => { - test(textDocumentTestsRequest.method, async () => { - client.setResponse(textDocumentTestsRequest, []); + test(TextDocumentTestsRequest.method, async () => { + client.setResponse(TextDocumentTestsRequest.type, []); const testClasses = await discoverer.getDocumentTests(pkg, file); assert.deepStrictEqual(testClasses, []); }); - test(workspaceTestsRequest.method, async () => { - client.setResponse(workspaceTestsRequest, []); + test(WorkspaceTestsRequest.method, async () => { + client.setResponse(WorkspaceTestsRequest.type, []); const testClasses = await discoverer.getWorkspaceTests(pkg); @@ -99,14 +124,14 @@ suite("LSPTestDiscovery Suite", () => { }); suite("Unsupported LSP version", () => { - test(textDocumentTestsRequest.method, async () => { - client.setResponseVersion(textDocumentTestsRequest, 0); + test(TextDocumentTestsRequest.method, async () => { + client.setResponseVersion(TextDocumentTestsRequest.type, 0); await assert.rejects(() => discoverer.getDocumentTests(pkg, file)); }); - test(workspaceTestsRequest.method, async () => { - client.setResponseVersion(workspaceTestsRequest, 0); + test(WorkspaceTestsRequest.method, async () => { + client.setResponseVersion(WorkspaceTestsRequest.type, 0); await assert.rejects(() => discoverer.getWorkspaceTests(pkg)); }); @@ -140,9 +165,9 @@ suite("LSPTestDiscovery Suite", () => { disabled: false, style: "swift-testing", tags: [], - location: ls.Location.create( + location: Location.create( file.fsPath, - ls.Range.create(ls.Position.create(1, 0), ls.Position.create(2, 0)) + Range.create(Position.create(1, 0), Position.create(2, 0)) ), children: [], }, @@ -150,21 +175,21 @@ suite("LSPTestDiscovery Suite", () => { expected = items.map(item => ({ ...item, - location: client.protocol2CodeConverter.asLocation(item.location), + location: client.languageClient.protocol2CodeConverter.asLocation(item.location), children: [], })); }); - test(textDocumentTestsRequest.method, async () => { - client.setResponse(textDocumentTestsRequest, items); + test(TextDocumentTestsRequest.method, async () => { + client.setResponse(TextDocumentTestsRequest.type, items); const testClasses = await discoverer.getDocumentTests(pkg, file); assert.deepStrictEqual(testClasses, expected); }); - test(workspaceTestsRequest.method, async () => { - client.setResponse(workspaceTestsRequest, items); + test(WorkspaceTestsRequest.method, async () => { + client.setResponse(WorkspaceTestsRequest.type, items); const testClasses = await discoverer.getWorkspaceTests(pkg); @@ -179,7 +204,7 @@ suite("LSPTestDiscovery Suite", () => { style: "XCTest", })); - client.setResponse(workspaceTestsRequest, items); + client.setResponse(WorkspaceTestsRequest.type, items); const testClasses = await discoverer.getWorkspaceTests(pkg); @@ -193,7 +218,7 @@ suite("LSPTestDiscovery Suite", () => { id: `${testTargetName}.topLevelTest()`, })); - client.setResponse(workspaceTestsRequest, items); + client.setResponse(WorkspaceTestsRequest.type, items); const target: Target = { c99name: testTargetName, diff --git a/test/unit-tests/sourcekit-lsp/LanguageClientManager.test.ts b/test/unit-tests/sourcekit-lsp/LanguageClientManager.test.ts index a1e563862..c775d3bc3 100644 --- a/test/unit-tests/sourcekit-lsp/LanguageClientManager.test.ts +++ b/test/unit-tests/sourcekit-lsp/LanguageClientManager.test.ts @@ -31,7 +31,6 @@ import { mockGlobalValue, mockFn, } from "../../MockUtils"; -import * as langClient from "vscode-languageclient/node"; import { Code2ProtocolConverter, DidChangeWorkspaceFoldersNotification, @@ -43,8 +42,10 @@ import { import { LanguageClientManager } from "../../../src/sourcekit-lsp/LanguageClientManager"; import configuration from "../../../src/configuration"; import { FolderContext } from "../../../src/FolderContext"; +import { LanguageClientFactory } from "../../../src/sourcekit-lsp/LanguageClientFactory"; suite("LanguageClientManager Suite", () => { + let languageClientFactoryMock: MockedObject; let languageClientMock: MockedObject; let mockedConverter: MockedObject; let changeStateEmitter: AsyncEventEmitter; @@ -54,7 +55,6 @@ suite("LanguageClientManager Suite", () => { let mockedToolchain: MockedObject; let mockedBuildFlags: MockedObject; - const mockedLangClientModule = mockGlobalModule(langClient); const mockedConfig = mockGlobalModule(configuration); const mockedEnvironment = mockGlobalValue(process, "env"); const mockedLspConfig = mockGlobalObject(configuration, "lsp"); @@ -149,7 +149,9 @@ suite("LanguageClientManager Suite", () => { onDidChangeState: mockFn(s => s.callsFake(changeStateEmitter.event)), }); // `new LanguageClient()` will always return the mocked LanguageClient - mockedLangClientModule.LanguageClient.returns(instance(languageClientMock)); + languageClientFactoryMock = mockObject({ + createLanguageClient: mockFn(s => s.returns(instance(languageClientMock))), + }); // LSP configuration defaults mockedConfig.path = ""; mockedConfig.buildArguments = []; @@ -164,11 +166,11 @@ suite("LanguageClientManager Suite", () => { }); test("launches SourceKit-LSP on startup", async () => { - const sut = new LanguageClientManager(instance(mockedWorkspace)); + const sut = new LanguageClientManager(instance(mockedWorkspace), languageClientFactoryMock); await waitForReturnedPromises(languageClientMock.start); expect(sut.state).to.equal(State.Running); - expect(mockedLangClientModule.LanguageClient).to.have.been.calledOnceWith( + expect(languageClientFactoryMock.createLanguageClient).to.have.been.calledOnceWith( /* id */ match.string, /* name */ match.string, /* serverOptions */ match.has("command", "/path/to/toolchain/bin/sourcekit-lsp"), @@ -198,7 +200,7 @@ suite("LanguageClientManager Suite", () => { }, workspaceContext: instance(mockedWorkspace), }); - new LanguageClientManager(instance(mockedWorkspace)); + new LanguageClientManager(instance(mockedWorkspace), languageClientFactoryMock); await waitForReturnedPromises(languageClientMock.start); // Add the first folder @@ -259,21 +261,21 @@ suite("LanguageClientManager Suite", () => { test("doesn't launch SourceKit-LSP if disabled by the user", async () => { mockedLspConfig.disable = true; - const sut = new LanguageClientManager(instance(mockedWorkspace)); + const sut = new LanguageClientManager(instance(mockedWorkspace), languageClientFactoryMock); await waitForReturnedPromises(languageClientMock.start); expect(sut.state).to.equal(State.Stopped); - expect(mockedLangClientModule.LanguageClient).to.not.have.been.called; + expect(languageClientFactoryMock.createLanguageClient).to.not.have.been.called; expect(languageClientMock.start).to.not.have.been.called; }); test("user can provide a custom SourceKit-LSP executable", async () => { mockedLspConfig.serverPath = "/path/to/my/custom/sourcekit-lsp"; - const sut = new LanguageClientManager(instance(mockedWorkspace)); + const sut = new LanguageClientManager(instance(mockedWorkspace), languageClientFactoryMock); await waitForReturnedPromises(languageClientMock.start); expect(sut.state).to.equal(State.Running); - expect(mockedLangClientModule.LanguageClient).to.have.been.calledOnceWith( + expect(languageClientFactoryMock.createLanguageClient).to.have.been.calledOnceWith( /* id */ match.string, /* name */ match.string, /* serverOptions */ match.has("command", "/path/to/my/custom/sourcekit-lsp"), @@ -312,11 +314,14 @@ suite("LanguageClientManager Suite", () => { }); test("doesn't launch SourceKit-LSP on startup", async () => { - const sut = new LanguageClientManager(instance(mockedWorkspace)); + const sut = new LanguageClientManager( + instance(mockedWorkspace), + languageClientFactoryMock + ); await waitForReturnedPromises(languageClientMock.start); expect(sut.state).to.equal(State.Stopped); - expect(mockedLangClientModule.LanguageClient).to.not.have.been.called; + expect(languageClientFactoryMock.createLanguageClient).to.not.have.been.called; expect(languageClientMock.start).to.not.have.been.called; }); @@ -330,7 +335,10 @@ suite("LanguageClientManager Suite", () => { ), }) ); - const sut = new LanguageClientManager(instance(mockedWorkspace)); + const sut = new LanguageClientManager( + instance(mockedWorkspace), + languageClientFactoryMock + ); await waitForReturnedPromises(languageClientMock.start); // Add the folder to the workspace @@ -341,7 +349,7 @@ suite("LanguageClientManager Suite", () => { }); expect(sut.state).to.equal(State.Running); - expect(mockedLangClientModule.LanguageClient).to.have.been.calledOnceWith( + expect(languageClientFactoryMock.createLanguageClient).to.have.been.calledOnceWith( /* id */ match.string, /* name */ match.string, /* serverOptions */ match.object, @@ -359,7 +367,10 @@ suite("LanguageClientManager Suite", () => { document: instance(mockedTextDocument), }) ); - const sut = new LanguageClientManager(instance(mockedWorkspace)); + const sut = new LanguageClientManager( + instance(mockedWorkspace), + languageClientFactoryMock + ); await waitForReturnedPromises(languageClientMock.start); // Add the first folder to the workspace @@ -379,8 +390,8 @@ suite("LanguageClientManager Suite", () => { }); expect(sut.state).to.equal(State.Running); - expect(mockedLangClientModule.LanguageClient).to.have.been.calledTwice; - expect(mockedLangClientModule.LanguageClient).to.have.been.calledWith( + expect(languageClientFactoryMock.createLanguageClient).to.have.been.calledTwice; + expect(languageClientFactoryMock.createLanguageClient).to.have.been.calledWith( /* id */ match.string, /* name */ match.string, /* serverOptions */ match.object,