Skip to content

Commit f432c77

Browse files
Rework capability registration and handler setup (#1327)
This PR changes how capability registration works such that: - 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 This should, in most cases, mean that *all* supported capabilities are dynamically registered once at startup. Trigger characters only change for projects where the variant separator is customizable, has been customized, and includes a character that is not already considered a trigger character (v4 does *not* allow customizing the variant separator but v3 did) Fixes #1319 - [x] Need to look into creating a reproduction w/ 500-ish v3 config files in it to see if I can repro the original issue. We call `updateCapabilities` after a build finishes and its likely that this is/was slow enough with IntelliSense trying to handle 500 separate v3 projects at once
1 parent f511faa commit f432c77

File tree

6 files changed

+410
-81
lines changed

6 files changed

+410
-81
lines changed

packages/tailwindcss-language-server/src/projects.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1086,6 +1086,11 @@ export async function createProjectService(
10861086
refreshDiagnostics()
10871087

10881088
updateCapabilities()
1089+
1090+
let isTestMode = params.initializationOptions?.testMode ?? false
1091+
if (!isTestMode) return
1092+
1093+
connection.sendNotification('@/tailwindCSS/projectReloaded')
10891094
}
10901095

10911096
for (let entry of projectConfig.config.entries) {

packages/tailwindcss-language-server/src/tw.ts

Lines changed: 123 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import type {
2121
WorkspaceFolder,
2222
CodeLensParams,
2323
CodeLens,
24+
ServerCapabilities,
25+
ClientCapabilities,
2426
} from 'vscode-languageserver/node'
2527
import {
2628
CompletionRequest,
@@ -624,6 +626,8 @@ export class TW {
624626

625627
console.log(`[Global] Initializing projects...`)
626628

629+
await this.updateCommonCapabilities()
630+
627631
// init projects for documents that are _already_ open
628632
let readyDocuments: string[] = []
629633
let enabledProjectCount = 0
@@ -640,8 +644,6 @@ export class TW {
640644

641645
console.log(`[Global] Initialized ${enabledProjectCount} projects`)
642646

643-
this.setupLSPHandlers()
644-
645647
this.disposables.push(
646648
this.connection.onDidChangeConfiguration(async ({ settings }) => {
647649
let previousExclude = globalSettings.tailwindCSS.files.exclude
@@ -763,7 +765,7 @@ export class TW {
763765
this.connection,
764766
params,
765767
this.documentService,
766-
() => this.updateCapabilities(),
768+
() => this.updateProjectCapabilities(),
767769
() => {
768770
for (let document of this.documentService.getAllDocuments()) {
769771
let project = this.getProject(document)
@@ -810,9 +812,7 @@ export class TW {
810812
}
811813

812814
setupLSPHandlers() {
813-
if (this.lspHandlersAdded) {
814-
return
815-
}
815+
if (this.lspHandlersAdded) return
816816
this.lspHandlersAdded = true
817817

818818
this.connection.onHover(this.onHover.bind(this))
@@ -858,43 +858,84 @@ export class TW {
858858
}
859859
}
860860

861-
private updateCapabilities() {
862-
if (!supportsDynamicRegistration(this.initializeParams)) {
863-
this.connection.client.register(DidChangeConfigurationNotification.type, undefined)
864-
return
861+
// Common capabilities are always supported by the language server and do not
862+
// require any project-specific information to know how to configure them.
863+
//
864+
// These capabilities will stay valid until/unless the server has to restart
865+
// in which case they'll be unregistered and then re-registered once project
866+
// discovery has completed
867+
private commonRegistrations: BulkUnregistration | undefined
868+
private async updateCommonCapabilities() {
869+
let capabilities = BulkRegistration.create()
870+
871+
let client = this.initializeParams.capabilities
872+
873+
if (client.textDocument?.hover?.dynamicRegistration) {
874+
capabilities.add(HoverRequest.type, { documentSelector: null })
865875
}
866876

867-
if (this.registrations) {
868-
this.registrations.then((r) => r.dispose())
877+
if (client.textDocument?.colorProvider?.dynamicRegistration) {
878+
capabilities.add(DocumentColorRequest.type, { documentSelector: null })
869879
}
870880

871-
let projects = Array.from(this.projects.values())
881+
if (client.textDocument?.codeAction?.dynamicRegistration) {
882+
capabilities.add(CodeActionRequest.type, { documentSelector: null })
883+
}
872884

873-
let capabilities = BulkRegistration.create()
885+
if (client.textDocument?.codeLens?.dynamicRegistration) {
886+
capabilities.add(CodeLensRequest.type, { documentSelector: null })
887+
}
888+
889+
if (client.textDocument?.documentLink?.dynamicRegistration) {
890+
capabilities.add(DocumentLinkRequest.type, { documentSelector: null })
891+
}
892+
893+
if (client.workspace?.didChangeConfiguration?.dynamicRegistration) {
894+
capabilities.add(DidChangeConfigurationNotification.type, undefined)
895+
}
874896

875-
// TODO: We should *not* be re-registering these capabilities
876-
// IDEA: These should probably be static registrations up front
877-
capabilities.add(HoverRequest.type, { documentSelector: null })
878-
capabilities.add(DocumentColorRequest.type, { documentSelector: null })
879-
capabilities.add(CodeActionRequest.type, { documentSelector: null })
880-
capabilities.add(CodeLensRequest.type, { documentSelector: null })
881-
capabilities.add(DocumentLinkRequest.type, { documentSelector: null })
882-
capabilities.add(DidChangeConfigurationNotification.type, undefined)
883-
884-
// TODO: Only re-register this if trigger characters change
885-
capabilities.add(CompletionRequest.type, {
897+
this.commonRegistrations = await this.connection.client.register(capabilities)
898+
}
899+
900+
// These capabilities depend on the projects we've found to appropriately
901+
// configure them. This may mean collecting information from all discovered
902+
// projects to determine what we can do and how
903+
private updateProjectCapabilities() {
904+
this.updateTriggerCharacters()
905+
}
906+
907+
private lastTriggerCharacters: Set<string> | undefined
908+
private completionRegistration: Disposable | undefined
909+
private async updateTriggerCharacters() {
910+
// If the client does not suppory dynamic registration of completions then
911+
// we cannot update the set of trigger characters
912+
let client = this.initializeParams.capabilities
913+
if (!client.textDocument?.completion?.dynamicRegistration) return
914+
915+
// The new set of trigger characters is all the static ones plus
916+
// any characters from any separator in v3 config
917+
let chars = new Set<string>(TRIGGER_CHARACTERS)
918+
919+
for (let project of this.projects.values()) {
920+
let sep = project.state.separator
921+
if (typeof sep !== 'string') continue
922+
923+
sep = sep.slice(-1)
924+
if (!sep) continue
925+
926+
chars.add(sep)
927+
}
928+
929+
// If the trigger characters haven't changed then we don't need to do anything
930+
if (equal(Array.from(chars), Array.from(this.lastTriggerCharacters ?? []))) return
931+
this.lastTriggerCharacters = chars
932+
933+
this.completionRegistration?.dispose()
934+
this.completionRegistration = await this.connection.client.register(CompletionRequest.type, {
886935
documentSelector: null,
887936
resolveProvider: true,
888-
triggerCharacters: [
889-
...TRIGGER_CHARACTERS,
890-
...projects
891-
.map((project) => project.state.separator)
892-
.filter((sep) => typeof sep === 'string')
893-
.map((sep) => sep.slice(-1)),
894-
].filter(Boolean),
937+
triggerCharacters: Array.from(chars),
895938
})
896-
897-
this.registrations = this.connection.client.register(capabilities)
898939
}
899940

900941
private getProject(document: TextDocumentIdentifier): ProjectService {
@@ -1016,47 +1057,58 @@ export class TW {
10161057
this.connection.onInitialize(async (params: InitializeParams): Promise<InitializeResult> => {
10171058
this.initializeParams = params
10181059

1019-
if (supportsDynamicRegistration(params)) {
1020-
return {
1021-
capabilities: {
1022-
textDocumentSync: TextDocumentSyncKind.Full,
1023-
workspace: {
1024-
workspaceFolders: {
1025-
changeNotifications: true,
1026-
},
1027-
},
1028-
},
1029-
}
1030-
}
1031-
10321060
this.setupLSPHandlers()
10331061

10341062
return {
1035-
capabilities: {
1036-
textDocumentSync: TextDocumentSyncKind.Full,
1037-
hoverProvider: true,
1038-
colorProvider: true,
1039-
codeActionProvider: true,
1040-
codeLensProvider: {
1041-
resolveProvider: false,
1042-
},
1043-
documentLinkProvider: {},
1044-
completionProvider: {
1045-
resolveProvider: true,
1046-
triggerCharacters: [...TRIGGER_CHARACTERS, ':'],
1047-
},
1048-
workspace: {
1049-
workspaceFolders: {
1050-
changeNotifications: true,
1051-
},
1052-
},
1053-
},
1063+
capabilities: this.computeServerCapabilities(params.capabilities),
10541064
}
10551065
})
10561066

10571067
this.connection.onInitialized(() => this.init())
10581068
}
10591069

1070+
computeServerCapabilities(client: ClientCapabilities) {
1071+
let capabilities: ServerCapabilities = {
1072+
textDocumentSync: TextDocumentSyncKind.Full,
1073+
workspace: {
1074+
workspaceFolders: {
1075+
changeNotifications: true,
1076+
},
1077+
},
1078+
}
1079+
1080+
if (!client.textDocument?.hover?.dynamicRegistration) {
1081+
capabilities.hoverProvider = true
1082+
}
1083+
1084+
if (!client.textDocument?.colorProvider?.dynamicRegistration) {
1085+
capabilities.colorProvider = true
1086+
}
1087+
1088+
if (!client.textDocument?.codeAction?.dynamicRegistration) {
1089+
capabilities.codeActionProvider = true
1090+
}
1091+
1092+
if (!client.textDocument?.codeLens?.dynamicRegistration) {
1093+
capabilities.codeLensProvider = {
1094+
resolveProvider: false,
1095+
}
1096+
}
1097+
1098+
if (!client.textDocument?.completion?.dynamicRegistration) {
1099+
capabilities.completionProvider = {
1100+
resolveProvider: true,
1101+
triggerCharacters: [...TRIGGER_CHARACTERS, ':'],
1102+
}
1103+
}
1104+
1105+
if (!client.textDocument?.documentLink?.dynamicRegistration) {
1106+
capabilities.documentLinkProvider = {}
1107+
}
1108+
1109+
return capabilities
1110+
}
1111+
10601112
listen() {
10611113
this.connection.listen()
10621114
}
@@ -1070,10 +1122,11 @@ export class TW {
10701122

10711123
this.refreshDiagnostics()
10721124

1073-
if (this.registrations) {
1074-
this.registrations.then((r) => r.dispose())
1075-
this.registrations = undefined
1076-
}
1125+
this.commonRegistrations?.dispose()
1126+
this.commonRegistrations = undefined
1127+
1128+
this.completionRegistration?.dispose()
1129+
this.completionRegistration = undefined
10771130

10781131
this.disposables.forEach((d) => d.dispose())
10791132
this.disposables.length = 0
@@ -1106,13 +1159,3 @@ export class TW {
11061159
}
11071160
}
11081161
}
1109-
1110-
function supportsDynamicRegistration(params: InitializeParams): boolean {
1111-
return (
1112-
params.capabilities.textDocument?.hover?.dynamicRegistration &&
1113-
params.capabilities.textDocument?.colorProvider?.dynamicRegistration &&
1114-
params.capabilities.textDocument?.codeAction?.dynamicRegistration &&
1115-
params.capabilities.textDocument?.completion?.dynamicRegistration &&
1116-
params.capabilities.textDocument?.documentLink?.dynamicRegistration
1117-
)
1118-
}

0 commit comments

Comments
 (0)