diff --git a/README.md b/README.md index b3c1301e..9e6293c9 100644 --- a/README.md +++ b/README.md @@ -305,7 +305,7 @@ The `$/typescriptVersion` notification params include two properties: ### 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 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: +Server asks the client (provided client supports `workspace/configuration` capability) for file-specific configuration options (`tabSize` and `insertSpaces`) that are required by `tsserver` to properly format 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 { diff --git a/src/features/fileConfigurationManager.ts b/src/features/fileConfigurationManager.ts index 02bf5bb3..69e4de17 100644 --- a/src/features/fileConfigurationManager.ts +++ b/src/features/fileConfigurationManager.ts @@ -11,9 +11,9 @@ import path from 'node:path'; import deepmerge from 'deepmerge'; -import type lsp from 'vscode-languageserver'; +import lsp from 'vscode-languageserver'; import { URI } from 'vscode-uri'; -import { CommandTypes, ModuleKind, ScriptTarget, type ts, type TypeScriptInitializationOptions } from '../ts-protocol.js'; +import { CommandTypes, ModuleKind, ScriptTarget, type ts, type TypeScriptInitializationOptions, SupportedFeatures } from '../ts-protocol.js'; import { ITypeScriptServiceClient } from '../typescriptService.js'; import { isTypeScriptDocument } from '../configuration/languageIds.js'; import { LspDocument } from '../document.js'; @@ -144,14 +144,29 @@ function areFileConfigurationsEqual(a: FileConfiguration, b: FileConfiguration): export default class FileConfigurationManager { public tsPreferences: Required = deepmerge({}, DEFAULT_TSSERVER_PREFERENCES); public workspaceConfiguration: WorkspaceConfiguration = deepmerge({}, DEFAULT_WORKSPACE_CONFIGURATION); - private readonly formatOptions: ResourceMap>; + private readonly fileOptionsCache: ResourceMap>; + private readonly formatOptionsCache: ResourceMap>; + private readonly initialConfigurationRequestsMap: ResourceMap; public constructor( private readonly client: ITypeScriptServiceClient, private readonly lspClient: LspClient, + private readonly features: SupportedFeatures, onCaseInsensitiveFileSystem: boolean, ) { - this.formatOptions = new ResourceMap(undefined, { onCaseInsensitiveFileSystem }); + this.fileOptionsCache = new ResourceMap(undefined, { onCaseInsensitiveFileSystem }); + this.formatOptionsCache = new ResourceMap(undefined, { onCaseInsensitiveFileSystem }); + this.initialConfigurationRequestsMap = new ResourceMap(undefined, { onCaseInsensitiveFileSystem }); + } + + public onDidOpenTextDocument(document: LspDocument): void { + const cancellation = new lsp.CancellationTokenSource(); + this.initialConfigurationRequestsMap.set(document.uri, cancellation); + this.ensureConfigurationForDocument(document, cancellation.token) + .then(() => { + this.initialConfigurationRequestsMap.delete(document.uri); + }) + .catch(() => {}); } public onDidCloseTextDocument(documentUri: URI): void { @@ -159,7 +174,8 @@ export default class FileConfigurationManager { // This is necessary since the tsserver now closed a project when its // last file in it closes which drops the stored formatting options // as well. - this.formatOptions.delete(documentUri); + this.fileOptionsCache.delete(documentUri); + this.initialConfigurationRequestsMap.get(documentUri)?.cancel(); } public mergeTsPreferences(preferences: ts.server.protocol.UserPreferences): void { @@ -215,7 +231,16 @@ export default class FileConfigurationManager { } private async getFormattingOptions(document: LspDocument): Promise> { - const formatConfiguration = await this.lspClient.getWorkspaceConfiguration | undefined>(document.uri.toString(), 'formattingOptions') || {}; + const formatOptionsCached = this.formatOptionsCache.get(document.uri); + if (formatOptionsCached) { + return formatOptionsCached; + } + + const formatConfiguration = + this.features.workspaceConfigurationSuppport + ? await this.lspClient.getWorkspaceConfiguration | undefined>(document.uri.toString(), 'formattingOptions') || {} + : {}; + const options: Partial = {}; if (typeof formatConfiguration.tabSize === 'number') { @@ -225,6 +250,7 @@ export default class FileConfigurationManager { options.insertSpaces = formatConfiguration.insertSpaces; } + this.formatOptionsCache.set(document.uri, options); return options; } @@ -234,7 +260,7 @@ export default class FileConfigurationManager { token?: lsp.CancellationToken, ): Promise { const currentOptions = this.getFileOptions(document, options); - const cachedOptions = this.formatOptions.get(document.uri); + const cachedOptions = this.fileOptionsCache.get(document.uri); if (cachedOptions) { const cachedOptionsValue = await cachedOptions; if (token?.isCancellationRequested) { @@ -255,7 +281,7 @@ export default class FileConfigurationManager { } })(); - this.formatOptions.set(document.uri, task); + this.fileOptionsCache.set(document.uri, task); await task; } @@ -272,7 +298,9 @@ export default class FileConfigurationManager { } public reset(): void { - this.formatOptions.clear(); + this.fileOptionsCache.clear(); + this.formatOptionsCache.clear(); + this.initialConfigurationRequestsMap.clear(); } private getFileOptions( diff --git a/src/lsp-server.ts b/src/lsp-server.ts index 9ed2fbe7..19d55813 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, this.options.lspClient, onCaseInsensitiveFileSystem()); + this.fileConfigurationManager = new FileConfigurationManager(this.tsClient, this.options.lspClient, this.features, onCaseInsensitiveFileSystem()); this.commandManager = new CommandManager(); this.diagnosticsManager = new DiagnosticsManager( diagnostics => this.options.lspClient.publishDiagnostics(diagnostics), @@ -116,7 +116,7 @@ export class LspServer { this.features.moveToFileCodeActionSupport = userInitializationOptions.supportsMoveToFileCodeAction && typescriptVersion.version?.gte(API.v520); - const { textDocument } = clientCapabilities; + const { textDocument, workspace } = clientCapabilities; if (textDocument) { const { codeAction, completion, definition, publishDiagnostics } = textDocument; if (codeAction) { @@ -140,6 +140,9 @@ export class LspServer { this.features.diagnosticsSupport = Boolean(publishDiagnostics); this.features.diagnosticsTagSupport = Boolean(publishDiagnostics?.tagSupport); } + if (workspace?.configuration) { + this.features.workspaceConfigurationSuppport = true; + } this.fileConfigurationManager.mergeTsPreferences({ useLabelDetailsInCompletionEntries: this.features.completionLabelDetails, @@ -369,12 +372,19 @@ export class LspServer { } didOpenTextDocument(params: lsp.DidOpenTextDocumentParams): void { - if (this.tsClient.toOpenDocument(params.textDocument.uri, { suppressAlertOnFailure: true })) { - throw new Error(`Can't open already open document: ${params.textDocument.uri}`); + const { uri, languageId } = params.textDocument; + + if (this.tsClient.toOpenDocument(uri, { suppressAlertOnFailure: true })) { + throw new Error(`Can't open already open document: ${uri}`); } if (!this.tsClient.openTextDocument(params.textDocument)) { - throw new Error(`Cannot open document '${params.textDocument.uri}' (languageId: ${params.textDocument.languageId}).`); + throw new Error(`Cannot open document '${uri}' (languageId: ${languageId}).`); + } + + const document = this.tsClient.toOpenDocument(uri); + if (document) { + this.fileConfigurationManager.onDidOpenTextDocument(document); } } diff --git a/src/ts-protocol.ts b/src/ts-protocol.ts index 7de7145d..a7f382a8 100644 --- a/src/ts-protocol.ts +++ b/src/ts-protocol.ts @@ -355,6 +355,7 @@ export interface SupportedFeatures { diagnosticsSupport?: boolean; diagnosticsTagSupport?: boolean; moveToFileCodeActionSupport?: boolean; + workspaceConfigurationSuppport?: boolean; } export interface TypeScriptPlugin {