diff --git a/README.md b/README.md index 55fcfdef..d126dcf2 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ Maintained by a [community of contributors](https://github.com/typescript-langua - [Apply Refactoring](#apply-refactoring) - [Organize Imports](#organize-imports) - [Rename File](#rename-file) + - [Send Tsserver Command](#send-tsserver-command) - [Configure plugin](#configure-plugin) - [Code Lenses \(`textDocument/codeLens`\)](#code-lenses-textdocumentcodelens) - [Inlay hints \(`textDocument/inlayHint`\)](#inlay-hints-textdocumentinlayhint) @@ -200,6 +201,35 @@ Most of the time, you'll execute commands with arguments retrieved from another void ``` +#### Send Tsserver Command + +- Request: + ```ts + { + command: `typescript.tsserverRequest` + arguments: [ + string, // command + any, // command arguments in a format that the command expects + ExecuteInfo, // configuration object used for the tsserver request (see below) + ] + } + ``` +- Response: + ```ts + any + ``` + +The `ExecuteInfo` object is defined as follows: + +```ts +type ExecuteInfo = { + executionTarget?: number; // 0 - semantic server, 1 - syntax server; default: 0 + expectsResult?: boolean; // default: true + isAsync?: boolean; // default: false + lowPriority?: boolean; // default: true +}; +``` + #### Configure plugin - Request: diff --git a/src/commands.ts b/src/commands.ts index 6d31682f..c5605222 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -8,6 +8,7 @@ import * as lsp from 'vscode-languageserver'; import { SourceDefinitionCommand } from './features/source-definition.js'; import { TypeScriptVersionSource } from './tsServer/versionProvider.js'; +import { TSServerRequestCommand } from './commands/tsserverRequests.js'; export const Commands = { APPLY_WORKSPACE_EDIT: '_typescript.applyWorkspaceEdit', @@ -20,6 +21,7 @@ export const Commands = { /** Commands below should be implemented by the client */ SELECT_REFACTORING: '_typescript.selectRefactoring', SOURCE_DEFINITION: SourceDefinitionCommand.id, + TS_SERVER_REQUEST: TSServerRequestCommand.id, }; type TypescriptVersionNotificationParams = { diff --git a/src/commands/tsserverRequests.ts b/src/commands/tsserverRequests.ts new file mode 100644 index 00000000..7eddb490 --- /dev/null +++ b/src/commands/tsserverRequests.ts @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2025 TypeFox and others. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + */ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as lsp from 'vscode-languageserver'; +import type { TsClient } from '../ts-client.js'; +import { type ExecuteInfo, type TypeScriptRequestTypes } from '../typescriptService.js'; + +interface RequestArgs { + readonly file?: unknown; +} + +export class TSServerRequestCommand { + public static readonly id = 'typescript.tsserverRequest'; + + public static async execute( + client: TsClient, + command: keyof TypeScriptRequestTypes, + args?: any, + config?: ExecuteInfo, + token?: lsp.CancellationToken, + ): Promise { + if (args && typeof args === 'object' && !Array.isArray(args)) { + const requestArgs = args as RequestArgs; + const hasFile = typeof requestArgs.file === 'string'; + if (hasFile) { + const newArgs = { ...args }; + if (hasFile) { + const document = client.toOpenDocument(requestArgs.file); + if (document) { + newArgs.file = document.filepath; + } + } + args = newArgs; + } + } + + if (config && token && typeof config === 'object' && !Array.isArray(config)) { + config.token = token; + } + + return client.executeCustom(command, args, config); + } +} diff --git a/src/lsp-server.test.ts b/src/lsp-server.test.ts index 6d7fc4d5..f87cd457 100644 --- a/src/lsp-server.test.ts +++ b/src/lsp-server.test.ts @@ -11,8 +11,9 @@ import * as lsp from 'vscode-languageserver'; import { TextDocument } from 'vscode-languageserver-textdocument'; import { uri, createServer, position, lastPosition, filePath, positionAfter, readContents, TestLspServer, openDocumentAndWaitForDiagnostics, range, lastRange } from './test-utils.js'; import { Commands } from './commands.js'; -import { SemicolonPreference } from './ts-protocol.js'; +import { CommandTypes, SemicolonPreference } from './ts-protocol.js'; import { CodeActionKind } from './utils/types.js'; +import { ExecutionTarget } from './tsServer/server.js'; const diagnostics: Map = new Map(); @@ -1850,6 +1851,38 @@ describe('executeCommand', () => { ); }); + it('send custom tsserver command', async () => { + const fooUri = uri('foo.ts'); + const doc = { + uri: fooUri, + languageId: 'typescript', + version: 1, + text: 'export function fn(): void {}\nexport function newFn(): void {}', + }; + await openDocumentAndWaitForDiagnostics(server, doc); + const result = await server.executeCommand({ + command: Commands.TS_SERVER_REQUEST, + arguments: [ + CommandTypes.ProjectInfo, + { + file: filePath('foo.ts'), + needFileNameList: false, + }, + { + executionTarget: ExecutionTarget.Semantic, + expectsResult: true, + isAsync: false, + lowPriority: true, + }, + ], + }); + expect(result).toBeDefined(); + expect(result.body).toMatchObject({ + // tsserver returns non-native path separators on Windows. + configFileName: filePath('tsconfig.json').replace(/\\/g, '/'), + }); + }); + it('go to source definition', async () => { // NOTE: This test needs to reference files that physically exist for the feature to work. const indexUri = uri('source-definition', 'index.ts'); diff --git a/src/lsp-server.ts b/src/lsp-server.ts index ef5907d2..84c23ff3 100644 --- a/src/lsp-server.ts +++ b/src/lsp-server.ts @@ -16,11 +16,13 @@ import { LspDocument } from './document.js'; import { asCompletionItems, asResolvedCompletionItem, CompletionContext, CompletionDataCache, getCompletionTriggerCharacter } from './completion.js'; import { asSignatureHelp, toTsTriggerReason } from './hover.js'; import { Commands, TypescriptVersionNotification } from './commands.js'; +import { TSServerRequestCommand } from './commands/tsserverRequests.js'; import { provideQuickFix } from './quickfix.js'; import { provideRefactors } from './refactor.js'; import { organizeImportsCommands, provideOrganizeImports } from './organize-imports.js'; import { CommandTypes, EventName, OrganizeImportsMode, TypeScriptInitializeParams, TypeScriptInitializationOptions, SupportedFeatures } from './ts-protocol.js'; import type { ts } from './ts-protocol.js'; +import { type TypeScriptRequestTypes, type ExecuteInfo } from './typescriptService.js'; import { collectDocumentSymbols, collectSymbolInformation } from './document-symbol.js'; import { fromProtocolCallHierarchyItem, fromProtocolCallHierarchyIncomingCall, fromProtocolCallHierarchyOutgoingCall } from './features/call-hierarchy.js'; import FileConfigurationManager from './features/fileConfigurationManager.js'; @@ -209,6 +211,7 @@ export class LspServer { Commands.ORGANIZE_IMPORTS, Commands.APPLY_RENAME_FILE, Commands.SOURCE_DEFINITION, + Commands.TS_SERVER_REQUEST, ], }, hoverProvider: true, @@ -934,6 +937,12 @@ export class LspServer { const [uri, position] = (params.arguments || []) as [lsp.DocumentUri?, lsp.Position?]; const reporter = await this.options.lspClient.createProgressReporter(token, workDoneProgress); return SourceDefinitionCommand.execute(uri, position, this.tsClient, this.options.lspClient, reporter, token); + } else if (params.command === Commands.TS_SERVER_REQUEST) { + const [command, args, config] = (params.arguments || []) as [keyof TypeScriptRequestTypes, unknown?, ExecuteInfo?]; + if (typeof command !== 'string') { + throw new Error(`"Command" argument must be a string, got: ${typeof command}`); + } + return TSServerRequestCommand.execute(this.tsClient, command, args, config, token); } else { this.logger.error(`Unknown command ${params.command}.`); } diff --git a/src/ts-client.ts b/src/ts-client.ts index 5b642d88..381e3657 100644 --- a/src/ts-client.ts +++ b/src/ts-client.ts @@ -21,7 +21,7 @@ import * as languageModeIds from './configuration/languageIds.js'; import { CommandTypes, EventName } from './ts-protocol.js'; import type { TypeScriptPlugin, ts } from './ts-protocol.js'; import type { ILogDirectoryProvider } from './tsServer/logDirectoryProvider.js'; -import { AsyncTsServerRequests, ClientCapabilities, ClientCapability, ExecConfig, NoResponseTsServerRequests, ITypeScriptServiceClient, ServerResponse, StandardTsServerRequests, TypeScriptRequestTypes } from './typescriptService.js'; +import { AsyncTsServerRequests, ClientCapabilities, ClientCapability, ExecConfig, NoResponseTsServerRequests, ITypeScriptServiceClient, ServerResponse, StandardTsServerRequests, TypeScriptRequestTypes, ExecuteInfo } from './typescriptService.js'; import { PluginManager } from './tsServer/plugins.js'; import type { ITypeScriptServer, TypeScriptServerExitEvent } from './tsServer/server.js'; import { TypeScriptServerError } from './tsServer/serverError.js'; @@ -530,7 +530,7 @@ export class TsClient implements ITypeScriptServiceClient { command: K, args: AsyncTsServerRequests[K][0], token: CancellationToken, - ): Promise> { + ): Promise> { return this.executeImpl(command, args, { isAsync: true, token, @@ -538,6 +538,24 @@ export class TsClient implements ITypeScriptServiceClient { })[0]!; } + // For use by TSServerRequestCommand. + public executeCustom( + command: K, + args: any, + executeInfo?: ExecuteInfo, + ): Promise> { + const updatedExecuteInfo: ExecuteInfo = { + expectsResult: true, + isAsync: false, + ...executeInfo, + }; + const executions = this.executeImpl(command, args, updatedExecuteInfo); + + return executions[0]!.catch(error => { + throw new ResponseError(1, (error as Error).message); + }); + } + public interruptGetErr(f: () => R): R { return this.documents.interruptGetErr(f); } @@ -558,7 +576,7 @@ export class TsClient implements ITypeScriptServiceClient { // return this._configuration; // } - private executeImpl(command: keyof TypeScriptRequestTypes, args: any, executeInfo: { isAsync: boolean; token?: CancellationToken; expectsResult: boolean; lowPriority?: boolean; requireSemantic?: boolean; }): Array> | undefined> { + private executeImpl(command: keyof TypeScriptRequestTypes, args: any, executeInfo: ExecuteInfo): Array> | undefined> { const serverState = this.serverState; if (serverState.type === ServerState.Type.Running) { return serverState.server.executeImpl(command, args, executeInfo); diff --git a/src/tsServer/server.ts b/src/tsServer/server.ts index 39fdf201..d48c6157 100644 --- a/src/tsServer/server.ts +++ b/src/tsServer/server.ts @@ -50,7 +50,7 @@ export interface ITypeScriptServer { * @return A list of all execute requests. If there are multiple entries, the first item is the primary * request while the rest are secondary ones. */ - executeImpl(command: keyof TypeScriptRequestTypes, args: any, executeInfo: { isAsync: boolean; token?: CancellationToken; expectsResult: boolean; lowPriority?: boolean; executionTarget?: ExecutionTarget; }): Array> | undefined>; + executeImpl(command: keyof TypeScriptRequestTypes, args: any, executeInfo: ExecuteInfo): Array> | undefined>; dispose(): void; } @@ -232,7 +232,7 @@ export class SingleTsServer implements ITypeScriptServer { } } - public executeImpl(command: keyof TypeScriptRequestTypes, args: any, executeInfo: { isAsync: boolean; token?: CancellationToken; expectsResult: boolean; lowPriority?: boolean; executionTarget?: ExecutionTarget; }): Array> | undefined> { + public executeImpl(command: keyof TypeScriptRequestTypes, args: any, executeInfo: ExecuteInfo): Array> | undefined> { const request = this._requestQueue.createRequest(command, args); const requestInfo: RequestItem = { request, @@ -556,7 +556,7 @@ export class SyntaxRoutingTsServer implements ITypeScriptServer { this.semanticServer.kill(); } - public executeImpl(command: keyof TypeScriptRequestTypes, args: any, executeInfo: { isAsync: boolean; token?: CancellationToken; expectsResult: boolean; lowPriority?: boolean; executionTarget?: ExecutionTarget; }): Array> | undefined> { + public executeImpl(command: keyof TypeScriptRequestTypes, args: any, executeInfo: ExecuteInfo): Array> | undefined> { return this.router.execute(command, args, executeInfo); } } diff --git a/src/typescriptService.ts b/src/typescriptService.ts index 465409da..a37c2b0a 100644 --- a/src/typescriptService.ts +++ b/src/typescriptService.ts @@ -41,6 +41,14 @@ export type ExecConfig = { readonly executionTarget?: ExecutionTarget; }; +export type ExecuteInfo = { + readonly executionTarget?: ExecutionTarget; + readonly expectsResult: boolean; + readonly isAsync: boolean; + readonly lowPriority?: boolean; + token?: lsp.CancellationToken; +}; + export enum ClientCapability { /** * Basic syntax server. All clients should support this. pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy