diff --git a/packages/tailwindcss-language-server/src/language/cssServer.ts b/packages/tailwindcss-language-server/src/language/cssServer.ts index 731eca3691ec58192c43be4c6d15139baecdf2ec..496a2511de903323408179797d7b4afe763eb72d 100644 --- a/packages/tailwindcss-language-server/src/language/cssServer.ts +++ b/packages/tailwindcss-language-server/src/language/cssServer.ts @@ -162,15 +162,10 @@ item, { ...item, label: 'theme()', - filterText: 'theme', documentation: { kind: 'markdown', value: 'Use the `theme()` function to access your Tailwind config values using dot notation.', - }, - command: { - title: '', - command: 'editor.action.triggerSuggest', }, textEdit: { ...item.textEdit, @@ -362,7 +357,6 @@ .replace( /@media(\s+screen\s*\([^)]+\))/g, (_match, screen) => `@media (${MEDIA_MARKER})${' '.repeat(screen.length - 4)}` ) - .replace(/(?<=\b(?:theme|config)\([^)]*)[.[\]]/g, '_') ) } diff --git a/packages/tailwindcss-language-server/src/server.ts b/packages/tailwindcss-language-server/src/server.ts index a3b6131252b1b5d0b45dbedb2e189db4f6ce5c36..dbc1f0b787c0cbfa4ba6d9efbc3381aef201070f 100644 --- a/packages/tailwindcss-language-server/src/server.ts +++ b/packages/tailwindcss-language-server/src/server.ts @@ -112,7 +112,6 @@ ' ', // @apply and emmet-style '.', // config/theme helper - '(', '[', // JIT "important" prefix '!', diff --git a/packages/tailwindcss-language-service/src/completionProvider.ts b/packages/tailwindcss-language-service/src/completionProvider.ts index 86f9fb70aa71b0bbb1c755fb20e3d66695fe776e..8ac58761c1f1eb7c0b6f2cf6f38cf0eddaef183d 100644 --- a/packages/tailwindcss-language-service/src/completionProvider.ts +++ b/packages/tailwindcss-language-service/src/completionProvider.ts @@ -503,11 +503,6 @@ } ) } -const NUMBER_REGEX = /^(\d+\.?|\d*\.\d+)$/ -function isNumber(str: string): boolean { - return NUMBER_REGEX.test(str) -} - async function provideClassNameCompletions( state: State, document: TextDocument, @@ -542,26 +537,14 @@ }) const match = text .substr(0, text.length - 1) // don't include that extra character from earlier - .match(/\b(?config|theme)\(\s*['"]?(?[^)'"]*)$/) + .match(/\b(?config|theme)\(['"](?[^'"]*)$/) if (match === null) { return null } - let alpha: string - let path = match.groups.path.replace(/^['"]+/g, '') - let matches = path.match(/^([^\s]+)(?![^\[]*\])(?:\s*\/\s*([^\/\s]*))$/) - if (matches) { - path = matches[1] - alpha = matches[2] - } - - if (alpha !== undefined) { - return null - } - let base = match.groups.helper === 'config' ? state.config : dlv(state.config, 'theme', {}) - let parts = path.split(/([\[\].]+)/) + let parts = match.groups.keys.split(/([\[\].]+)/) let keys = parts.filter((_, i) => i % 2 === 0) let separators = parts.filter((_, i) => i % 2 !== 0) // let obj = @@ -574,7 +557,7 @@ return arr.reduce((acc, cur) => acc + cur.length, 0) } let obj: any - let offset: number = keys[keys.length - 1].length + let offset: number = 0 let separator: string = separators.length ? separators[separators.length - 1] : null if (keys.length === 1) { @@ -593,73 +576,41 @@ } if (!obj) return null - let editRange = { - start: { - line: position.line, - character: position.character - offset, - }, - end: position, - } - return { isIncomplete: false, - items: Object.keys(obj) - .sort((a, z) => { - let aIsNumber = isNumber(a) - let zIsNumber = isNumber(z) - if (aIsNumber && !zIsNumber) { - return -1 - } - if (!aIsNumber && zIsNumber) { - return 1 - } - if (aIsNumber && zIsNumber) { - return parseFloat(a) - parseFloat(z) - } - return 0 - }) - .map((item, index) => { - let color = getColorFromValue(obj[item]) - const replaceDot: boolean = item.indexOf('.') !== -1 && separator && separator.endsWith('.') - const insertClosingBrace: boolean = - text.charAt(text.length - 1) !== ']' && - (replaceDot || (separator && separator.endsWith('['))) - const detail = stringifyConfigValue(obj[item]) + items: Object.keys(obj).map((item, index) => { + let color = getColorFromValue(obj[item]) + const replaceDot: boolean = item.indexOf('.') !== -1 && separator && separator.endsWith('.') + const insertClosingBrace: boolean = + text.charAt(text.length - 1) !== ']' && + (replaceDot || (separator && separator.endsWith('['))) + const detail = stringifyConfigValue(obj[item]) - return { - label: item, - sortText: naturalExpand(index), - commitCharacters: [!item.includes('.') && '.', !item.includes('[') && '['].filter( - Boolean - ), - kind: color ? 16 : isObject(obj[item]) ? 9 : 10, - // VS Code bug causes some values to not display in some cases - detail: detail === '0' || detail === 'transparent' ? `${detail} ` : detail, - documentation: - color && typeof color !== 'string' && (color.alpha ?? 1) !== 0 - ? culori.formatRgb(color) - : null, - textEdit: { - newText: `${item}${insertClosingBrace ? ']' : ''}`, - range: editRange, + return { + label: item, + filterText: `${replaceDot ? '.' : ''}${item}`, + sortText: naturalExpand(index), + kind: color ? 16 : isObject(obj[item]) ? 9 : 10, + // VS Code bug causes some values to not display in some cases + detail: detail === '0' || detail === 'transparent' ? `${detail} ` : detail, + documentation: + color && typeof color !== 'string' && (color.alpha ?? 1) !== 0 + ? culori.formatRgb(color) + : null, + textEdit: { + newText: `${replaceDot ? '[' : ''}${item}${insertClosingBrace ? ']' : ''}`, + range: { + start: { + line: position.line, + character: + position.character - keys[keys.length - 1].length - (replaceDot ? 1 : 0) - offset, + }, + end: position, }, - additionalTextEdits: replaceDot - ? [ - { - newText: '[', - range: { - start: { - ...editRange.start, - character: editRange.start.character - 1, - }, - end: editRange.start, - }, - }, - ] - : [], - data: 'helper', - } - }), + }, + data: 'helper', + } + }), } } diff --git a/packages/tailwindcss-language-service/src/diagnostics/getInvalidConfigPathDiagnostics.ts b/packages/tailwindcss-language-service/src/diagnostics/getInvalidConfigPathDiagnostics.ts index 0af368315ee8d9cb145467251a9cd2e403707383..716ce2c8a05086bd55fc13265b0e9284f2591666 100644 --- a/packages/tailwindcss-language-service/src/diagnostics/getInvalidConfigPathDiagnostics.ts +++ b/packages/tailwindcss-language-service/src/diagnostics/getInvalidConfigPathDiagnostics.ts @@ -1,12 +1,16 @@ import { State, Settings } from '../util/state' -import type { TextDocument } from 'vscode-languageserver' +import type { TextDocument, Range, DiagnosticSeverity } from 'vscode-languageserver' import { InvalidConfigPathDiagnostic, DiagnosticKind } from './types' -import { findHelperFunctionsInDocument } from '../util/find' +import { isCssDoc } from '../util/css' +import { getLanguageBoundaries } from '../util/getLanguageBoundaries' +import { findAll, indexToPosition } from '../util/find' import { stringToPath } from '../util/stringToPath' import isObject from '../util/isObject' import { closest } from '../util/closest' +import { absoluteRange } from '../util/absoluteRange' import { combinations } from '../util/combinations' import dlv from 'dlv' +import { getTextWithoutComments } from '../util/doc' function pathToString(path: string | string[]): string { if (typeof path === 'string') return path @@ -163,24 +167,54 @@ let severity = settings.tailwindCSS.lint.invalidConfigPath if (severity === 'ignore') return [] let diagnostics: InvalidConfigPathDiagnostic[] = [] + let ranges: Range[] = [] - findHelperFunctionsInDocument(state, document).forEach((helperFn) => { - let base = helperFn.helper === 'theme' ? ['theme'] : [] - let result = validateConfigPath(state, helperFn.path, base) + 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)) + } - if (result.isValid === true) { - return - } + ranges.forEach((range) => { + let text = getTextWithoutComments(document, 'css', range) + let matches = findAll( + /(?\s|^)(?config|theme)\((?['"])(?[^)]+)\k[^)]*\)/g, + text + ) - diagnostics.push({ - code: DiagnosticKind.InvalidConfigPath, - range: helperFn.ranges.path, - severity: - severity === 'error' - ? 1 /* DiagnosticSeverity.Error */ - : 2 /* DiagnosticSeverity.Warning */, - message: result.reason, - suggestions: result.suggestions, + matches.forEach((match) => { + let base = match.groups.helper === 'theme' ? ['theme'] : [] + let result = validateConfigPath(state, match.groups.key, base) + + if (result.isValid === true) { + return null + } + + let startIndex = + match.index + + match.groups.prefix.length + + match.groups.helper.length + + 1 + // open paren + match.groups.quote.length + + diagnostics.push({ + code: DiagnosticKind.InvalidConfigPath, + range: absoluteRange( + { + start: indexToPosition(text, startIndex), + end: indexToPosition(text, startIndex + match.groups.key.length), + }, + range + ), + severity: + severity === 'error' + ? 1 /* DiagnosticSeverity.Error */ + : 2 /* DiagnosticSeverity.Warning */, + message: result.reason, + suggestions: result.suggestions, + }) }) }) diff --git a/packages/tailwindcss-language-service/src/documentColorProvider.ts b/packages/tailwindcss-language-service/src/documentColorProvider.ts index dac832f646da92365e4ef4edc5844bc0e52f45a7..081d1c0c8086a553d36727711fb0aae744d38ca9 100644 --- a/packages/tailwindcss-language-service/src/documentColorProvider.ts +++ b/packages/tailwindcss-language-service/src/documentColorProvider.ts @@ -36,12 +36,12 @@ }) let helperFns = findHelperFunctionsInDocument(state, document) helperFns.forEach((fn) => { - let keys = stringToPath(fn.path) + let keys = stringToPath(fn.value) let base = fn.helper === 'theme' ? ['theme'] : [] let value = dlv(state.config, [...base, ...keys]) let color = getColorFromValue(value) if (color && typeof color !== 'string' && (color.alpha ?? 1) !== 0) { - colors.push({ range: fn.ranges.path, color: culoriColorToVscodeColor(color) }) + colors.push({ range: fn.valueRange, color: culoriColorToVscodeColor(color) }) } }) diff --git a/packages/tailwindcss-language-service/src/hoverProvider.ts b/packages/tailwindcss-language-service/src/hoverProvider.ts index f506bb27415453404d2b078e434866ebb9f47016..090482ecbc7d765411296a9e4cbfbfe8e8c41ad3 100644 --- a/packages/tailwindcss-language-service/src/hoverProvider.ts +++ b/packages/tailwindcss-language-service/src/hoverProvider.ts @@ -3,12 +3,12 @@ import type { Hover, TextDocument, Position } 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 { findClassNameAtPosition } from './util/find' import { validateApply } from './util/validateApply' import { getClassNameParts } from './util/getClassNameAtPosition' import * as jit from './util/jit' import { validateConfigPath } from './diagnostics/getInvalidConfigPathDiagnostics' -import { isWithinRange } from './util/isWithinRange' +import { getTextWithoutComments } from './util/doc' export async function doHover( state: State, @@ -22,34 +22,49 @@ ) } function provideCssHelperHover(state: State, document: TextDocument, position: Position): Hover { - if (!isCssContext(state, document, position)) { + if (!isCssContext(state, document, position)) return null + + const line = getTextWithoutComments(document, 'css').split('\n')[position.line] + + const match = line.match(/(?theme|config)\((?['"])(?[^)]+)\k[^)]*\)/) + + if (match === null) return null + + const startChar = match.index + match.groups.helper.length + 2 + const endChar = startChar + match.groups.key.length + + if (position.character < startChar || position.character >= endChar) { return null } - let helperFns = findHelperFunctionsInRange(document, { - start: { line: position.line, character: 0 }, - end: { line: position.line + 1, character: 0 }, - }) + let key = match.groups.key + .split(/(\[[^\]]+\]|\.)/) + .filter(Boolean) + .filter((x) => x !== '.') + .map((x) => x.replace(/^\[([^\]]+)\]$/, '$1')) - for (let helperFn of helperFns) { - if (isWithinRange(position, helperFn.ranges.path)) { - let validated = validateConfigPath( - state, - helperFn.path, - helperFn.helper === 'theme' ? ['theme'] : [] - ) - let value = validated.isValid ? stringifyConfigValue(validated.value) : null - if (value === null) { - return null - } - return { - contents: { kind: 'markdown', value: ['```plaintext', value, '```'].join('\n') }, - range: helperFn.ranges.path, - } - } + if (key.length === 0) return null + + if (match.groups.helper === 'theme') { + key = ['theme', ...key] } - return null + const value = validateConfigPath(state, key).isValid + ? stringifyConfigValue(dlv(state.config, key)) + : null + + if (value === null) return null + + return { + contents: { kind: 'markdown', value: ['```plaintext', value, '```'].join('\n') }, + range: { + start: { line: position.line, character: startChar }, + end: { + line: position.line, + character: endChar, + }, + }, + } } async function provideClassNameHover( diff --git a/packages/tailwindcss-language-service/src/util/find.ts b/packages/tailwindcss-language-service/src/util/find.ts index edcb59529869ed0bbdcf4f559a5fb160300f3e71..4e851158c1020139e6cc73d0214cc21127e1fe02 100644 --- a/packages/tailwindcss-language-service/src/util/find.ts +++ b/packages/tailwindcss-language-service/src/util/find.ts @@ -359,48 +359,36 @@ doc: TextDocument, range?: Range ): DocumentHelperFunction[] { const text = getTextWithoutComments(doc, 'css', range) - let matches = findAll( - /(?\s|^)(?config|theme)(?\(\s*)(?[^)]*?)\s*\)/g, + const matches = findAll( + /(?^|\s)(?theme|config)\((?:(?')([^']+)'|(?")([^"]+)")[^)]*\)/gm, text ) return matches.map((match) => { - let quotesBefore = '' - let path = match.groups.path.replace(/['"]+$/, '').replace(/^['"]+/, (m) => { - quotesBefore = m - return '' - }) - let matches = path.match(/^([^\s]+)(?![^\[]*\])(?:\s*\/\s*([^\/\s]+))$/) - if (matches) { - path = matches[1] - } - path = path.replace(/['"]*\s*$/, '') - - let startIndex = - match.index + - match.groups.prefix.length + - match.groups.helper.length + - match.groups.innerPrefix.length - + let value = match[4] || match[6] + let startIndex = match.index + match.groups.before.length return { + full: match[0].substr(match.groups.before.length), + value, helper: match.groups.helper === 'theme' ? 'theme' : 'config', - path, - ranges: { - full: resolveRange( - { - start: indexToPosition(text, startIndex), - end: indexToPosition(text, startIndex + match.groups.path.length), - }, - range - ), - path: resolveRange( - { - start: indexToPosition(text, startIndex + quotesBefore.length), - end: indexToPosition(text, startIndex + quotesBefore.length + path.length), - }, - range - ), - }, + quotes: match.groups.single ? "'" : '"', + range: resolveRange( + { + start: indexToPosition(text, startIndex), + end: indexToPosition(text, match.index + match[0].length), + }, + range + ), + valueRange: resolveRange( + { + start: indexToPosition(text, startIndex + match.groups.helper.length + 1), + end: indexToPosition( + text, + startIndex + match.groups.helper.length + 1 + 1 + value.length + 1 + ), + }, + range + ), } }) } diff --git a/packages/tailwindcss-language-service/src/util/state.ts b/packages/tailwindcss-language-service/src/util/state.ts index 31946432adf25beb59b436dd8ebc3420f88c5c51..d699ffb6c83f4b694966ceb8444c872c383a283e 100644 --- a/packages/tailwindcss-language-service/src/util/state.ts +++ b/packages/tailwindcss-language-service/src/util/state.ts @@ -124,12 +124,12 @@ classList: DocumentClassList } export type DocumentHelperFunction = { + full: string helper: 'theme' | 'config' - path: string - ranges: { - full: Range - path: Range - } + value: string + quotes: '"' | "'" + range: Range + valueRange: Range } export type ClassNameMeta = { diff --git a/packages/vscode-tailwindcss/src/extension.ts b/packages/vscode-tailwindcss/src/extension.ts index 4b2bdfb03c7c526ba31f67193e9c573fdec095d8..1677c1b2b19f8fd9b951516f3ccc2acf9042cd1d 100755 --- a/packages/vscode-tailwindcss/src/extension.ts +++ b/packages/vscode-tailwindcss/src/extension.ts @@ -378,11 +378,7 @@ middleware: { async resolveCompletionItem(item, token, next) { let result = await next(item, token) let selections = Window.activeTextEditor.selections - if ( - result['data'] === 'variant' && - selections.length > 1 && - result.additionalTextEdits?.length > 0 - ) { + if (selections.length > 1 && result.additionalTextEdits?.length > 0) { let length = selections[0].start.character - result.additionalTextEdits[0].range.start.character let prefixLength =