diff --git a/src/lsp/providers/codeActions/codeActionProvider.ts b/src/lsp/providers/codeActions/codeActionProvider.ts
index 62d2f09b41a553dac7bec6db1f963d07d5102b6c..1ad8c557040e553c6d546be503df3ad396ba6f89 100644
--- a/src/lsp/providers/codeActions/codeActionProvider.ts
+++ b/src/lsp/providers/codeActions/codeActionProvider.ts
@@ -1,20 +1,7 @@
-import {
CodeAction,
- CodeActionParams,
- CodeActionKind,
- Range,
TextEdit,
-} from 'vscode-languageserver'
-import { State } from '../../util/state'
-import { isWithinRange } from '../../util/isWithinRange'
-import { getClassNameParts } from '../../util/getClassNameAtPosition'
-const dlv = require('dlv')
-import dset from 'dset'
-import { removeRangesFromString } from '../../util/removeRangesFromString'
-import {
CodeActionKind,
-import { cssObjToAst } from '../../util/cssObjToAst'
-import isObject from '../../../util/isObject'
+import { State } from '../../util/state'
import { getDiagnostics } from '../diagnostics/diagnosticsProvider'
import { rangesEqual } from '../../util/rangesEqual'
import {
@@ -22,24 +9,18 @@ DiagnosticKind,
isInvalidApplyDiagnostic,
AugmentedDiagnostic,
CodeAction,
- CodeAction,
- CodeAction,
CodeActionParams,
CodeAction,
- CodeActionKind,
- CodeAction,
Range,
isInvalidTailwindDirectiveDiagnostic,
isInvalidScreenDiagnostic,
isInvalidVariantDiagnostic,
} from '../diagnostics/types'
import { flatten, dedupeBy } from '../../../util/array'
-import { joinWithAnd } from '../../util/joinWithAnd'
+import { provideUtilityConflictsCodeActions } from './provideUtilityConflictsCodeActions'
- CodeActionParams,
CodeAction,
-import { isCssDoc } from '../../util/css'
-import { absoluteRange } from '../../util/absoluteRange'
+ )
-import type { NodeSource, Root } from 'postcss'
+import { provideSuggestionCodeActions } from './provideSuggestionCodeActions'
async function getDiagnosticsFromCodeActionParams(
state: State,
@@ -66,349 +47,75 @@ export async function provideCodeActions(
state: State,
params: CodeActionParams
): Promise<CodeAction[]> {
- let codes = params.context.diagnostics
- .map((diagnostic) => diagnostic.code)
- .filter(Boolean) as DiagnosticKind[]
-
let diagnostics = await getDiagnosticsFromCodeActionParams(
state,
params,
- codes
- )
-
- let actions = diagnostics.map((diagnostic) => {
- if (isInvalidApplyDiagnostic(diagnostic)) {
- return provideInvalidApplyCodeActions(state, params, diagnostic)
- }
-
- if (isUtilityConflictsDiagnostic(diagnostic)) {
-} from 'vscode-languageserver'
CodeAction,
- }
- CodeActionParams,
TextEdit,
- if (
- isInvalidConfigPathDiagnostic(diagnostic) ||
- isInvalidTailwindDirectiveDiagnostic(diagnostic) ||
- isInvalidScreenDiagnostic(diagnostic) ||
- isInvalidVariantDiagnostic(diagnostic)
-} from 'vscode-languageserver'
import { State } from '../../util/state'
- return diagnostic.suggestions.map((suggestion) => ({
- title: `Replace with '${suggestion}'`,
- kind: CodeActionKind.QuickFix,
-import { State } from '../../util/state'
CodeAction,
- edit: {
- changes: {
- [params.textDocument.uri]: [
-import { State } from '../../util/state'
TextEdit,
- range: diagnostic.range,
- newText: suggestion,
-import { State } from '../../util/state'
import { isWithinRange } from '../../util/isWithinRange'
- ],
- },
-import { isWithinRange } from '../../util/isWithinRange'
CodeAction,
- }))
}
- CodeActionParams,
TextEdit,
- return []
- })
- CodeActionParams,
TextEdit,
- return Promise.all(actions)
- .then(flatten)
- .then((x) => dedupeBy(x, (item) => JSON.stringify(item.edit)))
-}
-function classNameToAst(
- state: State,
- classNameParts: string[],
- selector: string,
-import { getClassNameParts } from '../../util/getClassNameAtPosition'
CodeAction,
-) {
- const baseClassName = dlv(
- state.classNames.classNames,
- classNameParts[classNameParts.length - 1]
- )
-import { getClassNameParts } from '../../util/getClassNameAtPosition'
} from 'vscode-languageserver'
import {
-import { State } from '../../util/state'
- }
- const info = dlv(state.classNames.classNames, classNameParts)
- let context = info.__context || []
-const dlv = require('dlv')
CodeAction,
- const globalContexts = state.classNames.context
- let screens = dlv(
- state.config,
- 'theme.screens',
-const dlv = require('dlv')
} from 'vscode-languageserver'
- )
- if (!isObject(screens)) screens = {}
- screens = Object.keys(screens)
-import {
CodeAction,
-
- for (let i = 0; i < classNameParts.length - 1; i++) {
- let part = classNameParts[i]
- let common = globalContexts[part]
- if (!common) return null
- if (screens.includes(part)) {
- path.push(`@screen ${part}`)
-import {
isInvalidScreenDiagnostic,
- }
- }
CodeActionParams,
- TextEdit,
-import {
CodeAction,
-import { State } from '../../util/state'
-
- 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 ${classNameParts[classNameParts.length - 1]}${
- important ? ' !important' : ''
-import { removeRangesFromString } from '../../util/removeRangesFromString'
} from 'vscode-languageserver'
- },
- }
- if (path.length) {
-import {
CodeActionKind,
- } else {
-import detectIndent from 'detect-indent'
CodeAction,
- }
-
- return cssObjToAst(obj, state.modules.postcss)
- Range,
+} from 'vscode-languageserver'
Range,
-async function provideUtilityConflictsCodeActions(
- state: State,
- params: CodeActionParams,
- diagnostic: UtilityConflictsDiagnostic
-): Promise<CodeAction[]> {
- return [
- {
- title: `Delete ${joinWithAnd(
- diagnostic.otherClassNames.map(
- (otherClassName) => `'${otherClassName.className}'`
- )
- )}`,
-import { cssObjToAst } from '../../util/cssObjToAst'
CodeAction,
- diagnostics: [diagnostic],
- edit: {
- changes: {
- [params.textDocument.uri]: [
-import { cssObjToAst } from '../../util/cssObjToAst'
} from 'vscode-languageserver'
- range: diagnostic.className.classList.range,
- newText: removeRangesFromString(
-import {
TextEdit,
- diagnostic.otherClassNames.map(
-import isObject from '../../../util/isObject'
CodeAction,
- )
- ),
- },
- ],
- },
-import isObject from '../../../util/isObject'
} from 'vscode-languageserver'
- },
- ]
-}
-
-function postcssSourceToRange(source: NodeSource): Range {
-import {
} from 'vscode-languageserver'
- start: {
-import { getDiagnostics } from '../diagnostics/diagnosticsProvider'
CodeAction,
-import {
} from 'vscode-languageserver'
- CodeActionParams,
- },
- end: {
- line: source.end.line - 1,
- character: source.end.column,
- },
- }
- Range,
Range,
-async function provideInvalidApplyCodeActions(
- state: State,
- params: CodeActionParams,
-import {
+ CodeAction,
) {
-): Promise<CodeAction[]> {
- CodeActionKind,
CodeAction,
-import {
return diagnostic.suggestions.map((suggestion) => ({
-import {
+ CodeAction,
title: `Replace with '${suggestion}'`,
- let cssText = documentText
-import { rangesEqual } from '../../util/rangesEqual'
CodeAction,
-import {
import { State } from '../../util/state'
- CodeActionParams,
-
import {
- changes: {
- /\s+/
- ).length
-
- let className = diagnostic.className.className
- let classNameParts = getClassNameParts(state, className)
- let classNameInfo = dlv(state.classNames.classNames, classNameParts)
-
- if (Array.isArray(classNameInfo)) {
- return []
- }
-
- if (!isCssDoc(state, document)) {
- DiagnosticKind,
CodeAction,
- if (!languageBoundaries) return []
- cssRange = languageBoundaries.css.find((range) =>
- isWithinRange(diagnostic.range.start, range)
- )
- if (!cssRange) return []
- DiagnosticKind,
import { State } from '../../util/state'
- }
-
- try {
CodeAction,
CodeAction,
-import {
- return (root: Root) => {
- root.walkRules((rule) => {
- if (changes.length) return false
-
- rule.walkAtRules('apply', (atRule) => {
- let atRuleRange = postcssSourceToRange(atRule.source)
- if (cssRange) {
- isInvalidApplyDiagnostic,
import { State } from '../../util/state'
- }
CodeActionParams,
- TextEdit,
- if (!isWithinRange(diagnostic.range.start, atRuleRange))
- return true
-
- let ast = classNameToAst(
- state,
- classNameParts,
- rule.selector,
- diagnostic.className.classList.important
- )
-
CodeAction,
-import {
import { State } from '../../util/state'
-
- rule.after(ast.nodes)
- let insertedRule = rule.next()
- if (!insertedRule) return false
-
- if (totalClassNamesInClassList === 1) {
- atRule.remove()
- InvalidApplyDiagnostic,
CodeActionKind,
CodeAction,
- isInvalidConfigPathDiagnostic,
- range: diagnostic.className.classList.range,
- InvalidApplyDiagnostic,
} from 'vscode-languageserver'
- diagnostic.className.classList.classList,
- diagnostic.className.relativeRange
- ),
- })
- }
-
- let ruleRange = postcssSourceToRange(rule.source)
- if (cssRange) {
- ruleRange = absoluteRange(ruleRange, cssRange)
- }
-
- let outputIndent: string
- isUtilityConflictsDiagnostic,
Range,
CodeAction,
-
- range: ruleRange,
- isUtilityConflictsDiagnostic,
import { State } from '../../util/state'
- rule.toString() +
- (insertedRule.raws.before || '\n\n') +
- insertedRule
- .toString()
- .replace(/\n\s*\n/g, '\n')
- .replace(/(@apply [^;\n]+)$/gm, '$1;')
- UtilityConflictsDiagnostic,
Range,
- .replace(/^\s+/gm, (m: string) => {
- if (typeof outputIndent === 'undefined') outputIndent = m
- return m.replace(
- new RegExp(outputIndent, 'g'),
- documentIndent.indent
- )
- }),
- CodeAction,
})
- CodeActionParams,
TextEdit,
- return false
- })
- isInvalidConfigPathDiagnostic,
TextEdit,
- }
- }),
- isInvalidConfigPathDiagnostic,
import { isWithinRange } from '../../util/isWithinRange'
- } catch (_) {
- return []
- }
-
- if (!changes.length) {
- return []
- }
-
- return [
-import detectIndent from 'detect-indent'
} from 'vscode-languageserver'
- title: 'Extract to new rule',
- kind: CodeActionKind.QuickFix,
- diagnostics: [diagnostic],
- edit: {
- changes: {
- [params.textDocument.uri]: changes,
import { isWithinRange } from '../../util/isWithinRange'
- CodeAction,
- },
- },
-import isObject from '../../../util/isObject'
import { State } from '../../util/state'
}
diff --git a/src/lsp/providers/codeActions/provideInvalidApplyCodeActions.ts b/src/lsp/providers/codeActions/provideInvalidApplyCodeActions.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a0412aa590bac8f9d6c618571ce48593efbf7223
--- /dev/null
+++ b/src/lsp/providers/codeActions/provideInvalidApplyCodeActions.ts
@@ -0,0 +1,223 @@
+import {
+ CodeAction,
+ CodeActionParams,
+ CodeActionKind,
+ TextEdit,
+ Range,
+} from 'vscode-languageserver'
+import { State } from '../../util/state'
+import { InvalidApplyDiagnostic } from '../diagnostics/types'
+import { getClassNameParts } from '../../util/getClassNameAtPosition'
+import { getLanguageBoundaries } from '../../util/getLanguageBoundaries'
+import { isCssDoc } from '../../util/css'
+import { isWithinRange } from '../../util/isWithinRange'
+const dlv = require('dlv')
+import type { Root, NodeSource } from 'postcss'
+import { absoluteRange } from '../../util/absoluteRange'
+import { removeRangesFromString } from '../../util/removeRangesFromString'
+import detectIndent from 'detect-indent'
+import isObject from '../../../util/isObject'
+import { cssObjToAst } from '../../util/cssObjToAst'
+import dset from 'dset'
+
+export async function provideInvalidApplyCodeActions(
+ state: State,
+ params: CodeActionParams,
+ diagnostic: InvalidApplyDiagnostic
+): Promise<CodeAction[]> {
+ let document = state.editor.documents.get(params.textDocument.uri)
+ let documentText = document.getText()
+ let cssRange: Range
+ let cssText = documentText
+ const { postcss } = state.modules
+ let changes: TextEdit[] = []
+
+ let totalClassNamesInClassList = diagnostic.className.classList.classList.split(
+ /\s+/
+ ).length
+
+ let className = diagnostic.className.className
+ let classNameParts = getClassNameParts(state, className)
+ let classNameInfo = dlv(state.classNames.classNames, classNameParts)
+
+ if (Array.isArray(classNameInfo)) {
+ return []
+ }
+
+ if (!isCssDoc(state, document)) {
+ let languageBoundaries = getLanguageBoundaries(state, document)
+ if (!languageBoundaries) return []
+ cssRange = languageBoundaries.css.find((range) =>
+ isWithinRange(diagnostic.range.start, range)
+ )
+ if (!cssRange) return []
+ cssText = document.getText(cssRange)
+ }
+
+ try {
+ await postcss([
+ postcss.plugin('', (_options = {}) => {
+ return (root: Root) => {
+ root.walkRules((rule) => {
+ if (changes.length) return false
+
+ rule.walkAtRules('apply', (atRule) => {
+ let atRuleRange = postcssSourceToRange(atRule.source)
+ if (cssRange) {
+ atRuleRange = absoluteRange(atRuleRange, cssRange)
+ }
+
+ if (!isWithinRange(diagnostic.range.start, atRuleRange))
+ return true
+
+ let ast = classNameToAst(
+ state,
+ classNameParts,
+ rule.selector,
+ diagnostic.className.classList.important
+ )
+
+ if (!ast) return false
+
+ rule.after(ast.nodes)
+ let insertedRule = rule.next()
+ if (!insertedRule) return false
+
+ if (totalClassNamesInClassList === 1) {
+ atRule.remove()
+ } else {
+ changes.push({
+ range: diagnostic.className.classList.range,
+ newText: removeRangesFromString(
+ diagnostic.className.classList.classList,
+ diagnostic.className.relativeRange
+ ),
+ })
+ }
+
+ let ruleRange = postcssSourceToRange(rule.source)
+ if (cssRange) {
+ ruleRange = absoluteRange(ruleRange, cssRange)
+ }
+
+ let outputIndent: string
+ let documentIndent = detectIndent(documentText)
+
+ changes.push({
+ range: ruleRange,
+ 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(cssText, { from: undefined })
+ } catch (_) {
+ return []
+ }
+
+ if (!changes.length) {
+ return []
+ }
+
+ return [
+ {
+ title: 'Extract to new rule',
+ kind: CodeActionKind.QuickFix,
+ diagnostics: [diagnostic],
+ edit: {
+ changes: {
+ [params.textDocument.uri]: changes,
+ },
+ },
+ },
+ ]
+}
+
+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,
+ },
+ }
+}
+
+function classNameToAst(
+ state: State,
+ classNameParts: string[],
+ selector: string,
+ important: boolean = false
+) {
+ const baseClassName = dlv(
+ state.classNames.classNames,
+ classNameParts[classNameParts.length - 1]
+ )
+ if (!baseClassName) {
+ return null
+ }
+ const info = dlv(state.classNames.classNames, classNameParts)
+ 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 < classNameParts.length - 1; i++) {
+ let part = classNameParts[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 ${classNameParts[classNameParts.length - 1]}${
+ important ? ' !important' : ''
+ }`]: '',
+ },
+ }
+ if (path.length) {
+ dset(obj, path, rule)
+ } else {
+ obj = rule
+ }
+
+ return cssObjToAst(obj, state.modules.postcss)
+}