From efb6cff45868e4bf8613605f7b4e9254fb53167e Mon Sep 17 00:00:00 2001 From: Brad Cornes Date: Fri, 7 Oct 2022 17:05:54 +0100 Subject: [PATCH 01/26] wip --- .../tailwindcss-language-server/src/server.ts | 390 +++++++++++++----- 1 file changed, 283 insertions(+), 107 deletions(-) diff --git a/packages/tailwindcss-language-server/src/server.ts b/packages/tailwindcss-language-server/src/server.ts index 15d19007..7c543bfc 100644 --- a/packages/tailwindcss-language-server/src/server.ts +++ b/packages/tailwindcss-language-server/src/server.ts @@ -83,6 +83,7 @@ import { getFileFsPath, normalizeFileNameToFsPath } from './util/uri' import { equal } from 'tailwindcss-language-service/src/util/array' import preflight from 'tailwindcss/lib/css/preflight.css' import merge from 'deepmerge' +import { getTextWithoutComments } from 'tailwindcss-language-service/src/util/doc' // @ts-ignore global.__preflight = preflight @@ -102,6 +103,7 @@ new Function( const CONFIG_FILE_GLOB = '{tailwind,tailwind.config}.{js,cjs}' const PACKAGE_GLOB = '{package-lock.json,yarn.lock,pnpm-lock.yaml}' +const CSS_GLOB = '*.{css,scss,sass,less,pcss}' const TRIGGER_CHARACTERS = [ // class attributes '"', @@ -124,8 +126,8 @@ const colorNames = Object.keys(namedColors) const connection = process.argv.length <= 2 ? createConnection(process.stdin, process.stdout) : createConnection() -console.log = connection.console.log.bind(connection.console) -console.error = connection.console.error.bind(connection.console) +// console.log = connection.console.log.bind(connection.console) +// console.error = connection.console.error.bind(connection.console) process.on('unhandledRejection', (e: any) => { connection.console.error(formatError(`Unhandled exception`, e)) @@ -175,9 +177,11 @@ function firstOptional(...options: Array<() => T>): T | undefined { } interface ProjectService { + enabled: boolean + enable: () => void state: State tryInit: () => Promise - dispose: () => void + dispose: () => Promise onUpdateSettings: (settings: any) => void onFileEvents: (changes: Array<{ file: string; type: FileChangeType }>) => void onHover(params: TextDocumentPositionParams): Promise @@ -189,7 +193,11 @@ interface ProjectService { onCodeAction(params: CodeActionParams): Promise } -type ProjectConfig = { folder: string; configPath?: string; documentSelector?: string[] } +type ProjectConfig = { + folder: string + configPath?: string + documentSelector?: Array<{ pattern: string; priority: number }> +} function getMode(config: any): unknown { if (typeof config.mode !== 'undefined') { @@ -214,6 +222,61 @@ function deleteMode(config: any): void { } } +const documentSettingsCache: Map = new Map() +async function getConfiguration(uri?: string) { + if (documentSettingsCache.has(uri)) { + return documentSettingsCache.get(uri) + } + let [editor, tailwindCSS] = await Promise.all([ + connection.workspace.getConfiguration({ + section: 'editor', + scopeUri: uri, + }), + connection.workspace.getConfiguration({ + section: 'tailwindCSS', + scopeUri: uri, + }), + ]) + editor = isObject(editor) ? editor : {} + tailwindCSS = isObject(tailwindCSS) ? tailwindCSS : {} + + let config: Settings = merge( + { + editor: { tabSize: 2 }, + tailwindCSS: { + emmetCompletions: false, + classAttributes: ['class', 'className', 'ngClass'], + codeActions: true, + hovers: true, + suggestions: true, + validate: true, + colorDecorators: true, + rootFontSize: 16, + lint: { + cssConflict: 'warning', + invalidApply: 'error', + invalidScreen: 'error', + invalidVariant: 'error', + invalidConfigPath: 'error', + invalidTailwindDirective: 'error', + recommendedVariantOrder: 'warning', + }, + showPixelEquivalents: true, + includeLanguages: {}, + files: { exclude: ['**/.git/**', '**/node_modules/**', '**/.hg/**', '**/.svn/**'] }, + experimental: { + classRegex: [], + configFile: null, + }, + }, + }, + { editor, tailwindCSS }, + { arrayMerge: (_destinationArray, sourceArray, _options) => sourceArray } + ) + documentSettingsCache.set(uri, config) + return config +} + async function createProjectService( projectConfig: ProjectConfig, connection: Connection, @@ -221,65 +284,11 @@ async function createProjectService( documentService: DocumentService, updateCapabilities: () => void ): Promise { + let enabled = false const folder = projectConfig.folder - const disposables: Disposable[] = [] - const documentSettingsCache: Map = new Map() - - async function getConfiguration(uri?: string) { - if (documentSettingsCache.has(uri)) { - return documentSettingsCache.get(uri) - } - let [editor, tailwindCSS] = await Promise.all([ - connection.workspace.getConfiguration({ - section: 'editor', - scopeUri: uri, - }), - connection.workspace.getConfiguration({ - section: 'tailwindCSS', - scopeUri: uri, - }), - ]) - editor = isObject(editor) ? editor : {} - tailwindCSS = isObject(tailwindCSS) ? tailwindCSS : {} - - let config: Settings = merge( - { - editor: { tabSize: 2 }, - tailwindCSS: { - emmetCompletions: false, - classAttributes: ['class', 'className', 'ngClass'], - codeActions: true, - hovers: true, - suggestions: true, - validate: true, - colorDecorators: true, - rootFontSize: 16, - lint: { - cssConflict: 'warning', - invalidApply: 'error', - invalidScreen: 'error', - invalidVariant: 'error', - invalidConfigPath: 'error', - invalidTailwindDirective: 'error', - recommendedVariantOrder: 'warning', - }, - showPixelEquivalents: true, - includeLanguages: {}, - files: { exclude: ['**/.git/**', '**/node_modules/**', '**/.hg/**', '**/.svn/**'] }, - experimental: { - classRegex: [], - configFile: null, - }, - }, - }, - { editor, tailwindCSS }, - { arrayMerge: (_destinationArray, sourceArray, _options) => sourceArray } - ) - documentSettingsCache.set(uri, config) - return config - } + const disposables: Array> = [] - const state: State = { + let state: State = { enabled: false, editor: { connection, @@ -305,6 +314,10 @@ async function createProjectService( let ignore = state.editor.globalSettings.tailwindCSS.files.exclude function onFileEvents(changes: Array<{ file: string; type: FileChangeType }>): void { + if (!enabled) { + return + } + let needsInit = false let needsRebuild = false @@ -317,7 +330,9 @@ async function createProjectService( } } - let isConfigFile = minimatch(file, `**/${CONFIG_FILE_GLOB}`, { dot: true }) + let isConfigFile = projectConfig.configPath + ? change.file === projectConfig.configPath + : minimatch(file, `**/${CONFIG_FILE_GLOB}`, { dot: true }) let isPackageFile = minimatch(file, `**/${PACKAGE_GLOB}`, { dot: true }) let isDependency = state.dependencies && state.dependencies.includes(change.file) @@ -351,9 +366,15 @@ async function createProjectService( } if (params.capabilities.workspace?.didChangeWatchedFiles?.dynamicRegistration) { - connection.client.register(DidChangeWatchedFilesNotification.type, { - watchers: [{ globPattern: `**/${CONFIG_FILE_GLOB}` }, { globPattern: `**/${PACKAGE_GLOB}` }], - }) + disposables.push( + connection.client.register(DidChangeWatchedFilesNotification.type, { + watchers: [ + { globPattern: `**/${CONFIG_FILE_GLOB}` }, + { globPattern: `**/${PACKAGE_GLOB}` }, + { globPattern: `**/${CSS_GLOB}` }, + ], + }) + ) } else if (parcel.getBinding()) { let typeMap = { create: FileChangeType.Created, @@ -380,7 +401,7 @@ async function createProjectService( }) } else { let watch: typeof chokidar.watch = require('chokidar').watch - chokidarWatcher = watch([`**/${CONFIG_FILE_GLOB}`, `**/${PACKAGE_GLOB}`], { + chokidarWatcher = watch([`**/${CONFIG_FILE_GLOB}`, `**/${PACKAGE_GLOB}`, `**/${CSS_GLOB}`], { cwd: folder, ignorePermissionErrors: true, ignoreInitial: true, @@ -420,6 +441,9 @@ async function createProjectService( } async function tryInit() { + if (!enabled) { + return + } try { await init() } catch (error) { @@ -429,6 +453,9 @@ async function createProjectService( } async function tryRebuild() { + if (!enabled) { + return + } try { await rebuild() } catch (error) { @@ -964,11 +991,17 @@ async function createProjectService( } return { + enabled, + enable() { + enabled = true + }, state, tryInit, - dispose() { - for (let { dispose } of disposables) { - dispose() + async dispose() { + clearAllDiagnostics(state) + state = { enabled: false } + for (let disposable of disposables) { + ;(await disposable).dispose() } }, async onUpdateSettings(settings: any): Promise { @@ -1350,6 +1383,15 @@ async function getPlugins(config: any) { // } } +async function getConfigFileFromCssFile(cssFile: string): Promise { + let css = getTextWithoutComments(await fs.promises.readFile(cssFile, 'utf8'), 'css') + let match = css.match(/(?:\b|^)@config\s*(?'[^']+'|"[^"]+")/) + if (!match) { + return null + } + return path.resolve(path.dirname(cssFile), match.groups.config.slice(1, -1)) +} + class TW { private initialized = false private workspaces: Map @@ -1357,6 +1399,7 @@ class TW { private documentService: DocumentService public initializeParams: InitializeParams private registrations: Promise + private disposables: Disposable[] = [] constructor(private connection: Connection) { this.documentService = new DocumentService(this.connection) @@ -1392,9 +1435,10 @@ class TW { null ) as Settings['tailwindCSS']['experimental']['configFile'] - if (configFileOrFiles) { - let base = workspaceFolders[0].folder + let base = workspaceFolders[0].folder + let cssFileConfigMap: Map = new Map() + if (configFileOrFiles) { if ( typeof configFileOrFiles !== 'string' && (!isObject(configFileOrFiles) || @@ -1420,10 +1464,64 @@ class TW { configPath: path.resolve(base, relativeConfigPath), documentSelector: [] .concat(relativeDocumentSelectorOrSelectors) - .map((selector) => path.resolve(base, selector)), + .map((selector) => ({ priority: 1, pattern: path.resolve(base, selector) })), } } ) + } else { + let projects: Record> = {} + + let files = await glob([`**/${CONFIG_FILE_GLOB}`, `**/${CSS_GLOB}`], { + cwd: base, + ignore: (await getConfiguration()).tailwindCSS.files.exclude, + onlyFiles: true, + absolute: true, + suppressErrors: true, + dot: true, + concurrency: Math.max(os.cpus().length, 1), + }) + + for (let filename of files) { + try { + let normalizedFilename = normalizePath(filename) + let isCssFile = minimatch(normalizedFilename, `**/${CSS_GLOB}`, { dot: true }) + let configPath = isCssFile ? await getConfigFileFromCssFile(filename) : filename + if (!configPath) { + continue + } + let config = require(configPath) + let content = config.content?.files ?? config.content + content = Array.isArray(content) ? content : [] + // TODO: check version number + // if <3.2 this should always be `false` + let relativeEnabled = + config.future?.relativeContentPathsByDefault || config.content?.relative + let contentBase = relativeEnabled ? path.dirname(configPath) : base + let documentSelector = content + .map((item) => path.resolve(contentBase, item)) + .map((item) => ({ pattern: normalizePath(item), priority: 1 })) + .concat({ + pattern: normalizePath(path.join(path.dirname(filename), '**')), + priority: 2, + }) + .concat({ + pattern: normalizePath(path.join(path.dirname(configPath), '**')), + priority: 3, + }) + projects[configPath] = [...(projects[configPath] ?? []), ...documentSelector] + cssFileConfigMap.set(normalizedFilename, configPath) + } catch {} + } + + if (Object.keys(projects).length > 0) { + workspaceFolders = Object.entries(projects).map(([configPath, documentSelector]) => { + return { + folder: base, + configPath, + documentSelector, + } + }) + } } await Promise.all( @@ -1433,38 +1531,81 @@ class TW { this.setupLSPHandlers() if (this.initializeParams.capabilities.workspace?.didChangeWatchedFiles?.dynamicRegistration) { - this.connection.onDidChangeWatchedFiles(({ changes }) => { + this.disposables.push( + this.connection.onDidChangeWatchedFiles(async ({ changes }) => { + let needsRestart = false + + for (let change of changes) { + let filename = URI.parse(change.uri).fsPath + let normalizedFilename = normalizePath(filename) + let isCssFile = minimatch(normalizedFilename, `**/${CSS_GLOB}`, { + dot: true, + }) + if (isCssFile) { + let configPath = await getConfigFileFromCssFile(filename) + if ( + cssFileConfigMap.has(normalizedFilename) && + cssFileConfigMap.get(normalizedFilename) !== configPath + ) { + needsRestart = true + } else if (!cssFileConfigMap.has(normalizedFilename) && configPath) { + needsRestart = true + } + } + } + + if (needsRestart) { + this.restart() + return + } + + for (let [, project] of this.projects) { + project.onFileEvents( + changes.map(({ uri, type }) => ({ + file: URI.parse(uri).fsPath, + type, + })) + ) + } + }) + ) + } + + this.disposables.push( + this.connection.onDidChangeConfiguration(async ({ settings }) => { for (let [, project] of this.projects) { - project.onFileEvents( - changes.map(({ uri, type }) => ({ - file: URI.parse(uri).fsPath, - type, - })) - ) + project.onUpdateSettings(settings) } }) - } + ) - this.connection.onDidChangeConfiguration(async ({ settings }) => { - for (let [, project] of this.projects) { - project.onUpdateSettings(settings) - } - }) + this.disposables.push( + this.connection.onShutdown(() => { + this.dispose() + }) + ) - this.connection.onShutdown(() => { - this.dispose() - }) + this.disposables.push( + this.documentService.onDidChangeContent((change) => { + this.getProject(change.document)?.provideDiagnostics(change.document) + }) + ) - this.documentService.onDidChangeContent((change) => { - this.getProject(change.document)?.provideDiagnostics(change.document) - }) + this.disposables.push( + this.documentService.onDidOpen((event) => { + let project = this.getProject(event.document) + if (project && !project.enabled) { + project.enable() + project.tryInit() + } + }) + ) } private async addProject(projectConfig: ProjectConfig, params: InitializeParams): Promise { let key = JSON.stringify(projectConfig) - if (this.projects.has(key)) { - await this.projects.get(key).tryInit() - } else { + + if (!this.projects.has(key)) { const project = await createProjectService( projectConfig, this.connection, @@ -1473,17 +1614,24 @@ class TW { () => this.updateCapabilities() ) this.projects.set(key, project) - await project.tryInit() + } + + for (let document of this.documentService.getAllDocuments()) { + if (this.getProject(document) === this.projects.get(key)) { + this.projects.get(key).enable() + await this.projects.get(key).tryInit() + break + } } } private setupLSPHandlers() { - this.connection.onHover(this.onHover.bind(this)) - this.connection.onCompletion(this.onCompletion.bind(this)) - this.connection.onCompletionResolve(this.onCompletionResolve.bind(this)) - this.connection.onDocumentColor(this.onDocumentColor.bind(this)) - this.connection.onColorPresentation(this.onColorPresentation.bind(this)) - this.connection.onCodeAction(this.onCodeAction.bind(this)) + this.disposables.push(this.connection.onHover(this.onHover.bind(this))) + this.disposables.push(this.connection.onCompletion(this.onCompletion.bind(this))) + this.disposables.push(this.connection.onCompletionResolve(this.onCompletionResolve.bind(this))) + this.disposables.push(this.connection.onDocumentColor(this.onDocumentColor.bind(this))) + this.disposables.push(this.connection.onColorPresentation(this.onColorPresentation.bind(this))) + this.disposables.push(this.connection.onCodeAction(this.onCodeAction.bind(this))) } private updateCapabilities() { @@ -1509,9 +1657,10 @@ class TW { }) capabilities.add(DidChangeWatchedFilesNotification.type, { - watchers: projects.flatMap( - (project) => project.state.dependencies?.map((file) => ({ globPattern: file })) ?? [] - ), + watchers: projects.flatMap((project) => [ + { globPattern: project.state.configPath }, + ...(project.state.dependencies?.map((file) => ({ globPattern: file })) ?? []), + ]), }) this.registrations = this.connection.client.register(capabilities) @@ -1519,12 +1668,16 @@ class TW { private getProject(document: TextDocumentIdentifier): ProjectService { let fallbackProject: ProjectService + let matchedProject: ProjectService + let matchedPriority: number = Infinity + 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 + for (let { pattern, priority } of projectConfig.documentSelector) { + if (minimatch(URI.parse(document.uri).fsPath, pattern) && priority < matchedPriority) { + matchedProject = project + matchedPriority = priority } } } else { @@ -1533,6 +1686,11 @@ class TW { } } } + + if (matchedProject) { + return matchedProject + } + return fallbackProject } @@ -1568,6 +1726,21 @@ class TW { for (let [, project] of this.projects) { project.dispose() } + this.projects = new Map() + + if (this.registrations) { + this.registrations.then((r) => r.dispose()) + this.registrations = undefined + } + + this.disposables.forEach((d) => d.dispose()) + this.disposables.length = 0 + } + + restart(): void { + this.dispose() + this.initialized = false + this.init() } } @@ -1593,6 +1766,9 @@ class DocumentService { get onDidClose() { return this.documents.onDidClose } + get onDidOpen() { + return this.documents.onDidOpen + } } function supportsDynamicRegistration(connection: Connection, params: InitializeParams): boolean { From 795b40c199b3b12fd9b16d36207863bdff2b450e Mon Sep 17 00:00:00 2001 From: Brad Cornes Date: Wed, 12 Oct 2022 16:44:34 +0100 Subject: [PATCH 02/26] wip --- .../tailwindcss-language-server/src/server.ts | 698 ++++++++++++------ .../src/codeActions/codeActionProvider.ts | 4 + 2 files changed, 468 insertions(+), 234 deletions(-) diff --git a/packages/tailwindcss-language-server/src/server.ts b/packages/tailwindcss-language-server/src/server.ts index 7c543bfc..03276902 100644 --- a/packages/tailwindcss-language-server/src/server.ts +++ b/packages/tailwindcss-language-server/src/server.ts @@ -177,8 +177,9 @@ function firstOptional(...options: Array<() => T>): T | undefined { } interface ProjectService { - enabled: boolean + enabled: () => boolean enable: () => void + documentSelector: () => Array state: State tryInit: () => Promise dispose: () => Promise @@ -188,6 +189,7 @@ interface ProjectService { onCompletion(params: CompletionParams): Promise onCompletionResolve(item: CompletionItem): Promise provideDiagnostics(document: TextDocument): void + provideDiagnosticsForce(document: TextDocument): void onDocumentColor(params: DocumentColorParams): Promise onColorPresentation(params: ColorPresentationParams): Promise onCodeAction(params: CodeActionParams): Promise @@ -196,9 +198,11 @@ interface ProjectService { type ProjectConfig = { folder: string configPath?: string - documentSelector?: Array<{ pattern: string; priority: number }> + documentSelector?: Array } +type DocumentSelector = { pattern: string; priority: number } + function getMode(config: any): unknown { if (typeof config.mode !== 'undefined') { return config.mode @@ -277,16 +281,57 @@ async function getConfiguration(uri?: string) { return config } +function clearRequireCache(): void { + Object.keys(require.cache).forEach((key) => { + if (!key.endsWith('.node')) { + delete require.cache[key] + } + }) + Object.keys((Module as any)._pathCache).forEach((key) => { + delete (Module as any)._pathCache[key] + }) +} + +function withoutLogs(getter: () => T): T { + let fns = { + log: console.log, + warn: console.warn, + error: console.error, + } + for (let key in fns) { + console[key] = () => {} + } + try { + return getter() + } finally { + for (let key in fns) { + console[key] = fns[key] + } + } +} + +function withFallback(getter: () => T, fallback: T): T { + try { + return getter() + } catch (e) { + return fallback + } +} + async function createProjectService( projectConfig: ProjectConfig, connection: Connection, params: InitializeParams, documentService: DocumentService, - updateCapabilities: () => void + updateCapabilities: () => void, + checkOpenDocuments: () => void, + refreshDiagnostics: () => void, + watchPatterns: (patterns: string[]) => void ): Promise { let enabled = false const folder = projectConfig.folder const disposables: Array> = [] + let documentSelector = projectConfig.documentSelector let state: State = { enabled: false, @@ -310,14 +355,14 @@ async function createProjectService( }, } + function log(...args: any[]): void { + console.log(`[${path.relative(projectConfig.folder, projectConfig.configPath)}]`, ...args) + } + let chokidarWatcher: chokidar.FSWatcher let ignore = state.editor.globalSettings.tailwindCSS.files.exclude function onFileEvents(changes: Array<{ file: string; type: FileChangeType }>): void { - if (!enabled) { - return - } - let needsInit = false let needsRebuild = false @@ -338,10 +383,37 @@ async function createProjectService( if (!isConfigFile && !isPackageFile && !isDependency) continue + if (!enabled) { + if (projectConfig.configPath && (isConfigFile || isDependency)) { + // update document selector + let originalConfig = require(projectConfig.configPath) + let contentConfig: unknown = originalConfig.content?.files ?? originalConfig.content + let content = Array.isArray(contentConfig) ? contentConfig : [] + // TODO: check version number + // if <3.2 this should always be `false` + let relativeEnabled = + originalConfig.future?.relativeContentPathsByDefault || originalConfig.content?.relative + let contentBase = relativeEnabled ? path.dirname(state.configPath) : projectConfig.folder + let contentSelector = content + .filter((item): item is string => typeof item === 'string') + .map((item) => path.resolve(contentBase, item)) + .map((item) => ({ pattern: normalizePath(item), priority: 1 })) + documentSelector = [ + ...documentSelector.filter(({ priority }) => priority !== 1), + ...contentSelector, + ] + + checkOpenDocuments() + } + continue + } + if (change.type === FileChangeType.Created) { + log('File created:', change.file) needsInit = true break } else if (change.type === FileChangeType.Changed) { + log('File changed:', change.file) if (!state.enabled || isPackageFile) { needsInit = true break @@ -349,6 +421,7 @@ async function createProjectService( needsRebuild = true } } else if (change.type === FileChangeType.Deleted) { + log('File deleted:', change.file) if (!state.enabled || isPackageFile || isConfigFile) { needsInit = true break @@ -365,71 +438,9 @@ async function createProjectService( } } - if (params.capabilities.workspace?.didChangeWatchedFiles?.dynamicRegistration) { - disposables.push( - connection.client.register(DidChangeWatchedFilesNotification.type, { - watchers: [ - { globPattern: `**/${CONFIG_FILE_GLOB}` }, - { globPattern: `**/${PACKAGE_GLOB}` }, - { globPattern: `**/${CSS_GLOB}` }, - ], - }) - ) - } else if (parcel.getBinding()) { - let typeMap = { - create: FileChangeType.Created, - update: FileChangeType.Changed, - delete: FileChangeType.Deleted, - } - - let subscription = await parcel.subscribe( - folder, - (err, events) => { - onFileEvents(events.map((event) => ({ file: event.path, type: typeMap[event.type] }))) - }, - { - ignore: ignore.map((ignorePattern) => - path.resolve(folder, ignorePattern.replace(/^[*/]+/, '').replace(/[*/]+$/, '')) - ), - } - ) - - disposables.push({ - dispose() { - subscription.unsubscribe() - }, - }) - } else { - let watch: typeof chokidar.watch = require('chokidar').watch - chokidarWatcher = watch([`**/${CONFIG_FILE_GLOB}`, `**/${PACKAGE_GLOB}`, `**/${CSS_GLOB}`], { - cwd: folder, - ignorePermissionErrors: true, - ignoreInitial: true, - ignored: ignore, - awaitWriteFinish: { - stabilityThreshold: 100, - pollInterval: 20, - }, - }) - - await new Promise((resolve) => { - chokidarWatcher.on('ready', () => resolve()) - }) - - chokidarWatcher - .on('add', (file) => onFileEvents([{ file, type: FileChangeType.Created }])) - .on('change', (file) => onFileEvents([{ file, type: FileChangeType.Changed }])) - .on('unlink', (file) => onFileEvents([{ file, type: FileChangeType.Deleted }])) - - disposables.push({ - dispose() { - chokidarWatcher.close() - }, - }) - } - function resetState(): void { - clearAllDiagnostics(state) + // clearAllDiagnostics(state) + refreshDiagnostics() Object.keys(state).forEach((key) => { // Keep `dependencies` to ensure that they are still watched if (key !== 'editor' && key !== 'dependencies') { @@ -464,18 +475,9 @@ async function createProjectService( } } - function clearRequireCache(): void { - Object.keys(require.cache).forEach((key) => { - if (!key.endsWith('.node')) { - delete require.cache[key] - } - }) - Object.keys((Module as any)._pathCache).forEach((key) => { - delete (Module as any)._pathCache[key] - }) - } - async function init() { + log('Initializing...') + clearRequireCache() let configPath = projectConfig.configPath @@ -572,14 +574,14 @@ async function createProjectService( return } - console.log(`Found Tailwind CSS config file: ${configPath}`) + log(`Loaded Tailwind CSS config file: ${configPath}`) postcss = require(postcssPath) postcssSelectorParser = require(postcssSelectorParserPath) - console.log(`Loaded postcss v${postcssVersion}: ${postcssDir}`) + log(`Loaded postcss v${postcssVersion}: ${postcssDir}`) tailwindcss = require(tailwindcssPath) - console.log(`Loaded tailwindcss v${tailwindcssVersion}: ${tailwindDir}`) + log(`Loaded tailwindcss v${tailwindcssVersion}: ${tailwindDir}`) try { resolveConfigFn = require(resolveFrom(tailwindDir, './resolveConfig.js')) @@ -726,9 +728,9 @@ async function createProjectService( module: require('tailwindcss/lib/lib/expandApplyAtRules').default, }, } - console.log('Failed to load workspace modules.') - console.log(`Using bundled version of \`tailwindcss\`: v${tailwindcssVersion}`) - console.log(`Using bundled version of \`postcss\`: v${postcssVersion}`) + log('Failed to load workspace modules.') + log(`Using bundled version of \`tailwindcss\`: v${tailwindcssVersion}`) + log(`Using bundled version of \`postcss\`: v${postcssVersion}`) } state.configPath = configPath @@ -816,6 +818,8 @@ async function createProjectService( } async function rebuild() { + log('Building...') + clearRequireCache() const { tailwindcss, postcss, resolveConfig } = state.modules @@ -911,6 +915,24 @@ async function createProjectService( throw new SilentError(`Failed to load config file: ${state.configPath}`) } + ///////////////////// + let contentConfig: unknown = originalConfig.content?.files ?? originalConfig.content + let content = Array.isArray(contentConfig) ? contentConfig : [] + // TODO: check version number + // if <3.2 this should always be `false` + let relativeEnabled = + originalConfig.future?.relativeContentPathsByDefault || originalConfig.content?.relative + let contentBase = relativeEnabled ? path.dirname(state.configPath) : projectConfig.folder + let contentSelector = content + .filter((item): item is string => typeof item === 'string') + .map((item) => path.resolve(contentBase, item)) + .map((item) => ({ pattern: normalizePath(item), priority: 1 })) + documentSelector = [ + ...documentSelector.filter(({ priority }) => priority !== 1), + ...contentSelector, + ] + ////////////////////// + try { state.config = resolveConfig.module(originalConfig) state.separator = state.config.separator @@ -966,11 +988,12 @@ async function createProjectService( } } - if (state.dependencies) { - chokidarWatcher?.unwatch(state.dependencies) - } + // if (state.dependencies) { + // chokidarWatcher?.unwatch(state.dependencies) + // } state.dependencies = getModuleDependencies(state.configPath) - chokidarWatcher?.add(state.dependencies) + // chokidarWatcher?.add(state.dependencies) + watchPatterns([state.configPath, ...(state.dependencies ?? [])]) state.configId = getConfigId(state.configPath, state.dependencies) @@ -985,20 +1008,25 @@ async function createProjectService( state.enabled = true - updateAllDiagnostics(state) + // updateAllDiagnostics(state) + refreshDiagnostics() updateCapabilities() } return { - enabled, + enabled() { + return enabled + }, enable() { enabled = true }, state, + documentSelector() { + return documentSelector + }, tryInit, async dispose() { - clearAllDiagnostics(state) state = { enabled: false } for (let disposable of disposables) { ;(await disposable).dispose() @@ -1023,53 +1051,67 @@ async function createProjectService( }, onFileEvents, async onHover(params: TextDocumentPositionParams): Promise { - if (!state.enabled) return null - let document = documentService.getDocument(params.textDocument.uri) - if (!document) return null - let settings = await state.editor.getConfiguration(document.uri) - if (!settings.tailwindCSS.hovers) return null - if (await isExcluded(state, document)) return null - return doHover(state, document, params.position) + return withFallback(async () => { + if (!state.enabled) return null + let document = documentService.getDocument(params.textDocument.uri) + if (!document) return null + let settings = await state.editor.getConfiguration(document.uri) + if (!settings.tailwindCSS.hovers) return null + if (await isExcluded(state, document)) return null + return doHover(state, document, params.position) + }, null) }, async onCompletion(params: CompletionParams): Promise { - if (!state.enabled) return null - let document = documentService.getDocument(params.textDocument.uri) - 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 - 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 }, - })), - } + return withFallback(async () => { + if (!state.enabled) return null + let document = documentService.getDocument(params.textDocument.uri) + 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 + 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 }, + })), + } + }, null) }, onCompletionResolve(item: CompletionItem): Promise { - if (!state.enabled) return null - return resolveCompletionItem(state, { ...item, data: item.data?.originalData }) + return withFallback(() => { + if (!state.enabled) return null + return resolveCompletionItem(state, { ...item, data: item.data?.originalData }) + }, null) }, async onCodeAction(params: CodeActionParams): Promise { - if (!state.enabled) return null - let document = documentService.getDocument(params.textDocument.uri) - if (!document) return null - let settings = await state.editor.getConfiguration(document.uri) - if (!settings.tailwindCSS.codeActions) return null - return doCodeActions(state, params) + return withFallback(async () => { + if (!state.enabled) return null + let document = documentService.getDocument(params.textDocument.uri) + if (!document) return null + let settings = await state.editor.getConfiguration(document.uri) + if (!settings.tailwindCSS.codeActions) return null + return doCodeActions(state, params) + }, null) }, provideDiagnostics: debounce((document: TextDocument) => { if (!state.enabled) return provideDiagnostics(state, document) }, 500), + provideDiagnosticsForce: (document: TextDocument) => { + if (!state.enabled) return + provideDiagnostics(state, document) + }, async onDocumentColor(params: DocumentColorParams): Promise { - if (!state.enabled) return [] - let document = documentService.getDocument(params.textDocument.uri) - if (!document) return [] - if (await isExcluded(state, document)) return null - return getDocumentColors(state, document) + return withFallback(async () => { + if (!state.enabled) return [] + let document = documentService.getDocument(params.textDocument.uri) + if (!document) return [] + if (await isExcluded(state, document)) return null + return getDocumentColors(state, document) + }, null) }, async onColorPresentation(params: ColorPresentationParams): Promise { let document = documentService.getDocument(params.textDocument.uri) @@ -1238,19 +1280,21 @@ function getVariants(state: State): Record { for (let fn of fns) { let definition: string let container = root.clone() - let returnValue = fn({ - container, - separator: state.separator, - modifySelectors, - format: (def: string) => { - definition = def.replace(/:merge\(([^)]+)\)/g, '$1') - }, - wrap: (rule: Container) => { - if (isAtRule(rule)) { - definition = `@${rule.name} ${rule.params}` - } - }, - }) + let returnValue = withoutLogs(() => + fn({ + container, + separator: state.separator, + modifySelectors, + format: (def: string) => { + definition = def.replace(/:merge\(([^)]+)\)/g, '$1') + }, + wrap: (rule: Container) => { + if (isAtRule(rule)) { + definition = `@${rule.name} ${rule.params}` + } + }, + }) + ) if (!definition) { definition = returnValue @@ -1394,12 +1438,15 @@ async function getConfigFileFromCssFile(cssFile: string): Promise class TW { private initialized = false + private lspHandlersAdded = false private workspaces: Map private projects: Map private documentService: DocumentService public initializeParams: InitializeParams private registrations: Promise private disposables: Disposable[] = [] + private watchPatterns: (patterns: string[]) => void + private watched: string[] = [] constructor(private connection: Connection) { this.documentService = new DocumentService(this.connection) @@ -1410,32 +1457,24 @@ class TW { async init(): Promise { if (this.initialized) return - this.initialized = true + clearRequireCache() - // TODO - let workspaceFolders: Array = - false && - Array.isArray(this.initializeParams.workspaceFolders) && - this.initializeParams.capabilities.workspace?.workspaceFolders - ? this.initializeParams.workspaceFolders.map((el) => ({ - folder: getFileFsPath(el.uri), - })) - : this.initializeParams.rootPath - ? [{ folder: normalizeFileNameToFsPath(this.initializeParams.rootPath) }] - : [] + this.initialized = true - if (workspaceFolders.length === 0) { + if (!this.initializeParams.rootPath) { console.error('No workspace folders found, not initializing.') return } + let workspaceFolders: Array = [] + let configFileOrFiles = dlv( await connection.workspace.getConfiguration('tailwindCSS'), 'experimental.configFile', null ) as Settings['tailwindCSS']['experimental']['configFile'] - let base = workspaceFolders[0].folder + let base = normalizeFileNameToFsPath(this.initializeParams.rootPath) let cssFileConfigMap: Map = new Map() if (configFileOrFiles) { @@ -1469,7 +1508,7 @@ class TW { } ) } else { - let projects: Record> = {} + let projects: Record> = {} let files = await glob([`**/${CONFIG_FILE_GLOB}`, `**/${CSS_GLOB}`], { cwd: base, @@ -1482,35 +1521,50 @@ class TW { }) for (let filename of files) { + let normalizedFilename = normalizePath(filename) + let isCssFile = minimatch(normalizedFilename, `**/${CSS_GLOB}`, { dot: true }) + let configPath = isCssFile ? await getConfigFileFromCssFile(filename) : filename + if (!configPath) { + continue + } + + let contentSelector: Array = [] try { - let normalizedFilename = normalizePath(filename) - let isCssFile = minimatch(normalizedFilename, `**/${CSS_GLOB}`, { dot: true }) - let configPath = isCssFile ? await getConfigFileFromCssFile(filename) : filename - if (!configPath) { - continue - } let config = require(configPath) - let content = config.content?.files ?? config.content - content = Array.isArray(content) ? content : [] + let contentConfig: unknown = config.content?.files ?? config.content + let content = Array.isArray(contentConfig) ? contentConfig : [] // TODO: check version number // if <3.2 this should always be `false` let relativeEnabled = config.future?.relativeContentPathsByDefault || config.content?.relative let contentBase = relativeEnabled ? path.dirname(configPath) : base - let documentSelector = content + contentSelector = content + .filter((item): item is string => typeof item === 'string') .map((item) => path.resolve(contentBase, item)) .map((item) => ({ pattern: normalizePath(item), priority: 1 })) - .concat({ - pattern: normalizePath(path.join(path.dirname(filename), '**')), - priority: 2, - }) - .concat({ - pattern: normalizePath(path.join(path.dirname(configPath), '**')), - priority: 3, - }) - projects[configPath] = [...(projects[configPath] ?? []), ...documentSelector] - cssFileConfigMap.set(normalizedFilename, configPath) } catch {} + + let documentSelector = contentSelector + .concat({ + pattern: normalizePath(filename), + priority: 0, + }) + .concat({ + pattern: normalizePath(configPath), + priority: 0, + }) + .concat({ + pattern: normalizePath(path.join(path.dirname(filename), '**')), + priority: 2, + }) + .concat({ + pattern: normalizePath(path.join(path.dirname(configPath), '**')), + priority: 3, + }) + projects[configPath] = [...(projects[configPath] ?? []), ...documentSelector] + if (isCssFile) { + cssFileConfigMap.set(normalizedFilename, configPath) + } } if (Object.keys(projects).length > 0) { @@ -1524,53 +1578,201 @@ class TW { } } - await Promise.all( - workspaceFolders.map((projectConfig) => this.addProject(projectConfig, this.initializeParams)) - ) + console.log('[Global]', 'Creating projects:', JSON.stringify(workspaceFolders)) - this.setupLSPHandlers() + const onDidChangeWatchedFiles = async ( + changes: Array<{ file: string; type: FileChangeType }> + ): Promise<{ needsRestart: boolean }> => { + let needsRestart = false + + for (let change of changes) { + let normalizedFilename = normalizePath(change.file) + let isCssFile = minimatch(normalizedFilename, `**/${CSS_GLOB}`, { + dot: true, + }) + if (isCssFile) { + let configPath = await getConfigFileFromCssFile(change.file) + if ( + cssFileConfigMap.has(normalizedFilename) && + cssFileConfigMap.get(normalizedFilename) !== configPath + ) { + needsRestart = true + break + } else if (!cssFileConfigMap.has(normalizedFilename) && configPath) { + needsRestart = true + break + } + } + + let isConfigFile = minimatch(normalizedFilename, `**/${CONFIG_FILE_GLOB}`, { + dot: true, + }) + if (isConfigFile && change.type === FileChangeType.Created) { + needsRestart = true + break + } + + for (let [key] of this.projects) { + let projectConfig = JSON.parse(key) as ProjectConfig + if (change.file === projectConfig.configPath && change.type === FileChangeType.Deleted) { + needsRestart = true + break + } + } + } + + if (needsRestart) { + this.restart() + return + } + + for (let [, project] of this.projects) { + project.onFileEvents(changes) + } + } + + let ignore = (await getConfiguration()).tailwindCSS.files.exclude + // let watchPatterns: (patterns: string[]) => void = () => {} if (this.initializeParams.capabilities.workspace?.didChangeWatchedFiles?.dynamicRegistration) { this.disposables.push( this.connection.onDidChangeWatchedFiles(async ({ changes }) => { - let needsRestart = false + let normalizedChanges = changes + .map(({ uri, type }) => ({ + file: URI.parse(uri).fsPath, + type, + })) + .filter( + (change, changeIndex, changes) => + changes.findIndex((c) => c.file === change.file && c.type === change.type) === + changeIndex + ) + + await onDidChangeWatchedFiles(normalizedChanges) + }) + ) + + let disposable = await this.connection.client.register( + DidChangeWatchedFilesNotification.type, + { + watchers: [ + { globPattern: `**/${CONFIG_FILE_GLOB}` }, + { globPattern: `**/${PACKAGE_GLOB}` }, + { globPattern: `**/${CSS_GLOB}` }, + ], + } + ) - for (let change of changes) { - let filename = URI.parse(change.uri).fsPath - let normalizedFilename = normalizePath(filename) - let isCssFile = minimatch(normalizedFilename, `**/${CSS_GLOB}`, { - dot: true, + this.disposables.push(disposable) + + this.watchPatterns = (patterns) => { + let newPatterns = this.filterNewWatchPatterns(patterns) + if (newPatterns.length) { + console.log('[Global]', 'Adding watch patterns:', newPatterns.join(', ')) + this.connection.client + .register(DidChangeWatchedFilesNotification.type, { + watchers: newPatterns.map((pattern) => ({ globPattern: pattern })), }) - if (isCssFile) { - let configPath = await getConfigFileFromCssFile(filename) - if ( - cssFileConfigMap.has(normalizedFilename) && - cssFileConfigMap.get(normalizedFilename) !== configPath - ) { - needsRestart = true - } else if (!cssFileConfigMap.has(normalizedFilename) && configPath) { - needsRestart = true - } - } - } + .then((disposable) => { + this.disposables.push(disposable) + }) + } + } + } else if (parcel.getBinding()) { + let typeMap = { + create: FileChangeType.Created, + update: FileChangeType.Changed, + delete: FileChangeType.Deleted, + } - if (needsRestart) { - this.restart() - return - } + let subscription = await parcel.subscribe( + base, + (err, events) => { + onDidChangeWatchedFiles( + events.map((event) => ({ file: event.path, type: typeMap[event.type] })) + ) + }, + { + ignore: ignore.map((ignorePattern) => + path.resolve(base, ignorePattern.replace(/^[*/]+/, '').replace(/[*/]+$/, '')) + ), + } + ) - for (let [, project] of this.projects) { - project.onFileEvents( - changes.map(({ uri, type }) => ({ - file: URI.parse(uri).fsPath, - type, - })) - ) - } - }) + this.disposables.push({ + dispose() { + subscription.unsubscribe() + }, + }) + } else { + let watch: typeof chokidar.watch = require('chokidar').watch + let chokidarWatcher = watch( + [`**/${CONFIG_FILE_GLOB}`, `**/${PACKAGE_GLOB}`, `**/${CSS_GLOB}`], + { + cwd: base, + ignorePermissionErrors: true, + ignoreInitial: true, + ignored: ignore, + awaitWriteFinish: { + stabilityThreshold: 100, + pollInterval: 20, + }, + } ) + + await new Promise((resolve) => { + chokidarWatcher.on('ready', () => resolve()) + }) + + chokidarWatcher + .on('add', (file) => + onDidChangeWatchedFiles([ + { file: path.resolve(base, file), type: FileChangeType.Created }, + ]) + ) + .on('change', (file) => + onDidChangeWatchedFiles([ + { file: path.resolve(base, file), type: FileChangeType.Changed }, + ]) + ) + .on('unlink', (file) => + onDidChangeWatchedFiles([ + { file: path.resolve(base, file), type: FileChangeType.Deleted }, + ]) + ) + + this.disposables.push({ + dispose() { + chokidarWatcher.close() + }, + }) + + this.watchPatterns = (patterns) => { + let newPatterns = this.filterNewWatchPatterns(patterns) + if (newPatterns.length) { + console.log('[Global]', 'Adding watch patterns:', newPatterns.join(', ')) + chokidarWatcher.add(newPatterns) + } + } } + await Promise.all( + workspaceFolders.map((projectConfig) => + this.addProject(projectConfig, this.initializeParams, this.watchPatterns) + ) + ) + + // init projects for documents that are _already_ open + for (let document of this.documentService.getAllDocuments()) { + let project = this.getProject(document) + if (project && !project.enabled()) { + project.enable() + await project.tryInit() + } + } + + this.setupLSPHandlers() + this.disposables.push( this.connection.onDidChangeConfiguration(async ({ settings }) => { for (let [, project] of this.projects) { @@ -1594,7 +1796,7 @@ class TW { this.disposables.push( this.documentService.onDidOpen((event) => { let project = this.getProject(event.document) - if (project && !project.enabled) { + if (project && !project.enabled()) { project.enable() project.tryInit() } @@ -1602,7 +1804,17 @@ class TW { ) } - private async addProject(projectConfig: ProjectConfig, params: InitializeParams): Promise { + private filterNewWatchPatterns(patterns: string[]) { + let newWatchPatterns = patterns.filter((pattern) => !this.watched.includes(pattern)) + this.watched.push(...newWatchPatterns) + return newWatchPatterns + } + + private async addProject( + projectConfig: ProjectConfig, + params: InitializeParams, + watchPatterns: (patterns: string[]) => void + ): Promise { let key = JSON.stringify(projectConfig) if (!this.projects.has(key)) { @@ -1611,27 +1823,47 @@ class TW { this.connection, params, this.documentService, - () => this.updateCapabilities() + () => this.updateCapabilities(), + () => { + for (let document of this.documentService.getAllDocuments()) { + let project = this.getProject(document) + if (project && !project.enabled()) { + project.enable() + project.tryInit() + break + } + } + }, + () => this.refreshDiagnostics(), + (patterns: string[]) => watchPatterns(patterns) ) this.projects.set(key, project) } + } - for (let document of this.documentService.getAllDocuments()) { - if (this.getProject(document) === this.projects.get(key)) { - this.projects.get(key).enable() - await this.projects.get(key).tryInit() - break + private refreshDiagnostics() { + for (let doc of this.documentService.getAllDocuments()) { + let project = this.getProject(doc) + if (project) { + project.provideDiagnosticsForce(doc) + } else { + this.connection.sendDiagnostics({ uri: doc.uri, diagnostics: [] }) } } } private setupLSPHandlers() { - this.disposables.push(this.connection.onHover(this.onHover.bind(this))) - this.disposables.push(this.connection.onCompletion(this.onCompletion.bind(this))) - this.disposables.push(this.connection.onCompletionResolve(this.onCompletionResolve.bind(this))) - this.disposables.push(this.connection.onDocumentColor(this.onDocumentColor.bind(this))) - this.disposables.push(this.connection.onColorPresentation(this.onColorPresentation.bind(this))) - this.disposables.push(this.connection.onCodeAction(this.onCodeAction.bind(this))) + if (this.lspHandlersAdded) { + return + } + this.lspHandlersAdded = true + + this.connection.onHover(this.onHover.bind(this)) + this.connection.onCompletion(this.onCompletion.bind(this)) + this.connection.onCompletionResolve(this.onCompletionResolve.bind(this)) + this.connection.onDocumentColor(this.onDocumentColor.bind(this)) + this.connection.onColorPresentation(this.onColorPresentation.bind(this)) + this.connection.onCodeAction(this.onCodeAction.bind(this)) } private updateCapabilities() { @@ -1656,13 +1888,6 @@ class TW { ].filter(Boolean), }) - capabilities.add(DidChangeWatchedFilesNotification.type, { - watchers: projects.flatMap((project) => [ - { globPattern: project.state.configPath }, - ...(project.state.dependencies?.map((file) => ({ globPattern: file })) ?? []), - ]), - }) - this.registrations = this.connection.client.register(capabilities) } @@ -1674,7 +1899,7 @@ class TW { for (let [key, project] of this.projects) { let projectConfig = JSON.parse(key) as ProjectConfig if (projectConfig.configPath) { - for (let { pattern, priority } of projectConfig.documentSelector) { + for (let { pattern, priority } of project.documentSelector()) { if (minimatch(URI.parse(document.uri).fsPath, pattern) && priority < matchedPriority) { matchedProject = project matchedPriority = priority @@ -1728,6 +1953,8 @@ class TW { } this.projects = new Map() + this.refreshDiagnostics() + if (this.registrations) { this.registrations.then((r) => r.dispose()) this.registrations = undefined @@ -1735,9 +1962,12 @@ class TW { this.disposables.forEach((d) => d.dispose()) this.disposables.length = 0 + + this.watched.length = 0 } restart(): void { + console.log('----------\nRESTARTING\n----------') this.dispose() this.initialized = false this.init() diff --git a/packages/tailwindcss-language-service/src/codeActions/codeActionProvider.ts b/packages/tailwindcss-language-service/src/codeActions/codeActionProvider.ts index cc3fbc63..c18edda8 100644 --- a/packages/tailwindcss-language-service/src/codeActions/codeActionProvider.ts +++ b/packages/tailwindcss-language-service/src/codeActions/codeActionProvider.ts @@ -40,6 +40,10 @@ async function getDiagnosticsFromCodeActionParams( } export async function doCodeActions(state: State, params: CodeActionParams): Promise { + if (!state.enabled) { + return [] + } + let diagnostics = await getDiagnosticsFromCodeActionParams( state, params, From 0385d3d652789aa1ec79fd7cdb419d643a45815e Mon Sep 17 00:00:00 2001 From: Brad Cornes Date: Wed, 12 Oct 2022 17:37:11 +0100 Subject: [PATCH 03/26] Boot client if a CSS file contains `@config` --- packages/vscode-tailwindcss/src/extension.ts | 49 ++++++++++++++++---- 1 file changed, 41 insertions(+), 8 deletions(-) diff --git a/packages/vscode-tailwindcss/src/extension.ts b/packages/vscode-tailwindcss/src/extension.ts index 1677c1b2..fee51aab 100755 --- a/packages/vscode-tailwindcss/src/extension.ts +++ b/packages/vscode-tailwindcss/src/extension.ts @@ -48,6 +48,7 @@ const CLIENT_ID = 'tailwindcss-intellisense' const CLIENT_NAME = 'Tailwind CSS IntelliSense' const CONFIG_FILE_GLOB = '{tailwind,tailwind.config}.{js,cjs}' +const CSS_GLOB = '*.{css,scss,sass,less,pcss}' let clients: Map = new Map() let languages: Map = new Map() @@ -130,6 +131,11 @@ function mergeExcludes(settings: WorkspaceConfiguration, scope: ConfigurationSco } } +async function fileContainsAtConfig(uri: Uri) { + let contents = (await Workspace.fs.readFile(uri)).toString() + return /(^|\b)@config\s*['"]/.test(contents) +} + export async function activate(context: ExtensionContext) { let module = context.asAbsolutePath(path.join('dist', 'server.js')) let prod = path.join('dist', 'tailwindServer.js') @@ -149,20 +155,36 @@ export async function activate(context: ExtensionContext) { }) ) - let watcher = Workspace.createFileSystemWatcher(`**/${CONFIG_FILE_GLOB}`, false, true, true) + let configWatcher = Workspace.createFileSystemWatcher(`**/${CONFIG_FILE_GLOB}`, false, true, true) - watcher.onDidCreate((uri) => { + configWatcher.onDidCreate((uri) => { let folder = Workspace.getWorkspaceFolder(uri) - if (!folder) { + if (!folder || isExcluded(uri.fsPath, folder)) { return } - if (!isExcluded(uri.fsPath, folder)) { + folder = getOuterMostWorkspaceFolder(folder) + bootWorkspaceClient(folder) + }) + + context.subscriptions.push(configWatcher) + + let cssWatcher = Workspace.createFileSystemWatcher(`**/${CSS_GLOB}`, false, false, true) + + async function bootClientIfCssFileContainsAtConfig(uri: Uri) { + let folder = Workspace.getWorkspaceFolder(uri) + if (!folder || isExcluded(uri.fsPath, folder)) { + return + } + if (await fileContainsAtConfig(uri)) { folder = getOuterMostWorkspaceFolder(folder) bootWorkspaceClient(folder) } - }) + } - context.subscriptions.push(watcher) + cssWatcher.onDidCreate(bootClientIfCssFileContainsAtConfig) + cssWatcher.onDidChange(bootClientIfCssFileContainsAtConfig) + + context.subscriptions.push(cssWatcher) // TODO: check if the actual language MAPPING changed // not just the language IDs @@ -551,11 +573,22 @@ export async function activate(context: ExtensionContext) { 1 ) - if (!configFile) { + if (configFile) { + bootWorkspaceClient(folder) return } - bootWorkspaceClient(folder) + let cssFiles = await Workspace.findFiles( + new RelativePattern(folder, `**/${CSS_GLOB}`), + `{${getExcludePatterns(folder).join(',')}}` + ) + + for (let cssFile of cssFiles) { + if (await fileContainsAtConfig(cssFile)) { + bootWorkspaceClient(folder) + return + } + } } context.subscriptions.push(Workspace.onDidOpenTextDocument(didOpenTextDocument)) From a3928db29ee732a35d296ed7f4271bf4f766df38 Mon Sep 17 00:00:00 2001 From: Brad Cornes Date: Thu, 13 Oct 2022 09:00:52 +0100 Subject: [PATCH 04/26] wip --- .../tailwindcss-language-server/src/server.ts | 79 ++++++++++++++----- 1 file changed, 58 insertions(+), 21 deletions(-) diff --git a/packages/tailwindcss-language-server/src/server.ts b/packages/tailwindcss-language-server/src/server.ts index 03276902..24470212 100644 --- a/packages/tailwindcss-language-server/src/server.ts +++ b/packages/tailwindcss-language-server/src/server.ts @@ -126,8 +126,8 @@ const colorNames = Object.keys(namedColors) const connection = process.argv.length <= 2 ? createConnection(process.stdin, process.stdout) : createConnection() -// console.log = connection.console.log.bind(connection.console) -// console.error = connection.console.error.bind(connection.console) +console.log = connection.console.log.bind(connection.console) +console.error = connection.console.error.bind(connection.console) process.on('unhandledRejection', (e: any) => { connection.console.error(formatError(`Unhandled exception`, e)) @@ -378,10 +378,9 @@ async function createProjectService( let isConfigFile = projectConfig.configPath ? change.file === projectConfig.configPath : minimatch(file, `**/${CONFIG_FILE_GLOB}`, { dot: true }) - let isPackageFile = minimatch(file, `**/${PACKAGE_GLOB}`, { dot: true }) let isDependency = state.dependencies && state.dependencies.includes(change.file) - if (!isConfigFile && !isPackageFile && !isDependency) continue + if (!isConfigFile && !isDependency) continue if (!enabled) { if (projectConfig.configPath && (isConfigFile || isDependency)) { @@ -389,10 +388,11 @@ async function createProjectService( let originalConfig = require(projectConfig.configPath) let contentConfig: unknown = originalConfig.content?.files ?? originalConfig.content let content = Array.isArray(contentConfig) ? contentConfig : [] - // TODO: check version number - // if <3.2 this should always be `false` - let relativeEnabled = - originalConfig.future?.relativeContentPathsByDefault || originalConfig.content?.relative + // TODO `state.version` isn't going to exist here + let relativeEnabled = semver.gte(state.version, '3.2.0') + ? originalConfig.future?.relativeContentPathsByDefault || + originalConfig.content?.relative + : false let contentBase = relativeEnabled ? path.dirname(state.configPath) : projectConfig.folder let contentSelector = content .filter((item): item is string => typeof item === 'string') @@ -414,7 +414,7 @@ async function createProjectService( break } else if (change.type === FileChangeType.Changed) { log('File changed:', change.file) - if (!state.enabled || isPackageFile) { + if (!state.enabled) { needsInit = true break } else { @@ -422,7 +422,7 @@ async function createProjectService( } } else if (change.type === FileChangeType.Deleted) { log('File deleted:', change.file) - if (!state.enabled || isPackageFile || isConfigFile) { + if (!state.enabled || isConfigFile) { needsInit = true break } else { @@ -918,10 +918,9 @@ async function createProjectService( ///////////////////// let contentConfig: unknown = originalConfig.content?.files ?? originalConfig.content let content = Array.isArray(contentConfig) ? contentConfig : [] - // TODO: check version number - // if <3.2 this should always be `false` - let relativeEnabled = - originalConfig.future?.relativeContentPathsByDefault || originalConfig.content?.relative + let relativeEnabled = semver.gte(tailwindcss.version, '3.2.0') + ? originalConfig.future?.relativeContentPathsByDefault || originalConfig.content?.relative + : false let contentBase = relativeEnabled ? path.dirname(state.configPath) : projectConfig.folder let contentSelector = content .filter((item): item is string => typeof item === 'string') @@ -1476,6 +1475,7 @@ class TW { let base = normalizeFileNameToFsPath(this.initializeParams.rootPath) let cssFileConfigMap: Map = new Map() + let configTailwindVersionMap: Map = new Map() if (configFileOrFiles) { if ( @@ -1528,15 +1528,30 @@ class TW { continue } + let twVersion = require('tailwindcss/package.json').version + let isDefaultVersion = true + try { + let v = require(resolveFrom(path.dirname(configPath), 'tailwindcss/package.json')).version + if (typeof v === 'string') { + twVersion = v + isDefaultVersion = false + } + } catch {} + + if (isCssFile && (!semver.gte(twVersion, '3.2.0') || isDefaultVersion)) { + continue + } + + configTailwindVersionMap.set(configPath, twVersion) + let contentSelector: Array = [] try { let config = require(configPath) let contentConfig: unknown = config.content?.files ?? config.content let content = Array.isArray(contentConfig) ? contentConfig : [] - // TODO: check version number - // if <3.2 this should always be `false` - let relativeEnabled = - config.future?.relativeContentPathsByDefault || config.content?.relative + let relativeEnabled = semver.gte(twVersion, '3.2.0') + ? config.future?.relativeContentPathsByDefault || config.content?.relative + : false let contentBase = relativeEnabled ? path.dirname(configPath) : base contentSelector = content .filter((item): item is string => typeof item === 'string') @@ -1582,11 +1597,33 @@ class TW { const onDidChangeWatchedFiles = async ( changes: Array<{ file: string; type: FileChangeType }> - ): Promise<{ needsRestart: boolean }> => { + ): Promise => { let needsRestart = false - for (let change of changes) { + changeLoop: for (let change of changes) { let normalizedFilename = normalizePath(change.file) + + let isPackageFile = minimatch(normalizedFilename, `**/${PACKAGE_GLOB}`, { dot: true }) + if (isPackageFile) { + for (let [key] of this.projects) { + let projectConfig = JSON.parse(key) as ProjectConfig + let twVersion = require('tailwindcss/package.json').version + try { + let v = require(resolveFrom( + path.dirname(projectConfig.configPath), + 'tailwindcss/package.json' + )).version + if (typeof v === 'string') { + twVersion = v + } + } catch {} + if (configTailwindVersionMap.get(projectConfig.configPath) !== twVersion) { + needsRestart = true + break changeLoop + } + } + } + let isCssFile = minimatch(normalizedFilename, `**/${CSS_GLOB}`, { dot: true, }) @@ -1616,7 +1653,7 @@ class TW { let projectConfig = JSON.parse(key) as ProjectConfig if (change.file === projectConfig.configPath && change.type === FileChangeType.Deleted) { needsRestart = true - break + break changeLoop } } } From 4c7cec81bb5f6a6e447e50ae8c15ee7dd8953efc Mon Sep 17 00:00:00 2001 From: Brad Cornes Date: Thu, 13 Oct 2022 15:17:23 +0100 Subject: [PATCH 05/26] Check document exists --- packages/tailwindcss-language-server/src/server.ts | 1 + .../src/codeActions/codeActionProvider.ts | 1 + .../src/codeActions/provideInvalidApplyCodeActions.ts | 1 + 3 files changed, 3 insertions(+) diff --git a/packages/tailwindcss-language-server/src/server.ts b/packages/tailwindcss-language-server/src/server.ts index 24470212..fd101c02 100644 --- a/packages/tailwindcss-language-server/src/server.ts +++ b/packages/tailwindcss-language-server/src/server.ts @@ -1114,6 +1114,7 @@ async function createProjectService( }, async onColorPresentation(params: ColorPresentationParams): Promise { let document = documentService.getDocument(params.textDocument.uri) + if (!document) return [] let className = document.getText(params.range) let match = className.match( new RegExp(`-\\[(${colorNames.join('|')}|(?:(?:#|rgba?\\(|hsla?\\())[^\\]]+)\\]$`, 'i') diff --git a/packages/tailwindcss-language-service/src/codeActions/codeActionProvider.ts b/packages/tailwindcss-language-service/src/codeActions/codeActionProvider.ts index c18edda8..df30e661 100644 --- a/packages/tailwindcss-language-service/src/codeActions/codeActionProvider.ts +++ b/packages/tailwindcss-language-service/src/codeActions/codeActionProvider.ts @@ -24,6 +24,7 @@ async function getDiagnosticsFromCodeActionParams( only?: DiagnosticKind[] ): Promise { let document = state.editor.documents.get(params.textDocument.uri) + if (!document) return [] let diagnostics = await doValidate(state, document, only) return params.context.diagnostics diff --git a/packages/tailwindcss-language-service/src/codeActions/provideInvalidApplyCodeActions.ts b/packages/tailwindcss-language-service/src/codeActions/provideInvalidApplyCodeActions.ts index 3730492a..c212eead 100644 --- a/packages/tailwindcss-language-service/src/codeActions/provideInvalidApplyCodeActions.ts +++ b/packages/tailwindcss-language-service/src/codeActions/provideInvalidApplyCodeActions.ts @@ -25,6 +25,7 @@ export async function provideInvalidApplyCodeActions( diagnostic: InvalidApplyDiagnostic ): Promise { let document = state.editor.documents.get(params.textDocument.uri) + if (!document) return [] let documentText = getTextWithoutComments(document, 'css') let cssRange: Range let cssText = documentText From 44e38c375ce64830237dfc97ac48258e416978c7 Mon Sep 17 00:00:00 2001 From: Brad Cornes Date: Thu, 13 Oct 2022 15:18:02 +0100 Subject: [PATCH 06/26] wip --- .../tailwindcss-language-server/src/server.ts | 90 +++++++++++-------- 1 file changed, 52 insertions(+), 38 deletions(-) diff --git a/packages/tailwindcss-language-server/src/server.ts b/packages/tailwindcss-language-server/src/server.ts index fd101c02..396fada4 100644 --- a/packages/tailwindcss-language-server/src/server.ts +++ b/packages/tailwindcss-language-server/src/server.ts @@ -384,23 +384,14 @@ async function createProjectService( if (!enabled) { if (projectConfig.configPath && (isConfigFile || isDependency)) { - // update document selector - let originalConfig = require(projectConfig.configPath) - let contentConfig: unknown = originalConfig.content?.files ?? originalConfig.content - let content = Array.isArray(contentConfig) ? contentConfig : [] - // TODO `state.version` isn't going to exist here - let relativeEnabled = semver.gte(state.version, '3.2.0') - ? originalConfig.future?.relativeContentPathsByDefault || - originalConfig.content?.relative - : false - let contentBase = relativeEnabled ? path.dirname(state.configPath) : projectConfig.folder - let contentSelector = content - .filter((item): item is string => typeof item === 'string') - .map((item) => path.resolve(contentBase, item)) - .map((item) => ({ pattern: normalizePath(item), priority: 1 })) documentSelector = [ ...documentSelector.filter(({ priority }) => priority !== 1), - ...contentSelector, + // TODO `state.version` isn't going to exist here + ...getContentDocumentSelectorFromConfigFile( + projectConfig.configPath, + state.version, + projectConfig.folder + ), ] checkOpenDocuments() @@ -502,6 +493,8 @@ async function createProjectService( throw new SilentError('No config file found.') } + watchPatterns([configPath]) + const pnpPath = findUp.sync( (dir) => { let pnpFile = path.join(dir, '.pnp.js') @@ -916,19 +909,14 @@ async function createProjectService( } ///////////////////// - let contentConfig: unknown = originalConfig.content?.files ?? originalConfig.content - let content = Array.isArray(contentConfig) ? contentConfig : [] - let relativeEnabled = semver.gte(tailwindcss.version, '3.2.0') - ? originalConfig.future?.relativeContentPathsByDefault || originalConfig.content?.relative - : false - let contentBase = relativeEnabled ? path.dirname(state.configPath) : projectConfig.folder - let contentSelector = content - .filter((item): item is string => typeof item === 'string') - .map((item) => path.resolve(contentBase, item)) - .map((item) => ({ pattern: normalizePath(item), priority: 1 })) documentSelector = [ ...documentSelector.filter(({ priority }) => priority !== 1), - ...contentSelector, + ...getContentDocumentSelectorFromConfigFile( + state.configPath, + tailwindcss.version, + projectConfig.folder, + originalConfig + ), ] ////////////////////// @@ -992,7 +980,7 @@ async function createProjectService( // } state.dependencies = getModuleDependencies(state.configPath) // chokidarWatcher?.add(state.dependencies) - watchPatterns([state.configPath, ...(state.dependencies ?? [])]) + watchPatterns(state.dependencies ?? []) state.configId = getConfigId(state.configPath, state.dependencies) @@ -1436,6 +1424,42 @@ async function getConfigFileFromCssFile(cssFile: string): Promise return path.resolve(path.dirname(cssFile), match.groups.config.slice(1, -1)) } +function getContentDocumentSelectorFromConfigFile( + configPath: string, + tailwindVersion: string, + rootDir: string, + actualConfig?: any +): DocumentSelector[] { + let config = actualConfig ?? require(configPath) + let contentConfig: unknown = config.content?.files ?? config.content + let content = Array.isArray(contentConfig) ? contentConfig : [] + let relativeEnabled = semver.gte(tailwindVersion, '3.2.0') + ? config.future?.relativeContentPathsByDefault || config.content?.relative + : false + let contentBase: string + if (relativeEnabled) { + contentBase = path.dirname(configPath) + } else { + let pkgJsonPath = findUp.sync( + (dir) => { + let pkgJson = path.join(dir, 'package.json') + if (findUp.sync.exists(pkgJson)) { + return pkgJson + } + if (dir === rootDir) { + return findUp.stop + } + }, + { cwd: path.dirname(configPath) } + ) + contentBase = pkgJsonPath ? path.dirname(pkgJsonPath) : rootDir + } + return content + .filter((item): item is string => typeof item === 'string') + .map((item) => path.resolve(contentBase, item)) + .map((item) => ({ pattern: normalizePath(item), priority: 1 })) +} + class TW { private initialized = false private lspHandlersAdded = false @@ -1547,17 +1571,7 @@ class TW { let contentSelector: Array = [] try { - let config = require(configPath) - let contentConfig: unknown = config.content?.files ?? config.content - let content = Array.isArray(contentConfig) ? contentConfig : [] - let relativeEnabled = semver.gte(twVersion, '3.2.0') - ? config.future?.relativeContentPathsByDefault || config.content?.relative - : false - let contentBase = relativeEnabled ? path.dirname(configPath) : base - contentSelector = content - .filter((item): item is string => typeof item === 'string') - .map((item) => path.resolve(contentBase, item)) - .map((item) => ({ pattern: normalizePath(item), priority: 1 })) + contentSelector = getContentDocumentSelectorFromConfigFile(configPath, twVersion, base) } catch {} let documentSelector = contentSelector From fc30d0df3513ba73b078490c30bf3a9e92aafea0 Mon Sep 17 00:00:00 2001 From: Brad Cornes Date: Thu, 13 Oct 2022 15:59:36 +0100 Subject: [PATCH 07/26] Fix duplicate document selector --- packages/tailwindcss-language-server/src/server.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/tailwindcss-language-server/src/server.ts b/packages/tailwindcss-language-server/src/server.ts index 396fada4..371e9507 100644 --- a/packages/tailwindcss-language-server/src/server.ts +++ b/packages/tailwindcss-language-server/src/server.ts @@ -1579,10 +1579,14 @@ class TW { pattern: normalizePath(filename), priority: 0, }) - .concat({ - pattern: normalizePath(configPath), - priority: 0, - }) + .concat( + isCssFile + ? { + pattern: normalizePath(configPath), + priority: 0, + } + : [] + ) .concat({ pattern: normalizePath(path.join(path.dirname(filename), '**')), priority: 2, From 8032797049241ad825920ee111b897483e6a4eef Mon Sep 17 00:00:00 2001 From: Brad Cornes Date: Thu, 13 Oct 2022 16:11:38 +0100 Subject: [PATCH 08/26] wip --- .../tailwindcss-language-server/src/server.ts | 79 ++++++------------- 1 file changed, 26 insertions(+), 53 deletions(-) diff --git a/packages/tailwindcss-language-server/src/server.ts b/packages/tailwindcss-language-server/src/server.ts index 371e9507..81b1277c 100644 --- a/packages/tailwindcss-language-server/src/server.ts +++ b/packages/tailwindcss-language-server/src/server.ts @@ -61,11 +61,7 @@ import { Settings, ClassNames, } from 'tailwindcss-language-service/src/util/state' -import { - provideDiagnostics, - updateAllDiagnostics, - clearAllDiagnostics, -} from './lsp/diagnosticsProvider' +import { provideDiagnostics } from './lsp/diagnosticsProvider' import { doCodeActions } from 'tailwindcss-language-service/src/codeActions/codeActionProvider' import { getDocumentColors } from 'tailwindcss-language-service/src/documentColorProvider' import { debounce } from 'debounce' @@ -338,7 +334,6 @@ async function createProjectService( editor: { connection, folder, - globalSettings: await getConfiguration(), userLanguages: params.initializationOptions.userLanguages ? params.initializationOptions.userLanguages : {}, @@ -359,9 +354,6 @@ async function createProjectService( console.log(`[${path.relative(projectConfig.folder, projectConfig.configPath)}]`, ...args) } - let chokidarWatcher: chokidar.FSWatcher - let ignore = state.editor.globalSettings.tailwindCSS.files.exclude - function onFileEvents(changes: Array<{ file: string; type: FileChangeType }>): void { let needsInit = false let needsRebuild = false @@ -369,12 +361,6 @@ async function createProjectService( for (let change of changes) { let file = normalizePath(change.file) - for (let ignorePattern of ignore) { - if (minimatch(file, ignorePattern, { dot: true })) { - continue - } - } - let isConfigFile = projectConfig.configPath ? change.file === projectConfig.configPath : minimatch(file, `**/${CONFIG_FILE_GLOB}`, { dot: true }) @@ -473,22 +459,6 @@ async function createProjectService( let configPath = projectConfig.configPath - if (!configPath) { - configPath = ( - await glob([`**/${CONFIG_FILE_GLOB}`], { - cwd: folder, - ignore: state.editor.globalSettings.tailwindCSS.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.') } @@ -1020,20 +990,13 @@ async function createProjectService( } }, async onUpdateSettings(settings: any): Promise { - documentSettingsCache.clear() - let previousExclude = state.editor.globalSettings.tailwindCSS.files.exclude - state.editor.globalSettings = await state.editor.getConfiguration() - if (!equal(previousExclude, settings.tailwindCSS.files.exclude)) { - tryInit() + if (state.enabled) { + refreshDiagnostics() + } + if (settings.editor.colorDecorators) { + updateCapabilities() } else { - if (state.enabled) { - updateAllDiagnostics(state) - } - if (settings.editor.colorDecorators) { - updateCapabilities() - } else { - connection.sendNotification('@/tailwindCSS/clearColors') - } + connection.sendNotification('@/tailwindCSS/clearColors') } }, onFileEvents, @@ -1491,12 +1454,9 @@ class TW { } let workspaceFolders: Array = [] - - let configFileOrFiles = dlv( - await connection.workspace.getConfiguration('tailwindCSS'), - 'experimental.configFile', - null - ) as Settings['tailwindCSS']['experimental']['configFile'] + let globalSettings = await getConfiguration() + let ignore = globalSettings.tailwindCSS.files.exclude + let configFileOrFiles = globalSettings.tailwindCSS.experimental.configFile let base = normalizeFileNameToFsPath(this.initializeParams.rootPath) let cssFileConfigMap: Map = new Map() @@ -1622,6 +1582,12 @@ class TW { changeLoop: for (let change of changes) { let normalizedFilename = normalizePath(change.file) + for (let ignorePattern of ignore) { + if (minimatch(normalizedFilename, ignorePattern, { dot: true })) { + continue changeLoop + } + } + let isPackageFile = minimatch(normalizedFilename, `**/${PACKAGE_GLOB}`, { dot: true }) if (isPackageFile) { for (let [key] of this.projects) { @@ -1687,9 +1653,6 @@ class TW { } } - let ignore = (await getConfiguration()).tailwindCSS.files.exclude - // let watchPatterns: (patterns: string[]) => void = () => {} - if (this.initializeParams.capabilities.workspace?.didChangeWatchedFiles?.dynamicRegistration) { this.disposables.push( this.connection.onDidChangeWatchedFiles(async ({ changes }) => { @@ -1831,6 +1794,16 @@ class TW { this.disposables.push( this.connection.onDidChangeConfiguration(async ({ settings }) => { + let previousExclude = globalSettings.tailwindCSS.files.exclude + + documentSettingsCache.clear() + globalSettings = await getConfiguration() + + if (!equal(previousExclude, globalSettings.tailwindCSS.files.exclude)) { + this.restart() + return + } + for (let [, project] of this.projects) { project.onUpdateSettings(settings) } From 926885a6b376a93bb5bd77e5e5ddc084a26a83cd Mon Sep 17 00:00:00 2001 From: Brad Cornes Date: Thu, 13 Oct 2022 16:33:47 +0100 Subject: [PATCH 09/26] Use enum for document selector priorities --- .../tailwindcss-language-server/src/server.ts | 49 ++++++++++++++----- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/packages/tailwindcss-language-server/src/server.ts b/packages/tailwindcss-language-server/src/server.ts index 81b1277c..967cdc1a 100644 --- a/packages/tailwindcss-language-server/src/server.ts +++ b/packages/tailwindcss-language-server/src/server.ts @@ -198,6 +198,13 @@ type ProjectConfig = { } type DocumentSelector = { pattern: string; priority: number } +enum DocumentSelectorPriority { + CONFIG_FILE = 0, + CSS_FILE = 0, + CONTENT_FILE = 1, + CSS_DIRECTORY = 2, + CONFIG_DIRECTORY = 3, +} function getMode(config: any): unknown { if (typeof config.mode !== 'undefined') { @@ -371,7 +378,9 @@ async function createProjectService( if (!enabled) { if (projectConfig.configPath && (isConfigFile || isDependency)) { documentSelector = [ - ...documentSelector.filter(({ priority }) => priority !== 1), + ...documentSelector.filter( + ({ priority }) => priority !== DocumentSelectorPriority.CONTENT_FILE + ), // TODO `state.version` isn't going to exist here ...getContentDocumentSelectorFromConfigFile( projectConfig.configPath, @@ -880,7 +889,9 @@ async function createProjectService( ///////////////////// documentSelector = [ - ...documentSelector.filter(({ priority }) => priority !== 1), + ...documentSelector.filter( + ({ priority }) => priority !== DocumentSelectorPriority.CONTENT_FILE + ), ...getContentDocumentSelectorFromConfigFile( state.configPath, tailwindcss.version, @@ -1420,7 +1431,10 @@ function getContentDocumentSelectorFromConfigFile( return content .filter((item): item is string => typeof item === 'string') .map((item) => path.resolve(contentBase, item)) - .map((item) => ({ pattern: normalizePath(item), priority: 1 })) + .map((item) => ({ + pattern: normalizePath(item), + priority: DocumentSelectorPriority.CONTENT_FILE, + })) } class TW { @@ -1486,9 +1500,10 @@ class TW { return { folder: base, configPath: path.resolve(base, relativeConfigPath), - documentSelector: [] - .concat(relativeDocumentSelectorOrSelectors) - .map((selector) => ({ priority: 1, pattern: path.resolve(base, selector) })), + documentSelector: [].concat(relativeDocumentSelectorOrSelectors).map((selector) => ({ + priority: DocumentSelectorPriority.CONTENT_FILE, + pattern: path.resolve(base, selector), + })), } } ) @@ -1537,24 +1552,32 @@ class TW { let documentSelector = contentSelector .concat({ pattern: normalizePath(filename), - priority: 0, + priority: isCssFile + ? DocumentSelectorPriority.CSS_FILE + : DocumentSelectorPriority.CONFIG_FILE, }) .concat( isCssFile ? { pattern: normalizePath(configPath), - priority: 0, + priority: DocumentSelectorPriority.CONFIG_FILE, } : [] ) .concat({ pattern: normalizePath(path.join(path.dirname(filename), '**')), - priority: 2, - }) - .concat({ - pattern: normalizePath(path.join(path.dirname(configPath), '**')), - priority: 3, + priority: isCssFile + ? DocumentSelectorPriority.CSS_DIRECTORY + : DocumentSelectorPriority.CONFIG_DIRECTORY, }) + .concat( + isCssFile + ? { + pattern: normalizePath(path.join(path.dirname(configPath), '**')), + priority: DocumentSelectorPriority.CONFIG_DIRECTORY, + } + : [] + ) projects[configPath] = [...(projects[configPath] ?? []), ...documentSelector] if (isCssFile) { cssFileConfigMap.set(normalizedFilename, configPath) From 37562a4f519bfac81b10b094fd550fc3bf289fd5 Mon Sep 17 00:00:00 2001 From: Brad Cornes Date: Thu, 13 Oct 2022 16:34:04 +0100 Subject: [PATCH 10/26] Delete unused functions --- .../src/lsp/diagnosticsProvider.ts | 12 --------- .../src/diagnostics/diagnosticsProvider.ts | 26 ------------------- 2 files changed, 38 deletions(-) diff --git a/packages/tailwindcss-language-server/src/lsp/diagnosticsProvider.ts b/packages/tailwindcss-language-server/src/lsp/diagnosticsProvider.ts index e5f01a89..fe08b247 100644 --- a/packages/tailwindcss-language-server/src/lsp/diagnosticsProvider.ts +++ b/packages/tailwindcss-language-server/src/lsp/diagnosticsProvider.ts @@ -20,15 +20,3 @@ export function clearDiagnostics(state: State, document: TextDocument): void { diagnostics: [], }) } - -export function clearAllDiagnostics(state: State): void { - state.editor?.documents.all().forEach((document) => { - clearDiagnostics(state, document) - }) -} - -export function updateAllDiagnostics(state: State): void { - state.editor?.documents.all().forEach((document) => { - provideDiagnostics(state, document) - }) -} diff --git a/packages/tailwindcss-language-service/src/diagnostics/diagnosticsProvider.ts b/packages/tailwindcss-language-service/src/diagnostics/diagnosticsProvider.ts index 5ef686a6..a8d7f7dc 100644 --- a/packages/tailwindcss-language-service/src/diagnostics/diagnosticsProvider.ts +++ b/packages/tailwindcss-language-service/src/diagnostics/diagnosticsProvider.ts @@ -50,29 +50,3 @@ export async function doValidate( ] : [] } - -export async function provideDiagnostics(state: State, document: TextDocument) { - // state.editor.connection.sendDiagnostics({ - // uri: document.uri, - // diagnostics: await doValidate(state, document), - // }) -} - -export function clearDiagnostics(state: State, document: TextDocument): void { - // state.editor.connection.sendDiagnostics({ - // uri: document.uri, - // diagnostics: [], - // }) -} - -export function clearAllDiagnostics(state: State): void { - state.editor.documents.all().forEach((document) => { - clearDiagnostics(state, document) - }) -} - -export function updateAllDiagnostics(state: State): void { - state.editor.documents.all().forEach((document) => { - provideDiagnostics(state, document) - }) -} From b92cdb29a5060c42333aa8828cb5034b8433f782 Mon Sep 17 00:00:00 2001 From: Brad Cornes Date: Thu, 13 Oct 2022 16:34:20 +0100 Subject: [PATCH 11/26] Remove unused state type --- packages/tailwindcss-language-service/src/util/state.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/tailwindcss-language-service/src/util/state.ts b/packages/tailwindcss-language-service/src/util/state.ts index d699ffb6..4d0c8fc4 100644 --- a/packages/tailwindcss-language-service/src/util/state.ts +++ b/packages/tailwindcss-language-service/src/util/state.ts @@ -21,7 +21,6 @@ export type EditorState = { connection: Connection folder: string documents: TextDocuments - globalSettings: Settings userLanguages: Record capabilities: { configuration: boolean From 12e70f64011e282e91c872df9ffc5990d3f06586 Mon Sep 17 00:00:00 2001 From: Brad Cornes Date: Thu, 13 Oct 2022 16:38:35 +0100 Subject: [PATCH 12/26] Share glob patterns --- .../src/lib/constants.ts | 3 +++ .../tailwindcss-language-server/src/server.ts | 18 ++++++++---------- packages/vscode-tailwindcss/src/extension.ts | 8 +++----- 3 files changed, 14 insertions(+), 15 deletions(-) create mode 100644 packages/tailwindcss-language-server/src/lib/constants.ts diff --git a/packages/tailwindcss-language-server/src/lib/constants.ts b/packages/tailwindcss-language-server/src/lib/constants.ts new file mode 100644 index 00000000..3f5b96dc --- /dev/null +++ b/packages/tailwindcss-language-server/src/lib/constants.ts @@ -0,0 +1,3 @@ +export const CONFIG_GLOB = '{tailwind,tailwind.config}.{js,cjs}' +export const PACKAGE_LOCK_GLOB = '{package-lock.json,yarn.lock,pnpm-lock.yaml}' +export const CSS_GLOB = '*.{css,scss,sass,less,pcss}' diff --git a/packages/tailwindcss-language-server/src/server.ts b/packages/tailwindcss-language-server/src/server.ts index 967cdc1a..4ea82787 100644 --- a/packages/tailwindcss-language-server/src/server.ts +++ b/packages/tailwindcss-language-server/src/server.ts @@ -80,6 +80,7 @@ import { equal } from 'tailwindcss-language-service/src/util/array' import preflight from 'tailwindcss/lib/css/preflight.css' import merge from 'deepmerge' import { getTextWithoutComments } from 'tailwindcss-language-service/src/util/doc' +import { CONFIG_GLOB, CSS_GLOB, PACKAGE_LOCK_GLOB } from './lib/constants' // @ts-ignore global.__preflight = preflight @@ -97,9 +98,6 @@ new Function( ` )(require, __dirname) -const CONFIG_FILE_GLOB = '{tailwind,tailwind.config}.{js,cjs}' -const PACKAGE_GLOB = '{package-lock.json,yarn.lock,pnpm-lock.yaml}' -const CSS_GLOB = '*.{css,scss,sass,less,pcss}' const TRIGGER_CHARACTERS = [ // class attributes '"', @@ -370,7 +368,7 @@ async function createProjectService( let isConfigFile = projectConfig.configPath ? change.file === projectConfig.configPath - : minimatch(file, `**/${CONFIG_FILE_GLOB}`, { dot: true }) + : minimatch(file, `**/${CONFIG_GLOB}`, { dot: true }) let isDependency = state.dependencies && state.dependencies.includes(change.file) if (!isConfigFile && !isDependency) continue @@ -1510,7 +1508,7 @@ class TW { } else { let projects: Record> = {} - let files = await glob([`**/${CONFIG_FILE_GLOB}`, `**/${CSS_GLOB}`], { + let files = await glob([`**/${CONFIG_GLOB}`, `**/${CSS_GLOB}`], { cwd: base, ignore: (await getConfiguration()).tailwindCSS.files.exclude, onlyFiles: true, @@ -1611,7 +1609,7 @@ class TW { } } - let isPackageFile = minimatch(normalizedFilename, `**/${PACKAGE_GLOB}`, { dot: true }) + let isPackageFile = minimatch(normalizedFilename, `**/${PACKAGE_LOCK_GLOB}`, { dot: true }) if (isPackageFile) { for (let [key] of this.projects) { let projectConfig = JSON.parse(key) as ProjectConfig @@ -1649,7 +1647,7 @@ class TW { } } - let isConfigFile = minimatch(normalizedFilename, `**/${CONFIG_FILE_GLOB}`, { + let isConfigFile = minimatch(normalizedFilename, `**/${CONFIG_GLOB}`, { dot: true, }) if (isConfigFile && change.type === FileChangeType.Created) { @@ -1698,8 +1696,8 @@ class TW { DidChangeWatchedFilesNotification.type, { watchers: [ - { globPattern: `**/${CONFIG_FILE_GLOB}` }, - { globPattern: `**/${PACKAGE_GLOB}` }, + { globPattern: `**/${CONFIG_GLOB}` }, + { globPattern: `**/${PACKAGE_LOCK_GLOB}` }, { globPattern: `**/${CSS_GLOB}` }, ], } @@ -1749,7 +1747,7 @@ class TW { } else { let watch: typeof chokidar.watch = require('chokidar').watch let chokidarWatcher = watch( - [`**/${CONFIG_FILE_GLOB}`, `**/${PACKAGE_GLOB}`, `**/${CSS_GLOB}`], + [`**/${CONFIG_GLOB}`, `**/${PACKAGE_LOCK_GLOB}`, `**/${CSS_GLOB}`], { cwd: base, ignorePermissionErrors: true, diff --git a/packages/vscode-tailwindcss/src/extension.ts b/packages/vscode-tailwindcss/src/extension.ts index fee51aab..a16a5afb 100755 --- a/packages/vscode-tailwindcss/src/extension.ts +++ b/packages/vscode-tailwindcss/src/extension.ts @@ -41,15 +41,13 @@ import isObject from 'tailwindcss-language-service/src/util/isObject' import { dedupe, equal } from 'tailwindcss-language-service/src/util/array' import namedColors from 'color-name' import minimatch from 'minimatch' +import { CONFIG_GLOB, CSS_GLOB } from 'tailwindcss-language-server/src/lib/constants' const colorNames = Object.keys(namedColors) const CLIENT_ID = 'tailwindcss-intellisense' const CLIENT_NAME = 'Tailwind CSS IntelliSense' -const CONFIG_FILE_GLOB = '{tailwind,tailwind.config}.{js,cjs}' -const CSS_GLOB = '*.{css,scss,sass,less,pcss}' - let clients: Map = new Map() let languages: Map = new Map() let searchedFolders: Set = new Set() @@ -155,7 +153,7 @@ export async function activate(context: ExtensionContext) { }) ) - let configWatcher = Workspace.createFileSystemWatcher(`**/${CONFIG_FILE_GLOB}`, false, true, true) + let configWatcher = Workspace.createFileSystemWatcher(`**/${CONFIG_GLOB}`, false, true, true) configWatcher.onDidCreate((uri) => { let folder = Workspace.getWorkspaceFolder(uri) @@ -568,7 +566,7 @@ export async function activate(context: ExtensionContext) { searchedFolders.add(folder.uri.toString()) let [configFile] = await Workspace.findFiles( - new RelativePattern(folder, `**/${CONFIG_FILE_GLOB}`), + new RelativePattern(folder, `**/${CONFIG_GLOB}`), `{${getExcludePatterns(folder).join(',')}}`, 1 ) From bcc8a84a8f56a5a1292a59b7811af8e54e83b064 Mon Sep 17 00:00:00 2001 From: Brad Cornes Date: Thu, 13 Oct 2022 16:39:15 +0100 Subject: [PATCH 13/26] Update config file glob --- packages/tailwindcss-language-server/src/lib/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tailwindcss-language-server/src/lib/constants.ts b/packages/tailwindcss-language-server/src/lib/constants.ts index 3f5b96dc..7324327c 100644 --- a/packages/tailwindcss-language-server/src/lib/constants.ts +++ b/packages/tailwindcss-language-server/src/lib/constants.ts @@ -1,3 +1,3 @@ -export const CONFIG_GLOB = '{tailwind,tailwind.config}.{js,cjs}' +export const CONFIG_GLOB = '{tailwind,tailwind.config,tailwind.*.config,tailwind.config.*}.{js,cjs}' export const PACKAGE_LOCK_GLOB = '{package-lock.json,yarn.lock,pnpm-lock.yaml}' export const CSS_GLOB = '*.{css,scss,sass,less,pcss}' From c6203c791b7e32d39e5f3060a655a741d0b42157 Mon Sep 17 00:00:00 2001 From: Brad Cornes Date: Mon, 17 Oct 2022 10:47:25 +0100 Subject: [PATCH 14/26] fix logs --- packages/tailwindcss-language-server/src/server.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/tailwindcss-language-server/src/server.ts b/packages/tailwindcss-language-server/src/server.ts index 4ea82787..445735e2 100644 --- a/packages/tailwindcss-language-server/src/server.ts +++ b/packages/tailwindcss-language-server/src/server.ts @@ -355,8 +355,10 @@ async function createProjectService( }, } - function log(...args: any[]): void { - console.log(`[${path.relative(projectConfig.folder, projectConfig.configPath)}]`, ...args) + function log(...args: string[]): void { + console.log( + `[${path.relative(projectConfig.folder, projectConfig.configPath)}] ${args.join(' ')}` + ) } function onFileEvents(changes: Array<{ file: string; type: FileChangeType }>): void { @@ -1593,7 +1595,7 @@ class TW { } } - console.log('[Global]', 'Creating projects:', JSON.stringify(workspaceFolders)) + console.log(`[Global] Creating projects: ${JSON.stringify(workspaceFolders)}`) const onDidChangeWatchedFiles = async ( changes: Array<{ file: string; type: FileChangeType }> @@ -1708,7 +1710,7 @@ class TW { this.watchPatterns = (patterns) => { let newPatterns = this.filterNewWatchPatterns(patterns) if (newPatterns.length) { - console.log('[Global]', 'Adding watch patterns:', newPatterns.join(', ')) + console.log(`[Global] Adding watch patterns: ${newPatterns.join(', ')}`) this.connection.client .register(DidChangeWatchedFilesNotification.type, { watchers: newPatterns.map((pattern) => ({ globPattern: pattern })), @@ -1790,7 +1792,7 @@ class TW { this.watchPatterns = (patterns) => { let newPatterns = this.filterNewWatchPatterns(patterns) if (newPatterns.length) { - console.log('[Global]', 'Adding watch patterns:', newPatterns.join(', ')) + console.log(`[Global] Adding watch patterns: ${newPatterns.join(', ')}`) chokidarWatcher.add(newPatterns) } } From 76da857578eac1c750b0af58279d07f2350a799e Mon Sep 17 00:00:00 2001 From: Brad Cornes Date: Mon, 17 Oct 2022 12:39:25 +0100 Subject: [PATCH 15/26] Fix filename checks on Windows --- packages/tailwindcss-language-server/src/server.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/tailwindcss-language-server/src/server.ts b/packages/tailwindcss-language-server/src/server.ts index 445735e2..e50d8be7 100644 --- a/packages/tailwindcss-language-server/src/server.ts +++ b/packages/tailwindcss-language-server/src/server.ts @@ -369,7 +369,7 @@ async function createProjectService( let file = normalizePath(change.file) let isConfigFile = projectConfig.configPath - ? change.file === projectConfig.configPath + ? file === projectConfig.configPath : minimatch(file, `**/${CONFIG_GLOB}`, { dot: true }) let isDependency = state.dependencies && state.dependencies.includes(change.file) @@ -1659,7 +1659,10 @@ class TW { for (let [key] of this.projects) { let projectConfig = JSON.parse(key) as ProjectConfig - if (change.file === projectConfig.configPath && change.type === FileChangeType.Deleted) { + if ( + normalizedFilename === projectConfig.configPath && + change.type === FileChangeType.Deleted + ) { needsRestart = true break changeLoop } From bc6cca6ebe06f2071a01bfba9d54402cc05d33bc Mon Sep 17 00:00:00 2001 From: Brad Cornes Date: Mon, 17 Oct 2022 12:39:44 +0100 Subject: [PATCH 16/26] Don't show error popups --- packages/tailwindcss-language-server/src/util/error.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/tailwindcss-language-server/src/util/error.ts b/packages/tailwindcss-language-server/src/util/error.ts index 620412e0..00fdac25 100644 --- a/packages/tailwindcss-language-server/src/util/error.ts +++ b/packages/tailwindcss-language-server/src/util/error.ts @@ -25,11 +25,11 @@ export function showError( message: string = 'Tailwind CSS' ): void { console.error(formatError(message, err)) - if (!(err instanceof SilentError)) { - connection.sendNotification('@/tailwindCSS/error', { - message: formatError(message, err, false), - }) - } + // if (!(err instanceof SilentError)) { + // connection.sendNotification('@/tailwindCSS/error', { + // message: formatError(message, err, false), + // }) + // } } export function SilentError(message: string) { From 699bd56ec3e2803c73c2d6685b343616a6a502a8 Mon Sep 17 00:00:00 2001 From: Brad Cornes Date: Mon, 17 Oct 2022 13:42:57 +0100 Subject: [PATCH 17/26] wip --- .../tailwindcss-language-server/src/server.ts | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/packages/tailwindcss-language-server/src/server.ts b/packages/tailwindcss-language-server/src/server.ts index e50d8be7..87f8d27d 100644 --- a/packages/tailwindcss-language-server/src/server.ts +++ b/packages/tailwindcss-language-server/src/server.ts @@ -327,7 +327,8 @@ async function createProjectService( updateCapabilities: () => void, checkOpenDocuments: () => void, refreshDiagnostics: () => void, - watchPatterns: (patterns: string[]) => void + watchPatterns: (patterns: string[]) => void, + initialTailwindVersion: string ): Promise { let enabled = false const folder = projectConfig.folder @@ -355,6 +356,10 @@ async function createProjectService( }, } + if (projectConfig.configPath) { + watchPatterns([projectConfig.configPath, ...getModuleDependencies(projectConfig.configPath)]) + } + function log(...args: string[]): void { console.log( `[${path.relative(projectConfig.folder, projectConfig.configPath)}] ${args.join(' ')}` @@ -381,10 +386,9 @@ async function createProjectService( ...documentSelector.filter( ({ priority }) => priority !== DocumentSelectorPriority.CONTENT_FILE ), - // TODO `state.version` isn't going to exist here ...getContentDocumentSelectorFromConfigFile( projectConfig.configPath, - state.version, + initialTailwindVersion, projectConfig.folder ), ] @@ -1803,7 +1807,12 @@ class TW { await Promise.all( workspaceFolders.map((projectConfig) => - this.addProject(projectConfig, this.initializeParams, this.watchPatterns) + this.addProject( + projectConfig, + this.initializeParams, + this.watchPatterns, + configTailwindVersionMap.get(projectConfig.configPath) + ) ) ) @@ -1868,7 +1877,8 @@ class TW { private async addProject( projectConfig: ProjectConfig, params: InitializeParams, - watchPatterns: (patterns: string[]) => void + watchPatterns: (patterns: string[]) => void, + tailwindVersion: string ): Promise { let key = JSON.stringify(projectConfig) @@ -1890,7 +1900,8 @@ class TW { } }, () => this.refreshDiagnostics(), - (patterns: string[]) => watchPatterns(patterns) + (patterns: string[]) => watchPatterns(patterns), + tailwindVersion ) this.projects.set(key, project) } From 9a976f0a5a6123fd10879834d4e2b777e85cbae0 Mon Sep 17 00:00:00 2001 From: Brad Cornes Date: Mon, 17 Oct 2022 14:47:06 +0100 Subject: [PATCH 18/26] handle negated content paths --- .../tailwindcss-language-server/src/server.ts | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/packages/tailwindcss-language-server/src/server.ts b/packages/tailwindcss-language-server/src/server.ts index 87f8d27d..bb6867ff 100644 --- a/packages/tailwindcss-language-server/src/server.ts +++ b/packages/tailwindcss-language-server/src/server.ts @@ -1434,7 +1434,11 @@ function getContentDocumentSelectorFromConfigFile( } return content .filter((item): item is string => typeof item === 'string') - .map((item) => path.resolve(contentBase, item)) + .map((item) => + item.startsWith('!') + ? `!${path.resolve(contentBase, item.slice(1))}` + : path.resolve(contentBase, item) + ) .map((item) => ({ pattern: normalizePath(item), priority: DocumentSelectorPriority.CONTENT_FILE, @@ -1965,8 +1969,25 @@ class TW { for (let [key, project] of this.projects) { let projectConfig = JSON.parse(key) as ProjectConfig if (projectConfig.configPath) { - for (let { pattern, priority } of project.documentSelector()) { - if (minimatch(URI.parse(document.uri).fsPath, pattern) && priority < matchedPriority) { + let documentSelector = project + .documentSelector() + .concat() + // move all the negated patterns to the front + .sort((a, z) => { + if (a.pattern.startsWith('!') && !z.pattern.startsWith('!')) { + return -1 + } + if (!a.pattern.startsWith('!') && z.pattern.startsWith('!')) { + return 1 + } + return 0 + }) + for (let { pattern, priority } of documentSelector) { + let fsPath = URI.parse(document.uri).fsPath + if (pattern.startsWith('!') && minimatch(fsPath, pattern.slice(1), { dot: true })) { + break + } + if (minimatch(fsPath, pattern, { dot: true }) && priority < matchedPriority) { matchedProject = project matchedPriority = priority } From dbe586412989e3d68beb9335b9624e95367c5130 Mon Sep 17 00:00:00 2001 From: Brad Cornes Date: Mon, 17 Oct 2022 20:11:02 +0100 Subject: [PATCH 19/26] Handle non-tailwind dependency installs --- packages/tailwindcss-language-server/src/server.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/tailwindcss-language-server/src/server.ts b/packages/tailwindcss-language-server/src/server.ts index bb6867ff..92397673 100644 --- a/packages/tailwindcss-language-server/src/server.ts +++ b/packages/tailwindcss-language-server/src/server.ts @@ -377,8 +377,9 @@ async function createProjectService( ? file === projectConfig.configPath : minimatch(file, `**/${CONFIG_GLOB}`, { dot: true }) let isDependency = state.dependencies && state.dependencies.includes(change.file) + let isPackageFile = minimatch(file, `**/${PACKAGE_LOCK_GLOB}`, { dot: true }) - if (!isConfigFile && !isDependency) continue + if (!isConfigFile && !isDependency && !isPackageFile) continue if (!enabled) { if (projectConfig.configPath && (isConfigFile || isDependency)) { @@ -404,7 +405,7 @@ async function createProjectService( break } else if (change.type === FileChangeType.Changed) { log('File changed:', change.file) - if (!state.enabled) { + if (!state.enabled || isPackageFile) { needsInit = true break } else { @@ -412,7 +413,7 @@ async function createProjectService( } } else if (change.type === FileChangeType.Deleted) { log('File deleted:', change.file) - if (!state.enabled || isConfigFile) { + if (!state.enabled || isConfigFile || isPackageFile) { needsInit = true break } else { From 3e923db3dd74770337f7b467514bda0c5689c70c Mon Sep 17 00:00:00 2001 From: Brad Cornes Date: Mon, 17 Oct 2022 20:49:00 +0100 Subject: [PATCH 20/26] add package root to document selectors --- .../tailwindcss-language-server/src/server.ts | 41 ++++++++++++------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/packages/tailwindcss-language-server/src/server.ts b/packages/tailwindcss-language-server/src/server.ts index 92397673..e76b1449 100644 --- a/packages/tailwindcss-language-server/src/server.ts +++ b/packages/tailwindcss-language-server/src/server.ts @@ -195,14 +195,15 @@ type ProjectConfig = { documentSelector?: Array } -type DocumentSelector = { pattern: string; priority: number } enum DocumentSelectorPriority { CONFIG_FILE = 0, CSS_FILE = 0, CONTENT_FILE = 1, CSS_DIRECTORY = 2, CONFIG_DIRECTORY = 3, + ROOT_DIRECTORY = 4, } +type DocumentSelector = { pattern: string; priority: DocumentSelectorPriority } function getMode(config: any): unknown { if (typeof config.mode !== 'undefined') { @@ -1403,6 +1404,26 @@ async function getConfigFileFromCssFile(cssFile: string): Promise return path.resolve(path.dirname(cssFile), match.groups.config.slice(1, -1)) } +function getPackageRoot(cwd: string, rootDir: string) { + try { + let pkgJsonPath = findUp.sync( + (dir) => { + let pkgJson = path.join(dir, 'package.json') + if (findUp.sync.exists(pkgJson)) { + return pkgJson + } + if (dir === rootDir) { + return findUp.stop + } + }, + { cwd } + ) + return pkgJsonPath ? path.dirname(pkgJsonPath) : rootDir + } catch { + return rootDir + } +} + function getContentDocumentSelectorFromConfigFile( configPath: string, tailwindVersion: string, @@ -1419,19 +1440,7 @@ function getContentDocumentSelectorFromConfigFile( if (relativeEnabled) { contentBase = path.dirname(configPath) } else { - let pkgJsonPath = findUp.sync( - (dir) => { - let pkgJson = path.join(dir, 'package.json') - if (findUp.sync.exists(pkgJson)) { - return pkgJson - } - if (dir === rootDir) { - return findUp.stop - } - }, - { cwd: path.dirname(configPath) } - ) - contentBase = pkgJsonPath ? path.dirname(pkgJsonPath) : rootDir + contentBase = getPackageRoot(path.dirname(configPath), rootDir) } return content .filter((item): item is string => typeof item === 'string') @@ -1587,6 +1596,10 @@ class TW { } : [] ) + .concat({ + pattern: normalizePath(path.join(getPackageRoot(path.dirname(configPath), base), '**')), + priority: DocumentSelectorPriority.ROOT_DIRECTORY, + }) projects[configPath] = [...(projects[configPath] ?? []), ...documentSelector] if (isCssFile) { cssFileConfigMap.set(normalizedFilename, configPath) From 2920da8770db26c05f2eeb4a6a229bc835030b51 Mon Sep 17 00:00:00 2001 From: Brad Cornes Date: Mon, 17 Oct 2022 20:58:21 +0100 Subject: [PATCH 21/26] tidy --- .../tailwindcss-language-server/src/server.ts | 42 ++++++++++--------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/packages/tailwindcss-language-server/src/server.ts b/packages/tailwindcss-language-server/src/server.ts index e76b1449..50d8093d 100644 --- a/packages/tailwindcss-language-server/src/server.ts +++ b/packages/tailwindcss-language-server/src/server.ts @@ -1567,40 +1567,44 @@ class TW { contentSelector = getContentDocumentSelectorFromConfigFile(configPath, twVersion, base) } catch {} - let documentSelector = contentSelector - .concat({ + let documentSelector: DocumentSelector[] = [ + { pattern: normalizePath(filename), priority: isCssFile ? DocumentSelectorPriority.CSS_FILE : DocumentSelectorPriority.CONFIG_FILE, - }) - .concat( - isCssFile - ? { + }, + ...(isCssFile + ? [ + { pattern: normalizePath(configPath), priority: DocumentSelectorPriority.CONFIG_FILE, - } - : [] - ) - .concat({ + }, + ] + : []), + ...contentSelector, + { pattern: normalizePath(path.join(path.dirname(filename), '**')), priority: isCssFile ? DocumentSelectorPriority.CSS_DIRECTORY : DocumentSelectorPriority.CONFIG_DIRECTORY, - }) - .concat( - isCssFile - ? { + }, + ...(isCssFile + ? [ + { pattern: normalizePath(path.join(path.dirname(configPath), '**')), priority: DocumentSelectorPriority.CONFIG_DIRECTORY, - } - : [] - ) - .concat({ + }, + ] + : []), + { pattern: normalizePath(path.join(getPackageRoot(path.dirname(configPath), base), '**')), priority: DocumentSelectorPriority.ROOT_DIRECTORY, - }) + }, + ] + projects[configPath] = [...(projects[configPath] ?? []), ...documentSelector] + if (isCssFile) { cssFileConfigMap.set(normalizedFilename, configPath) } From 04b9e57c320332b3996a771e2be72b5e877e8570 Mon Sep 17 00:00:00 2001 From: Brad Cornes Date: Tue, 18 Oct 2022 11:03:26 +0100 Subject: [PATCH 22/26] wip --- .../tailwindcss-language-server/src/server.ts | 57 +++++++++++++++---- 1 file changed, 47 insertions(+), 10 deletions(-) diff --git a/packages/tailwindcss-language-server/src/server.ts b/packages/tailwindcss-language-server/src/server.ts index 50d8093d..b352fb04 100644 --- a/packages/tailwindcss-language-server/src/server.ts +++ b/packages/tailwindcss-language-server/src/server.ts @@ -320,6 +320,38 @@ function withFallback(getter: () => T, fallback: T): T { } } +function dirContains(dir: string, file: string): boolean { + let relative = path.relative(dir, file) + return !!relative && !relative.startsWith('..') && !path.isAbsolute(relative) +} + +function changeAffectsFile(change: string, files: string[]): boolean { + for (let file of files) { + console.log({ change, file, contains: dirContains(change, file) }) + if (change === file || dirContains(change, file)) { + return true + } + } + return false +} + +// We need to add parent directories to the watcher: +// https://github.com/microsoft/vscode/issues/60813 +function getWatchPatternsForFile(file: string): string[] { + let tmp: string + let dir = path.dirname(file) + let patterns: string[] = [file, dir] + while (true) { + dir = path.dirname((tmp = dir)) + if (tmp === dir) { + break + } else { + patterns.push(dir) + } + } + return patterns +} + async function createProjectService( projectConfig: ProjectConfig, connection: Connection, @@ -358,7 +390,14 @@ async function createProjectService( } if (projectConfig.configPath) { - watchPatterns([projectConfig.configPath, ...getModuleDependencies(projectConfig.configPath)]) + let deps = [] + try { + deps = getModuleDependencies(projectConfig.configPath) + } catch {} + watchPatterns([ + ...getWatchPatternsForFile(projectConfig.configPath), + ...deps.flatMap((dep) => getWatchPatternsForFile(dep)), + ]) } function log(...args: string[]): void { @@ -374,10 +413,8 @@ async function createProjectService( for (let change of changes) { let file = normalizePath(change.file) - let isConfigFile = projectConfig.configPath - ? file === projectConfig.configPath - : minimatch(file, `**/${CONFIG_GLOB}`, { dot: true }) - let isDependency = state.dependencies && state.dependencies.includes(change.file) + let isConfigFile = changeAffectsFile(file, [projectConfig.configPath]) + let isDependency = changeAffectsFile(change.file, state.dependencies ?? []) let isPackageFile = minimatch(file, `**/${PACKAGE_LOCK_GLOB}`, { dot: true }) if (!isConfigFile && !isDependency && !isPackageFile) continue @@ -432,7 +469,6 @@ async function createProjectService( function resetState(): void { // clearAllDiagnostics(state) - refreshDiagnostics() Object.keys(state).forEach((key) => { // Keep `dependencies` to ensure that they are still watched if (key !== 'editor' && key !== 'dependencies') { @@ -440,6 +476,7 @@ async function createProjectService( } }) state.enabled = false + refreshDiagnostics() updateCapabilities() } @@ -478,7 +515,7 @@ async function createProjectService( throw new SilentError('No config file found.') } - watchPatterns([configPath]) + watchPatterns(getWatchPatternsForFile(configPath)) const pnpPath = findUp.sync( (dir) => { @@ -967,7 +1004,7 @@ async function createProjectService( // } state.dependencies = getModuleDependencies(state.configPath) // chokidarWatcher?.add(state.dependencies) - watchPatterns(state.dependencies ?? []) + watchPatterns((state.dependencies ?? []).flatMap((dep) => getWatchPatternsForFile(dep))) state.configId = getConfigId(state.configPath, state.dependencies) @@ -1686,8 +1723,8 @@ class TW { for (let [key] of this.projects) { let projectConfig = JSON.parse(key) as ProjectConfig if ( - normalizedFilename === projectConfig.configPath && - change.type === FileChangeType.Deleted + change.type === FileChangeType.Deleted && + changeAffectsFile(normalizedFilename, [projectConfig.configPath]) ) { needsRestart = true break changeLoop From a929bb696f4241f6c76067b6aee4db57a3790ede Mon Sep 17 00:00:00 2001 From: Brad Cornes Date: Tue, 18 Oct 2022 11:03:39 +0100 Subject: [PATCH 23/26] dedupe document selectors --- packages/tailwindcss-language-server/src/server.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/tailwindcss-language-server/src/server.ts b/packages/tailwindcss-language-server/src/server.ts index b352fb04..812801b0 100644 --- a/packages/tailwindcss-language-server/src/server.ts +++ b/packages/tailwindcss-language-server/src/server.ts @@ -1652,7 +1652,12 @@ class TW { return { folder: base, configPath, - documentSelector, + documentSelector: documentSelector + .sort((a, z) => a.priority - z.priority) + .filter( + ({ pattern }, index, documentSelectors) => + documentSelectors.findIndex(({ pattern: p }) => p === pattern) === index + ), } }) } From 62658cfd841f1e5f25038b30c3a8baa7182f97d4 Mon Sep 17 00:00:00 2001 From: Brad Cornes Date: Tue, 18 Oct 2022 15:08:17 +0100 Subject: [PATCH 24/26] Fix `@config` regex --- packages/tailwindcss-language-server/src/server.ts | 2 +- packages/vscode-tailwindcss/src/extension.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/tailwindcss-language-server/src/server.ts b/packages/tailwindcss-language-server/src/server.ts index f6fc951b..eff2c274 100644 --- a/packages/tailwindcss-language-server/src/server.ts +++ b/packages/tailwindcss-language-server/src/server.ts @@ -1488,7 +1488,7 @@ async function getPlugins(config: any) { async function getConfigFileFromCssFile(cssFile: string): Promise { let css = getTextWithoutComments(await fs.promises.readFile(cssFile, 'utf8'), 'css') - let match = css.match(/(?:\b|^)@config\s*(?'[^']+'|"[^"]+")/) + let match = css.match(/@config\s*(?'[^']+'|"[^"]+")/) if (!match) { return null } diff --git a/packages/vscode-tailwindcss/src/extension.ts b/packages/vscode-tailwindcss/src/extension.ts index b8ec200d..e618e06c 100755 --- a/packages/vscode-tailwindcss/src/extension.ts +++ b/packages/vscode-tailwindcss/src/extension.ts @@ -132,7 +132,7 @@ function mergeExcludes(settings: WorkspaceConfiguration, scope: ConfigurationSco async function fileContainsAtConfig(uri: Uri) { let contents = (await Workspace.fs.readFile(uri)).toString() - return /(^|\b)@config\s*['"]/.test(contents) + return /@config\s*['"]/.test(contents) } export async function activate(context: ExtensionContext) { From 3138265ae7c018dfb117934f3525da01a9198397 Mon Sep 17 00:00:00 2001 From: Brad Cornes Date: Tue, 18 Oct 2022 19:43:32 +0100 Subject: [PATCH 25/26] Fix document selectors when using `experimental.configFile` --- .../tailwindcss-language-server/src/server.ts | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/packages/tailwindcss-language-server/src/server.ts b/packages/tailwindcss-language-server/src/server.ts index eff2c274..50076d81 100644 --- a/packages/tailwindcss-language-server/src/server.ts +++ b/packages/tailwindcss-language-server/src/server.ts @@ -200,9 +200,11 @@ type ProjectConfig = { folder: string configPath?: string documentSelector?: Array + isUserConfigured: boolean } enum DocumentSelectorPriority { + USER_CONFIGURED = 0, CONFIG_FILE = 0, CSS_FILE = 0, CONTENT_FILE = 1, @@ -448,7 +450,11 @@ async function createProjectService( if (!isConfigFile && !isDependency && !isPackageFile) continue if (!enabled) { - if (projectConfig.configPath && (isConfigFile || isDependency)) { + if ( + !projectConfig.isUserConfigured && + projectConfig.configPath && + (isConfigFile || isDependency) + ) { documentSelector = [ ...documentSelector.filter( ({ priority }) => priority !== DocumentSelectorPriority.CONTENT_FILE @@ -959,17 +965,19 @@ async function createProjectService( } ///////////////////// - documentSelector = [ - ...documentSelector.filter( - ({ priority }) => priority !== DocumentSelectorPriority.CONTENT_FILE - ), - ...getContentDocumentSelectorFromConfigFile( - state.configPath, - tailwindcss.version, - projectConfig.folder, - originalConfig - ), - ] + if (!projectConfig.isUserConfigured) { + documentSelector = [ + ...documentSelector.filter( + ({ priority }) => priority !== DocumentSelectorPriority.CONTENT_FILE + ), + ...getContentDocumentSelectorFromConfigFile( + state.configPath, + tailwindcss.version, + projectConfig.folder, + originalConfig + ), + ] + } ////////////////////// try { @@ -1610,9 +1618,10 @@ class TW { folder: base, configPath: path.resolve(base, relativeConfigPath), documentSelector: [].concat(relativeDocumentSelectorOrSelectors).map((selector) => ({ - priority: DocumentSelectorPriority.CONTENT_FILE, + priority: DocumentSelectorPriority.USER_CONFIGURED, pattern: path.resolve(base, selector), })), + isUserConfigured: true, } } ) @@ -1706,6 +1715,7 @@ class TW { return { folder: base, configPath, + isUserConfigured: false, documentSelector: documentSelector .sort((a, z) => a.priority - z.priority) .filter( From 0dca6f67924dd7593ebc2f6e45de4acea76617af Mon Sep 17 00:00:00 2001 From: Brad Cornes Date: Tue, 18 Oct 2022 19:43:56 +0100 Subject: [PATCH 26/26] Remove log --- packages/tailwindcss-language-server/src/server.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/tailwindcss-language-server/src/server.ts b/packages/tailwindcss-language-server/src/server.ts index 50076d81..0b9b4f57 100644 --- a/packages/tailwindcss-language-server/src/server.ts +++ b/packages/tailwindcss-language-server/src/server.ts @@ -336,7 +336,6 @@ function dirContains(dir: string, file: string): boolean { function changeAffectsFile(change: string, files: string[]): boolean { for (let file of files) { - console.log({ change, file, contains: dirContains(change, file) }) if (change === file || dirContains(change, file)) { return true }