Home

tailwind-ctp-intellisense @master - refs - log -
-
https://git.jolheiser.com/tailwind-ctp-intellisense.git
Tailwind intellisense + Catppuccin
tree log patch
tidy up
Brad Cornes <bradlc41@gmail.com>
4 years ago
9 changed files, 575 additions(+), 560 deletions(-)
M src/lsp/providers/codeActionProvider/index.ts -> src/lsp/providers/codeActions/codeActionProvider.ts
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,
-          ],
         },
       },
     },
M src/lsp/providers/diagnostics/diagnosticsProvider.ts -> src/lsp/providers/diagnostics/diagnosticsProvider.ts
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,
I src/lsp/providers/diagnostics/getInvalidApplyDiagnostics.ts
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)
+}
I src/lsp/providers/diagnostics/getInvalidConfigPathDiagnostics.ts
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
+}
I src/lsp/providers/diagnostics/getInvalidScreenDiagnostics.ts
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
+}
I src/lsp/providers/diagnostics/getInvalidTailwindDirectiveDiagnostics.ts
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
+}
I src/lsp/providers/diagnostics/getInvalidVariantDiagnostics.ts
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
+}
I src/lsp/providers/diagnostics/getUtilityConflictDiagnostics.ts
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
+}
M src/lsp/server.ts -> src/lsp/server.ts
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) }