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)
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'
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,

View File

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

View File

@ -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<CssConflictDiagnostic[]> {
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)

View File

@ -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<InvalidApplyDiagnostic[]> {
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)

View File

@ -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) => {

View File

@ -77,7 +77,7 @@ async function provideClassNameHover(
document: TextDocument,
position: Position
): Promise<Hover> {
let className = findClassNameAtPosition(state, document, position)
let className = await findClassNameAtPosition(state, document, position)
if (className === null) return null
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'
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<DocumentClassName[]> {
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<DocumentClassName[]> {
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<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(
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<DocumentClassList[]> {
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<DocumentClassList[]> {
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<DocumentClassName> {
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) {