diff --git a/packages/tailwindcss-language-server/package.json b/packages/tailwindcss-language-server/package.json index dd812e3c9..c4dc2984f 100644 --- a/packages/tailwindcss-language-server/package.json +++ b/packages/tailwindcss-language-server/package.json @@ -1,6 +1,6 @@ { "name": "@tailwindcss/language-server", - "version": "0.14.12", + "version": "0.14.13", "description": "Tailwind CSS Language Server", "license": "MIT", "repository": { @@ -40,7 +40,7 @@ "@tailwindcss/forms": "0.5.3", "@tailwindcss/language-service": "workspace:*", "@tailwindcss/line-clamp": "0.4.2", - "@tailwindcss/oxide": "^4.0.15", + "@tailwindcss/oxide": "^4.1.0", "@tailwindcss/typography": "0.5.7", "@types/braces": "3.0.1", "@types/color-name": "^1.1.3", @@ -67,7 +67,6 @@ "dset": "3.1.4", "enhanced-resolve": "^5.16.1", "esbuild": "^0.25.0", - "fast-glob": "3.2.4", "find-up": "5.0.0", "jiti": "^2.3.3", "klona": "2.0.4", @@ -84,7 +83,8 @@ "rimraf": "3.0.2", "stack-trace": "0.0.10", "tailwindcss": "3.4.17", - "tailwindcss-v4": "npm:tailwindcss@4.0.6", + "tailwindcss-v4": "npm:tailwindcss@4.1.1", + "tinyglobby": "^0.2.12", "tsconfck": "^3.1.4", "tsconfig-paths": "^4.2.0", "typescript": "5.3.3", diff --git a/packages/tailwindcss-language-server/src/language/css-server.ts b/packages/tailwindcss-language-server/src/language/css-server.ts index a43910b22..73e967fcc 100644 --- a/packages/tailwindcss-language-server/src/language/css-server.ts +++ b/packages/tailwindcss-language-server/src/language/css-server.ts @@ -13,7 +13,7 @@ import { CompletionItemKind, Connection, } from 'vscode-languageserver/node' -import { TextDocument } from 'vscode-languageserver-textdocument' +import { Position, TextDocument } from 'vscode-languageserver-textdocument' import { Utils, URI } from 'vscode-uri' import { getLanguageModelCache } from './languageModelCache' import { Stylesheet } from 'vscode-css-languageservice' @@ -121,6 +121,7 @@ export class CssServer { async function withDocumentAndSettings( uri: string, callback: (result: { + original: TextDocument document: TextDocument settings: LanguageSettings | undefined }) => T | Promise, @@ -130,13 +131,64 @@ export class CssServer { return null } return await callback({ + original: document, document: createVirtualCssDocument(document), settings: await getDocumentSettings(document), }) } + function isInImportDirective(doc: TextDocument, pos: Position) { + let text = doc.getText({ + start: { line: pos.line, character: 0 }, + end: pos, + }) + + // Scan backwards to see if we're inside an `@import` directive + let foundImport = false + let foundDirective = false + + for (let i = text.length - 1; i >= 0; i--) { + let char = text[i] + if (char === '\n') break + + if (char === '(' && !foundDirective) { + if (text.startsWith(' source(', i - 7)) { + foundDirective = true + } + + // + else if (text.startsWith(' theme(', i - 6)) { + foundDirective = true + } + + // + else if (text.startsWith(' prefix(', i - 7)) { + foundDirective = true + } + } + + // + else if (char === '@' && !foundImport) { + if (text.startsWith('@import ', i)) { + foundImport = true + } + } + } + + return foundImport && foundDirective + } + connection.onCompletion(async ({ textDocument, position }, _token) => - withDocumentAndSettings(textDocument.uri, async ({ document, settings }) => { + withDocumentAndSettings(textDocument.uri, async ({ original, document, settings }) => { + // If we're inside source(…), prefix(…), or theme(…), don't show + // completions from the CSS language server + if (isInImportDirective(original, position)) { + return { + isIncomplete: false, + items: [], + } + } + let result = await cssLanguageService.doComplete2( document, position, diff --git a/packages/tailwindcss-language-server/src/project-locator.test.ts b/packages/tailwindcss-language-server/src/project-locator.test.ts index 429f0b27b..d8c35479b 100644 --- a/packages/tailwindcss-language-server/src/project-locator.test.ts +++ b/packages/tailwindcss-language-server/src/project-locator.test.ts @@ -123,6 +123,7 @@ testFixture('v4/workspaces', [ '{URL}/packages/admin/**', '{URL}/packages/admin/app.css', '{URL}/packages/admin/package.json', + '{URL}/packages/admin/tw.css', ], }, { @@ -147,8 +148,8 @@ testLocator({ 'package.json': json` { "dependencies": { - "tailwindcss": "^4.0.15", - "@tailwindcss/oxide": "^4.0.15" + "tailwindcss": "4.1.0", + "@tailwindcss/oxide": "4.1.0" } } `, @@ -164,7 +165,7 @@ testLocator({ content: [ '/*', '/package.json', - '/src/**/*.{aspx,astro,cjs,cts,eex,erb,gjs,gts,haml,handlebars,hbs,heex,html,jade,js,jsx,liquid,md,mdx,mjs,mts,mustache,njk,nunjucks,php,pug,py,razor,rb,rhtml,rs,slim,svelte,tpl,ts,tsx,twig,vue}', + '/src/**/*.{aspx,astro,cjs,css,cts,eex,erb,gjs,gts,haml,handlebars,hbs,heex,html,jade,js,json,jsx,liquid,md,mdx,mjs,mts,mustache,njk,nunjucks,php,pug,py,razor,rb,rhtml,rs,slim,svelte,tpl,ts,tsx,twig,vue}', '/src/components/example.html', '/src/index.html', ], @@ -178,8 +179,8 @@ testLocator({ 'package.json': json` { "dependencies": { - "tailwindcss": "^4.0.15", - "@tailwindcss/oxide": "^4.0.15" + "tailwindcss": "4.1.0", + "@tailwindcss/oxide": "4.1.0" } } `, @@ -197,7 +198,7 @@ testLocator({ content: [ '/*', '/package.json', - '/src/**/*.{aspx,astro,cjs,cts,eex,erb,gjs,gts,haml,handlebars,hbs,heex,html,jade,js,jsx,liquid,md,mdx,mjs,mts,mustache,njk,nunjucks,php,pug,py,razor,rb,rhtml,rs,slim,svelte,tpl,ts,tsx,twig,vue}', + '/src/**/*.{aspx,astro,cjs,css,cts,eex,erb,gjs,gts,haml,handlebars,hbs,heex,html,jade,js,json,jsx,liquid,md,mdx,mjs,mts,mustache,njk,nunjucks,php,pug,py,razor,rb,rhtml,rs,slim,svelte,tpl,ts,tsx,twig,vue}', '/src/components/example.html', '/src/index.html', ], @@ -211,8 +212,8 @@ testLocator({ 'package.json': json` { "dependencies": { - "tailwindcss": "^4.0.15", - "@tailwindcss/oxide": "^4.0.15" + "tailwindcss": "4.1.0", + "@tailwindcss/oxide": "4.1.0" } } `, @@ -245,36 +246,40 @@ testLocator({ content: [ '/*', '/admin/foo.bin', - '/admin/{**/*.bin,**/*.{aspx,astro,cjs,cts,eex,erb,gjs,gts,haml,handlebars,hbs,heex,html,jade,js,jsx,liquid,md,mdx,mjs,mts,mustache,njk,nunjucks,php,pug,py,razor,rb,rhtml,rs,slim,svelte,tpl,ts,tsx,twig,vue}}', + '/admin/tw.css', + '/admin/ui.css', + '/admin/{**/*.bin,**/*.{aspx,astro,bin,cjs,css,cts,eex,erb,gjs,gts,haml,handlebars,hbs,heex,html,jade,js,json,jsx,liquid,md,mdx,mjs,mts,mustache,njk,nunjucks,php,pug,py,razor,rb,rhtml,rs,slim,svelte,tpl,ts,tsx,twig,vue}}', '/package.json', '/shared.html', - '/web/**/*.{aspx,astro,cjs,cts,eex,erb,gjs,gts,haml,handlebars,hbs,heex,html,jade,js,jsx,liquid,md,mdx,mjs,mts,mustache,njk,nunjucks,php,pug,py,razor,rb,rhtml,rs,slim,svelte,tpl,ts,tsx,twig,vue}', + '/web/**/*.{aspx,astro,bin,cjs,css,cts,eex,erb,gjs,gts,haml,handlebars,hbs,heex,html,jade,js,json,jsx,liquid,md,mdx,mjs,mts,mustache,njk,nunjucks,php,pug,py,razor,rb,rhtml,rs,slim,svelte,tpl,ts,tsx,twig,vue}', + '/web/app.css', ], }, { config: '/web/app.css', content: [ '/*', - '/admin/**/*.{aspx,astro,cjs,cts,eex,erb,gjs,gts,haml,handlebars,hbs,heex,html,jade,js,jsx,liquid,md,mdx,mjs,mts,mustache,njk,nunjucks,php,pug,py,razor,rb,rhtml,rs,slim,svelte,tpl,ts,tsx,twig,vue}', + '/admin/**/*.{aspx,astro,bin,cjs,css,cts,eex,erb,gjs,gts,haml,handlebars,hbs,heex,html,jade,js,json,jsx,liquid,md,mdx,mjs,mts,mustache,njk,nunjucks,php,pug,py,razor,rb,rhtml,rs,slim,svelte,tpl,ts,tsx,twig,vue}', + '/admin/app.css', + '/admin/tw.css', + '/admin/ui.css', '/package.json', '/shared.html', '/web/bar.bin', - '/web/{**/*.{aspx,astro,cjs,cts,eex,erb,gjs,gts,haml,handlebars,hbs,heex,html,jade,js,jsx,liquid,md,mdx,mjs,mts,mustache,njk,nunjucks,php,pug,py,razor,rb,rhtml,rs,slim,svelte,tpl,ts,tsx,twig,vue},*.bin}', + '/web/{**/*.{aspx,astro,bin,cjs,css,cts,eex,erb,gjs,gts,haml,handlebars,hbs,heex,html,jade,js,json,jsx,liquid,md,mdx,mjs,mts,mustache,njk,nunjucks,php,pug,py,razor,rb,rhtml,rs,slim,svelte,tpl,ts,tsx,twig,vue},*.bin}', ], }, ], }) testLocator({ - // TODO: Enable once v4.1 is released - options: { skip: true }, name: 'automatic content detection with negative custom sources', fs: { 'package.json': json` { "dependencies": { - "tailwindcss": "0.0.0-insiders.3e53e25", - "@tailwindcss/oxide": "0.0.0-insiders.3e53e25" + "tailwindcss": "4.1.0", + "@tailwindcss/oxide": "4.1.0" } } `, @@ -293,7 +298,7 @@ testLocator({ '/*', '/package.json', '/src/index.html', - '/src/{**/*.html,**/*.{aspx,astro,cjs,cts,eex,erb,gjs,gts,haml,handlebars,hbs,heex,html,jade,js,json,jsx,liquid,md,mdx,mjs,mts,mustache,njk,nunjucks,php,pug,py,razor,rb,rhtml,rs,slim,svelte,tpl,ts,tsx,twig,vue}}', + '/src/{**/*.html,**/*.{aspx,astro,cjs,css,cts,eex,erb,gjs,gts,haml,handlebars,hbs,heex,html,jade,js,json,jsx,liquid,md,mdx,mjs,mts,mustache,njk,nunjucks,php,pug,py,razor,rb,rhtml,rs,slim,svelte,tpl,ts,tsx,twig,vue}}', ], }, ], @@ -303,7 +308,7 @@ testFixture('v4/missing-files', [ // { config: 'app.css', - content: ['{URL}/*', '{URL}/package.json'], + content: ['{URL}/*', '{URL}/i-exist.css', '{URL}/package.json'], }, ]) @@ -314,7 +319,8 @@ testFixture('v4/path-mappings', [ content: [ '{URL}/*', '{URL}/package.json', - '{URL}/src/**/*.{aspx,astro,cjs,cts,eex,erb,gjs,gts,haml,handlebars,hbs,heex,html,jade,js,jsx,liquid,md,mdx,mjs,mts,mustache,njk,nunjucks,php,pug,py,razor,rb,rhtml,rs,slim,svelte,tpl,ts,tsx,twig,vue}', + '{URL}/src/**/*.{aspx,astro,cjs,css,cts,eex,erb,gjs,gts,haml,handlebars,hbs,heex,html,jade,js,json,jsx,liquid,md,mdx,mjs,mts,mustache,njk,nunjucks,php,pug,py,razor,rb,rhtml,rs,slim,svelte,tpl,ts,tsx,twig,vue}', + '{URL}/src/a/file.css', '{URL}/src/a/my-config.ts', '{URL}/src/a/my-plugin.ts', '{URL}/tsconfig.json', @@ -326,7 +332,7 @@ testFixture('v4/invalid-import-order', [ // { config: 'tailwind.css', - content: ['{URL}/*', '{URL}/package.json'], + content: ['{URL}/*', '{URL}/a.css', '{URL}/b.css', '{URL}/package.json'], }, ]) @@ -338,7 +344,7 @@ testLocator({ 'package.json': json` { "dependencies": { - "tailwindcss": "^4.0.2" + "tailwindcss": "4.1.0" } } `, @@ -386,7 +392,7 @@ testLocator({ 'package.json': json` { "dependencies": { - "tailwindcss": "4.0.6" + "tailwindcss": "4.1.0" } } `, @@ -415,13 +421,47 @@ testLocator({ }, expected: [ { - version: '4.0.6', + version: '4.1.0', config: '/src/articles/articles.css', content: [], }, ], }) +testLocator({ + name: 'Recursive symlinks do not cause infinite traversal loops', + fs: { + 'src/a/b/c/index.css': css` + @import 'tailwindcss'; + `, + 'src/a/b/c/z': symlinkTo('src', 'dir'), + 'src/a/b/x': symlinkTo('src', 'dir'), + 'src/a/b/y': symlinkTo('src', 'dir'), + 'src/a/b/z': symlinkTo('src', 'dir'), + 'src/a/x': symlinkTo('src', 'dir'), + + 'src/b/c/d/z': symlinkTo('src', 'dir'), + 'src/b/c/d/index.css': css``, + 'src/b/c/x': symlinkTo('src', 'dir'), + 'src/b/c/y': symlinkTo('src', 'dir'), + 'src/b/c/z': symlinkTo('src', 'dir'), + 'src/b/x': symlinkTo('src', 'dir'), + + 'src/c/d/e/z': symlinkTo('src', 'dir'), + 'src/c/d/x': symlinkTo('src', 'dir'), + 'src/c/d/y': symlinkTo('src', 'dir'), + 'src/c/d/z': symlinkTo('src', 'dir'), + 'src/c/x': symlinkTo('src', 'dir'), + }, + expected: [ + { + version: '4.0.6 (bundled)', + config: '/src/a/b/c/index.css', + content: [], + }, + ], +}) + // --- function testLocator({ diff --git a/packages/tailwindcss-language-server/src/project-locator.ts b/packages/tailwindcss-language-server/src/project-locator.ts index c04738bf8..de562b633 100644 --- a/packages/tailwindcss-language-server/src/project-locator.ts +++ b/packages/tailwindcss-language-server/src/project-locator.ts @@ -1,7 +1,6 @@ -import * as os from 'node:os' import * as path from 'node:path' import * as fs from 'node:fs/promises' -import glob from 'fast-glob' +import { glob } from 'tinyglobby' import picomatch from 'picomatch' import type { Settings } from '@tailwindcss/language-service/src/util/state' import { CONFIG_GLOB, CSS_GLOB } from './lib/constants' @@ -276,14 +275,14 @@ export class ProjectLocator { private async findConfigs(): Promise { // Look for config files and CSS files - let files = await glob([`**/${CONFIG_GLOB}`, `**/${CSS_GLOB}`], { + let files = await glob({ + patterns: [`**/${CONFIG_GLOB}`, `**/${CSS_GLOB}`], cwd: this.base, ignore: this.settings.tailwindCSS.files.exclude, onlyFiles: true, absolute: true, - suppressErrors: true, + followSymbolicLinks: true, dot: true, - concurrency: Math.max(os.cpus().length, 1), }) let realpaths = await Promise.all(files.map((file) => fs.realpath(file))) diff --git a/packages/tailwindcss-language-server/src/util/v4/design-system.ts b/packages/tailwindcss-language-server/src/util/v4/design-system.ts index 05d2ecd3e..f312b95c0 100644 --- a/packages/tailwindcss-language-server/src/util/v4/design-system.ts +++ b/packages/tailwindcss-language-server/src/util/v4/design-system.ts @@ -219,6 +219,14 @@ export async function loadDesignSystem( Object.assign(design, { dependencies: () => dependencies, + // TODOs: + // + // 1. Remove PostCSS parsing — its roughly 60% of the processing time + // ex: compiling 19k classes take 650ms and 400ms of that is PostCSS + // + // - Replace `candidatesToCss` with a `candidatesToAst` API + // First step would be to convert to a PostCSS AST by transforming the nodes directly + // Then it would be to drop the PostCSS AST representation entirely in all v4 code paths compile(classes: string[]): (postcss.Root | null)[] { let css = design.candidatesToCss(classes) let errors: any[] = [] diff --git a/packages/tailwindcss-language-server/tests/colors/colors.test.js b/packages/tailwindcss-language-server/tests/colors/colors.test.js index 5016bacc3..4780a4fb2 100644 --- a/packages/tailwindcss-language-server/tests/colors/colors.test.js +++ b/packages/tailwindcss-language-server/tests/colors/colors.test.js @@ -334,7 +334,7 @@ defineTest({ expect(c.project).toMatchObject({ tailwind: { - version: '4.0.6', + version: '4.1.1', isDefaultVersion: true, }, }) @@ -373,7 +373,7 @@ defineTest({ expect(c.project).toMatchObject({ tailwind: { - version: '4.0.6', + version: '4.1.1', isDefaultVersion: true, }, }) diff --git a/packages/tailwindcss-language-server/tests/completions/completions.test.js b/packages/tailwindcss-language-server/tests/completions/completions.test.js index dbe9a3522..ce5c7831e 100644 --- a/packages/tailwindcss-language-server/tests/completions/completions.test.js +++ b/packages/tailwindcss-language-server/tests/completions/completions.test.js @@ -313,8 +313,8 @@ withFixture('v4/basic', (c) => { let result = await completion({ lang, text, position, settings }) let textEdit = expect.objectContaining({ range: { start: position, end: position } }) - expect(result.items.length).toBe(13172) - expect(result.items.filter((item) => item.label.endsWith(':')).length).toBe(317) + expect(result.items.length).toBe(19283) + expect(result.items.filter((item) => item.label.endsWith(':')).length).toBe(346) expect(result).toEqual({ isIncomplete: false, items: expect.arrayContaining([ @@ -488,7 +488,7 @@ withFixture('v4/basic', (c) => { }) // Make sure `@slot` is NOT suggested by default - expect(result.items.length).toBe(7) + expect(result.items.length).toBe(8) expect(result.items).not.toEqual( expect.arrayContaining([ expect.objectContaining({ kind: 14, label: '@slot', sortText: '-0000000' }), @@ -627,7 +627,7 @@ withFixture('v4/basic', (c) => { expect(resolved).toEqual({ ...item, - detail: 'background-color: oklch(0.637 0.237 25.331);', + detail: 'background-color: oklch(63.7% 0.237 25.331);', documentation: '#fb2c36', }) }) @@ -692,7 +692,7 @@ defineTest({ // ^ let completion = await document.completions({ line: 0, character: 23 }) - expect(completion?.items.length).toBe(12289) + expect(completion?.items.length).toBe(19236) }, }) @@ -714,7 +714,7 @@ defineTest({ // ^ let completion = await document.completions({ line: 0, character: 22 }) - expect(completion?.items.length).toBe(12289) + expect(completion?.items.length).toBe(19236) }, }) @@ -736,7 +736,81 @@ defineTest({ // ^ let completion = await document.completions({ line: 0, character: 31 }) - expect(completion?.items.length).toBe(12289) + expect(completion?.items.length).toBe(19236) + }, +}) + +defineTest({ + name: 'v4: Completions show after a variant arbitrary value, using prefixes', + fs: { + 'app.css': css` + @import 'tailwindcss' prefix(tw); + `, + }, + prepare: async ({ root }) => ({ client: await createClient({ root }) }), + handle: async ({ client }) => { + let document = await client.open({ + lang: 'html', + text: '
', + }) + + //
+ // ^ + let completion = await document.completions({ line: 0, character: 26 }) + + expect(completion?.items.length).toBe(19236) + }, +}) + +defineTest({ + name: 'v4: Variant and utility suggestions show prefix when one has been typed', + fs: { + 'app.css': css` + @import 'tailwindcss' prefix(tw); + `, + }, + prepare: async ({ root }) => ({ client: await createClient({ root }) }), + handle: async ({ client }) => { + let document = await client.open({ + lang: 'html', + text: '
', + }) + + //
+ // ^ + let completion = await document.completions({ line: 0, character: 12 }) + + expect(completion?.items.length).toBe(19237) + + // Verify that variants and utilities are all prefixed + let prefixed = completion.items.filter((item) => !item.label.startsWith('tw:')) + expect(prefixed).toHaveLength(0) + }, +}) + +defineTest({ + name: 'v4: Variant and utility suggestions hide prefix when it has been typed', + fs: { + 'app.css': css` + @import 'tailwindcss' prefix(tw); + `, + }, + prepare: async ({ root }) => ({ client: await createClient({ root }) }), + handle: async ({ client }) => { + let document = await client.open({ + lang: 'html', + text: '
', + }) + + //
+ // ^ + let completion = await document.completions({ line: 0, character: 15 }) + + expect(completion?.items.length).toBe(19236) + + // Verify that no variants and utilities have prefixes + let prefixed = completion.items.filter((item) => item.label.startsWith('tw:')) + expect(prefixed).toHaveLength(0) }, }) @@ -765,7 +839,7 @@ defineTest({ // ^ let completion = await document.completions({ line: 0, character: 20 }) - expect(completion?.items.length).toBe(12289) + expect(completion?.items.length).toBe(19236) }, }) @@ -796,7 +870,7 @@ defineTest({ // ^ let completion = await document.completions({ line: 1, character: 22 }) - expect(completion?.items.length).toBe(12289) + expect(completion?.items.length).toBe(19236) }, }) @@ -886,24 +960,24 @@ defineTest({ // ^ let completionA = await document.completions({ line: 0, character: 13 }) - expect(completionA?.items.length).toBe(12289) + expect(completionA?.items.length).toBe(19236) // return ; // ^ let completionB = await document.completions({ line: 3, character: 30 }) - expect(completionB?.items.length).toBe(12289) + expect(completionB?.items.length).toBe(19236) // return ; // ^ let completionC = await document.completions({ line: 7, character: 30 }) - expect(completionC?.items.length).toBe(12289) + expect(completionC?.items.length).toBe(19236) // let y = cva(""); // ^ let completionD = await document.completions({ line: 10, character: 13 }) - expect(completionD?.items.length).toBe(12289) + expect(completionD?.items.length).toBe(19236) }, }) diff --git a/packages/tailwindcss-language-server/tests/css/css-server.test.ts b/packages/tailwindcss-language-server/tests/css/css-server.test.ts index e8970e08a..388e94af7 100644 --- a/packages/tailwindcss-language-server/tests/css/css-server.test.ts +++ b/packages/tailwindcss-language-server/tests/css/css-server.test.ts @@ -689,3 +689,49 @@ defineTest({ expect(await doc.diagnostics()).toEqual([]) }, }) + +defineTest({ + name: 'completions are hidden inside @import source(…)/theme(…)/prefix(…) functions', + prepare: async ({ root }) => ({ + client: await createClient({ + server: 'css', + root, + }), + }), + handle: async ({ client }) => { + let doc = await client.open({ + lang: 'tailwindcss', + name: 'file-1.css', + text: css` + @import './file.css' source(none); + @import './file.css' theme(inline); + @import './file.css' prefix(tw); + @import './file.css' source(none) theme(inline) prefix(tw); + `, + }) + + // @import './file.css' source(none) + // ^ + // @import './file.css' theme(inline); + // ^ + // @import './file.css' prefix(tw); + // ^ + let completionsA = await doc.completions({ line: 0, character: 29 }) + let completionsB = await doc.completions({ line: 1, character: 28 }) + let completionsC = await doc.completions({ line: 2, character: 29 }) + + expect(completionsA).toEqual({ isIncomplete: false, items: [] }) + expect(completionsB).toEqual({ isIncomplete: false, items: [] }) + expect(completionsC).toEqual({ isIncomplete: false, items: [] }) + + // @import './file.css' source(none) theme(inline) prefix(tw); + // ^ ^ ^ + let completionsD = await doc.completions({ line: 3, character: 29 }) + let completionsE = await doc.completions({ line: 3, character: 41 }) + let completionsF = await doc.completions({ line: 3, character: 56 }) + + expect(completionsD).toEqual({ isIncomplete: false, items: [] }) + expect(completionsE).toEqual({ isIncomplete: false, items: [] }) + expect(completionsF).toEqual({ isIncomplete: false, items: [] }) + }, +}) diff --git a/packages/tailwindcss-language-server/tests/env/v4.test.js b/packages/tailwindcss-language-server/tests/env/v4.test.js index 632eb1a2a..dc33c79f2 100644 --- a/packages/tailwindcss-language-server/tests/env/v4.test.js +++ b/packages/tailwindcss-language-server/tests/env/v4.test.js @@ -21,7 +21,7 @@ defineTest({ expect(await client.project()).toMatchObject({ tailwind: { - version: '4.0.6', + version: '4.1.1', isDefaultVersion: true, }, }) @@ -49,7 +49,7 @@ defineTest({ }, }) - expect(completion?.items.length).toBe(12288) + expect(completion?.items.length).toBe(19235) }, }) @@ -137,7 +137,7 @@ defineTest({ expect(await client.project()).toMatchObject({ tailwind: { - version: '4.0.6', + version: '4.1.1', isDefaultVersion: true, }, }) @@ -188,7 +188,7 @@ defineTest({ 'package.json': json` { "dependencies": { - "tailwindcss": "4.0.1" + "tailwindcss": "4.1.1" } } `, @@ -205,7 +205,7 @@ defineTest({ expect(await client.project()).toMatchObject({ tailwind: { - version: '4.0.1', + version: '4.1.1', isDefaultVersion: false, }, }) @@ -233,7 +233,7 @@ defineTest({ }, }) - expect(completion?.items.length).toBe(12288) + expect(completion?.items.length).toBe(19235) }, }) @@ -243,7 +243,7 @@ defineTest({ 'package.json': json` { "dependencies": { - "tailwindcss": "4.0.1" + "tailwindcss": "4.1.1" } } `, @@ -270,7 +270,7 @@ defineTest({ expect(await client.project()).toMatchObject({ tailwind: { - version: '4.0.1', + version: '4.1.1', isDefaultVersion: false, }, }) @@ -322,7 +322,7 @@ defineTest({ expect(await client.project()).toMatchObject({ tailwind: { - version: '4.0.6', + version: '4.1.1', isDefaultVersion: true, }, }) @@ -354,7 +354,7 @@ defineTest({ 'package.json': json` { "dependencies": { - "tailwindcss": "4.0.1" + "tailwindcss": "4.1.1" } } `, @@ -831,7 +831,7 @@ defineTest({ expect(await client.project()).toMatchObject({ tailwind: { - version: '4.0.6', + version: '4.1.1', isDefaultVersion: true, }, }) diff --git a/packages/tailwindcss-language-server/tests/fixtures/v4/basic/package-lock.json b/packages/tailwindcss-language-server/tests/fixtures/v4/basic/package-lock.json index 6275c3dc3..ab6be9775 100644 --- a/packages/tailwindcss-language-server/tests/fixtures/v4/basic/package-lock.json +++ b/packages/tailwindcss-language-server/tests/fixtures/v4/basic/package-lock.json @@ -5,13 +5,13 @@ "packages": { "": { "dependencies": { - "tailwindcss": "^4.0.15" + "tailwindcss": "4.1.1" } }, "node_modules/tailwindcss": { - "version": "4.0.15", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.15.tgz", - "integrity": "sha512-6ZMg+hHdMJpjpeCCFasX7K+U615U9D+7k5/cDK/iRwl6GptF24+I/AbKgOnXhVKePzrEyIXutLv36n4cRsq3Sg==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.1.tgz", + "integrity": "sha512-QNbdmeS979Efzim2g/bEvfuh+fTcIdp1y7gA+sb6OYSW74rt7Cr7M78AKdf6HqWT3d5AiTb7SwTT3sLQxr4/qw==", "license": "MIT" } } diff --git a/packages/tailwindcss-language-server/tests/fixtures/v4/basic/package.json b/packages/tailwindcss-language-server/tests/fixtures/v4/basic/package.json index b6cb53b1e..43b975c93 100644 --- a/packages/tailwindcss-language-server/tests/fixtures/v4/basic/package.json +++ b/packages/tailwindcss-language-server/tests/fixtures/v4/basic/package.json @@ -1,5 +1,5 @@ { "dependencies": { - "tailwindcss": "^4.0.15" + "tailwindcss": "4.1.1" } } diff --git a/packages/tailwindcss-language-server/tests/fixtures/v4/css-loading-js/package-lock.json b/packages/tailwindcss-language-server/tests/fixtures/v4/css-loading-js/package-lock.json index 5089dc65d..b92fb8488 100644 --- a/packages/tailwindcss-language-server/tests/fixtures/v4/css-loading-js/package-lock.json +++ b/packages/tailwindcss-language-server/tests/fixtures/v4/css-loading-js/package-lock.json @@ -5,13 +5,13 @@ "packages": { "": { "dependencies": { - "tailwindcss": "^4.0.15" + "tailwindcss": "4.1.1" } }, "node_modules/tailwindcss": { - "version": "4.0.15", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.15.tgz", - "integrity": "sha512-6ZMg+hHdMJpjpeCCFasX7K+U615U9D+7k5/cDK/iRwl6GptF24+I/AbKgOnXhVKePzrEyIXutLv36n4cRsq3Sg==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.1.tgz", + "integrity": "sha512-QNbdmeS979Efzim2g/bEvfuh+fTcIdp1y7gA+sb6OYSW74rt7Cr7M78AKdf6HqWT3d5AiTb7SwTT3sLQxr4/qw==", "license": "MIT" } } diff --git a/packages/tailwindcss-language-server/tests/fixtures/v4/css-loading-js/package.json b/packages/tailwindcss-language-server/tests/fixtures/v4/css-loading-js/package.json index b6cb53b1e..43b975c93 100644 --- a/packages/tailwindcss-language-server/tests/fixtures/v4/css-loading-js/package.json +++ b/packages/tailwindcss-language-server/tests/fixtures/v4/css-loading-js/package.json @@ -1,5 +1,5 @@ { "dependencies": { - "tailwindcss": "^4.0.15" + "tailwindcss": "4.1.1" } } diff --git a/packages/tailwindcss-language-server/tests/fixtures/v4/dependencies/package-lock.json b/packages/tailwindcss-language-server/tests/fixtures/v4/dependencies/package-lock.json index 555ee6602..f4352dc60 100644 --- a/packages/tailwindcss-language-server/tests/fixtures/v4/dependencies/package-lock.json +++ b/packages/tailwindcss-language-server/tests/fixtures/v4/dependencies/package-lock.json @@ -5,13 +5,13 @@ "packages": { "": { "dependencies": { - "tailwindcss": "^4.0.15" + "tailwindcss": "4.1.1" } }, "node_modules/tailwindcss": { - "version": "4.0.15", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.15.tgz", - "integrity": "sha512-6ZMg+hHdMJpjpeCCFasX7K+U615U9D+7k5/cDK/iRwl6GptF24+I/AbKgOnXhVKePzrEyIXutLv36n4cRsq3Sg==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.1.tgz", + "integrity": "sha512-QNbdmeS979Efzim2g/bEvfuh+fTcIdp1y7gA+sb6OYSW74rt7Cr7M78AKdf6HqWT3d5AiTb7SwTT3sLQxr4/qw==", "license": "MIT" } } diff --git a/packages/tailwindcss-language-server/tests/fixtures/v4/dependencies/package.json b/packages/tailwindcss-language-server/tests/fixtures/v4/dependencies/package.json index b6cb53b1e..43b975c93 100644 --- a/packages/tailwindcss-language-server/tests/fixtures/v4/dependencies/package.json +++ b/packages/tailwindcss-language-server/tests/fixtures/v4/dependencies/package.json @@ -1,5 +1,5 @@ { "dependencies": { - "tailwindcss": "^4.0.15" + "tailwindcss": "4.1.1" } } diff --git a/packages/tailwindcss-language-server/tests/fixtures/v4/invalid-import-order/package-lock.json b/packages/tailwindcss-language-server/tests/fixtures/v4/invalid-import-order/package-lock.json index 24d978d86..0a12b281b 100644 --- a/packages/tailwindcss-language-server/tests/fixtures/v4/invalid-import-order/package-lock.json +++ b/packages/tailwindcss-language-server/tests/fixtures/v4/invalid-import-order/package-lock.json @@ -5,13 +5,13 @@ "packages": { "": { "dependencies": { - "tailwindcss": "^4.0.15" + "tailwindcss": "4.1.1" } }, "node_modules/tailwindcss": { - "version": "4.0.15", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.15.tgz", - "integrity": "sha512-6ZMg+hHdMJpjpeCCFasX7K+U615U9D+7k5/cDK/iRwl6GptF24+I/AbKgOnXhVKePzrEyIXutLv36n4cRsq3Sg==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.1.tgz", + "integrity": "sha512-QNbdmeS979Efzim2g/bEvfuh+fTcIdp1y7gA+sb6OYSW74rt7Cr7M78AKdf6HqWT3d5AiTb7SwTT3sLQxr4/qw==", "license": "MIT" } } diff --git a/packages/tailwindcss-language-server/tests/fixtures/v4/invalid-import-order/package.json b/packages/tailwindcss-language-server/tests/fixtures/v4/invalid-import-order/package.json index b6cb53b1e..43b975c93 100644 --- a/packages/tailwindcss-language-server/tests/fixtures/v4/invalid-import-order/package.json +++ b/packages/tailwindcss-language-server/tests/fixtures/v4/invalid-import-order/package.json @@ -1,5 +1,5 @@ { "dependencies": { - "tailwindcss": "^4.0.15" + "tailwindcss": "4.1.1" } } diff --git a/packages/tailwindcss-language-server/tests/fixtures/v4/missing-files/package-lock.json b/packages/tailwindcss-language-server/tests/fixtures/v4/missing-files/package-lock.json index ed4d2d9a8..c95b25d7b 100644 --- a/packages/tailwindcss-language-server/tests/fixtures/v4/missing-files/package-lock.json +++ b/packages/tailwindcss-language-server/tests/fixtures/v4/missing-files/package-lock.json @@ -5,13 +5,13 @@ "packages": { "": { "dependencies": { - "tailwindcss": "^4.0.15" + "tailwindcss": "4.1.1" } }, "node_modules/tailwindcss": { - "version": "4.0.15", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.15.tgz", - "integrity": "sha512-6ZMg+hHdMJpjpeCCFasX7K+U615U9D+7k5/cDK/iRwl6GptF24+I/AbKgOnXhVKePzrEyIXutLv36n4cRsq3Sg==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.1.tgz", + "integrity": "sha512-QNbdmeS979Efzim2g/bEvfuh+fTcIdp1y7gA+sb6OYSW74rt7Cr7M78AKdf6HqWT3d5AiTb7SwTT3sLQxr4/qw==", "license": "MIT" } } diff --git a/packages/tailwindcss-language-server/tests/fixtures/v4/missing-files/package.json b/packages/tailwindcss-language-server/tests/fixtures/v4/missing-files/package.json index b6cb53b1e..43b975c93 100644 --- a/packages/tailwindcss-language-server/tests/fixtures/v4/missing-files/package.json +++ b/packages/tailwindcss-language-server/tests/fixtures/v4/missing-files/package.json @@ -1,5 +1,5 @@ { "dependencies": { - "tailwindcss": "^4.0.15" + "tailwindcss": "4.1.1" } } diff --git a/packages/tailwindcss-language-server/tests/fixtures/v4/multi-config/package-lock.json b/packages/tailwindcss-language-server/tests/fixtures/v4/multi-config/package-lock.json index c4664645d..58a06ceec 100644 --- a/packages/tailwindcss-language-server/tests/fixtures/v4/multi-config/package-lock.json +++ b/packages/tailwindcss-language-server/tests/fixtures/v4/multi-config/package-lock.json @@ -5,13 +5,13 @@ "packages": { "": { "dependencies": { - "tailwindcss": "^4.0.15" + "tailwindcss": "4.1.1" } }, "node_modules/tailwindcss": { - "version": "4.0.15", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.15.tgz", - "integrity": "sha512-6ZMg+hHdMJpjpeCCFasX7K+U615U9D+7k5/cDK/iRwl6GptF24+I/AbKgOnXhVKePzrEyIXutLv36n4cRsq3Sg==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.1.tgz", + "integrity": "sha512-QNbdmeS979Efzim2g/bEvfuh+fTcIdp1y7gA+sb6OYSW74rt7Cr7M78AKdf6HqWT3d5AiTb7SwTT3sLQxr4/qw==", "license": "MIT" } } diff --git a/packages/tailwindcss-language-server/tests/fixtures/v4/multi-config/package.json b/packages/tailwindcss-language-server/tests/fixtures/v4/multi-config/package.json index b6cb53b1e..43b975c93 100644 --- a/packages/tailwindcss-language-server/tests/fixtures/v4/multi-config/package.json +++ b/packages/tailwindcss-language-server/tests/fixtures/v4/multi-config/package.json @@ -1,5 +1,5 @@ { "dependencies": { - "tailwindcss": "^4.0.15" + "tailwindcss": "4.1.1" } } diff --git a/packages/tailwindcss-language-server/tests/fixtures/v4/path-mappings/package-lock.json b/packages/tailwindcss-language-server/tests/fixtures/v4/path-mappings/package-lock.json index 651bf7c97..e6cc18e9a 100644 --- a/packages/tailwindcss-language-server/tests/fixtures/v4/path-mappings/package-lock.json +++ b/packages/tailwindcss-language-server/tests/fixtures/v4/path-mappings/package-lock.json @@ -5,13 +5,13 @@ "packages": { "": { "dependencies": { - "tailwindcss": "^4.0.15" + "tailwindcss": "4.1.1" } }, "node_modules/tailwindcss": { - "version": "4.0.15", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.15.tgz", - "integrity": "sha512-6ZMg+hHdMJpjpeCCFasX7K+U615U9D+7k5/cDK/iRwl6GptF24+I/AbKgOnXhVKePzrEyIXutLv36n4cRsq3Sg==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.1.tgz", + "integrity": "sha512-QNbdmeS979Efzim2g/bEvfuh+fTcIdp1y7gA+sb6OYSW74rt7Cr7M78AKdf6HqWT3d5AiTb7SwTT3sLQxr4/qw==", "license": "MIT" } } diff --git a/packages/tailwindcss-language-server/tests/fixtures/v4/path-mappings/package.json b/packages/tailwindcss-language-server/tests/fixtures/v4/path-mappings/package.json index b6cb53b1e..43b975c93 100644 --- a/packages/tailwindcss-language-server/tests/fixtures/v4/path-mappings/package.json +++ b/packages/tailwindcss-language-server/tests/fixtures/v4/path-mappings/package.json @@ -1,5 +1,5 @@ { "dependencies": { - "tailwindcss": "^4.0.15" + "tailwindcss": "4.1.1" } } diff --git a/packages/tailwindcss-language-server/tests/fixtures/v4/with-prefix/package-lock.json b/packages/tailwindcss-language-server/tests/fixtures/v4/with-prefix/package-lock.json index 1e5486aeb..34a2cc131 100644 --- a/packages/tailwindcss-language-server/tests/fixtures/v4/with-prefix/package-lock.json +++ b/packages/tailwindcss-language-server/tests/fixtures/v4/with-prefix/package-lock.json @@ -5,13 +5,13 @@ "packages": { "": { "dependencies": { - "tailwindcss": "^4.0.15" + "tailwindcss": "4.1.1" } }, "node_modules/tailwindcss": { - "version": "4.0.15", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.15.tgz", - "integrity": "sha512-6ZMg+hHdMJpjpeCCFasX7K+U615U9D+7k5/cDK/iRwl6GptF24+I/AbKgOnXhVKePzrEyIXutLv36n4cRsq3Sg==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.1.tgz", + "integrity": "sha512-QNbdmeS979Efzim2g/bEvfuh+fTcIdp1y7gA+sb6OYSW74rt7Cr7M78AKdf6HqWT3d5AiTb7SwTT3sLQxr4/qw==", "license": "MIT" } } diff --git a/packages/tailwindcss-language-server/tests/fixtures/v4/with-prefix/package.json b/packages/tailwindcss-language-server/tests/fixtures/v4/with-prefix/package.json index b6cb53b1e..43b975c93 100644 --- a/packages/tailwindcss-language-server/tests/fixtures/v4/with-prefix/package.json +++ b/packages/tailwindcss-language-server/tests/fixtures/v4/with-prefix/package.json @@ -1,5 +1,5 @@ { "dependencies": { - "tailwindcss": "^4.0.15" + "tailwindcss": "4.1.1" } } diff --git a/packages/tailwindcss-language-server/tests/fixtures/v4/workspaces/package-lock.json b/packages/tailwindcss-language-server/tests/fixtures/v4/workspaces/package-lock.json index b622b7b76..a88643baf 100644 --- a/packages/tailwindcss-language-server/tests/fixtures/v4/workspaces/package-lock.json +++ b/packages/tailwindcss-language-server/tests/fixtures/v4/workspaces/package-lock.json @@ -8,7 +8,7 @@ "packages/*" ], "dependencies": { - "tailwindcss": "^4.0.15" + "tailwindcss": "4.1.1" } }, "node_modules/@private/admin": { @@ -32,9 +32,9 @@ "link": true }, "node_modules/tailwindcss": { - "version": "4.0.15", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.15.tgz", - "integrity": "sha512-6ZMg+hHdMJpjpeCCFasX7K+U615U9D+7k5/cDK/iRwl6GptF24+I/AbKgOnXhVKePzrEyIXutLv36n4cRsq3Sg==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.1.tgz", + "integrity": "sha512-QNbdmeS979Efzim2g/bEvfuh+fTcIdp1y7gA+sb6OYSW74rt7Cr7M78AKdf6HqWT3d5AiTb7SwTT3sLQxr4/qw==", "license": "MIT" }, "packages/admin": { diff --git a/packages/tailwindcss-language-server/tests/fixtures/v4/workspaces/package.json b/packages/tailwindcss-language-server/tests/fixtures/v4/workspaces/package.json index aa5e54db9..9291956a3 100644 --- a/packages/tailwindcss-language-server/tests/fixtures/v4/workspaces/package.json +++ b/packages/tailwindcss-language-server/tests/fixtures/v4/workspaces/package.json @@ -3,6 +3,6 @@ "packages/*" ], "dependencies": { - "tailwindcss": "^4.0.15" + "tailwindcss": "4.1.1" } } diff --git a/packages/tailwindcss-language-server/tests/hover/hover.test.js b/packages/tailwindcss-language-server/tests/hover/hover.test.js index ac97e414d..379f41996 100644 --- a/packages/tailwindcss-language-server/tests/hover/hover.test.js +++ b/packages/tailwindcss-language-server/tests/hover/hover.test.js @@ -1,5 +1,7 @@ -import { test } from 'vitest' +import { expect, test } from 'vitest' import { withFixture } from '../common' +import { css, defineTest } from '../../src/testing' +import { createClient } from '../utils/client' withFixture('basic', (c) => { async function testHover( @@ -214,7 +216,7 @@ withFixture('v4/basic', (c) => { text: '
', position: { line: 0, character: 13 }, expected: - '.bg-red-500 {\n background-color: var(--color-red-500) /* oklch(0.637 0.237 25.331) = #fb2c36 */;\n}', + '.bg-red-500 {\n background-color: var(--color-red-500) /* oklch(63.7% 0.237 25.331) = #fb2c36 */;\n}', expectedRange: { start: { line: 0, character: 12 }, end: { line: 0, character: 22 }, @@ -231,16 +233,15 @@ withFixture('v4/basic', (c) => { }, }) - test.todo('arbitrary value with theme function') - // testHover('arbitrary value with theme function', { - // text: '
', - // position: { line: 0, character: 13 }, - // expected: '.p-\\[theme\\(spacing\\.4\\)\\] {\n' + ' padding: 1rem /* 16px */;\n' + '}', - // expectedRange: { - // start: { line: 0, character: 12 }, - // end: { line: 0, character: 32 }, - // }, - // }) + testHover('arbitrary value with theme function', { + text: '
', + position: { line: 0, character: 13 }, + expected: '.p-\\[theme\\(spacing\\.4\\)\\] {\n' + ' padding: 1rem /* 16px */;\n' + '}', + expectedRange: { + start: { line: 0, character: 12 }, + end: { line: 0, character: 32 }, + }, + }) testHover('arbitrary property', { text: '
', @@ -397,7 +398,14 @@ withFixture('v4/basic', (c) => { expected: { contents: { kind: 'markdown', - value: ['```plaintext', '80rem /* 1280px */', '```'].join('\n'), + value: [ + // + '```css', + '@theme {', + ' --breakpoint-xl: 80rem /* 1280px */;', + '}', + '```', + ].join('\n'), }, range: { start: { line: 0, character: 23 }, @@ -545,3 +553,72 @@ withFixture('v4/path-mappings', (c) => { }, }) }) + +defineTest({ + name: 'Can hover showing theme values used in var(…) and theme(…) functions', + fs: { + 'app.css': css` + @import 'tailwindcss'; + `, + }, + + prepare: async ({ root }) => ({ client: await createClient({ root }) }), + + handle: async ({ client }) => { + let doc = await client.open({ + lang: 'css', + text: css` + .foo { + color: theme(--color-black); + } + .bar { + color: var(--color-black); + } + `, + }) + + // color: theme(--color-black); + // ^ + let hoverTheme = await doc.hover({ line: 1, character: 18 }) + + // color: var(--color-black); + // ^ + let hoverVar = await doc.hover({ line: 4, character: 16 }) + + expect(hoverTheme).toEqual({ + contents: { + kind: 'markdown', + value: [ + // + '```css', + '@theme {', + ' --color-black: #000;', + '}', + '```', + ].join('\n'), + }, + range: { + start: { line: 1, character: 15 }, + end: { line: 1, character: 28 }, + }, + }) + + expect(hoverVar).toEqual({ + contents: { + kind: 'markdown', + value: [ + // + '```css', + '@theme {', + ' --color-black: #000;', + '}', + '```', + ].join('\n'), + }, + range: { + start: { line: 4, character: 13 }, + end: { line: 4, character: 26 }, + }, + }) + }, +}) diff --git a/packages/tailwindcss-language-server/tests/prepare.mjs b/packages/tailwindcss-language-server/tests/prepare.mjs index 19b2c6d5a..c529e6300 100644 --- a/packages/tailwindcss-language-server/tests/prepare.mjs +++ b/packages/tailwindcss-language-server/tests/prepare.mjs @@ -2,19 +2,23 @@ import { exec } from 'node:child_process' import * as path from 'node:path' import { fileURLToPath } from 'node:url' import { promisify } from 'node:util' -import glob from 'fast-glob' +import { glob } from 'tinyglobby' const __dirname = path.dirname(fileURLToPath(import.meta.url)) -const fixtures = glob.sync(['tests/fixtures/*/package.json', 'tests/fixtures/v4/*/package.json'], { - cwd: path.resolve(__dirname, '..'), +const root = path.resolve(__dirname, '..') + +const fixtures = await glob({ + cwd: root, + patterns: ['tests/fixtures/*/package.json', 'tests/fixtures/v4/*/package.json'], + absolute: true, }) const execAsync = promisify(exec) await Promise.all( fixtures.map(async (fixture) => { - console.log(`Installing dependencies for ${fixture}`) + console.log(`Installing dependencies for ${path.relative(root, fixture)}`) await execAsync('npm install', { cwd: path.dirname(fixture) }) }), diff --git a/packages/tailwindcss-language-service/package.json b/packages/tailwindcss-language-service/package.json index 48fae16ee..23907838e 100644 --- a/packages/tailwindcss-language-service/package.json +++ b/packages/tailwindcss-language-service/package.json @@ -1,6 +1,6 @@ { "name": "@tailwindcss/language-service", - "version": "0.14.12", + "version": "0.14.13", "main": "dist/index.js", "typings": "dist/index.d.ts", "files": [ diff --git a/packages/tailwindcss-language-service/src/completionProvider.ts b/packages/tailwindcss-language-service/src/completionProvider.ts index 5c0c55528..843e9a8e8 100644 --- a/packages/tailwindcss-language-service/src/completionProvider.ts +++ b/packages/tailwindcss-language-service/src/completionProvider.ts @@ -261,29 +261,25 @@ export function completionsFromClassList( // TODO: This is a bit of a hack if (prefix.length > 0) { - // No variants seen: suggest the prefix only + // No variants seen: + // - suggest the prefix as a variant + // - Modify the remaining items to include the prefix in the variant name if (existingVariants.length === 0) { - items = items.slice(0, 1) + items = items.map((item, idx) => { + if (idx === 0) return item - return withDefaults( - { - isIncomplete: false, - items, - }, - { - data: { - ...(state.completionItemData ?? {}), - ...(important ? { important } : {}), - variants: existingVariants, - }, - range: replacementRange, - }, - state.editor.capabilities.itemDefaults, - ) + item.label = `${prefix}:${item.label}` + + if (item.textEditText) { + item.textEditText = `${prefix}:${item.textEditText}` + } + + return item + }) } // The first variant is not the prefix: don't suggest anything - if (existingVariants[0] !== prefix) { + if (existingVariants.length > 0 && existingVariants[0] !== prefix) { return null } } @@ -304,6 +300,10 @@ export function completionsFromClassList( documentation = formatColor(color) } + if (prefix.length > 0 && existingVariants.length === 0) { + className = `${prefix}:${className}` + } + items.push({ label: className, kind, diff --git a/packages/tailwindcss-language-service/src/diagnostics/getInvalidConfigPathDiagnostics.ts b/packages/tailwindcss-language-service/src/diagnostics/getInvalidConfigPathDiagnostics.ts index d20655e33..76864281c 100644 --- a/packages/tailwindcss-language-service/src/diagnostics/getInvalidConfigPathDiagnostics.ts +++ b/packages/tailwindcss-language-service/src/diagnostics/getInvalidConfigPathDiagnostics.ts @@ -186,6 +186,11 @@ export function getInvalidConfigPathDiagnostics( findHelperFunctionsInDocument(state, document).forEach((helperFn) => { let base = helperFn.helper === 'theme' ? ['theme'] : [] + + // var(…) may not refer to theme values but other values in the cascade + // so they can't be unconditionally validated + if (helperFn.helper === 'var') return + let result = validateConfigPath(state, helperFn.path, base) if (result.isValid === true) { diff --git a/packages/tailwindcss-language-service/src/hoverProvider.ts b/packages/tailwindcss-language-service/src/hoverProvider.ts index 1db68bed8..583cc80f9 100644 --- a/packages/tailwindcss-language-service/src/hoverProvider.ts +++ b/packages/tailwindcss-language-service/src/hoverProvider.ts @@ -53,6 +53,8 @@ async function provideCssHelperHover( for (let helperFn of helperFns) { if (!isWithinRange(position, helperFn.ranges.path)) continue + if (helperFn.helper === 'var' && !state.v4) continue + let validated = validateConfigPath( state, helperFn.path, @@ -67,8 +69,21 @@ async function provideCssHelperHover( value = addPixelEquivalentsToValue(value, settings.tailwindCSS.rootFontSize) } + let lines = ['```plaintext', value, '```'] + + if (state.v4 && helperFn.path.startsWith('--')) { + lines = [ + // + '```css', + '@theme {', + ` ${helperFn.path}: ${value};`, + '}', + '```', + ] + } + return { - contents: { kind: 'markdown', value: ['```plaintext', value, '```'].join('\n') }, + contents: { kind: 'markdown', value: lines.join('\n') }, range: helperFn.ranges.path, } } diff --git a/packages/tailwindcss-language-service/src/util/color.ts b/packages/tailwindcss-language-service/src/util/color.ts index 4b0d3b84d..a1a99d661 100644 --- a/packages/tailwindcss-language-service/src/util/color.ts +++ b/packages/tailwindcss-language-service/src/util/color.ts @@ -57,7 +57,7 @@ const colorRegex = new RegExp( ) function getColorsInString(state: State, str: string): (culori.Color | KeywordColor)[] { - if (/(?:box|drop)-shadow/.test(str)) return [] + if (/(?:box|drop)-shadow/.test(str) && !/--tw-drop-shadow/.test(str)) return [] function toColor(match: RegExpMatchArray) { let color = match[1].replace(/var\([^)]+\)/, '1') @@ -85,6 +85,17 @@ function getColorFromDecls( ) { return false } + + // ignore mask-image & mask-composite + if (prop === 'mask-image' || prop === 'mask-composite') { + return false + } + + // ignore `--tw-drop-shadow` + if (prop === '--tw-drop-shadow') { + return false + } + return true }) @@ -177,8 +188,25 @@ function getColorFromRoot(state: State, css: postcss.Root): culori.Color | Keywo return getColorFromDecls(state, decls) } +let isNegative = /^-/ +let isNumericUtility = + /^-?((min-|max-)?[wh]|z|start|order|opacity|rounded|row|col|size|basis|end|duration|ease|font|top|left|bottom|right|inset|leading|cursor|(space|scale|skew|rotate)-[xyz]|gap(-[xy])?|(scroll-)?[pm][trblxyse]?)-/ +let isMaskUtility = /^-?mask-/ + +function isLikelyColorless(className: string) { + if (isNegative.test(className)) return true + // TODO: This is **not** correct but is intentional because there are 5k mask utilities and a LOT of them are colors + // This causes a massive slowdown when building the design system + if (isMaskUtility.test(className)) return true + if (isNumericUtility.test(className)) return true + return false +} + export function getColor(state: State, className: string): culori.Color | KeywordColor | null { if (state.v4) { + // FIXME: This is a performance optimization and not strictly correct + if (isLikelyColorless(className)) return null + let css = state.designSystem.compile([className])[0] let color = getColorFromRoot(state, css) diff --git a/packages/tailwindcss-language-service/src/util/find.ts b/packages/tailwindcss-language-service/src/util/find.ts index 032187988..9118403da 100644 --- a/packages/tailwindcss-language-service/src/util/find.ts +++ b/packages/tailwindcss-language-service/src/util/find.ts @@ -405,7 +405,7 @@ export function findHelperFunctionsInRange( ): DocumentHelperFunction[] { const text = getTextWithoutComments(doc, 'css', range) let matches = findAll( - /(?[\W])(?config|theme|--theme)(?\(\s*)(?[^)]*?)\s*\)/g, + /(?[\W])(?config|theme|--theme|var)(?\(\s*)(?[^)]*?)\s*\)/g, text, ) @@ -450,10 +450,12 @@ export function findHelperFunctionsInRange( match.groups.helper.length + match.groups.innerPrefix.length - let helper: 'config' | 'theme' = 'config' + let helper: 'config' | 'theme' | 'var' = 'config' if (match.groups.helper === 'theme' || match.groups.helper === '--theme') { helper = 'theme' + } else if (match.groups.helper === 'var') { + helper = 'var' } return { diff --git a/packages/tailwindcss-language-service/src/util/getVariantsFromClassName.ts b/packages/tailwindcss-language-service/src/util/getVariantsFromClassName.ts index c30e729af..823b20f81 100644 --- a/packages/tailwindcss-language-service/src/util/getVariantsFromClassName.ts +++ b/packages/tailwindcss-language-service/src/util/getVariantsFromClassName.ts @@ -33,6 +33,12 @@ export function getVariantsFromClassName( // NOTE: This should never happen if (!state.designSystem) return false + let prefix = state.designSystem.theme.prefix ?? '' + + if (prefix !== '') { + className = `${prefix}:${className}` + } + // We don't use `compile()` so there's no overhead from PostCSS let compiled = state.designSystem.candidatesToCss([className]) diff --git a/packages/tailwindcss-language-service/src/util/rewriting/var-fallbacks.ts b/packages/tailwindcss-language-service/src/util/rewriting/var-fallbacks.ts index 728b53bf9..91dcc91db 100644 --- a/packages/tailwindcss-language-service/src/util/rewriting/var-fallbacks.ts +++ b/packages/tailwindcss-language-service/src/util/rewriting/var-fallbacks.ts @@ -17,6 +17,14 @@ export function replaceCssVarsWithFallbacks(state: State, str: string): string { return fallback } + if ( + name === '--tw-text-shadow-alpha' || + name === '--tw-drop-shadow-alpha' || + name === '--tw-shadow-alpha' + ) { + return '100%' + } + // Don't touch it since there's no suitable replacement return null }, diff --git a/packages/tailwindcss-language-service/src/util/state.ts b/packages/tailwindcss-language-service/src/util/state.ts index 119e5f59b..8c819eb8f 100644 --- a/packages/tailwindcss-language-service/src/util/state.ts +++ b/packages/tailwindcss-language-service/src/util/state.ts @@ -161,7 +161,7 @@ export type DocumentClassName = { } export type DocumentHelperFunction = { - helper: 'theme' | 'config' + helper: 'theme' | 'config' | 'var' path: string ranges: { full: Range diff --git a/packages/vscode-tailwindcss/CHANGELOG.md b/packages/vscode-tailwindcss/CHANGELOG.md index 31a8ca66a..1750eb693 100644 --- a/packages/vscode-tailwindcss/CHANGELOG.md +++ b/packages/vscode-tailwindcss/CHANGELOG.md @@ -2,7 +2,16 @@ ## Prerelease -- Nothing yet! +- Only scan the file system once when needed ([#1287](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1287)) +- Don't follow recursive symlinks when searching for projects ([#1270](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1270)) + +# 0.14.13 + +- Hide completions from CSS language server inside `@import "…" source(…)` ([#1091](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1091)) +- Bump bundled v4 fallback to v4.1.1 ([#1294](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1294)) +- Show color swatches for most new v4.1 utilities ([#1294](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1294)) +- Support theme key hovers in the CSS `var()` function ([#1289](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1289)) +- Show theme key hovers inside `@theme` for better context and syntax highlighting ([#1289](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1289)) # 0.14.12 diff --git a/packages/vscode-tailwindcss/package.json b/packages/vscode-tailwindcss/package.json index 26af65d38..4f55fb4a6 100644 --- a/packages/vscode-tailwindcss/package.json +++ b/packages/vscode-tailwindcss/package.json @@ -1,6 +1,6 @@ { "name": "vscode-tailwindcss", - "version": "0.14.12", + "version": "0.14.13", "displayName": "Tailwind CSS IntelliSense", "description": "Intelligent Tailwind CSS tooling for VS Code", "author": "Brad Cornes ", diff --git a/packages/vscode-tailwindcss/src/analyze.ts b/packages/vscode-tailwindcss/src/analyze.ts new file mode 100644 index 000000000..ec9b2b9a3 --- /dev/null +++ b/packages/vscode-tailwindcss/src/analyze.ts @@ -0,0 +1,93 @@ +import { workspace, RelativePattern, CancellationToken, Uri, WorkspaceFolder } from 'vscode' +import braces from 'braces' +import { CONFIG_GLOB, CSS_GLOB } from '@tailwindcss/language-server/src/lib/constants' +import { getExcludePatterns } from './exclusions' + +export interface SearchOptions { + folders: readonly WorkspaceFolder[] + token: CancellationToken +} + +export async function anyWorkspaceFoldersNeedServer({ folders, token }: SearchOptions) { + // An explicit config file setting means we need the server + for (let folder of folders) { + let settings = workspace.getConfiguration('tailwindCSS', folder) + let configFilePath = settings.get('experimental.configFile') + + // No setting provided + if (!configFilePath) continue + + // Ths config file may be a string: + // A path pointing to a CSS or JS config file + if (typeof configFilePath === 'string') return true + + // Ths config file may be an object: + // A map of config files to one or more globs + // + // If we get an empty object the language server will do a search anyway so + // we'll act as if no option was passed to be consistent + if (typeof configFilePath === 'object' && Object.values(configFilePath).length > 0) return true + } + + let configs: Array<() => Thenable> = [] + let stylesheets: Array<() => Thenable> = [] + + for (let folder of folders) { + let exclusions = getExcludePatterns(folder).flatMap((pattern) => braces.expand(pattern)) + let exclude = `{${exclusions.join(',').replace(/{/g, '%7B').replace(/}/g, '%7D')}}` + + configs.push(() => + workspace.findFiles( + new RelativePattern(folder, `**/${CONFIG_GLOB}`), + exclude, + undefined, + token, + ), + ) + + stylesheets.push(() => + workspace.findFiles(new RelativePattern(folder, `**/${CSS_GLOB}`), exclude, undefined, token), + ) + } + + // If we find a config file then we need the server + let configUrls = await Promise.all(configs.map((fn) => fn())) + for (let group of configUrls) { + if (group.length > 0) { + return true + } + } + + // If we find a possibly-related stylesheet then we need the server + // The step is done last because it requires reading individual files + // to determine if the server should be started. + // + // This is also, unfortunately, prone to starting the server unncessarily + // in projects that don't use TailwindCSS so we do this one-by-one instead + // of all at once to keep disk I/O low. + let stylesheetUrls = await Promise.all(stylesheets.map((fn) => fn())) + for (let group of stylesheetUrls) { + for (let file of group) { + if (await fileMayBeTailwindRelated(file)) { + return true + } + } + } +} + +let HAS_CONFIG = /@config\s*['"]/ +let HAS_IMPORT = /@import\s*['"]/ +let HAS_TAILWIND = /@tailwind\s*[^;]+;/ +let HAS_THEME = /@theme\s*\{/ + +export async function fileMayBeTailwindRelated(uri: Uri) { + let buffer = await workspace.fs.readFile(uri) + let contents = buffer.toString() + + return ( + HAS_CONFIG.test(contents) || + HAS_IMPORT.test(contents) || + HAS_TAILWIND.test(contents) || + HAS_THEME.test(contents) + ) +} diff --git a/packages/vscode-tailwindcss/src/api.ts b/packages/vscode-tailwindcss/src/api.ts new file mode 100644 index 000000000..d7f67de64 --- /dev/null +++ b/packages/vscode-tailwindcss/src/api.ts @@ -0,0 +1,49 @@ +import { workspace, CancellationTokenSource, OutputChannel, ExtensionContext, Uri } from 'vscode' +import { anyWorkspaceFoldersNeedServer, fileMayBeTailwindRelated } from './analyze' + +interface ApiOptions { + context: ExtensionContext + outputChannel: OutputChannel +} + +export async function createApi({ context, outputChannel }: ApiOptions) { + let folderAnalysis: Promise | null = null + + async function workspaceNeedsLanguageServer() { + if (folderAnalysis) return folderAnalysis + + let source: CancellationTokenSource | null = new CancellationTokenSource() + source.token.onCancellationRequested(() => { + source?.dispose() + source = null + + outputChannel.appendLine( + 'Server was not started. Search for Tailwind CSS-related files was taking too long.', + ) + }) + + // Cancel the search after roughly 15 seconds + setTimeout(() => source?.cancel(), 15_000) + context.subscriptions.push(source) + + folderAnalysis ??= anyWorkspaceFoldersNeedServer({ + token: source.token, + folders: workspace.workspaceFolders ?? [], + }) + + let result = await folderAnalysis + source?.dispose() + return result + } + + async function stylesheetNeedsLanguageServer(uri: Uri) { + outputChannel.appendLine(`Checking if ${uri.fsPath} may be Tailwind-related…`) + + return fileMayBeTailwindRelated(uri) + } + + return { + workspaceNeedsLanguageServer, + stylesheetNeedsLanguageServer, + } +} diff --git a/packages/vscode-tailwindcss/src/exclusions.ts b/packages/vscode-tailwindcss/src/exclusions.ts new file mode 100644 index 000000000..46ffd599a --- /dev/null +++ b/packages/vscode-tailwindcss/src/exclusions.ts @@ -0,0 +1,49 @@ +import { + workspace, + type WorkspaceConfiguration, + type ConfigurationScope, + type WorkspaceFolder, +} from 'vscode' +import picomatch from 'picomatch' +import * as path from 'node:path' + +function getGlobalExcludePatterns(scope: ConfigurationScope | null): string[] { + return Object.entries(workspace.getConfiguration('files', scope)?.get('exclude') ?? []) + .filter(([, value]) => value === true) + .map(([key]) => key) + .filter(Boolean) +} + +export function getExcludePatterns(scope: ConfigurationScope | null): string[] { + return [ + ...getGlobalExcludePatterns(scope), + ...(workspace.getConfiguration('tailwindCSS', scope).get('files.exclude')).filter( + Boolean, + ), + ] +} + +export function isExcluded(file: string, folder: WorkspaceFolder): boolean { + for (let pattern of getExcludePatterns(folder)) { + let matcher = picomatch(path.join(folder.uri.fsPath, pattern)) + + if (matcher(file)) { + return true + } + } + + return false +} + +export function mergeExcludes( + settings: WorkspaceConfiguration, + scope: ConfigurationScope | null, +): any { + return { + ...settings, + files: { + ...settings.files, + exclude: getExcludePatterns(scope), + }, + } +} diff --git a/packages/vscode-tailwindcss/src/extension.ts b/packages/vscode-tailwindcss/src/extension.ts index a37486160..467d4e085 100755 --- a/packages/vscode-tailwindcss/src/extension.ts +++ b/packages/vscode-tailwindcss/src/extension.ts @@ -4,9 +4,7 @@ import type { TextDocument, WorkspaceFolder, ConfigurationScope, - WorkspaceConfiguration, Selection, - CancellationToken, } from 'vscode' import { workspace as Workspace, @@ -16,8 +14,6 @@ import { SymbolInformation, Position, Range, - RelativePattern, - CancellationTokenSource, } from 'vscode' import type { DocumentFilter, @@ -34,11 +30,11 @@ import { languages as defaultLanguages } from '@tailwindcss/language-service/src import * as semver from '@tailwindcss/language-service/src/util/semver' import isObject from '@tailwindcss/language-service/src/util/isObject' import namedColors from 'color-name' -import picomatch from 'picomatch' import { CONFIG_GLOB, CSS_GLOB } from '@tailwindcss/language-server/src/lib/constants' -import braces from 'braces' import normalizePath from 'normalize-path' import * as servers from './servers/index' +import { isExcluded, mergeExcludes } from './exclusions' +import { createApi } from './api' const colorNames = Object.keys(namedColors) @@ -52,60 +48,6 @@ function getUserLanguages(folder?: WorkspaceFolder): Record { return isObject(langs) ? langs : {} } -function getGlobalExcludePatterns(scope: ConfigurationScope | null): string[] { - return Object.entries(Workspace.getConfiguration('files', scope)?.get('exclude') ?? []) - .filter(([, value]) => value === true) - .map(([key]) => key) - .filter(Boolean) -} - -function getExcludePatterns(scope: ConfigurationScope | null): string[] { - return [ - ...getGlobalExcludePatterns(scope), - ...(Workspace.getConfiguration('tailwindCSS', scope).get('files.exclude')).filter( - Boolean, - ), - ] -} - -function isExcluded(file: string, folder: WorkspaceFolder): boolean { - for (let pattern of getExcludePatterns(folder)) { - let matcher = picomatch(path.join(folder.uri.fsPath, pattern)) - - if (matcher(file)) { - return true - } - } - - return false -} - -function mergeExcludes(settings: WorkspaceConfiguration, scope: ConfigurationScope | null): any { - return { - ...settings, - files: { - ...settings.files, - exclude: getExcludePatterns(scope), - }, - } -} - -async function fileMayBeTailwindRelated(uri: Uri) { - let contents = (await Workspace.fs.readFile(uri)).toString() - - let HAS_CONFIG = /@config\s*['"]/ - let HAS_IMPORT = /@import\s*['"]/ - let HAS_TAILWIND = /@tailwind\s*[^;]+;/ - let HAS_THEME = /@theme\s*\{/ - - return ( - HAS_CONFIG.test(contents) || - HAS_IMPORT.test(contents) || - HAS_TAILWIND.test(contents) || - HAS_THEME.test(contents) - ) -} - function selectionsAreEqual( aSelections: readonly Selection[], bSelections: readonly Selection[], @@ -177,6 +119,12 @@ function resetActiveTextEditorContext(): void { export async function activate(context: ExtensionContext) { let outputChannel = Window.createOutputChannel(CLIENT_NAME) + + let api = await createApi({ + context, + outputChannel, + }) + context.subscriptions.push(outputChannel) context.subscriptions.push( commands.registerCommand('tailwindCSS.showOutput', () => { @@ -266,10 +214,10 @@ export async function activate(context: ExtensionContext) { let configWatcher = Workspace.createFileSystemWatcher(`**/${CONFIG_GLOB}`, false, true, true) configWatcher.onDidCreate(async (uri) => { + if (currentClient) return let folder = Workspace.getWorkspaceFolder(uri) - if (!folder || isExcluded(uri.fsPath, folder)) { - return - } + if (!folder || isExcluded(uri.fsPath, folder)) return + await bootWorkspaceClient() }) @@ -278,13 +226,12 @@ export async function activate(context: ExtensionContext) { let cssWatcher = Workspace.createFileSystemWatcher(`**/${CSS_GLOB}`, false, false, true) async function bootClientIfCssFileMayBeTailwindRelated(uri: Uri) { + if (currentClient) return let folder = Workspace.getWorkspaceFolder(uri) - if (!folder || isExcluded(uri.fsPath, folder)) { - return - } - if (await fileMayBeTailwindRelated(uri)) { - await bootWorkspaceClient() - } + if (!folder || isExcluded(uri.fsPath, folder)) return + if (!(await api.stylesheetNeedsLanguageServer(uri))) return + + await bootWorkspaceClient() } cssWatcher.onDidCreate(bootClientIfCssFileMayBeTailwindRelated) @@ -578,111 +525,34 @@ export async function activate(context: ExtensionContext) { return client } - async function bootClientIfNeeded(): Promise { - if (currentClient) { - return - } - - let source: CancellationTokenSource | null = new CancellationTokenSource() - source.token.onCancellationRequested(() => { - source?.dispose() - source = null - outputChannel.appendLine( - 'Server was not started. Search for Tailwind CSS-related files was taking too long.', - ) - }) - - // Cancel the search after roughly 15 seconds - setTimeout(() => source?.cancel(), 15_000) - - if (!(await anyFolderNeedsLanguageServer(Workspace.workspaceFolders ?? [], source!.token))) { - source?.dispose() - return - } - - source?.dispose() - - await bootWorkspaceClient() - } - - async function anyFolderNeedsLanguageServer( - folders: readonly WorkspaceFolder[], - token: CancellationToken, - ): Promise { - for (let folder of folders) { - if (await folderNeedsLanguageServer(folder, token)) { - return true - } - } - - return false - } - - async function folderNeedsLanguageServer( - folder: WorkspaceFolder, - token: CancellationToken, - ): Promise { - let settings = Workspace.getConfiguration('tailwindCSS', folder) - if (settings.get('experimental.configFile') !== null) { - return true - } - - let exclude = `{${getExcludePatterns(folder) - .flatMap((pattern) => braces.expand(pattern)) - .join(',') - .replace(/{/g, '%7B') - .replace(/}/g, '%7D')}}` - - let configFiles = await Workspace.findFiles( - new RelativePattern(folder, `**/${CONFIG_GLOB}`), - exclude, - 1, - token, - ) - - for (let file of configFiles) { - return true - } - - let cssFiles = await Workspace.findFiles( - new RelativePattern(folder, `**/${CSS_GLOB}`), - exclude, - undefined, - token, - ) - - for (let file of cssFiles) { - outputChannel.appendLine(`Checking if ${file.fsPath} may be Tailwind-related…`) - - if (await fileMayBeTailwindRelated(file)) { - return true - } - } - - return false - } - + /** + * Note that this method can fire *many* times even for documents that are + * not in a visible editor. It's critical that this doesn't start any + * expensive operations more than is necessary. + */ async function didOpenTextDocument(document: TextDocument): Promise { if (document.languageId === 'tailwindcss') { servers.css.boot(context, outputChannel) } + if (currentClient) return + // We are only interested in language mode text - if (document.uri.scheme !== 'file') { - return - } + if (document.uri.scheme !== 'file') return - let uri = document.uri - let folder = Workspace.getWorkspaceFolder(uri) + let folder = Workspace.getWorkspaceFolder(document.uri) // Files outside a folder can't be handled. This might depend on the language. // Single file languages like JSON might handle files outside the workspace folders. - if (!folder) return + if (!folder || isExcluded(document.uri.fsPath, folder)) return + + if (!(await api.workspaceNeedsLanguageServer())) return - await bootClientIfNeeded() + await bootWorkspaceClient() } context.subscriptions.push(Workspace.onDidOpenTextDocument(didOpenTextDocument)) + Workspace.textDocuments.forEach(didOpenTextDocument) context.subscriptions.push( Workspace.onDidChangeWorkspaceFolders(async () => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 071263e70..c4e0edafc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -51,8 +51,8 @@ importers: specifier: 0.4.2 version: 0.4.2(tailwindcss@3.4.17) '@tailwindcss/oxide': - specifier: ^4.0.15 - version: 4.0.15 + specifier: ^4.1.0 + version: 4.1.0 '@tailwindcss/typography': specifier: 0.5.7 version: 0.5.7(tailwindcss@3.4.17) @@ -131,9 +131,6 @@ importers: esbuild: specifier: ^0.25.0 version: 0.25.0 - fast-glob: - specifier: 3.2.4 - version: 3.2.4 find-up: specifier: 5.0.0 version: 5.0.0 @@ -183,8 +180,11 @@ importers: specifier: 3.4.17 version: 3.4.17 tailwindcss-v4: - specifier: npm:tailwindcss@4.0.6 - version: tailwindcss@4.0.6 + specifier: npm:tailwindcss@4.1.1 + version: tailwindcss@4.1.1 + tinyglobby: + specifier: ^0.2.12 + version: 0.2.12 tsconfck: specifier: ^3.1.4 version: 3.1.4(typescript@5.3.3) @@ -890,74 +890,74 @@ packages: peerDependencies: tailwindcss: '>=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1' - '@tailwindcss/oxide-android-arm64@4.0.15': - resolution: {integrity: sha512-EBuyfSKkom7N+CB3A+7c0m4+qzKuiN0WCvzPvj5ZoRu4NlQadg/mthc1tl5k9b5ffRGsbDvP4k21azU4VwVk3Q==} + '@tailwindcss/oxide-android-arm64@4.1.0': + resolution: {integrity: sha512-UredFljuHey2Kh5qyYfQVBr0Xfq70ZE5Df6i5IubNYQGs2JXXT4VL0SIUjwzHx5W9T6t7dT7banunlV6lthGPQ==} engines: {node: '>= 10'} cpu: [arm64] os: [android] - '@tailwindcss/oxide-darwin-arm64@4.0.15': - resolution: {integrity: sha512-ObVAnEpLepMhV9VoO0JSit66jiN5C4YCqW3TflsE9boo2Z7FIjV80RFbgeL2opBhtxbaNEDa6D0/hq/EP03kgQ==} + '@tailwindcss/oxide-darwin-arm64@4.1.0': + resolution: {integrity: sha512-QHQ/46lRVwH9zEBNiRk8AJ3Af4pMq6DuZAI//q323qrPOXjsRdrhLsH9LUO3mqBfHr5EZNUxN3Am5vpO89sntw==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@tailwindcss/oxide-darwin-x64@4.0.15': - resolution: {integrity: sha512-IElwoFhUinOr9MyKmGTPNi1Rwdh68JReFgYWibPWTGuevkHkLWKEflZc2jtI5lWZ5U9JjUnUfnY43I4fEXrc4g==} + '@tailwindcss/oxide-darwin-x64@4.1.0': + resolution: {integrity: sha512-lEMgYHCvQQ6x2KOZ4FwnPprwfnc+UnjzwXRqEYIhB/NlYvXQD1QMf7oKEDRqy94DiZaYox9ZRfG2YJOBgM0UkA==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@tailwindcss/oxide-freebsd-x64@4.0.15': - resolution: {integrity: sha512-6BLLqyx7SIYRBOnTZ8wgfXANLJV5TQd3PevRJZp0vn42eO58A2LykRKdvL1qyPfdpmEVtF+uVOEZ4QTMqDRAWA==} + '@tailwindcss/oxide-freebsd-x64@4.1.0': + resolution: {integrity: sha512-9fdImTc+2lA5yHqJ61oeTXfCtzylNOzJVFhyWwVQAJESJJbVCPnj6f+b+Zf/AYAdKQfS6FCThbPEahkQrDCgLQ==} engines: {node: '>= 10'} cpu: [x64] os: [freebsd] - '@tailwindcss/oxide-linux-arm-gnueabihf@4.0.15': - resolution: {integrity: sha512-Zy63EVqO9241Pfg6G0IlRIWyY5vNcWrL5dd2WAKVJZRQVeolXEf1KfjkyeAAlErDj72cnyXObEZjMoPEKHpdNw==} + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.0': + resolution: {integrity: sha512-HB0bTkUOuTLLSdadyRhKE9yps4/ZBjrojbHTPMSvvf/8yBLZRPpWb+A6IgW5R+2A2AL4KhVPgLwWfoXsErxJFg==} engines: {node: '>= 10'} cpu: [arm] os: [linux] - '@tailwindcss/oxide-linux-arm64-gnu@4.0.15': - resolution: {integrity: sha512-2NemGQeaTbtIp1Z2wyerbVEJZTkAWhMDOhhR5z/zJ75yMNf8yLnE+sAlyf6yGDNr+1RqvWrRhhCFt7i0CIxe4Q==} + '@tailwindcss/oxide-linux-arm64-gnu@4.1.0': + resolution: {integrity: sha512-+QtYCwvKLjC46h6RikKkpELJWrpiMMtgyK0aaqhwPLEx1icGgIhwz8dqrkAiqbFRE0KiRrE2aenhYoEkplyRmA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@tailwindcss/oxide-linux-arm64-musl@4.0.15': - resolution: {integrity: sha512-342GVnhH/6PkVgKtEzvNVuQ4D+Q7B7qplvuH20Cfz9qEtydG6IQczTZ5IT4JPlh931MG1NUCVxg+CIorr1WJyw==} + '@tailwindcss/oxide-linux-arm64-musl@4.1.0': + resolution: {integrity: sha512-nApadFKM9GauzuPZPlt6TKfELavMHqJ0gVd+GYkYBTwr2t9KhgCAb2sKiFDDIhs1a7gOjsU7P1lEauv3iKFp+Q==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@tailwindcss/oxide-linux-x64-gnu@4.0.15': - resolution: {integrity: sha512-g76GxlKH124RuGqacCEFc2nbzRl7bBrlC8qDQMiUABkiifDRHOIUjgKbLNG4RuR9hQAD/MKsqZ7A8L08zsoBrw==} + '@tailwindcss/oxide-linux-x64-gnu@4.1.0': + resolution: {integrity: sha512-cp0Rf9Wit2kZHhrV8HIoDFD8dxU2+ZTCFCFbDj3a07pGyyPwLCJm5H5VipKXgYrBaLmlYu73ERidW0S5sdEXEg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@tailwindcss/oxide-linux-x64-musl@4.0.15': - resolution: {integrity: sha512-Gg/Y1XrKEvKpq6WeNt2h8rMIKOBj/W3mNa5NMvkQgMC7iO0+UNLrYmt6zgZufht66HozNpn+tJMbbkZ5a3LczA==} + '@tailwindcss/oxide-linux-x64-musl@4.1.0': + resolution: {integrity: sha512-4/wf42XWBJGXsOS6BhgPhdQbg/qyfdZ1nZvTL9sJoxYN+Ah+cfY5Dd7R0smzI8hmgCRt3TD1lYb72ChTyIA59w==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@tailwindcss/oxide-win32-arm64-msvc@4.0.15': - resolution: {integrity: sha512-7QtSSJwYZ7ZK1phVgcNZpuf7c7gaCj8Wb0xjliligT5qCGCp79OV2n3SJummVZdw4fbTNKUOYMO7m1GinppZyA==} + '@tailwindcss/oxide-win32-arm64-msvc@4.1.0': + resolution: {integrity: sha512-caXJJ0G6NwGbcoxEYdH3MZYN84C3PldaMdAEPMU6bjJXURQlKdSlQ/Ecis7/nSgBkMkicZyhqWmb36Tw/BFSIw==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@tailwindcss/oxide-win32-x64-msvc@4.0.15': - resolution: {integrity: sha512-JQ5H+5MLhOjpgNp6KomouE0ZuKmk3hO5h7/ClMNAQ8gZI2zkli3IH8ZqLbd2DVfXDbdxN2xvooIEeIlkIoSCqw==} + '@tailwindcss/oxide-win32-x64-msvc@4.1.0': + resolution: {integrity: sha512-ZHXRXRxB7HBmkUE8U13nmkGGYfR1I2vsuhiYjeDDUFIYpk1BL6caU8hvzkSlL/X5CAQNdIUUJRGom5I0ZyfJOA==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@tailwindcss/oxide@4.0.15': - resolution: {integrity: sha512-e0uHrKfPu7JJGMfjwVNyt5M0u+OP8kUmhACwIRlM+JNBuReDVQ63yAD1NWe5DwJtdaHjugNBil76j+ks3zlk6g==} + '@tailwindcss/oxide@4.1.0': + resolution: {integrity: sha512-A33oyZKpPFH08d7xkl13Dc8OTsbPhsuls0z9gUCxIHvn8c1BsUACddQxL6HwaeJR1fSYyXZUw8bdWcD8bVawpQ==} engines: {node: '>= 10'} '@tailwindcss/typography@0.5.7': @@ -1480,10 +1480,6 @@ packages: resolution: {integrity: sha512-80F22aiJ3GLyVnS/B3HzgR6RelZVumzj9jkL0Rhz4h0xYbNW9PjlQz5h3J/SShErbXBc295vseR4/MIbVmUbeA==} engines: {node: '>=12.0.0'} - fast-glob@3.2.4: - resolution: {integrity: sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ==} - engines: {node: '>=8'} - fast-glob@3.3.2: resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} engines: {node: '>=8.6.0'} @@ -1494,6 +1490,14 @@ packages: fd-slicer@1.1.0: resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + fdir@6.4.3: + resolution: {integrity: sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -1815,10 +1819,6 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} - micromatch@4.0.7: - resolution: {integrity: sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==} - engines: {node: '>=8.6'} - micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -2440,8 +2440,8 @@ packages: engines: {node: '>=14.0.0'} hasBin: true - tailwindcss@4.0.6: - resolution: {integrity: sha512-mysewHYJKaXgNOW6pp5xon/emCsfAMnO8WMaGKZZ35fomnR/T5gYnRg2/yRTTrtXiEl1tiVkeRt0eMO6HxEZqw==} + tailwindcss@4.1.1: + resolution: {integrity: sha512-QNbdmeS979Efzim2g/bEvfuh+fTcIdp1y7gA+sb6OYSW74rt7Cr7M78AKdf6HqWT3d5AiTb7SwTT3sLQxr4/qw==} tapable@2.2.1: resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} @@ -2467,6 +2467,10 @@ packages: tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinyglobby@0.2.12: + resolution: {integrity: sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==} + engines: {node: '>=12.0.0'} + tinypool@1.0.2: resolution: {integrity: sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==} engines: {node: ^18.0.0 || >=20.0.0} @@ -3105,52 +3109,52 @@ snapshots: dependencies: tailwindcss: 3.4.17 - '@tailwindcss/oxide-android-arm64@4.0.15': + '@tailwindcss/oxide-android-arm64@4.1.0': optional: true - '@tailwindcss/oxide-darwin-arm64@4.0.15': + '@tailwindcss/oxide-darwin-arm64@4.1.0': optional: true - '@tailwindcss/oxide-darwin-x64@4.0.15': + '@tailwindcss/oxide-darwin-x64@4.1.0': optional: true - '@tailwindcss/oxide-freebsd-x64@4.0.15': + '@tailwindcss/oxide-freebsd-x64@4.1.0': optional: true - '@tailwindcss/oxide-linux-arm-gnueabihf@4.0.15': + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.0': optional: true - '@tailwindcss/oxide-linux-arm64-gnu@4.0.15': + '@tailwindcss/oxide-linux-arm64-gnu@4.1.0': optional: true - '@tailwindcss/oxide-linux-arm64-musl@4.0.15': + '@tailwindcss/oxide-linux-arm64-musl@4.1.0': optional: true - '@tailwindcss/oxide-linux-x64-gnu@4.0.15': + '@tailwindcss/oxide-linux-x64-gnu@4.1.0': optional: true - '@tailwindcss/oxide-linux-x64-musl@4.0.15': + '@tailwindcss/oxide-linux-x64-musl@4.1.0': optional: true - '@tailwindcss/oxide-win32-arm64-msvc@4.0.15': + '@tailwindcss/oxide-win32-arm64-msvc@4.1.0': optional: true - '@tailwindcss/oxide-win32-x64-msvc@4.0.15': + '@tailwindcss/oxide-win32-x64-msvc@4.1.0': optional: true - '@tailwindcss/oxide@4.0.15': + '@tailwindcss/oxide@4.1.0': optionalDependencies: - '@tailwindcss/oxide-android-arm64': 4.0.15 - '@tailwindcss/oxide-darwin-arm64': 4.0.15 - '@tailwindcss/oxide-darwin-x64': 4.0.15 - '@tailwindcss/oxide-freebsd-x64': 4.0.15 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.0.15 - '@tailwindcss/oxide-linux-arm64-gnu': 4.0.15 - '@tailwindcss/oxide-linux-arm64-musl': 4.0.15 - '@tailwindcss/oxide-linux-x64-gnu': 4.0.15 - '@tailwindcss/oxide-linux-x64-musl': 4.0.15 - '@tailwindcss/oxide-win32-arm64-msvc': 4.0.15 - '@tailwindcss/oxide-win32-x64-msvc': 4.0.15 + '@tailwindcss/oxide-android-arm64': 4.1.0 + '@tailwindcss/oxide-darwin-arm64': 4.1.0 + '@tailwindcss/oxide-darwin-x64': 4.1.0 + '@tailwindcss/oxide-freebsd-x64': 4.1.0 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.0 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.0 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.0 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.0 + '@tailwindcss/oxide-linux-x64-musl': 4.1.0 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.0 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.0 '@tailwindcss/typography@0.5.7(tailwindcss@3.4.17)': dependencies: @@ -3717,15 +3721,6 @@ snapshots: expect-type@1.2.0: {} - fast-glob@3.2.4: - dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 - glob-parent: 5.1.2 - merge2: 1.4.1 - micromatch: 4.0.7 - picomatch: 2.3.1 - fast-glob@3.3.2: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -3742,6 +3737,10 @@ snapshots: dependencies: pend: 1.2.0 + fdir@6.4.3(picomatch@4.0.2): + optionalDependencies: + picomatch: 4.0.2 + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -4045,11 +4044,6 @@ snapshots: merge2@1.4.1: {} - micromatch@4.0.7: - dependencies: - braces: 3.0.3 - picomatch: 2.3.1 - micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -4710,7 +4704,7 @@ snapshots: transitivePeerDependencies: - ts-node - tailwindcss@4.0.6: {} + tailwindcss@4.1.1: {} tapable@2.2.1: {} @@ -4743,6 +4737,11 @@ snapshots: tinyexec@0.3.2: {} + tinyglobby@0.2.12: + dependencies: + fdir: 6.4.3(picomatch@4.0.2) + picomatch: 4.0.2 + tinypool@1.0.2: {} tinyrainbow@2.0.0: {}