diff --git a/packages/tailwindcss-language-server/tests/diagnostics/css-conflict/vue-style-lang-sass.json b/packages/tailwindcss-language-server/tests/diagnostics/css-conflict/vue-style-lang-sass.json new file mode 100644 index 00000000..7e9da86b --- /dev/null +++ b/packages/tailwindcss-language-server/tests/diagnostics/css-conflict/vue-style-lang-sass.json @@ -0,0 +1,116 @@ +{ + "code": "", + "language": "vue", + "expected": [ + { + "code": "cssConflict", + "className": { + "className": "uppercase", + "classList": { + "classList": "uppercase lowercase", + "range": { + "start": { "line": 2, "character": 9 }, + "end": { "line": 2, "character": 28 } + }, + "important": false + }, + "relativeRange": { + "start": { "line": 0, "character": 0 }, + "end": { "line": 0, "character": 9 } + }, + "range": { "start": { "line": 2, "character": 9 }, "end": { "line": 2, "character": 18 } } + }, + "otherClassNames": [ + { + "className": "lowercase", + "classList": { + "classList": "uppercase lowercase", + "range": { + "start": { "line": 2, "character": 9 }, + "end": { "line": 2, "character": 28 } + }, + "important": false + }, + "relativeRange": { + "start": { "line": 0, "character": 10 }, + "end": { "line": 0, "character": 19 } + }, + "range": { + "start": { "line": 2, "character": 19 }, + "end": { "line": 2, "character": 28 } + } + } + ], + "range": { "start": { "line": 2, "character": 9 }, "end": { "line": 2, "character": 18 } }, + "severity": 2, + "message": "'uppercase' applies the same CSS properties as 'lowercase'.", + "relatedInformation": [ + { + "message": "lowercase", + "location": { + "uri": "{{URI}}", + "range": { + "start": { "line": 2, "character": 19 }, + "end": { "line": 2, "character": 28 } + } + } + } + ] + }, + { + "code": "cssConflict", + "className": { + "className": "lowercase", + "classList": { + "classList": "uppercase lowercase", + "range": { + "start": { "line": 2, "character": 9 }, + "end": { "line": 2, "character": 28 } + }, + "important": false + }, + "relativeRange": { + "start": { "line": 0, "character": 10 }, + "end": { "line": 0, "character": 19 } + }, + "range": { "start": { "line": 2, "character": 19 }, "end": { "line": 2, "character": 28 } } + }, + "otherClassNames": [ + { + "className": "uppercase", + "classList": { + "classList": "uppercase lowercase", + "range": { + "start": { "line": 2, "character": 9 }, + "end": { "line": 2, "character": 28 } + }, + "important": false + }, + "relativeRange": { + "start": { "line": 0, "character": 0 }, + "end": { "line": 0, "character": 9 } + }, + "range": { + "start": { "line": 2, "character": 9 }, + "end": { "line": 2, "character": 18 } + } + } + ], + "range": { "start": { "line": 2, "character": 19 }, "end": { "line": 2, "character": 28 } }, + "severity": 2, + "message": "'lowercase' applies the same CSS properties as 'uppercase'.", + "relatedInformation": [ + { + "message": "uppercase", + "location": { + "uri": "{{URI}}", + "range": { + "start": { "line": 2, "character": 9 }, + "end": { "line": 2, "character": 18 } + } + } + } + ] + } + ] +} diff --git a/packages/tailwindcss-language-server/tests/diagnostics/diagnostics.test.js b/packages/tailwindcss-language-server/tests/diagnostics/diagnostics.test.js index 4da6946d..4a37ee2d 100644 --- a/packages/tailwindcss-language-server/tests/diagnostics/diagnostics.test.js +++ b/packages/tailwindcss-language-server/tests/diagnostics/diagnostics.test.js @@ -32,6 +32,7 @@ withFixture('basic', (c) => { testFixture('css-conflict/css') testFixture('css-conflict/css-multi-rule') testFixture('css-conflict/css-multi-prop') + testFixture('css-conflict/vue-style-lang-sass') testFixture('invalid-screen/simple') testFixture('invalid-theme/simple') }) @@ -63,6 +64,7 @@ withFixture('v4/basic', (c) => { testFixture('css-conflict/variants-positive') testFixture('css-conflict/jsx-concat-negative') testFixture('css-conflict/jsx-concat-positive') + testFixture('css-conflict/vue-style-lang-sass') // testFixture('css-conflict/css') // testFixture('css-conflict/css-multi-rule') // testFixture('css-conflict/css-multi-prop') diff --git a/packages/tailwindcss-language-server/tests/hover/hover.test.js b/packages/tailwindcss-language-server/tests/hover/hover.test.js index 544ea1e3..f1b847c3 100644 --- a/packages/tailwindcss-language-server/tests/hover/hover.test.js +++ b/packages/tailwindcss-language-server/tests/hover/hover.test.js @@ -75,6 +75,20 @@ withFixture('basic', (c) => { end: { line: 0, character: 31 }, }, }) + + testHover('vue `, + position: { line: 2, character: 13 }, + expected: '.underline {\n' + ' text-decoration-line: underline;\n' + '}', + expectedRange: { + start: { line: 2, character: 9 }, + end: { line: 2, character: 18 }, + }, + }) }) withFixture('v4/basic', (c) => { @@ -148,4 +162,18 @@ withFixture('v4/basic', (c) => { end: { line: 0, character: 31 }, }, }) + + testHover('vue `, + position: { line: 2, character: 13 }, + expected: '.underline {\n' + ' text-decoration-line: underline;\n' + '}', + expectedRange: { + start: { line: 2, character: 9 }, + end: { line: 2, character: 18 }, + }, + }) }) diff --git a/packages/tailwindcss-language-service/src/util/css.ts b/packages/tailwindcss-language-service/src/util/css.ts index 14950397..079e4221 100644 --- a/packages/tailwindcss-language-service/src/util/css.ts +++ b/packages/tailwindcss-language-service/src/util/css.ts @@ -6,12 +6,21 @@ import type { State } from './state' import { cssLanguages } from './languages' import { getLanguageBoundaries } from './getLanguageBoundaries' -export function isCssDoc(state: State, doc: TextDocument): boolean { - const userCssLanguages = Object.keys(state.editor.userLanguages).filter((lang) => - cssLanguages.includes(state.editor.userLanguages[lang]), - ) +function getCssLanguages(state: State) { + const userCssLanguages = Object + .keys(state.editor.userLanguages) + .filter((lang) => cssLanguages.includes(state.editor.userLanguages[lang])) + + return [...cssLanguages, ...userCssLanguages] +} - return [...cssLanguages, ...userCssLanguages].indexOf(doc.languageId) !== -1 +export function isCssLanguage(state: State, lang: string) { + return getCssLanguages(state).indexOf(lang) !== -1 +} + + +export function isCssDoc(state: State, doc: TextDocument): boolean { + return isCssLanguage(state, doc.languageId) } export function isCssContext(state: State, doc: TextDocument, position: Position): boolean { diff --git a/packages/tailwindcss-language-service/src/util/find.ts b/packages/tailwindcss-language-service/src/util/find.ts index 94fae47d..fead1bd7 100644 --- a/packages/tailwindcss-language-service/src/util/find.ts +++ b/packages/tailwindcss-language-service/src/util/find.ts @@ -3,7 +3,7 @@ import type { TextDocument } from 'vscode-languageserver-textdocument' import type { DocumentClassName, DocumentClassList, State, DocumentHelperFunction } from './state' import lineColumn from 'line-column' import { isCssContext, isCssDoc } from './css' -import { isHtmlContext } from './html' +import { isHtmlContext, isVueDoc } from './html' import { isWithinRange } from './isWithinRange' import { isJsxContext } from './js' import { dedupeByRange, flatten } from './array' @@ -97,9 +97,10 @@ export function findClassListsInCssRange( state: State, doc: TextDocument, range?: Range, + lang?: string, ): DocumentClassList[] { const text = getTextWithoutComments(doc, 'css', range) - let regex = isSemicolonlessCssLanguage(doc.languageId, state.editor?.userLanguages) + let regex = isSemicolonlessCssLanguage(lang ?? doc.languageId, state.editor?.userLanguages) ? /(@apply\s+)(?[^}\r\n]+?)(?\s*!important)?(?:\r|\n|}|$)/g : /(@apply\s+)(?[^;}]+?)(?\s*!important)?\s*[;}]/g const matches = findAll(regex, text) @@ -302,7 +303,7 @@ export async function findClassListsInDocument( )), ...boundaries .filter((b) => b.type === 'css') - .map(({ range }) => findClassListsInCssRange(state, doc, range)), + .map(({ range, lang }) => findClassListsInCssRange(state, doc, range, lang)), await findCustomClassLists(state, doc), ]), ) @@ -408,14 +409,36 @@ export async function findClassNameAtPosition( doc: TextDocument, position: Position, ): Promise { - let classNames = [] + let classNames: DocumentClassName[] = [] const positionOffset = doc.offsetAt(position) const searchRange: Range = { start: doc.positionAt(Math.max(0, positionOffset - 2000)), end: doc.positionAt(positionOffset + 2000), } - if (isCssContext(state, doc, position)) { + if (isVueDoc(doc)) { + let boundaries = getLanguageBoundaries(state, doc) + + let groups = await Promise.all(boundaries.map(async ({ type, range, lang }) => { + if (type === 'css') { + return findClassListsInCssRange(state, doc, range, lang) + } + + if (type === 'html') { + return await findClassListsInHtmlRange(state, doc, 'html', range) + } + + if (type === 'jsx') { + return await findClassListsInHtmlRange(state, doc, 'jsx', range) + } + + return [] + })) + + classNames = dedupeByRange(flatten(groups)).flatMap( + (classList) => getClassNamesInClassList(classList, state.blocklist) + ) + } else if (isCssContext(state, doc, position)) { classNames = await findClassNamesInRange(state, doc, searchRange, 'css') } else if (isHtmlContext(state, doc, position)) { classNames = await findClassNamesInRange(state, doc, searchRange, 'html') diff --git a/packages/tailwindcss-language-service/src/util/getLanguageBoundaries.ts b/packages/tailwindcss-language-service/src/util/getLanguageBoundaries.ts index 83f59309..7630b652 100644 --- a/packages/tailwindcss-language-service/src/util/getLanguageBoundaries.ts +++ b/packages/tailwindcss-language-service/src/util/getLanguageBoundaries.ts @@ -7,8 +7,13 @@ import { isJsDoc } from './js' import moo from 'moo' import Cache from 'tmp-cache' import { getTextWithoutComments } from './doc' +import { isCssLanguage } from './css' -export type LanguageBoundary = { type: 'html' | 'js' | 'css' | (string & {}); range: Range } +export type LanguageBoundary = { + type: 'html' | 'js' | 'css' | (string & {}); + range: Range + lang?: string +} let htmlScriptTypes = [ // https://v3-migration.vuejs.org/breaking-changes/inline-template-attribute.html#option-1-use-script-tag @@ -92,6 +97,13 @@ let vueStates = { htmlBlockStart: { match: '', next: 'html' }, htmlBlockEnd: { match: '/>', pop: 1 }, @@ -193,5 +205,13 @@ export function getLanguageBoundaries( cache.set(cacheKey, boundaries) + for (let boundary of boundaries) { + if (boundary.type === 'css') continue + if (!isCssLanguage(state, boundary.type)) continue + + boundary.lang = boundary.type + boundary.type = 'css' + } + return boundaries } diff --git a/packages/vscode-tailwindcss/CHANGELOG.md b/packages/vscode-tailwindcss/CHANGELOG.md index 04c206c3..cc138c86 100644 --- a/packages/vscode-tailwindcss/CHANGELOG.md +++ b/packages/vscode-tailwindcss/CHANGELOG.md @@ -4,6 +4,7 @@ - Fix crash when class regex matches an empty string (#897) - Support Astro's `class:list` attribute by default (#890) +- Fix hovers and CSS conflict detection in Vue `