From 3f248f2c01958b7002cfb48cb66ad0f2d2c65780 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Thu, 30 Oct 2025 17:52:17 +0100 Subject: [PATCH 1/3] feat: ask for file-specific formatting options --- README.md | 13 +++++++++++++ src/features/fileConfigurationManager.ts | 20 ++++++++++++++++---- src/lsp-client.ts | 5 +++++ src/lsp-server.ts | 24 ++++++++++++++---------- src/test-utils.ts | 4 ++++ 5 files changed, 52 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index e7f05258..7e938a8d 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ - [Code Lenses \(`textDocument/codeLens`\)](#code-lenses-textdocumentcodelens) - [Inlay hints \(`textDocument/inlayHint`\)](#inlay-hints-textdocumentinlayhint) - [TypeScript Version Notification](#typescript-version-notification) + - [Workspace Configuration request for formatting settings](#workspace-configuration-request-for-formatting-settings) - [Development](#development) - [Build](#build) - [Dev](#dev) @@ -275,6 +276,18 @@ The `$/typescriptVersion` notification params include two properties: - `version` - a semantic version (for example `4.8.4`) - `source` - a string specifying whether used TypeScript version comes from the local workspace (`workspace`), is explicitly specified through a `initializationOptions.tsserver.path` setting (`user-setting`) or was bundled with the server (`bundled`) + +### Workspace Configuration request for formatting settings + +Server asks the client for file-specific configuration options (`tabSize` and `insertSpaces`) that are required by `tsserver` to properly format the file edits when for example using "Organize imports" or performing other file modifications. Those options have to be dynamically provided by the client/editor since the values can differ for each file. For this reason server sends a `workspace/configuration` request with `scopeUri` equal to file's URI and `section` equal to `formattingOptions`. The client is expected to return a configuration that includes the following properties: + +```js +{ + "tabSize": number + "insertSpaces": boolean +} +``` + ## Development ### Build diff --git a/src/features/fileConfigurationManager.ts b/src/features/fileConfigurationManager.ts index 92d7f15a..1e6c5d31 100644 --- a/src/features/fileConfigurationManager.ts +++ b/src/features/fileConfigurationManager.ts @@ -17,6 +17,7 @@ import { CommandTypes, ModuleKind, ScriptTarget, type ts, type TypeScriptInitial import { ITypeScriptServiceClient } from '../typescriptService.js'; import { isTypeScriptDocument } from '../configuration/languageIds.js'; import { LspDocument } from '../document.js'; +import type { LspClient } from '../lsp-client.js'; import API from '../utils/api.js'; import { equals } from '../utils/objects.js'; import { ResourceMap } from '../utils/resourceMap.js'; @@ -147,6 +148,7 @@ export default class FileConfigurationManager { public constructor( private readonly client: ITypeScriptServiceClient, + private readonly lspClient: LspClient, onCaseInsensitiveFileSystem: boolean, ) { this.formatOptions = new ResourceMap(undefined, { onCaseInsensitiveFileSystem }); @@ -208,12 +210,22 @@ export default class FileConfigurationManager { document: LspDocument, token?: lsp.CancellationToken, ): Promise { - return this.ensureConfigurationOptions(document, undefined, token); + const formattingOptions = await this.getFormattingOptions(document); + return this.ensureConfigurationOptions(document, formattingOptions, token); + } + + private async getFormattingOptions(document: LspDocument): Promise> { + const formatConfiguration = await this.lspClient.getWorkspaceConfiguration | undefined>(document.uri.toString(), 'formattingOptions') || {}; + + return { + tabSize: typeof formatConfiguration.tabSize === 'number' ? formatConfiguration.tabSize : undefined, + insertSpaces: typeof formatConfiguration.insertSpaces === 'boolean' ? formatConfiguration.insertSpaces : undefined, + }; } public async ensureConfigurationOptions( document: LspDocument, - options?: lsp.FormattingOptions, + options?: Partial, token?: lsp.CancellationToken, ): Promise { const currentOptions = this.getFileOptions(document, options); @@ -260,7 +272,7 @@ export default class FileConfigurationManager { private getFileOptions( document: LspDocument, - options?: lsp.FormattingOptions, + options?: Partial, ): FileConfiguration { return { formatOptions: this.getFormatOptions(document, options), @@ -270,7 +282,7 @@ export default class FileConfigurationManager { private getFormatOptions( document: LspDocument, - formattingOptions?: lsp.FormattingOptions, + formattingOptions?: Partial, ): ts.server.protocol.FormatCodeSettings { const workspacePreferences = this.getWorkspacePreferencesForFile(document); diff --git a/src/lsp-client.ts b/src/lsp-client.ts index cf14e7fc..d3a5f184 100644 --- a/src/lsp-client.ts +++ b/src/lsp-client.ts @@ -24,6 +24,7 @@ export interface LspClient { applyWorkspaceEdit(args: lsp.ApplyWorkspaceEditParams): Promise; rename(args: lsp.TextDocumentPositionParams): Promise; sendNotification

(type: lsp.NotificationType

, params: P): Promise; + getWorkspaceConfiguration(scopeUri: string, section: string): Promise; } // Hack around the LSP library that makes it otherwise impossible to differentiate between Null and Client-initiated reporter. @@ -66,6 +67,10 @@ export class LspClientImpl implements LspClient { this.connection.sendNotification(lsp.LogMessageNotification.type, args); } + async getWorkspaceConfiguration(scopeUri: string, section: string): Promise { + return await this.connection.workspace.getConfiguration({ scopeUri, section }) as R; + } + async applyWorkspaceEdit(params: lsp.ApplyWorkspaceEditParams): Promise { return this.connection.workspace.applyEdit(params); } diff --git a/src/lsp-server.ts b/src/lsp-server.ts index 13cd8c69..30dc102b 100644 --- a/src/lsp-server.ts +++ b/src/lsp-server.ts @@ -65,7 +65,7 @@ export class LspServer { constructor(private options: LspServerConfiguration) { this.logger = new PrefixingLogger(options.logger, '[lspserver]'); this.tsClient = new TsClient(onCaseInsensitiveFileSystem(), this.logger, options.lspClient); - this.fileConfigurationManager = new FileConfigurationManager(this.tsClient, onCaseInsensitiveFileSystem()); + this.fileConfigurationManager = new FileConfigurationManager(this.tsClient, this.options.lspClient, onCaseInsensitiveFileSystem()); this.commandManager = new CommandManager(); this.diagnosticsManager = new DiagnosticsManager( diagnostics => this.options.lspClient.publishDiagnostics(diagnostics), @@ -816,15 +816,19 @@ export class LspServer { skipDestructiveCodeActions = documentHasErrors; mode = OrganizeImportsMode.SortAndCombine; } - const response = await this.tsClient.interruptGetErr(() => this.tsClient.execute( - CommandTypes.OrganizeImports, - { - scope: { type: 'file', args: fileRangeArgs }, - // Deprecated in 4.9; `mode` takes priority. - skipDestructiveCodeActions, - mode, - }, - token)); + const response = await this.tsClient.interruptGetErr(async () => { + await this.fileConfigurationManager.ensureConfigurationForDocument(document, token); + + return this.tsClient.execute( + CommandTypes.OrganizeImports, + { + scope: { type: 'file', args: fileRangeArgs }, + // Deprecated in 4.9; `mode` takes priority. + skipDestructiveCodeActions, + mode, + }, + token); + }); if (response.type === 'response' && response.body) { actions.push(...provideOrganizeImports(command, response, this.tsClient)); } diff --git a/src/test-utils.ts b/src/test-utils.ts index aaad8098..368cfaa7 100644 --- a/src/test-utils.ts +++ b/src/test-utils.ts @@ -191,6 +191,10 @@ export class TestLspClient implements LspClient { sendNotification

