From b79dbfc9f9d243fd489a4b73642c8976f983d84d Mon Sep 17 00:00:00 2001 From: Brad Cornes Date: Thu, 18 Jun 2020 10:54:01 +0100 Subject: [PATCH] refactor diagnostics and add types --- src/lsp/providers/codeActionProvider/index.ts | 75 ++++--- .../{ => diagnostics}/diagnosticsProvider.ts | 195 +++++++++++------- src/lsp/providers/diagnostics/types.ts | 82 ++++++++ src/lsp/server.ts | 2 +- src/lsp/util/rangesEqual.ts | 10 + 5 files changed, 253 insertions(+), 111 deletions(-) rename src/lsp/providers/{ => diagnostics}/diagnosticsProvider.ts (71%) create mode 100644 src/lsp/providers/diagnostics/types.ts create mode 100644 src/lsp/util/rangesEqual.ts diff --git a/src/lsp/providers/codeActionProvider/index.ts b/src/lsp/providers/codeActionProvider/index.ts index 19d724e..28b5144 100644 --- a/src/lsp/providers/codeActionProvider/index.ts +++ b/src/lsp/providers/codeActionProvider/index.ts @@ -4,10 +4,9 @@ import { CodeActionKind, Range, TextEdit, - Diagnostic, } from 'vscode-languageserver' import { State } from '../../util/state' -import { findLast, findClassNamesInRange } from '../../util/find' +import { findLast } from '../../util/find' import { isWithinRange } from '../../util/isWithinRange' import { getClassNameParts } from '../../util/getClassNameAtPosition' const dlv = require('dlv') @@ -16,19 +15,50 @@ import { removeRangeFromString } from '../../util/removeRangeFromString' import detectIndent from 'detect-indent' import { cssObjToAst } from '../../util/cssObjToAst' import isObject from '../../../util/isObject' +import { getDiagnostics } from '../diagnostics/diagnosticsProvider' +import { rangesEqual } from '../../util/rangesEqual' +import { + DiagnosticKind, + isInvalidApplyDiagnostic, + AugmentedDiagnostic, + InvalidApplyDiagnostic, +} from '../diagnostics/types' -export function provideCodeActions( +async function getDiagnosticsFromCodeActionParams( + state: State, + params: CodeActionParams, + only?: DiagnosticKind[] +): Promise { + let document = state.editor.documents.get(params.textDocument.uri) + let diagnostics = await getDiagnostics(state, document, only) + + return params.context.diagnostics + .map((diagnostic) => { + return diagnostics.find((d) => { + return rangesEqual(d.range, diagnostic.range) + }) + }) + .filter(Boolean) +} + +export async function provideCodeActions( state: State, params: CodeActionParams ): Promise { - if (params.context.diagnostics.length === 0) { - return null - } + let codes = params.context.diagnostics + .map((diagnostic) => diagnostic.code) + .filter(Boolean) as DiagnosticKind[] + + let diagnostics = await getDiagnosticsFromCodeActionParams( + state, + params, + codes + ) return Promise.all( - params.context.diagnostics + diagnostics .map((diagnostic) => { - if (diagnostic.code === 'invalidApply') { + if (isInvalidApplyDiagnostic(diagnostic)) { return provideInvalidApplyCodeAction(state, params, diagnostic) } @@ -127,31 +157,14 @@ function classNameToAst( async function provideInvalidApplyCodeAction( state: State, params: CodeActionParams, - diagnostic: Diagnostic + diagnostic: InvalidApplyDiagnostic ): Promise { let document = state.editor.documents.get(params.textDocument.uri) let documentText = document.getText() const { postcss } = state.modules let change: TextEdit - let documentClassNames = findClassNamesInRange( - document, - { - start: { - line: Math.max(0, diagnostic.range.start.line - 10), - character: 0, - }, - end: { line: diagnostic.range.start.line + 10, character: 0 }, - }, - 'css' - ) - let documentClassName = documentClassNames.find((className) => - isWithinRange(diagnostic.range.start, className.range) - ) - if (!documentClassName) { - return null - } - let totalClassNamesInClassList = documentClassName.classList.classList.split( + let totalClassNamesInClassList = diagnostic.className.classList.classList.split( /\s+/ ).length @@ -184,7 +197,7 @@ async function provideInvalidApplyCodeAction( state, className, rule.selector, - documentClassName.classList.important + diagnostic.className.classList.important ) if (!ast) { @@ -250,10 +263,10 @@ async function provideInvalidApplyCodeAction( ...(totalClassNamesInClassList > 1 ? [ { - range: documentClassName.classList.range, + range: diagnostic.className.classList.range, newText: removeRangeFromString( - documentClassName.classList.classList, - documentClassName.relativeRange + diagnostic.className.classList.classList, + diagnostic.className.relativeRange ), }, ] diff --git a/src/lsp/providers/diagnosticsProvider.ts b/src/lsp/providers/diagnostics/diagnosticsProvider.ts similarity index 71% rename from src/lsp/providers/diagnosticsProvider.ts rename to src/lsp/providers/diagnostics/diagnosticsProvider.ts index f6fa909..56362d5 100644 --- a/src/lsp/providers/diagnosticsProvider.ts +++ b/src/lsp/providers/diagnostics/diagnosticsProvider.ts @@ -1,95 +1,103 @@ -import { - TextDocument, - Diagnostic, - DiagnosticSeverity, - Range, -} from 'vscode-languageserver' -import { State, Settings } from '../util/state' -import { isCssDoc } from '../util/css' +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 { getClassNameDecls } from '../util/getClassNameDecls' -import { equal } from '../../util/array' -import { getDocumentSettings } from '../util/getDocumentSettings' +} from '../../util/find' +import { getClassNameMeta } from '../../util/getClassNameMeta' +import { getClassNameDecls } from '../../util/getClassNameDecls' +import { equal } from '../../../util/array' +import { getDocumentSettings } from '../../util/getDocumentSettings' 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 { 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, + DiagnosticKind, + UtilityConflictsDiagnostic, + InvalidScreenDiagnostic, + InvalidVariantDiagnostic, + InvalidConfigPathDiagnostic, + InvalidTailwindDirectiveDiagnostic, + AugmentedDiagnostic, +} from './types' function getInvalidApplyDiagnostics( state: State, document: TextDocument, settings: Settings -): Diagnostic[] { +): InvalidApplyDiagnostic[] { let severity = settings.lint.invalidApply if (severity === 'ignore') return [] const classNames = findClassNamesInRange(document, undefined, 'css') - let diagnostics: Diagnostic[] = classNames - .map(({ className, range }) => { - const meta = getClassNameMeta(state, className) - if (!meta) return null + let diagnostics: InvalidApplyDiagnostic[] = classNames.map((className) => { + const meta = getClassNameMeta(state, className.className) + if (!meta) return null - let message: string + let message: string - if (Array.isArray(meta)) { - message = `'@apply' cannot be used with '${className}' because it is included in multiple rulesets.` - } else if (meta.source !== 'utilities') { - message = `'@apply' cannot be used with '${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}' because it is nested inside of an at-rule ('${meta.context[0]}').` - } else { - message = `'@apply' cannot be used with '${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}' because its definition includes a pseudo-selector ('${meta.pseudo[0]}')` - } else { - message = `'@apply' cannot be used with '${className}' because its definition includes pseudo-selectors (${meta.pseudo - .map((p) => `'${p}'`) - .join(', ')}).` - } + 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(', ')}).` } - - if (!message) return null - - return { - severity: - severity === 'error' - ? DiagnosticSeverity.Error - : DiagnosticSeverity.Warning, - range, - message, - code: 'invalidApply', + } 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(', ')}).` } - }) - .filter(Boolean) + } - return diagnostics + 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 -): Diagnostic[] { +): UtilityConflictsDiagnostic[] { let severity = settings.lint.utilityConflicts if (severity === 'ignore') return [] - let diagnostics: Diagnostic[] = [] + let diagnostics: UtilityConflictsDiagnostic[] = [] const classLists = findClassListsInDocument(state, document) classLists.forEach((classList) => { @@ -115,6 +123,9 @@ function getUtilityConflictDiagnostics( equal(meta.pseudo, otherMeta.pseudo) ) { diagnostics.push({ + code: DiagnosticKind.UtilityConflicts, + className, + otherClassName, range: className.range, severity: severity === 'error' @@ -143,11 +154,11 @@ function getInvalidScreenDiagnostics( state: State, document: TextDocument, settings: Settings -): Diagnostic[] { +): InvalidScreenDiagnostic[] { let severity = settings.lint.invalidScreen if (severity === 'ignore') return [] - let diagnostics: Diagnostic[] = [] + let diagnostics: InvalidScreenDiagnostic[] = [] let ranges: Range[] = [] if (isCssDoc(state, document)) { @@ -178,6 +189,7 @@ function getInvalidScreenDiagnostics( } diagnostics.push({ + code: DiagnosticKind.InvalidScreen, range: absoluteRange( { start: indexToPosition( @@ -204,11 +216,11 @@ function getInvalidVariantDiagnostics( state: State, document: TextDocument, settings: Settings -): Diagnostic[] { +): InvalidVariantDiagnostic[] { let severity = settings.lint.invalidVariant if (severity === 'ignore') return [] - let diagnostics: Diagnostic[] = [] + let diagnostics: InvalidVariantDiagnostic[] = [] let ranges: Range[] = [] if (isCssDoc(state, document)) { @@ -244,6 +256,7 @@ function getInvalidVariantDiagnostics( listStartIndex + variants.slice(0, i).join('').length diagnostics.push({ + code: DiagnosticKind.InvalidVariant, range: absoluteRange( { start: indexToPosition(text, variantStartIndex), @@ -268,11 +281,11 @@ function getInvalidConfigPathDiagnostics( state: State, document: TextDocument, settings: Settings -): Diagnostic[] { +): InvalidConfigPathDiagnostic[] { let severity = settings.lint.invalidConfigPath if (severity === 'ignore') return [] - let diagnostics: Diagnostic[] = [] + let diagnostics: InvalidConfigPathDiagnostic[] = [] let ranges: Range[] = [] if (isCssDoc(state, document)) { @@ -381,6 +394,7 @@ function getInvalidConfigPathDiagnostics( match.groups.quote.length diagnostics.push({ + code: DiagnosticKind.InvalidConfigPath, range: absoluteRange( { start: indexToPosition(text, startIndex), @@ -404,11 +418,11 @@ function getInvalidTailwindDirectiveDiagnostics( state: State, document: TextDocument, settings: Settings -): Diagnostic[] { +): InvalidTailwindDirectiveDiagnostic[] { let severity = settings.lint.invalidTailwindDirective if (severity === 'ignore') return [] - let diagnostics: Diagnostic[] = [] + let diagnostics: InvalidTailwindDirectiveDiagnostic[] = [] let ranges: Range[] = [] if (isCssDoc(state, document)) { @@ -446,6 +460,7 @@ function getInvalidTailwindDirectiveDiagnostics( } diagnostics.push({ + code: DiagnosticKind.InvalidTailwindDirective, range: absoluteRange( { start: indexToPosition( @@ -468,26 +483,48 @@ function getInvalidTailwindDirectiveDiagnostics( return diagnostics } -export async function provideDiagnostics( +export async function getDiagnostics( state: State, - document: TextDocument -): Promise { + document: TextDocument, + only: DiagnosticKind[] = [ + DiagnosticKind.UtilityConflicts, + DiagnosticKind.InvalidApply, + DiagnosticKind.InvalidScreen, + DiagnosticKind.InvalidVariant, + DiagnosticKind.InvalidConfigPath, + DiagnosticKind.InvalidTailwindDirective, + ] +): Promise { const settings = await getDocumentSettings(state, document) - const diagnostics: Diagnostic[] = settings.validate + return settings.validate ? [ - ...getUtilityConflictDiagnostics(state, document, settings), - ...getInvalidApplyDiagnostics(state, document, settings), - ...getInvalidScreenDiagnostics(state, document, settings), - ...getInvalidVariantDiagnostics(state, document, settings), - ...getInvalidConfigPathDiagnostics(state, document, settings), - ...getInvalidTailwindDirectiveDiagnostics(state, document, settings), + ...(only.includes(DiagnosticKind.UtilityConflicts) + ? getUtilityConflictDiagnostics(state, document, settings) + : []), + ...(only.includes(DiagnosticKind.InvalidApply) + ? getInvalidApplyDiagnostics(state, document, settings) + : []), + ...(only.includes(DiagnosticKind.InvalidScreen) + ? getInvalidScreenDiagnostics(state, document, settings) + : []), + ...(only.includes(DiagnosticKind.InvalidVariant) + ? getInvalidVariantDiagnostics(state, document, settings) + : []), + ...(only.includes(DiagnosticKind.InvalidConfigPath) + ? getInvalidConfigPathDiagnostics(state, document, settings) + : []), + ...(only.includes(DiagnosticKind.InvalidTailwindDirective) + ? getInvalidTailwindDirectiveDiagnostics(state, document, settings) + : []), ] : [] +} +export async function provideDiagnostics(state: State, document: TextDocument) { state.editor.connection.sendDiagnostics({ uri: document.uri, - diagnostics, + diagnostics: await getDiagnostics(state, document), }) } diff --git a/src/lsp/providers/diagnostics/types.ts b/src/lsp/providers/diagnostics/types.ts new file mode 100644 index 0000000..684cda7 --- /dev/null +++ b/src/lsp/providers/diagnostics/types.ts @@ -0,0 +1,82 @@ +import { Diagnostic } from 'vscode-languageserver' +import { DocumentClassName, DocumentClassList } from '../../util/state' + +export enum DiagnosticKind { + UtilityConflicts = 'utilityConflicts', + InvalidApply = 'invalidApply', + InvalidScreen = 'invalidScreen', + InvalidVariant = 'invalidVariant', + InvalidConfigPath = 'invalidConfigPath', + InvalidTailwindDirective = 'invalidTailwindDirective', +} + +export type UtilityConflictsDiagnostic = Diagnostic & { + code: DiagnosticKind.UtilityConflicts + className: DocumentClassName + otherClassName: DocumentClassName +} + +export function isUtilityConflictsDiagnostic( + diagnostic: AugmentedDiagnostic +): diagnostic is UtilityConflictsDiagnostic { + return diagnostic.code === DiagnosticKind.UtilityConflicts +} + +export type InvalidApplyDiagnostic = Diagnostic & { + code: DiagnosticKind.InvalidApply + className: DocumentClassName +} + +export function isInvalidApplyDiagnostic( + diagnostic: AugmentedDiagnostic +): diagnostic is InvalidApplyDiagnostic { + return diagnostic.code === DiagnosticKind.InvalidApply +} + +export type InvalidScreenDiagnostic = Diagnostic & { + code: DiagnosticKind.InvalidScreen +} + +export function isInvalidScreenDiagnostic( + diagnostic: AugmentedDiagnostic +): diagnostic is InvalidScreenDiagnostic { + return diagnostic.code === DiagnosticKind.InvalidScreen +} + +export type InvalidVariantDiagnostic = Diagnostic & { + code: DiagnosticKind.InvalidVariant +} + +export function isInvalidVariantDiagnostic( + diagnostic: AugmentedDiagnostic +): diagnostic is InvalidVariantDiagnostic { + return diagnostic.code === DiagnosticKind.InvalidVariant +} + +export type InvalidConfigPathDiagnostic = Diagnostic & { + code: DiagnosticKind.InvalidConfigPath +} + +export function isInvalidConfigPathDiagnostic( + diagnostic: AugmentedDiagnostic +): diagnostic is InvalidConfigPathDiagnostic { + return diagnostic.code === DiagnosticKind.InvalidConfigPath +} + +export type InvalidTailwindDirectiveDiagnostic = Diagnostic & { + code: DiagnosticKind.InvalidTailwindDirective +} + +export function isInvalidTailwindDirectiveDiagnostic( + diagnostic: AugmentedDiagnostic +): diagnostic is InvalidTailwindDirectiveDiagnostic { + return diagnostic.code === DiagnosticKind.InvalidTailwindDirective +} + +export type AugmentedDiagnostic = + | UtilityConflictsDiagnostic + | InvalidApplyDiagnostic + | InvalidScreenDiagnostic + | InvalidVariantDiagnostic + | InvalidConfigPathDiagnostic + | InvalidTailwindDirectiveDiagnostic diff --git a/src/lsp/server.ts b/src/lsp/server.ts index 6c928e6..f4b8013 100644 --- a/src/lsp/server.ts +++ b/src/lsp/server.ts @@ -32,7 +32,7 @@ import { provideDiagnostics, updateAllDiagnostics, clearAllDiagnostics, -} from './providers/diagnosticsProvider' +} from './providers/diagnostics/diagnosticsProvider' import { createEmitter } from '../lib/emitter' import { provideCodeActions } from './providers/codeActionProvider' diff --git a/src/lsp/util/rangesEqual.ts b/src/lsp/util/rangesEqual.ts new file mode 100644 index 0000000..220cebd --- /dev/null +++ b/src/lsp/util/rangesEqual.ts @@ -0,0 +1,10 @@ +import { Range } from 'vscode-languageserver' + +export function rangesEqual(a: Range, b: Range): boolean { + return ( + a.start.line === b.start.line && + a.start.character === b.start.character && + a.end.line === b.end.line && + a.end.character === b.end.character + ) +}