From d042dee00013efd8761d78fe6d3866027cf13906 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 7 Jan 2025 13:00:12 -0500 Subject: [PATCH 01/10] Support `@reference` during project discovery --- .../src/css/resolve-css-imports.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/tailwindcss-language-server/src/css/resolve-css-imports.ts b/packages/tailwindcss-language-server/src/css/resolve-css-imports.ts index 7cfd7f3f..af1861c9 100644 --- a/packages/tailwindcss-language-server/src/css/resolve-css-imports.ts +++ b/packages/tailwindcss-language-server/src/css/resolve-css-imports.ts @@ -12,6 +12,17 @@ export function resolveCssImports({ loose?: boolean }) { return postcss([ + // Replace `@reference "…"` with `@import "…" reference` + { + postcssPlugin: 'replace-at-reference', + Once(root) { + root.walkAtRules('reference', (atRule) => { + atRule.name = 'import' + atRule.params += ' reference' + }) + }, + }, + // Hoist imports to the top of the file { postcssPlugin: 'hoist-at-import', From 0c1503c82bf01544214765e6221bdfad2511a505 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 7 Jan 2025 13:18:40 -0500 Subject: [PATCH 02/10] Ensure `@reference` is highlighted --- packages/vscode-tailwindcss/syntaxes/at-rules.tmLanguage.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vscode-tailwindcss/syntaxes/at-rules.tmLanguage.json b/packages/vscode-tailwindcss/syntaxes/at-rules.tmLanguage.json index 1ae0b7ec..eb78b038 100644 --- a/packages/vscode-tailwindcss/syntaxes/at-rules.tmLanguage.json +++ b/packages/vscode-tailwindcss/syntaxes/at-rules.tmLanguage.json @@ -5,7 +5,7 @@ "name": "TailwindCSS", "patterns": [ { - "begin": "(?i)((@)import)(?:\\s+|$|(?=['\"]|/\\*))", + "begin": "(?i)((@)(?:import|reference))(?:\\s+|$|(?=['\"]|/\\*))", "beginCaptures": { "1": { "name": "keyword.control.at-rule.import.css" From df26e01fec17cb9b1463edf1c4760a1336498230 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 7 Jan 2025 13:18:58 -0500 Subject: [PATCH 03/10] Add support for `@reference` in diagnostics --- .../src/language/cssServer.ts | 19 ++++++++++++------- .../src/completions/file-paths.ts | 15 +++++++++------ .../getInvalidSourceDiagnostics.ts | 2 +- .../src/documentLinksProvider.ts | 1 + 4 files changed, 23 insertions(+), 14 deletions(-) diff --git a/packages/tailwindcss-language-server/src/language/cssServer.ts b/packages/tailwindcss-language-server/src/language/cssServer.ts index bb228b19..a759a823 100644 --- a/packages/tailwindcss-language-server/src/language/cssServer.ts +++ b/packages/tailwindcss-language-server/src/language/cssServer.ts @@ -348,17 +348,22 @@ function createVirtualCssDocument(textDocument: TextDocument): TextDocument { .replace(/@variants(\s+[^{]+){/g, replace()) .replace(/@responsive(\s*){/g, replace()) .replace(/@layer(\s+[^{]{2,}){/g, replace(-3)) + .replace(/@reference\s*([^;]{2,})/g, '@import $1') .replace( /@media(\s+screen\s*\([^)]+\))/g, (_match, screen) => `@media (${MEDIA_MARKER})${' '.repeat(screen.length - 4)}`, ) - .replace(/@import\s*("(?:[^"]+)"|'(?:[^']+)')\s*(.*?)(?=;|$)/g, (_match, url, other) => { - // Remove`source(…)`, `theme(…)`, and `prefix(…)` from `@import`s - // otherwise we'll show syntax-error diagnostics which we don't want - other = other.replace(/((source|theme|prefix)\([^)]+\)\s*)+?/g, '') - - return `@import "${url.slice(1, -1)}" ${other}` - }) + .replace( + /@import(\s*)("(?:[^"]+)"|'(?:[^']+)')\s*(.*?)(?=;|$)/g, + (_match, spaces, url, other) => { + // Remove`source(…)`, `theme(…)`, and `prefix(…)` from `@import`s + // otherwise we'll show syntax-error diagnostics which we don't want + other = other.replace(/((source|theme|prefix)\([^)]+\)\s*)+?/g, '') + + // We have to add the spaces here so the character positions line up + return `@import${spaces}"${url.slice(1, -1)}" ${other}` + }, + ) .replace(/(?<=\b(?:theme|config)\([^)]*)[.[\]]/g, '_') return TextDocument.create( diff --git a/packages/tailwindcss-language-service/src/completions/file-paths.ts b/packages/tailwindcss-language-service/src/completions/file-paths.ts index f9b1898d..9bdd5b41 100644 --- a/packages/tailwindcss-language-service/src/completions/file-paths.ts +++ b/packages/tailwindcss-language-service/src/completions/file-paths.ts @@ -6,8 +6,10 @@ const PATTERN_CUSTOM_V3 = /@(?config)\s*(?'[^']*|"[^"]*)$/ // @import … source('…') // @tailwind utilities source('…') -const PATTERN_IMPORT_SOURCE = /@(?import)\s*(?'[^']*'|"[^"]*")\s*source\((?'[^']*|"[^"]*)$/ -const PATTERN_UTIL_SOURCE = /@(?tailwind)\s+utilities\s+source\((?'[^']*|"[^"]*)?$/ +const PATTERN_IMPORT_SOURCE = + /@(?(?:import|reference))\s*(?'[^']*'|"[^"]*")\s*source\((?'[^']*|"[^"]*)$/ +const PATTERN_UTIL_SOURCE = + /@(?tailwind)\s+utilities\s+source\((?'[^']*|"[^"]*)?$/ export type FileDirective = { directive: string @@ -17,14 +19,15 @@ export type FileDirective = { export async function findFileDirective(state: State, text: string): Promise { if (state.v4) { - let match = text.match(PATTERN_CUSTOM_V4) - ?? text.match(PATTERN_IMPORT_SOURCE) - ?? text.match(PATTERN_UTIL_SOURCE) + let match = + text.match(PATTERN_CUSTOM_V4) ?? + text.match(PATTERN_IMPORT_SOURCE) ?? + text.match(PATTERN_UTIL_SOURCE) if (!match) return null let directive = match.groups.directive - let partial = match.groups.partial?.slice(1) ?? "" // remove leading quote + let partial = match.groups.partial?.slice(1) ?? '' // remove leading quote // Most suggestions are for JS files so we'll default to that let suggest: FileDirective['suggest'] = 'script' diff --git a/packages/tailwindcss-language-service/src/diagnostics/getInvalidSourceDiagnostics.ts b/packages/tailwindcss-language-service/src/diagnostics/getInvalidSourceDiagnostics.ts index e8b4ba56..e7d9703d 100644 --- a/packages/tailwindcss-language-service/src/diagnostics/getInvalidSourceDiagnostics.ts +++ b/packages/tailwindcss-language-service/src/diagnostics/getInvalidSourceDiagnostics.ts @@ -8,7 +8,7 @@ import { absoluteRange } from '../util/absoluteRange' // @import … source('…') // @tailwind utilities source('…') const PATTERN_IMPORT_SOURCE = - /(?:\s|^)@(?import)\s*(?'[^']*'|"[^"]*")\s*source\((?'[^']*'?|"[^"]*"?|[a-z]*|\)|;)/dg + /(?:\s|^)@(?(?:import|reference))\s*(?'[^']*'|"[^"]*")\s*source\((?'[^']*'?|"[^"]*"?|[a-z]*|\)|;)/dg const PATTERN_UTIL_SOURCE = /(?:\s|^)@(?tailwind)\s+(?\S+)\s+source\((?'[^']*'?|"[^"]*"?|[a-z]*|\)|;)/dg diff --git a/packages/tailwindcss-language-service/src/documentLinksProvider.ts b/packages/tailwindcss-language-service/src/documentLinksProvider.ts index a0fcd1a2..e469aa83 100644 --- a/packages/tailwindcss-language-service/src/documentLinksProvider.ts +++ b/packages/tailwindcss-language-service/src/documentLinksProvider.ts @@ -20,6 +20,7 @@ export function getDocumentLinks( /@plugin\s*(?'[^']+'|"[^"]+")/g, /@source\s*(?'[^']+'|"[^"]+")/g, /@import\s*('[^']*'|"[^"]*")\s*source\((?'[^']*'?|"[^"]*"?)/g, + /@reference\s*('[^']*'|"[^"]*")\s*source\((?'[^']*'?|"[^"]*"?)/g, /@tailwind\s*utilities\s*source\((?'[^']*'?|"[^"]*"?)/g, ) } From ee5ada5c6081f12cd5fde13567be038d44a6b9c1 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 7 Jan 2025 13:20:56 -0500 Subject: [PATCH 04/10] Update changelog --- packages/vscode-tailwindcss/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/vscode-tailwindcss/CHANGELOG.md b/packages/vscode-tailwindcss/CHANGELOG.md index e1ec4bf6..11d4a23d 100644 --- a/packages/vscode-tailwindcss/CHANGELOG.md +++ b/packages/vscode-tailwindcss/CHANGELOG.md @@ -9,6 +9,7 @@ - Add variant suggestions to `@variant` ([#1127](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1127)) - Don't suggest at-rules when nested that cannot be used in a nested context ([#1127](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1127)) - Make sure completions work when using prefixes in v4 ([#1129](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1129)) +- Add support for `@reference` ([#1117](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1117)) ## 0.12.18 From 39fa90c9b0bdec71fefdb85d88f23d588f29ae10 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Fri, 10 Jan 2025 12:41:46 -0500 Subject: [PATCH 05/10] =?UTF-8?q?Add=20completions=20for=20`=E2=80=94value?= =?UTF-8?q?(=E2=80=A6)`=20and=20`=E2=80=94modifier(=E2=80=A6)`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/completionProvider.ts | 258 +++++++++++++++++- .../getInvalidConfigPathDiagnostics.ts | 12 +- .../src/util/braceLevel.ts | 14 +- .../src/util/segment.ts | 102 +++++++ .../src/util/v4/theme-keys.ts | 31 +++ 5 files changed, 404 insertions(+), 13 deletions(-) create mode 100644 packages/tailwindcss-language-service/src/util/segment.ts create mode 100644 packages/tailwindcss-language-service/src/util/v4/theme-keys.ts diff --git a/packages/tailwindcss-language-service/src/completionProvider.ts b/packages/tailwindcss-language-service/src/completionProvider.ts index 1acf0afd..dad2d7b5 100644 --- a/packages/tailwindcss-language-service/src/completionProvider.ts +++ b/packages/tailwindcss-language-service/src/completionProvider.ts @@ -7,6 +7,7 @@ import { type CompletionList, type Position, type CompletionContext, + InsertTextFormat, } from 'vscode-languageserver' import type { TextDocument } from 'vscode-languageserver-textdocument' import dlv from 'dlv' @@ -18,7 +19,7 @@ import { findLast, matchClassAttributes } from './util/find' import { stringifyConfigValue, stringifyCss } from './util/stringify' import { stringifyScreen, Screen } from './util/screens' import isObject from './util/isObject' -import braceLevel from './util/braceLevel' +import { braceLevel, parenLevel } from './util/braceLevel' import * as emmetHelper from 'vscode-emmet-helper-bundled' import { isValidLocationForEmmetAbbreviation } from './util/isValidLocationForEmmetAbbreviation' import { isJsDoc, isJsxContext } from './util/js' @@ -41,6 +42,9 @@ import { IS_SCRIPT_SOURCE, IS_TEMPLATE_SOURCE } from './metadata/extensions' import * as postcss from 'postcss' import { findFileDirective } from './completions/file-paths' import type { ThemeEntry } from './util/v4' +import { posix } from 'node:path/win32' +import { segment } from './util/segment' +import { resolveKnownThemeKeys, resolveKnownThemeNamespaces } from './util/v4/theme-keys' let isUtil = (className) => Array.isArray(className.__info) @@ -1128,6 +1132,172 @@ function provideCssHelperCompletions( ) } +function getCsstUtilityNameAtPosition( + state: State, + document: TextDocument, + position: Position, +): { root: string; kind: 'static' | 'functional' } | null { + if (!isCssContext(state, document, position)) return null + if (!isInsideAtRule('utility', document, position)) return null + + let text = document.getText({ + start: { line: 0, character: 0 }, + end: position, + }) + + // Make sure we're in a functional utility block + let block = text.lastIndexOf(`@utility`) + if (block === -1) return null + + let curly = text.indexOf('{', block) + if (curly === -1) return null + + let root = text.slice(block + 8, curly).trim() + + if (root.length === 0) return null + + if (root.endsWith('-*')) { + root = root.slice(0, -2) + + if (root.length === 0) return null + + return { root, kind: 'functional' } + } + + return { root: root, kind: 'static' } +} + +function provideUtilityFunctionCompletions( + state: State, + document: TextDocument, + position: Position, +): CompletionList { + let utilityName = getCsstUtilityNameAtPosition(state, document, position) + if (!utilityName) return null + + let text = document.getText({ + start: { line: position.line, character: 0 }, + end: position, + }) + + // Make sure we're in "value position" + // e.g. --foo: + let pattern = /^[^:]+:[^;]*$/ + if (!pattern.test(text)) return null + + return withDefaults( + { + isIncomplete: false, + items: [ + { + label: '--value()', + textEditText: '--value($1)', + sortText: '-00000', + insertTextFormat: InsertTextFormat.Snippet, + kind: CompletionItemKind.Function, + documentation: { + kind: 'markdown' as typeof MarkupKind.Markdown, + value: 'Reference a value based on the name of the utility. e.g. the `md` in `text-md`', + }, + command: { command: 'editor.action.triggerSuggest', title: '' }, + }, + { + label: '--modifier()', + textEditText: '--modifier($1)', + sortText: '-00001', + insertTextFormat: InsertTextFormat.Snippet, + kind: CompletionItemKind.Function, + documentation: { + kind: 'markdown' as typeof MarkupKind.Markdown, + value: "Reference a value based on the utility's modifier. e.g. the `6` in `text-md/6`", + }, + }, + ], + }, + { + data: { + ...(state.completionItemData ?? {}), + }, + range: { + start: position, + end: position, + }, + }, + state.editor.capabilities.itemDefaults, + ) +} + +async function provideUtilityFunctionArgumentCompletions( + state: State, + document: TextDocument, + position: Position, +): Promise { + let utilityName = getCsstUtilityNameAtPosition(state, document, position) + if (!utilityName) return null + + let text = document.getText({ + start: { line: position.line, character: 0 }, + end: position, + }) + + // Look to see if we're inside --value() or --modifier() + let fn = null + let fnStart = 0 + let valueIdx = text.lastIndexOf('--value(') + let modifierIdx = text.lastIndexOf('--modifier(') + let fnIdx = Math.max(valueIdx, modifierIdx) + if (fnIdx === -1) return null + + if (fnIdx === valueIdx) { + fn = '--value' + } else if (fnIdx === modifierIdx) { + fn = '--modifier' + } + + fnStart = fnIdx + fn.length + 1 + + // Make sure we're actaully inside the function + if (parenLevel(text.slice(fnIdx)) === 0) return null + + let args = Array.from(await knownUtilityFunctionArguments(state, fn)) + + let parts = segment(text.slice(fnStart), ',').map((s) => s.trim()) + + // Only suggest at the start of the argument + if (parts.at(-1) !== '') return null + + // Remove items that are already used + args = args.filter((arg) => !parts.includes(arg.name)) + + let items: CompletionItem[] = args.map((arg, idx) => ({ + label: arg.name, + insertText: arg.name, + kind: CompletionItemKind.Constant, + sortText: naturalExpand(idx, args.length), + documentation: { + kind: 'markdown' as typeof MarkupKind.Markdown, + value: arg.description.replace(/\{utility\}-/g, `${utilityName.root}-`), + }, + })) + + return withDefaults( + { + isIncomplete: true, + items, + }, + { + data: { + ...(state.completionItemData ?? {}), + }, + range: { + start: position, + end: position, + }, + }, + state.editor.capabilities.itemDefaults, + ) +} + function provideTailwindDirectiveCompletions( state: State, document: TextDocument, @@ -2001,6 +2171,8 @@ export async function doComplete( const result = (await provideClassNameCompletions(state, document, position, context)) || (await provideThemeDirectiveCompletions(state, document, position)) || + (await provideUtilityFunctionArgumentCompletions(state, document, position)) || + provideUtilityFunctionCompletions(state, document, position) || provideCssHelperCompletions(state, document, position) || provideCssDirectiveCompletions(state, document, position) || provideScreenDirectiveCompletions(state, document, position) || @@ -2170,3 +2342,87 @@ async function getCssDetail(state: State, className: any): Promise { } return null } + +type UtilityFn = '--value' | '--modifier' + +interface UtilityFnArg { + name: string + description: string +} + +async function knownUtilityFunctionArguments(state: State, fn: UtilityFn): Promise { + if (!state.designSystem) return [] + + let args: UtilityFnArg[] = [] + + let namespaces = resolveKnownThemeNamespaces(state.designSystem) + + for (let ns of namespaces) { + args.push({ + name: `${ns}-*`, + description: `Support theme values from \`${ns}-*\``, + }) + } + + args.push({ + name: 'integer', + description: 'Support integer values, e.g. `{utility}-6`', + }) + + args.push({ + name: 'number', + description: + 'Support numeric values in increments of 0.25, e.g. `{utility}-6` and `{utility}-7.25`', + }) + + args.push({ + name: 'percentage', + description: 'Support integer percentage values, e.g. `{utility}-50%` and `{utility}-21%`', + }) + + if (fn === '--value') { + args.push({ + name: 'ratio', + description: 'Support fractions, e.g. `{utility}-1/5` and `{utility}-16/9`', + }) + } + + args.push({ + name: '[integer]', + description: 'Support arbitrary integer values, e.g. `{utility}-[123]`', + }) + + args.push({ + name: '[number]', + description: 'Support arbitrary numeric values, e.g. `{utility}-[10]` and `{utility}-[10.234]`', + }) + + args.push({ + name: '[percentage]', + description: + 'Support arbitrary percentage values, e.g. `{utility}-[10%]` and `{utility}-[10.234%]`', + }) + + args.push({ + name: '[ratio]', + description: 'Support arbitrary fractions, e.g. `{utility}-[1/5]` and `{utility}-[16/9]`', + }) + + args.push({ + name: '[color]', + description: + 'Support arbitrary color values, e.g. `{utility}-[#639]` and `{utility}-[oklch(44.03% 0.1603 303.37)]`', + }) + + args.push({ + name: '[angle]', + description: 'Support arbitrary angle, e.g. `{utility}-[12deg]` and `{utility}-[0.21rad]`', + }) + + args.push({ + name: '[url]', + description: "Support arbitrary URL functions, e.g. `{utility}-['url(…)']`", + }) + + return args +} diff --git a/packages/tailwindcss-language-service/src/diagnostics/getInvalidConfigPathDiagnostics.ts b/packages/tailwindcss-language-service/src/diagnostics/getInvalidConfigPathDiagnostics.ts index 1b3ecb16..d20655e3 100644 --- a/packages/tailwindcss-language-service/src/diagnostics/getInvalidConfigPathDiagnostics.ts +++ b/packages/tailwindcss-language-service/src/diagnostics/getInvalidConfigPathDiagnostics.ts @@ -5,6 +5,7 @@ import { stringToPath } from '../util/stringToPath' import isObject from '../util/isObject' import { closest, distance } from '../util/closest' import { combinations } from '../util/combinations' +import { resolveKnownThemeKeys } from '../util/v4/theme-keys' import dlv from 'dlv' import type { TextDocument } from 'vscode-languageserver-textdocument' import type { DesignSystem } from '../util/v4' @@ -228,17 +229,6 @@ function resolveThemeValue(design: DesignSystem, path: string) { return value } -function resolveKnownThemeKeys(design: DesignSystem): string[] { - let validThemeKeys = Array.from(design.theme.entries(), ([key]) => key) - - let prefixLength = design.theme.prefix?.length ?? 0 - - return prefixLength > 0 - ? // Strip the configured prefix from the list of valid theme keys - validThemeKeys.map((key) => `--${key.slice(prefixLength + 3)}`) - : validThemeKeys -} - function validateV4ThemePath(state: State, path: string): ValidationResult { let prefix = state.designSystem.theme.prefix ?? null diff --git a/packages/tailwindcss-language-service/src/util/braceLevel.ts b/packages/tailwindcss-language-service/src/util/braceLevel.ts index 5b0e871d..a7245b17 100644 --- a/packages/tailwindcss-language-service/src/util/braceLevel.ts +++ b/packages/tailwindcss-language-service/src/util/braceLevel.ts @@ -1,4 +1,4 @@ -export default function braceLevel(text: string) { +export function braceLevel(text: string) { let count = 0 for (let i = text.length - 1; i >= 0; i--) { @@ -9,3 +9,15 @@ export default function braceLevel(text: string) { return count } + +export function parenLevel(text: string) { + let count = 0 + + for (let i = text.length - 1; i >= 0; i--) { + let char = text.charCodeAt(i) + + count += Number(char === 0x28 /* ( */) - Number(char === 0x29 /* ) */) + } + + return count +} diff --git a/packages/tailwindcss-language-service/src/util/segment.ts b/packages/tailwindcss-language-service/src/util/segment.ts new file mode 100644 index 00000000..018485db --- /dev/null +++ b/packages/tailwindcss-language-service/src/util/segment.ts @@ -0,0 +1,102 @@ +const BACKSLASH = 0x5c +const OPEN_CURLY = 0x7b +const CLOSE_CURLY = 0x7d +const OPEN_PAREN = 0x28 +const CLOSE_PAREN = 0x29 +const OPEN_BRACKET = 0x5b +const CLOSE_BRACKET = 0x5d +const DOUBLE_QUOTE = 0x22 +const SINGLE_QUOTE = 0x27 + +// This is a shared buffer that is used to keep track of the current nesting level +// of parens, brackets, and braces. It is used to determine if a character is at +// the top-level of a string. This is a performance optimization to avoid memory +// allocations on every call to `segment`. +const closingBracketStack = new Uint8Array(256) + +/** + * This splits a string on a top-level character. + * + * Regex doesn't support recursion (at least not the JS-flavored version), + * so we have to use a tiny state machine to keep track of paren placement. + * + * Expected behavior using commas: + * var(--a, 0 0 1px rgb(0, 0, 0)), 0 0 1px rgb(0, 0, 0) + * ┬ ┬ ┬ ┬ + * x x x ╰──────── Split because top-level + * ╰──────────────┴──┴───────────── Ignored b/c inside >= 1 levels of parens + */ +export function segment(input: string, separator: string) { + // SAFETY: We can use an index into a shared buffer because this function is + // synchronous, non-recursive, and runs in a single-threaded environment. + let stackPos = 0 + let parts: string[] = [] + let lastPos = 0 + let len = input.length + + let separatorCode = separator.charCodeAt(0) + + for (let idx = 0; idx < len; idx++) { + let char = input.charCodeAt(idx) + + if (stackPos === 0 && char === separatorCode) { + parts.push(input.slice(lastPos, idx)) + lastPos = idx + 1 + continue + } + + switch (char) { + case BACKSLASH: + // The next character is escaped, so we skip it. + idx += 1 + break + // Strings should be handled as-is until the end of the string. No need to + // worry about balancing parens, brackets, or curlies inside a string. + case SINGLE_QUOTE: + case DOUBLE_QUOTE: + // Ensure we don't go out of bounds. + while (++idx < len) { + let nextChar = input.charCodeAt(idx) + + // The next character is escaped, so we skip it. + if (nextChar === BACKSLASH) { + idx += 1 + continue + } + + if (nextChar === char) { + break + } + } + break + case OPEN_PAREN: + closingBracketStack[stackPos] = CLOSE_PAREN + stackPos++ + break + case OPEN_BRACKET: + closingBracketStack[stackPos] = CLOSE_BRACKET + stackPos++ + break + case OPEN_CURLY: + closingBracketStack[stackPos] = CLOSE_CURLY + stackPos++ + break + case CLOSE_BRACKET: + case CLOSE_CURLY: + case CLOSE_PAREN: + if (stackPos > 0 && char === closingBracketStack[stackPos - 1]) { + // SAFETY: The buffer does not need to be mutated because the stack is + // only ever read from or written to its current position. Its current + // position is only ever incremented after writing to it. Meaning that + // the buffer can be dirty for the next use and still be correct since + // reading/writing always starts at position `0`. + stackPos-- + } + break + } + } + + parts.push(input.slice(lastPos)) + + return parts +} diff --git a/packages/tailwindcss-language-service/src/util/v4/theme-keys.ts b/packages/tailwindcss-language-service/src/util/v4/theme-keys.ts new file mode 100644 index 00000000..dc2f93c4 --- /dev/null +++ b/packages/tailwindcss-language-service/src/util/v4/theme-keys.ts @@ -0,0 +1,31 @@ +import { DesignSystem } from './design-system' + +export function resolveKnownThemeKeys(design: DesignSystem): string[] { + let validThemeKeys = Array.from(design.theme.entries(), ([key]) => key) + + let prefixLength = design.theme.prefix?.length ?? 0 + + return prefixLength > 0 + ? // Strip the configured prefix from the list of valid theme keys + validThemeKeys.map((key) => `--${key.slice(prefixLength + 3)}`) + : validThemeKeys +} + +export function resolveKnownThemeNamespaces(design: DesignSystem): string[] { + return [ + '--breakpoint', + '--color', + '--animate', + '--blur', + '--radius', + '--shadow', + '--inset-shadow', + '--drop-shadow', + '--container', + '--font', + '--font-size', + '--tracking', + '--leading', + '--ease', + ] +} From 85b3e23d997c9257480babb516725f29a96a666d Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 21 Jan 2025 09:32:00 -0500 Subject: [PATCH 06/10] =?UTF-8?q?Make=20sure=20we=20highlight=20`--theme(?= =?UTF-8?q?=E2=80=A6)`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/vscode-tailwindcss/syntaxes/theme-fn.tmLanguage.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vscode-tailwindcss/syntaxes/theme-fn.tmLanguage.json b/packages/vscode-tailwindcss/syntaxes/theme-fn.tmLanguage.json index e8404f64..eab0467f 100644 --- a/packages/vscode-tailwindcss/syntaxes/theme-fn.tmLanguage.json +++ b/packages/vscode-tailwindcss/syntaxes/theme-fn.tmLanguage.json @@ -31,7 +31,7 @@ ] }, { - "begin": "(?i)(? Date: Tue, 21 Jan 2025 09:34:07 -0500 Subject: [PATCH 07/10] Support hovers and validations for `--theme` --- packages/tailwindcss-language-service/src/util/find.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/tailwindcss-language-service/src/util/find.ts b/packages/tailwindcss-language-service/src/util/find.ts index de1d6203..55271c98 100644 --- a/packages/tailwindcss-language-service/src/util/find.ts +++ b/packages/tailwindcss-language-service/src/util/find.ts @@ -350,7 +350,7 @@ export function findHelperFunctionsInRange( ): DocumentHelperFunction[] { const text = getTextWithoutComments(doc, 'css', range) let matches = findAll( - /(?[\s:;/*(){}])(?config|theme)(?\(\s*)(?[^)]*?)\s*\)/g, + /(?[\s:;/*(){}])(?config|theme|--theme)(?\(\s*)(?[^)]*?)\s*\)/g, text, ) @@ -395,8 +395,14 @@ export function findHelperFunctionsInRange( match.groups.helper.length + match.groups.innerPrefix.length + let helper = 'config' + + if (match.groups.helper === 'theme' || match.groups.helper === '--theme') { + helper = 'theme' + } + return { - helper: match.groups.helper === 'theme' ? 'theme' : 'config', + helper, path, ranges: { full: resolveRange( From 8591e6e5aa21c57827a724d288af3061a018b636 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 21 Jan 2025 09:39:29 -0500 Subject: [PATCH 08/10] Add highlighting for `--spacing` and `--alpha` --- .../syntaxes/screen-fn.tmLanguage.json | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/packages/vscode-tailwindcss/syntaxes/screen-fn.tmLanguage.json b/packages/vscode-tailwindcss/syntaxes/screen-fn.tmLanguage.json index bf2f9293..2913d422 100644 --- a/packages/vscode-tailwindcss/syntaxes/screen-fn.tmLanguage.json +++ b/packages/vscode-tailwindcss/syntaxes/screen-fn.tmLanguage.json @@ -32,6 +32,64 @@ "name": "variable.parameter.screen.tailwind" } ] + }, + { + "begin": "(?i)(? Date: Tue, 21 Jan 2025 09:56:57 -0500 Subject: [PATCH 09/10] =?UTF-8?q?Add=20completions=20for=20`=E2=80=94theme?= =?UTF-8?q?(=E2=80=A6)`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/completionProvider.ts | 43 +++++++++++++++---- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/packages/tailwindcss-language-service/src/completionProvider.ts b/packages/tailwindcss-language-service/src/completionProvider.ts index dad2d7b5..0f1fd5dd 100644 --- a/packages/tailwindcss-language-service/src/completionProvider.ts +++ b/packages/tailwindcss-language-service/src/completionProvider.ts @@ -1005,7 +1005,7 @@ function provideCssHelperCompletions( const match = text .substr(0, text.length - 1) // don't include that extra character from earlier - .match(/[\s:;/*(){}](?config|theme)\(\s*['"]?(?[^)'"]*)$/) + .match(/[\s:;/*(){}](?config|theme|--theme)\(\s*['"]?(?[^)'"]*)$/) if (match === null) { return null @@ -1023,6 +1023,39 @@ function provideCssHelperCompletions( return null } + let editRange = { + start: { + line: position.line, + character: position.character, + }, + end: position, + } + + if (state.v4 && match.groups.helper === '--theme') { + // List known theme keys + let validThemeKeys = resolveKnownThemeKeys(state.designSystem) + + let items: CompletionItem[] = validThemeKeys.map((themeKey, index) => { + return { + label: themeKey, + sortText: naturalExpand(index, validThemeKeys.length), + kind: 9, + } + }) + + return withDefaults( + { isIncomplete: false, items }, + { + range: editRange, + data: { + ...(state.completionItemData ?? {}), + _type: 'helper', + }, + }, + state.editor.capabilities.itemDefaults, + ) + } + let base = match.groups.helper === 'config' ? state.config : dlv(state.config, 'theme', {}) let parts = path.split(/([\[\].]+)/) let keys = parts.filter((_, i) => i % 2 === 0) @@ -1056,13 +1089,7 @@ function provideCssHelperCompletions( if (!obj) return null - let editRange = { - start: { - line: position.line, - character: position.character - offset, - }, - end: position, - } + editRange.start.character = position.character - offset return withDefaults( { From 1b737fbe176648c4d50630264062a1d5f4e5b6e6 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 21 Jan 2025 11:31:25 -0500 Subject: [PATCH 10/10] Update changelog --- packages/vscode-tailwindcss/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/vscode-tailwindcss/CHANGELOG.md b/packages/vscode-tailwindcss/CHANGELOG.md index 11d4a23d..1b2d5db4 100644 --- a/packages/vscode-tailwindcss/CHANGELOG.md +++ b/packages/vscode-tailwindcss/CHANGELOG.md @@ -10,6 +10,8 @@ - Don't suggest at-rules when nested that cannot be used in a nested context ([#1127](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1127)) - Make sure completions work when using prefixes in v4 ([#1129](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1129)) - Add support for `@reference` ([#1117](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1117)) +- Add support for `--theme(…)`, `--utility(…)`, and `--modifier(…)` ([#1117](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1117)) +- Add basic completions for `--utility(…)` and `--modifier(…)` ([#1117](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1117)) ## 0.12.18