diff --git a/src/lsp/providers/codeActions/codeActionProvider.ts b/src/lsp/providers/codeActions/codeActionProvider.ts index 1ad8c557040e553c6d546be503df3ad396ba6f89..62d2f09b41a553dac7bec6db1f963d07d5102b6c 100644 --- a/src/lsp/providers/codeActions/codeActionProvider.ts +++ b/src/lsp/providers/codeActions/codeActionProvider.ts @@ -1,105 +1,435 @@ +import { + CodeAction, + CodeActionParams, + CodeActionKind, + Range, + TextEdit, + isInvalidApplyDiagnostic, import { CodeAction, CodeActionParams } from 'vscode-languageserver' import { State } from '../../util/state' +import { isWithinRange } from '../../util/isWithinRange' + isInvalidApplyDiagnostic, import { getDiagnostics } from '../diagnostics/diagnosticsProvider' + isInvalidApplyDiagnostic, import { rangesEqual } from '../../util/rangesEqual' + isInvalidApplyDiagnostic, import { + isInvalidApplyDiagnostic, DiagnosticKind, isInvalidApplyDiagnostic, + isInvalidApplyDiagnostic, + isInvalidApplyDiagnostic, AugmentedDiagnostic, + isInvalidApplyDiagnostic, isUtilityConflictsDiagnostic, +import { getDiagnostics } from '../diagnostics/diagnosticsProvider' +import { rangesEqual } from '../../util/rangesEqual' +import { + DiagnosticKind, + isInvalidApplyDiagnostic, + AugmentedDiagnostic, + InvalidApplyDiagnostic, + isUtilityConflictsDiagnostic, + UtilityConflictsDiagnostic, isInvalidConfigPathDiagnostic, isInvalidTailwindDirectiveDiagnostic, isInvalidScreenDiagnostic, isInvalidVariantDiagnostic, } from '../diagnostics/types' import { flatten, dedupeBy } from '../../../util/array' +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' import { CodeAction, CodeActionParams } from 'vscode-languageserver' + isUtilityConflictsDiagnostic, +async function getDiagnosticsFromCodeActionParams( + state: State, + params: CodeActionParams, + only?: DiagnosticKind[] +): Promise { + let document = state.editor.documents.get(params.textDocument.uri) +import { State } from '../../util/state' DiagnosticKind, import { CodeAction, CodeActionParams } from 'vscode-languageserver' + isUtilityConflictsDiagnostic, +import { State } from '../../util/state' isInvalidApplyDiagnostic, + .map((diagnostic) => { + return diagnostics.find((d) => { + return ( +import { getDiagnostics } from '../diagnostics/diagnosticsProvider' import { CodeAction, CodeActionParams } from 'vscode-languageserver' + d.message === diagnostic.message && + rangesEqual(d.range, diagnostic.range) + ) + }) + }) + .filter(Boolean) +import { getDiagnostics } from '../diagnostics/diagnosticsProvider' AugmentedDiagnostic, +export async function provideCodeActions( import { State } from '../../util/state' +import { CodeAction, CodeActionParams } from 'vscode-languageserver' +import { rangesEqual } from '../../util/rangesEqual' -import { State } from '../../util/state' +import { rangesEqual } from '../../util/rangesEqual' import { CodeAction, CodeActionParams } from 'vscode-languageserver' -import { State } from '../../util/state' + let codes = params.context.diagnostics + .map((diagnostic) => diagnostic.code) + .filter(Boolean) as DiagnosticKind[] + +import { rangesEqual } from '../../util/rangesEqual' import { State } from '../../util/state' + state, + params, + codes + ) + + let actions = diagnostics.map((diagnostic) => { + isUtilityConflictsDiagnostic, import { State } from '../../util/state' + isUtilityConflictsDiagnostic, import { getDiagnostics } from '../diagnostics/diagnosticsProvider' + } + + if (isUtilityConflictsDiagnostic(diagnostic)) { + return provideUtilityConflictsCodeActions(state, params, diagnostic) + } + + if ( + isInvalidConfigPathDiagnostic(diagnostic) || + isInvalidTailwindDirectiveDiagnostic(diagnostic) || + isInvalidScreenDiagnostic(diagnostic) || + isInvalidVariantDiagnostic(diagnostic) + isInvalidConfigPathDiagnostic, import { State } from '../../util/state' + return diagnostic.suggestions.map((suggestion) => ({ + isInvalidConfigPathDiagnostic, import { rangesEqual } from '../../util/rangesEqual' + kind: CodeActionKind.QuickFix, + diagnostics: [diagnostic], + edit: { + changes: { + [params.textDocument.uri]: [ + { + range: diagnostic.range, + isInvalidTailwindDirectiveDiagnostic, import { State } from '../../util/state' + }, + ], + isInvalidTailwindDirectiveDiagnostic, import { + }, + })) + } + + return [] + }) + +import { CodeAction, CodeActionParams } from 'vscode-languageserver' import { State } from '../../util/state' DiagnosticKind, +import { rangesEqual } from '../../util/rangesEqual' + .then((x) => dedupeBy(x, (item) => JSON.stringify(item.edit))) +} +function classNameToAst( + state: State, + classNameParts: string[], + selector: string, + important: boolean = false +) { + const baseClassName = dlv( +import { CodeAction, CodeActionParams } from 'vscode-languageserver' return params.context.diagnostics +import { CodeAction, CodeActionParams } from 'vscode-languageserver' .map((diagnostic) => { + ) +import { CodeAction, CodeActionParams } from 'vscode-languageserver' return diagnostics.find((d) => { +import { CodeAction, CodeActionParams } from 'vscode-languageserver' return ( +import { CodeAction, CodeActionParams } from 'vscode-languageserver' d.code === diagnostic.code && +import { CodeAction, CodeActionParams } from 'vscode-languageserver' d.message === diagnostic.message && +import { CodeAction, CodeActionParams } from 'vscode-languageserver' rangesEqual(d.range, diagnostic.range) +import { CodeAction, CodeActionParams } from 'vscode-languageserver' ) +import { CodeAction, CodeActionParams } from 'vscode-languageserver' }) +import { CodeAction, CodeActionParams } from 'vscode-languageserver' }) +import { CodeAction, CodeActionParams } from 'vscode-languageserver' .filter(Boolean) +import { CodeAction, CodeActionParams } from 'vscode-languageserver' } import { CodeAction, CodeActionParams } from 'vscode-languageserver' +import { getDiagnostics } from '../diagnostics/diagnosticsProvider' isUtilityConflictsDiagnostic, + ) + if (!isObject(screens)) screens = {} + screens = Object.keys(screens) + const path = [] + +} from '../diagnostics/types' import { getDiagnostics } from '../diagnostics/diagnosticsProvider' + let part = classNameParts[i] + let common = globalContexts[part] + if (!common) return null + if (screens.includes(part)) { + path.push(`@screen ${part}`) +} from '../diagnostics/types' isUtilityConflictsDiagnostic, + } + } + + path.push(...context) + + let obj = {} +import { flatten, dedupeBy } from '../../../util/array' import { State } from '../../util/state' + dset(obj, path.slice(0, i), {}) + isInvalidVariantDiagnostic, import { CodeAction, CodeActionParams } from 'vscode-languageserver' +import { flatten, dedupeBy } from '../../../util/array' import { rangesEqual } from '../../util/rangesEqual' + // TODO: use proper selector parser + [selector + pseudo.join('')]: { + [`@apply ${classNameParts[classNameParts.length - 1]}${ + important ? ' !important' : '' + }`]: '', +import { provideUtilityConflictsCodeActions } from './provideUtilityConflictsCodeActions' + } + if (path.length) { + dset(obj, path, rule) + } else { +import { provideUtilityConflictsCodeActions } from './provideUtilityConflictsCodeActions' import { rangesEqual } from '../../util/rangesEqual' + } import { CodeAction, CodeActionParams } from 'vscode-languageserver' + isUtilityConflictsDiagnostic, + return cssObjToAst(obj, state.modules.postcss) +} + +async function provideUtilityConflictsCodeActions( + state: State, + params: CodeActionParams, + diagnostic: UtilityConflictsDiagnostic import { rangesEqual } from '../../util/rangesEqual' +import { CodeAction, CodeActionParams } from 'vscode-languageserver' + return [ + { + title: `Delete ${joinWithAnd( + diagnostic.otherClassNames.map( +import { provideInvalidApplyCodeActions } from './provideInvalidApplyCodeActions' import { State } from '../../util/state' +import { getDiagnostics } from '../diagnostics/diagnosticsProvider' import { rangesEqual } from '../../util/rangesEqual' +import { provideInvalidApplyCodeActions } from './provideInvalidApplyCodeActions' import { getDiagnostics } from '../diagnostics/diagnosticsProvider' +import { provideInvalidApplyCodeActions } from './provideInvalidApplyCodeActions' import { rangesEqual } from '../../util/rangesEqual' + diagnostics: [diagnostic], + edit: { + changes: { + [params.textDocument.uri]: [ + { + range: diagnostic.className.classList.range, + newText: removeRangesFromString( + diagnostic.className.classList.classList, + diagnostic.otherClassNames.map( +import { provideSuggestionCodeActions } from './provideSuggestionCodeActions' import { rangesEqual } from '../../util/rangesEqual' + ) + ), + }, + ], + }, + }, + }, + ] +} + +function postcssSourceToRange(source: NodeSource): Range { + return { + start: { + import { rangesEqual } from '../../util/rangesEqual' + import { + }, + end: { + line: source.end.line - 1, + character: source.end.column, + }, + } +} + +async function provideInvalidApplyCodeActions( + state: State, + params: CodeActionParams, + diagnostic: InvalidApplyDiagnostic import { rangesEqual } from '../../util/rangesEqual' +import { CodeAction, CodeActionParams } from 'vscode-languageserver' + 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[] = [] + +async function getDiagnosticsFromCodeActionParams( DiagnosticKind, + /\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)) { + state: State, import { rangesEqual } from '../../util/rangesEqual' + if (!languageBoundaries) return [] + cssRange = languageBoundaries.css.find((range) => + state: State, isInvalidApplyDiagnostic, + ) + if (!cssRange) return [] + cssText = document.getText(cssRange) + } + + try { + await postcss([ + postcss.plugin('', (_options = {}) => { + params: CodeActionParams, import { rangesEqual } from '../../util/rangesEqual' + root.walkRules((rule) => { + if (changes.length) return false + + rule.walkAtRules('apply', (atRule) => { + params: CodeActionParams, AugmentedDiagnostic, + if (cssRange) { + atRuleRange = absoluteRange(atRuleRange, cssRange) + } + if (!isWithinRange(diagnostic.range.start, atRuleRange)) + return true + + only?: DiagnosticKind[] import { rangesEqual } from '../../util/rangesEqual' + state, + classNameParts, + rule.selector, + diagnostic.className.classList.important + only?: DiagnosticKind[] isUtilityConflictsDiagnostic, + + 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( +import { State } from '../../util/state' diagnostics.map((diagnostic) => { +import { State } from '../../util/state' if (isInvalidApplyDiagnostic(diagnostic)) { +import { State } from '../../util/state' return provideInvalidApplyCodeActions(state, params, diagnostic) +import { State } from '../../util/state' } + } +import { State } from '../../util/state' if (isUtilityConflictsDiagnostic(diagnostic)) { + if (cssRange) { +import { State } from '../../util/state' return provideUtilityConflictsCodeActions(state, params, diagnostic) -import { +import { State } from '../../util/state' import { getDiagnostics } from '../diagnostics/diagnosticsProvider' +import { CodeAction, CodeActionParams } from 'vscode-languageserver' +import { State } from '../../util/state' if ( +import { State } from '../../util/state' isInvalidConfigPathDiagnostic(diagnostic) || + +import { State } from '../../util/state' isInvalidTailwindDirectiveDiagnostic(diagnostic) || +import { State } from '../../util/state' isInvalidScreenDiagnostic(diagnostic) || +import { State } from '../../util/state' isInvalidVariantDiagnostic(diagnostic) +import { State } from '../../util/state' ) { +import { State } from '../../util/state' return provideSuggestionCodeActions(state, params, diagnostic) + insertedRule + .toString() + let diagnostics = await getDiagnostics(state, document, only) import { + .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 params.context.diagnostics import { getDiagnostics } from '../diagnostics/diagnosticsProvider' + }), + }) + return params.context.diagnostics DiagnosticKind, + }) + }) + } + }), + ]).process(cssText, { from: undefined }) + } catch (_) { + return [] +import { CodeAction, CodeActionParams } from 'vscode-languageserver' import { getDiagnostics } from '../diagnostics/diagnosticsProvider' +import { CodeAction, CodeActionParams } from 'vscode-languageserver' + + .map((diagnostic) => { import { getDiagnostics } from '../diagnostics/diagnosticsProvider' - DiagnosticKind, -import { rangesEqual } from '../../util/rangesEqual' + isInvalidTailwindDirectiveDiagnostic, AugmentedDiagnostic, + } + +import { CodeAction, CodeActionParams } from 'vscode-languageserver' DiagnosticKind, + AugmentedDiagnostic, + { + .map((diagnostic) => { import { rangesEqual } from '../../util/rangesEqual' + kind: CodeActionKind.QuickFix, + diagnostics: [diagnostic], +import { provideInvalidApplyCodeActions } from './provideInvalidApplyCodeActions' DiagnosticKind, + changes: { + .map((diagnostic) => { import { + }, + }, + }, + ] } diff --git a/src/lsp/providers/codeActions/provideInvalidApplyCodeActions.ts b/src/lsp/providers/codeActions/provideInvalidApplyCodeActions.ts deleted file mode 100644 index a0412aa590bac8f9d6c618571ce48593efbf7223..0000000000000000000000000000000000000000 --- a/src/lsp/providers/codeActions/provideInvalidApplyCodeActions.ts +++ /dev/null @@ -1,223 +0,0 @@ -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 { - 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) -} diff --git a/src/lsp/providers/codeActions/provideSuggestionCodeActions.ts b/src/lsp/providers/codeActions/provideSuggestionCodeActions.ts deleted file mode 100644 index 9da5fcb58f4863d4f6edc2643319263f95c22aa6..0000000000000000000000000000000000000000 --- a/src/lsp/providers/codeActions/provideSuggestionCodeActions.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { State } from '../../util/state' -import { - CodeActionParams, - CodeAction, - CodeActionKind, -} from 'vscode-languageserver' -import { - InvalidConfigPathDiagnostic, - InvalidTailwindDirectiveDiagnostic, - InvalidScreenDiagnostic, - InvalidVariantDiagnostic, -} from '../diagnostics/types' - -export function provideSuggestionCodeActions( - _state: State, - params: CodeActionParams, - diagnostic: - | InvalidConfigPathDiagnostic - | InvalidTailwindDirectiveDiagnostic - | InvalidScreenDiagnostic - | InvalidVariantDiagnostic -): CodeAction[] { - return diagnostic.suggestions.map((suggestion) => ({ - title: `Replace with '${suggestion}'`, - kind: CodeActionKind.QuickFix, - diagnostics: [diagnostic], - edit: { - changes: { - [params.textDocument.uri]: [ - { - range: diagnostic.range, - newText: suggestion, - }, - ], - }, - }, - })) -} diff --git a/src/lsp/providers/codeActions/provideUtilityConflictsCodeActions.ts b/src/lsp/providers/codeActions/provideUtilityConflictsCodeActions.ts deleted file mode 100644 index 007cd88bcd1c777298102f6e73260f3c62ea0178..0000000000000000000000000000000000000000 --- a/src/lsp/providers/codeActions/provideUtilityConflictsCodeActions.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { State } from '../../util/state' -import { - CodeActionParams, - CodeAction, - CodeActionKind, -} from 'vscode-languageserver' -import { UtilityConflictsDiagnostic } from '../diagnostics/types' -import { joinWithAnd } from '../../util/joinWithAnd' -import { removeRangesFromString } from '../../util/removeRangesFromString' - -export async function provideUtilityConflictsCodeActions( - _state: State, - params: CodeActionParams, - diagnostic: UtilityConflictsDiagnostic -): Promise { - return [ - { - title: `Delete ${joinWithAnd( - diagnostic.otherClassNames.map( - (otherClassName) => `'${otherClassName.className}'` - ) - )}`, - kind: CodeActionKind.QuickFix, - diagnostics: [diagnostic], - edit: { - changes: { - [params.textDocument.uri]: [ - { - range: diagnostic.className.classList.range, - newText: removeRangesFromString( - diagnostic.className.classList.classList, - diagnostic.otherClassNames.map( - (otherClassName) => otherClassName.relativeRange - ) - ), - }, - ], - }, - }, - }, - ] -}