(_type: lsp.NotificationType

, _params: P): Promise { throw new Error('unsupported'); } + + async getWorkspaceConfiguration(_scopeUri: string, _section: string): Promise { + return Promise.resolve(undefined); + } } export class TestLspServer extends LspServer { From d54d3f6e6ed7a024543f931508bea82a9d05638f Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Thu, 30 Oct 2025 17:54:45 +0100 Subject: [PATCH 2/3] markdown --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 7e938a8d..c7220f48 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,7 @@ Most of the time, you'll execute commands with arguments retrieved from another - Request: ```ts { - command: `_typescript.goToSourceDefinition` + command: '_typescript.goToSourceDefinition' arguments: [ lsp.DocumentUri, // String URI of the document lsp.Position, // Line and character position (zero-based) @@ -131,7 +131,7 @@ Most of the time, you'll execute commands with arguments retrieved from another - Request: ```ts { - command: `_typescript.applyRefactoring` + command: '_typescript.applyRefactoring' arguments: [ tsp.GetEditsForRefactorRequestArgs, ] @@ -147,7 +147,7 @@ Most of the time, you'll execute commands with arguments retrieved from another - Request: ```ts { - command: `_typescript.organizeImports` + command: '_typescript.organizeImports' arguments: [ // The "skipDestructiveCodeActions" argument is supported from Typescript 4.4+ [string] | [string, { skipDestructiveCodeActions?: boolean }], @@ -164,7 +164,7 @@ Most of the time, you'll execute commands with arguments retrieved from another - Request: ```ts { - command: `_typescript.applyRenameFile` + command: '_typescript.applyRenameFile' arguments: [ { sourceUri: string; targetUri: string; }, ] @@ -180,7 +180,7 @@ Most of the time, you'll execute commands with arguments retrieved from another - Request: ```ts { - command: `typescript.tsserverRequest` + command: 'typescript.tsserverRequest' arguments: [ string, // command any, // command arguments in a format that the command expects @@ -209,7 +209,7 @@ type ExecuteInfo = { - Request: ```ts { - command: `_typescript.configurePlugin` + command: '_typescript.configurePlugin' arguments: [pluginName: string, configuration: any] } ``` From c0031709ecae81336b1f337de4051e594f68945f Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Thu, 30 Oct 2025 17:57:06 +0100 Subject: [PATCH 3/3] formatting --- README.md | 178 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 98 insertions(+), 80 deletions(-) diff --git a/README.md b/README.md index c7220f48..ab12c490 100644 --- a/README.md +++ b/README.md @@ -109,89 +109,104 @@ Most of the time, you'll execute commands with arguments retrieved from another #### Go to Source Definition -- Request: - ```ts - { - command: '_typescript.goToSourceDefinition' - arguments: [ - lsp.DocumentUri, // String URI of the document - lsp.Position, // Line and character position (zero-based) - ] - } - ``` -- Response: - ```ts - lsp.Location[] | null - ``` +Request: + +```ts +{ + command: '_typescript.goToSourceDefinition' + arguments: [ + lsp.DocumentUri, // String URI of the document + lsp.Position, // Line and character position (zero-based) + ] +} +``` + +Response: + +```ts +lsp.Location[] | null +``` (This command is supported from Typescript 4.7.) #### Apply Refactoring -- Request: - ```ts - { - command: '_typescript.applyRefactoring' - arguments: [ - tsp.GetEditsForRefactorRequestArgs, - ] - } - ``` -- Response: - ```ts - void - ``` +Request: + +```ts +{ + command: '_typescript.applyRefactoring' + arguments: [ + tsp.GetEditsForRefactorRequestArgs, + ] +} +``` + +Response: + +```ts +void +``` #### Organize Imports -- Request: - ```ts - { - command: '_typescript.organizeImports' - arguments: [ - // The "skipDestructiveCodeActions" argument is supported from Typescript 4.4+ - [string] | [string, { skipDestructiveCodeActions?: boolean }], - ] - } - ``` -- Response: - ```ts - void - ``` +Request: + +```ts +{ + command: '_typescript.organizeImports' + arguments: [ + // The "skipDestructiveCodeActions" argument is supported from Typescript 4.4+ + [string] | [string, { skipDestructiveCodeActions?: boolean }], + ] +} +``` + +Response: + +```ts +void +``` #### Rename File -- Request: - ```ts - { - command: '_typescript.applyRenameFile' - arguments: [ - { sourceUri: string; targetUri: string; }, - ] - } - ``` -- Response: - ```ts - void - ``` +Request: + +```ts +{ + command: '_typescript.applyRenameFile' + arguments: [ + { sourceUri: string; targetUri: string; }, + ] +} +``` + +Response: + +```ts +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 - ``` +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: @@ -206,17 +221,20 @@ type ExecuteInfo = { #### Configure plugin -- Request: - ```ts - { - command: '_typescript.configurePlugin' - arguments: [pluginName: string, configuration: any] - } - ``` -- Response: - ```ts - void - ``` +Request: + +```ts +{ + command: '_typescript.configurePlugin' + arguments: [pluginName: string, configuration: any] +} +``` + +Response: + +```ts +void +``` ### Code Lenses (`textDocument/codeLens`)