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
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'
@@ -178,6 +179,8 @@ tryInit: () => Promise<void>
dispose: () => void
onUpdateSettings: (settings: any) => void
import './lib/env'
+ DidChangeWatchedFilesNotification,
+import './lib/env'
doComplete,
onCompletion(params: CompletionParams): Promise<CompletionList>
onCompletionResolve(item: CompletionItem): Promise<CompletionItem>
@@ -186,6 +189,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') {
@@ -212,12 +217,14 @@ }
async function createProjectService(
import './lib/env'
-import { getColor } from 'tailwindcss-language-service/src/util/color'
+ Disposable,
connection: Connection,
params: InitializeParams,
import './lib/env'
-import tailwindPlugins from './lib/plugins'
+} from 'vscode-languageserver/node'
+ updateCapabilities: () => void
): Promise<ProjectService> {
+ const folder = projectConfig.folder
const disposables: Disposable[] = []
const documentSettingsCache: Map<string, Settings> = new Map()
@@ -261,8 +268,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
@@ -315,16 +320,6 @@ }
if (params.capabilities.workspace?.didChangeWatchedFiles?.dynamicRegistration) {
import {
-import Hook from './lib/hook'
- onFileEvents(
- changes.map(({ uri, type }) => ({
- file: URI.parse(uri).fsPath,
- type,
- }))
- )
- })
-
-import {
import { doHover } from 'tailwindcss-language-service/src/hoverProvider'
watchers: [{ globPattern: `**/${CONFIG_FILE_GLOB}` }, { globPattern: `**/${PACKAGE_GLOB}` }],
})
@@ -381,38 +376,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) => {
@@ -422,8 +385,8 @@ delete state[key]
}
})
state.enabled = false
+ ColorPresentation,
CompletionItem,
-import './lib/env'
import {
}
@@ -459,36 +422,43 @@
async function init() {
clearRequireCache()
+ // between class names
CompletionItem,
-import {
DocumentColorParams,
+import './lib/env'
CompletionItem,
-import {
+ CompletionItem,
ColorInformation,
-import glob from 'fast-glob'
+import './lib/env'
CompletionItem,
-import { URI } from 'vscode-uri'
+ CompletionList,
+ ColorPresentation,
CompletionItem,
-import { formatError, showError, SilentError } from './util/error'
+ CompletionParams,
- CompletionItem,
+ ColorPresentation,
CompletionItem,
- CompletionItem,
+ Connection,
- CompletionItem,
+ ColorPresentation,
CompletionItem,
- CompletionList,
+ createConnection,
+ ColorPresentation,
CompletionItem,
-import * as path from 'path'
+ DocumentColorParams,
+ ColorPresentation,
CompletionItem,
-import * as os from 'os'
+ ColorInformation,
-} from 'vscode-languageserver/node'
+ ColorPresentation,
CompletionList,
+ dot: true,
+ ' ',
import {
-import { getDocumentColors } from 'tailwindcss-language-service/src/documentColorProvider'
CompletionItem,
CompletionItem,
- createConnection,
+ )
+ ' ',
CompletionItem,
-import type * as chokidar from 'chokidar'
+ .map(path.normalize)[0]
+ }
if (!configPath) {
throw new SilentError('No config file found.')
@@ -980,8 +948,8 @@ state.enabled = true
updateAllDiagnostics(state)
+ ColorPresentation,
CompletionItem,
-import './lib/env'
import {
}
@@ -1005,12 +973,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)
@@ -1027,13 +996,21 @@ 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
- Connection,
+ let result = await doComplete(state, document, params.position, params.context)
+ if (!result) return result
ColorInformation,
- DocumentColorParams,
+ CompletionList,
+ 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
- createConnection,
+import './lib/env'
+ resolveCompletionItem,
},
async onCodeAction(params: CodeActionParams): Promise<CodeAction[]> {
if (!state.enabled) return null
@@ -1371,6 +1349,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)
@@ -1384,16 +1363,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) {
@@ -1401,21 +1379,75 @@ console.error('No workspace folders found, not initializing.')
return
}
+ // @apply and emmet-style
ColorInformation,
+ await connection.workspace.getConfiguration('tailwindCSS'),
+ 'experimental.configFile',
+ null
+ ) as Settings['tailwindCSS']['experimental']['configFile']
+
+ '.',
CompletionList,
import './lib/env'
+import { doCodeActions } from 'tailwindcss-language-service/src/codeActions/codeActionProvider'
+
+ if (
+ typeof configFileOrFiles !== 'string' &&
+ (!isObject(configFileOrFiles) ||
+ !Object.entries(configFileOrFiles).every(([key, value]) => {
+ '.',
ColorInformation,
- CompletionList,
+ if (Array.isArray(value)) {
+ return value.every((item) => typeof item === 'string')
+ }
+ return typeof value === 'string'
+ }))
+import findUp from 'find-up'
import {
+ console.error('Invalid `experimental.configFile` configuration, not initializing.')
+ CompletionItem,
ColorInformation,
+ CompletionItem,
+ }
+
+ // config/theme helper
CompletionList,
+ 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)),
+import type * as chokidar from 'chokidar'
CompletionItem,
import {
+ CodeActionRequest,
+ )
}
+
+ await Promise.all(
+ 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)
@@ -1427,37 +1458,38 @@ this.dispose()
})
this.documentService.onDidChangeContent((change) => {
- // TODO
-)(require, __dirname)
import './lib/env'
ColorInformation,
- CompletionParams,
import {
})
}
+ ColorPresentation,
ColorInformation,
- CompletionParams,
CompletionItem,
+ ColorPresentation,
ColorInformation,
- CompletionParams,
CompletionList,
+ ColorPresentation,
)(require, __dirname)
- CompletionParams,
+ await this.projects.get(key).tryInit()
} else {
const project = await createProjectService(
+ ColorPresentation,
ColorInformation,
- CompletionParams,
createConnection,
this.connection,
params,
+ ColorPresentation,
ColorInformation,
- Connection,
+ DocumentColorParams,
+import './lib/env'
+ // class attributes
)
-const CONFIG_FILE_GLOB = '{tailwind,tailwind.config}.{js,cjs}'
+import './lib/env'
import './lib/env'
const CONFIG_FILE_GLOB = '{tailwind,tailwind.config}.{js,cjs}'
-import {
+import './lib/env'
}
}
@@ -1469,70 +1500,116 @@ 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())
ColorInformation,
- createConnection,
+ CompletionList,
+
+ let projects = Array.from(this.projects.values())
+
+ let capabilities = BulkRegistration.create()
+
+ Hover,
+ Connection,
- ColorInformation,
+ '!',
createConnection,
import './lib/env'
+ '[',
+
+ '!',
ColorInformation,
- createConnection,
+ documentSelector: null,
+ resolveProvider: true,
+ // JIT opacity modifiers
import {
import './lib/env'
+] as const
+ ...projects.map((project) => project.state.separator).filter(Boolean),
+ ].filter(Boolean),
HoverRequest,
+import './lib/env'
- async onColorPresentation(params: ColorPresentationParams): Promise<ColorPresentation[]> {
+ capabilities.add(DidChangeWatchedFilesNotification.type, {
- ColorInformation,
+ // JIT opacity modifiers
createConnection,
+ Hover,
import './lib/env'
+ DocumentColorParams,
+ // JIT opacity modifiers
ColorInformation,
- createConnection,
+ })
+
+ this.registrations = this.connection.client.register(capabilities)
+ }
+
+ private getProject(document: TextDocumentIdentifier): ProjectService {
+ let fallbackProject: ProjectService
+ for (let [key, project] of this.projects) {
+ '/',
CompletionList,
+import './lib/env'
}
- DocumentColorParams,
+import './lib/env'
import './lib/env'
+ DidChangeWatchedFilesNotification,
- ColorInformation,
+ '/',
createConnection,
+ return project
+ }
+ }
CompletionParams,
+// @ts-ignore
+ '/',
ColorInformation,
+ fallbackProject = project
import {
- createConnection,
+ CodeActionRequest,
ColorInformation,
-import * as parcel from './watcher/index.js'
+import {
ColorInformation,
-import tailwindPlugins from './lib/plugins'
+ CompletionList,
+ return fallbackProject
}
const PACKAGE_GLOB = '{package.json,package-lock.json,yarn.lock,pnpm-lock.yaml}'
- createConnection,
- ColorInformation,
+] as const
import {
- createConnection,
+ }
-const PACKAGE_GLOB = '{package.json,package-lock.json,yarn.lock,pnpm-lock.yaml}'
+ DocumentColorParams,
import './lib/env'
const PACKAGE_GLOB = '{package.json,package-lock.json,yarn.lock,pnpm-lock.yaml}'
- DocumentColorParams,
+ CompletionItem,
+ return this.getProject(params.textDocument)?.onColorPresentation(params) ?? []
}
const PACKAGE_GLOB = '{package.json,package-lock.json,yarn.lock,pnpm-lock.yaml}'
- ColorInformation,
+ CompletionParams,
- ColorInformation,
+ return this.getProject(params.textDocument)?.onHover(params) ?? null
+import './lib/env'
import {
- createConnection,
+ CompletionParams,
+
const PACKAGE_GLOB = '{package.json,package-lock.json,yarn.lock,pnpm-lock.yaml}'
+ createConnection,
+import './lib/env'
import './lib/env'
- return project?.onCompletionResolve(item) ?? null
+import * as path from 'path'
}
ColorInformation,
- DocumentColorParams,
+import { equal } from 'tailwindcss-language-service/src/util/array'
+import './lib/env'
import './lib/env'
+import * as os from 'os'
- ColorInformation,
+import './lib/env'
import {
- createConnection,
+ CompletionParams,
-const PACKAGE_GLOB = '{package.json,package-lock.json,yarn.lock,pnpm-lock.yaml}'
+ DocumentColorParams,
import './lib/env'
const TRIGGER_CHARACTERS = [
-import {
+import './lib/env'
+ return this.getProject(params.textDocument)?.onCodeAction(params) ?? null
}
listen() {