Skip to content

Commit

Permalink
Fix hovers and conflicts in Vue <style lang="sass"> blocks (#930)
Browse files Browse the repository at this point in the history
* Refactor

* Handle Sass in Vue style blocks

* Update changelog
  • Loading branch information
thecrypticace committed Mar 22, 2024
1 parent e191eb5 commit f7534ae
Show file tree
Hide file tree
Showing 7 changed files with 210 additions and 11 deletions.
@@ -0,0 +1,116 @@
{
"code": "<style lang=\"sass\">\n.foo\n @apply uppercase lowercase\n</style>",
"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 }
}
}
}
]
}
]
}
Expand Up @@ -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')
})
Expand Down Expand Up @@ -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')
Expand Down
28 changes: 28 additions & 0 deletions packages/tailwindcss-language-server/tests/hover/hover.test.js
Expand Up @@ -75,6 +75,20 @@ withFixture('basic', (c) => {
end: { line: 0, character: 31 },
},
})

testHover('vue <style lang=sass>', {
lang: 'vue',
text: `<style lang="sass">
.foo
@apply underline
</style>`,
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) => {
Expand Down Expand Up @@ -148,4 +162,18 @@ withFixture('v4/basic', (c) => {
end: { line: 0, character: 31 },
},
})

testHover('vue <style lang=sass>', {
lang: 'vue',
text: `<style lang="sass">
.foo
@apply underline
</style>`,
position: { line: 2, character: 13 },
expected: '.underline {\n' + ' text-decoration-line: underline;\n' + '}',
expectedRange: {
start: { line: 2, character: 9 },
end: { line: 2, character: 18 },
},
})
})
19 changes: 14 additions & 5 deletions packages/tailwindcss-language-service/src/util/css.ts
Expand Up @@ -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 {
Expand Down
33 changes: 28 additions & 5 deletions packages/tailwindcss-language-service/src/util/find.ts
Expand Up @@ -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'
Expand Down Expand Up @@ -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+)(?<classList>[^}\r\n]+?)(?<important>\s*!important)?(?:\r|\n|}|$)/g
: /(@apply\s+)(?<classList>[^;}]+?)(?<important>\s*!important)?\s*[;}]/g
const matches = findAll(regex, text)
Expand Down Expand Up @@ -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),
]),
)
Expand Down Expand Up @@ -408,14 +409,36 @@ export async function findClassNameAtPosition(
doc: TextDocument,
position: Position,
): Promise<DocumentClassName> {
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')
Expand Down
Expand Up @@ -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
Expand Down Expand Up @@ -92,6 +97,13 @@ let vueStates = {
htmlBlockStart: { match: '<template', push: 'htmlBlock' },
...states.main,
},

cssBlock: {
langAttrStartDouble: { match: 'lang="', push: 'langAttrDouble' },
langAttrStartSingle: { match: "lang='", push: 'langAttrSingle' },
...states.cssBlock,
},

htmlBlock: {
htmlStart: { match: '>', next: 'html' },
htmlBlockEnd: { match: '/>', pop: 1 },
Expand Down Expand Up @@ -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
}
1 change: 1 addition & 0 deletions packages/vscode-tailwindcss/CHANGELOG.md
Expand Up @@ -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 `<style lang="sass">` blocks (#930)

## 0.10.5

Expand Down

0 comments on commit f7534ae

Please sign in to comment.