From 81c8a22c59fe6d876d1b185ed09c03780f0501e7 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 10 Dec 2025 19:55:02 -0500 Subject: [PATCH 1/4] Cleanup --- .../src/css/ast.ts | 95 +------------------ .../src/css/css-context.ts | 32 +++++++ .../src/css/index.ts | 1 + .../src/css/to-postcss-ast.ts | 2 +- 4 files changed, 35 insertions(+), 95 deletions(-) create mode 100644 packages/tailwindcss-language-service/src/css/css-context.ts diff --git a/packages/tailwindcss-language-service/src/css/ast.ts b/packages/tailwindcss-language-service/src/css/ast.ts index ac95d204..11546640 100644 --- a/packages/tailwindcss-language-service/src/css/ast.ts +++ b/packages/tailwindcss-language-service/src/css/ast.ts @@ -1,6 +1,5 @@ -import { parseAtRule } from './parse' import type { SourceLocation } from './source' -import type { VisitContext } from '../util/walk' +import { parseAtRule } from './parse' const AT_SIGN = 0x40 @@ -117,95 +116,3 @@ export function atRoot(nodes: AstNode[]): AtRoot { nodes, } } - -export function cloneAstNode(node: T): T { - switch (node.kind) { - case 'rule': - return { - kind: node.kind, - selector: node.selector, - nodes: node.nodes.map(cloneAstNode), - src: node.src, - dst: node.dst, - } satisfies StyleRule as T - - case 'at-rule': - return { - kind: node.kind, - name: node.name, - params: node.params, - nodes: node.nodes.map(cloneAstNode), - src: node.src, - dst: node.dst, - } satisfies AtRule as T - - case 'at-root': - return { - kind: node.kind, - nodes: node.nodes.map(cloneAstNode), - src: node.src, - dst: node.dst, - } satisfies AtRoot as T - - case 'context': - return { - kind: node.kind, - context: { ...node.context }, - nodes: node.nodes.map(cloneAstNode), - src: node.src, - dst: node.dst, - } satisfies Context as T - - case 'declaration': - return { - kind: node.kind, - property: node.property, - value: node.value, - important: node.important, - src: node.src, - dst: node.dst, - } satisfies Declaration as T - - case 'comment': - return { - kind: node.kind, - value: node.value, - src: node.src, - dst: node.dst, - } satisfies Comment as T - - default: - node satisfies never - throw new Error(`Unknown node kind: ${(node as any).kind}`) - } -} - -export function cssContext( - ctx: VisitContext, -): VisitContext & { context: Record } { - return { - depth: ctx.depth, - get context() { - let context: Record = {} - for (let child of ctx.path()) { - if (child.kind === 'context') { - Object.assign(context, child.context) - } - } - - // Once computed, we never need to compute this again - Object.defineProperty(this, 'context', { value: context }) - return context - }, - get parent() { - let parent = (this.path().pop() as Extract) ?? null - - // Once computed, we never need to compute this again - Object.defineProperty(this, 'parent', { value: parent }) - return parent - }, - path() { - return ctx.path().filter((n) => n.kind !== 'context') - }, - } -} diff --git a/packages/tailwindcss-language-service/src/css/css-context.ts b/packages/tailwindcss-language-service/src/css/css-context.ts new file mode 100644 index 00000000..02ca1001 --- /dev/null +++ b/packages/tailwindcss-language-service/src/css/css-context.ts @@ -0,0 +1,32 @@ +import type { VisitContext } from '../util/walk' +import type { AstNode } from './ast' + +export function cssContext( + ctx: VisitContext, +): VisitContext & { context: Record } { + return { + depth: ctx.depth, + get context() { + let context: Record = {} + for (let child of ctx.path()) { + if (child.kind === 'context') { + Object.assign(context, child.context) + } + } + + // Once computed, we never need to compute this again + Object.defineProperty(this, 'context', { value: context }) + return context + }, + get parent() { + let parent = (this.path().pop() as Extract) ?? null + + // Once computed, we never need to compute this again + Object.defineProperty(this, 'parent', { value: parent }) + return parent + }, + path() { + return ctx.path().filter((n) => n.kind !== 'context') + }, + } +} diff --git a/packages/tailwindcss-language-service/src/css/index.ts b/packages/tailwindcss-language-service/src/css/index.ts index c053e61b..3612b101 100644 --- a/packages/tailwindcss-language-service/src/css/index.ts +++ b/packages/tailwindcss-language-service/src/css/index.ts @@ -1,6 +1,7 @@ export * from './ast' export * from './source' export { parse } from './parse' +export { cloneAstNode } from './clone-ast-node' export { fromPostCSSAst } from './from-postcss-ast' export { toPostCSSAst } from './to-postcss-ast' export { toCss } from './to-css' diff --git a/packages/tailwindcss-language-service/src/css/to-postcss-ast.ts b/packages/tailwindcss-language-service/src/css/to-postcss-ast.ts index 3495f323..c12584f5 100644 --- a/packages/tailwindcss-language-service/src/css/to-postcss-ast.ts +++ b/packages/tailwindcss-language-service/src/css/to-postcss-ast.ts @@ -1,5 +1,5 @@ import * as postcss from 'postcss' -import { atRule, comment, decl, styleRule, type AstNode } from './ast' +import type { AstNode } from './ast' import type { Source, SourceLocation } from './source' import { DefaultMap } from '../util/default-map' import { createLineTable, LineTable } from '../util/line-table' From 43939ce648a4191cbfd0a77bc20d7ea0486f9886 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 16 Dec 2025 22:29:10 -0500 Subject: [PATCH 2/4] Allow at-rules to have empty bodies --- packages/tailwindcss-language-service/src/css/ast.ts | 8 ++++---- .../src/css/clone-ast-node.ts | 2 +- .../src/css/from-postcss-ast.ts | 4 ++-- packages/tailwindcss-language-service/src/css/parse.ts | 8 +++++++- packages/tailwindcss-language-service/src/css/to-css.ts | 2 +- .../src/css/to-postcss-ast.ts | 8 ++++++-- 6 files changed, 21 insertions(+), 11 deletions(-) diff --git a/packages/tailwindcss-language-service/src/css/ast.ts b/packages/tailwindcss-language-service/src/css/ast.ts index 11546640..6d206c96 100644 --- a/packages/tailwindcss-language-service/src/css/ast.ts +++ b/packages/tailwindcss-language-service/src/css/ast.ts @@ -16,7 +16,7 @@ export type AtRule = { kind: 'at-rule' name: string params: string - nodes: AstNode[] + nodes: AstNode[] | null src?: SourceLocation dst?: SourceLocation @@ -69,7 +69,7 @@ export function styleRule(selector: string, nodes: AstNode[] = []): StyleRule { } } -export function atRule(name: string, params: string = '', nodes: AstNode[] = []): AtRule { +export function atRule(name: string, params: string = '', nodes: AstNode[] | null = []): AtRule { return { kind: 'at-rule', name, @@ -78,12 +78,12 @@ export function atRule(name: string, params: string = '', nodes: AstNode[] = []) } } -export function rule(selector: string, nodes: AstNode[] = []): StyleRule | AtRule { +export function rule(selector: string, nodes: AstNode[] | null = []): StyleRule | AtRule { if (selector.charCodeAt(0) === AT_SIGN) { return parseAtRule(selector, nodes) } - return styleRule(selector, nodes) + return styleRule(selector, nodes ?? []) } export function decl(property: string, value: string | undefined, important = false): Declaration { diff --git a/packages/tailwindcss-language-service/src/css/clone-ast-node.ts b/packages/tailwindcss-language-service/src/css/clone-ast-node.ts index ec62e482..7d0c4ea8 100644 --- a/packages/tailwindcss-language-service/src/css/clone-ast-node.ts +++ b/packages/tailwindcss-language-service/src/css/clone-ast-node.ts @@ -16,7 +16,7 @@ export function cloneAstNode(node: T): T { kind: node.kind, name: node.name, params: node.params, - nodes: node.nodes.map(cloneAstNode), + nodes: node.nodes?.map(cloneAstNode) ?? null, src: node.src, dst: node.dst, } satisfies AtRule as T diff --git a/packages/tailwindcss-language-service/src/css/from-postcss-ast.ts b/packages/tailwindcss-language-service/src/css/from-postcss-ast.ts index 9fb26ff7..04cc59ad 100644 --- a/packages/tailwindcss-language-service/src/css/from-postcss-ast.ts +++ b/packages/tailwindcss-language-service/src/css/from-postcss-ast.ts @@ -44,9 +44,9 @@ export function fromPostCSSAst(root: postcss.Root): AstNode[] { // AtRule else if (node.type === 'atrule') { - let astNode = atRule(`@${node.name}`, node.params) + let astNode = atRule(`@${node.name}`, node.params, node.nodes ? [] : null) astNode.src = toSource(node) - node.each((child) => transform(child, astNode.nodes)) + node.each((child) => transform(child, astNode.nodes!)) parent.push(astNode) } diff --git a/packages/tailwindcss-language-service/src/css/parse.ts b/packages/tailwindcss-language-service/src/css/parse.ts index d0f8ff22..4386f6d6 100644 --- a/packages/tailwindcss-language-service/src/css/parse.ts +++ b/packages/tailwindcss-language-service/src/css/parse.ts @@ -277,6 +277,7 @@ export function parse(input: string, opts?: ParseOptions): AstNode[] { } if (parent) { + parent.nodes ??= [] parent.nodes.push(declaration) } else { ast.push(declaration) @@ -303,6 +304,7 @@ export function parse(input: string, opts?: ParseOptions): AstNode[] { // At-rule is nested inside of a rule, attach it to the parent. if (parent) { + parent.nodes ??= [] parent.nodes.push(node) } @@ -343,6 +345,7 @@ export function parse(input: string, opts?: ParseOptions): AstNode[] { } if (parent) { + parent.nodes ??= [] parent.nodes.push(declaration) } else { ast.push(declaration) @@ -369,6 +372,7 @@ export function parse(input: string, opts?: ParseOptions): AstNode[] { // Attach the rule to the parent in case it's nested. if (parent) { + parent.nodes ??= [] parent.nodes.push(node) } @@ -421,6 +425,7 @@ export function parse(input: string, opts?: ParseOptions): AstNode[] { // At-rule is nested inside of a rule, attach it to the parent. if (parent) { + parent.nodes ??= [] parent.nodes.push(node) } @@ -460,6 +465,7 @@ export function parse(input: string, opts?: ParseOptions): AstNode[] { node.dst = [source, bufferStart, i] } + parent.nodes ??= [] parent.nodes.push(node) } } @@ -548,7 +554,7 @@ export function parse(input: string, opts?: ParseOptions): AstNode[] { return ast } -export function parseAtRule(buffer: string, nodes: AstNode[] = []): AtRule { +export function parseAtRule(buffer: string, nodes: AstNode[] | null = []): AtRule { let name = buffer let params = '' diff --git a/packages/tailwindcss-language-service/src/css/to-css.ts b/packages/tailwindcss-language-service/src/css/to-css.ts index 83e94742..cc532f67 100644 --- a/packages/tailwindcss-language-service/src/css/to-css.ts +++ b/packages/tailwindcss-language-service/src/css/to-css.ts @@ -91,7 +91,7 @@ export function toCss(ast: AstNode[], track?: boolean): string { // ```css // @layer base, components, utilities; // ``` - if (node.nodes.length === 0) { + if (!node.nodes) { let css = `${indent}${node.name} ${node.params};\n` if (track) { diff --git a/packages/tailwindcss-language-service/src/css/to-postcss-ast.ts b/packages/tailwindcss-language-service/src/css/to-postcss-ast.ts index c12584f5..7fd6f979 100644 --- a/packages/tailwindcss-language-service/src/css/to-postcss-ast.ts +++ b/packages/tailwindcss-language-service/src/css/to-postcss-ast.ts @@ -85,11 +85,15 @@ export function toPostCSSAst(ast: AstNode[], source?: postcss.Source): postcss.R // AtRule else if (node.kind === 'at-rule') { - let astNode = postcss.atRule({ name: node.name.slice(1), params: node.params }) + let astNode = postcss.atRule({ + name: node.name.slice(1), + params: node.params, + ...(node.nodes ? { nodes: [] } : {}), + }) updateSource(astNode, node.src) astNode.raws.semicolon = true parent.append(astNode) - for (let child of node.nodes) { + for (let child of node.nodes ?? []) { transform(child, astNode) } } From 8bbc921837fadefa191046a3e97b881ebb0bf23b Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 16 Dec 2025 22:29:27 -0500 Subject: [PATCH 3/4] Tweak AST returned by the design system --- .../src/util/v4/design-system.ts | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) 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 7731e4e4..e8f96909 100644 --- a/packages/tailwindcss-language-server/src/util/v4/design-system.ts +++ b/packages/tailwindcss-language-server/src/util/v4/design-system.ts @@ -10,6 +10,7 @@ import type { Jiti } from 'jiti/lib/types' import { assets } from './assets' import { plugins } from './plugins' import { AstNode, cloneAstNode, parse } from '@tailwindcss/language-service/src/css' +import { walk, WalkAction } from '@tailwindcss/language-service/src/util/walk' const HAS_V4_IMPORT = /@import\s*(?:'tailwindcss'|"tailwindcss")/ const HAS_V4_THEME = /@theme\s*\{/ @@ -240,7 +241,26 @@ export async function loadDesignSystem( let str = css[idx] if (Array.isArray(str)) { - cache[cls] = str + let ast = str.map(cloneAstNode) + + // Rewrite at-rules with zero nodes to act as if they have no body + // + // At a future time we'll only do this conditionally for earlier + // Tailwind CSS v4 versions. We have to clone the AST *first* + // because if the AST was shared with Tailwind CSS internals + // and we mutated it we could break things. + walk(ast, (node) => { + if (node.kind !== 'at-rule') return WalkAction.Continue + if (node.nodes === null) return WalkAction.Continue + if (node.nodes.length !== 0) return WalkAction.Continue + + node.nodes = null + + return WalkAction.Continue + }) + + cache[cls] = ast + continue } From 1ac6c6a496bd76e7ed363472d4649bc16a26f7f8 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 18 Dec 2025 14:30:23 -0500 Subject: [PATCH 4/4] Update changelog --- packages/vscode-tailwindcss/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vscode-tailwindcss/CHANGELOG.md b/packages/vscode-tailwindcss/CHANGELOG.md index 9a23f7ca..2f0b4502 100644 --- a/packages/vscode-tailwindcss/CHANGELOG.md +++ b/packages/vscode-tailwindcss/CHANGELOG.md @@ -4,7 +4,7 @@ - Add a source to all emitted diagnostics ([#1491](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1491)) - Improve performance in large files ([#1507](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1507)) -- Improve utility lookup performance when using v4 ([#1509](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1509)) +- Improve utility lookup performance when using v4 ([#1509](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1509), [#1512](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1512)) - Fix project initalization when stylesheet is named `tailwindcss.css` ([#1517](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1517)) ## 0.14.29