From 52c2bc2799f0a9c448e5412fd2168ba54c41ae91 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Sat, 19 Apr 2025 16:55:18 -0400 Subject: [PATCH 1/8] Rework capability registration and handler setup - Dynamic registrations are honored on a per-capability basis rather than only when the client supports several capabilities being dynamically registered - Most capabilities are registered _once_ after project discovery and only re-registered after a server restart - The completion capability will not re-register itself if trigger characters have not changed --- .../tailwindcss-language-server/src/tw.ts | 203 +++++++++++------- 1 file changed, 123 insertions(+), 80 deletions(-) diff --git a/packages/tailwindcss-language-server/src/tw.ts b/packages/tailwindcss-language-server/src/tw.ts index 20ac0158..04050e0b 100644 --- a/packages/tailwindcss-language-server/src/tw.ts +++ b/packages/tailwindcss-language-server/src/tw.ts @@ -21,6 +21,8 @@ import type { WorkspaceFolder, CodeLensParams, CodeLens, + ServerCapabilities, + ClientCapabilities, } from 'vscode-languageserver/node' import { CompletionRequest, @@ -624,6 +626,8 @@ 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 @@ -640,8 +644,6 @@ export class TW { console.log(`[Global] Initialized ${enabledProjectCount} projects`) - this.setupLSPHandlers() - this.disposables.push( this.connection.onDidChangeConfiguration(async ({ settings }) => { let previousExclude = globalSettings.tailwindCSS.files.exclude @@ -763,7 +765,7 @@ export class TW { this.connection, params, this.documentService, - () => this.updateCapabilities(), + () => this.updateProjectCapabilities(), () => { for (let document of this.documentService.getAllDocuments()) { let project = this.getProject(document) @@ -810,9 +812,7 @@ export class TW { } setupLSPHandlers() { - if (this.lspHandlersAdded) { - return - } + if (this.lspHandlersAdded) return this.lspHandlersAdded = true this.connection.onHover(this.onHover.bind(this)) @@ -858,43 +858,84 @@ export class TW { } } - private updateCapabilities() { - if (!supportsDynamicRegistration(this.initializeParams)) { - this.connection.client.register(DidChangeConfigurationNotification.type, undefined) - return + // Common capabilities are always supported by the language server and do not + // require any project-specific information to know how to configure them. + // + // These capabilities will stay valid until/unless the server has to restart + // in which case they'll be unregistered and then re-registered once project + // discovery has completed + private commonRegistrations: BulkUnregistration | undefined + private async updateCommonCapabilities() { + let capabilities = BulkRegistration.create() + + let client = this.initializeParams.capabilities + + if (client.textDocument?.hover?.dynamicRegistration) { + capabilities.add(HoverRequest.type, { documentSelector: null }) } - if (this.registrations) { - this.registrations.then((r) => r.dispose()) + if (client.textDocument?.colorProvider?.dynamicRegistration) { + capabilities.add(DocumentColorRequest.type, { documentSelector: null }) } - let projects = Array.from(this.projects.values()) + if (client.textDocument?.codeAction?.dynamicRegistration) { + capabilities.add(CodeActionRequest.type, { documentSelector: null }) + } - let capabilities = BulkRegistration.create() + if (client.textDocument?.codeLens?.dynamicRegistration) { + capabilities.add(CodeLensRequest.type, { documentSelector: null }) + } + + if (client.textDocument?.documentLink?.dynamicRegistration) { + capabilities.add(DocumentLinkRequest.type, { documentSelector: null }) + } + + if (client.workspace?.didChangeConfiguration?.dynamicRegistration) { + capabilities.add(DidChangeConfigurationNotification.type, undefined) + } - // TODO: We should *not* be re-registering these capabilities - // IDEA: These should probably be static registrations up front - capabilities.add(HoverRequest.type, { documentSelector: null }) - capabilities.add(DocumentColorRequest.type, { documentSelector: null }) - capabilities.add(CodeActionRequest.type, { documentSelector: null }) - capabilities.add(CodeLensRequest.type, { documentSelector: null }) - capabilities.add(DocumentLinkRequest.type, { documentSelector: null }) - capabilities.add(DidChangeConfigurationNotification.type, undefined) - - // TODO: Only re-register this if trigger characters change - capabilities.add(CompletionRequest.type, { + this.commonRegistrations = await this.connection.client.register(capabilities) + } + + // These capabilities depend on the projects we've found to appropriately + // configure them. This may mean collecting information from all discovered + // projects to determine what we can do and how + private updateProjectCapabilities() { + this.updateTriggerCharacters() + } + + private lastTriggerCharacters: Set | undefined + private completionRegistration: Disposable | undefined + private async updateTriggerCharacters() { + // If the client does not suppory dynamic registration of completions then + // we cannot update the set of trigger characters + let client = this.initializeParams.capabilities + if (client.textDocument?.completion?.dynamicRegistration) return + + // The new set of trigger characters is all the static ones plus + // any characters from any separator in v3 config + let chars = new Set(TRIGGER_CHARACTERS) + + for (let project of this.projects.values()) { + let sep = project.state.separator + if (typeof sep !== 'string') continue + + sep = sep.slice(-1) + if (!sep) continue + + chars.add(sep) + } + + // If the trigger characters haven't changed then we don't need to do anything + if (equal(Array.from(chars), Array.from(this.lastTriggerCharacters))) return + this.lastTriggerCharacters = chars + + this.completionRegistration.dispose() + this.completionRegistration = await this.connection.client.register(CompletionRequest.type, { documentSelector: null, resolveProvider: true, - triggerCharacters: [ - ...TRIGGER_CHARACTERS, - ...projects - .map((project) => project.state.separator) - .filter((sep) => typeof sep === 'string') - .map((sep) => sep.slice(-1)), - ].filter(Boolean), + triggerCharacters: Array.from(chars), }) - - this.registrations = this.connection.client.register(capabilities) } private getProject(document: TextDocumentIdentifier): ProjectService { @@ -1016,47 +1057,58 @@ export class TW { this.connection.onInitialize(async (params: InitializeParams): Promise => { this.initializeParams = params - if (supportsDynamicRegistration(params)) { - return { - capabilities: { - textDocumentSync: TextDocumentSyncKind.Full, - workspace: { - workspaceFolders: { - changeNotifications: true, - }, - }, - }, - } - } - this.setupLSPHandlers() return { - capabilities: { - textDocumentSync: TextDocumentSyncKind.Full, - hoverProvider: true, - colorProvider: true, - codeActionProvider: true, - codeLensProvider: { - resolveProvider: false, - }, - documentLinkProvider: {}, - completionProvider: { - resolveProvider: true, - triggerCharacters: [...TRIGGER_CHARACTERS, ':'], - }, - workspace: { - workspaceFolders: { - changeNotifications: true, - }, - }, - }, + capabilities: this.computeServerCapabilities(params.capabilities), } }) this.connection.onInitialized(() => this.init()) } + computeServerCapabilities(client: ClientCapabilities) { + let capabilities: ServerCapabilities = { + textDocumentSync: TextDocumentSyncKind.Full, + workspace: { + workspaceFolders: { + changeNotifications: true, + }, + }, + } + + if (!client.textDocument?.hover?.dynamicRegistration) { + capabilities.hoverProvider = true + } + + if (!client.textDocument?.colorProvider?.dynamicRegistration) { + capabilities.colorProvider = true + } + + if (!client.textDocument?.codeAction?.dynamicRegistration) { + capabilities.codeActionProvider = true + } + + if (!client.textDocument?.codeLens?.dynamicRegistration) { + capabilities.codeLensProvider = { + resolveProvider: false, + } + } + + if (!client.textDocument?.completion?.dynamicRegistration) { + capabilities.completionProvider = { + resolveProvider: true, + triggerCharacters: [...TRIGGER_CHARACTERS, ':'], + } + } + + if (!client.textDocument?.documentLink?.dynamicRegistration) { + capabilities.documentLinkProvider = {} + } + + return capabilities + } + listen() { this.connection.listen() } @@ -1070,10 +1122,11 @@ export class TW { this.refreshDiagnostics() - if (this.registrations) { - this.registrations.then((r) => r.dispose()) - this.registrations = undefined - } + this.commonRegistrations?.dispose() + this.commonRegistrations = undefined + + this.completionRegistration?.dispose() + this.completionRegistration = undefined this.disposables.forEach((d) => d.dispose()) this.disposables.length = 0 @@ -1106,13 +1159,3 @@ export class TW { } } } - -function supportsDynamicRegistration(params: InitializeParams): boolean { - return ( - params.capabilities.textDocument?.hover?.dynamicRegistration && - params.capabilities.textDocument?.colorProvider?.dynamicRegistration && - params.capabilities.textDocument?.codeAction?.dynamicRegistration && - params.capabilities.textDocument?.completion?.dynamicRegistration && - params.capabilities.textDocument?.documentLink?.dynamicRegistration - ) -} From bdff89061e4b385da435dbd3f0e63db6390f5e55 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Fri, 25 Apr 2025 11:02:58 -0400 Subject: [PATCH 2/8] Keep track of registered capabilities in tests --- .../tests/utils/client.ts | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/packages/tailwindcss-language-server/tests/utils/client.ts b/packages/tailwindcss-language-server/tests/utils/client.ts index 760b805b..a1074a52 100644 --- a/packages/tailwindcss-language-server/tests/utils/client.ts +++ b/packages/tailwindcss-language-server/tests/utils/client.ts @@ -15,7 +15,9 @@ import { NotificationHandler, ProtocolConnection, PublishDiagnosticsParams, + Registration, SymbolInformation, + UnregistrationRequest, WorkspaceFolder, } from 'vscode-languageserver' import type { Position } from 'vscode-languageserver-textdocument' @@ -191,6 +193,16 @@ export interface Client extends ClientWorkspace { */ readonly conn: ProtocolConnection + /** + * Get the currently registered server capabilities + */ + serverCapabilities: Registration[] + + /** + * Get the currently registered server capabilities + */ + onServerCapabilitiesChanged(cb: () => void): void + /** * Get a workspace by name */ @@ -428,12 +440,40 @@ export async function createClient(opts: ClientOptions): Promise { }) } + let serverCapabilityChangeCallbacks: (() => void)[] = [] + + function onServerCapabilitiesChanged(cb: () => void) { + serverCapabilityChangeCallbacks.push(cb) + } + + let registeredCapabilities: Registration[] = [] + conn.onRequest(RegistrationRequest.type, ({ registrations }) => { trace('Registering capabilities') for (let registration of registrations) { + registeredCapabilities.push(registration) trace('-', registration.method) } + + for (let cb of serverCapabilityChangeCallbacks) cb() + }) + + conn.onRequest(UnregistrationRequest.type, ({ unregisterations }) => { + trace('Unregistering capabilities') + + let idsToRemove = new Set() + + for (let registration of unregisterations) { + idsToRemove.add(registration.id) + trace('-', registration.method) + } + + registeredCapabilities = registeredCapabilities.filter( + (capability) => !idsToRemove.has(capability.id), + ) + + for (let cb of serverCapabilityChangeCallbacks) cb() }) // TODO: Remove this its a hack @@ -495,6 +535,10 @@ export async function createClient(opts: ClientOptions): Promise { return { ...clientWorkspaces[0], + get serverCapabilities() { + return registeredCapabilities + }, + onServerCapabilitiesChanged, workspace, updateSettings, } From 89492617190846a4ad191a2af739252bb72092ca Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Fri, 25 Apr 2025 11:22:01 -0400 Subject: [PATCH 3/8] Allow the client to notify the server that files have changed --- .../tests/utils/client.ts | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/packages/tailwindcss-language-server/tests/utils/client.ts b/packages/tailwindcss-language-server/tests/utils/client.ts index a1074a52..681843a8 100644 --- a/packages/tailwindcss-language-server/tests/utils/client.ts +++ b/packages/tailwindcss-language-server/tests/utils/client.ts @@ -6,11 +6,14 @@ import { CompletionList, CompletionParams, Diagnostic, + DidChangeWatchedFilesNotification, Disposable, DocumentLink, DocumentLinkRequest, DocumentSymbol, DocumentSymbolRequest, + FileChangeType, + FileEvent, Hover, NotificationHandler, ProtocolConnection, @@ -85,6 +88,12 @@ export interface DocumentDescriptor { settings?: Settings } +export interface ChangedFiles { + created?: string[] + changed?: string[] + deleted?: string[] +} + export interface ClientDocument { /** * The URI to the document @@ -203,6 +212,11 @@ export interface Client extends ClientWorkspace { */ onServerCapabilitiesChanged(cb: () => void): void + /** + * Tell the server that files on disk have changed + */ + notifyChangedFiles(changes: ChangedFiles): Promise + /** * Get a workspace by name */ @@ -533,12 +547,33 @@ export async function createClient(opts: ClientOptions): Promise { await initPromise } + function notifyChangedFiles(changes: ChangedFiles) { + let events: FileEvent[] = [] + + for (const path of changes?.created ?? []) { + events.push({ uri: URI.file(path).toString(), type: FileChangeType.Created }) + } + + for (const path of changes?.changed ?? []) { + events.push({ uri: URI.file(path).toString(), type: FileChangeType.Changed }) + } + + for (const path of changes?.deleted ?? []) { + events.push({ uri: URI.file(path).toString(), type: FileChangeType.Deleted }) + } + + return conn.sendNotification(DidChangeWatchedFilesNotification.type, { + changes: events, + }) + } + return { ...clientWorkspaces[0], get serverCapabilities() { return registeredCapabilities }, onServerCapabilitiesChanged, + notifyChangedFiles, workspace, updateSettings, } From 80269eb2c70f57a293aee3bdb26b34bde2a6321a Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Fri, 25 Apr 2025 11:22:31 -0400 Subject: [PATCH 4/8] Add test to verify changing that changing the separator changes trigger characters --- .../tests/env/capabilities.test.ts | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 packages/tailwindcss-language-server/tests/env/capabilities.test.ts diff --git a/packages/tailwindcss-language-server/tests/env/capabilities.test.ts b/packages/tailwindcss-language-server/tests/env/capabilities.test.ts new file mode 100644 index 00000000..46838981 --- /dev/null +++ b/packages/tailwindcss-language-server/tests/env/capabilities.test.ts @@ -0,0 +1,105 @@ +import { expect } from 'vitest' +import { defineTest, js } from '../../src/testing' +import { createClient } from '../utils/client' +import * as fs from 'node:fs/promises' + +defineTest({ + name: 'Changing the separator registers new trigger characters', + fs: { + 'tailwind.config.js': js` + module.exports = { + separator: ':', + } + `, + }, + prepare: async ({ root }) => ({ client: await createClient({ root }) }), + handle: async ({ root, client }) => { + // Initially don't have any registered capabilities because dynamic + // registration is delayed until after project initialization + expect(client.serverCapabilities).toEqual([]) + + // We open a document so a project gets initialized + await client.open({ + lang: 'html', + text: '
', + }) + + // And now capabilities are registered + expect(client.serverCapabilities).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + method: 'textDocument/hover', + }), + + expect.objectContaining({ + method: 'textDocument/completion', + registerOptions: { + documentSelector: null, + resolveProvider: true, + triggerCharacters: ['"', "'", '`', ' ', '.', '(', '[', ']', '!', '/', '-', ':'], + }, + }), + ]), + ) + + let countBeforeChange = client.serverCapabilities.length + let capabilitiesDidChange = Promise.race([ + new Promise((_, reject) => { + setTimeout(() => reject('capabilities did not change within 5s'), 5_000) + }), + + new Promise((resolve) => { + client.onServerCapabilitiesChanged(() => { + if (client.serverCapabilities.length !== countBeforeChange) return + resolve() + }) + }), + ]) + + await fs.writeFile( + `${root}/tailwind.config.js`, + js` + module.exports = { + separator: '_', + } + `, + ) + + // After changing the config + client.notifyChangedFiles({ + changed: [`${root}/tailwind.config.js`], + }) + + // We should see that the capabilities have changed + await capabilitiesDidChange + + // Capabilities are now registered + expect(client.serverCapabilities).toContainEqual( + expect.objectContaining({ + method: 'textDocument/hover', + }), + ) + + expect(client.serverCapabilities).toContainEqual( + expect.objectContaining({ + method: 'textDocument/completion', + registerOptions: { + documentSelector: null, + resolveProvider: true, + triggerCharacters: ['"', "'", '`', ' ', '.', '(', '[', ']', '!', '/', '-', '_'], + }, + }), + ) + + expect(client.serverCapabilities).not.toContainEqual( + expect.objectContaining({ + method: 'textDocument/completion', + registerOptions: { + documentSelector: null, + resolveProvider: true, + triggerCharacters: ['"', "'", '`', ' ', '.', '(', '[', ']', '!', '/', '-', ':'], + }, + }), + ) + }, +}) From 0b77142079e44302252f0129f56a66743baf8695 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Fri, 25 Apr 2025 11:22:35 -0400 Subject: [PATCH 5/8] Fix errors --- packages/tailwindcss-language-server/src/tw.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/tailwindcss-language-server/src/tw.ts b/packages/tailwindcss-language-server/src/tw.ts index 04050e0b..792c887d 100644 --- a/packages/tailwindcss-language-server/src/tw.ts +++ b/packages/tailwindcss-language-server/src/tw.ts @@ -910,7 +910,7 @@ export class TW { // If the client does not suppory dynamic registration of completions then // we cannot update the set of trigger characters let client = this.initializeParams.capabilities - if (client.textDocument?.completion?.dynamicRegistration) return + if (!client.textDocument?.completion?.dynamicRegistration) return // The new set of trigger characters is all the static ones plus // any characters from any separator in v3 config @@ -927,10 +927,10 @@ export class TW { } // If the trigger characters haven't changed then we don't need to do anything - if (equal(Array.from(chars), Array.from(this.lastTriggerCharacters))) return + if (equal(Array.from(chars), Array.from(this.lastTriggerCharacters ?? []))) return this.lastTriggerCharacters = chars - this.completionRegistration.dispose() + this.completionRegistration?.dispose() this.completionRegistration = await this.connection.client.register(CompletionRequest.type, { documentSelector: null, resolveProvider: true, From 82c5dd7cb3368381f5872a4068795b6bdccb42e2 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Fri, 25 Apr 2025 11:23:29 -0400 Subject: [PATCH 6/8] Update changelog --- packages/vscode-tailwindcss/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vscode-tailwindcss/CHANGELOG.md b/packages/vscode-tailwindcss/CHANGELOG.md index 122181c8..53c9592a 100644 --- a/packages/vscode-tailwindcss/CHANGELOG.md +++ b/packages/vscode-tailwindcss/CHANGELOG.md @@ -2,7 +2,7 @@ ## Prerelease -- Nothing yet! +- Improve dynamic capability registration in the language server ([#1327](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1327)) # 0.14.16 From 6a270723d2519c7df102c08c38db3ed4afb9d4ba Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Fri, 25 Apr 2025 11:47:06 -0400 Subject: [PATCH 7/8] Add assertions to restart test --- .../tests/env/restart.test.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/tailwindcss-language-server/tests/env/restart.test.ts b/packages/tailwindcss-language-server/tests/env/restart.test.ts index 49b632c3..35b595f8 100644 --- a/packages/tailwindcss-language-server/tests/env/restart.test.ts +++ b/packages/tailwindcss-language-server/tests/env/restart.test.ts @@ -140,6 +140,9 @@ defineTest({ }, }) + expect(client.serverCapabilities).not.toEqual([]) + let ids1 = client.serverCapabilities.map((cap) => cap.id) + // Remove the CSS file let didRestart = new Promise((resolve) => { client.conn.onNotification('@/tailwindCSS/serverRestarted', resolve) @@ -147,6 +150,9 @@ defineTest({ await fs.unlink(path.resolve(root, 'app.css')) await didRestart + expect(client.serverCapabilities).not.toEqual([]) + let ids2 = client.serverCapabilities.map((cap) => cap.id) + //
// ^ let hover2 = await doc.hover({ line: 0, character: 13 }) @@ -164,11 +170,23 @@ defineTest({ ) await didRestartAgain + expect(client.serverCapabilities).not.toEqual([]) + let ids3 = client.serverCapabilities.map((cap) => cap.id) + await new Promise((resolve) => setTimeout(resolve, 500)) //
// ^ let hover3 = await doc.hover({ line: 0, character: 13 }) expect(hover3).toEqual(null) + + expect(ids1).not.toContainEqual(expect.toBeOneOf(ids2)) + expect(ids1).not.toContainEqual(expect.toBeOneOf(ids3)) + + expect(ids2).not.toContainEqual(expect.toBeOneOf(ids1)) + expect(ids2).not.toContainEqual(expect.toBeOneOf(ids3)) + + expect(ids3).not.toContainEqual(expect.toBeOneOf(ids1)) + expect(ids3).not.toContainEqual(expect.toBeOneOf(ids2)) }, }) From 5a5d4758e6712c53677ce89992e6c8575e28622f Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Fri, 25 Apr 2025 12:23:40 -0400 Subject: [PATCH 8/8] Add test --- .../src/projects.ts | 5 ++ .../tests/env/capabilities.test.ts | 79 +++++++++++++++++++ 2 files changed, 84 insertions(+) diff --git a/packages/tailwindcss-language-server/src/projects.ts b/packages/tailwindcss-language-server/src/projects.ts index 04160569..21e7bb3b 100644 --- a/packages/tailwindcss-language-server/src/projects.ts +++ b/packages/tailwindcss-language-server/src/projects.ts @@ -1086,6 +1086,11 @@ export async function createProjectService( refreshDiagnostics() updateCapabilities() + + let isTestMode = params.initializationOptions?.testMode ?? false + if (!isTestMode) return + + connection.sendNotification('@/tailwindCSS/projectReloaded') } for (let entry of projectConfig.config.entries) { diff --git a/packages/tailwindcss-language-server/tests/env/capabilities.test.ts b/packages/tailwindcss-language-server/tests/env/capabilities.test.ts index 46838981..22eac608 100644 --- a/packages/tailwindcss-language-server/tests/env/capabilities.test.ts +++ b/packages/tailwindcss-language-server/tests/env/capabilities.test.ts @@ -103,3 +103,82 @@ defineTest({ ) }, }) + +defineTest({ + name: 'Config updates do not register new trigger characters if the separator has not changed', + fs: { + 'tailwind.config.js': js` + module.exports = { + separator: ':', + theme: { + colors: { + primary: '#f00', + } + } + } + `, + }, + prepare: async ({ root }) => ({ client: await createClient({ root }) }), + handle: async ({ root, client }) => { + // Initially don't have any registered capabilities because dynamic + // registration is delayed until after project initialization + expect(client.serverCapabilities).toEqual([]) + + // We open a document so a project gets initialized + await client.open({ + lang: 'html', + text: '
', + }) + + // And now capabilities are registered + expect(client.serverCapabilities).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + method: 'textDocument/hover', + }), + + expect.objectContaining({ + method: 'textDocument/completion', + registerOptions: { + documentSelector: null, + resolveProvider: true, + triggerCharacters: ['"', "'", '`', ' ', '.', '(', '[', ']', '!', '/', '-', ':'], + }, + }), + ]), + ) + + let idsBefore = client.serverCapabilities.map((cap) => cap.id) + + await fs.writeFile( + `${root}/tailwind.config.js`, + js` + module.exports = { + separator: ':', + theme: { + colors: { + primary: '#0f0', + } + } + } + `, + ) + + let didReload = new Promise((resolve) => { + client.conn.onNotification('@/tailwindCSS/projectReloaded', resolve) + }) + + // After changing the config + client.notifyChangedFiles({ + changed: [`${root}/tailwind.config.js`], + }) + + // Wait for the project to finish building + await didReload + + // No capabilities should have changed + let idsAfter = client.serverCapabilities.map((cap) => cap.id) + + expect(idsBefore).toEqual(idsAfter) + }, +})