From 531528ba60825b117eb1e8eb9da3fdc93e78de1c Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 20 May 2025 08:55:47 -0400 Subject: [PATCH 1/3] Only register common capabilities once --- packages/tailwindcss-language-server/src/tw.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/tailwindcss-language-server/src/tw.ts b/packages/tailwindcss-language-server/src/tw.ts index 882d1be2..8f766f85 100644 --- a/packages/tailwindcss-language-server/src/tw.ts +++ b/packages/tailwindcss-language-server/src/tw.ts @@ -174,6 +174,10 @@ export class TW { } } + if (results.some((result) => result.status === 'fulfilled')) { + await this.updateCommonCapabilities() + } + await this.listenForEvents() } @@ -628,8 +632,6 @@ export class TW { console.log(`[Global] Initializing projects...`) - await this.updateCommonCapabilities() - // init projects for documents that are _already_ open let readyDocuments: string[] = [] let enabledProjectCount = 0 @@ -896,6 +898,7 @@ export class TW { capabilities.add(DidChangeConfigurationNotification.type, undefined) } + this.commonRegistrations?.dispose() this.commonRegistrations = await this.connection.client.register(capabilities) } From effe7934f055cb77a0170c34272f109e78ea3705 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 20 May 2025 09:15:06 -0400 Subject: [PATCH 2/3] Add todo --- packages/tailwindcss-language-server/src/tw.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/tailwindcss-language-server/src/tw.ts b/packages/tailwindcss-language-server/src/tw.ts index 8f766f85..2bc0ad93 100644 --- a/packages/tailwindcss-language-server/src/tw.ts +++ b/packages/tailwindcss-language-server/src/tw.ts @@ -941,6 +941,18 @@ export class TW { this.lastTriggerCharacters = chars + // TODO: This might technically be a race condition if: + // - There are multiple workspace roots + // - There are multiple projects with different separators + // + // Everything up to this point is synchronous including the bailout code + // so it *should* be fine + // + // The proper fix here is to: + // - Refactor workspace folder initialization so discovery, initialization, + // file events, config watchers, etc… are all shared. + // - Remove the need for the "restart" concept in the server for as much as + // possible. Each project should be capable of reloading its modules. this.completionRegistration?.dispose() this.completionRegistration = await this.connection.client.register(CompletionRequest.type, { documentSelector: null, From c7356d4e500a04c27f128553ddc54242b66f55a2 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 20 May 2025 09:39:00 -0400 Subject: [PATCH 3/3] Work around race condition --- .../tailwindcss-language-server/src/tw.ts | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/packages/tailwindcss-language-server/src/tw.ts b/packages/tailwindcss-language-server/src/tw.ts index 2bc0ad93..bc1b3bcc 100644 --- a/packages/tailwindcss-language-server/src/tw.ts +++ b/packages/tailwindcss-language-server/src/tw.ts @@ -910,7 +910,7 @@ export class TW { } private lastTriggerCharacters: Set | undefined - private completionRegistration: Disposable | undefined + private completionRegistration: Promise | undefined private async updateTriggerCharacters() { // If the client does not suppory dynamic registration of completions then // we cannot update the set of trigger characters @@ -941,24 +941,27 @@ export class TW { this.lastTriggerCharacters = chars - // TODO: This might technically be a race condition if: - // - There are multiple workspace roots - // - There are multiple projects with different separators - // - // Everything up to this point is synchronous including the bailout code - // so it *should* be fine + let current = this.completionRegistration + this.completionRegistration = this.connection.client.register(CompletionRequest.type, { + documentSelector: null, + resolveProvider: true, + triggerCharacters: Array.from(chars), + }) + + // NOTE: + // This weird setup works around a race condition where multiple projects + // with different separators update their capabilities at the same time. It + // is extremely unlikely but it could cause `CompletionRequest` to be + // registered more than once with the LSP client. // - // The proper fix here is to: + // We store the promises meaning everything up to this point is synchronous + // so it should be fine but really the proper fix here is to: // - Refactor workspace folder initialization so discovery, initialization, // file events, config watchers, etc… are all shared. // - Remove the need for the "restart" concept in the server for as much as // possible. Each project should be capable of reloading its modules. - this.completionRegistration?.dispose() - this.completionRegistration = await this.connection.client.register(CompletionRequest.type, { - documentSelector: null, - resolveProvider: true, - triggerCharacters: Array.from(chars), - }) + await current?.then((r) => r.dispose()) + await this.completionRegistration } private getProject(document: TextDocumentIdentifier): ProjectService { @@ -1149,7 +1152,7 @@ export class TW { this.commonRegistrations = undefined this.lastTriggerCharacters.clear() - this.completionRegistration?.dispose() + this.completionRegistration?.then((r) => r.dispose()) this.completionRegistration = undefined this.disposables.forEach((d) => d.dispose())