Skip to content

feat: add support for typescript.tsserverRequest command #967

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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 = {
Expand Down
51 changes: 51 additions & 0 deletions src/commands/tsserverRequests.ts
Original file line number Diff line number Diff line change
@@ -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<unknown> {
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);
}
}
35 changes: 34 additions & 1 deletion src/lsp-server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, lsp.PublishDiagnosticsParams> = new Map();

Expand Down Expand Up @@ -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');
Expand Down
9 changes: 9 additions & 0 deletions src/lsp-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -209,6 +211,7 @@ export class LspServer {
Commands.ORGANIZE_IMPORTS,
Commands.APPLY_RENAME_FILE,
Commands.SOURCE_DEFINITION,
Commands.TS_SERVER_REQUEST,
],
},
hoverProvider: true,
Expand Down Expand Up @@ -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}.`);
}
Expand Down
24 changes: 21 additions & 3 deletions src/ts-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -530,14 +530,32 @@ export class TsClient implements ITypeScriptServiceClient {
command: K,
args: AsyncTsServerRequests[K][0],
token: CancellationToken,
): Promise<ServerResponse.Response<ts.server.protocol.Response>> {
): Promise<ServerResponse.Response<AsyncTsServerRequests[K][1]>> {
return this.executeImpl(command, args, {
isAsync: true,
token,
expectsResult: true,
})[0]!;
}

// For use by TSServerRequestCommand.
public executeCustom<K extends keyof TypeScriptRequestTypes>(
command: K,
args: any,
executeInfo?: ExecuteInfo,
): Promise<ServerResponse.Response<ts.server.protocol.Response>> {
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<R>(f: () => R): R {
return this.documents.interruptGetErr(f);
}
Expand All @@ -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<Promise<ServerResponse.Response<ts.server.protocol.Response>> | undefined> {
private executeImpl(command: keyof TypeScriptRequestTypes, args: any, executeInfo: ExecuteInfo): Array<Promise<ServerResponse.Response<ts.server.protocol.Response>> | undefined> {
const serverState = this.serverState;
if (serverState.type === ServerState.Type.Running) {
return serverState.server.executeImpl(command, args, executeInfo);
Expand Down
6 changes: 3 additions & 3 deletions src/tsServer/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Promise<ServerResponse.Response<ts.server.protocol.Response>> | undefined>;
executeImpl(command: keyof TypeScriptRequestTypes, args: any, executeInfo: ExecuteInfo): Array<Promise<ServerResponse.Response<ts.server.protocol.Response>> | undefined>;

dispose(): void;
}
Expand Down Expand Up @@ -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<Promise<ServerResponse.Response<ts.server.protocol.Response>> | undefined> {
public executeImpl(command: keyof TypeScriptRequestTypes, args: any, executeInfo: ExecuteInfo): Array<Promise<ServerResponse.Response<ts.server.protocol.Response>> | undefined> {
const request = this._requestQueue.createRequest(command, args);
const requestInfo: RequestItem = {
request,
Expand Down Expand Up @@ -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<Promise<ServerResponse.Response<ts.server.protocol.Response>> | undefined> {
public executeImpl(command: keyof TypeScriptRequestTypes, args: any, executeInfo: ExecuteInfo): Array<Promise<ServerResponse.Response<ts.server.protocol.Response>> | undefined> {
return this.router.execute(command, args, executeInfo);
}
}
Expand Down
8 changes: 8 additions & 0 deletions src/typescriptService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading
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