From 7b916ab3c6d14c16d2e7b8beba51ff4c84038412 Mon Sep 17 00:00:00 2001 From: Brad Cornes Date: Thu, 18 Jun 2020 14:22:28 +0100 Subject: [PATCH] consolidate conflict diagnostics and update quick fixes --- src/lsp/providers/codeActionProvider/index.ts | 35 ++++------ .../diagnostics/diagnosticsProvider.ts | 70 +++++++++++-------- src/lsp/providers/diagnostics/types.ts | 2 +- src/lsp/util/joinWithAnd.ts | 11 +++ src/lsp/util/removeRangeFromString.ts | 16 ----- src/lsp/util/removeRangesFromString.ts | 39 +++++++++++ 6 files changed, 105 insertions(+), 68 deletions(-) create mode 100644 src/lsp/util/joinWithAnd.ts delete mode 100644 src/lsp/util/removeRangeFromString.ts create mode 100644 src/lsp/util/removeRangesFromString.ts diff --git a/src/lsp/providers/codeActionProvider/index.ts b/src/lsp/providers/codeActionProvider/index.ts index 959411a..5bced1d 100644 --- a/src/lsp/providers/codeActionProvider/index.ts +++ b/src/lsp/providers/codeActionProvider/index.ts @@ -11,7 +11,7 @@ import { isWithinRange } from '../../util/isWithinRange' import { getClassNameParts } from '../../util/getClassNameAtPosition' const dlv = require('dlv') import dset from 'dset' -import { removeRangeFromString } from '../../util/removeRangeFromString' +import { removeRangesFromString } from '../../util/removeRangesFromString' import detectIndent from 'detect-indent' import { cssObjToAst } from '../../util/cssObjToAst' import isObject from '../../../util/isObject' @@ -26,6 +26,7 @@ import { UtilityConflictsDiagnostic, } from '../diagnostics/types' import { flatten, dedupeBy } from '../../../util/array' +import { joinWithAnd } from '../../util/joinWithAnd' async function getDiagnosticsFromCodeActionParams( state: State, @@ -174,7 +175,11 @@ async function provideUtilityConflictsCodeActions( ): Promise { return [ { - title: `Delete '${diagnostic.className.className}'`, + title: `Delete ${joinWithAnd( + diagnostic.otherClassNames.map( + (otherClassName) => `'${otherClassName.className}'` + ) + )}`, kind: CodeActionKind.QuickFix, diagnostics: [diagnostic], edit: { @@ -182,27 +187,11 @@ async function provideUtilityConflictsCodeActions( [params.textDocument.uri]: [ { range: diagnostic.className.classList.range, - newText: removeRangeFromString( + newText: removeRangesFromString( diagnostic.className.classList.classList, - diagnostic.className.relativeRange - ), - }, - ], - }, - }, - }, - { - title: `Delete '${diagnostic.otherClassName.className}'`, - kind: CodeActionKind.QuickFix, - diagnostics: [diagnostic], - edit: { - changes: { - [params.textDocument.uri]: [ - { - range: diagnostic.className.classList.range, - newText: removeRangeFromString( - diagnostic.className.classList.classList, - diagnostic.otherClassName.relativeRange + diagnostic.otherClassNames.map( + (otherClassName) => otherClassName.relativeRange + ) ), }, ], @@ -323,7 +312,7 @@ async function provideInvalidApplyCodeActions( ? [ { range: diagnostic.className.classList.range, - newText: removeRangeFromString( + newText: removeRangesFromString( diagnostic.className.classList.classList, diagnostic.className.relativeRange ), diff --git a/src/lsp/providers/diagnostics/diagnosticsProvider.ts b/src/lsp/providers/diagnostics/diagnosticsProvider.ts index 56362d5..1c98a39 100644 --- a/src/lsp/providers/diagnostics/diagnosticsProvider.ts +++ b/src/lsp/providers/diagnostics/diagnosticsProvider.ts @@ -29,6 +29,7 @@ import { InvalidTailwindDirectiveDiagnostic, AugmentedDiagnostic, } from './types' +import { joinWithAnd } from '../../util/joinWithAnd' function getInvalidApplyDiagnostics( state: State, @@ -104,45 +105,58 @@ function getUtilityConflictDiagnostics( 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) - otherClassNames.forEach((otherClassName) => { - let decls = getClassNameDecls(state, className.className) - if (!decls) return + let conflictingClassNames = otherClassNames.filter((otherClassName) => { let otherDecls = getClassNameDecls(state, otherClassName.className) - if (!otherDecls) return + if (!otherDecls) return false - let meta = getClassNameMeta(state, className.className) let otherMeta = getClassNameMeta(state, otherClassName.className) - if ( - equal(Object.keys(decls), Object.keys(otherDecls)) && + return ( + equal(properties, Object.keys(otherDecls)) && !Array.isArray(meta) && !Array.isArray(otherMeta) && equal(meta.context, otherMeta.context) && equal(meta.pseudo, otherMeta.pseudo) - ) { - diagnostics.push({ - code: DiagnosticKind.UtilityConflicts, - className, - otherClassName, - range: className.range, - severity: - severity === 'error' - ? DiagnosticSeverity.Error - : DiagnosticSeverity.Warning, - message: `'${className.className}' and '${otherClassName.className}' apply the same CSS properties.`, - relatedInformation: [ - { - message: otherClassName.className, - location: { - uri: document.uri, - range: otherClassName.range, - }, + ) + }) + + 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, }, - ], - }) - } + } + } + ), }) }) }) diff --git a/src/lsp/providers/diagnostics/types.ts b/src/lsp/providers/diagnostics/types.ts index 684cda7..00fc979 100644 --- a/src/lsp/providers/diagnostics/types.ts +++ b/src/lsp/providers/diagnostics/types.ts @@ -13,7 +13,7 @@ export enum DiagnosticKind { export type UtilityConflictsDiagnostic = Diagnostic & { code: DiagnosticKind.UtilityConflicts className: DocumentClassName - otherClassName: DocumentClassName + otherClassNames: DocumentClassName[] } export function isUtilityConflictsDiagnostic( diff --git a/src/lsp/util/joinWithAnd.ts b/src/lsp/util/joinWithAnd.ts new file mode 100644 index 0000000..2b2efb7 --- /dev/null +++ b/src/lsp/util/joinWithAnd.ts @@ -0,0 +1,11 @@ +export function joinWithAnd(strings: string[]): string { + return strings.reduce((acc, cur, i) => { + if (i === 0) { + return cur + } + if (strings.length > 1 && i === strings.length - 1) { + return `${acc} and ${cur}` + } + return `${acc}, ${cur}` + }, '') +} diff --git a/src/lsp/util/removeRangeFromString.ts b/src/lsp/util/removeRangeFromString.ts deleted file mode 100644 index 7479373..0000000 --- a/src/lsp/util/removeRangeFromString.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Range } from 'vscode-languageserver' -import lineColumn from 'line-column' - -export function removeRangeFromString(str: string, range: Range): string { - let finder = lineColumn(str + '\n', { origin: 0 }) - let start = finder.toIndex(range.start.line, range.start.character) - let end = finder.toIndex(range.end.line, range.end.character) - for (let i = start - 1; i >= 0; i--) { - if (/\s/.test(str.charAt(i))) { - start = i - } else { - break - } - } - return (str.substr(0, start) + str.substr(end)).trim() -} diff --git a/src/lsp/util/removeRangesFromString.ts b/src/lsp/util/removeRangesFromString.ts new file mode 100644 index 0000000..f97d62b --- /dev/null +++ b/src/lsp/util/removeRangesFromString.ts @@ -0,0 +1,39 @@ +import { Range } from 'vscode-languageserver' +import lineColumn from 'line-column' +import { ensureArray } from '../../util/array' + +export function removeRangesFromString( + str: string, + rangeOrRanges: Range | Range[] +): string { + let ranges = ensureArray(rangeOrRanges) + let finder = lineColumn(str + '\n', { origin: 0 }) + let indexRanges: { start: number; end: number }[] = [] + + ranges.forEach((range) => { + let start = finder.toIndex(range.start.line, range.start.character) + let end = finder.toIndex(range.end.line, range.end.character) + for (let i = start - 1; i >= 0; i--) { + if (/\s/.test(str.charAt(i))) { + start = i + } else { + break + } + } + indexRanges.push({ start, end }) + }) + + indexRanges.sort((a, b) => a.start - b.start) + + let result = '' + let i = 0 + + indexRanges.forEach((indexRange) => { + result += str.substring(i, indexRange.start) + i = indexRange.end + }) + + result += str.substring(i) + + return result.trim() +}