From de6273dad2cf0c9577b33c3f25b67a4e78561450 Mon Sep 17 00:00:00 2001 From: Brad Cornes Date: Fri, 12 Jun 2020 11:41:39 +0100 Subject: [PATCH] add initial class name conflict diagnostics --- src/lsp/providers/diagnosticsProvider.ts | 91 +++++++++++++++++------- src/lsp/util/find.ts | 89 +++++++++++++---------- 2 files changed, 116 insertions(+), 64 deletions(-) diff --git a/src/lsp/providers/diagnosticsProvider.ts b/src/lsp/providers/diagnosticsProvider.ts index e76bf49..9f483ac 100644 --- a/src/lsp/providers/diagnosticsProvider.ts +++ b/src/lsp/providers/diagnosticsProvider.ts @@ -5,10 +5,16 @@ import { } from 'vscode-languageserver' import { State } from '../util/state' import { isCssDoc } from '../util/css' -import { findClassNamesInRange } from '../util/find' +import { + findClassNamesInRange, + findClassListsInDocument, + getClassNamesInClassList, +} from '../util/find' import { getClassNameMeta } from '../util/getClassNameMeta' +import { getClassNameDecls } from '../util/getClassNameDecls' +import { equal } from '../../util/array' -function provideCssDiagnostics(state: State, document: TextDocument): void { +function getCssDiagnostics(state: State, document: TextDocument): Diagnostic[] { const classNames = findClassNamesInRange(document, undefined, 'css') let diagnostics: Diagnostic[] = classNames @@ -46,38 +52,73 @@ function provideCssDiagnostics(state: State, document: TextDocument): void { severity: DiagnosticSeverity.Error, range, message, - // source: 'ex', } }) .filter(Boolean) - // if (state.editor.capabilities.diagnosticRelatedInformation) { - // diagnostic.relatedInformation = [ - // { - // location: { - // uri: document.uri, - // range: Object.assign({}, diagnostic.range), - // }, - // message: '', - // }, - // { - // location: { - // uri: document.uri, - // range: Object.assign({}, diagnostic.range), - // }, - // message: '', - // }, - // ] - // } + return diagnostics +} - state.editor.connection.sendDiagnostics({ uri: document.uri, diagnostics }) +function getConflictDiagnostics( + state: State, + document: TextDocument +): Diagnostic[] { + let diagnostics: Diagnostic[] = [] + const classLists = findClassListsInDocument(state, document) + + classLists.forEach((classList) => { + const classNames = getClassNamesInClassList(classList) + + classNames.forEach((className, index) => { + let otherClassNames = classNames.filter((_className, i) => i !== index) + otherClassNames.forEach((otherClassName) => { + let decls = getClassNameDecls(state, className.className) + if (!decls) return + + let otherDecls = getClassNameDecls(state, otherClassName.className) + if (!otherDecls) return + + let meta = getClassNameMeta(state, className.className) + let otherMeta = getClassNameMeta(state, otherClassName.className) + + if ( + equal(Object.keys(decls), Object.keys(otherDecls)) && + !Array.isArray(meta) && + !Array.isArray(otherMeta) && + equal(meta.context, otherMeta.context) && + equal(meta.pseudo, otherMeta.pseudo) + ) { + diagnostics.push({ + range: className.range, + severity: DiagnosticSeverity.Warning, + message: `You can’t use \`${className.className}\` and \`${otherClassName.className}\` together`, + relatedInformation: [ + { + message: otherClassName.className, + location: { + uri: document.uri, + range: otherClassName.range, + }, + }, + ], + }) + } + }) + }) + }) + + return diagnostics } export async function provideDiagnostics( state: State, document: TextDocument ): Promise { - if (isCssDoc(state, document)) { - return provideCssDiagnostics(state, document) - } + state.editor.connection.sendDiagnostics({ + uri: document.uri, + diagnostics: [ + ...getConflictDiagnostics(state, document), + ...(isCssDoc(state, document) ? getCssDiagnostics(state, document) : []), + ], + }) } diff --git a/src/lsp/util/find.ts b/src/lsp/util/find.ts index 6b1bfca..9a04ac4 100644 --- a/src/lsp/util/find.ts +++ b/src/lsp/util/find.ts @@ -6,6 +6,7 @@ import { isHtmlContext, isHtmlDoc, isSvelteDoc, isVueDoc } from './html' import { isWithinRange } from './isWithinRange' import { isJsContext, isJsDoc } from './js' import { getClassAttributeLexer } from './lexers' +import { flatten } from '../../util/array' export function findAll(re: RegExp, str: string): RegExpMatchArray[] { let match: RegExpMatchArray @@ -24,44 +25,53 @@ export function findLast(re: RegExp, str: string): RegExpMatchArray { return matches[matches.length - 1] } +export function getClassNamesInClassList({ + classList, + range, +}: DocumentClassList): DocumentClassName[] { + const parts = classList.split(/(\s+)/) + const names: DocumentClassName[] = [] + let index = 0 + for (let i = 0; i < parts.length; i++) { + if (i % 2 === 0) { + const start = indexToPosition(classList, index) + const end = indexToPosition(classList, index + parts[i].length) + names.push({ + className: parts[i], + range: { + start: { + line: range.start.line + start.line, + character: + (end.line === 0 ? range.start.character : 0) + start.character, + }, + end: { + line: range.start.line + end.line, + character: + (end.line === 0 ? range.start.character : 0) + end.character, + }, + }, + }) + } + index += parts[i].length + } + return names +} + export function findClassNamesInRange( doc: TextDocument, range?: Range, mode?: 'html' | 'css' ): DocumentClassName[] { const classLists = findClassListsInRange(doc, range, mode) - return [].concat.apply( - [], - classLists.map(({ classList, range }) => { - const parts = classList.split(/(\s+)/) - const names: DocumentClassName[] = [] - let index = 0 - for (let i = 0; i < parts.length; i++) { - if (i % 2 === 0) { - const start = indexToPosition(classList, index) - const end = indexToPosition(classList, index + parts[i].length) - names.push({ - className: parts[i], - range: { - start: { - line: range.start.line + start.line, - character: - (end.line === 0 ? range.start.character : 0) + - start.character, - }, - end: { - line: range.start.line + end.line, - character: - (end.line === 0 ? range.start.character : 0) + end.character, - }, - }, - }) - } - index += parts[i].length - } - return names - }) - ) + return flatten(classLists.map(getClassNamesInClassList)) +} + +export function findClassNamesInDocument( + state: State, + doc: TextDocument +): DocumentClassName[] { + const classLists = findClassListsInDocument(state, doc) + return flatten(classLists.map(getClassNamesInClassList)) } export function findClassListsInCssRange( @@ -98,7 +108,7 @@ export function findClassListsInCssRange( export function findClassListsInHtmlRange( doc: TextDocument, - range: Range + range?: Range ): DocumentClassList[] { const text = doc.getText(range) const matches = findAll(/[\s:]class(?:Name)?=['"`{]/g, text) @@ -174,15 +184,16 @@ export function findClassListsInHtmlRange( classList: value, range: { start: { - line: range.start.line + start.line, + line: (range?.start.line || 0) + start.line, character: - (end.line === 0 ? range.start.character : 0) + + (end.line === 0 ? range?.start.character || 0 : 0) + start.character, }, end: { - line: range.start.line + end.line, + line: (range?.start.line || 0) + end.line, character: - (end.line === 0 ? range.start.character : 0) + end.character, + (end.line === 0 ? range?.start.character || 0 : 0) + + end.character, }, }, } @@ -196,8 +207,8 @@ export function findClassListsInHtmlRange( export function findClassListsInRange( doc: TextDocument, - range: Range, - mode: 'html' | 'css' + range?: Range, + mode?: 'html' | 'css' ): DocumentClassList[] { if (mode === 'css') { return findClassListsInCssRange(doc, range)