From 4f17e2358dde3653350d04731f2e840253d3cfb4 Mon Sep 17 00:00:00 2001 From: Brad Cornes Date: Mon, 15 Jun 2020 11:24:05 +0100 Subject: [PATCH] enable diagnostics in multi-language documents --- src/lsp/providers/diagnosticsProvider.ts | 246 ++++++++++++++--------- src/lsp/util/absoluteRange.ts | 18 ++ src/lsp/util/find.ts | 70 +------ src/lsp/util/getLanguageBoundaries.ts | 75 +++++++ 4 files changed, 252 insertions(+), 157 deletions(-) create mode 100644 src/lsp/util/absoluteRange.ts create mode 100644 src/lsp/util/getLanguageBoundaries.ts diff --git a/src/lsp/providers/diagnosticsProvider.ts b/src/lsp/providers/diagnosticsProvider.ts index 866c07d..bb59ff5 100644 --- a/src/lsp/providers/diagnosticsProvider.ts +++ b/src/lsp/providers/diagnosticsProvider.ts @@ -2,6 +2,7 @@ import { TextDocument, Diagnostic, DiagnosticSeverity, + Range, } from 'vscode-languageserver' import { State, Settings } from '../util/state' import { isCssDoc } from '../util/css' @@ -18,6 +19,8 @@ import { equal, flatten } 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' function getUnsupportedApplyDiagnostics( state: State, @@ -140,35 +143,51 @@ function getUnknownScreenDiagnostics( let severity = settings.lint.unknownScreen if (severity === 'ignore') return [] - let text = document.getText() - let matches = findAll(/(?:\s|^)@screen\s+(?[^\s{]+)/g, text) + let diagnostics: Diagnostic[] = [] + let ranges: Range[] = [] - let screens = Object.keys( - dlv(state.config, 'theme.screens', dlv(state.config, 'screens', {})) - ) + if (isCssDoc(state, document)) { + ranges.push(undefined) + } else { + let boundaries = getLanguageBoundaries(state, document) + if (!boundaries) return [] + ranges.push(...boundaries.css) + } - return matches - .map((match) => { + 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 } - return { - range: { - start: indexToPosition( - text, - match.index + match[0].length - match.groups.screen.length - ), - end: indexToPosition(text, match.index + match[0].length), - }, + diagnostics.push({ + 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: 'Unknown screen', - } + }) }) - .filter(Boolean) + }) + + return diagnostics } function getUnknownVariantDiagnostics( @@ -179,43 +198,54 @@ function getUnknownVariantDiagnostics( let severity = settings.lint.unknownVariant if (severity === 'ignore') return [] - let text = document.getText() - let matches = findAll(/(?:\s|^)@variants\s+(?[^{]+)/g, text) + let diagnostics: Diagnostic[] = [] + let ranges: Range[] = [] - return flatten( - matches - .map((match) => { - let diagnostics: Diagnostic[] = [] - let variants = match.groups.variants.split(/(\s*,\s*)/) - let listStartIndex = - match.index + match[0].length - match.groups.variants.length + if (isCssDoc(state, document)) { + ranges.push(undefined) + } else { + let boundaries = getLanguageBoundaries(state, document) + if (!boundaries) return [] + ranges.push(...boundaries.css) + } - for (let i = 0; i < variants.length; i += 2) { - let variant = variants[i].trim() - if (state.variants.includes(variant)) { - continue - } + ranges.forEach((range) => { + let text = document.getText(range) + let matches = findAll(/(?:\s|^)@variants\s+(?[^{]+)/g, text) - let variantStartIndex = - listStartIndex + variants.slice(0, i).join('').length + matches.forEach((match) => { + let variants = match.groups.variants.split(/(\s*,\s*)/) + let listStartIndex = + match.index + match[0].length - match.groups.variants.length - diagnostics.push({ - range: { + for (let i = 0; i < variants.length; i += 2) { + let variant = variants[i].trim() + if (state.variants.includes(variant)) { + continue + } + + let variantStartIndex = + listStartIndex + variants.slice(0, i).join('').length + + diagnostics.push({ + range: absoluteRange( + { start: indexToPosition(text, variantStartIndex), end: indexToPosition(text, variantStartIndex + variant.length), }, - severity: - severity === 'error' - ? DiagnosticSeverity.Error - : DiagnosticSeverity.Warning, - message: `Unknown variant: ${variant}`, - }) - } + range + ), + severity: + severity === 'error' + ? DiagnosticSeverity.Error + : DiagnosticSeverity.Warning, + message: `Unknown variant: ${variant}`, + }) + } + }) + }) - return diagnostics - }) - .filter(Boolean) - ) + return diagnostics } function getUnknownConfigKeyDiagnostics( @@ -226,14 +256,25 @@ function getUnknownConfigKeyDiagnostics( let severity = settings.lint.unknownConfigKey if (severity === 'ignore') return [] - let text = document.getText() - let matches = findAll( - /(?\s|^)(?config|theme)\((?['"])(?[^)]+)\k\)/g, - text - ) + let diagnostics: Diagnostic[] = [] + let ranges: Range[] = [] - return matches - .map((match) => { + 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 = match.groups.key.split(/[.\[\]]/).filter(Boolean) let value = dlv(state.config, [...base, ...keys]) @@ -251,19 +292,24 @@ function getUnknownConfigKeyDiagnostics( 1 + // open paren match.groups.quote.length - return { - range: { - start: indexToPosition(text, startIndex), - end: indexToPosition(text, startIndex + match.groups.key.length), - }, + diagnostics.push({ + range: absoluteRange( + { + start: indexToPosition(text, startIndex), + end: indexToPosition(text, startIndex + match.groups.key.length), + }, + range + ), severity: severity === 'error' ? DiagnosticSeverity.Error : DiagnosticSeverity.Warning, message: `Unknown ${match.groups.helper} key: ${match.groups.key}`, - } + }) }) - .filter(Boolean) + }) + + return diagnostics } function getUnsupportedTailwindDirectiveDiagnostics( @@ -274,30 +320,44 @@ function getUnsupportedTailwindDirectiveDiagnostics( let severity = settings.lint.unsupportedTailwindDirective if (severity === 'ignore') return [] - let text = document.getText() - let matches = findAll(/(?:\s|^)@tailwind\s+(?[^;]+)/g, text) + let diagnostics: Diagnostic[] = [] + let ranges: Range[] = [] - let allowed = [ - 'utilities', - 'components', - 'screens', - semver.gte(state.version, '1.0.0-beta.1') ? 'base' : 'preflight', - ] + if (isCssDoc(state, document)) { + ranges.push(undefined) + } else { + let boundaries = getLanguageBoundaries(state, document) + if (!boundaries) return [] + ranges.push(...boundaries.css) + } - return matches - .map((match) => { - if (allowed.includes(match.groups.value)) { + 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 } - return { - range: { - start: indexToPosition( - text, - match.index + match[0].length - match.groups.value.length - ), - end: indexToPosition(text, match.index + match[0].length), - }, + diagnostics.push({ + 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 @@ -305,9 +365,11 @@ function getUnsupportedTailwindDirectiveDiagnostics( message: `Unsupported value: ${match.groups.value}${ match.groups.value === 'preflight' ? '. Use base instead.' : '' }`, - } + }) }) - .filter(Boolean) + }) + + return diagnostics } export async function provideDiagnostics( @@ -319,19 +381,15 @@ export async function provideDiagnostics( const diagnostics: Diagnostic[] = settings.validate ? [ ...getUtilityConflictDiagnostics(state, document, settings), - ...(isCssDoc(state, document) - ? [ - ...getUnsupportedApplyDiagnostics(state, document, settings), - ...getUnknownScreenDiagnostics(state, document, settings), - ...getUnknownVariantDiagnostics(state, document, settings), - ...getUnknownConfigKeyDiagnostics(state, document, settings), - ...getUnsupportedTailwindDirectiveDiagnostics( - state, - document, - settings - ), - ] - : []), + ...getUnsupportedApplyDiagnostics(state, document, settings), + ...getUnknownScreenDiagnostics(state, document, settings), + ...getUnknownVariantDiagnostics(state, document, settings), + ...getUnknownConfigKeyDiagnostics(state, document, settings), + ...getUnsupportedTailwindDirectiveDiagnostics( + state, + document, + settings + ), ] : [] diff --git a/src/lsp/util/absoluteRange.ts b/src/lsp/util/absoluteRange.ts new file mode 100644 index 0000000..9250e4f --- /dev/null +++ b/src/lsp/util/absoluteRange.ts @@ -0,0 +1,18 @@ +import { Range } from 'vscode-languageserver' + +export function absoluteRange(range: Range, reference?: Range) { + return { + start: { + line: (reference?.start.line || 0) + range.start.line, + character: + (range.end.line === 0 ? reference?.start.character || 0 : 0) + + range.start.character, + }, + end: { + line: (reference?.start.line || 0) + range.end.line, + character: + (range.end.line === 0 ? reference?.start.character || 0 : 0) + + range.end.character, + }, + } +} diff --git a/src/lsp/util/find.ts b/src/lsp/util/find.ts index 0f996a4..609de62 100644 --- a/src/lsp/util/find.ts +++ b/src/lsp/util/find.ts @@ -10,6 +10,7 @@ import { getClassAttributeLexer, getComputedClassAttributeLexer, } from './lexers' +import { getLanguageBoundaries } from './getLanguageBoundaries' export function findAll(re: RegExp, str: string): RegExpMatchArray[] { let match: RegExpMatchArray @@ -230,70 +231,13 @@ export function findClassListsInDocument( return findClassListsInCssRange(doc) } - if (isVueDoc(doc)) { - let text = doc.getText() - let blocks = findAll( - /<(?template|style|script)\b[^>]*>.*?(<\/\k>|$)/gis, - text - ) - let htmlRanges: Range[] = [] - let cssRanges: Range[] = [] - for (let i = 0; i < blocks.length; i++) { - let range = { - start: indexToPosition(text, blocks[i].index), - end: indexToPosition(text, blocks[i].index + blocks[i][0].length), - } - if (blocks[i].groups.type === 'style') { - cssRanges.push(range) - } else { - htmlRanges.push(range) - } - } - return [].concat.apply( - [], - [ - ...htmlRanges.map((range) => findClassListsInHtmlRange(doc, range)), - ...cssRanges.map((range) => findClassListsInCssRange(doc, range)), - ] - ) - } + let boundaries = getLanguageBoundaries(state, doc) + if (!boundaries) return [] - if (isHtmlDoc(state, doc) || isJsDoc(state, doc) || isSvelteDoc(doc)) { - let text = doc.getText() - let styleBlocks = findAll(/]*>|>).*?(<\/style>|$)/gis, text) - let htmlRanges: Range[] = [] - let cssRanges: Range[] = [] - let currentIndex = 0 - - for (let i = 0; i < styleBlocks.length; i++) { - htmlRanges.push({ - start: indexToPosition(text, currentIndex), - end: indexToPosition(text, styleBlocks[i].index), - }) - cssRanges.push({ - start: indexToPosition(text, styleBlocks[i].index), - end: indexToPosition( - text, - styleBlocks[i].index + styleBlocks[i][0].length - ), - }) - currentIndex = styleBlocks[i].index + styleBlocks[i][0].length - } - htmlRanges.push({ - start: indexToPosition(text, currentIndex), - end: indexToPosition(text, text.length), - }) - - return [].concat.apply( - [], - [ - ...htmlRanges.map((range) => findClassListsInHtmlRange(doc, range)), - ...cssRanges.map((range) => findClassListsInCssRange(doc, range)), - ] - ) - } - - return [] + return flatten([ + ...boundaries.html.map((range) => findClassListsInHtmlRange(doc, range)), + ...boundaries.css.map((range) => findClassListsInCssRange(doc, range)), + ]) } export function indexToPosition(str: string, index: number): Position { diff --git a/src/lsp/util/getLanguageBoundaries.ts b/src/lsp/util/getLanguageBoundaries.ts new file mode 100644 index 0000000..dfef230 --- /dev/null +++ b/src/lsp/util/getLanguageBoundaries.ts @@ -0,0 +1,75 @@ +import { TextDocument, Range } from 'vscode-languageserver' +import { isVueDoc, isHtmlDoc, isSvelteDoc } from './html' +import { State } from './state' +import { findAll, indexToPosition } from './find' +import { isJsDoc } from './js' + +export interface LanguageBoundaries { + html: Range[] + css: Range[] +} + +export function getLanguageBoundaries( + state: State, + doc: TextDocument +): LanguageBoundaries | null { + if (isVueDoc(doc)) { + let text = doc.getText() + let blocks = findAll( + /<(?template|style|script)\b[^>]*>.*?(<\/\k>|$)/gis, + text + ) + let htmlRanges: Range[] = [] + let cssRanges: Range[] = [] + for (let i = 0; i < blocks.length; i++) { + let range = { + start: indexToPosition(text, blocks[i].index), + end: indexToPosition(text, blocks[i].index + blocks[i][0].length), + } + if (blocks[i].groups.type === 'style') { + cssRanges.push(range) + } else { + htmlRanges.push(range) + } + } + + return { + html: htmlRanges, + css: cssRanges, + } + } + + if (isHtmlDoc(state, doc) || isJsDoc(state, doc) || isSvelteDoc(doc)) { + let text = doc.getText() + let styleBlocks = findAll(/]*>|>).*?(<\/style>|$)/gis, text) + let htmlRanges: Range[] = [] + let cssRanges: Range[] = [] + let currentIndex = 0 + + for (let i = 0; i < styleBlocks.length; i++) { + htmlRanges.push({ + start: indexToPosition(text, currentIndex), + end: indexToPosition(text, styleBlocks[i].index), + }) + cssRanges.push({ + start: indexToPosition(text, styleBlocks[i].index), + end: indexToPosition( + text, + styleBlocks[i].index + styleBlocks[i][0].length + ), + }) + currentIndex = styleBlocks[i].index + styleBlocks[i][0].length + } + htmlRanges.push({ + start: indexToPosition(text, currentIndex), + end: indexToPosition(text, text.length), + }) + + return { + html: htmlRanges, + css: cssRanges, + } + } + + return null +}