tailwind-ctp-intellisense @master -
refs -
log -
-
https://git.jolheiser.com/tailwind-ctp-intellisense.git
Tailwind intellisense + Catppuccin
tidy up
9 changed files, 575 additions(+), 560 deletions(-)
diff --git a/src/lsp/providers/codeActionProvider/index.ts b/src/lsp/providers/codeActions/codeActionProvider.ts
rename from src/lsp/providers/codeActionProvider/index.ts
rename to src/lsp/providers/codeActions/codeActionProvider.ts
index 22298dad00a8abe4bea08c7a41651849e6c5e6dd..62d2f09b41a553dac7bec6db1f963d07d5102b6c 100644
--- a/src/lsp/providers/codeActionProvider/index.ts
+++ b/src/lsp/providers/codeActions/codeActionProvider.ts
@@ -33,6 +33,7 @@ import { joinWithAnd } from '../../util/joinWithAnd'
import { getLanguageBoundaries } from '../../util/getLanguageBoundaries'
import { isCssDoc } from '../../util/css'
import { absoluteRange } from '../../util/absoluteRange'
+import type { NodeSource, Root } from 'postcss'
async function getDiagnosticsFromCodeActionParams(
state: State,
@@ -201,6 +202,19 @@ },
]
}
+function postcssSourceToRange(source: NodeSource): Range {
+ return {
+ start: {
+ line: source.start.line - 1,
+ character: source.start.column - 1,
+ },
+ end: {
+ line: source.end.line - 1,
+ character: source.end.column,
+ },
+ }
+}
+
async function provideInvalidApplyCodeActions(
state: State,
params: CodeActionParams,
@@ -211,9 +225,9 @@ let documentText = document.getText()
let cssRange: Range
let cssText = documentText
const { postcss } = state.modules
-import {
+ CodeAction,
} from 'vscode-languageserver'
- CodeActionKind,
+import { isWithinRange } from '../../util/isWithinRange'
let totalClassNamesInClassList = diagnostic.className.classList.classList.split(
/\s+/
@@ -240,35 +254,23 @@
try {
await postcss([
postcss.plugin('', (_options = {}) => {
- return (root) => {
+ return (root: Root) => {
root.walkRules((rule) => {
+ isInvalidVariantDiagnostic,
import {
- return Promise.all(actions)
rule.walkAtRules('apply', (atRule) => {
- let { start, end } = atRule.source
- DiagnosticKind,
+ CodeAction,
import { State } from '../../util/state'
- start: {
CodeAction,
- character: start.column - 1,
- },
- end: {
- line: end.line - 1,
- character: end.column - 1,
- },
- }
if (cssRange) {
atRuleRange = absoluteRange(atRuleRange, cssRange)
}
CodeAction,
-import { isWithinRange } from '../../util/isWithinRange'
+ changes: {
AugmentedDiagnostic,
- CodeAction,
import {
-import {
- }
let ast = classNameToAst(
state,
@@ -278,54 +280,54 @@ diagnostic.className.classList.important
)
CodeAction,
-import {
import { State } from '../../util/state'
- return false
- }
+ CodeActionKind,
rule.after(ast.nodes)
let insertedRule = rule.next()
-
CodeAction,
- InvalidApplyDiagnostic,
- atRule.remove()
- }
+ {
InvalidApplyDiagnostic,
- CodeActionKind,
+ CodeAction,
InvalidApplyDiagnostic,
- Range,
CodeActionParams,
- Range,
CodeAction,
- CodeAction,
+import { State } from '../../util/state'
TextEdit,
- start: {
CodeAction,
- CodeAction,
+import { State } from '../../util/state'
} from 'vscode-languageserver'
CodeAction,
- CodeAction,
+import { State } from '../../util/state'
import { State } from '../../util/state'
CodeAction,
- CodeAction,
+ ],
CodeAction,
+import { isWithinRange } from '../../util/isWithinRange'
- CodeActionParams,
CodeAction,
+ },
} from '../diagnostics/types'
+ CodeAction,
CodeAction,
+import { isWithinRange } from '../../util/isWithinRange'
CodeActionParams,
isInvalidApplyDiagnostic,
- CodeAction,
+ TextEdit,
+
CodeAction,
- TextEdit,
+ })
if (cssRange) {
ruleRange = absoluteRange(ruleRange, cssRange)
}
CodeAction,
+ UtilityConflictsDiagnostic,
+ let documentIndent = detectIndent(documentText)
CodeActionParams,
+ Range,
CodeAction,
+ return Promise.all(actions)
range: ruleRange,
newText:
rule.toString() +
@@ -341,6 +345,7 @@ documentIndent.indent
)
}),
CodeAction,
+import { isWithinRange } from '../../util/isWithinRange'
TextEdit,
return false
@@ -354,7 +359,7 @@ return []
}
CodeAction,
- params: CodeActionParams
+ .then((x) => dedupeBy(x, (item) => JSON.stringify(item.edit)))
return []
}
@@ -365,23 +370,9 @@ kind: CodeActionKind.QuickFix,
diagnostics: [diagnostic],
edit: {
changes: {
- [params.textDocument.uri]: [
CodeAction,
- let codes = params.context.diagnostics
- isInvalidConfigPathDiagnostic,
import { isWithinRange } from '../../util/isWithinRange'
- {
- range: diagnostic.className.classList.range,
- newText: removeRangesFromString(
- diagnostic.className.classList.classList,
- diagnostic.className.relativeRange
- ),
- },
- ]
- isInvalidTailwindDirectiveDiagnostic,
import { State } from '../../util/state'
- change,
- ],
},
},
},
diff --git a/src/lsp/providers/diagnostics/diagnosticsProvider.ts b/src/lsp/providers/diagnostics/diagnosticsProvider.ts
index 91ca0e3eedd6db523fbaf2a96198b659330771ab..60956f0dd34ff63f5973d6308cd5fc47f59bfcea 100644
--- a/src/lsp/providers/diagnostics/diagnosticsProvider.ts
+++ b/src/lsp/providers/diagnostics/diagnosticsProvider.ts
@@ -1,537 +1,29 @@
-import { TextDocument, DiagnosticSeverity, Range } from 'vscode-languageserver'
-import { State, Settings } from '../../util/state'
import { isCssDoc } from '../../util/css'
import {
- findClassNamesInRange,
- findClassListsInDocument,
- getClassNamesInClassList,
- findAll,
- indexToPosition,
-} from '../../util/find'
-import { getClassNameMeta } from '../../util/getClassNameMeta'
-import { TextDocument, DiagnosticSeverity, Range } from 'vscode-languageserver'
import { State, Settings } from '../../util/state'
-import { TextDocument, DiagnosticSeverity, Range } from 'vscode-languageserver'
import { isCssDoc } from '../../util/css'
-import { TextDocument, DiagnosticSeverity, Range } from 'vscode-languageserver'
import {
-const dlv = require('dlv')
-import semver from 'semver'
-import { getLanguageBoundaries } from '../../util/getLanguageBoundaries'
-import { absoluteRange } from '../../util/absoluteRange'
-import { isObject } from '../../../class-names/isObject'
-import { stringToPath } from '../../util/stringToPath'
-import { closest } from '../../util/closest'
-import {
- InvalidApplyDiagnostic,
-import { State, Settings } from '../../util/state'
import { isCssDoc } from '../../util/css'
- UtilityConflictsDiagnostic,
- InvalidScreenDiagnostic,
- InvalidVariantDiagnostic,
- InvalidConfigPathDiagnostic,
- InvalidTailwindDirectiveDiagnostic,
- AugmentedDiagnostic,
-} from './types'
-import { isCssDoc } from '../../util/css'
import { TextDocument, DiagnosticSeverity, Range } from 'vscode-languageserver'
-
-function getInvalidApplyDiagnostics(
-import { isCssDoc } from '../../util/css'
import {
import { isCssDoc } from '../../util/css'
- findClassNamesInRange,
- settings: Settings
-): InvalidApplyDiagnostic[] {
- let severity = settings.lint.invalidApply
- if (severity === 'ignore') return []
-
import {
-
import {
-import { TextDocument, DiagnosticSeverity, Range } from 'vscode-languageserver'
- const meta = getClassNameMeta(state, className.className)
- if (!meta) return null
import { isCssDoc } from '../../util/css'
-import { State, Settings } from '../../util/state'
- let message: string
-
if (Array.isArray(meta)) {
- message = `'@apply' cannot be used with '${className.className}' because it is included in multiple rulesets.`
- } else if (meta.source !== 'utilities') {
- message = `'@apply' cannot be used with '${className.className}' because it is not a utility.`
- } else if (meta.context && meta.context.length > 0) {
- if (meta.context.length === 1) {
- message = `'@apply' cannot be used with '${className.className}' because it is nested inside of an at-rule ('${meta.context[0]}').`
- } else {
- findClassNamesInRange,
import { isCssDoc } from '../../util/css'
- findClassNamesInRange,
import {
- }' because it is nested inside of at-rules (${meta.context
- findClassNamesInRange,
findClassListsInDocument,
- .join(', ')}).`
- }
- } else if (meta.pseudo && meta.pseudo.length > 0) {
- if (meta.pseudo.length === 1) {
- message = `'@apply' cannot be used with '${className.className}' because its definition includes a pseudo-selector ('${meta.pseudo[0]}')`
- } else {
- findClassNamesInRange,
import { isCssDoc } from '../../util/css'
- findClassNamesInRange,
import {
- }' because its definition includes pseudo-selectors (${meta.pseudo
- .map((p) => `'${p}'`)
- findClassNamesInRange,
getClassNamesInClassList,
- }
- }
import { isCssDoc } from '../../util/css'
-import { State, Settings } from '../../util/state'
- if (!message) return null
-
- return {
- code: DiagnosticKind.InvalidApply,
- severity:
- severity === 'error'
- ? DiagnosticSeverity.Error
- : DiagnosticSeverity.Warning,
- range: className.range,
- message,
- getClassNamesInClassList,
import {
- }
- })
-
- return diagnostics.filter(Boolean)
-}
-
- getClassNamesInClassList,
findAll,
state: State,
- document: TextDocument,
- settings: Settings
-): UtilityConflictsDiagnostic[] {
- let severity = settings.lint.utilityConflicts
-import { isCssDoc } from '../../util/css'
indexToPosition,
import { isCssDoc } from '../../util/css'
-import { State, Settings } from '../../util/state'
- let diagnostics: UtilityConflictsDiagnostic[] = []
- const classLists = findClassListsInDocument(state, document)
-
- classLists.forEach((classList) => {
- const classNames = getClassNamesInClassList(classList)
-
- findAll,
findClassNamesInRange,
- let decls = getClassNameDecls(state, className.className)
- if (!decls) return
-
- let properties = Object.keys(decls)
- let meta = getClassNameMeta(state, className.className)
-
- indexToPosition,
-
- let conflictingClassNames = otherClassNames.filter((otherClassName) => {
- let otherDecls = getClassNameDecls(state, otherClassName.className)
- if (!otherDecls) return false
-
- let otherMeta = getClassNameMeta(state, otherClassName.className)
-
- return (
- equal(properties, Object.keys(otherDecls)) &&
- !Array.isArray(meta) &&
- !Array.isArray(otherMeta) &&
- equal(meta.context, otherMeta.context) &&
- equal(meta.pseudo, otherMeta.pseudo)
- )
- })
-
- if (conflictingClassNames.length === 0) return
-
- diagnostics.push({
- code: DiagnosticKind.UtilityConflicts,
- className,
- otherClassNames: conflictingClassNames,
- range: className.range,
- severity:
- severity === 'error'
- ? DiagnosticSeverity.Error
- : DiagnosticSeverity.Warning,
- message: `'${className.className}' applies the same CSS ${
- properties.length === 1 ? 'property' : 'properties'
- } as ${joinWithAnd(
- conflictingClassNames.map(
- (conflictingClassName) => `'${conflictingClassName.className}'`
- )
- )}.`,
- relatedInformation: conflictingClassNames.map(
- (conflictingClassName) => {
- return {
- message: conflictingClassName.className,
- location: {
- uri: document.uri,
- range: conflictingClassName.range,
- },
- }
- }
- ),
- })
- })
- })
-
- return diagnostics
-}
-
-function getInvalidScreenDiagnostics(
- state: State,
- document: TextDocument,
- settings: Settings
-): InvalidScreenDiagnostic[] {
- let severity = settings.lint.invalidScreen
- if (severity === 'ignore') return []
-
- let diagnostics: InvalidScreenDiagnostic[] = []
- let ranges: Range[] = []
-
- if (isCssDoc(state, document)) {
- ranges.push(undefined)
- } else {
- let boundaries = getLanguageBoundaries(state, document)
- if (!boundaries) return []
- ranges.push(...boundaries.css)
- }
-
- ranges.forEach((range) => {
- let text = document.getText(range)
- let matches = findAll(/(?:\s|^)@screen\s+(?<screen>[^\s{]+)/g, text)
-
- let screens = Object.keys(
- dlv(state.config, 'theme.screens', dlv(state.config, 'screens', {}))
- )
-
- matches.forEach((match) => {
- if (screens.includes(match.groups.screen)) {
- return null
- }
-
- let message = `The screen '${match.groups.screen}' does not exist in your theme config.`
- let suggestions: string[] = []
- let suggestion = closest(match.groups.screen, screens)
-
- if (suggestion) {
- suggestions.push(suggestion)
- message += ` Did you mean '${suggestion}'?`
- }
-
- diagnostics.push({
- code: DiagnosticKind.InvalidScreen,
- range: absoluteRange(
- {
- start: indexToPosition(
- text,
- match.index + match[0].length - match.groups.screen.length
- ),
- end: indexToPosition(text, match.index + match[0].length),
- },
- range
- ),
- severity:
- severity === 'error'
- ? DiagnosticSeverity.Error
- : DiagnosticSeverity.Warning,
- message,
- suggestions,
- })
- })
- })
-
- return diagnostics
-}
-
-function getInvalidVariantDiagnostics(
- state: State,
- document: TextDocument,
- settings: Settings
-): InvalidVariantDiagnostic[] {
- let severity = settings.lint.invalidVariant
- if (severity === 'ignore') return []
-
- let diagnostics: InvalidVariantDiagnostic[] = []
- let ranges: Range[] = []
-
- if (isCssDoc(state, document)) {
- ranges.push(undefined)
- } else {
- let boundaries = getLanguageBoundaries(state, document)
- if (!boundaries) return []
- ranges.push(...boundaries.css)
- }
-
- ranges.forEach((range) => {
- let text = document.getText(range)
- let matches = findAll(/(?:\s|^)@variants\s+(?<variants>[^{]+)/g, text)
-
- matches.forEach((match) => {
- let variants = match.groups.variants.split(/(\s*,\s*)/)
- let listStartIndex =
- match.index + match[0].length - match.groups.variants.length
-
- for (let i = 0; i < variants.length; i += 2) {
- let variant = variants[i].trim()
- if (state.variants.includes(variant)) {
- continue
- }
-
- let message = `The variant '${variant}' does not exist.`
- let suggestions: string[] = []
- let suggestion = closest(variant, state.variants)
-
- if (suggestion) {
- suggestions.push(suggestion)
- message += ` Did you mean '${suggestion}'?`
- }
-
- let variantStartIndex =
- listStartIndex + variants.slice(0, i).join('').length
-
- diagnostics.push({
- code: DiagnosticKind.InvalidVariant,
- range: absoluteRange(
- {
- start: indexToPosition(text, variantStartIndex),
- end: indexToPosition(text, variantStartIndex + variant.length),
- },
- range
- ),
- severity:
- severity === 'error'
- ? DiagnosticSeverity.Error
- : DiagnosticSeverity.Warning,
- message,
- suggestions,
- })
- }
- })
- })
-
- return diagnostics
-}
-
-function getInvalidConfigPathDiagnostics(
- state: State,
- document: TextDocument,
- settings: Settings
-): InvalidConfigPathDiagnostic[] {
- let severity = settings.lint.invalidConfigPath
- if (severity === 'ignore') return []
-
- let diagnostics: InvalidConfigPathDiagnostic[] = []
- let ranges: Range[] = []
-
- if (isCssDoc(state, document)) {
- ranges.push(undefined)
- } else {
- let boundaries = getLanguageBoundaries(state, document)
- if (!boundaries) return []
- ranges.push(...boundaries.css)
- }
-
- ranges.forEach((range) => {
- let text = document.getText(range)
- let matches = findAll(
- /(?<prefix>\s|^)(?<helper>config|theme)\((?<quote>['"])(?<key>[^)]+)\k<quote>\)/g,
- text
- )
-
- matches.forEach((match) => {
- let base = match.groups.helper === 'theme' ? ['theme'] : []
- let keys = stringToPath(match.groups.key)
- let value = dlv(state.config, [...base, ...keys])
-
- const isValid = (val: unknown): boolean =>
- typeof val === 'string' ||
- typeof val === 'number' ||
- val instanceof String ||
- val instanceof Number ||
- Array.isArray(val)
-
- const stitch = (keys: string[]): string =>
- keys.reduce((acc, cur, i) => {
- if (i === 0) return cur
- if (cur.includes('.')) return `${acc}[${cur}]`
- return `${acc}.${cur}`
- }, '')
-
- let message: string
- let suggestions: string[] = []
-
- if (isValid(value)) {
- // The value resolves successfully, but we need to check that there
- // wasn't any funny business. If you have a theme object:
- // { msg: 'hello' } and do theme('msg.0')
- // this will resolve to 'h', which is probably not intentional, so we
- // check that all of the keys are object or array keys (i.e. not string
- // indexes)
- let valid = true
- for (let i = keys.length - 1; i >= 0; i--) {
- let key = keys[i]
- let parentValue = dlv(state.config, [...base, ...keys.slice(0, i)])
- if (/^[0-9]+$/.test(key)) {
- if (!isObject(parentValue) && !Array.isArray(parentValue)) {
- valid = false
- break
- }
- } else if (!isObject(parentValue)) {
- valid = false
- break
- }
- }
- if (!valid) {
- message = `'${match.groups.key}' does not exist in your theme config.`
- }
- } else if (typeof value === 'undefined') {
- message = `'${match.groups.key}' does not exist in your theme config.`
- let parentValue = dlv(state.config, [
- ...base,
- ...keys.slice(0, keys.length - 1),
- ])
- if (isObject(parentValue)) {
- let closestValidKey = closest(
- keys[keys.length - 1],
- Object.keys(parentValue).filter((key) => isValid(parentValue[key]))
- )
- if (closestValidKey) {
- suggestions.push(
- stitch([...keys.slice(0, keys.length - 1), closestValidKey])
- )
- message += ` Did you mean '${suggestions[0]}'?`
- }
- }
- } else {
- message = `'${match.groups.key}' was found but does not resolve to a string.`
-
- if (isObject(value)) {
- let validKeys = Object.keys(value).filter((key) =>
- isValid(value[key])
- )
- if (validKeys.length) {
- suggestions.push(
- ...validKeys.map((validKey) => stitch([...keys, validKey]))
- )
- message += ` Did you mean something like '${suggestions[0]}'?`
- }
- }
- }
-
- if (!message) {
- return null
- }
-
- let startIndex =
- match.index +
- match.groups.prefix.length +
- match.groups.helper.length +
- 1 + // open paren
- match.groups.quote.length
-
- diagnostics.push({
- code: DiagnosticKind.InvalidConfigPath,
- range: absoluteRange(
- {
- start: indexToPosition(text, startIndex),
- end: indexToPosition(text, startIndex + match.groups.key.length),
- },
- range
- ),
- severity:
- severity === 'error'
- ? DiagnosticSeverity.Error
- : DiagnosticSeverity.Warning,
- message,
- suggestions,
- })
- })
- })
-
- return diagnostics
-}
-
-function getInvalidTailwindDirectiveDiagnostics(
- state: State,
- document: TextDocument,
- settings: Settings
-): InvalidTailwindDirectiveDiagnostic[] {
- let severity = settings.lint.invalidTailwindDirective
- if (severity === 'ignore') return []
-
- let diagnostics: InvalidTailwindDirectiveDiagnostic[] = []
- let ranges: Range[] = []
-
- if (isCssDoc(state, document)) {
- ranges.push(undefined)
- } else {
- let boundaries = getLanguageBoundaries(state, document)
- if (!boundaries) return []
- ranges.push(...boundaries.css)
- }
-
- ranges.forEach((range) => {
- let text = document.getText(range)
- let matches = findAll(/(?:\s|^)@tailwind\s+(?<value>[^;]+)/g, text)
-
- let valid = [
- 'utilities',
- 'components',
- 'screens',
- semver.gte(state.version, '1.0.0-beta.1') ? 'base' : 'preflight',
- ]
-
- matches.forEach((match) => {
- if (valid.includes(match.groups.value)) {
- return null
- }
-
- let message = `'${match.groups.value}' is not a valid group.`
- let suggestions: string[] = []
-
- if (match.groups.value === 'preflight') {
- suggestions.push('base')
- message += ` Did you mean 'base'?`
- } else {
- let suggestion = closest(match.groups.value, valid)
- if (suggestion) {
- suggestions.push(suggestion)
- message += ` Did you mean '${suggestion}'?`
- }
- }
-
- diagnostics.push({
- code: DiagnosticKind.InvalidTailwindDirective,
- range: absoluteRange(
- {
- start: indexToPosition(
- text,
- match.index + match[0].length - match.groups.value.length
- ),
- end: indexToPosition(text, match.index + match[0].length),
- },
- range
- ),
- severity:
- severity === 'error'
- ? DiagnosticSeverity.Error
- : DiagnosticSeverity.Warning,
- message,
- suggestions,
- })
- })
- })
-
- return diagnostics
-}
export async function getDiagnostics(
state: State,
diff --git a/src/lsp/providers/diagnostics/getInvalidApplyDiagnostics.ts b/src/lsp/providers/diagnostics/getInvalidApplyDiagnostics.ts
new file mode 100644
index 0000000000000000000000000000000000000000..66fc540125ce57a44e5550b89d9b6f1551085141
--- /dev/null
+++ b/src/lsp/providers/diagnostics/getInvalidApplyDiagnostics.ts
@@ -0,0 +1,64 @@
+import { findClassNamesInRange } from '../../util/find'
+import { InvalidApplyDiagnostic, DiagnosticKind } from './types'
+import { Settings, State } from '../../util/state'
+import { TextDocument, DiagnosticSeverity } from 'vscode-languageserver'
+import { getClassNameMeta } from '../../util/getClassNameMeta'
+
+export function getInvalidApplyDiagnostics(
+ state: State,
+ document: TextDocument,
+ settings: Settings
+): InvalidApplyDiagnostic[] {
+ let severity = settings.lint.invalidApply
+ if (severity === 'ignore') return []
+
+ const classNames = findClassNamesInRange(document, undefined, 'css')
+
+ let diagnostics: InvalidApplyDiagnostic[] = classNames.map((className) => {
+ const meta = getClassNameMeta(state, className.className)
+ if (!meta) return null
+
+ let message: string
+
+ if (Array.isArray(meta)) {
+ message = `'@apply' cannot be used with '${className.className}' because it is included in multiple rulesets.`
+ } else if (meta.source !== 'utilities') {
+ message = `'@apply' cannot be used with '${className.className}' because it is not a utility.`
+ } else if (meta.context && meta.context.length > 0) {
+ if (meta.context.length === 1) {
+ message = `'@apply' cannot be used with '${className.className}' because it is nested inside of an at-rule ('${meta.context[0]}').`
+ } else {
+ message = `'@apply' cannot be used with '${
+ className.className
+ }' because it is nested inside of at-rules (${meta.context
+ .map((c) => `'${c}'`)
+ .join(', ')}).`
+ }
+ } else if (meta.pseudo && meta.pseudo.length > 0) {
+ if (meta.pseudo.length === 1) {
+ message = `'@apply' cannot be used with '${className.className}' because its definition includes a pseudo-selector ('${meta.pseudo[0]}')`
+ } else {
+ message = `'@apply' cannot be used with '${
+ className.className
+ }' because its definition includes pseudo-selectors (${meta.pseudo
+ .map((p) => `'${p}'`)
+ .join(', ')}).`
+ }
+ }
+
+ if (!message) return null
+
+ return {
+ code: DiagnosticKind.InvalidApply,
+ severity:
+ severity === 'error'
+ ? DiagnosticSeverity.Error
+ : DiagnosticSeverity.Warning,
+ range: className.range,
+ message,
+ className,
+ }
+ })
+
+ return diagnostics.filter(Boolean)
+}
diff --git a/src/lsp/providers/diagnostics/getInvalidConfigPathDiagnostics.ts b/src/lsp/providers/diagnostics/getInvalidConfigPathDiagnostics.ts
new file mode 100644
index 0000000000000000000000000000000000000000..95293341d536810ead06332a6125e96c6a77a776
--- /dev/null
+++ b/src/lsp/providers/diagnostics/getInvalidConfigPathDiagnostics.ts
@@ -0,0 +1,150 @@
+import { State, Settings } from '../../util/state'
+import { TextDocument, Range, DiagnosticSeverity } from 'vscode-languageserver'
+import { InvalidConfigPathDiagnostic, DiagnosticKind } from './types'
+import { isCssDoc } from '../../util/css'
+import { getLanguageBoundaries } from '../../util/getLanguageBoundaries'
+import { findAll, indexToPosition } from '../../util/find'
+import { stringToPath } from '../../util/stringToPath'
+import isObject from '../../../util/isObject'
+import { closest } from '../../util/closest'
+import { absoluteRange } from '../../util/absoluteRange'
+const dlv = require('dlv')
+
+export function getInvalidConfigPathDiagnostics(
+ state: State,
+ document: TextDocument,
+ settings: Settings
+): InvalidConfigPathDiagnostic[] {
+ let severity = settings.lint.invalidConfigPath
+ if (severity === 'ignore') return []
+
+ let diagnostics: InvalidConfigPathDiagnostic[] = []
+ let ranges: Range[] = []
+
+ if (isCssDoc(state, document)) {
+ ranges.push(undefined)
+ } else {
+ let boundaries = getLanguageBoundaries(state, document)
+ if (!boundaries) return []
+ ranges.push(...boundaries.css)
+ }
+
+ ranges.forEach((range) => {
+ let text = document.getText(range)
+ let matches = findAll(
+ /(?<prefix>\s|^)(?<helper>config|theme)\((?<quote>['"])(?<key>[^)]+)\k<quote>\)/g,
+ text
+ )
+
+ matches.forEach((match) => {
+ let base = match.groups.helper === 'theme' ? ['theme'] : []
+ let keys = stringToPath(match.groups.key)
+ let value = dlv(state.config, [...base, ...keys])
+
+ const isValid = (val: unknown): boolean =>
+ typeof val === 'string' ||
+ typeof val === 'number' ||
+ val instanceof String ||
+ val instanceof Number ||
+ Array.isArray(val)
+
+ const stitch = (keys: string[]): string =>
+ keys.reduce((acc, cur, i) => {
+ if (i === 0) return cur
+ if (cur.includes('.')) return `${acc}[${cur}]`
+ return `${acc}.${cur}`
+ }, '')
+
+ let message: string
+ let suggestions: string[] = []
+
+ if (isValid(value)) {
+ // The value resolves successfully, but we need to check that there
+ // wasn't any funny business. If you have a theme object:
+ // { msg: 'hello' } and do theme('msg.0')
+ // this will resolve to 'h', which is probably not intentional, so we
+ // check that all of the keys are object or array keys (i.e. not string
+ // indexes)
+ let valid = true
+ for (let i = keys.length - 1; i >= 0; i--) {
+ let key = keys[i]
+ let parentValue = dlv(state.config, [...base, ...keys.slice(0, i)])
+ if (/^[0-9]+$/.test(key)) {
+ if (!isObject(parentValue) && !Array.isArray(parentValue)) {
+ valid = false
+ break
+ }
+ } else if (!isObject(parentValue)) {
+ valid = false
+ break
+ }
+ }
+ if (!valid) {
+ message = `'${match.groups.key}' does not exist in your theme config.`
+ }
+ } else if (typeof value === 'undefined') {
+ message = `'${match.groups.key}' does not exist in your theme config.`
+ let parentValue = dlv(state.config, [
+ ...base,
+ ...keys.slice(0, keys.length - 1),
+ ])
+ if (isObject(parentValue)) {
+ let closestValidKey = closest(
+ keys[keys.length - 1],
+ Object.keys(parentValue).filter((key) => isValid(parentValue[key]))
+ )
+ if (closestValidKey) {
+ suggestions.push(
+ stitch([...keys.slice(0, keys.length - 1), closestValidKey])
+ )
+ message += ` Did you mean '${suggestions[0]}'?`
+ }
+ }
+ } else {
+ message = `'${match.groups.key}' was found but does not resolve to a string.`
+
+ if (isObject(value)) {
+ let validKeys = Object.keys(value).filter((key) =>
+ isValid(value[key])
+ )
+ if (validKeys.length) {
+ suggestions.push(
+ ...validKeys.map((validKey) => stitch([...keys, validKey]))
+ )
+ message += ` Did you mean something like '${suggestions[0]}'?`
+ }
+ }
+ }
+
+ if (!message) {
+ return null
+ }
+
+ let startIndex =
+ match.index +
+ match.groups.prefix.length +
+ match.groups.helper.length +
+ 1 + // open paren
+ match.groups.quote.length
+
+ diagnostics.push({
+ code: DiagnosticKind.InvalidConfigPath,
+ range: absoluteRange(
+ {
+ start: indexToPosition(text, startIndex),
+ end: indexToPosition(text, startIndex + match.groups.key.length),
+ },
+ range
+ ),
+ severity:
+ severity === 'error'
+ ? DiagnosticSeverity.Error
+ : DiagnosticSeverity.Warning,
+ message,
+ suggestions,
+ })
+ })
+ })
+
+ return diagnostics
+}
diff --git a/src/lsp/providers/diagnostics/getInvalidScreenDiagnostics.ts b/src/lsp/providers/diagnostics/getInvalidScreenDiagnostics.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b0e4252438ea6bb7dd47071189c3881aae219225
--- /dev/null
+++ b/src/lsp/providers/diagnostics/getInvalidScreenDiagnostics.ts
@@ -0,0 +1,75 @@
+import { State, Settings } from '../../util/state'
+import { TextDocument, Range, DiagnosticSeverity } from 'vscode-languageserver'
+import { InvalidScreenDiagnostic, DiagnosticKind } from './types'
+import { isCssDoc } from '../../util/css'
+import { getLanguageBoundaries } from '../../util/getLanguageBoundaries'
+import { findAll, indexToPosition } from '../../util/find'
+import { closest } from '../../util/closest'
+import { absoluteRange } from '../../util/absoluteRange'
+const dlv = require('dlv')
+
+export function getInvalidScreenDiagnostics(
+ state: State,
+ document: TextDocument,
+ settings: Settings
+): InvalidScreenDiagnostic[] {
+ let severity = settings.lint.invalidScreen
+ if (severity === 'ignore') return []
+
+ let diagnostics: InvalidScreenDiagnostic[] = []
+ let ranges: Range[] = []
+
+ if (isCssDoc(state, document)) {
+ ranges.push(undefined)
+ } else {
+ let boundaries = getLanguageBoundaries(state, document)
+ if (!boundaries) return []
+ ranges.push(...boundaries.css)
+ }
+
+ ranges.forEach((range) => {
+ let text = document.getText(range)
+ let matches = findAll(/(?:\s|^)@screen\s+(?<screen>[^\s{]+)/g, text)
+
+ let screens = Object.keys(
+ dlv(state.config, 'theme.screens', dlv(state.config, 'screens', {}))
+ )
+
+ matches.forEach((match) => {
+ if (screens.includes(match.groups.screen)) {
+ return null
+ }
+
+ let message = `The screen '${match.groups.screen}' does not exist in your theme config.`
+ let suggestions: string[] = []
+ let suggestion = closest(match.groups.screen, screens)
+
+ if (suggestion) {
+ suggestions.push(suggestion)
+ message += ` Did you mean '${suggestion}'?`
+ }
+
+ diagnostics.push({
+ code: DiagnosticKind.InvalidScreen,
+ range: absoluteRange(
+ {
+ start: indexToPosition(
+ text,
+ match.index + match[0].length - match.groups.screen.length
+ ),
+ end: indexToPosition(text, match.index + match[0].length),
+ },
+ range
+ ),
+ severity:
+ severity === 'error'
+ ? DiagnosticSeverity.Error
+ : DiagnosticSeverity.Warning,
+ message,
+ suggestions,
+ })
+ })
+ })
+
+ return diagnostics
+}
diff --git a/src/lsp/providers/diagnostics/getInvalidTailwindDirectiveDiagnostics.ts b/src/lsp/providers/diagnostics/getInvalidTailwindDirectiveDiagnostics.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9b88bdb06e0e5c3754c6c5eecbf23af7c342a21d
--- /dev/null
+++ b/src/lsp/providers/diagnostics/getInvalidTailwindDirectiveDiagnostics.ts
@@ -0,0 +1,83 @@
+import { State, Settings } from '../../util/state'
+import { TextDocument, Range, DiagnosticSeverity } from 'vscode-languageserver'
+import { InvalidTailwindDirectiveDiagnostic, DiagnosticKind } from './types'
+import { isCssDoc } from '../../util/css'
+import { getLanguageBoundaries } from '../../util/getLanguageBoundaries'
+import { findAll, indexToPosition } from '../../util/find'
+import semver from 'semver'
+import { closest } from '../../util/closest'
+import { absoluteRange } from '../../util/absoluteRange'
+
+export function getInvalidTailwindDirectiveDiagnostics(
+ state: State,
+ document: TextDocument,
+ settings: Settings
+): InvalidTailwindDirectiveDiagnostic[] {
+ let severity = settings.lint.invalidTailwindDirective
+ if (severity === 'ignore') return []
+
+ let diagnostics: InvalidTailwindDirectiveDiagnostic[] = []
+ let ranges: Range[] = []
+
+ if (isCssDoc(state, document)) {
+ ranges.push(undefined)
+ } else {
+ let boundaries = getLanguageBoundaries(state, document)
+ if (!boundaries) return []
+ ranges.push(...boundaries.css)
+ }
+
+ ranges.forEach((range) => {
+ let text = document.getText(range)
+ let matches = findAll(/(?:\s|^)@tailwind\s+(?<value>[^;]+)/g, text)
+
+ let valid = [
+ 'utilities',
+ 'components',
+ 'screens',
+ semver.gte(state.version, '1.0.0-beta.1') ? 'base' : 'preflight',
+ ]
+
+ matches.forEach((match) => {
+ if (valid.includes(match.groups.value)) {
+ return null
+ }
+
+ let message = `'${match.groups.value}' is not a valid group.`
+ let suggestions: string[] = []
+
+ if (match.groups.value === 'preflight') {
+ suggestions.push('base')
+ message += ` Did you mean 'base'?`
+ } else {
+ let suggestion = closest(match.groups.value, valid)
+ if (suggestion) {
+ suggestions.push(suggestion)
+ message += ` Did you mean '${suggestion}'?`
+ }
+ }
+
+ diagnostics.push({
+ code: DiagnosticKind.InvalidTailwindDirective,
+ range: absoluteRange(
+ {
+ start: indexToPosition(
+ text,
+ match.index + match[0].length - match.groups.value.length
+ ),
+ end: indexToPosition(text, match.index + match[0].length),
+ },
+ range
+ ),
+ severity:
+ severity === 'error'
+ ? DiagnosticSeverity.Error
+ : DiagnosticSeverity.Warning,
+ message,
+ suggestions,
+ })
+ })
+ })
+
+ return diagnostics
+}
diff --git a/src/lsp/providers/diagnostics/getInvalidVariantDiagnostics.ts b/src/lsp/providers/diagnostics/getInvalidVariantDiagnostics.ts
new file mode 100644
index 0000000000000000000000000000000000000000..006740103c2776d4d68d6452dd5f808b8100a5d2
--- /dev/null
+++ b/src/lsp/providers/diagnostics/getInvalidVariantDiagnostics.ts
@@ -0,0 +1,77 @@
+import { State, Settings } from '../../util/state'
+import { TextDocument, Range, DiagnosticSeverity } from 'vscode-languageserver'
+import { InvalidVariantDiagnostic, DiagnosticKind } from './types'
+import { isCssDoc } from '../../util/css'
+import { getLanguageBoundaries } from '../../util/getLanguageBoundaries'
+import { findAll, indexToPosition } from '../../util/find'
+import { closest } from '../../util/closest'
+import { absoluteRange } from '../../util/absoluteRange'
+
+export function getInvalidVariantDiagnostics(
+ state: State,
+ document: TextDocument,
+ settings: Settings
+): InvalidVariantDiagnostic[] {
+ let severity = settings.lint.invalidVariant
+ if (severity === 'ignore') return []
+
+ let diagnostics: InvalidVariantDiagnostic[] = []
+ let ranges: Range[] = []
+
+ if (isCssDoc(state, document)) {
+ ranges.push(undefined)
+ } else {
+ let boundaries = getLanguageBoundaries(state, document)
+ if (!boundaries) return []
+ ranges.push(...boundaries.css)
+ }
+
+ ranges.forEach((range) => {
+ let text = document.getText(range)
+ let matches = findAll(/(?:\s|^)@variants\s+(?<variants>[^{]+)/g, text)
+
+ matches.forEach((match) => {
+ let variants = match.groups.variants.split(/(\s*,\s*)/)
+ let listStartIndex =
+ match.index + match[0].length - match.groups.variants.length
+
+ for (let i = 0; i < variants.length; i += 2) {
+ let variant = variants[i].trim()
+ if (state.variants.includes(variant)) {
+ continue
+ }
+
+ let message = `The variant '${variant}' does not exist.`
+ let suggestions: string[] = []
+ let suggestion = closest(variant, state.variants)
+
+ if (suggestion) {
+ suggestions.push(suggestion)
+ message += ` Did you mean '${suggestion}'?`
+ }
+
+ let variantStartIndex =
+ listStartIndex + variants.slice(0, i).join('').length
+
+ diagnostics.push({
+ code: DiagnosticKind.InvalidVariant,
+ range: absoluteRange(
+ {
+ start: indexToPosition(text, variantStartIndex),
+ end: indexToPosition(text, variantStartIndex + variant.length),
+ },
+ range
+ ),
+ severity:
+ severity === 'error'
+ ? DiagnosticSeverity.Error
+ : DiagnosticSeverity.Warning,
+ message,
+ suggestions,
+ })
+ }
+ })
+ })
+
+ return diagnostics
+}
diff --git a/src/lsp/providers/diagnostics/getUtilityConflictDiagnostics.ts b/src/lsp/providers/diagnostics/getUtilityConflictDiagnostics.ts
new file mode 100644
index 0000000000000000000000000000000000000000..80216e09d137b326a30211fcb7283dc32c2fea8d
--- /dev/null
+++ b/src/lsp/providers/diagnostics/getUtilityConflictDiagnostics.ts
@@ -0,0 +1,85 @@
+import { joinWithAnd } from '../../util/joinWithAnd'
+import { State, Settings } from '../../util/state'
+import { TextDocument, DiagnosticSeverity } from 'vscode-languageserver'
+import { UtilityConflictsDiagnostic, DiagnosticKind } from './types'
+import {
+ findClassListsInDocument,
+ getClassNamesInClassList,
+} from '../../util/find'
+import { getClassNameDecls } from '../../util/getClassNameDecls'
+import { getClassNameMeta } from '../../util/getClassNameMeta'
+import { equal } from '../../../util/array'
+
+export function getUtilityConflictDiagnostics(
+ state: State,
+ document: TextDocument,
+ settings: Settings
+): UtilityConflictsDiagnostic[] {
+ let severity = settings.lint.utilityConflicts
+ if (severity === 'ignore') return []
+
+ let diagnostics: UtilityConflictsDiagnostic[] = []
+ const classLists = findClassListsInDocument(state, document)
+
+ classLists.forEach((classList) => {
+ const classNames = getClassNamesInClassList(classList)
+
+ classNames.forEach((className, index) => {
+ let decls = getClassNameDecls(state, className.className)
+ if (!decls) return
+
+ let properties = Object.keys(decls)
+ let meta = getClassNameMeta(state, className.className)
+
+ let otherClassNames = classNames.filter((_className, i) => i !== index)
+
+ let conflictingClassNames = otherClassNames.filter((otherClassName) => {
+ let otherDecls = getClassNameDecls(state, otherClassName.className)
+ if (!otherDecls) return false
+
+ let otherMeta = getClassNameMeta(state, otherClassName.className)
+
+ return (
+ equal(properties, Object.keys(otherDecls)) &&
+ !Array.isArray(meta) &&
+ !Array.isArray(otherMeta) &&
+ equal(meta.context, otherMeta.context) &&
+ equal(meta.pseudo, otherMeta.pseudo)
+ )
+ })
+
+ if (conflictingClassNames.length === 0) return
+
+ diagnostics.push({
+ code: DiagnosticKind.UtilityConflicts,
+ className,
+ otherClassNames: conflictingClassNames,
+ range: className.range,
+ severity:
+ severity === 'error'
+ ? DiagnosticSeverity.Error
+ : DiagnosticSeverity.Warning,
+ message: `'${className.className}' applies the same CSS ${
+ properties.length === 1 ? 'property' : 'properties'
+ } as ${joinWithAnd(
+ conflictingClassNames.map(
+ (conflictingClassName) => `'${conflictingClassName.className}'`
+ )
+ )}.`,
+ relatedInformation: conflictingClassNames.map(
+ (conflictingClassName) => {
+ return {
+ message: conflictingClassName.className,
+ location: {
+ uri: document.uri,
+ range: conflictingClassName.range,
+ },
+ }
+ }
+ ),
+ })
+ })
+ })
+
+ return diagnostics
+}
diff --git a/src/lsp/server.ts b/src/lsp/server.ts
index f4b8013534ea0de51cbb9c67f30c90d3187afb76..d543c304df5ad3c9d358ac820cd530ea6e4b5bb6 100644
--- a/src/lsp/server.ts
+++ b/src/lsp/server.ts
@@ -34,7 +34,7 @@ updateAllDiagnostics,
clearAllDiagnostics,
} from './providers/diagnostics/diagnosticsProvider'
import { createEmitter } from '../lib/emitter'
-import { provideCodeActions } from './providers/codeActionProvider'
+import { provideCodeActions } from './providers/codeActions/codeActionProvider'
let connection = createConnection(ProposedFeatures.all)
let state: State = { enabled: false, emitter: createEmitter(connection) }