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 `