diff --git a/src/lsp/providers/codeActions/codeActionProvider.ts b/src/lsp/providers/codeActionProvider/index.ts rename from src/lsp/providers/codeActions/codeActionProvider.ts rename to src/lsp/providers/codeActionProvider/index.ts index 62d2f09b41a553dac7bec6db1f963d07d5102b6c..22298dad00a8abe4bea08c7a41651849e6c5e6dd 100644 --- a/src/lsp/providers/codeActions/codeActionProvider.ts +++ b/src/lsp/providers/codeActionProvider/index.ts @@ -34,8 +34,6 @@ import { getLanguageBoundaries } from '../../util/getLanguageBoundaries' import { isCssDoc } from '../../util/css' import { absoluteRange } from '../../util/absoluteRange' CodeActionParams, - Range, - CodeActionParams, TextEdit, async function getDiagnosticsFromCodeActionParams( state: State, @@ -205,20 +203,6 @@ ] } import { - return provideInvalidApplyCodeActions(state, params, diagnostic) - return { - start: { - line: source.start.line - 1, - character: source.start.column - 1, - }, - end: { - line: source.end.line - 1, - character: source.end.column, - }, - } -} - -import { isInvalidVariantDiagnostic(diagnostic) state: State, params: CodeActionParams, @@ -229,7 +213,7 @@ let documentText = document.getText() let cssRange: Range let cssText = documentText const { postcss } = state.modules - let changes: TextEdit[] = [] + let change: TextEdit let totalClassNamesInClassList = diagnostic.className.classList.classList.split( /\s+/ @@ -257,24 +241,40 @@ try { await postcss([ postcss.plugin('', (_options = {}) => { CodeAction, + codes + CodeAction, + CodeActionParams, CodeAction, + ) + isInvalidApplyDiagnostic, - CodeActionParams, + Range, + let { start, end } = atRule.source + let atRuleRange: Range = { + start: { CodeAction, +} from 'vscode-languageserver' - CodeActionKind, + character: start.column - 1, + }, + isInvalidScreenDiagnostic, CodeActionParams, - TextEdit, + line: end.line - 1, CodeAction, +} from 'vscode-languageserver' Range, CodeAction, + return provideUtilityConflictsCodeActions(state, params, diagnostic) + CodeAction, - TextEdit, +import { isWithinRange } from '../../util/isWithinRange' if (cssRange) { atRuleRange = absoluteRange(atRuleRange, cssRange) } CodeAction, -import { getClassNameParts } from '../../util/getClassNameAtPosition' + isInvalidScreenDiagnostic(diagnostic) || + // keep looking return true + } let ast = classNameToAst( state, @@ -283,51 +284,56 @@ diagnostic.className.classList.important ) CodeAction, -import { +} from 'vscode-languageserver' import { State } from '../../util/state' + return false + } rule.after(ast.nodes) let insertedRule = rule.next() - if (!insertedRule) return false if (totalClassNamesInClassList === 1) { atRule.remove() CodeAction, +import { isWithinRange } from '../../util/isWithinRange' + CodeAction, + CodeActionParams, CodeActionKind, CodeAction, - CodeAction, + CodeActionParams, Range, + CodeAction, + title: `Replace with '${suggestion}'`, isInvalidTailwindDirectiveDiagnostic, +import { isWithinRange } from '../../util/isWithinRange' CodeAction, - isInvalidScreenDiagnostic, + kind: CodeActionKind.QuickFix, CodeAction, +import { State } from '../../util/state' CodeAction, -import { State } from '../../util/state' CodeAction, +} from 'vscode-languageserver' CodeAction, -import { isWithinRange } from '../../util/isWithinRange' CodeAction, +} from 'vscode-languageserver' CodeActionParams, CodeAction, +import { State } from '../../util/state' CodeActionParams, -import { CodeAction, -import { isWithinRange } from '../../util/isWithinRange' + changes: { - + }, CodeAction, -import { getLanguageBoundaries } from '../../util/getLanguageBoundaries' +import { isWithinRange } from '../../util/isWithinRange' if (cssRange) { ruleRange = absoluteRange(ruleRange, cssRange) } CodeAction, -import { absoluteRange } from '../../util/absoluteRange' - isUtilityConflictsDiagnostic, +import { State } from '../../util/state' Range, - - changes.push({ range: ruleRange, newText: rule.toString() + @@ -344,7 +351,7 @@ documentIndent.indent ) }), CodeAction, - }) +import { isWithinRange } from '../../util/isWithinRange' return false }) @@ -357,8 +364,8 @@ return [] } CodeAction, +import { State } from '../../util/state' TextEdit, -import { return [] } @@ -369,9 +376,22 @@ kind: CodeActionKind.QuickFix, diagnostics: [diagnostic], edit: { changes: { + [params.textDocument.uri]: [ + ...(totalClassNamesInClassList > 1 + ? [ + { + range: diagnostic.className.classList.range, + newText: removeRangesFromString( CodeAction, - TextEdit, + }, +} from '../diagnostics/types' CodeActionParams, + ), + }, + ] + : []), + change, + ], }, }, }, diff --git a/src/lsp/providers/diagnostics/diagnosticsProvider.ts b/src/lsp/providers/diagnostics/diagnosticsProvider.ts index 60956f0dd34ff63f5973d6308cd5fc47f59bfcea..91ca0e3eedd6db523fbaf2a96198b659330771ab 100644 --- a/src/lsp/providers/diagnostics/diagnosticsProvider.ts +++ b/src/lsp/providers/diagnostics/diagnosticsProvider.ts @@ -1,13 +1,526 @@ +import { TextDocument, DiagnosticSeverity, Range } from 'vscode-languageserver' +import { State, Settings } from '../../util/state' +import { isCssDoc } from '../../util/css' +import { + findClassNamesInRange, + findClassListsInDocument, +import { getInvalidApplyDiagnostics } from './getInvalidApplyDiagnostics' import { TextDocument } from 'vscode-languageserver' +import { getInvalidApplyDiagnostics } from './getInvalidApplyDiagnostics' import { State } from '../../util/state' +import { getInvalidApplyDiagnostics } from './getInvalidApplyDiagnostics' import { getDocumentSettings } from '../../util/getDocumentSettings' +import { getInvalidApplyDiagnostics } from './getInvalidApplyDiagnostics' import { DiagnosticKind, AugmentedDiagnostic } from './types' +import { getInvalidApplyDiagnostics } from './getInvalidApplyDiagnostics' import { getUtilityConflictDiagnostics } from './getUtilityConflictDiagnostics' import { getInvalidApplyDiagnostics } from './getInvalidApplyDiagnostics' +import { getInvalidApplyDiagnostics } from './getInvalidApplyDiagnostics' +import { getInvalidApplyDiagnostics } from './getInvalidApplyDiagnostics' import { getInvalidScreenDiagnostics } from './getInvalidScreenDiagnostics' +import { getDocumentSettings } from '../../util/getDocumentSettings' +import { getInvalidApplyDiagnostics } from './getInvalidApplyDiagnostics' import { getInvalidVariantDiagnostics } from './getInvalidVariantDiagnostics' +import { getInvalidApplyDiagnostics } from './getInvalidApplyDiagnostics' import { getInvalidConfigPathDiagnostics } from './getInvalidConfigPathDiagnostics' +import { getLanguageBoundaries } from '../../util/getLanguageBoundaries' +import { getInvalidScreenDiagnostics } from './getInvalidScreenDiagnostics' import { TextDocument } from 'vscode-languageserver' +import { isObject } from '../../../class-names/isObject' +import { stringToPath } from '../../util/stringToPath' +import { closest } from '../../util/closest' +import { + InvalidApplyDiagnostic, + DiagnosticKind, + UtilityConflictsDiagnostic, + InvalidScreenDiagnostic, + InvalidVariantDiagnostic, +import { getInvalidVariantDiagnostics } from './getInvalidVariantDiagnostics' + InvalidTailwindDirectiveDiagnostic, + AugmentedDiagnostic, +} from './types' +import { joinWithAnd } from '../../util/joinWithAnd' + +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) +} + +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 +} + +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+(?[^\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+(?[^{]+)/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( + /(?\s|^)(?config|theme)\((?['"])(?[^)]+)\k\)/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+(?[^;]+)/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 deleted file mode 100644 index 66fc540125ce57a44e5550b89d9b6f1551085141..0000000000000000000000000000000000000000 --- a/src/lsp/providers/diagnostics/getInvalidApplyDiagnostics.ts +++ /dev/null @@ -1,64 +0,0 @@ -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 deleted file mode 100644 index 95293341d536810ead06332a6125e96c6a77a776..0000000000000000000000000000000000000000 --- a/src/lsp/providers/diagnostics/getInvalidConfigPathDiagnostics.ts +++ /dev/null @@ -1,150 +0,0 @@ -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( - /(?\s|^)(?config|theme)\((?['"])(?[^)]+)\k\)/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 deleted file mode 100644 index b0e4252438ea6bb7dd47071189c3881aae219225..0000000000000000000000000000000000000000 --- a/src/lsp/providers/diagnostics/getInvalidScreenDiagnostics.ts +++ /dev/null @@ -1,75 +0,0 @@ -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+(?[^\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 deleted file mode 100644 index 9b88bdb06e0e5c3754c6c5eecbf23af7c342a21d..0000000000000000000000000000000000000000 --- a/src/lsp/providers/diagnostics/getInvalidTailwindDirectiveDiagnostics.ts +++ /dev/null @@ -1,83 +0,0 @@ -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+(?[^;]+)/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 deleted file mode 100644 index 006740103c2776d4d68d6452dd5f808b8100a5d2..0000000000000000000000000000000000000000 --- a/src/lsp/providers/diagnostics/getInvalidVariantDiagnostics.ts +++ /dev/null @@ -1,77 +0,0 @@ -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+(?[^{]+)/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 deleted file mode 100644 index 80216e09d137b326a30211fcb7283dc32c2fea8d..0000000000000000000000000000000000000000 --- a/src/lsp/providers/diagnostics/getUtilityConflictDiagnostics.ts +++ /dev/null @@ -1,85 +0,0 @@ -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 d543c304df5ad3c9d358ac820cd530ea6e4b5bb6..f4b8013534ea0de51cbb9c67f30c90d3187afb76 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/codeActions/codeActionProvider' +import { provideCodeActions } from './providers/codeActionProvider' let connection = createConnection(ProposedFeatures.all) let state: State = { enabled: false, emitter: createEmitter(connection) }