tailwind-ctp-intellisense @master -
refs -
log -
-
https://git.jolheiser.com/tailwind-ctp-intellisense.git
Tailwind intellisense + Catppuccin
refactor class name extraction and stringify
7 changed files, 286 additions(+), 213 deletions(-)
diff --git a/packages/tailwindcss-class-names/src/extractClassNames.mjs b/packages/tailwindcss-class-names/src/extractClassNames.mjs
index c802e4e6bee916c81bf4ac80acf0f3439471a671..4ccb6622101ef1538bfffa21842e3265d0706a23 100644
--- a/packages/tailwindcss-class-names/src/extractClassNames.mjs
+++ b/packages/tailwindcss-class-names/src/extractClassNames.mjs
@@ -18,20 +18,6 @@ const classNames = []
const { nodes: subSelectors } = selectorParser().astSync(selector)
for (let i = 0; i < subSelectors.length; i++) {
- // const final = subSelectors[i].nodes[subSelectors[i].nodes.length - 1]
-
- // if (final.type === 'class') {
- // const scope = subSelectors[i].nodes.slice(
- // 0,
- // subSelectors[i].nodes.length - 1
- // )
-
- // classNames.push({
- // className: String(final).trim(),
- // scope: createSelectorFromNodes(scope)
- // })
- // }
-
let scope = []
for (let j = 0; j < subSelectors[i].nodes.length; j++) {
let node = subSelectors[i].nodes[j]
@@ -47,39 +33,28 @@ next = subSelectors[i].nodes[j + 1]
}
classNames.push({
- className: String(node)
- .trim()
- .substr(1),
+ className: node.value.trim(),
scope: createSelectorFromNodes(scope),
__rule: j === subSelectors[i].nodes.length - 1,
- // __pseudo: createSelectorFromNodes(pseudo)
- __pseudo: pseudo.length === 0 ? null : pseudo.map(String)
+ __pseudo: pseudo.length === 0 ? null : pseudo.map(String),
})
}
scope.push(node, ...pseudo)
}
}
- // console.log(classNames)
-
return classNames
}
-// console.log(getClassNamesFromSelector('h1, h2, h3, .foo .bar, .baz'))
-
-// const css = fs.readFileSync(path.resolve(__dirname, 'tailwind.css'), 'utf8')
-
async function process(ast) {
- const start = new Date()
-
const tree = {}
const commonContext = {}
- ast.root.walkRules(rule => {
+ ast.root.walkRules((rule) => {
const classNames = getClassNamesFromSelector(rule.selector)
- const decls = { __decls: true }
- rule.walkDecls(decl => {
+ const decls = {}
+ rule.walkDecls((decl) => {
decls[decl.prop] = decl.value
})
@@ -96,49 +71,48 @@ for (let i = 0; i < classNames.length; i++) {
const context = keys.concat([])
const baseKeys = classNames[i].className.split('__TAILWIND_SEPARATOR__')
const contextKeys = baseKeys.slice(0, baseKeys.length - 1)
+ const index = []
- if (classNames[i].scope) {
- let index = []
- const existing = dlv(tree, baseKeys)
- if (typeof existing !== 'undefined') {
- if (Array.isArray(existing)) {
- const scopeIndex = existing.findIndex(
- x => x.__scope === classNames[i].scope
- )
- if (scopeIndex > -1) {
- keys.unshift(scopeIndex)
- index.push(scopeIndex)
- } else {
- keys.unshift(existing.length)
- index.push(existing.length)
- }
+ const existing = dlv(tree, baseKeys)
+ if (typeof existing !== 'undefined') {
+ if (Array.isArray(existing)) {
+ const scopeIndex = existing.findIndex(
+ (x) =>
+ x.__scope === classNames[i].scope &&
+ arraysEqual(existing.__context, context)
+ )
+ if (scopeIndex > -1) {
+ keys.unshift(scopeIndex)
+ index.push(scopeIndex)
} else {
- if (existing.__scope !== classNames[i].scope) {
- dset(tree, baseKeys, [existing])
- keys.unshift(1)
- index.push(1)
- }
+ keys.unshift(existing.length)
+ index.push(existing.length)
}
- }
- if (classNames[i].__rule) {
- dset(tree, [...baseKeys, ...index, '__rule'], true)
- dsetEach(tree, [...baseKeys, ...keys], decls)
- }
- if (classNames[i].__pseudo) {
- dset(tree, [...baseKeys, ...keys, '__pseudo'], classNames[i].__pseudo)
- }
- dset(tree, [...baseKeys, ...index, '__scope'], classNames[i].scope)
- } else {
- if (classNames[i].__rule) {
- dset(tree, [...baseKeys, '__rule'], true)
- dsetEach(tree, [...baseKeys, ...keys], decls)
} else {
- dset(tree, [...baseKeys, ...keys], {})
+ if (
+ existing.__scope !== classNames[i].scope ||
+ !arraysEqual(existing.__context, context)
+ ) {
+ dset(tree, baseKeys, [existing])
+ keys.unshift(1)
+ index.push(1)
+ }
}
- if (classNames[i].__pseudo) {
- dset(tree, [...baseKeys, ...keys, '__pseudo'], classNames[i].__pseudo)
- }
+ }
+ if (classNames[i].__rule) {
+ dset(tree, [...baseKeys, ...index, '__rule'], true)
+
+ dsetEach(tree, [...baseKeys, ...index], decls)
+ }
+ if (classNames[i].__pseudo) {
+ dset(tree, [...baseKeys, '__pseudo'], classNames[i].__pseudo)
}
+ dset(tree, [...baseKeys, ...index, '__scope'], classNames[i].scope)
+ dset(
+ tree,
+ [...baseKeys, ...index, '__context'],
+ context.concat([]).reverse()
+ )
// common context
if (classNames[i].__pseudo) {
@@ -157,15 +131,12 @@ }
}
}
})
- // console.log(`${new Date() - start}ms`)
- // console.log(tree)
- // console.log(commonContext)
return { classNames: tree, context: commonContext }
}
function intersection(arr1, arr2) {
- return arr1.filter(value => arr2.indexOf(value) !== -1)
+ return arr1.filter((value) => arr2.indexOf(value) !== -1)
}
function dsetEach(obj, keys, values) {
@@ -175,14 +146,15 @@ dset(obj, [...keys, k[i]], values[k[i]])
}
}
-export default process
+function arraysEqual(a, b) {
+ if (a === b) return true
+ if (a == null || b == null) return false
+ if (a.length !== b.length) return false
+
+ for (let i = 0; i < a.length; ++i) {
+ if (a[i] !== b[i]) return false
+ }
+ return true
+}
-// process(`
-// .bg-red {
-// background-color: red;
-// }
-// .bg-red {
-// color: white;
-// }`).then(x => {
-// console.log(x)
-// })
+export default process
diff --git a/packages/tailwindcss-class-names/tests/extractClassNames.test.js b/packages/tailwindcss-class-names/tests/extractClassNames.test.js
index b276787c05b815552a7f1ca835d2efa5ac5fe396..19290e671a71bea06ee61060cba5c41002db3ce1 100644
--- a/packages/tailwindcss-class-names/tests/extractClassNames.test.js
+++ b/packages/tailwindcss-class-names/tests/extractClassNames.test.js
@@ -3,9 +3,73 @@ const esmImport = require('esm')(module)
const process = esmImport('../src/extractClassNames.mjs').default
postcss = postcss([postcss.plugin('no-op', () => () => {})])
-const processCss = async css =>
+const processCss = async (css) =>
process(await postcss.process(css, { from: undefined }))
+test('processes default container plugin', async () => {
+ const result = await processCss(`
+ .container {
+ width: 100%
+ }
+
+ @media (min-width: 640px) {
+ .container {
+ max-width: 640px
+ }
+ }
+
+ @media (min-width: 768px) {
+ .container {
+ max-width: 768px
+ }
+ }
+
+ @media (min-width: 1024px) {
+ .container {
+ max-width: 1024px
+ }
+ }
+
+ @media (min-width: 1280px) {
+ .container {
+ max-width: 1280px
+ }
+ }
+ `)
+ expect(result).toEqual({
+ context: {},
+ classNames: {
+ container: [
+ { __context: [], __rule: true, __scope: null, width: '100%' },
+ {
+ __rule: true,
+ __scope: null,
+ __context: ['@media (min-width: 640px)'],
+ 'max-width': '640px',
+ },
+ {
+ __rule: true,
+ __scope: null,
+ __context: ['@media (min-width: 768px)'],
+ 'max-width': '768px',
+ },
+ {
+ __rule: true,
+ __scope: null,
+ __context: ['@media (min-width: 1024px)'],
+ 'max-width': '1024px',
+ },
+ {
+ __rule: true,
+ __scope: null,
+ __context: ['@media (min-width: 1280px)'],
+ 'max-width': '1280px',
+ },
+ ],
+ },
+ })
+})
+
test('foo', async () => {
const result = await processCss(`
@media (min-width: 640px) {
@@ -24,43 +88,42 @@
expect(result).toEqual({
context: {
sm: ['@media (min-width: 640px)'],
- hover: [':hover']
+ hover: [':hover'],
},
classNames: {
sm: {
'bg-red': {
__rule: true,
- '@media (min-width: 640px)': {
- __decls: true,
- 'background-color': 'red'
- }
+ __scope: null,
+ __context: ['@media (min-width: 640px)'],
+ 'background-color': 'red',
},
hover: {
'bg-red': {
__rule: true,
- '@media (min-width: 640px)': {
- __decls: true,
- __pseudo: [':hover'],
- 'background-color': 'red'
- }
- }
- }
+ __scope: null,
+ __context: ['@media (min-width: 640px)'],
+ __pseudo: [':hover'],
+ 'background-color': 'red',
+ },
+ },
},
hover: {
'bg-red': {
__rule: true,
- __decls: true,
+ __scope: null,
__pseudo: [':hover'],
- 'background-color': 'red'
- }
- }
- }
+ __context: [],
+ 'background-color': 'red',
+ },
+ },
+ },
})
})
-test('processes basic css', async () => {
+test.only('processes basic css', async () => {
const result = await processCss(`
- .bg-red {
+ .bg-red\\:foo {
background-color: red;
}
`)
@@ -70,10 +133,11 @@ context: {},
classNames: {
'bg-red': {
__rule: true,
- __decls: true,
- 'background-color': 'red'
- }
- }
+ __scope: null,
+ __context: [],
+ 'background-color': 'red',
+ },
+ },
})
})
@@ -89,11 +153,12 @@ context: {},
classNames: {
'bg-red': {
__rule: true,
- __decls: true,
+ __scope: null,
+ __context: [],
__pseudo: [':first-child', '::after'],
- 'background-color': 'red'
- }
- }
+ 'background-color': 'red',
+ },
+ },
})
})
@@ -108,15 +173,17 @@ expect(result).toEqual({
context: {},
classNames: {
scope: {
- __pseudo: [':hover']
+ __context: [],
+ __pseudo: [':hover'],
+ __scope: null,
},
'bg-red': {
+ __context: [],
__rule: true,
- __decls: true,
__scope: '.scope:hover',
- 'background-color': 'red'
- }
- }
+ 'background-color': 'red',
+ },
+ },
})
})
@@ -133,15 +200,17 @@ context: {},
classNames: {
'bg-red': {
__rule: true,
- __decls: true,
- 'background-color': 'red'
+ __scope: null,
+ __context: [],
+ 'background-color': 'red',
},
'bg-red-again': {
__rule: true,
- __decls: true,
- 'background-color': 'red'
- }
- }
+ __scope: null,
+ __context: [],
+ 'background-color': 'red',
+ },
+ },
})
})
@@ -159,12 +228,35 @@ context: {},
classNames: {
'bg-red': {
__rule: true,
- '@media (min-width: 768px)': {
- __decls: true,
- 'background-color': 'red'
+ __scope: null,
+ __context: ['@media (min-width: 768px)'],
+ 'background-color': 'red',
+ },
+ },
+ })
+})
+
+test('processes nested at-rules', async () => {
+ const result = await processCss(`
+ @supports (display: grid) {
+ @media (min-width: 768px) {
+ .bg-red {
+ background-color: red;
}
}
}
+ `)
+
+ expect(result).toEqual({
+ context: {},
+ classNames: {
+ 'bg-red': {
+ __rule: true,
+ __scope: null,
+ __context: ['@supports (display: grid)', '@media (min-width: 768px)'],
+ 'background-color': 'red',
+ },
+ },
})
})
@@ -183,11 +275,12 @@ context: {},
classNames: {
'bg-red': {
__rule: true,
- __decls: true,
+ __scope: null,
+ __context: [],
'background-color': 'red',
- color: 'white'
- }
- }
+ color: 'white',
+ },
+ },
})
})
@@ -201,14 +294,17 @@
expect(result).toEqual({
context: {},
classNames: {
- scope: {},
+ scope: {
+ __context: [],
+ __scope: null,
+ },
'bg-red': {
__rule: true,
- __decls: true,
+ __context: [],
__scope: '.scope',
- 'background-color': 'red'
- }
- }
+ 'background-color': 'red',
+ },
+ },
})
})
@@ -228,29 +324,29 @@
expect(result).toEqual({
context: {},
classNames: {
- scope1: {},
- scope2: {},
- scope3: {},
+ scope1: { __context: [], __scope: null },
+ scope2: { __context: [], __scope: null },
+ scope3: { __context: [], __scope: null },
'bg-red': [
{
__rule: true,
- __decls: true,
+ __context: [],
__scope: '.scope1',
- 'background-color': 'red'
+ 'background-color': 'red',
},
{
__rule: true,
- __decls: true,
+ __context: [],
__scope: '.scope2 +',
- 'background-color': 'red'
+ 'background-color': 'red',
},
{
__rule: true,
- __decls: true,
+ __context: [],
__scope: '.scope3 >',
- 'background-color': 'red'
- }
- ]
- }
+ 'background-color': 'red',
+ },
+ ],
+ },
})
})
diff --git a/packages/tailwindcss-language-server/src/providers/completionProvider.ts b/packages/tailwindcss-language-server/src/providers/completionProvider.ts
index 84c54c72463959fc711571a6c0c08b6845058f4f..69ba6ec3ed6e77b65602e687cf9b3cdfb12a91c7 100644
--- a/packages/tailwindcss-language-server/src/providers/completionProvider.ts
+++ b/packages/tailwindcss-language-server/src/providers/completionProvider.ts
@@ -27,6 +27,7 @@ // TODO
let sep = ':'
let parts = partialClassName.split(sep)
let subset: any
+ let subsetKey: string[] = []
let isSubset: boolean = false
let replacementRange = {
@@ -42,6 +43,7 @@ let keys = parts.slice(0, i).filter(Boolean)
subset = dlv(state.classNames.classNames, keys)
if (typeof subset !== 'undefined' && typeof subset.__rule === 'undefined') {
isSubset = true
+ subsetKey = keys
replacementRange = {
...replacementRange,
start: {
@@ -62,7 +64,7 @@ items: Object.keys(isSubset ? subset : state.classNames.classNames).map(
(className) => {
let kind: CompletionItemKind = CompletionItemKind.Constant
let documentation: string = null
- if (isContextItem(state, [className])) {
+ if (isContextItem(state, [...subsetKey, className])) {
kind = CompletionItemKind.Module
} else {
const color = getColor(state, [className])
@@ -76,6 +78,7 @@ return {
label: className,
kind,
documentation,
+ data: [...subsetKey, className],
textEdit: {
newText: className,
range: replacementRange,
@@ -514,20 +517,20 @@ ) {
return item
}
- const className = state.classNames.classNames[item.label]
- if (isContextItem(state, [item.label])) {
- item.detail = state.classNames.context[item.label].join(', ')
+ const className = dlv(state.classNames.classNames, item.data)
+ if (isContextItem(state, item.data)) {
+ item.detail = state.classNames.context[
+ item.data[item.data.length - 1]
+ ].join(', ')
} else {
item.detail = getCssDetail(state, className)
if (!item.documentation) {
- item.documentation = stringifyCss(className)
- if (item.detail === item.documentation) {
- item.documentation = null
- } else {
- // item.documentation = {
- // kind: MarkupKind.Markdown,
- // value: ['```css', item.documentation, '```'].join('\n')
- // }
+ const css = stringifyCss(item.data.join(':'), className)
+ if (css) {
+ item.documentation = {
+ kind: MarkupKind.Markdown,
+ value: ['```css', css, '```'].join('\n'),
+ }
}
}
}
@@ -537,7 +540,8 @@
function isContextItem(state: State, keys: string[]): boolean {
const item = dlv(state.classNames.classNames, keys)
return Boolean(
- !item.__rule &&
+ isObject(item) &&
+ !item.__rule &&
!Array.isArray(item) &&
state.classNames.context[keys[keys.length - 1]]
)
@@ -555,13 +559,8 @@ function getCssDetail(state: State, className: any): string {
if (Array.isArray(className)) {
return `${className.length} rules`
}
- let withoutMeta = removeMeta(className)
- if (className.__decls === true) {
- return stringifyDecls(withoutMeta)
+ if (className.__rule === true) {
+ return stringifyDecls(removeMeta(className))
}
- let keys = Object.keys(withoutMeta)
- if (keys.length === 1) {
- return getCssDetail(state, className[keys[0]])
- }
- return `${keys.length} rules`
+ return null
}
diff --git a/packages/tailwindcss-language-server/src/providers/hoverProvider.ts b/packages/tailwindcss-language-server/src/providers/hoverProvider.ts
index 5ce792101bf49e000c10ce9bfb4523f44eda5827..20147baff96b89fdf928f536d26634892e9ca10e 100644
--- a/packages/tailwindcss-language-server/src/providers/hoverProvider.ts
+++ b/packages/tailwindcss-language-server/src/providers/hoverProvider.ts
@@ -6,7 +6,6 @@ getClassNameParts,
} from '../util/getClassNameAtPosition'
import { stringifyCss, stringifyConfigValue } from '../util/stringify'
const dlv = require('dlv')
-import escapeClassName from 'css.escape'
import { isHtmlContext } from '../util/html'
import { isCssContext } from '../util/css'
@@ -90,21 +89,11 @@
return {
contents: {
language: 'css',
- value: stringifyCss(dlv(state.classNames.classNames, parts), {
- selector: augmentClassName(parts, state),
- }),
+ value: stringifyCss(
+ hovered.className,
+ dlv(state.classNames.classNames, parts)
+ ),
},
range: hovered.range,
}
}
-
-// TODO
-function augmentClassName(className: string | string[], state: State): string {
- const parts = Array.isArray(className)
- ? className
- : getClassNameParts(state, className)
- const obj = dlv(state.classNames.classNames, parts)
- const pseudo = obj.__pseudo ? obj.__pseudo.join('') : ''
- const scope = obj.__scope ? `${obj.__scope} ` : ''
- return `${scope}.${escapeClassName(parts.join(state.separator))}${pseudo}`
-}
diff --git a/packages/tailwindcss-language-server/src/util/color.ts b/packages/tailwindcss-language-server/src/util/color.ts
index 0ac2f8a32fdda94058a64b233e2b834f5dd9ef4e..bb44bba0b14ca9d8ed3c17e67d8f16186164a834 100644
--- a/packages/tailwindcss-language-server/src/util/color.ts
+++ b/packages/tailwindcss-language-server/src/util/color.ts
@@ -16,7 +16,7 @@ 'fill',
'outline-color',
'stop-color',
'stroke',
- 'text-decoration-color'
+ 'text-decoration-color',
]
const COLOR_NAMES = {
@@ -169,12 +169,12 @@ wheat: '#f5deb3',
white: '#fff',
whitesmoke: '#f5f5f5',
yellow: '#ff0',
- yellowgreen: '#9acd32'
+ yellowgreen: '#9acd32',
}
export function getColor(state: State, keys: string[]): string {
const item = dlv(state.classNames.classNames, keys)
- if (!item.__decls) return null
+ if (!item.__rule) return null
const props = Object.keys(removeMeta(item))
if (props.length === 0 || props.length > 1) return null
const prop = props[0]
diff --git a/packages/tailwindcss-language-server/src/util/getClassNameAtPosition.ts b/packages/tailwindcss-language-server/src/util/getClassNameAtPosition.ts
index 709ca96fa29845b5d89327b67a8866f1211fb520..e7e0cf2cf4255e2e420c816dfc9af8e747ed265c 100644
--- a/packages/tailwindcss-language-server/src/util/getClassNameAtPosition.ts
+++ b/packages/tailwindcss-language-server/src/util/getClassNameAtPosition.ts
@@ -49,7 +49,8 @@ className = className.replace(/^\./, '')
let parts: string[] = className.split(separator)
if (parts.length === 1) {
- return dlv(state.classNames.classNames, [className, '__rule']) === true
+ return dlv(state.classNames.classNames, [className, '__rule']) === true ||
+ Array.isArray(dlv(state.classNames.classNames, [className]))
? [className]
: null
}
@@ -73,7 +74,10 @@ }),
]
return possibilities.find((key) => {
- if (dlv(state.classNames.classNames, [...key, '__rule']) === true) {
+ if (
+ dlv(state.classNames.classNames, [...key, '__rule']) === true ||
+ Array.isArray(dlv(state.classNames.classNames, [...key]))
+ ) {
return true
}
return false
diff --git a/packages/tailwindcss-language-server/src/util/stringify.ts b/packages/tailwindcss-language-server/src/util/stringify.ts
index e8b1b7d7bb5ba7eb3c4a25f4de5ed709e463c313..5433e90926433db92e6f14389dd4c1d796df38a3 100644
--- a/packages/tailwindcss-language-server/src/util/stringify.ts
+++ b/packages/tailwindcss-language-server/src/util/stringify.ts
@@ -1,4 +1,6 @@
import removeMeta from './removeMeta'
+const dlv = require('dlv')
+import escapeClassName from 'css.escape'
export function stringifyConfigValue(x: any): string {
if (typeof x === 'string') return x
@@ -12,34 +14,45 @@ }
return null
}
-export function stringifyCss(
- obj: any,
- { indent = 0, selector }: { indent?: number; selector?: string } = {}
-): string {
- let indentStr = '\t'.repeat(indent)
- if (obj.__decls === true) {
- let before = ''
- let after = ''
- if (selector) {
- before = `${indentStr}${selector} {\n`
- after = `\n${indentStr}}`
- indentStr += '\t'
- }
- return (
- before +
- Object.keys(removeMeta(obj)).reduce((acc, curr, i) => {
- return `${acc}${i === 0 ? '' : '\n'}${indentStr}${curr}: ${obj[curr]};`
- }, '') +
- after
- )
+export function stringifyCss(className: string, obj: any): string {
+ if (obj.__rule !== true && !Array.isArray(obj)) return null
+
+ if (Array.isArray(obj)) {
+ const rules = obj.map((x) => stringifyCss(className, x)).filter(Boolean)
+ if (rules.length === 0) return null
+ return rules.join('\n\n')
+ }
+
+ let css = ``
+
+ const context = dlv(obj, '__context', [])
+ const props = Object.keys(removeMeta(obj))
+ if (props.length === 0) return null
+
+ for (let i = 0; i < context.length; i++) {
+ css += `${'\t'.repeat(i)}${context[i]} {\n`
}
- return Object.keys(removeMeta(obj)).reduce((acc, curr, i) => {
- return `${acc}${i === 0 ? '' : '\n'}${indentStr}${curr} {\n${stringifyCss(
- obj[curr],
- {
- indent: indent + 1,
- selector,
- }
- )}\n${indentStr}}`
+
+ const indentStr = '\t'.repeat(context.length)
+ const decls = props.reduce((acc, curr, i) => {
+ return `${acc}${i === 0 ? '' : '\n'}${indentStr + '\t'}${curr}: ${
+ obj[curr]
+ };`
}, '')
+ css += `${indentStr}${augmentClassName(
+ className,
+ obj
+ )} {\n${decls}\n${indentStr}}`
+
+ for (let i = context.length - 1; i >= 0; i--) {
+ css += `${'\t'.repeat(i)}\n}`
+ }
+
+ return css
+}
+
+function augmentClassName(className: string, obj: any): string {
+ const pseudo = obj.__pseudo ? obj.__pseudo.join('') : ''
+ const scope = obj.__scope ? `${obj.__scope} ` : ''
+ return `${scope}.${escapeClassName(className)}${pseudo}`
}