From cb785063160c63ade8b0821e0b92f1a52bd9a062 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Mon, 4 Nov 2024 15:44:00 -0500 Subject: [PATCH 01/43] Fix lazy language server creation We were booting the LSP no matter what which broke detection when CSS-files were created --- packages/vscode-tailwindcss/src/extension.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vscode-tailwindcss/src/extension.ts b/packages/vscode-tailwindcss/src/extension.ts index 3df1c216..d717d5a5 100755 --- a/packages/vscode-tailwindcss/src/extension.ts +++ b/packages/vscode-tailwindcss/src/extension.ts @@ -609,7 +609,7 @@ export async function activate(context: ExtensionContext) { return } - if (!anyFolderNeedsLanguageServer(Workspace.workspaceFolders ?? [])) { + if (!await anyFolderNeedsLanguageServer(Workspace.workspaceFolders ?? [])) { return } From 5d06852e6cbbc5002de308ef0ebbfc78bafc2a2e Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Mon, 4 Nov 2024 17:09:21 -0500 Subject: [PATCH 02/43] =?UTF-8?q?Don=E2=80=99t=20suggest=20`.d.ts`=20files?= =?UTF-8?q?=20for=20`@config`=20and=20`@plugin`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tests/fixtures/v4/dependencies/file.d.ts | 1 + .../src/metadata/extensions.ts | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) create mode 100644 packages/tailwindcss-language-server/tests/fixtures/v4/dependencies/file.d.ts diff --git a/packages/tailwindcss-language-server/tests/fixtures/v4/dependencies/file.d.ts b/packages/tailwindcss-language-server/tests/fixtures/v4/dependencies/file.d.ts new file mode 100644 index 00000000..39a995a9 --- /dev/null +++ b/packages/tailwindcss-language-server/tests/fixtures/v4/dependencies/file.d.ts @@ -0,0 +1 @@ +export type ColorSpace = 'srgb' | 'display-p3' diff --git a/packages/tailwindcss-language-service/src/metadata/extensions.ts b/packages/tailwindcss-language-service/src/metadata/extensions.ts index dab890d5..15babe94 100644 --- a/packages/tailwindcss-language-service/src/metadata/extensions.ts +++ b/packages/tailwindcss-language-service/src/metadata/extensions.ts @@ -3,9 +3,9 @@ let scriptExtensions = [ 'js', 'cjs', 'mjs', - 'ts', - 'mts', - 'cts', + '(? Date: Mon, 11 Nov 2024 10:43:41 -0500 Subject: [PATCH 03/43] Refactor --- .../src/language/cssServer.ts | 14 +-- .../src/completionProvider.ts | 28 +++--- .../src/completions/file-paths.ts | 40 +++++++++ .../getInvalidTailwindDirectiveDiagnostics.ts | 89 ++++++++++++------- .../src/documentLinksProvider.ts | 30 +++---- .../src/util/v4/design-system.ts | 2 +- 6 files changed, 131 insertions(+), 72 deletions(-) create mode 100644 packages/tailwindcss-language-service/src/completions/file-paths.ts diff --git a/packages/tailwindcss-language-server/src/language/cssServer.ts b/packages/tailwindcss-language-server/src/language/cssServer.ts index 11caad45..1549be67 100644 --- a/packages/tailwindcss-language-server/src/language/cssServer.ts +++ b/packages/tailwindcss-language-server/src/language/cssServer.ts @@ -336,11 +336,7 @@ function replace(delta = 0) { } function createVirtualCssDocument(textDocument: TextDocument): TextDocument { - return TextDocument.create( - textDocument.uri, - textDocument.languageId, - textDocument.version, - textDocument + let content = textDocument .getText() .replace(/@screen(\s+[^{]+){/g, replace(-2)) .replace(/@variants(\s+[^{]+){/g, replace()) @@ -350,7 +346,13 @@ function createVirtualCssDocument(textDocument: TextDocument): TextDocument { /@media(\s+screen\s*\([^)]+\))/g, (_match, screen) => `@media (${MEDIA_MARKER})${' '.repeat(screen.length - 4)}`, ) - .replace(/(?<=\b(?:theme|config)\([^)]*)[.[\]]/g, '_'), + .replace(/(?<=\b(?:theme|config)\([^)]*)[.[\]]/g, '_') + + return TextDocument.create( + textDocument.uri, + textDocument.languageId, + textDocument.version, + content, ) } diff --git a/packages/tailwindcss-language-service/src/completionProvider.ts b/packages/tailwindcss-language-service/src/completionProvider.ts index a8f1f21a..cd2f68cd 100644 --- a/packages/tailwindcss-language-service/src/completionProvider.ts +++ b/packages/tailwindcss-language-service/src/completionProvider.ts @@ -39,6 +39,7 @@ import { import { customClassesIn } from './util/classes' import { IS_SCRIPT_SOURCE, IS_TEMPLATE_SOURCE } from './metadata/extensions' import * as postcss from 'postcss' +import { findFileDirective } from './completions/file-paths' let isUtil = (className) => Array.isArray(className.__info) @@ -1613,27 +1614,28 @@ async function provideFileDirectiveCompletions( return null } - let pattern = state.v4 - ? /@(?config|plugin|source)\s*(?'[^']*|"[^"]*)$/ - : /@(?config)\s*(?'[^']*|"[^"]*)$/ - let text = document.getText({ start: { line: position.line, character: 0 }, end: position }) - let match = text.match(pattern) - if (!match) { - return null + + let fd = await findFileDirective(state, text) + if (!fd) return null + + let { partial, suggest } = fd + + function isAllowedFile(name: string) { + if (suggest === 'script') return IS_SCRIPT_SOURCE.test(name) + + if (suggest === 'source') return IS_TEMPLATE_SOURCE.test(name) + + return false } - let directive = match.groups.directive - let partial = match.groups.partial.slice(1) // remove quote + let valueBeforeLastSlash = partial.substring(0, partial.lastIndexOf('/')) let valueAfterLastSlash = partial.substring(partial.lastIndexOf('/') + 1) let entries = await state.editor.readDirectory(document, valueBeforeLastSlash || '.') - let isAllowedFile = directive === 'source' ? IS_TEMPLATE_SOURCE : IS_SCRIPT_SOURCE - - // Only show directories and JavaScript/TypeScript files entries = entries.filter(([name, type]) => { - return type.isDirectory || isAllowedFile.test(name) + return type.isDirectory || isAllowedFile(name) }) return withDefaults( diff --git a/packages/tailwindcss-language-service/src/completions/file-paths.ts b/packages/tailwindcss-language-service/src/completions/file-paths.ts new file mode 100644 index 00000000..cb81eb1e --- /dev/null +++ b/packages/tailwindcss-language-service/src/completions/file-paths.ts @@ -0,0 +1,40 @@ +import type { State } from '../util/state' + +// @config, @plugin, @source +const PATTERN_CUSTOM_V4 = /@(?config|plugin|source)\s*(?'[^']*|"[^"]*)$/ +const PATTERN_CUSTOM_V3 = /@(?config)\s*(?'[^']*|"[^"]*)$/ + +export type FileDirective = { + directive: string + partial: string + suggest: 'script' | 'source' +} + +export async function findFileDirective(state: State, text: string): Promise { + if (state.v4) { + let match = text.match(PATTERN_CUSTOM_V4) + + if (!match) return null + + let directive = match.groups.directive + 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' + + // If we're looking at @source then it's for a template file + if (directive === 'source') { + suggest = 'source' + } + + return { directive, partial, suggest } + } + + let match = text.match(PATTERN_CUSTOM_V3) + if (!match) return null + + let directive = match.groups.directive + let partial = match.groups.partial.slice(1) // remove leading quote + + return { directive, partial, suggest: 'script' } +} diff --git a/packages/tailwindcss-language-service/src/diagnostics/getInvalidTailwindDirectiveDiagnostics.ts b/packages/tailwindcss-language-service/src/diagnostics/getInvalidTailwindDirectiveDiagnostics.ts index 7325d49e..7c18c1a6 100644 --- a/packages/tailwindcss-language-service/src/diagnostics/getInvalidTailwindDirectiveDiagnostics.ts +++ b/packages/tailwindcss-language-service/src/diagnostics/getInvalidTailwindDirectiveDiagnostics.ts @@ -37,51 +37,23 @@ export function getInvalidTailwindDirectiveDiagnostics( regex = /(?:\s|^)@tailwind\s+(?[^;]+)/g } - let hasVariantsDirective = state.jit && semver.gte(state.version, '2.1.99') - ranges.forEach((range) => { let text = getTextWithoutComments(document, 'css', range) let matches = findAll(regex, text) - let valid = [ - 'utilities', - 'components', - 'screens', - semver.gte(state.version, '1.0.0-beta.1') ? 'base' : 'preflight', - hasVariantsDirective && 'variants', - ].filter(Boolean) + matches.forEach((match) => { + let layerName = match.groups.value - let suggestable = valid + let result = validateLayerName(state, layerName) + if (!result) return - if (hasVariantsDirective) { - // Don't suggest `screens`, because it's deprecated - suggestable = suggestable.filter((value) => value !== 'screens') - } - - matches.forEach((match) => { - if (valid.includes(match.groups.value)) { - return null - } - - let message = `'${match.groups.value}' is not a valid value.` - let suggestions: string[] = [] - - if (match.groups.value === 'preflight') { - suggestions.push('base') - message += ` Did you mean 'base'?` - } else { - let suggestion = closest(match.groups.value, suggestable) - if (suggestion) { - suggestions.push(suggestion) - message += ` Did you mean '${suggestion}'?` - } - } + let { message, suggestions } = result diagnostics.push({ code: DiagnosticKind.InvalidTailwindDirective, range: absoluteRange( { - start: indexToPosition(text, match.index + match[0].length - match.groups.value.length), + start: indexToPosition(text, match.index + match[0].length - layerName.length), end: indexToPosition(text, match.index + match[0].length), }, range, @@ -98,3 +70,52 @@ export function getInvalidTailwindDirectiveDiagnostics( return diagnostics } + +function validateLayerName( + state: State, + layerName: string, +): { message: string; suggestions: string[] } | null { + let valid = ['utilities', 'components', 'screens'] + + if (semver.gte(state.version, '1.0.0-beta.1')) { + valid.push('base') + } else { + valid.push('preflight') + } + + let hasVariantsDirective = state.jit && semver.gte(state.version, '2.1.99') + + if (hasVariantsDirective) { + valid.push('variants') + } + + if (valid.includes(layerName)) { + return null + } + + let suggestable = valid + + if (hasVariantsDirective) { + // Don't suggest `screens`, because it's deprecated + suggestable = suggestable.filter((value) => value !== 'screens') + } + + let message = `'${layerName}' is not a valid value.` + let suggestions: string[] = [] + + if (layerName === 'preflight') { + suggestions.push('base') + message += ` Did you mean 'base'?` + } else { + let suggestion = closest(layerName, suggestable) + if (suggestion) { + suggestions.push(suggestion) + message += ` Did you mean '${suggestion}'?` + } + } + + return { + message, + suggestions, + } +} diff --git a/packages/tailwindcss-language-service/src/documentLinksProvider.ts b/packages/tailwindcss-language-service/src/documentLinksProvider.ts index 3dcc15bb..67a82da3 100644 --- a/packages/tailwindcss-language-service/src/documentLinksProvider.ts +++ b/packages/tailwindcss-language-service/src/documentLinksProvider.ts @@ -7,6 +7,7 @@ import { findAll, indexToPosition } from './util/find' import { getTextWithoutComments } from './util/doc' import { absoluteRange } from './util/absoluteRange' import * as semver from './util/semver' +import { getCssBlocks } from './util/language-blocks' export function getDocumentLinks( state: State, @@ -38,18 +39,10 @@ function getDirectiveLinks( } let links: DocumentLink[] = [] - let ranges: Range[] = [] - if (isCssDoc(state, document)) { - ranges.push(undefined) - } else { - let boundaries = getLanguageBoundaries(state, document) - if (!boundaries) return [] - ranges.push(...boundaries.filter((b) => b.type === 'css').map(({ range }) => range)) - } + for (let block of getCssBlocks(state, document)) { + let text = block.text - for (let range of ranges) { - let text = getTextWithoutComments(document, 'css', range) let matches: RegExpMatchArray[] = [] for (let pattern of patterns) { @@ -57,15 +50,16 @@ function getDirectiveLinks( } for (let match of matches) { + let path = match.groups.path.slice(1, -1) + + let range = { + start: indexToPosition(text, match.index + match[0].length - match.groups.path.length), + end: indexToPosition(text, match.index + match[0].length), + } + links.push({ - target: resolveTarget(match.groups.path.slice(1, -1)), - range: absoluteRange( - { - start: indexToPosition(text, match.index + match[0].length - match.groups.path.length), - end: indexToPosition(text, match.index + match[0].length), - }, - range, - ), + target: resolveTarget(path), + range: absoluteRange(range, block.range), }) } } diff --git a/packages/tailwindcss-language-service/src/util/v4/design-system.ts b/packages/tailwindcss-language-service/src/util/v4/design-system.ts index ca2f9ad2..ee9396a8 100644 --- a/packages/tailwindcss-language-service/src/util/v4/design-system.ts +++ b/packages/tailwindcss-language-service/src/util/v4/design-system.ts @@ -3,7 +3,7 @@ import type { Rule } from './ast' import type { NamedVariant } from './candidate' export interface Theme { - // Prefix didn't exist on + // Prefix didn't exist for earlier Tailwind versions prefix?: string entries(): [string, any][] } From 75cb5d424dce0c4bac152742bad3be2ca4b71c84 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Mon, 4 Nov 2024 17:13:04 -0500 Subject: [PATCH 04/43] =?UTF-8?q?Suggest=20completions=20for=20`source(?= =?UTF-8?q?=E2=80=A6)`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tests/completions/at-config.test.js | 86 +++++++++++++++++++ .../src/completionProvider.ts | 17 ++-- .../src/completions/file-paths.test.ts | 13 +++ .../src/completions/file-paths.ts | 17 +++- 4 files changed, 124 insertions(+), 9 deletions(-) create mode 100644 packages/tailwindcss-language-service/src/completions/file-paths.test.ts diff --git a/packages/tailwindcss-language-server/tests/completions/at-config.test.js b/packages/tailwindcss-language-server/tests/completions/at-config.test.js index b97d1873..15d99ac6 100644 --- a/packages/tailwindcss-language-server/tests/completions/at-config.test.js +++ b/packages/tailwindcss-language-server/tests/completions/at-config.test.js @@ -296,4 +296,90 @@ withFixture('v4/dependencies', (c) => { ], }) }) + + test.concurrent('@import "…" source(…)', async ({ expect }) => { + let result = await completion({ + text: '@import "tailwindcss" source("', + lang: 'css', + position: { + line: 0, + character: 30, + }, + }) + + expect(result).toEqual({ + isIncomplete: false, + items: [ + { + label: 'sub-dir/', + kind: 19, + command: { command: 'editor.action.triggerSuggest', title: '' }, + data: expect.anything(), + textEdit: { + newText: 'sub-dir/', + range: { start: { line: 0, character: 30 }, end: { line: 0, character: 30 } }, + }, + }, + ], + }) + }) + + test.concurrent('@tailwind utilities source(…)', async ({ expect }) => { + let result = await completion({ + text: '@tailwind utilities source("', + lang: 'css', + position: { + line: 0, + character: 28, + }, + }) + + expect(result).toEqual({ + isIncomplete: false, + items: [ + { + label: 'sub-dir/', + kind: 19, + command: { command: 'editor.action.triggerSuggest', title: '' }, + data: expect.anything(), + textEdit: { + newText: 'sub-dir/', + range: { start: { line: 0, character: 28 }, end: { line: 0, character: 28 } }, + }, + }, + ], + }) + }) + + test.concurrent('@import "…" source(…) directory', async ({ expect }) => { + let result = await completion({ + text: '@import "tailwindcss" source("sub-dir/', + lang: 'css', + position: { + line: 0, + character: 38, + }, + }) + + expect(result).toEqual({ + isIncomplete: false, + items: [], + }) + }) + + test.concurrent('@tailwind utilities source(…) directory', async ({ expect }) => { + let result = await completion({ + text: '@tailwind utilities source("sub-dir/', + lang: 'css', + position: { + line: 0, + character: 36, + }, + }) + + expect(result).toEqual({ + isIncomplete: false, + items: [], + }) + }) }) diff --git a/packages/tailwindcss-language-service/src/completionProvider.ts b/packages/tailwindcss-language-service/src/completionProvider.ts index cd2f68cd..5081b50b 100644 --- a/packages/tailwindcss-language-service/src/completionProvider.ts +++ b/packages/tailwindcss-language-service/src/completionProvider.ts @@ -1626,6 +1626,9 @@ async function provideFileDirectiveCompletions( if (suggest === 'source') return IS_TEMPLATE_SOURCE.test(name) + // Files are not allowed but directories are + if (suggest === 'directory') return false + return false } @@ -1638,16 +1641,16 @@ async function provideFileDirectiveCompletions( return type.isDirectory || isAllowedFile(name) }) + let items: CompletionItem[] = entries.map(([name, type]) => ({ + label: type.isDirectory ? name + '/' : name, + kind: type.isDirectory ? 19 : 17, + command: type.isDirectory ? { command: 'editor.action.triggerSuggest', title: '' } : undefined, + })) + return withDefaults( { isIncomplete: false, - items: entries.map(([name, type]) => ({ - label: type.isDirectory ? name + '/' : name, - kind: type.isDirectory ? 19 : 17, - command: type.isDirectory - ? { command: 'editor.action.triggerSuggest', title: '' } - : undefined, - })), + items, }, { data: { diff --git a/packages/tailwindcss-language-service/src/completions/file-paths.test.ts b/packages/tailwindcss-language-service/src/completions/file-paths.test.ts new file mode 100644 index 00000000..4ab9150d --- /dev/null +++ b/packages/tailwindcss-language-service/src/completions/file-paths.test.ts @@ -0,0 +1,13 @@ +import { expect, test } from 'vitest' +import { findFileDirective } from './file-paths' + +let findV3 = (text: string) => findFileDirective({ enabled: true, v4: false }, text) +let findV4 = (text: string) => findFileDirective({ enabled: true, v4: true }, text) + +test('…', async () => { + await expect(findV4('@import "tailwindcss" source("./')).resolves.toEqual({ + directive: 'import', + partial: './', + suggest: 'directory', + }) +}) diff --git a/packages/tailwindcss-language-service/src/completions/file-paths.ts b/packages/tailwindcss-language-service/src/completions/file-paths.ts index cb81eb1e..f9b1898d 100644 --- a/packages/tailwindcss-language-service/src/completions/file-paths.ts +++ b/packages/tailwindcss-language-service/src/completions/file-paths.ts @@ -4,20 +4,27 @@ import type { State } from '../util/state' const PATTERN_CUSTOM_V4 = /@(?config|plugin|source)\s*(?'[^']*|"[^"]*)$/ 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\((?'[^']*|"[^"]*)?$/ + export type FileDirective = { directive: string partial: string - suggest: 'script' | 'source' + suggest: 'script' | 'source' | 'directory' } 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) 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' @@ -27,6 +34,12 @@ export async function findFileDirective(state: State, text: string): Promise Date: Mon, 4 Nov 2024 17:13:45 -0500 Subject: [PATCH 05/43] =?UTF-8?q?Don=E2=80=99t=20show=20syntax=20error=20f?= =?UTF-8?q?or=20`@import=20=E2=80=A6=20source(=E2=80=A6)`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tailwindcss-language-server/src/language/cssServer.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/tailwindcss-language-server/src/language/cssServer.ts b/packages/tailwindcss-language-server/src/language/cssServer.ts index 1549be67..2e9d4f54 100644 --- a/packages/tailwindcss-language-server/src/language/cssServer.ts +++ b/packages/tailwindcss-language-server/src/language/cssServer.ts @@ -346,6 +346,12 @@ function createVirtualCssDocument(textDocument: TextDocument): TextDocument { /@media(\s+screen\s*\([^)]+\))/g, (_match, screen) => `@media (${MEDIA_MARKER})${' '.repeat(screen.length - 4)}`, ) + // Replace `@import "…" source()` with `@import "…"` otherwise we'll + // get warnings about expecting a semi-colon instead of the source function + .replace( + /@import\s*("(?:[^"]+)"|'(?:[^']+)')\s*source\([^)]+\)/g, + (_match, url) => `@import "${url.slice(1, -1)}"`, + ) .replace(/(?<=\b(?:theme|config)\([^)]*)[.[\]]/g, '_') return TextDocument.create( From 636fbb51af4c23ec8dc7cc8d00b8f6989e023004 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 6 Nov 2024 14:28:06 -0500 Subject: [PATCH 06/43] =?UTF-8?q?Fix=20syntax=20highlighting=20for=20`@tai?= =?UTF-8?q?lwind=20utilities=20source(=E2=80=A6)`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../syntaxes/at-rules.tmLanguage.json | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/packages/vscode-tailwindcss/syntaxes/at-rules.tmLanguage.json b/packages/vscode-tailwindcss/syntaxes/at-rules.tmLanguage.json index d50b1007..8d8103cf 100644 --- a/packages/vscode-tailwindcss/syntaxes/at-rules.tmLanguage.json +++ b/packages/vscode-tailwindcss/syntaxes/at-rules.tmLanguage.json @@ -28,6 +28,9 @@ { "include": "source.css#escapes" }, + { + "include": "#source-fn" + }, { "match": "[^\\s;]+?", "name": "variable.parameter.tailwind.tailwind" @@ -446,6 +449,36 @@ "name": "punctuation.terminator.rule.css" } ] + }, + "source-fn": { + "patterns": [ + { + "begin": "(?i)(?:\\s*)(? Date: Thu, 7 Nov 2024 16:30:34 -0500 Subject: [PATCH 07/43] Highlight `@source none` as invalid --- packages/vscode-tailwindcss/syntaxes/at-rules.tmLanguage.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/vscode-tailwindcss/syntaxes/at-rules.tmLanguage.json b/packages/vscode-tailwindcss/syntaxes/at-rules.tmLanguage.json index 8d8103cf..067435a6 100644 --- a/packages/vscode-tailwindcss/syntaxes/at-rules.tmLanguage.json +++ b/packages/vscode-tailwindcss/syntaxes/at-rules.tmLanguage.json @@ -203,6 +203,10 @@ } }, "patterns": [ + { + "match": "none(?=;)", + "name": "invalid.illegal.invalid-source.css" + }, { "include": "source.css#string" } From 8d27fac71b7655070108771b8353c5f695c9d057 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 7 Nov 2024 16:31:27 -0500 Subject: [PATCH 08/43] =?UTF-8?q?Highlight=20`@import=20=E2=80=9C=E2=80=A6?= =?UTF-8?q?=E2=80=9D=20source(=E2=80=A6)`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../syntaxes/at-rules.tmLanguage.json | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/packages/vscode-tailwindcss/syntaxes/at-rules.tmLanguage.json b/packages/vscode-tailwindcss/syntaxes/at-rules.tmLanguage.json index 067435a6..800c3ef6 100644 --- a/packages/vscode-tailwindcss/syntaxes/at-rules.tmLanguage.json +++ b/packages/vscode-tailwindcss/syntaxes/at-rules.tmLanguage.json @@ -4,6 +4,47 @@ "injectionSelector": "L:source.css -comment -source.css.scss", "name": "TailwindCSS", "patterns": [ + { + "begin": "(?i)((@)import)(?:\\s+|$|(?=['\"]|/\\*))", + "beginCaptures": { + "1": { + "name": "keyword.control.at-rule.import.css" + }, + "2": { + "name": "punctuation.definition.keyword.css" + } + }, + "end": ";", + "endCaptures": { + "0": { + "name": "punctuation.terminator.rule.css" + } + }, + "name": "meta.at-rule.import.css", + "patterns": [ + { + "begin": "\\G\\s*(?=/\\*)", + "end": "(?<=\\*/)\\s*", + "patterns": [ + { + "include": "source.css#comment-block" + } + ] + }, + { + "include": "source.css#string" + }, + { + "include": "source.css#url" + }, + { + "include": "#source-fn" + }, + { + "include": "source.css#media-query-list" + } + ] + }, { "begin": "(?i)((@)tailwind)(?=\\s|/\\*|$)", "beginCaptures": { From b93d8da608df8ca01f4708d062ec3f12b71d7fc2 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 7 Nov 2024 20:42:02 -0500 Subject: [PATCH 09/43] =?UTF-8?q?Don=E2=80=99t=20show=20diagnostic=20for?= =?UTF-8?q?=20`@tailwind=20utilities=20source(=E2=80=A6)`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../getInvalidTailwindDirectiveDiagnostics.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/tailwindcss-language-service/src/diagnostics/getInvalidTailwindDirectiveDiagnostics.ts b/packages/tailwindcss-language-service/src/diagnostics/getInvalidTailwindDirectiveDiagnostics.ts index 7c18c1a6..2b901ccf 100644 --- a/packages/tailwindcss-language-service/src/diagnostics/getInvalidTailwindDirectiveDiagnostics.ts +++ b/packages/tailwindcss-language-service/src/diagnostics/getInvalidTailwindDirectiveDiagnostics.ts @@ -75,6 +75,25 @@ function validateLayerName( state: State, layerName: string, ): { message: string; suggestions: string[] } | null { + if (state.v4) { + // `@tailwind utilities` is valid + if (layerName === 'utilities') { + return null + } + + let parts = layerName.split(/\s+/) + + // `@tailwind utilities source(…)` is valid + if (parts[0] === 'utilities' && parts[1]?.startsWith('source(')) { + return null + } + + return { + message: `'${layerName}' is not a valid value.`, + suggestions: [], + } + } + let valid = ['utilities', 'components', 'screens'] if (semver.gte(state.version, '1.0.0-beta.1')) { From 8ccf4d0776003f33c42985e2590bf0357f2ee4ee Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 7 Nov 2024 20:42:45 -0500 Subject: [PATCH 10/43] Add diagnostic for `@tailwind base / preflight` --- .../tests/diagnostics/diagnostics.test.js | 30 +++++++++++++++++++ .../getInvalidTailwindDirectiveDiagnostics.ts | 8 +++++ 2 files changed, 38 insertions(+) diff --git a/packages/tailwindcss-language-server/tests/diagnostics/diagnostics.test.js b/packages/tailwindcss-language-server/tests/diagnostics/diagnostics.test.js index 48cdd4f7..bce076df 100644 --- a/packages/tailwindcss-language-server/tests/diagnostics/diagnostics.test.js +++ b/packages/tailwindcss-language-server/tests/diagnostics/diagnostics.test.js @@ -314,4 +314,34 @@ withFixture('v4/basic', (c) => { }, ], }) + + testMatch('Old Tailwind directives warn when used in a v4 project', { + language: 'css', + code: ` + @tailwind base; + @tailwind preflight; + `, + expected: [ + { + code: 'invalidTailwindDirective', + message: + "'@tailwind base' is no longer available in v4. Use '@import \"tailwindcss/base\"' instead.", + range: { + start: { line: 1, character: 16 }, + end: { line: 1, character: 20 }, + }, + severity: 1, + }, + { + code: 'invalidTailwindDirective', + message: + "'@tailwind preflight' is no longer available in v4. Use '@import \"tailwindcss/base\"' instead.", + range: { + start: { line: 2, character: 16 }, + end: { line: 2, character: 25 }, + }, + severity: 1, + }, + ], + }) }) diff --git a/packages/tailwindcss-language-service/src/diagnostics/getInvalidTailwindDirectiveDiagnostics.ts b/packages/tailwindcss-language-service/src/diagnostics/getInvalidTailwindDirectiveDiagnostics.ts index 2b901ccf..31f2d047 100644 --- a/packages/tailwindcss-language-service/src/diagnostics/getInvalidTailwindDirectiveDiagnostics.ts +++ b/packages/tailwindcss-language-service/src/diagnostics/getInvalidTailwindDirectiveDiagnostics.ts @@ -81,6 +81,14 @@ function validateLayerName( return null } + // `@tailwind base | preflight` do not exist in v4 + if (layerName === 'base' || layerName === 'preflight') { + return { + message: `'@tailwind ${layerName}' is no longer available in v4. Use '@import "tailwindcss/base"' instead.`, + suggestions: [], + } + } + let parts = layerName.split(/\s+/) // `@tailwind utilities source(…)` is valid From c2b2c81578dadeab3e24147f70251ce6950939db Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 7 Nov 2024 20:42:33 -0500 Subject: [PATCH 11/43] Add diagnostic for `@tailwind components / screens / variants` --- .../tests/diagnostics/diagnostics.test.js | 33 +++++++++++++++++++ .../getInvalidTailwindDirectiveDiagnostics.ts | 8 +++++ 2 files changed, 41 insertions(+) diff --git a/packages/tailwindcss-language-server/tests/diagnostics/diagnostics.test.js b/packages/tailwindcss-language-server/tests/diagnostics/diagnostics.test.js index bce076df..7bbaa2d7 100644 --- a/packages/tailwindcss-language-server/tests/diagnostics/diagnostics.test.js +++ b/packages/tailwindcss-language-server/tests/diagnostics/diagnostics.test.js @@ -320,6 +320,9 @@ withFixture('v4/basic', (c) => { code: ` @tailwind base; @tailwind preflight; + @tailwind components; + @tailwind screens; + @tailwind variants; `, expected: [ { @@ -342,6 +345,36 @@ withFixture('v4/basic', (c) => { }, severity: 1, }, + { + code: 'invalidTailwindDirective', + message: + "'@tailwind components' is no longer available in v4. Use '@tailwind utilities' instead.", + range: { + start: { line: 3, character: 16 }, + end: { line: 3, character: 26 }, + }, + severity: 1, + }, + { + code: 'invalidTailwindDirective', + message: + "'@tailwind screens' is no longer available in v4. Use '@tailwind utilities' instead.", + range: { + start: { line: 4, character: 16 }, + end: { line: 4, character: 23 }, + }, + severity: 1, + }, + { + code: 'invalidTailwindDirective', + message: + "'@tailwind variants' is no longer available in v4. Use '@tailwind utilities' instead.", + range: { + start: { line: 5, character: 16 }, + end: { line: 5, character: 24 }, + }, + severity: 1, + }, ], }) }) diff --git a/packages/tailwindcss-language-service/src/diagnostics/getInvalidTailwindDirectiveDiagnostics.ts b/packages/tailwindcss-language-service/src/diagnostics/getInvalidTailwindDirectiveDiagnostics.ts index 31f2d047..71d4e068 100644 --- a/packages/tailwindcss-language-service/src/diagnostics/getInvalidTailwindDirectiveDiagnostics.ts +++ b/packages/tailwindcss-language-service/src/diagnostics/getInvalidTailwindDirectiveDiagnostics.ts @@ -89,6 +89,14 @@ function validateLayerName( } } + // `@tailwind components | screens | variants` do not exist in v4 + if (layerName === 'components' || layerName === 'screens' || layerName === 'variants') { + return { + message: `'@tailwind ${layerName}' is no longer available in v4. Use '@tailwind utilities' instead.`, + suggestions: [], + } + } + let parts = layerName.split(/\s+/) // `@tailwind utilities source(…)` is valid From c52af63cbc77de307af9318fed0fdf0c5743dae5 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Fri, 8 Nov 2024 15:32:31 -0500 Subject: [PATCH 12/43] =?UTF-8?q?Add=20diagnostics=20for=20invalid=20uses?= =?UTF-8?q?=20of=20`source(=E2=80=A6)`=20and=20`@source`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tailwindcss-language-server/src/config.ts | 1 + .../diagnostics/source-diagnostics.test.js | 194 ++++++++++++++++++ .../src/diagnostics/diagnosticsProvider.ts | 5 + .../getInvalidSourceDiagnostics.ts | 185 +++++++++++++++++ .../src/diagnostics/types.ts | 12 ++ .../src/util/language-blocks.ts | 46 +++++ .../src/util/state.ts | 1 + 7 files changed, 444 insertions(+) create mode 100644 packages/tailwindcss-language-server/tests/diagnostics/source-diagnostics.test.js create mode 100644 packages/tailwindcss-language-service/src/diagnostics/getInvalidSourceDiagnostics.ts create mode 100644 packages/tailwindcss-language-service/src/util/language-blocks.ts diff --git a/packages/tailwindcss-language-server/src/config.ts b/packages/tailwindcss-language-server/src/config.ts index 2e417fce..90fb3207 100644 --- a/packages/tailwindcss-language-server/src/config.ts +++ b/packages/tailwindcss-language-server/src/config.ts @@ -27,6 +27,7 @@ function getDefaultSettings(): Settings { invalidVariant: 'error', invalidConfigPath: 'error', invalidTailwindDirective: 'error', + invalidSourceDirective: 'error', recommendedVariantOrder: 'warning', }, showPixelEquivalents: true, diff --git a/packages/tailwindcss-language-server/tests/diagnostics/source-diagnostics.test.js b/packages/tailwindcss-language-server/tests/diagnostics/source-diagnostics.test.js new file mode 100644 index 00000000..1363de81 --- /dev/null +++ b/packages/tailwindcss-language-server/tests/diagnostics/source-diagnostics.test.js @@ -0,0 +1,194 @@ +import { expect, test } from 'vitest' +import { withFixture } from '../common' + +const css = String.raw + +withFixture('v4/basic', (c) => { + function runTest(name, { code, expected, language }) { + test(name, async () => { + let promise = new Promise((resolve) => { + c.onNotification('textDocument/publishDiagnostics', ({ diagnostics }) => { + resolve(diagnostics) + }) + }) + + let doc = await c.openDocument({ text: code, lang: language }) + let diagnostics = await promise + + expected = JSON.parse(JSON.stringify(expected).replaceAll('{{URI}}', doc.uri)) + + expect(diagnostics).toMatchObject(expected) + }) + } + + runTest('Source directives require paths', { + language: 'css', + code: ` + @import 'tailwindcss' source(); + @import 'tailwindcss' source(''); + @import 'tailwindcss' source(""); + @tailwind utilities source(); + @tailwind utilities source(''); + @tailwind utilities source(""); + `, + expected: [ + { + code: 'invalidSourceDirective', + message: 'Need a path for the source directive', + range: { + start: { line: 1, character: 35 }, + end: { line: 1, character: 35 }, + }, + }, + { + code: 'invalidSourceDirective', + message: 'Need a path for the source directive', + range: { + start: { line: 2, character: 35 }, + end: { line: 2, character: 37 }, + }, + }, + { + code: 'invalidSourceDirective', + message: 'Need a path for the source directive', + range: { + start: { line: 3, character: 35 }, + end: { line: 3, character: 37 }, + }, + }, + { + code: 'invalidSourceDirective', + message: 'Need a path for the source directive', + range: { + start: { line: 4, character: 33 }, + end: { line: 4, character: 33 }, + }, + }, + { + code: 'invalidSourceDirective', + message: 'Need a path for the source directive', + range: { + start: { line: 5, character: 33 }, + end: { line: 5, character: 35 }, + }, + }, + { + code: 'invalidSourceDirective', + message: 'Need a path for the source directive', + range: { + start: { line: 6, character: 33 }, + end: { line: 6, character: 35 }, + }, + }, + ], + }) + + runTest('source(none) must not be mispelled', { + language: 'css', + code: ` + @import 'tailwindcss' source(no); + @tailwind utilities source(no); + `, + expected: [ + { + code: 'invalidSourceDirective', + message: '`source(no)` is invalid. Did you mean `source(none)`.', + range: { + start: { line: 1, character: 35 }, + end: { line: 1, character: 37 }, + }, + }, + { + code: 'invalidSourceDirective', + message: '`source(no)` is invalid. Did you mean `source(none)`.', + range: { + start: { line: 2, character: 33 }, + end: { line: 2, character: 35 }, + }, + }, + ], + }) + + runTest('source("…") does not produce diagnostics', { + language: 'css', + code: ` + @import 'tailwindcss' source('../app'); + @tailwind utilities source('../app'); + @import 'tailwindcss' source("../app"); + @tailwind utilities source("../app"); + `, + expected: [], + }) + + runTest('source("…") must not be a glob', { + language: 'css', + code: ` + @import 'tailwindcss' source('../app/**/*.html'); + @import 'tailwindcss' source('../app/index.{html,js}'); + @tailwind utilities source('../app/**/*.html'); + @tailwind utilities source('../app/index.{html,js}'); + `, + expected: [ + { + code: 'invalidSourceDirective', + message: `source('../app/**/*.html') uses a glob but a glob cannot be used here. Use a directory name instead.`, + range: { + start: { line: 1, character: 35 }, + end: { line: 1, character: 53 }, + }, + }, + { + code: 'invalidSourceDirective', + message: `source('../app/index.{html,js}') uses a glob but a glob cannot be used here. Use a directory name instead.`, + range: { + start: { line: 2, character: 35 }, + end: { line: 2, character: 59 }, + }, + }, + { + code: 'invalidSourceDirective', + message: `source('../app/**/*.html') uses a glob but a glob cannot be used here. Use a directory name instead.`, + range: { + start: { line: 3, character: 33 }, + end: { line: 3, character: 51 }, + }, + }, + { + code: 'invalidSourceDirective', + message: `source('../app/index.{html,js}') uses a glob but a glob cannot be used here. Use a directory name instead.`, + range: { + start: { line: 4, character: 33 }, + end: { line: 4, character: 57 }, + }, + }, + ], + }) + + runTest('paths given to source("…") must error when not POSIX', { + language: 'css', + code: String.raw` + @import 'tailwindcss' source('C:\\absolute\\path'); + @import 'tailwindcss' source('C:relative.txt'); + `, + expected: [ + { + code: 'invalidSourceDirective', + message: + 'POSIX-style paths are required with source() but `C:\\absolute\\path` is a Windows-style path.', + range: { + start: { line: 1, character: 35 }, + end: { line: 1, character: 55 }, + }, + }, + { + code: 'invalidSourceDirective', + message: + 'POSIX-style paths are required with source() but `C:relative.txt` is a Windows-style path.', + range: { + start: { line: 2, character: 35 }, + end: { line: 2, character: 51 }, + }, + }, + ], + }) +}) diff --git a/packages/tailwindcss-language-service/src/diagnostics/diagnosticsProvider.ts b/packages/tailwindcss-language-service/src/diagnostics/diagnosticsProvider.ts index 34c03b22..18994526 100644 --- a/packages/tailwindcss-language-service/src/diagnostics/diagnosticsProvider.ts +++ b/packages/tailwindcss-language-service/src/diagnostics/diagnosticsProvider.ts @@ -8,6 +8,7 @@ import { getInvalidVariantDiagnostics } from './getInvalidVariantDiagnostics' import { getInvalidConfigPathDiagnostics } from './getInvalidConfigPathDiagnostics' import { getInvalidTailwindDirectiveDiagnostics } from './getInvalidTailwindDirectiveDiagnostics' import { getRecommendedVariantOrderDiagnostics } from './getRecommendedVariantOrderDiagnostics' +import { getInvalidSourceDiagnostics } from './getInvalidSourceDiagnostics' export async function doValidate( state: State, @@ -19,6 +20,7 @@ export async function doValidate( DiagnosticKind.InvalidVariant, DiagnosticKind.InvalidConfigPath, DiagnosticKind.InvalidTailwindDirective, + DiagnosticKind.InvalidSourceDirective, DiagnosticKind.RecommendedVariantOrder, ], ): Promise { @@ -44,6 +46,9 @@ export async function doValidate( ...(only.includes(DiagnosticKind.InvalidTailwindDirective) ? getInvalidTailwindDirectiveDiagnostics(state, document, settings) : []), + ...(only.includes(DiagnosticKind.InvalidSourceDirective) + ? getInvalidSourceDiagnostics(state, document, settings) + : []), ...(only.includes(DiagnosticKind.RecommendedVariantOrder) ? await getRecommendedVariantOrderDiagnostics(state, document, settings) : []), diff --git a/packages/tailwindcss-language-service/src/diagnostics/getInvalidSourceDiagnostics.ts b/packages/tailwindcss-language-service/src/diagnostics/getInvalidSourceDiagnostics.ts new file mode 100644 index 00000000..4284f46b --- /dev/null +++ b/packages/tailwindcss-language-service/src/diagnostics/getInvalidSourceDiagnostics.ts @@ -0,0 +1,185 @@ +import type { State, Settings } from '../util/state' +import { Diagnostic, type Range } from 'vscode-languageserver' +import { + type InvalidConfigPathDiagnostic, + DiagnosticKind, + InvalidSourceDirectiveDiagnostic, +} from './types' +import { findAll, findHelperFunctionsInDocument, indexToPosition } from '../util/find' +import { stringToPath } from '../util/stringToPath' +import isObject from '../util/isObject' +import { closest, distance } from '../util/closest' +import { combinations } from '../util/combinations' +import dlv from 'dlv' +import type { TextDocument } from 'vscode-languageserver-textdocument' +import type { DesignSystem } from '../util/v4' +import { getLanguageBoundaries } from '../util/getLanguageBoundaries' +import { isCssDoc } from '../util/css' +import { getCssBlocks } from '../util/language-blocks' +import { isSemicolonlessCssLanguage } from '../util/languages' +import { absoluteRange } from '../util/absoluteRange' + +// @import … source('…') +// @tailwind utilities source('…') +const PATTERN_IMPORT_SOURCE = + /(?:\s|^)@(?import)\s*(?'[^']*'|"[^"]*")\s*source\((?'[^']*'?|"[^"]*"?|[a-z]*|\)|;)/dg +const PATTERN_UTIL_SOURCE = + /(?:\s|^)@(?tailwind)\s+(?\S+)\s+source\((?'[^']*'?|"[^"]*"?|[a-z]*|\)|;)/dg + +// @source … +const PATTERN_AT_SOURCE = + /(?:\s|^)@(?source)\s*(?'[^']*'?|"[^"]*"?|[a-z]*|\)|;)/dg + +const HAS_DRIVE_LETTER = /^[A-Z]:/ + +export function getInvalidSourceDiagnostics( + state: State, + document: TextDocument, + settings: Settings, +): InvalidSourceDirectiveDiagnostic[] { + let severity = settings.tailwindCSS.lint.invalidSourceDirective + if (severity === 'ignore') return [] + + let diagnostics: InvalidSourceDirectiveDiagnostic[] = [] + + function add(diag: Omit) { + diagnostics.push({ + code: DiagnosticKind.InvalidSourceDirective, + severity: + severity === 'error' + ? 1 /* DiagnosticSeverity.Error */ + : 2 /* DiagnosticSeverity.Warning */, + ...diag, + }) + } + + for (let block of getCssBlocks(state, document)) { + let text = block.text + + let matches = [ + ...findAll(PATTERN_IMPORT_SOURCE, text), + ...findAll(PATTERN_UTIL_SOURCE, text), + ...findAll(PATTERN_AT_SOURCE, text), + ] + + for (let match of matches) { + let directive = match.groups.directive + let source = match.groups.source?.trim() ?? '' + let rawSource = source + let sourceRange = match.indices.groups.source + let isQuoted = false + + if (source.startsWith("'")) { + source = source.slice(1) + isQuoted = true + } else if (source.startsWith('"')) { + source = source.slice(1) + isQuoted = true + } + + if (source.endsWith("'")) { + source = source.slice(0, -1) + isQuoted = true + } else if (source.endsWith('"')) { + source = source.slice(0, -1) + isQuoted = true + } + + source = source.trim() + + // - `@import "tailwindcss" source()` + // - `@import "tailwindcss" source('')` + // - `@import "tailwindcss" source("")` + + // - `@source ;` + // - `@source '';` + // - `@source "";` + if (source === '' || source === ')' || source === ';') { + let range = { + start: indexToPosition(text, sourceRange[0]), + end: indexToPosition(text, sourceRange[1]), + } + + add({ + message: 'Need a path for the source directive', + range: absoluteRange(range, block.range), + }) + } + + // - `@import "tailwindcss" source(no)` + // - `@tailwind utilities source('')` + else if (directive !== 'source' && source !== 'none' && !isQuoted) { + let range = { + start: indexToPosition(text, sourceRange[0]), + end: indexToPosition(text, sourceRange[1]), + } + + add({ + message: `\`source(${source})\` is invalid. Did you mean \`source(none)\`.`, + range: absoluteRange(range, block.range), + }) + } + + // Detection of Windows-style paths + else if (source.includes('\\\\') || HAS_DRIVE_LETTER.test(source)) { + source = source.replaceAll('\\\\', '\\') + + let range = { + start: indexToPosition(text, sourceRange[0]), + end: indexToPosition(text, sourceRange[1]), + } + + add({ + message: `POSIX-style paths are required with source() but \`${source}\` is a Windows-style path.`, + range: absoluteRange(range, block.range), + }) + } + + // Detection of globs in non-`@source` directives + else if ( + directive !== 'source' && + (source.includes('*') || source.includes('{') || source.includes('}')) + ) { + let range = { + start: indexToPosition(text, sourceRange[0]), + end: indexToPosition(text, sourceRange[1]), + } + + add({ + message: `source(${rawSource}) uses a glob but a glob cannot be used here. Use a directory name instead.`, + range: absoluteRange(range, block.range), + }) + } + + // `@source none` is invalid + else if (directive === 'source' && source === 'none') { + let range = { + start: indexToPosition(text, sourceRange[0]), + end: indexToPosition(text, sourceRange[1]), + } + + add({ + message: + '`@source none;` is not valid. Did you mean to use `source(none)` on an `@import`?', + range: absoluteRange(range, block.range), + }) + } + + // - `@import "tailwindcss" source(no)` + // - `@tailwind utilities source('')` + else if (directive === 'source' && source !== 'none' && !isQuoted) { + let range = { + start: indexToPosition(text, sourceRange[0]), + end: indexToPosition(text, sourceRange[1]), + } + + add({ + message: `\`@source ${rawSource};\` is invalid.`, + range: absoluteRange(range, block.range), + }) + } + } + } + + return diagnostics +} diff --git a/packages/tailwindcss-language-service/src/diagnostics/types.ts b/packages/tailwindcss-language-service/src/diagnostics/types.ts index 115079a2..7cb68a7e 100644 --- a/packages/tailwindcss-language-service/src/diagnostics/types.ts +++ b/packages/tailwindcss-language-service/src/diagnostics/types.ts @@ -8,6 +8,7 @@ export enum DiagnosticKind { InvalidVariant = 'invalidVariant', InvalidConfigPath = 'invalidConfigPath', InvalidTailwindDirective = 'invalidTailwindDirective', + InvalidSourceDirective = 'invalidSourceDirective', RecommendedVariantOrder = 'recommendedVariantOrder', } @@ -78,6 +79,16 @@ export function isInvalidTailwindDirectiveDiagnostic( return diagnostic.code === DiagnosticKind.InvalidTailwindDirective } +export type InvalidSourceDirectiveDiagnostic = Diagnostic & { + code: DiagnosticKind.InvalidSourceDirective +} + +export function isInvalidSourceDirectiveDiagnostic( + diagnostic: AugmentedDiagnostic, +): diagnostic is InvalidSourceDirectiveDiagnostic { + return diagnostic.code === DiagnosticKind.InvalidSourceDirective +} + export type RecommendedVariantOrderDiagnostic = Diagnostic & { code: DiagnosticKind.RecommendedVariantOrder suggestions: string[] @@ -96,4 +107,5 @@ export type AugmentedDiagnostic = | InvalidVariantDiagnostic | InvalidConfigPathDiagnostic | InvalidTailwindDirectiveDiagnostic + | InvalidSourceDirectiveDiagnostic | RecommendedVariantOrderDiagnostic diff --git a/packages/tailwindcss-language-service/src/util/language-blocks.ts b/packages/tailwindcss-language-service/src/util/language-blocks.ts new file mode 100644 index 00000000..cee66a35 --- /dev/null +++ b/packages/tailwindcss-language-service/src/util/language-blocks.ts @@ -0,0 +1,46 @@ +import type { State } from '../util/state' +import { type Range } from 'vscode-languageserver' +import type { TextDocument } from 'vscode-languageserver-textdocument' +import { getLanguageBoundaries } from '../util/getLanguageBoundaries' +import { isCssDoc } from '../util/css' +import { getTextWithoutComments } from './doc' + +export interface LanguageBlock { + document: TextDocument + range: Range | undefined + lang: string + readonly text: string +} + +/** */ +export function* getCssBlocks( + state: State, + document: TextDocument, +): Iterable { + if (isCssDoc(state, document)) { + yield { + document, + range: undefined, + lang: document.languageId, + get text() { + return getTextWithoutComments(document, 'css') + }, + } + } else { + let boundaries = getLanguageBoundaries(state, document) + if (!boundaries) return [] + + for (let boundary of boundaries) { + if (boundary.type !== 'css') continue + + yield { + document, + range: boundary.range, + lang: boundary.lang ?? document.languageId, + get text() { + return getTextWithoutComments(document, 'css', boundary.range) + }, + } + } + } +} diff --git a/packages/tailwindcss-language-service/src/util/state.ts b/packages/tailwindcss-language-service/src/util/state.ts index abe8863c..dd1966ce 100644 --- a/packages/tailwindcss-language-service/src/util/state.ts +++ b/packages/tailwindcss-language-service/src/util/state.ts @@ -59,6 +59,7 @@ export type TailwindCssSettings = { invalidVariant: DiagnosticSeveritySetting invalidConfigPath: DiagnosticSeveritySetting invalidTailwindDirective: DiagnosticSeveritySetting + invalidSourceDirective: DiagnosticSeveritySetting recommendedVariantOrder: DiagnosticSeveritySetting } experimental: { From 9f626f8d131e3802d93aaf5aff39a6bbe01e0f1c Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Fri, 8 Nov 2024 15:49:12 -0500 Subject: [PATCH 13/43] Detect paths in `source()` directives --- .../document-links/document-links.test.js | 18 ++++++++++++++++++ .../src/documentLinksProvider.ts | 2 ++ 2 files changed, 20 insertions(+) diff --git a/packages/tailwindcss-language-server/tests/document-links/document-links.test.js b/packages/tailwindcss-language-server/tests/document-links/document-links.test.js index 595eeec2..39c1d801 100644 --- a/packages/tailwindcss-language-server/tests/document-links/document-links.test.js +++ b/packages/tailwindcss-language-server/tests/document-links/document-links.test.js @@ -130,4 +130,22 @@ withFixture('v4/basic', (c) => { }, ], }) + + testDocumentLinks('Directories in source(…) show links', { + text: ` + @import "tailwindcss" source("../../"); + @tailwind utilities source("../../"); + `, + lang: 'css', + expected: [ + { + target: `file://${path.resolve('./tests/fixtures').replace(/@/g, '%40')}`, + range: { start: { line: 1, character: 35 }, end: { line: 1, character: 43 } }, + }, + { + target: `file://${path.resolve('./tests/fixtures').replace(/@/g, '%40')}`, + range: { start: { line: 2, character: 33 }, end: { line: 2, character: 41 } }, + }, + ], + }) }) diff --git a/packages/tailwindcss-language-service/src/documentLinksProvider.ts b/packages/tailwindcss-language-service/src/documentLinksProvider.ts index 67a82da3..06401059 100644 --- a/packages/tailwindcss-language-service/src/documentLinksProvider.ts +++ b/packages/tailwindcss-language-service/src/documentLinksProvider.ts @@ -22,6 +22,8 @@ export function getDocumentLinks( patterns.push( /@plugin\s*(?'[^']+'|"[^"]+")/g, /@source\s*(?'[^']+'|"[^"]+")/g, + /@import\s*('[^']*'|"[^"]*")\s*source\((?'[^']*'?|"[^"]*"?)/g, + /@tailwind\s*utilities\s*source\((?'[^']*'?|"[^"]*"?)/g, ) } From 78dd58512ec3118fde5ea2b5cac89f03e13e9178 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Fri, 8 Nov 2024 15:49:26 -0500 Subject: [PATCH 14/43] =?UTF-8?q?Don=E2=80=99t=20detect=20document=20links?= =?UTF-8?q?=20for=20glob-style=20paths?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tests/document-links/document-links.test.js | 9 +++++++++ .../src/documentLinksProvider.ts | 5 +++++ 2 files changed, 14 insertions(+) diff --git a/packages/tailwindcss-language-server/tests/document-links/document-links.test.js b/packages/tailwindcss-language-server/tests/document-links/document-links.test.js index 39c1d801..2c272bfb 100644 --- a/packages/tailwindcss-language-server/tests/document-links/document-links.test.js +++ b/packages/tailwindcss-language-server/tests/document-links/document-links.test.js @@ -148,4 +148,13 @@ withFixture('v4/basic', (c) => { }, ], }) + + testDocumentLinks('Globs in source(…) do not show links', { + text: ` + @import "tailwindcss" source("../{a,b,c}"); + @tailwind utilities source("../{a,b,c}"); + `, + lang: 'css', + expected: [], + }) }) diff --git a/packages/tailwindcss-language-service/src/documentLinksProvider.ts b/packages/tailwindcss-language-service/src/documentLinksProvider.ts index 06401059..6397fdb9 100644 --- a/packages/tailwindcss-language-service/src/documentLinksProvider.ts +++ b/packages/tailwindcss-language-service/src/documentLinksProvider.ts @@ -54,6 +54,11 @@ function getDirectiveLinks( for (let match of matches) { let path = match.groups.path.slice(1, -1) + // Ignore glob-like paths + if (path.includes('*') || path.includes('{') || path.includes('}')) { + continue + } + let range = { start: indexToPosition(text, match.index + match[0].length - match.groups.path.length), end: indexToPosition(text, match.index + match[0].length), From f432e120f4a27185936ea7d737d8febd08727158 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Fri, 8 Nov 2024 18:13:11 -0500 Subject: [PATCH 15/43] Show expansion of `@source` globs on hover --- .../tests/hover/hover.test.js | 54 ++++++++++--- .../tailwindcss-language-service/package.json | 1 + .../src/hoverProvider.ts | 76 ++++++++++++++++++- 3 files changed, 116 insertions(+), 15 deletions(-) diff --git a/packages/tailwindcss-language-server/tests/hover/hover.test.js b/packages/tailwindcss-language-server/tests/hover/hover.test.js index ce271963..878fed21 100644 --- a/packages/tailwindcss-language-server/tests/hover/hover.test.js +++ b/packages/tailwindcss-language-server/tests/hover/hover.test.js @@ -157,7 +157,10 @@ withFixture('basic', (c) => { }) withFixture('v4/basic', (c) => { - async function testHover(name, { text, lang, position, expected, expectedRange, settings }) { + async function testHover( + name, + { text, exact = false, lang, position, expected, expectedRange, settings }, + ) { test.concurrent(name, async ({ expect }) => { let textDocument = await c.openDocument({ text, lang, settings }) let res = await c.sendRequest('textDocument/hover', { @@ -165,17 +168,17 @@ withFixture('v4/basic', (c) => { position, }) - expect(res).toEqual( - expected - ? { - contents: { - language: 'css', - value: expected, - }, - range: expectedRange, - } - : expected, - ) + if (!exact && expected) { + expected = { + contents: { + language: 'css', + value: expected, + }, + range: expectedRange, + } + } + + expect(res).toEqual(expected) }) } @@ -242,6 +245,33 @@ withFixture('v4/basic', (c) => { end: { line: 2, character: 18 }, }, }) + + testHover('css @source glob expansion', { + exact: true, + lang: 'css', + text: `@source "../{app,components}/**/*.jsx"`, + position: { line: 0, character: 23 }, + expected: { + contents: { + kind: 'markdown', + value: [ + '**Expansion**', + '```plaintext', + '- ../app/**/*.jsx', + '- ../components/**/*.jsx', + '```', + ].join('\n'), + }, + range: { + start: { line: 0, character: 8 }, + end: { line: 0, character: 38 }, + }, + }, + expectedRange: { + start: { line: 2, character: 9 }, + end: { line: 2, character: 18 }, + }, + }) }) withFixture('v4/css-loading-js', (c) => { diff --git a/packages/tailwindcss-language-service/package.json b/packages/tailwindcss-language-service/package.json index 3c66323d..29034fb0 100644 --- a/packages/tailwindcss-language-service/package.json +++ b/packages/tailwindcss-language-service/package.json @@ -19,6 +19,7 @@ "@types/culori": "^2.1.0", "@types/moo": "0.5.3", "@types/semver": "7.3.10", + "braces": "3.0.3", "color-name": "1.1.4", "css.escape": "1.5.1", "culori": "^4.0.1", diff --git a/packages/tailwindcss-language-service/src/hoverProvider.ts b/packages/tailwindcss-language-service/src/hoverProvider.ts index 1ef981a0..5798b66f 100644 --- a/packages/tailwindcss-language-service/src/hoverProvider.ts +++ b/packages/tailwindcss-language-service/src/hoverProvider.ts @@ -1,9 +1,14 @@ import type { State } from './util/state' -import type { Hover, Position } from 'vscode-languageserver' +import type { Hover, MarkupContent, Position, Range } from 'vscode-languageserver' import { stringifyCss, stringifyConfigValue } from './util/stringify' import dlv from 'dlv' import { isCssContext } from './util/css' -import { findClassNameAtPosition, findHelperFunctionsInRange } from './util/find' +import { + findAll, + findClassNameAtPosition, + findHelperFunctionsInRange, + indexToPosition, +} from './util/find' import { validateApply } from './util/validateApply' import { getClassNameParts } from './util/getClassNameAtPosition' import * as jit from './util/jit' @@ -11,6 +16,9 @@ import { validateConfigPath } from './diagnostics/getInvalidConfigPathDiagnostic import { isWithinRange } from './util/isWithinRange' import type { TextDocument } from 'vscode-languageserver-textdocument' import { addPixelEquivalentsToValue } from './util/pixelEquivalents' +import { getTextWithoutComments } from './util/doc' +import braces from 'braces' +import { absoluteRange } from './util/absoluteRange' export async function doHover( state: State, @@ -19,7 +27,8 @@ export async function doHover( ): Promise { return ( (await provideClassNameHover(state, document, position)) || - (await provideCssHelperHover(state, document, position)) + (await provideCssHelperHover(state, document, position)) || + (await provideSourceGlobHover(state, document, position)) ) } @@ -133,3 +142,64 @@ async function provideClassNameHover( range: className.range, } } + +function markdown(lines: string[]): MarkupContent { + return { + kind: 'markdown', + value: lines.join('\n'), + } +} + +async function provideSourceGlobHover( + state: State, + document: TextDocument, + position: Position, +): Promise { + if (!isCssContext(state, document, position)) { + return null + } + + let range = { + start: { line: position.line, character: 0 }, + end: { line: position.line + 1, character: 0 }, + } + + let text = getTextWithoutComments(document, 'css', range) + + let pattern = /@source\s*(?'[^']+'|"[^"]+")/dg + + for (let match of findAll(pattern, text)) { + let path = match.groups.path.slice(1, -1) + + // Ignore paths that don't need brace expansion + if (!path.includes('{') || !path.includes('}')) continue + + // Ignore paths that don't contain the current position + let slice: Range = absoluteRange( + { + start: indexToPosition(text, match.indices.groups.path[0]), + end: indexToPosition(text, match.indices.groups.path[1]), + }, + range, + ) + + if (!isWithinRange(position, slice)) continue + + // Perform brace expansion + let paths = new Set(braces.expand(path)) + if (paths.size < 2) continue + + return { + range: slice, + contents: markdown([ + // + '**Expansion**', + '```plaintext', + ...Array.from(paths, (path) => `- ${path}`), + '```', + ]), + } + } + + return null +} From 3e8d3d47953a07255707bcc9608496805a945b47 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Fri, 8 Nov 2024 18:13:18 -0500 Subject: [PATCH 16/43] Update lockfile --- pnpm-lock.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 64c07a3b..f0ca5d41 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -227,6 +227,9 @@ importers: '@types/semver': specifier: 7.3.10 version: 7.3.10 + braces: + specifier: 3.0.3 + version: 3.0.3 color-name: specifier: 1.1.4 version: 1.1.4 From b5a5cd711fd650394039619901b19e05d9aa5cfa Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 12 Nov 2024 18:02:37 -0500 Subject: [PATCH 17/43] wip --- .../src/diagnostics/getInvalidSourceDiagnostics.ts | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/packages/tailwindcss-language-service/src/diagnostics/getInvalidSourceDiagnostics.ts b/packages/tailwindcss-language-service/src/diagnostics/getInvalidSourceDiagnostics.ts index 4284f46b..1da01c71 100644 --- a/packages/tailwindcss-language-service/src/diagnostics/getInvalidSourceDiagnostics.ts +++ b/packages/tailwindcss-language-service/src/diagnostics/getInvalidSourceDiagnostics.ts @@ -1,22 +1,11 @@ import type { State, Settings } from '../util/state' -import { Diagnostic, type Range } from 'vscode-languageserver' import { - type InvalidConfigPathDiagnostic, DiagnosticKind, InvalidSourceDirectiveDiagnostic, } from './types' -import { findAll, findHelperFunctionsInDocument, indexToPosition } from '../util/find' -import { stringToPath } from '../util/stringToPath' -import isObject from '../util/isObject' -import { closest, distance } from '../util/closest' -import { combinations } from '../util/combinations' -import dlv from 'dlv' +import { findAll, indexToPosition } from '../util/find' import type { TextDocument } from 'vscode-languageserver-textdocument' -import type { DesignSystem } from '../util/v4' -import { getLanguageBoundaries } from '../util/getLanguageBoundaries' -import { isCssDoc } from '../util/css' import { getCssBlocks } from '../util/language-blocks' -import { isSemicolonlessCssLanguage } from '../util/languages' import { absoluteRange } from '../util/absoluteRange' // @import … source('…') From 40c66d11c6da63569e86145bba6221dbd493a393 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 13 Nov 2024 07:49:31 -0500 Subject: [PATCH 18/43] Update packages/tailwindcss-language-server/tests/diagnostics/source-diagnostics.test.js Co-authored-by: Robin Malfait --- .../tests/diagnostics/source-diagnostics.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tailwindcss-language-server/tests/diagnostics/source-diagnostics.test.js b/packages/tailwindcss-language-server/tests/diagnostics/source-diagnostics.test.js index 1363de81..1c20c621 100644 --- a/packages/tailwindcss-language-server/tests/diagnostics/source-diagnostics.test.js +++ b/packages/tailwindcss-language-server/tests/diagnostics/source-diagnostics.test.js @@ -83,7 +83,7 @@ withFixture('v4/basic', (c) => { ], }) - runTest('source(none) must not be mispelled', { + runTest('source(none) must not be misspelled', { language: 'css', code: ` @import 'tailwindcss' source(no); From 3152177a7d69ba740c8f90684986ea6985860604 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 13 Nov 2024 07:49:47 -0500 Subject: [PATCH 19/43] Update packages/tailwindcss-language-server/tests/diagnostics/source-diagnostics.test.js Co-authored-by: Robin Malfait --- .../tests/diagnostics/source-diagnostics.test.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/tailwindcss-language-server/tests/diagnostics/source-diagnostics.test.js b/packages/tailwindcss-language-server/tests/diagnostics/source-diagnostics.test.js index 1c20c621..ebe30d22 100644 --- a/packages/tailwindcss-language-server/tests/diagnostics/source-diagnostics.test.js +++ b/packages/tailwindcss-language-server/tests/diagnostics/source-diagnostics.test.js @@ -1,8 +1,6 @@ import { expect, test } from 'vitest' import { withFixture } from '../common' -const css = String.raw - withFixture('v4/basic', (c) => { function runTest(name, { code, expected, language }) { test(name, async () => { From 30b18fd4b7cc3fbb96476a40dc28924f10cbbdbc Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 13 Nov 2024 07:56:11 -0500 Subject: [PATCH 20/43] Fix typo --- packages/tailwindcss-language-service/src/hoverProvider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tailwindcss-language-service/src/hoverProvider.ts b/packages/tailwindcss-language-service/src/hoverProvider.ts index 5798b66f..51ef67e3 100644 --- a/packages/tailwindcss-language-service/src/hoverProvider.ts +++ b/packages/tailwindcss-language-service/src/hoverProvider.ts @@ -57,7 +57,7 @@ async function provideCssHelperHover( helperFn.helper === 'theme' ? ['theme'] : [], ) - // This property may not exist in the state object because of compatability with Tailwind Play + // This property may not exist in the state object because of compatibility with Tailwind Play let value = validated.isValid ? stringifyConfigValue(validated.value) : null if (value === null) return null From 565a57e84cd488fe687bb6660dcb6e10f7f8aecf Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 13 Nov 2024 07:56:44 -0500 Subject: [PATCH 21/43] Cleanup code --- .../src/documentLinksProvider.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/tailwindcss-language-service/src/documentLinksProvider.ts b/packages/tailwindcss-language-service/src/documentLinksProvider.ts index 6397fdb9..5545a764 100644 --- a/packages/tailwindcss-language-service/src/documentLinksProvider.ts +++ b/packages/tailwindcss-language-service/src/documentLinksProvider.ts @@ -1,10 +1,7 @@ import type { TextDocument } from 'vscode-languageserver-textdocument' import type { State } from './util/state' import type { DocumentLink, Range } from 'vscode-languageserver' -import { isCssDoc } from './util/css' -import { getLanguageBoundaries } from './util/getLanguageBoundaries' import { findAll, indexToPosition } from './util/find' -import { getTextWithoutComments } from './util/doc' import { absoluteRange } from './util/absoluteRange' import * as semver from './util/semver' import { getCssBlocks } from './util/language-blocks' @@ -14,9 +11,7 @@ export function getDocumentLinks( document: TextDocument, resolveTarget: (linkPath: string) => string, ): DocumentLink[] { - let patterns = [ - /@config\s*(?'[^']+'|"[^"]+")/g, - ] + let patterns = [/@config\s*(?'[^']+'|"[^"]+")/g] if (state.v4) { patterns.push( From 20499c0b464595e1ae290091df2c26f79103b2fa Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 13 Nov 2024 08:16:33 -0500 Subject: [PATCH 22/43] Update tests --- .../src/completions/file-paths.test.ts | 51 +++++++++++++++++-- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/packages/tailwindcss-language-service/src/completions/file-paths.test.ts b/packages/tailwindcss-language-service/src/completions/file-paths.test.ts index 4ab9150d..c79fcbdd 100644 --- a/packages/tailwindcss-language-service/src/completions/file-paths.test.ts +++ b/packages/tailwindcss-language-service/src/completions/file-paths.test.ts @@ -1,13 +1,56 @@ import { expect, test } from 'vitest' import { findFileDirective } from './file-paths' -let findV3 = (text: string) => findFileDirective({ enabled: true, v4: false }, text) -let findV4 = (text: string) => findFileDirective({ enabled: true, v4: true }, text) +test('Detecting v3 directives that point to files', async () => { + function find(text: string) { + return findFileDirective({ enabled: true, v4: false }, text) + } -test('…', async () => { - await expect(findV4('@import "tailwindcss" source("./')).resolves.toEqual({ + await expect(find('@config "./')).resolves.toEqual({ + directive: 'config', + partial: './', + suggest: 'script', + }) + + // The following are not supported in v3 + await expect(find('@plugin "./')).resolves.toEqual(null) + await expect(find('@source "./')).resolves.toEqual(null) + await expect(find('@import "tailwindcss" source("./')).resolves.toEqual(null) + await expect(find('@tailwind utilities source("./')).resolves.toEqual(null) +}) + +test('Detecting v4 directives that point to files', async () => { + function find(text: string) { + return findFileDirective({ enabled: true, v4: true }, text) + } + + await expect(find('@config "./')).resolves.toEqual({ + directive: 'config', + partial: './', + suggest: 'script', + }) + + await expect(find('@plugin "./')).resolves.toEqual({ + directive: 'plugin', + partial: './', + suggest: 'script', + }) + + await expect(find('@source "./')).resolves.toEqual({ + directive: 'source', + partial: './', + suggest: 'source', + }) + + await expect(find('@import "tailwindcss" source("./')).resolves.toEqual({ directive: 'import', partial: './', suggest: 'directory', }) + + await expect(find('@tailwind utilities source("./')).resolves.toEqual({ + directive: 'tailwind', + partial: './', + suggest: 'directory', + }) }) From 8433c8f23822d08944cd1e3a678d0464152a6ede Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 13 Nov 2024 08:25:19 -0500 Subject: [PATCH 23/43] Add suggestion to diagnostic --- .../src/diagnostics/getInvalidTailwindDirectiveDiagnostics.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tailwindcss-language-service/src/diagnostics/getInvalidTailwindDirectiveDiagnostics.ts b/packages/tailwindcss-language-service/src/diagnostics/getInvalidTailwindDirectiveDiagnostics.ts index 71d4e068..7ccfd962 100644 --- a/packages/tailwindcss-language-service/src/diagnostics/getInvalidTailwindDirectiveDiagnostics.ts +++ b/packages/tailwindcss-language-service/src/diagnostics/getInvalidTailwindDirectiveDiagnostics.ts @@ -93,7 +93,7 @@ function validateLayerName( if (layerName === 'components' || layerName === 'screens' || layerName === 'variants') { return { message: `'@tailwind ${layerName}' is no longer available in v4. Use '@tailwind utilities' instead.`, - suggestions: [], + suggestions: ['utilities'], } } From cdd802956f68a6077eb0ba4814b9a0d827392eb2 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 13 Nov 2024 08:27:14 -0500 Subject: [PATCH 24/43] Update tests --- .../tests/diagnostics/diagnostics.test.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/tailwindcss-language-server/tests/diagnostics/diagnostics.test.js b/packages/tailwindcss-language-server/tests/diagnostics/diagnostics.test.js index 7bbaa2d7..96f92fb0 100644 --- a/packages/tailwindcss-language-server/tests/diagnostics/diagnostics.test.js +++ b/packages/tailwindcss-language-server/tests/diagnostics/diagnostics.test.js @@ -329,6 +329,7 @@ withFixture('v4/basic', (c) => { code: 'invalidTailwindDirective', message: "'@tailwind base' is no longer available in v4. Use '@import \"tailwindcss/base\"' instead.", + suggestions: [], range: { start: { line: 1, character: 16 }, end: { line: 1, character: 20 }, @@ -339,6 +340,7 @@ withFixture('v4/basic', (c) => { code: 'invalidTailwindDirective', message: "'@tailwind preflight' is no longer available in v4. Use '@import \"tailwindcss/base\"' instead.", + suggestions: [], range: { start: { line: 2, character: 16 }, end: { line: 2, character: 25 }, @@ -349,6 +351,7 @@ withFixture('v4/basic', (c) => { code: 'invalidTailwindDirective', message: "'@tailwind components' is no longer available in v4. Use '@tailwind utilities' instead.", + suggestions: ['utilities'], range: { start: { line: 3, character: 16 }, end: { line: 3, character: 26 }, @@ -359,6 +362,7 @@ withFixture('v4/basic', (c) => { code: 'invalidTailwindDirective', message: "'@tailwind screens' is no longer available in v4. Use '@tailwind utilities' instead.", + suggestions: ['utilities'], range: { start: { line: 4, character: 16 }, end: { line: 4, character: 23 }, @@ -369,6 +373,7 @@ withFixture('v4/basic', (c) => { code: 'invalidTailwindDirective', message: "'@tailwind variants' is no longer available in v4. Use '@tailwind utilities' instead.", + suggestions: ['utilities'], range: { start: { line: 5, character: 16 }, end: { line: 5, character: 24 }, From 0b384fc55c3d7f1f8ec288266c4d16304096260d Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 13 Nov 2024 08:28:11 -0500 Subject: [PATCH 25/43] Fix punctuation --- .../tests/diagnostics/source-diagnostics.test.js | 4 ++-- .../src/diagnostics/getInvalidSourceDiagnostics.ts | 7 ++----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/tailwindcss-language-server/tests/diagnostics/source-diagnostics.test.js b/packages/tailwindcss-language-server/tests/diagnostics/source-diagnostics.test.js index ebe30d22..9fc70164 100644 --- a/packages/tailwindcss-language-server/tests/diagnostics/source-diagnostics.test.js +++ b/packages/tailwindcss-language-server/tests/diagnostics/source-diagnostics.test.js @@ -90,7 +90,7 @@ withFixture('v4/basic', (c) => { expected: [ { code: 'invalidSourceDirective', - message: '`source(no)` is invalid. Did you mean `source(none)`.', + message: '`source(no)` is invalid. Did you mean `source(none)`?', range: { start: { line: 1, character: 35 }, end: { line: 1, character: 37 }, @@ -98,7 +98,7 @@ withFixture('v4/basic', (c) => { }, { code: 'invalidSourceDirective', - message: '`source(no)` is invalid. Did you mean `source(none)`.', + message: '`source(no)` is invalid. Did you mean `source(none)`?', range: { start: { line: 2, character: 33 }, end: { line: 2, character: 35 }, diff --git a/packages/tailwindcss-language-service/src/diagnostics/getInvalidSourceDiagnostics.ts b/packages/tailwindcss-language-service/src/diagnostics/getInvalidSourceDiagnostics.ts index 1da01c71..29b697e3 100644 --- a/packages/tailwindcss-language-service/src/diagnostics/getInvalidSourceDiagnostics.ts +++ b/packages/tailwindcss-language-service/src/diagnostics/getInvalidSourceDiagnostics.ts @@ -1,8 +1,5 @@ import type { State, Settings } from '../util/state' -import { - DiagnosticKind, - InvalidSourceDirectiveDiagnostic, -} from './types' +import { DiagnosticKind, InvalidSourceDirectiveDiagnostic } from './types' import { findAll, indexToPosition } from '../util/find' import type { TextDocument } from 'vscode-languageserver-textdocument' import { getCssBlocks } from '../util/language-blocks' @@ -104,7 +101,7 @@ export function getInvalidSourceDiagnostics( } add({ - message: `\`source(${source})\` is invalid. Did you mean \`source(none)\`.`, + message: `\`source(${source})\` is invalid. Did you mean \`source(none)\`?`, range: absoluteRange(range, block.range), }) } From 0d1e4731bc059c5d9dab155634595621aa918899 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 13 Nov 2024 08:36:01 -0500 Subject: [PATCH 26/43] Tweak diagnostic messages --- .../diagnostics/source-diagnostics.test.js | 24 +++++++++---------- .../getInvalidSourceDiagnostics.ts | 6 ++--- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/tailwindcss-language-server/tests/diagnostics/source-diagnostics.test.js b/packages/tailwindcss-language-server/tests/diagnostics/source-diagnostics.test.js index 9fc70164..7725c106 100644 --- a/packages/tailwindcss-language-server/tests/diagnostics/source-diagnostics.test.js +++ b/packages/tailwindcss-language-server/tests/diagnostics/source-diagnostics.test.js @@ -32,7 +32,7 @@ withFixture('v4/basic', (c) => { expected: [ { code: 'invalidSourceDirective', - message: 'Need a path for the source directive', + message: 'The source directive requires a path to a directory.', range: { start: { line: 1, character: 35 }, end: { line: 1, character: 35 }, @@ -40,7 +40,7 @@ withFixture('v4/basic', (c) => { }, { code: 'invalidSourceDirective', - message: 'Need a path for the source directive', + message: 'The source directive requires a path to a directory.', range: { start: { line: 2, character: 35 }, end: { line: 2, character: 37 }, @@ -48,7 +48,7 @@ withFixture('v4/basic', (c) => { }, { code: 'invalidSourceDirective', - message: 'Need a path for the source directive', + message: 'The source directive requires a path to a directory.', range: { start: { line: 3, character: 35 }, end: { line: 3, character: 37 }, @@ -56,7 +56,7 @@ withFixture('v4/basic', (c) => { }, { code: 'invalidSourceDirective', - message: 'Need a path for the source directive', + message: 'The source directive requires a path to a directory.', range: { start: { line: 4, character: 33 }, end: { line: 4, character: 33 }, @@ -64,7 +64,7 @@ withFixture('v4/basic', (c) => { }, { code: 'invalidSourceDirective', - message: 'Need a path for the source directive', + message: 'The source directive requires a path to a directory.', range: { start: { line: 5, character: 33 }, end: { line: 5, character: 35 }, @@ -72,7 +72,7 @@ withFixture('v4/basic', (c) => { }, { code: 'invalidSourceDirective', - message: 'Need a path for the source directive', + message: 'The source directive requires a path to a directory.', range: { start: { line: 6, character: 33 }, end: { line: 6, character: 35 }, @@ -129,7 +129,7 @@ withFixture('v4/basic', (c) => { expected: [ { code: 'invalidSourceDirective', - message: `source('../app/**/*.html') uses a glob but a glob cannot be used here. Use a directory name instead.`, + message: `\`source('../app/**/*.html')\` uses a glob but a glob cannot be used here. Use a directory name instead.`, range: { start: { line: 1, character: 35 }, end: { line: 1, character: 53 }, @@ -137,7 +137,7 @@ withFixture('v4/basic', (c) => { }, { code: 'invalidSourceDirective', - message: `source('../app/index.{html,js}') uses a glob but a glob cannot be used here. Use a directory name instead.`, + message: `\`source('../app/index.{html,js}')\` uses a glob but a glob cannot be used here. Use a directory name instead.`, range: { start: { line: 2, character: 35 }, end: { line: 2, character: 59 }, @@ -145,7 +145,7 @@ withFixture('v4/basic', (c) => { }, { code: 'invalidSourceDirective', - message: `source('../app/**/*.html') uses a glob but a glob cannot be used here. Use a directory name instead.`, + message: `\`source('../app/**/*.html')\` uses a glob but a glob cannot be used here. Use a directory name instead.`, range: { start: { line: 3, character: 33 }, end: { line: 3, character: 51 }, @@ -153,7 +153,7 @@ withFixture('v4/basic', (c) => { }, { code: 'invalidSourceDirective', - message: `source('../app/index.{html,js}') uses a glob but a glob cannot be used here. Use a directory name instead.`, + message: `\`source('../app/index.{html,js}')\` uses a glob but a glob cannot be used here. Use a directory name instead.`, range: { start: { line: 4, character: 33 }, end: { line: 4, character: 57 }, @@ -172,7 +172,7 @@ withFixture('v4/basic', (c) => { { code: 'invalidSourceDirective', message: - 'POSIX-style paths are required with source() but `C:\\absolute\\path` is a Windows-style path.', + 'POSIX-style paths are required with `source(…)` but `C:\\absolute\\path` is a Windows-style path.', range: { start: { line: 1, character: 35 }, end: { line: 1, character: 55 }, @@ -181,7 +181,7 @@ withFixture('v4/basic', (c) => { { code: 'invalidSourceDirective', message: - 'POSIX-style paths are required with source() but `C:relative.txt` is a Windows-style path.', + 'POSIX-style paths are required with `source(…)` but `C:relative.txt` is a Windows-style path.', range: { start: { line: 2, character: 35 }, end: { line: 2, character: 51 }, diff --git a/packages/tailwindcss-language-service/src/diagnostics/getInvalidSourceDiagnostics.ts b/packages/tailwindcss-language-service/src/diagnostics/getInvalidSourceDiagnostics.ts index 29b697e3..5c0b5dd2 100644 --- a/packages/tailwindcss-language-service/src/diagnostics/getInvalidSourceDiagnostics.ts +++ b/packages/tailwindcss-language-service/src/diagnostics/getInvalidSourceDiagnostics.ts @@ -87,7 +87,7 @@ export function getInvalidSourceDiagnostics( } add({ - message: 'Need a path for the source directive', + message: 'The source directive requires a path to a directory.', range: absoluteRange(range, block.range), }) } @@ -116,7 +116,7 @@ export function getInvalidSourceDiagnostics( } add({ - message: `POSIX-style paths are required with source() but \`${source}\` is a Windows-style path.`, + message: `POSIX-style paths are required with \`source(…)\` but \`${source}\` is a Windows-style path.`, range: absoluteRange(range, block.range), }) } @@ -132,7 +132,7 @@ export function getInvalidSourceDiagnostics( } add({ - message: `source(${rawSource}) uses a glob but a glob cannot be used here. Use a directory name instead.`, + message: `\`source(${rawSource})\` uses a glob but a glob cannot be used here. Use a directory name instead.`, range: absoluteRange(range, block.range), }) } From 6df9b025072633e0595a40f2f7a963ec62400794 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 14 Nov 2024 09:28:12 -0500 Subject: [PATCH 27/43] Cleanup --- .../tailwindcss-language-service/src/util/language-blocks.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/tailwindcss-language-service/src/util/language-blocks.ts b/packages/tailwindcss-language-service/src/util/language-blocks.ts index cee66a35..10a3fe14 100644 --- a/packages/tailwindcss-language-service/src/util/language-blocks.ts +++ b/packages/tailwindcss-language-service/src/util/language-blocks.ts @@ -12,7 +12,6 @@ export interface LanguageBlock { readonly text: string } -/** */ export function* getCssBlocks( state: State, document: TextDocument, From 2a2b3a25e42bba273c2e0125a081093bbd4dc689 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 14 Nov 2024 09:31:31 -0500 Subject: [PATCH 28/43] =?UTF-8?q?Remove=20source(=E2=80=A6)=20glob=20diagn?= =?UTF-8?q?ostic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is actually valid except for Vite --- .../diagnostics/source-diagnostics.test.js | 44 ------------------- .../getInvalidSourceDiagnostics.ts | 16 ------- 2 files changed, 60 deletions(-) diff --git a/packages/tailwindcss-language-server/tests/diagnostics/source-diagnostics.test.js b/packages/tailwindcss-language-server/tests/diagnostics/source-diagnostics.test.js index 7725c106..f6287b63 100644 --- a/packages/tailwindcss-language-server/tests/diagnostics/source-diagnostics.test.js +++ b/packages/tailwindcss-language-server/tests/diagnostics/source-diagnostics.test.js @@ -118,50 +118,6 @@ withFixture('v4/basic', (c) => { expected: [], }) - runTest('source("…") must not be a glob', { - language: 'css', - code: ` - @import 'tailwindcss' source('../app/**/*.html'); - @import 'tailwindcss' source('../app/index.{html,js}'); - @tailwind utilities source('../app/**/*.html'); - @tailwind utilities source('../app/index.{html,js}'); - `, - expected: [ - { - code: 'invalidSourceDirective', - message: `\`source('../app/**/*.html')\` uses a glob but a glob cannot be used here. Use a directory name instead.`, - range: { - start: { line: 1, character: 35 }, - end: { line: 1, character: 53 }, - }, - }, - { - code: 'invalidSourceDirective', - message: `\`source('../app/index.{html,js}')\` uses a glob but a glob cannot be used here. Use a directory name instead.`, - range: { - start: { line: 2, character: 35 }, - end: { line: 2, character: 59 }, - }, - }, - { - code: 'invalidSourceDirective', - message: `\`source('../app/**/*.html')\` uses a glob but a glob cannot be used here. Use a directory name instead.`, - range: { - start: { line: 3, character: 33 }, - end: { line: 3, character: 51 }, - }, - }, - { - code: 'invalidSourceDirective', - message: `\`source('../app/index.{html,js}')\` uses a glob but a glob cannot be used here. Use a directory name instead.`, - range: { - start: { line: 4, character: 33 }, - end: { line: 4, character: 57 }, - }, - }, - ], - }) - runTest('paths given to source("…") must error when not POSIX', { language: 'css', code: String.raw` diff --git a/packages/tailwindcss-language-service/src/diagnostics/getInvalidSourceDiagnostics.ts b/packages/tailwindcss-language-service/src/diagnostics/getInvalidSourceDiagnostics.ts index 5c0b5dd2..2ab040b2 100644 --- a/packages/tailwindcss-language-service/src/diagnostics/getInvalidSourceDiagnostics.ts +++ b/packages/tailwindcss-language-service/src/diagnostics/getInvalidSourceDiagnostics.ts @@ -121,22 +121,6 @@ export function getInvalidSourceDiagnostics( }) } - // Detection of globs in non-`@source` directives - else if ( - directive !== 'source' && - (source.includes('*') || source.includes('{') || source.includes('}')) - ) { - let range = { - start: indexToPosition(text, sourceRange[0]), - end: indexToPosition(text, sourceRange[1]), - } - - add({ - message: `\`source(${rawSource})\` uses a glob but a glob cannot be used here. Use a directory name instead.`, - range: absoluteRange(range, block.range), - }) - } - // `@source none` is invalid else if (directive === 'source' && source === 'none') { let range = { From c3ad870e8b46a07fafdf0e3a6050b34572619948 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 14 Nov 2024 09:40:07 -0500 Subject: [PATCH 29/43] Tweak path check --- .../src/diagnostics/getInvalidSourceDiagnostics.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tailwindcss-language-service/src/diagnostics/getInvalidSourceDiagnostics.ts b/packages/tailwindcss-language-service/src/diagnostics/getInvalidSourceDiagnostics.ts index 2ab040b2..e8b4ba56 100644 --- a/packages/tailwindcss-language-service/src/diagnostics/getInvalidSourceDiagnostics.ts +++ b/packages/tailwindcss-language-service/src/diagnostics/getInvalidSourceDiagnostics.ts @@ -107,7 +107,7 @@ export function getInvalidSourceDiagnostics( } // Detection of Windows-style paths - else if (source.includes('\\\\') || HAS_DRIVE_LETTER.test(source)) { + else if (source.includes('\\') || HAS_DRIVE_LETTER.test(source)) { source = source.replaceAll('\\\\', '\\') let range = { From c4a660de645eebc1f96060b77077b022cd426842 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 14 Nov 2024 09:40:20 -0500 Subject: [PATCH 30/43] =?UTF-8?q?Don=E2=80=99t=20link=20windows-style=20pa?= =?UTF-8?q?ths?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tests/document-links/document-links.test.js | 15 +++++++++++++++ .../src/documentLinksProvider.ts | 7 +++++++ 2 files changed, 22 insertions(+) diff --git a/packages/tailwindcss-language-server/tests/document-links/document-links.test.js b/packages/tailwindcss-language-server/tests/document-links/document-links.test.js index 2c272bfb..861f74c9 100644 --- a/packages/tailwindcss-language-server/tests/document-links/document-links.test.js +++ b/packages/tailwindcss-language-server/tests/document-links/document-links.test.js @@ -157,4 +157,19 @@ withFixture('v4/basic', (c) => { lang: 'css', expected: [], }) + + testDocumentLinks('Windows paths in source(…) do not show links', { + text: String.raw` + @import "tailwindcss" source("..\foo\bar"); + @tailwind utilities source("..\foo\bar"); + + @import "tailwindcss" source("C:\foo\bar"); + @tailwind utilities source("C:\foo\bar"); + + @import "tailwindcss" source("C:foo"); + @tailwind utilities source("C:bar"); + `, + lang: 'css', + expected: [], + }) }) diff --git a/packages/tailwindcss-language-service/src/documentLinksProvider.ts b/packages/tailwindcss-language-service/src/documentLinksProvider.ts index 5545a764..b18a4711 100644 --- a/packages/tailwindcss-language-service/src/documentLinksProvider.ts +++ b/packages/tailwindcss-language-service/src/documentLinksProvider.ts @@ -6,6 +6,8 @@ import { absoluteRange } from './util/absoluteRange' import * as semver from './util/semver' import { getCssBlocks } from './util/language-blocks' +const HAS_DRIVE_LETTER = /^[A-Z]:/ + export function getDocumentLinks( state: State, document: TextDocument, @@ -54,6 +56,11 @@ function getDirectiveLinks( continue } + // Ignore Windows-style paths + if (path.includes('\\') || HAS_DRIVE_LETTER.test(path)) { + continue + } + let range = { start: indexToPosition(text, match.index + match[0].length - match.groups.path.length), end: indexToPosition(text, match.index + match[0].length), From eef8b1646633798c6c8fd5bdcb9ee48a91e76cc9 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 14 Nov 2024 09:49:06 -0500 Subject: [PATCH 31/43] Fix diagnostic --- .../tests/diagnostics/diagnostics.test.js | 4 ++-- .../src/diagnostics/getInvalidTailwindDirectiveDiagnostics.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/tailwindcss-language-server/tests/diagnostics/diagnostics.test.js b/packages/tailwindcss-language-server/tests/diagnostics/diagnostics.test.js index 96f92fb0..56fe9f7f 100644 --- a/packages/tailwindcss-language-server/tests/diagnostics/diagnostics.test.js +++ b/packages/tailwindcss-language-server/tests/diagnostics/diagnostics.test.js @@ -328,7 +328,7 @@ withFixture('v4/basic', (c) => { { code: 'invalidTailwindDirective', message: - "'@tailwind base' is no longer available in v4. Use '@import \"tailwindcss/base\"' instead.", + "'@tailwind base' is no longer available in v4. Use '@import \"tailwindcss/preflight\"' instead.", suggestions: [], range: { start: { line: 1, character: 16 }, @@ -339,7 +339,7 @@ withFixture('v4/basic', (c) => { { code: 'invalidTailwindDirective', message: - "'@tailwind preflight' is no longer available in v4. Use '@import \"tailwindcss/base\"' instead.", + "'@tailwind preflight' is no longer available in v4. Use '@import \"tailwindcss/preflight\"' instead.", suggestions: [], range: { start: { line: 2, character: 16 }, diff --git a/packages/tailwindcss-language-service/src/diagnostics/getInvalidTailwindDirectiveDiagnostics.ts b/packages/tailwindcss-language-service/src/diagnostics/getInvalidTailwindDirectiveDiagnostics.ts index 7ccfd962..480bb8cd 100644 --- a/packages/tailwindcss-language-service/src/diagnostics/getInvalidTailwindDirectiveDiagnostics.ts +++ b/packages/tailwindcss-language-service/src/diagnostics/getInvalidTailwindDirectiveDiagnostics.ts @@ -84,7 +84,7 @@ function validateLayerName( // `@tailwind base | preflight` do not exist in v4 if (layerName === 'base' || layerName === 'preflight') { return { - message: `'@tailwind ${layerName}' is no longer available in v4. Use '@import "tailwindcss/base"' instead.`, + message: `'@tailwind ${layerName}' is no longer available in v4. Use '@import "tailwindcss/preflight"' instead.`, suggestions: [], } } From c629289360fc72fd2e83870a3540744f8bbb59e6 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 14 Nov 2024 09:49:33 -0500 Subject: [PATCH 32/43] =?UTF-8?q?Don=E2=80=99t=20show=20syntax=20errors=20?= =?UTF-8?q?for=20`@import=20=E2=80=9C=E2=80=A6=E2=80=9D=20theme(=E2=80=A6)?= =?UTF-8?q?`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tailwindcss-language-server/src/language/cssServer.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/tailwindcss-language-server/src/language/cssServer.ts b/packages/tailwindcss-language-server/src/language/cssServer.ts index 2e9d4f54..05340697 100644 --- a/packages/tailwindcss-language-server/src/language/cssServer.ts +++ b/packages/tailwindcss-language-server/src/language/cssServer.ts @@ -352,6 +352,12 @@ function createVirtualCssDocument(textDocument: TextDocument): TextDocument { /@import\s*("(?:[^"]+)"|'(?:[^']+)')\s*source\([^)]+\)/g, (_match, url) => `@import "${url.slice(1, -1)}"`, ) + // Replace `@import "…" theme()` with `@import "…"` otherwise we'll + // get warnings about expecting a semi-colon instead of the theme function + .replace( + /@import\s*("(?:[^"]+)"|'(?:[^']+)')\s*theme\([^)]+\)/g, + (_match, url) => `@import "${url.slice(1, -1)}"`, + ) .replace(/(?<=\b(?:theme|config)\([^)]*)[.[\]]/g, '_') return TextDocument.create( From 2e22ab49e7a1545414d41b9cd821343a6fb48f7d Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 14 Nov 2024 10:01:49 -0500 Subject: [PATCH 33/43] Highlight theme function on import statements --- .../syntaxes/at-rules.tmLanguage.json | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/packages/vscode-tailwindcss/syntaxes/at-rules.tmLanguage.json b/packages/vscode-tailwindcss/syntaxes/at-rules.tmLanguage.json index 800c3ef6..cd740892 100644 --- a/packages/vscode-tailwindcss/syntaxes/at-rules.tmLanguage.json +++ b/packages/vscode-tailwindcss/syntaxes/at-rules.tmLanguage.json @@ -40,6 +40,9 @@ { "include": "#source-fn" }, + { + "include": "#theme-meta-fn" + }, { "include": "source.css#media-query-list" } @@ -524,6 +527,53 @@ ] } ] + }, + "theme-meta-fn": { + "patterns": [ + { + "begin": "(?i)(?:\\s*)(? Date: Thu, 14 Nov 2024 10:06:20 -0500 Subject: [PATCH 34/43] wip --- 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 cd740892..87498a91 100644 --- a/packages/vscode-tailwindcss/syntaxes/at-rules.tmLanguage.json +++ b/packages/vscode-tailwindcss/syntaxes/at-rules.tmLanguage.json @@ -534,7 +534,7 @@ "begin": "(?i)(?:\\s*)(? Date: Thu, 14 Nov 2024 10:07:45 -0500 Subject: [PATCH 35/43] =?UTF-8?q?Don=E2=80=99t=20show=20syntax=20error=20f?= =?UTF-8?q?or=20`@import=20=E2=80=9C=E2=80=A6=E2=80=9D=20prefix(=E2=80=A6)?= =?UTF-8?q?`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tailwindcss-language-server/src/language/cssServer.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/tailwindcss-language-server/src/language/cssServer.ts b/packages/tailwindcss-language-server/src/language/cssServer.ts index 05340697..907432c1 100644 --- a/packages/tailwindcss-language-server/src/language/cssServer.ts +++ b/packages/tailwindcss-language-server/src/language/cssServer.ts @@ -358,6 +358,12 @@ function createVirtualCssDocument(textDocument: TextDocument): TextDocument { /@import\s*("(?:[^"]+)"|'(?:[^']+)')\s*theme\([^)]+\)/g, (_match, url) => `@import "${url.slice(1, -1)}"`, ) + // Replace `@import "…" prefix()` with `@import "…"` otherwise we'll + // get warnings about expecting a semi-colon instead of the theme function + .replace( + /@import\s*("(?:[^"]+)"|'(?:[^']+)')\s*prefix\([^)]+\)/g, + (_match, url) => `@import "${url.slice(1, -1)}"`, + ) .replace(/(?<=\b(?:theme|config)\([^)]*)[.[\]]/g, '_') return TextDocument.create( From 50631a7b9e57cbee2dbceb4c17e9b2ba759b2fb0 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 14 Nov 2024 10:07:57 -0500 Subject: [PATCH 36/43] Highlight prefix function on imports --- .../syntaxes/at-rules.tmLanguage.json | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/packages/vscode-tailwindcss/syntaxes/at-rules.tmLanguage.json b/packages/vscode-tailwindcss/syntaxes/at-rules.tmLanguage.json index 87498a91..92d02bd3 100644 --- a/packages/vscode-tailwindcss/syntaxes/at-rules.tmLanguage.json +++ b/packages/vscode-tailwindcss/syntaxes/at-rules.tmLanguage.json @@ -43,6 +43,9 @@ { "include": "#theme-meta-fn" }, + { + "include": "#prefix-meta-fn" + }, { "include": "source.css#media-query-list" } @@ -574,6 +577,37 @@ ] } ] + }, + "prefix-meta-fn": { + "patterns": [ + { + "begin": "(?i)(?:\\s*)(? Date: Thu, 14 Nov 2024 10:11:32 -0500 Subject: [PATCH 37/43] Simplify regexes --- .../src/language/cssServer.ts | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/packages/tailwindcss-language-server/src/language/cssServer.ts b/packages/tailwindcss-language-server/src/language/cssServer.ts index 907432c1..c33e3ebd 100644 --- a/packages/tailwindcss-language-server/src/language/cssServer.ts +++ b/packages/tailwindcss-language-server/src/language/cssServer.ts @@ -346,22 +346,10 @@ function createVirtualCssDocument(textDocument: TextDocument): TextDocument { /@media(\s+screen\s*\([^)]+\))/g, (_match, screen) => `@media (${MEDIA_MARKER})${' '.repeat(screen.length - 4)}`, ) - // Replace `@import "…" source()` with `@import "…"` otherwise we'll - // get warnings about expecting a semi-colon instead of the source function + // Remove`source(…)`, `theme(…)`, and `prefix(…)` from `@import`s + // otherwise we'll show syntax-error diagnostics which we don't want .replace( - /@import\s*("(?:[^"]+)"|'(?:[^']+)')\s*source\([^)]+\)/g, - (_match, url) => `@import "${url.slice(1, -1)}"`, - ) - // Replace `@import "…" theme()` with `@import "…"` otherwise we'll - // get warnings about expecting a semi-colon instead of the theme function - .replace( - /@import\s*("(?:[^"]+)"|'(?:[^']+)')\s*theme\([^)]+\)/g, - (_match, url) => `@import "${url.slice(1, -1)}"`, - ) - // Replace `@import "…" prefix()` with `@import "…"` otherwise we'll - // get warnings about expecting a semi-colon instead of the theme function - .replace( - /@import\s*("(?:[^"]+)"|'(?:[^']+)')\s*prefix\([^)]+\)/g, + /@import\s*("(?:[^"]+)"|'(?:[^']+)')\s*((source|theme|prefix)\([^)]+\)\s*)+/g, (_match, url) => `@import "${url.slice(1, -1)}"`, ) .replace(/(?<=\b(?:theme|config)\([^)]*)[.[\]]/g, '_') From 997911e1f9d9e70ac104b5b7cd9a04470760e898 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 14 Nov 2024 10:12:16 -0500 Subject: [PATCH 38/43] Ignore theme functions attached to an `@import` We still want to diagnose theme functions in the media query list of an import which we do here. --- .../src/util/find.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/tailwindcss-language-service/src/util/find.ts b/packages/tailwindcss-language-service/src/util/find.ts index 31de45c5..de1d6203 100644 --- a/packages/tailwindcss-language-service/src/util/find.ts +++ b/packages/tailwindcss-language-service/src/util/find.ts @@ -354,6 +354,24 @@ export function findHelperFunctionsInRange( text, ) + // Eliminate matches that are on an `@import` + matches = matches.filter((match) => { + // Scan backwards to see if we're in an `@import` statement + for (let i = match.index - 1; i >= 0; i--) { + let char = text[i] + if (char === '\n') break + if (char === ';') break + // Detecting theme(…) inside the media query list of `@import` is okay + if (char === '(') break + if (char === ')') break + if (text.startsWith('@import', i)) { + return false + } + } + + return true + }) + return matches.map((match) => { let quotesBefore = '' let path = match.groups.path From 5482ae21b088cd3401b0cd3741faea2966828c6c Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 14 Nov 2024 15:04:02 -0500 Subject: [PATCH 39/43] =?UTF-8?q?Provide=20completions=20for=20@theme=20an?= =?UTF-8?q?d=20`@import=20theme(=E2=80=A6)`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/completionProvider.ts | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/packages/tailwindcss-language-service/src/completionProvider.ts b/packages/tailwindcss-language-service/src/completionProvider.ts index 5081b50b..5b380c29 100644 --- a/packages/tailwindcss-language-service/src/completionProvider.ts +++ b/packages/tailwindcss-language-service/src/completionProvider.ts @@ -1600,6 +1600,97 @@ function isInsideAtRule(name: string, document: TextDocument, position: Position return braceLevel(text.slice(block)) > 0 } +// Provide completions for directives that take file paths +const PATTERN_AT_THEME = /@(?theme)\s+(?:(?[^{]+)\s$|$)/ +const PATTERN_IMPORT_THEME = /@(?import)\s*[^;]+?theme\((?[^)]+)?\s$/ + +async function provideThemeDirectiveCompletions( + state: State, + document: TextDocument, + position: Position, +): Promise { + if (!state.v4) return null + + let text = document.getText({ start: { line: position.line, character: 0 }, end: position }) + + let match = text.match(PATTERN_AT_THEME) ?? text.match(PATTERN_IMPORT_THEME) + + console.log({ text, match }) + + // Are we in a context where suggesting theme(…) stuff makes sense? + if (!match) return null + + let directive = match.groups.directive + let parts = new Set( + (match.groups.parts ?? '') + .trim() + .split(/\s+/) + .map((part) => part.trim()) + .filter((part) => part !== ''), + ) + + let items: CompletionItem[] = [ + { + label: 'reference', + documentation: { + kind: 'markdown', + value: + directive === 'import' + ? `Don't emit CSS variables for imported theme values.` + : `Don't emit CSS variables for these theme values.`, + }, + sortText: '-000000', + }, + { + label: 'inline', + documentation: { + kind: 'markdown', + value: + directive === 'import' + ? `Inline imported theme values into generated utilities instead of using \`var(…)\`.` + : `Inline these theme values into generated utilities instead of using \`var(…)\`.`, + }, + sortText: '-000001', + }, + { + label: 'default', + documentation: { + kind: 'markdown', + value: + directive === 'import' + ? `Allow imported theme values to be overriden by JS configs and plugins.` + : `Allow these theme values to be overriden by JS configs and plugins.`, + }, + sortText: '-000003', + }, + ] + + items = items.filter((item) => !parts.has(item.label)) + + if (items.length === 0) return null + + return withDefaults( + { + isIncomplete: false, + items, + }, + { + data: { + ...(state.completionItemData ?? {}), + _type: 'filesystem', + }, + range: { + start: { + line: position.line, + character: position.character, + }, + end: position, + }, + }, + state.editor.capabilities.itemDefaults, + ) +} + // Provide completions for directives that take file paths async function provideFileDirectiveCompletions( state: State, @@ -1756,6 +1847,7 @@ export async function doComplete( const result = (await provideClassNameCompletions(state, document, position, context)) || + (await provideThemeDirectiveCompletions(state, document, position)) || provideCssHelperCompletions(state, document, position) || provideCssDirectiveCompletions(state, document, position) || provideScreenDirectiveCompletions(state, document, position) || From 3b3c202fcb33247120f4e9ff621580a5ad771107 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Fri, 15 Nov 2024 12:56:18 -0500 Subject: [PATCH 40/43] Tweak pattern --- packages/tailwindcss-language-service/src/completionProvider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tailwindcss-language-service/src/completionProvider.ts b/packages/tailwindcss-language-service/src/completionProvider.ts index 5b380c29..f8e69970 100644 --- a/packages/tailwindcss-language-service/src/completionProvider.ts +++ b/packages/tailwindcss-language-service/src/completionProvider.ts @@ -1602,7 +1602,7 @@ function isInsideAtRule(name: string, document: TextDocument, position: Position // Provide completions for directives that take file paths const PATTERN_AT_THEME = /@(?theme)\s+(?:(?[^{]+)\s$|$)/ -const PATTERN_IMPORT_THEME = /@(?import)\s*[^;]+?theme\((?[^)]+)?\s$/ +const PATTERN_IMPORT_THEME = /@(?import)\s*[^;]+?theme\((?:(?[^)]+)\s$|$)/ async function provideThemeDirectiveCompletions( state: State, From 618779bce8e497c2766ae02654ff83eed77335e5 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Fri, 15 Nov 2024 12:56:22 -0500 Subject: [PATCH 41/43] Remove log --- packages/tailwindcss-language-service/src/completionProvider.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/tailwindcss-language-service/src/completionProvider.ts b/packages/tailwindcss-language-service/src/completionProvider.ts index f8e69970..157c4610 100644 --- a/packages/tailwindcss-language-service/src/completionProvider.ts +++ b/packages/tailwindcss-language-service/src/completionProvider.ts @@ -1615,8 +1615,6 @@ async function provideThemeDirectiveCompletions( let match = text.match(PATTERN_AT_THEME) ?? text.match(PATTERN_IMPORT_THEME) - console.log({ text, match }) - // Are we in a context where suggesting theme(…) stuff makes sense? if (!match) return null From 3566b90dc4a25fcc75436224da87bea336d6cf8b Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Fri, 15 Nov 2024 12:56:27 -0500 Subject: [PATCH 42/43] Add tests --- .../tests/completions/completions.test.js | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/packages/tailwindcss-language-server/tests/completions/completions.test.js b/packages/tailwindcss-language-server/tests/completions/completions.test.js index 767a3102..cd678845 100644 --- a/packages/tailwindcss-language-server/tests/completions/completions.test.js +++ b/packages/tailwindcss-language-server/tests/completions/completions.test.js @@ -507,6 +507,40 @@ withFixture('v4/basic', (c) => { ) }) + test.concurrent('@theme suggests options', async ({ expect }) => { + let result = await completion({ + lang: 'css', + text: '@theme ', + position: { line: 0, character: 7 }, + }) + + expect(result.items.length).toBe(3) + expect(result.items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ label: 'reference' }), + expect.objectContaining({ label: 'inline' }), + expect.objectContaining({ label: 'default' }), + ]), + ) + }) + + test.concurrent('@import "…" theme(…) suggests options', async ({ expect }) => { + let result = await completion({ + lang: 'css', + text: '@import "tailwindcss/theme" theme()', + position: { line: 0, character: 34 }, + }) + + expect(result.items.length).toBe(3) + expect(result.items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ label: 'reference' }), + expect.objectContaining({ label: 'inline' }), + expect.objectContaining({ label: 'default' }), + ]), + ) + }) + test.concurrent('resolve', async ({ expect }) => { let result = await completion({ text: '
', From 7a5f00194e566f0083a1b2100c7a874802232e30 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Fri, 15 Nov 2024 13:51:05 -0500 Subject: [PATCH 43/43] Update changelog --- packages/vscode-tailwindcss/CHANGELOG.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/vscode-tailwindcss/CHANGELOG.md b/packages/vscode-tailwindcss/CHANGELOG.md index 665d847e..69321177 100644 --- a/packages/vscode-tailwindcss/CHANGELOG.md +++ b/packages/vscode-tailwindcss/CHANGELOG.md @@ -2,7 +2,21 @@ ## Prerelease -- Nothing yet! +- Add suggestions for theme options ([#1083](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1083)) +- Add suggestions when using `@source "…"` and `source(…)` ([#1083](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1083)) +- Show brace expansion when hovering `@source` ([#1083](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1083)) +- Highlight `source(…)`, `theme(…)`, and `prefix(…)` when used with `@import "…"` ([#1083](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1083)) +- Highlight `@tailwind utilities source(…)` ([#1083](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1083)) +- Show document links when using `source(…)` ([#1083](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1083)) + +- Ensure language server starts as needed ([#1083](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1083)) +- Don't show syntax errors when using `source(…)`, `theme(…)`, or `prefix(…)` with `@import "…"` ([#1083](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1083)) +- Don't show warning when using `@tailwind utilities source(…)` ([#1083](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1083)) +- Don't suggest TypeScript declaration files for `@config`, `@plugin`, and `@source` ([#1083](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1083)) +- Don't link Windows-style paths in `@source`, `@config`, and `@plugin` ([#1083](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1083)) + +- Warn on invalid uses of `source(…)`, `@source`, `@config`, and `@plugin` ([#1083](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1083)) +- Warn when a v4 project uses an old `@tailwind` directive ([#1083](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1083)) ## 0.12.13