Home

tailwind-ctp-intellisense @master - refs - log -
-
https://git.jolheiser.com/tailwind-ctp-intellisense.git
Tailwind intellisense + Catppuccin
tree log patch
Add `experimental.configFile` setting (#541) * Add experimental `configFile` setting * Fix initial capability registration * Update readme * Add setting default and description * Remove unused variable * Be more defensive when reading setting * Fix type * Fix type
Signature
-----BEGIN PGP SIGNATURE----- wsBcBAABCAAQBQJiZqrnCRBK7hj4Ov3rIwAAWIwIAAniintxrTZeD/4WpuKJ6uZX gs9HN4nlWA9ea1IZ0mqiqomdjOrZx/fxnBvOI7EDu5vzG+Eb+LS++BqATOfklbfw y8YPVN9YAr5xVJ1fmTD5s30ycgIkpzcBR1toz8M47XPIVfssNgu89/1+b3uOgpPa YesreRQFHz7YAIDDluwve0Lbk10rL8hxrvKhGom9xQ0EVOm45hpXBamvfsbAtrqV GxbdpnGFVXOV5G7DnCqPp/FSOYFa9e+2FDzKh6EIVR5Ow3RcBSSNgk262VNrCMdE TaCpQEpy3fVrx8s4xISsLnSR0NXLzraLSoe6bhOio5CWBzR5HOe7prnAd1Xa3+c= =g4d7 -----END PGP SIGNATURE-----
Brad Cornes <hello@bradley.dev>
2 years ago
5 changed files, 211 additions(+), 103 deletions(-)
M packages/tailwindcss-language-server/src/server.ts -> packages/tailwindcss-language-server/src/server.ts
diff --git a/packages/tailwindcss-language-server/src/server.ts b/packages/tailwindcss-language-server/src/server.ts
index f0828133bacb40d84c43d926e09a3a97e69e03c1..615fd950af66066fcf399e3803fb15c1f219695a 100644
--- a/packages/tailwindcss-language-server/src/server.ts
+++ b/packages/tailwindcss-language-server/src/server.ts
@@ -26,6 +26,7 @@   HoverRequest,
   DidChangeWatchedFilesNotification,
   FileChangeType,
   Disposable,
+  TextDocumentIdentifier,
 } from 'vscode-languageserver/node'
 import { TextDocument } from 'vscode-languageserver-textdocument'
 import { URI } from 'vscode-uri'
@@ -177,6 +178,7 @@   state: State
   tryInit: () => Promise<void>
   dispose: () => void
   onUpdateSettings: (settings: any) => void
+  onFileEvents: (changes: Array<{ file: string; type: FileChangeType }>) => void
   onHover(params: TextDocumentPositionParams): Promise<Hover>
   onCompletion(params: CompletionParams): Promise<CompletionList>
   onCompletionResolve(item: CompletionItem): Promise<CompletionItem>
@@ -185,6 +187,8 @@   onDocumentColor(params: DocumentColorParams): Promise<ColorInformation[]>
   onColorPresentation(params: ColorPresentationParams): Promise<ColorPresentation[]>
   onCodeAction(params: CodeActionParams): Promise<CodeAction[]>
 }
+
+type ProjectConfig = { folder: string; configPath?: string; documentSelector?: string[] }
 
 function getMode(config: any): unknown {
   if (typeof config.mode !== 'undefined') {
@@ -210,11 +214,13 @@   }
 }
 
 async function createProjectService(
-  folder: string,
+  projectConfig: ProjectConfig,
   connection: Connection,
   params: InitializeParams,
-  documentService: DocumentService
+  documentService: DocumentService,
+  updateCapabilities: () => void
 ): Promise<ProjectService> {
+  const folder = projectConfig.folder
   const disposables: Disposable[] = []
   const documentSettingsCache: Map<string, Settings> = new Map()
 
@@ -258,8 +264,6 @@         return connection.sendRequest('@/tailwindCSS/getDocumentSymbols', { uri })
       },
     },
   }
-
-  let registrations: Promise<BulkUnregistration>
 
   let chokidarWatcher: chokidar.FSWatcher
   let ignore = state.editor.globalSettings.tailwindCSS.files?.exclude ?? DEFAULT_FILES_EXCLUDE
@@ -311,15 +315,6 @@     }
   }
 
   if (params.capabilities.workspace?.didChangeWatchedFiles?.dynamicRegistration) {
-    connection.onDidChangeWatchedFiles(({ changes }) => {
-      onFileEvents(
-        changes.map(({ uri, type }) => ({
-          file: URI.parse(uri).fsPath,
-          type,
-        }))
-      )
-    })
-
     connection.client.register(DidChangeWatchedFilesNotification.type, {
       watchers: [{ globPattern: `**/${CONFIG_FILE_GLOB}` }, { globPattern: `**/${PACKAGE_GLOB}` }],
     })
@@ -376,38 +371,6 @@       },
     })
   }
 
