From b2d47d220b423e0c079b134e5eab47b5d48e7cf6 Mon Sep 17 00:00:00 2001 From: Brad Cornes Date: Mon, 7 Dec 2020 15:39:44 +0000 Subject: [PATCH] add hover, color decorator, linting support for classRegex setting (#129) --- .../lsp/providers/documentColorProvider.ts | 2 +- .../src/completionProvider.ts | 58 +-------- .../src/diagnostics/diagnosticsProvider.ts | 4 +- .../diagnostics/getCssConflictDiagnostics.ts | 6 +- .../diagnostics/getInvalidApplyDiagnostics.ts | 12 +- .../src/documentColorProvider.ts | 4 +- .../src/hoverProvider.ts | 2 +- .../src/util/createMultiRegexp.ts | 55 ++++++++ .../src/util/find.ts | 123 +++++++++++++++--- 9 files changed, 178 insertions(+), 88 deletions(-) create mode 100644 packages/tailwindcss-language-service/src/util/createMultiRegexp.ts diff --git a/packages/tailwindcss-intellisense/src/lsp/providers/documentColorProvider.ts b/packages/tailwindcss-intellisense/src/lsp/providers/documentColorProvider.ts index 01f61fe..daab7f7 100644 --- a/packages/tailwindcss-intellisense/src/lsp/providers/documentColorProvider.ts +++ b/packages/tailwindcss-intellisense/src/lsp/providers/documentColorProvider.ts @@ -10,7 +10,7 @@ export function registerDocumentColorProvider(state: State) { let doc = state.editor.documents.get(document) if (!doc) return { colors: [] } - return { colors: getDocumentColors(state, doc) } + return { colors: await getDocumentColors(state, doc) } } ) } diff --git a/packages/tailwindcss-language-service/src/completionProvider.ts b/packages/tailwindcss-language-service/src/completionProvider.ts index 6aa6d24..1a160df 100644 --- a/packages/tailwindcss-language-service/src/completionProvider.ts +++ b/packages/tailwindcss-language-service/src/completionProvider.ts @@ -31,8 +31,8 @@ import { } from './util/lexers' import { validateApply } from './util/validateApply' import { flagEnabled } from './util/flagEnabled' -import MultiRegexp from 'multi-regexp2' import { remToPx } from './util/remToPx' +import { createMultiRegexp } from './util/createMultiRegexp' export function completionsFromClassList( state: State, @@ -197,62 +197,6 @@ function provideClassAttributeCompletions( return null } -function createMultiRegexp(regexString: string) { - let insideCharClass = false - let captureGroupIndex = -1 - - for (let i = 0; i < regexString.length; i++) { - if ( - !insideCharClass && - regexString[i] === '[' && - regexString[i - 1] !== '\\' - ) { - insideCharClass = true - } else if ( - insideCharClass && - regexString[i] === ']' && - regexString[i - 1] !== '\\' - ) { - insideCharClass = false - } else if ( - !insideCharClass && - regexString[i] === '(' && - regexString.substr(i + 1, 2) !== '?:' - ) { - captureGroupIndex = i - break - } - } - - const re = /(?:[^\\]|^)\(\?:/g - let match: RegExpExecArray - let nonCaptureGroupIndexes: number[] = [] - - while ((match = re.exec(regexString)) !== null) { - if (match[0].startsWith('(')) { - nonCaptureGroupIndexes.push(match.index) - } else { - nonCaptureGroupIndexes.push(match.index + 1) - } - } - - const regex = new MultiRegexp( - new RegExp( - regexString.replace(re, (m) => m.substr(0, m.length - 2)), - 'g' - ) - ) - - let groupIndex = - 1 + nonCaptureGroupIndexes.filter((i) => i < captureGroupIndex).length - - return { - exec: (str: string) => { - return regex.execForGroup(str, groupIndex) - }, - } -} - async function provideCustomClassNameCompletions( state: State, document: TextDocument, diff --git a/packages/tailwindcss-language-service/src/diagnostics/diagnosticsProvider.ts b/packages/tailwindcss-language-service/src/diagnostics/diagnosticsProvider.ts index 84ab3db..8db104f 100644 --- a/packages/tailwindcss-language-service/src/diagnostics/diagnosticsProvider.ts +++ b/packages/tailwindcss-language-service/src/diagnostics/diagnosticsProvider.ts @@ -26,10 +26,10 @@ export async function doValidate( return settings.validate ? [ ...(only.includes(DiagnosticKind.CssConflict) - ? getCssConflictDiagnostics(state, document, settings) + ? await getCssConflictDiagnostics(state, document, settings) : []), ...(only.includes(DiagnosticKind.InvalidApply) - ? getInvalidApplyDiagnostics(state, document, settings) + ? await getInvalidApplyDiagnostics(state, document, settings) : []), ...(only.includes(DiagnosticKind.InvalidScreen) ? getInvalidScreenDiagnostics(state, document, settings) diff --git a/packages/tailwindcss-language-service/src/diagnostics/getCssConflictDiagnostics.ts b/packages/tailwindcss-language-service/src/diagnostics/getCssConflictDiagnostics.ts index 4fe6c82..ceeae4f 100644 --- a/packages/tailwindcss-language-service/src/diagnostics/getCssConflictDiagnostics.ts +++ b/packages/tailwindcss-language-service/src/diagnostics/getCssConflictDiagnostics.ts @@ -10,16 +10,16 @@ import { getClassNameDecls } from '../util/getClassNameDecls' import { getClassNameMeta } from '../util/getClassNameMeta' import { equal } from '../util/array' -export function getCssConflictDiagnostics( +export async function getCssConflictDiagnostics( state: State, document: TextDocument, settings: Settings -): CssConflictDiagnostic[] { +): Promise { let severity = settings.lint.cssConflict if (severity === 'ignore') return [] let diagnostics: CssConflictDiagnostic[] = [] - const classLists = findClassListsInDocument(state, document) + const classLists = await findClassListsInDocument(state, document) classLists.forEach((classList) => { const classNames = getClassNamesInClassList(classList) diff --git a/packages/tailwindcss-language-service/src/diagnostics/getInvalidApplyDiagnostics.ts b/packages/tailwindcss-language-service/src/diagnostics/getInvalidApplyDiagnostics.ts index 3e6d03e..cc04d69 100644 --- a/packages/tailwindcss-language-service/src/diagnostics/getInvalidApplyDiagnostics.ts +++ b/packages/tailwindcss-language-service/src/diagnostics/getInvalidApplyDiagnostics.ts @@ -4,15 +4,21 @@ import { Settings, State } from '../util/state' import type { TextDocument, DiagnosticSeverity } from 'vscode-languageserver' import { validateApply } from '../util/validateApply' -export function getInvalidApplyDiagnostics( +export async function getInvalidApplyDiagnostics( state: State, document: TextDocument, settings: Settings -): InvalidApplyDiagnostic[] { +): Promise { let severity = settings.lint.invalidApply if (severity === 'ignore') return [] - const classNames = findClassNamesInRange(document, undefined, 'css') + const classNames = await findClassNamesInRange( + state, + document, + undefined, + 'css', + false + ) let diagnostics: InvalidApplyDiagnostic[] = classNames.map((className) => { let result = validateApply(state, className.className) diff --git a/packages/tailwindcss-language-service/src/documentColorProvider.ts b/packages/tailwindcss-language-service/src/documentColorProvider.ts index fa6dcc8..69a158b 100644 --- a/packages/tailwindcss-language-service/src/documentColorProvider.ts +++ b/packages/tailwindcss-language-service/src/documentColorProvider.ts @@ -10,11 +10,11 @@ import { stringToPath } from './util/stringToPath' import type { TextDocument } from 'vscode-languageserver' const dlv = require('dlv') -export function getDocumentColors(state: State, document: TextDocument) { +export async function getDocumentColors(state: State, document: TextDocument) { let colors = [] if (!state.enabled) return colors - let classLists = findClassListsInDocument(state, document) + let classLists = await findClassListsInDocument(state, document) classLists.forEach((classList) => { let classNames = getClassNamesInClassList(classList) classNames.forEach((className) => { diff --git a/packages/tailwindcss-language-service/src/hoverProvider.ts b/packages/tailwindcss-language-service/src/hoverProvider.ts index 03f1150..92fa34d 100644 --- a/packages/tailwindcss-language-service/src/hoverProvider.ts +++ b/packages/tailwindcss-language-service/src/hoverProvider.ts @@ -77,7 +77,7 @@ async function provideClassNameHover( document: TextDocument, position: Position ): Promise { - let className = findClassNameAtPosition(state, document, position) + let className = await findClassNameAtPosition(state, document, position) if (className === null) return null const parts = getClassNameParts(state, className.className) diff --git a/packages/tailwindcss-language-service/src/util/createMultiRegexp.ts b/packages/tailwindcss-language-service/src/util/createMultiRegexp.ts new file mode 100644 index 0000000..9d8d2be --- /dev/null +++ b/packages/tailwindcss-language-service/src/util/createMultiRegexp.ts @@ -0,0 +1,55 @@ +export function createMultiRegexp(regexString: string) { + let insideCharClass = false + let captureGroupIndex = -1 + + for (let i = 0; i < regexString.length; i++) { + if ( + !insideCharClass && + regexString[i] === '[' && + regexString[i - 1] !== '\\' + ) { + insideCharClass = true + } else if ( + insideCharClass && + regexString[i] === ']' && + regexString[i - 1] !== '\\' + ) { + insideCharClass = false + } else if ( + !insideCharClass && + regexString[i] === '(' && + regexString.substr(i + 1, 2) !== '?:' + ) { + captureGroupIndex = i + break + } + } + + const re = /(?:[^\\]|^)\(\?:/g + let match: RegExpExecArray + let nonCaptureGroupIndexes: number[] = [] + + while ((match = re.exec(regexString)) !== null) { + if (match[0].startsWith('(')) { + nonCaptureGroupIndexes.push(match.index) + } else { + nonCaptureGroupIndexes.push(match.index + 1) + } + } + + const regex = new MultiRegexp( + new RegExp( + regexString.replace(re, (m) => m.substr(0, m.length - 2)), + 'g' + ) + ) + + let groupIndex = + 1 + nonCaptureGroupIndexes.filter((i) => i < captureGroupIndex).length + + return { + exec: (str: string) => { + return regex.execForGroup(str, groupIndex) + }, + } +} diff --git a/packages/tailwindcss-language-service/src/util/find.ts b/packages/tailwindcss-language-service/src/util/find.ts index 15bb694..a680d8c 100644 --- a/packages/tailwindcss-language-service/src/util/find.ts +++ b/packages/tailwindcss-language-service/src/util/find.ts @@ -17,6 +17,9 @@ import { } from './lexers' import { getLanguageBoundaries } from './getLanguageBoundaries' import { resolveRange } from './resolveRange' +import { getDocumentSettings } from './getDocumentSettings' +const dlv = require('dlv') +import { createMultiRegexp } from './createMultiRegexp' export function findAll(re: RegExp, str: string): RegExpMatchArray[] { let match: RegExpMatchArray @@ -77,20 +80,28 @@ export function getClassNamesInClassList({ return names } -export function findClassNamesInRange( +export async function findClassNamesInRange( + state: State, doc: TextDocument, range?: Range, - mode?: 'html' | 'css' -): DocumentClassName[] { - const classLists = findClassListsInRange(doc, range, mode) + mode?: 'html' | 'css', + includeCustom: boolean = true +): Promise { + const classLists = await findClassListsInRange( + state, + doc, + range, + mode, + includeCustom + ) return flatten(classLists.map(getClassNamesInClassList)) } -export function findClassNamesInDocument( +export async function findClassNamesInDocument( state: State, doc: TextDocument -): DocumentClassName[] { - const classLists = findClassListsInDocument(state, doc) +): Promise { + const classLists = await findClassListsInDocument(state, doc) return flatten(classLists.map(getClassNamesInClassList)) } @@ -130,12 +141,77 @@ export function findClassListsInCssRange( }) } +async function findCustomClassLists( + state: State, + doc: TextDocument, + range?: Range +): Promise { + const settings = await getDocumentSettings(state, doc) + const regexes = dlv(settings, 'experimental.classRegex', []) + + if (!Array.isArray(regexes) || regexes.length === 0) return [] + + const text = doc.getText(range) + const result: DocumentClassList[] = [] + + for (let i = 0; i < regexes.length; i++) { + try { + let [containerRegex, classRegex] = Array.isArray(regexes[i]) + ? regexes[i] + : [regexes[i]] + + containerRegex = createMultiRegexp(containerRegex) + let containerMatch + + while ((containerMatch = containerRegex.exec(text)) !== null) { + const searchStart = doc.offsetAt( + range?.start || { line: 0, character: 0 } + ) + const matchStart = searchStart + containerMatch.start + const matchEnd = searchStart + containerMatch.end + + if (classRegex) { + classRegex = createMultiRegexp(classRegex) + let classMatch + + while ( + (classMatch = classRegex.exec(containerMatch.match)) !== null + ) { + const classMatchStart = matchStart + classMatch.start + const classMatchEnd = matchStart + classMatch.end + result.push({ + classList: classMatch.match, + range: { + start: doc.positionAt(classMatchStart), + end: doc.positionAt(classMatchEnd), + }, + }) + } + } else { + result.push({ + classList: containerMatch.match, + range: { + start: doc.positionAt(matchStart), + end: doc.positionAt(matchEnd), + }, + }) + } + } + } catch (_) {} + } + + return result +} + export function findClassListsInHtmlRange( doc: TextDocument, range?: Range ): DocumentClassList[] { const text = doc.getText(range) - const matches = findAll(/(?:\s|:)(?:class(?:Name)?|\[ngClass\])=['"`{]/g, text) + const matches = findAll( + /(?:\s|:)(?:class(?:Name)?|\[ngClass\])=['"`{]/g, + text + ) const result: DocumentClassList[] = [] matches.forEach((match) => { @@ -232,21 +308,29 @@ export function findClassListsInHtmlRange( return result } -export function findClassListsInRange( +export async function findClassListsInRange( + state: State, doc: TextDocument, range?: Range, - mode?: 'html' | 'css' -): DocumentClassList[] { + mode?: 'html' | 'css', + includeCustom: boolean = true +): Promise { + let classLists: DocumentClassList[] if (mode === 'css') { - return findClassListsInCssRange(doc, range) + classLists = findClassListsInCssRange(doc, range) + } else { + classLists = findClassListsInHtmlRange(doc, range) } - return findClassListsInHtmlRange(doc, range) + return [ + ...classLists, + ...(includeCustom ? await findCustomClassLists(state, doc, range) : []), + ] } -export function findClassListsInDocument( +export async function findClassListsInDocument( state: State, doc: TextDocument -): DocumentClassList[] { +): Promise { if (isCssDoc(state, doc)) { return findClassListsInCssRange(doc) } @@ -257,6 +341,7 @@ export function findClassListsInDocument( return flatten([ ...boundaries.html.map((range) => findClassListsInHtmlRange(doc, range)), ...boundaries.css.map((range) => findClassListsInCssRange(doc, range)), + await findCustomClassLists(state, doc), ]) } @@ -323,11 +408,11 @@ export function indexToPosition(str: string, index: number): Position { return { line: line - 1, character: col - 1 } } -export function findClassNameAtPosition( +export async function findClassNameAtPosition( state: State, doc: TextDocument, position: Position -): DocumentClassName { +): Promise { let classNames = [] const searchRange = { start: { line: Math.max(position.line - 10, 0), character: 0 }, @@ -335,12 +420,12 @@ export function findClassNameAtPosition( } if (isCssContext(state, doc, position)) { - classNames = findClassNamesInRange(doc, searchRange, 'css') + classNames = await findClassNamesInRange(state, doc, searchRange, 'css') } else if ( isHtmlContext(state, doc, position) || isJsContext(state, doc, position) ) { - classNames = findClassNamesInRange(doc, searchRange, 'html') + classNames = await findClassNamesInRange(state, doc, searchRange, 'html') } if (classNames.length === 0) {