add hover, color decorator, linting support for classRegex setting (#129)

master
Brad Cornes 2020-12-07 15:39:44 +00:00
parent 5bd09789f4
commit b2d47d220b
9 changed files with 178 additions and 88 deletions

View File

@ -10,7 +10,7 @@ export function registerDocumentColorProvider(state: State) {
let doc = state.editor.documents.get(document) let doc = state.editor.documents.get(document)
if (!doc) return { colors: [] } if (!doc) return { colors: [] }
return { colors: getDocumentColors(state, doc) } return { colors: await getDocumentColors(state, doc) }
} }
) )
} }

View File

@ -31,8 +31,8 @@ import {
} from './util/lexers' } from './util/lexers'
import { validateApply } from './util/validateApply' import { validateApply } from './util/validateApply'
import { flagEnabled } from './util/flagEnabled' import { flagEnabled } from './util/flagEnabled'
import MultiRegexp from 'multi-regexp2'
import { remToPx } from './util/remToPx' import { remToPx } from './util/remToPx'
import { createMultiRegexp } from './util/createMultiRegexp'
export function completionsFromClassList( export function completionsFromClassList(
state: State, state: State,
@ -197,62 +197,6 @@ function provideClassAttributeCompletions(
return null 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( async function provideCustomClassNameCompletions(
state: State, state: State,
document: TextDocument, document: TextDocument,

View File

@ -26,10 +26,10 @@ export async function doValidate(
return settings.validate return settings.validate
? [ ? [
...(only.includes(DiagnosticKind.CssConflict) ...(only.includes(DiagnosticKind.CssConflict)
? getCssConflictDiagnostics(state, document, settings) ? await getCssConflictDiagnostics(state, document, settings)
: []), : []),
...(only.includes(DiagnosticKind.InvalidApply) ...(only.includes(DiagnosticKind.InvalidApply)
? getInvalidApplyDiagnostics(state, document, settings) ? await getInvalidApplyDiagnostics(state, document, settings)
: []), : []),
...(only.includes(DiagnosticKind.InvalidScreen) ...(only.includes(DiagnosticKind.InvalidScreen)
? getInvalidScreenDiagnostics(state, document, settings) ? getInvalidScreenDiagnostics(state, document, settings)

View File

@ -10,16 +10,16 @@ import { getClassNameDecls } from '../util/getClassNameDecls'
import { getClassNameMeta } from '../util/getClassNameMeta' import { getClassNameMeta } from '../util/getClassNameMeta'
import { equal } from '../util/array' import { equal } from '../util/array'
export function getCssConflictDiagnostics( export async function getCssConflictDiagnostics(
state: State, state: State,
document: TextDocument, document: TextDocument,
settings: Settings settings: Settings
): CssConflictDiagnostic[] { ): Promise<CssConflictDiagnostic[]> {
let severity = settings.lint.cssConflict let severity = settings.lint.cssConflict
if (severity === 'ignore') return [] if (severity === 'ignore') return []
let diagnostics: CssConflictDiagnostic[] = [] let diagnostics: CssConflictDiagnostic[] = []
const classLists = findClassListsInDocument(state, document) const classLists = await findClassListsInDocument(state, document)
classLists.forEach((classList) => { classLists.forEach((classList) => {
const classNames = getClassNamesInClassList(classList) const classNames = getClassNamesInClassList(classList)

View File

@ -4,15 +4,21 @@ import { Settings, State } from '../util/state'
import type { TextDocument, DiagnosticSeverity } from 'vscode-languageserver' import type { TextDocument, DiagnosticSeverity } from 'vscode-languageserver'
import { validateApply } from '../util/validateApply' import { validateApply } from '../util/validateApply'
export function getInvalidApplyDiagnostics( export async function getInvalidApplyDiagnostics(
state: State, state: State,
document: TextDocument, document: TextDocument,
settings: Settings settings: Settings
): InvalidApplyDiagnostic[] { ): Promise<InvalidApplyDiagnostic[]> {
let severity = settings.lint.invalidApply let severity = settings.lint.invalidApply
if (severity === 'ignore') return [] 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 diagnostics: InvalidApplyDiagnostic[] = classNames.map((className) => {
let result = validateApply(state, className.className) let result = validateApply(state, className.className)

View File

@ -10,11 +10,11 @@ import { stringToPath } from './util/stringToPath'
import type { TextDocument } from 'vscode-languageserver' import type { TextDocument } from 'vscode-languageserver'
const dlv = require('dlv') const dlv = require('dlv')
export function getDocumentColors(state: State, document: TextDocument) { export async function getDocumentColors(state: State, document: TextDocument) {
let colors = [] let colors = []
if (!state.enabled) return colors if (!state.enabled) return colors
let classLists = findClassListsInDocument(state, document) let classLists = await findClassListsInDocument(state, document)
classLists.forEach((classList) => { classLists.forEach((classList) => {
let classNames = getClassNamesInClassList(classList) let classNames = getClassNamesInClassList(classList)
classNames.forEach((className) => { classNames.forEach((className) => {

View File

@ -77,7 +77,7 @@ async function provideClassNameHover(
document: TextDocument, document: TextDocument,
position: Position position: Position
): Promise<Hover> { ): Promise<Hover> {
let className = findClassNameAtPosition(state, document, position) let className = await findClassNameAtPosition(state, document, position)
if (className === null) return null if (className === null) return null
const parts = getClassNameParts(state, className.className) const parts = getClassNameParts(state, className.className)

View File

@ -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)
},
}
}

View File

@ -17,6 +17,9 @@ import {
} from './lexers' } from './lexers'
import { getLanguageBoundaries } from './getLanguageBoundaries' import { getLanguageBoundaries } from './getLanguageBoundaries'
import { resolveRange } from './resolveRange' import { resolveRange } from './resolveRange'
import { getDocumentSettings } from './getDocumentSettings'
const dlv = require('dlv')
import { createMultiRegexp } from './createMultiRegexp'
export function findAll(re: RegExp, str: string): RegExpMatchArray[] { export function findAll(re: RegExp, str: string): RegExpMatchArray[] {
let match: RegExpMatchArray let match: RegExpMatchArray
@ -77,20 +80,28 @@ export function getClassNamesInClassList({
return names return names
} }
export function findClassNamesInRange( export async function findClassNamesInRange(
state: State,
doc: TextDocument, doc: TextDocument,
range?: Range, range?: Range,
mode?: 'html' | 'css' mode?: 'html' | 'css',
): DocumentClassName[] { includeCustom: boolean = true
const classLists = findClassListsInRange(doc, range, mode) ): Promise<DocumentClassName[]> {
const classLists = await findClassListsInRange(
state,
doc,
range,
mode,
includeCustom
)
return flatten(classLists.map(getClassNamesInClassList)) return flatten(classLists.map(getClassNamesInClassList))
} }
export function findClassNamesInDocument( export async function findClassNamesInDocument(
state: State, state: State,
doc: TextDocument doc: TextDocument
): DocumentClassName[] { ): Promise<DocumentClassName[]> {
const classLists = findClassListsInDocument(state, doc) const classLists = await findClassListsInDocument(state, doc)
return flatten(classLists.map(getClassNamesInClassList)) return flatten(classLists.map(getClassNamesInClassList))
} }
@ -130,12 +141,77 @@ export function findClassListsInCssRange(
}) })
} }
async function findCustomClassLists(
state: State,
doc: TextDocument,
range?: Range
): Promise<DocumentClassList[]> {
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( export function findClassListsInHtmlRange(
doc: TextDocument, doc: TextDocument,
range?: Range range?: Range
): DocumentClassList[] { ): DocumentClassList[] {
const text = doc.getText(range) 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[] = [] const result: DocumentClassList[] = []
matches.forEach((match) => { matches.forEach((match) => {
@ -232,21 +308,29 @@ export function findClassListsInHtmlRange(
return result return result
} }
export function findClassListsInRange( export async function findClassListsInRange(
state: State,
doc: TextDocument, doc: TextDocument,
range?: Range, range?: Range,
mode?: 'html' | 'css' mode?: 'html' | 'css',
): DocumentClassList[] { includeCustom: boolean = true
): Promise<DocumentClassList[]> {
let classLists: DocumentClassList[]
if (mode === 'css') { 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, state: State,
doc: TextDocument doc: TextDocument
): DocumentClassList[] { ): Promise<DocumentClassList[]> {
if (isCssDoc(state, doc)) { if (isCssDoc(state, doc)) {
return findClassListsInCssRange(doc) return findClassListsInCssRange(doc)
} }
@ -257,6 +341,7 @@ export function findClassListsInDocument(
return flatten([ return flatten([
...boundaries.html.map((range) => findClassListsInHtmlRange(doc, range)), ...boundaries.html.map((range) => findClassListsInHtmlRange(doc, range)),
...boundaries.css.map((range) => findClassListsInCssRange(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 } return { line: line - 1, character: col - 1 }
} }
export function findClassNameAtPosition( export async function findClassNameAtPosition(
state: State, state: State,
doc: TextDocument, doc: TextDocument,
position: Position position: Position
): DocumentClassName { ): Promise<DocumentClassName> {
let classNames = [] let classNames = []
const searchRange = { const searchRange = {
start: { line: Math.max(position.line - 10, 0), character: 0 }, start: { line: Math.max(position.line - 10, 0), character: 0 },
@ -335,12 +420,12 @@ export function findClassNameAtPosition(
} }
if (isCssContext(state, doc, position)) { if (isCssContext(state, doc, position)) {
classNames = findClassNamesInRange(doc, searchRange, 'css') classNames = await findClassNamesInRange(state, doc, searchRange, 'css')
} else if ( } else if (
isHtmlContext(state, doc, position) || isHtmlContext(state, doc, position) ||
isJsContext(state, doc, position) isJsContext(state, doc, position)
) { ) {
classNames = findClassNamesInRange(doc, searchRange, 'html') classNames = await findClassNamesInRange(state, doc, searchRange, 'html')
} }
if (classNames.length === 0) { if (classNames.length === 0) {