-  function registerCapabilities(watchFiles: string[] = []): void {
-    if (supportsDynamicRegistration(connection, params)) {
-      if (registrations) {
-        registrations.then((r) => r.dispose())
-      }
-
-      let capabilities = BulkRegistration.create()
-
-      capabilities.add(HoverRequest.type, {
-        documentSelector: null,
-      })
-      capabilities.add(DocumentColorRequest.type, {
-        documentSelector: null,
-      })
-      capabilities.add(CodeActionRequest.type, {
-        documentSelector: null,
-      })
-      capabilities.add(CompletionRequest.type, {
-        documentSelector: null,
-        resolveProvider: true,
-        triggerCharacters: [...TRIGGER_CHARACTERS, state.separator].filter(Boolean),
-      })
-      if (watchFiles.length > 0) {
-        capabilities.add(DidChangeWatchedFilesNotification.type, {
-          watchers: watchFiles.map((file) => ({ globPattern: file })),
-        })
-      }
-
-      registrations = connection.client.register(capabilities)
-    }
-  }
-
   function resetState(): void {
     clearAllDiagnostics(state)
     Object.keys(state).forEach((key) => {
@@ -417,7 +380,7 @@         delete state[key]
       }
     })
     state.enabled = false
-    registerCapabilities(state.dependencies)
+    updateCapabilities()
   }
 
   async function tryInit() {
@@ -452,19 +415,23 @@
   async function init() {
     clearRequireCache()
 
-    let [configPath] = (
-      await glob([`**/${CONFIG_FILE_GLOB}`], {
-        cwd: folder,
-        ignore: state.editor.globalSettings.tailwindCSS.files?.exclude ?? DEFAULT_FILES_EXCLUDE,
-        onlyFiles: true,
-        absolute: true,
-        suppressErrors: true,
-        dot: true,
-        concurrency: Math.max(os.cpus().length, 1),
-      })
-    )
-      .sort((a: string, b: string) => a.split('/').length - b.split('/').length)
-      .map(path.normalize)
+    let configPath = projectConfig.configPath
+
+    if (!configPath) {
+      configPath = (
+        await glob([`**/${CONFIG_FILE_GLOB}`], {
+          cwd: folder,
+          ignore: state.editor.globalSettings.tailwindCSS.files?.exclude ?? DEFAULT_FILES_EXCLUDE,
+          onlyFiles: true,
+          absolute: true,
+          suppressErrors: true,
+          dot: true,
+          concurrency: Math.max(os.cpus().length, 1),
+        })
+      )
+        .sort((a: string, b: string) => a.split('/').length - b.split('/').length)
+        .map(path.normalize)[0]
+    }
 
     if (!configPath) {
       throw new SilentError('No config file found.')
@@ -957,7 +924,7 @@     state.enabled = true
 
     updateAllDiagnostics(state)
 
-    registerCapabilities(state.dependencies)
+    updateCapabilities()
   }
 
   return {
@@ -980,12 +947,13 @@         if (state.enabled) {
           updateAllDiagnostics(state)
         }
         if (settings.editor.colorDecorators) {
-          registerCapabilities(state.dependencies)
+          updateCapabilities()
         } else {
           connection.sendNotification('@/tailwindCSS/clearColors')
         }
       }
     },
+    onFileEvents,
     async onHover(params: TextDocumentPositionParams): Promise<Hover> {
       if (!state.enabled) return null
       let document = documentService.getDocument(params.textDocument.uri)
@@ -1002,11 +970,19 @@       if (!document) return null
       let settings = await state.editor.getConfiguration(document.uri)
       if (!settings.tailwindCSS.suggestions) return null
       if (await isExcluded(state, document)) return null
-      return doComplete(state, document, params.position, params.context)
+      let result = await doComplete(state, document, params.position, params.context)
+      if (!result) return result
+      return {
+        isIncomplete: result.isIncomplete,
+        items: result.items.map((item) => ({
+          ...item,
+          data: { projectKey: JSON.stringify(projectConfig), originalData: item.data },
+        })),
+      }
     },
     onCompletionResolve(item: CompletionItem): Promise<CompletionItem> {
       if (!state.enabled) return null
-      return resolveCompletionItem(state, item)
+      return resolveCompletionItem(state, { ...item, data: item.data?.originalData })
     },
     async onCodeAction(params: CodeActionParams): Promise<CodeAction[]> {
       if (!state.enabled) return null
@@ -1345,6 +1321,7 @@   private workspaces: Map<string, { name: string; workspaceFsPath: string }>
   private projects: Map<string, ProjectService>
   private documentService: DocumentService
   public initializeParams: InitializeParams
+  private registrations: Promise<BulkUnregistration>
 
   constructor(private connection: Connection) {
     this.documentService = new DocumentService(this.connection)
@@ -1358,16 +1335,15 @@
     this.initialized = true
 
     // TODO
-    const workspaceFolders =
+    let workspaceFolders: Array<ProjectConfig> =
       false &&
       Array.isArray(this.initializeParams.workspaceFolders) &&
       this.initializeParams.capabilities.workspace?.workspaceFolders
         ? this.initializeParams.workspaceFolders.map((el) => ({
-            name: el.name,
-            fsPath: getFileFsPath(el.uri),
+            folder: getFileFsPath(el.uri),
           }))
         : this.initializeParams.rootPath
-        ? [{ name: '', fsPath: normalizeFileNameToFsPath(this.initializeParams.rootPath) }]
+        ? [{ folder: normalizeFileNameToFsPath(this.initializeParams.rootPath) }]
         : []
 
     if (workspaceFolders.length === 0) {
@@ -1375,14 +1351,65 @@       console.error('No workspace folders found, not initializing.')
       return
     }
 
+    let configFileOrFiles = dlv(
+      await connection.workspace.getConfiguration('tailwindCSS'),
+      'experimental.configFile',
+      null
+    ) as Settings['tailwindCSS']['experimental']['configFile']
+
+    if (configFileOrFiles) {
+      let base = workspaceFolders[0].folder
+
+      if (
+        typeof configFileOrFiles !== 'string' &&
+        (!isObject(configFileOrFiles) ||
+          !Object.entries(configFileOrFiles).every(([key, value]) => {
+            if (typeof key !== 'string') return false
+            if (Array.isArray(value)) {
+              return value.every((item) => typeof item === 'string')
+            }
+            return typeof value === 'string'
+          }))
+      ) {
+        console.error('Invalid `experimental.configFile` configuration, not initializing.')
+        return
+      }
+
+      let configFiles =
+        typeof configFileOrFiles === 'string' ? { [configFileOrFiles]: '**' } : configFileOrFiles
+
+      workspaceFolders = Object.entries(configFiles).map(
+        ([relativeConfigPath, relativeDocumentSelectorOrSelectors]) => {
+          return {
+            folder: base,
+            configPath: path.join(base, relativeConfigPath),
+            documentSelector: []
+              .concat(relativeDocumentSelectorOrSelectors)
+              .map((selector) => path.join(base, selector)),
+          }
+        }
+      )
+    }
+
     await Promise.all(
-      workspaceFolders.map(async (folder) => {
-        return this.addProject(folder.fsPath, this.initializeParams)
-      })
+      workspaceFolders.map((projectConfig) => this.addProject(projectConfig, this.initializeParams))
     )
 
     this.setupLSPHandlers()
 
+    if (this.initializeParams.capabilities.workspace?.didChangeWatchedFiles?.dynamicRegistration) {
+      this.connection.onDidChangeWatchedFiles(({ changes }) => {
+        for (let [, project] of this.projects) {
+          project.onFileEvents(
+            changes.map(({ uri, type }) => ({
+              file: URI.parse(uri).fsPath,
+              type,
+            }))
+          )
+        }
+      })
+    }
+
     this.connection.onDidChangeConfiguration(async ({ settings }) => {
       for (let [, project] of this.projects) {
         project.onUpdateSettings(settings)
@@ -1394,24 +1421,24 @@       this.dispose()
     })
 
     this.documentService.onDidChangeContent((change) => {
-      // TODO
-      const project = Array.from(this.projects.values())[0]
-      project?.provideDiagnostics(change.document)
+      this.getProject(change.document)?.provideDiagnostics(change.document)
     })
   }
 
-  private async addProject(folder: string, params: InitializeParams): Promise<void> {
-    if (this.projects.has(folder)) {
-      await this.projects.get(folder).tryInit()
+  private async addProject(projectConfig: ProjectConfig, params: InitializeParams): Promise<void> {
+    let key = JSON.stringify(projectConfig)
+    if (this.projects.has(key)) {
+      await this.projects.get(key).tryInit()
     } else {
       const project = await createProjectService(
-        folder,
+        projectConfig,
         this.connection,
         params,
-        this.documentService
+        this.documentService,
+        () => this.updateCapabilities()
       )
+      this.projects.set(key, project)
       await project.tryInit()
-      this.projects.set(folder, project)
     }
   }
 
@@ -1424,38 +1451,78 @@     this.connection.onColorPresentation(this.onColorPresentation.bind(this))
     this.connection.onCodeAction(this.onCodeAction.bind(this))
   }
 
+  private updateCapabilities() {
+    if (this.registrations) {
+      this.registrations.then((r) => r.dispose())
+    }
+
+    let projects = Array.from(this.projects.values())
+
+    let capabilities = BulkRegistration.create()
+
+    capabilities.add(HoverRequest.type, { documentSelector: null })
+    capabilities.add(DocumentColorRequest.type, { documentSelector: null })
+    capabilities.add(CodeActionRequest.type, { documentSelector: null })
+
+    capabilities.add(CompletionRequest.type, {
+      documentSelector: null,
+      resolveProvider: true,
+      triggerCharacters: [
+        ...TRIGGER_CHARACTERS,
+        ...projects.map((project) => project.state.separator).filter(Boolean),
+      ].filter(Boolean),
+    })
+
+    capabilities.add(DidChangeWatchedFilesNotification.type, {
+      watchers: projects.flatMap((project) =>
+        project.state.dependencies.map((file) => ({ globPattern: file }))
+      ),
+    })
+
+    this.registrations = this.connection.client.register(capabilities)
+  }
+
+  private getProject(document: TextDocumentIdentifier): ProjectService {
+    let fallbackProject: ProjectService
+    for (let [key, project] of this.projects) {
+      let projectConfig = JSON.parse(key) as ProjectConfig
+      if (projectConfig.configPath) {
+        for (let selector of projectConfig.documentSelector) {
+          if (minimatch(URI.parse(document.uri).fsPath, selector)) {
+            return project
+          }
+        }
+      } else {
+        if (!fallbackProject) {
+          fallbackProject = project
+        }
+      }
+    }
+    return fallbackProject
+  }
+
   async onDocumentColor(params: DocumentColorParams): Promise<ColorInformation[]> {
-    const project = Array.from(this.projects.values())[0]
-    return project?.onDocumentColor(params) ?? []
+    return this.getProject(params.textDocument)?.onDocumentColor(params) ?? []
   }
 
   async onColorPresentation(params: ColorPresentationParams): Promise<ColorPresentation[]> {
-    const project = Array.from(this.projects.values())[0]
-    return project?.onColorPresentation(params) ?? []
+    return this.getProject(params.textDocument)?.onColorPresentation(params) ?? []
   }
 
   async onHover(params: TextDocumentPositionParams): Promise<Hover> {
-    // TODO
-    const project = Array.from(this.projects.values())[0]
-    return project?.onHover(params) ?? null
+    return this.getProject(params.textDocument)?.onHover(params) ?? null
   }
 
   async onCompletion(params: CompletionParams): Promise<CompletionList> {
-    // TODO
-    const project = Array.from(this.projects.values())[0]
-    return project?.onCompletion(params) ?? null
+    return this.getProject(params.textDocument)?.onCompletion(params) ?? null
   }
 
   async onCompletionResolve(item: CompletionItem): Promise<CompletionItem> {
-    // TODO
-    const project = Array.from(this.projects.values())[0]
-    return project?.onCompletionResolve(item) ?? null
+    return this.projects.get(item.data.projectKey)?.onCompletionResolve(item) ?? null
   }
 
   onCodeAction(params: CodeActionParams): Promise<CodeAction[]> {
-    // TODO
-    const project = Array.from(this.projects.values())[0]
-    return project?.onCodeAction(params) ?? null
+    return this.getProject(params.textDocument)?.onCodeAction(params) ?? null
   }
 
   listen() {
M packages/tailwindcss-language-service/src/util/state.ts -> packages/tailwindcss-language-service/src/util/state.ts
diff --git a/packages/tailwindcss-language-service/src/util/state.ts b/packages/tailwindcss-language-service/src/util/state.ts
index 061dd11163c3d2ee308425ec0c8ea841c374ab7b..ff34d5bdc86411955799aec4780c19c23c8f12e3 100644
--- a/packages/tailwindcss-language-service/src/util/state.ts
+++ b/packages/tailwindcss-language-service/src/util/state.ts
@@ -59,6 +59,7 @@       recommendedVariantOrder: DiagnosticSeveritySetting
     }
     experimental: {
       classRegex: string[]
+      configFile: string | Record<string, string | string[]>
     }
     files: {
       exclude: string[]
M packages/vscode-tailwindcss/README.md -> packages/vscode-tailwindcss/README.md
diff --git a/packages/vscode-tailwindcss/README.md b/packages/vscode-tailwindcss/README.md
index ce5068fc896efabb6d04abe340c73af08080d989..3a2eb4c50fcc1da0ebd32a9929025ab5a7dc80cf 100644
--- a/packages/vscode-tailwindcss/README.md
+++ b/packages/vscode-tailwindcss/README.md
@@ -146,6 +146,31 @@ ### `tailwindCSS.inspectPort`
 
 Enable the Node.js inspector agent for the language server and listen on the specified port. **Default: `null`**
 
+## Experimental Extension Settings
+
+**_Experimental settings may be changed or removed at any time._**
+
+### `tailwindCSS.experimental.configFile`
+
+**Default: `null`**
+
+By default the extension will automatically use the first `tailwind.config.js` or `tailwind.config.cjs` file that it can find to provide Tailwind CSS IntelliSense. Use this setting to manually specify the config file(s) yourself instead.
+
+If your project contains a single Tailwind config file you can specify a string value:
+
+```
+"tailwindCSS.experimental.configFile": ".config/tailwind.config.js"
+```
+
+For projects with multiple config files use an object where each key is a config file path and each value is a glob pattern (or array of glob patterns) representing the set of files that the config file applies to:
+
+```
+"tailwindCSS.experimental.configFile": {
+  "themes/simple/tailwind.config.js": "themes/simple/**",
+  "themes/neon/tailwind.config.js": "themes/neon/**"
+}
+```
+
 ## Troubleshooting
 
 If you’re having issues getting the IntelliSense features to activate, there are a few things you can check:
M packages/vscode-tailwindcss/package.json -> packages/vscode-tailwindcss/package.json
diff --git a/packages/vscode-tailwindcss/package.json b/packages/vscode-tailwindcss/package.json
index 67ef3f662d34e379e4ac08c0a7f16d8313fdd55e..7838b93dea0d03b1e1ccfe76c5e40b7133983eeb 100755
--- a/packages/vscode-tailwindcss/package.json
+++ b/packages/vscode-tailwindcss/package.json
@@ -277,6 +277,15 @@         "tailwindCSS.experimental.classRegex": {
           "type": "array",
           "scope": "language-overridable"
         },
+        "tailwindCSS.experimental.configFile": {
+          "type": [
+            "null",
+            "string",
+            "object"
+          ],
+          "default": null,
+          "markdownDescription": "Manually specify the Tailwind config file or files that should be read to provide IntelliSense features. Can either be a single string value, or an object where each key is a config file path and each value is a glob or array of globs representing the set of files that the config file applies to."
+        },
         "tailwindCSS.showPixelEquivalents": {
           "type": "boolean",
           "default": true,
M packages/vscode-tailwindcss/src/extension.ts -> packages/vscode-tailwindcss/src/extension.ts
diff --git a/packages/vscode-tailwindcss/src/extension.ts b/packages/vscode-tailwindcss/src/extension.ts
index fc19acbc4dcd612810d9f95d245fcfc28cb77ff7..35b4f75926374d12fe6ccccc4617808097e10172 100755
--- a/packages/vscode-tailwindcss/src/extension.ts
+++ b/packages/vscode-tailwindcss/src/extension.ts
@@ -169,24 +169,30 @@   // not just the language IDs
   // e.g. "plaintext" already exists but you change it from "html" to "css"
   context.subscriptions.push(
     Workspace.onDidChangeConfiguration((event) => {
-      clients.forEach((client, key) => {
+      ;[...clients].forEach(([key, client]) => {
         const folder = Workspace.getWorkspaceFolder(Uri.parse(key))
+        let reboot = false
 
-        if (event.affectsConfiguration('tailwindCSS', folder)) {
+        if (event.affectsConfiguration('tailwindCSS.includeLanguages', folder)) {
           const userLanguages = getUserLanguages(folder)
           if (userLanguages) {
             const userLanguageIds = Object.keys(userLanguages)
             const newLanguages = dedupe([...defaultLanguages, ...userLanguageIds])
             if (!equal(newLanguages, languages.get(folder.uri.toString()))) {
               languages.set(folder.uri.toString(), newLanguages)
-
-              if (client) {
-                clients.delete(folder.uri.toString())
-                client.stop()
-                bootWorkspaceClient(folder)
-              }
+              reboot = true
             }
           }
+        }
+
+        if (event.affectsConfiguration('tailwindCSS.experimental.configFile', folder)) {
+          reboot = true
+        }
+
+        if (reboot && client) {
+          clients.delete(folder.uri.toString())
+          client.stop()
+          bootWorkspaceClient(folder)
         }
       })
     })