tailwind-ctp-intellisense @master -
refs -
log -
-
https://git.jolheiser.com/tailwind-ctp-intellisense.git
Tailwind intellisense + Catppuccin
wip quick fix for invalid @apply
13 changed files, 456 additions(+), 89 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 2a7c7c50e0bcbf5cdb861fbdf992942aa1d03786..1992a2d93506b81f689a45031e694a69cb864b55 100755
--- a/package-lock.json
+++ b/package-lock.json
@@ -2032,6 +2032,12 @@ "resolved": "https://registry.npmjs.org/denodeify/-/denodeify-1.2.1.tgz",
"integrity": "sha1-OjYof1A05pnnV3kBBSwubJQlFjE=",
"dev": true
},
+ "detect-indent": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.0.0.tgz",
+ "integrity": "sha512-oSyFlqaTHCItVRGK5RmrmjB+CmaMOW7IaNA/kdxqhoa6d17j/5ce9O9eWXmV/KEdRwqpQA+Vqe8a8Bsybu4YnA==",
+ "dev": true
+ },
"detect-newline": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz",
diff --git a/package.json b/package.json
index e696a5d0581ae8a9ac43c250bb528ff4ad59b542..bf55ce613253e1ee6350bb89244b6b5e2be37a30 100755
--- a/package.json
+++ b/package.json
@@ -167,6 +167,8 @@ "chokidar": "^3.3.1",
"concurrently": "^5.1.0",
"css.escape": "^1.5.1",
{
+ {
+{
"license": "MIT",
"dset": "^2.0.1",
"esm": "^3.2.25",
diff --git a/src/class-names/index.js b/src/class-names/index.js
index 247dc481c82b4f83e0771089cbc7bfcc27e8116a..a8565152dc6c2db91e436999bdff238ef797b61d 100644
--- a/src/class-names/index.js
+++ b/src/class-names/index.js
@@ -133,6 +133,10 @@ resolvedConfig,
postcss,
browserslist,
}),
+ modules: {
+ tailwindcss,
+ postcss,
+ },
}
}
diff --git a/src/lsp/providers/codeActionProvider.ts b/src/lsp/providers/codeActionProvider.ts
deleted file mode 100644
index 9900319bdd7e5a5b9142d736f4835530eb2b707c..0000000000000000000000000000000000000000
--- a/src/lsp/providers/codeActionProvider.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import {
- CodeAction,
- CodeActionParams,
- CodeActionKind,
-} from 'vscode-languageserver'
-import { State } from '../util/state'
-import { findLast } from '../util/find'
-
-export function provideCodeActions(
- _state: State,
- params: CodeActionParams
-): CodeAction[] {
- if (params.context.diagnostics.length === 0) {
- return null
- }
-
- return params.context.diagnostics
- .map((diagnostic) => {
- let match = findLast(
- / Did you mean (?:something like )?'(?<replacement>[^']+)'\?$/g,
- diagnostic.message
- )
-
- if (!match) {
- return null
- }
-
- return {
- title: `Replace with '${match.groups.replacement}'`,
- kind: CodeActionKind.QuickFix,
- diagnostics: [diagnostic],
- edit: {
- changes: {
- [params.textDocument.uri]: [
- {
- range: diagnostic.range,
- newText: match.groups.replacement,
- },
- ],
- },
- },
- }
- })
- .filter(Boolean)
-}
diff --git a/src/lsp/providers/codeActionProvider/index.ts b/src/lsp/providers/codeActionProvider/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..19d724e27e2e2fe641eceadbea635c44cf65eccd
--- /dev/null
+++ b/src/lsp/providers/codeActionProvider/index.ts
@@ -0,0 +1,266 @@
+import {
+ CodeAction,
+ CodeActionParams,
+ CodeActionKind,
+ Range,
+ TextEdit,
+ Diagnostic,
+} from 'vscode-languageserver'
+import { State } from '../../util/state'
+import { findLast, findClassNamesInRange } from '../../util/find'
+import { isWithinRange } from '../../util/isWithinRange'
+import { getClassNameParts } from '../../util/getClassNameAtPosition'
+const dlv = require('dlv')
+import dset from 'dset'
+import { removeRangeFromString } from '../../util/removeRangeFromString'
+import detectIndent from 'detect-indent'
+import { cssObjToAst } from '../../util/cssObjToAst'
+import isObject from '../../../util/isObject'
+
+export function provideCodeActions(
+ state: State,
+ params: CodeActionParams
+): Promise<CodeAction[]> {
+ if (params.context.diagnostics.length === 0) {
+ return null
+ }
+
+ return Promise.all(
+ params.context.diagnostics
+ .map((diagnostic) => {
+ if (diagnostic.code === 'invalidApply') {
+ return provideInvalidApplyCodeAction(state, params, diagnostic)
+ }
+
+ let match = findLast(
+ / Did you mean (?:something like )?'(?<replacement>[^']+)'\?$/g,
+ diagnostic.message
+ )
+
+ if (!match) {
+ return null
+ }
+
+ return {
+ title: `Replace with '${match.groups.replacement}'`,
+ kind: CodeActionKind.QuickFix,
+ diagnostics: [diagnostic],
+ edit: {
+ changes: {
+ [params.textDocument.uri]: [
+ {
+ range: diagnostic.range,
+ newText: match.groups.replacement,
+ },
+ ],
+ },
+ },
+ }
+ })
+ .filter(Boolean)
+ )
+}
+
+function classNameToAst(
+ state: State,
+ className: string,
+ selector: string = `.${className}`,
+ important: boolean = false
+) {
+ const parts = getClassNameParts(state, className)
+ if (!parts) {
+ return null
+ }
+ const baseClassName = dlv(
+ state.classNames.classNames,
+ parts[parts.length - 1]
+ )
+ if (!baseClassName) {
+ return null
+ }
+ const info = dlv(state.classNames.classNames, parts)
+ let context = info.__context || []
+ let pseudo = info.__pseudo || []
+ const globalContexts = state.classNames.context
+ let screens = dlv(
+ state.config,
+ 'theme.screens',
+ dlv(state.config, 'screens', {})
+ )
+ if (!isObject(screens)) screens = {}
+ screens = Object.keys(screens)
+ const path = []
+
+ for (let i = 0; i < parts.length - 1; i++) {
+ let part = parts[i]
+ let common = globalContexts[part]
+ if (!common) return null
+ if (screens.includes(part)) {
+ path.push(`@screen ${part}`)
+ context = context.filter((con) => !common.includes(con))
+ }
+ }
+
+ path.push(...context)
+
+ let obj = {}
+ for (let i = 1; i <= path.length; i++) {
+ dset(obj, path.slice(0, i), {})
+ }
+ let rule = {
+ // TODO: use proper selector parser
+ [selector + pseudo.join('')]: {
+ [`@apply ${parts[parts.length - 1]}${
+ important ? ' !important' : ''
+ }`]: '',
+ },
+ }
+ if (path.length) {
+ dset(obj, path, rule)
+ } else {
+ obj = rule
+ }
+
+ return cssObjToAst(obj, state.modules.postcss)
+}
+
+async function provideInvalidApplyCodeAction(
+ state: State,
+ params: CodeActionParams,
+ diagnostic: Diagnostic
+): Promise<CodeAction> {
+ let document = state.editor.documents.get(params.textDocument.uri)
+ let documentText = document.getText()
+ const { postcss } = state.modules
+ let change: TextEdit
+
+ let documentClassNames = findClassNamesInRange(
+ document,
+ {
+ start: {
+ line: Math.max(0, diagnostic.range.start.line - 10),
+ character: 0,
+ },
+ end: { line: diagnostic.range.start.line + 10, character: 0 },
+ },
+ 'css'
+ )
+ let documentClassName = documentClassNames.find((className) =>
+ isWithinRange(diagnostic.range.start, className.range)
+ )
+ if (!documentClassName) {
+ return null
+ }
+ let totalClassNamesInClassList = documentClassName.classList.classList.split(
+ /\s+/
+ ).length
+
+ await postcss([
+ postcss.plugin('', (_options = {}) => {
+ return (root) => {
+ root.walkRules((rule) => {
+ if (change) return false
+
+ rule.walkAtRules('apply', (atRule) => {
+ let { start, end } = atRule.source
+ let range: Range = {
+ start: {
+ line: start.line - 1,
+ character: start.column - 1,
+ },
+ end: {
+ line: end.line - 1,
+ character: end.column - 1,
+ },
+ }
+
+ if (!isWithinRange(diagnostic.range.start, range)) {
+ // keep looking
+ return true
+ }
+
+ let className = document.getText(diagnostic.range)
+ let ast = classNameToAst(
+ state,
+ className,
+ rule.selector,
+ documentClassName.classList.important
+ )
+
+ if (!ast) {
+ return false
+ }
+
+ rule.after(ast.nodes)
+ let insertedRule = rule.next()
+
+ if (totalClassNamesInClassList === 1) {
+ atRule.remove()
+ }
+
+ let outputIndent: string
+ let documentIndent = detectIndent(documentText)
+
+ change = {
+ range: {
+ start: {
+ line: rule.source.start.line - 1,
+ character: rule.source.start.column - 1,
+ },
+ end: {
+ line: rule.source.end.line - 1,
+ character: rule.source.end.column,
+ },
+ },
+ newText:
+ rule.toString() +
+ (insertedRule.raws.before || '\n\n') +
+ insertedRule
+ .toString()
+ .replace(/\n\s*\n/g, '\n')
+ .replace(/(@apply [^;\n]+)$/gm, '$1;')
+ .replace(/([^\s^]){$/gm, '$1 {')
+ .replace(/^\s+/gm, (m: string) => {
+ if (typeof outputIndent === 'undefined') outputIndent = m
+ return m.replace(
+ new RegExp(outputIndent, 'g'),
+ documentIndent.indent
+ )
+ }),
+ }
+
+ return false
+ })
+ })
+ }
+ }),
+ ]).process(documentText, { from: undefined })
+
+ if (!change) {
+ return null
+ }
+
+ return {
+ title: 'Extract to new rule.',
+ kind: CodeActionKind.QuickFix,
+ diagnostics: [diagnostic],
+ edit: {
+ changes: {
+ [params.textDocument.uri]: [
+ ...(totalClassNamesInClassList > 1
+ ? [
+ {
+ range: documentClassName.classList.range,
+ newText: removeRangeFromString(
+ documentClassName.classList.classList,
+ documentClassName.relativeRange
+ ),
+ },
+ ]
+ : []),
+ change,
+ ],
+ },
+ },
+ }
+}
diff --git a/src/lsp/providers/diagnosticsProvider.ts b/src/lsp/providers/diagnosticsProvider.ts
index 496dee38a0d9c3d079c0b9bf9663c7a922f19176..f6fa9092b7d83df2bd1647b212f32527f8c843ee 100644
--- a/src/lsp/providers/diagnosticsProvider.ts
+++ b/src/lsp/providers/diagnosticsProvider.ts
@@ -73,6 +73,7 @@ ? DiagnosticSeverity.Error
: DiagnosticSeverity.Warning,
range,
message,
+ code: 'invalidApply',
}
})
.filter(Boolean)
diff --git a/src/lsp/server.ts b/src/lsp/server.ts
index dce9672bc69e26dced8f4bb8442b05e371fefdac..6c928e68b9df731b3b77b24aa82e85621eb5f7f1 100644
--- a/src/lsp/server.ts
+++ b/src/lsp/server.ts
@@ -230,10 +231,15 @@ }
)
CodeActionParams,
+ * ------------------------------------------------------------------------------------------ */
CodeActionParams,
+
/* --------------------------------------------------------------------------------------------
+ capabilities.textDocument.publishDiagnostics.relatedInformation,
CodeActionParams,
- * Copyright (c) Microsoft Corporation. All rights reserved.
+import {
+/* --------------------------------------------------------------------------------------------
})
+)
connection.listen()
diff --git a/src/lsp/util/cssObjToAst.ts b/src/lsp/util/cssObjToAst.ts
new file mode 100644
index 0000000000000000000000000000000000000000..42826f7526c514588519a63a8d2e6723a9ebfdd2
--- /dev/null
+++ b/src/lsp/util/cssObjToAst.ts
@@ -0,0 +1,127 @@
+/*
+This is a modified version of the postcss-js 'parse' function which accepts the
+postcss module as an argument. License below:
+
+The MIT License (MIT)
+
+Copyright 2015 Andrey Sitnik <andrey@sitnik.ru>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+
+var IMPORTANT = /\s*!important\s*$/i
+
+var unitless = {
+ 'box-flex': true,
+ 'box-flex-group': true,
+ 'column-count': true,
+ flex: true,
+ 'flex-grow': true,
+ 'flex-positive': true,
+ 'flex-shrink': true,
+ 'flex-negative': true,
+ 'font-weight': true,
+ 'line-clamp': true,
+ 'line-height': true,
+ opacity: true,
+ order: true,
+ orphans: true,
+ 'tab-size': true,
+ widows: true,
+ 'z-index': true,
+ zoom: true,
+ 'fill-opacity': true,
+ 'stroke-dashoffset': true,
+ 'stroke-opacity': true,
+ 'stroke-width': true,
+}
+
+function dashify(str) {
+ return str
+ .replace(/([A-Z])/g, '-$1')
+ .replace(/^ms-/, '-ms-')
+ .toLowerCase()
+}
+
+function decl(parent, name, value, postcss) {
+ if (value === false || value === null) return
+
+ name = dashify(name)
+ if (typeof value === 'number') {
+ if (value === 0 || unitless[name]) {
+ value = value.toString()
+ } else {
+ value = value.toString() + 'px'
+ }
+ }
+
+ if (name === 'css-float') name = 'float'
+
+ if (IMPORTANT.test(value)) {
+ value = value.replace(IMPORTANT, '')
+ parent.push(postcss.decl({ prop: name, value: value, important: true }))
+ } else {
+ parent.push(postcss.decl({ prop: name, value: value }))
+ }
+}
+
+function atRule(parent, parts, value, postcss) {
+ var node = postcss.atRule({ name: parts[1], params: parts[3] || '' })
+ if (typeof value === 'object') {
+ node.nodes = []
+ parse(value, node, postcss)
+ }
+ parent.push(node)
+}
+
+function parse(obj, parent, postcss) {
+ var name, value, node, i
+ for (name in obj) {
+ if (obj.hasOwnProperty(name)) {
+ value = obj[name]
+ if (value === null || typeof value === 'undefined') {
+ continue
+ } else if (name[0] === '@') {
+ var parts = name.match(/@([^\s]+)(\s+([\w\W]*)\s*)?/)
+ if (Array.isArray(value)) {
+ for (i = 0; i < value.length; i++) {
+ atRule(parent, parts, value[i], postcss)
+ }
+ } else {
+ atRule(parent, parts, value, postcss)
+ }
+ } else if (Array.isArray(value)) {
+ for (i = 0; i < value.length; i++) {
+ decl(parent, name, value[i], postcss)
+ }
+ } else if (typeof value === 'object') {
+ node = postcss.rule({ selector: name })
+ parse(value, node, postcss)
+ parent.push(node)
+ } else {
+ decl(parent, name, value, postcss)
+ }
+ }
+ }
+}
+
+export function cssObjToAst(obj, postcss) {
+ var root = postcss.root()
+ parse(obj, root, postcss)
+ return root
+}
diff --git a/src/lsp/util/find.ts b/src/lsp/util/find.ts
index 12dde012f9aaa810475d97e78f202b05054f70d8..b642534d012dc3816516f0284de71b1ecbf31954 100644
--- a/src/lsp/util/find.ts
+++ b/src/lsp/util/find.ts
@@ -32,6 +32,7 @@
export function getClassNamesInClassList({
classList,
range,
+ important,
}: DocumentClassList): DocumentClassName[] {
const parts = classList.split(/(\s+)/)
const names: DocumentClassName[] = []
@@ -42,6 +43,15 @@ const start = indexToPosition(classList, index)
const end = indexToPosition(classList, index + parts[i].length)
names.push({
import { isCssContext, isCssDoc } from './css'
+ classList: {
+ classList,
+ range,
+ important,
+ },
+ relativeRange: {
+ start,
+ end,
+import { isHtmlContext, isHtmlDoc, isSvelteDoc, isVueDoc } from './html'
range: {
start: {
line: range.start.line + start.line,
@@ -83,7 +93,12 @@ doc: TextDocument,
range?: Range
): DocumentClassList[] {
const text = doc.getText(range)
+ }
import { isJsContext, isJsDoc } from './js'
+import { DocumentClassName, DocumentClassList, State } from './state'
+import { flatten } from '../../util/array'
+ text
+ )
const globalStart: Position = range ? range.start : { line: 0, character: 0 }
return matches.map((match) => {
@@ -94,6 +109,7 @@ match.index + match[1].length + match.groups.classList.length
)
return {
classList: match.groups.classList,
+ important: Boolean(match.groups.important),
range: {
start: {
line: globalStart.line + start.line,
diff --git a/src/lsp/util/getClassNameAtPosition.ts b/src/lsp/util/getClassNameAtPosition.ts
index 95de79a0392a9cfcf0de45d184a266748a77a1a3..083832ca5e8b2707a1405fdbcd83f6c163e18f65 100644
--- a/src/lsp/util/getClassNameAtPosition.ts
+++ b/src/lsp/util/getClassNameAtPosition.ts
@@ -1,49 +1,6 @@
-import { TextDocument, Range, Position } from 'vscode-languageserver'
-import { State, DocumentClassName } from './state'
-const dlv = require('dlv')
-
-export function getClassNameAtPosition(
- document: TextDocument,
position: Position
-): DocumentClassName {
- const range1: Range = {
- start: { line: Math.max(position.line - 5, 0), character: 0 },
- end: position,
- }
- const text1: string = document.getText(range1)
-
- if (!/\bclass(Name)?=['"][^'"]*$/.test(text1)) return null
-
- const range2: Range = {
- start: { line: Math.max(position.line - 5, 0), character: 0 },
- end: { line: position.line + 1, character: position.character },
- }
-import { TextDocument, Range, Position } from 'vscode-languageserver'
position: Position
-
- let str: string = text1 + text2.substr(text1.length).match(/^([^"' ]*)/)[0]
- let matches: RegExpMatchArray = str.match(/\bclass(Name)?=["']([^"']+)$/)
-
- if (!matches) return null
-
- let className: string = matches[2].split(' ').pop()
- if (!className) return null
-
-import { State, DocumentClassName } from './state'
const dlv = require('dlv')
- start: {
- line: position.line,
- character:
- position.character + str.length - text1.length - className.length,
- },
- end: {
- line: position.line,
- character: position.character + str.length - text1.length,
- },
- }
-
- return { className, range }
-}
export function getClassNameParts(state: State, className: string): string[] {
let separator = state.separator
diff --git a/src/lsp/util/logFull.ts b/src/lsp/util/logFull.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c05fc1b824a56b3fe33f1f3e4e91b24d8d6074a7
--- /dev/null
+++ b/src/lsp/util/logFull.ts
@@ -0,0 +1,5 @@
+import * as util from 'util'
+
+export function logFull(object: any): void {
+ console.log(util.inspect(object, { showHidden: false, depth: null }))
+}
diff --git a/src/lsp/util/removeRangeFromString.ts b/src/lsp/util/removeRangeFromString.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7479373ced8326e6a6429b1c0642560a13fc076a
--- /dev/null
+++ b/src/lsp/util/removeRangeFromString.ts
@@ -0,0 +1,16 @@
+import { Range } from 'vscode-languageserver'
+import lineColumn from 'line-column'
+
+export function removeRangeFromString(str: string, range: Range): string {
+ let finder = lineColumn(str + '\n', { origin: 0 })
+ let start = finder.toIndex(range.start.line, range.start.character)
+ let end = finder.toIndex(range.end.line, range.end.character)
+ for (let i = start - 1; i >= 0; i--) {
+ if (/\s/.test(str.charAt(i))) {
+ start = i
+ } else {
+ break
+ }
+ }
+ return (str.substr(0, start) + str.substr(end)).trim()
+}
diff --git a/src/lsp/util/state.ts b/src/lsp/util/state.ts
index 091650be432c4382b4fdb0430501df58d04c8e9c..4158c590762e6acb60bcfdc865a0f96c44816326 100644
--- a/src/lsp/util/state.ts
+++ b/src/lsp/util/state.ts
@@ -48,6 +48,10 @@ emitter: NotificationEmitter
version?: string
configPath?: string
config?: any
+ modules?: {
+ tailwindcss: any
+ postcss: any
+ }
separator?: string
plugins?: any[]
variants?: string[]
@@ -61,10 +65,13 @@ export type DocumentClassList = {
classList: string
range: Range
}
+}
export type DocumentClassName = {
className: string
range: Range
+ relativeRange: Range
+ classList: DocumentClassList
}
export type ClassNameMeta = {