add hover, color decorator, linting support for classRegex setting (#129)
parent
5bd09789f4
commit
b2d47d220b
|
@ -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) }
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -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) {
|
||||||
|
|
Loading…
Reference in New Issue