2020-10-08 15:20:54 +00:00
|
|
|
import type { TextDocument, Range, Position } from 'vscode-languageserver'
|
2021-05-04 11:40:50 +00:00
|
|
|
import { DocumentClassName, DocumentClassList, State, DocumentHelperFunction } from './state'
|
2020-04-17 17:59:19 +00:00
|
|
|
import lineColumn from 'line-column'
|
2020-05-24 17:32:25 +00:00
|
|
|
import { isCssContext, isCssDoc } from './css'
|
2021-05-04 11:41:13 +00:00
|
|
|
import { isHtmlContext } from './html'
|
2020-05-17 16:13:14 +00:00
|
|
|
import { isWithinRange } from './isWithinRange'
|
2021-05-04 11:41:13 +00:00
|
|
|
import { isJsContext } from './js'
|
2020-10-08 15:20:54 +00:00
|
|
|
import { flatten } from './array'
|
2021-05-04 11:40:50 +00:00
|
|
|
import { getClassAttributeLexer, getComputedClassAttributeLexer } from './lexers'
|
2020-06-15 10:24:05 +00:00
|
|
|
import { getLanguageBoundaries } from './getLanguageBoundaries'
|
2020-08-12 17:45:36 +00:00
|
|
|
import { resolveRange } from './resolveRange'
|
2020-12-07 15:39:44 +00:00
|
|
|
const dlv = require('dlv')
|
|
|
|
import { createMultiRegexp } from './createMultiRegexp'
|
2020-04-17 17:59:19 +00:00
|
|
|
|
2020-04-11 21:20:45 +00:00
|
|
|
export function findAll(re: RegExp, str: string): RegExpMatchArray[] {
|
|
|
|
let match: RegExpMatchArray
|
|
|
|
let matches: RegExpMatchArray[] = []
|
|
|
|
while ((match = re.exec(str)) !== null) {
|
|
|
|
matches.push({ ...match })
|
|
|
|
}
|
|
|
|
return matches
|
|
|
|
}
|
|
|
|
|
|
|
|
export function findLast(re: RegExp, str: string): RegExpMatchArray {
|
|
|
|
const matches = findAll(re, str)
|
|
|
|
if (matches.length === 0) {
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
return matches[matches.length - 1]
|
|
|
|
}
|
|
|
|
|
2020-06-12 10:41:39 +00:00
|
|
|
export function getClassNamesInClassList({
|
|
|
|
classList,
|
|
|
|
range,
|
2020-06-17 17:34:53 +00:00
|
|
|
important,
|
2020-06-12 10:41:39 +00:00
|
|
|
}: DocumentClassList): DocumentClassName[] {
|
|
|
|
const parts = classList.split(/(\s+)/)
|
|
|
|
const names: DocumentClassName[] = []
|
|
|
|
let index = 0
|
|
|
|
for (let i = 0; i < parts.length; i++) {
|
|
|
|
if (i % 2 === 0) {
|
|
|
|
const start = indexToPosition(classList, index)
|
|
|
|
const end = indexToPosition(classList, index + parts[i].length)
|
|
|
|
names.push({
|
|
|
|
className: parts[i],
|
2020-06-17 17:34:53 +00:00
|
|
|
classList: {
|
|
|
|
classList,
|
|
|
|
range,
|
|
|
|
important,
|
|
|
|
},
|
|
|
|
relativeRange: {
|
|
|
|
start,
|
|
|
|
end,
|
|
|
|
},
|
2020-06-12 10:41:39 +00:00
|
|
|
range: {
|
|
|
|
start: {
|
|
|
|
line: range.start.line + start.line,
|
2021-05-04 11:40:50 +00:00
|
|
|
character: (end.line === 0 ? range.start.character : 0) + start.character,
|
2020-06-12 10:41:39 +00:00
|
|
|
},
|
|
|
|
end: {
|
|
|
|
line: range.start.line + end.line,
|
2021-05-04 11:40:50 +00:00
|
|
|
character: (end.line === 0 ? range.start.character : 0) + end.character,
|
2020-06-12 10:41:39 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
index += parts[i].length
|
|
|
|
}
|
|
|
|
return names
|
|
|
|
}
|
|
|
|
|
2020-12-07 15:39:44 +00:00
|
|
|
export async function findClassNamesInRange(
|
|
|
|
state: State,
|
2020-04-17 17:59:19 +00:00
|
|
|
doc: TextDocument,
|
2020-05-24 17:32:25 +00:00
|
|
|
range?: Range,
|
2020-12-07 15:39:44 +00:00
|
|
|
mode?: 'html' | 'css',
|
|
|
|
includeCustom: boolean = true
|
|
|
|
): Promise<DocumentClassName[]> {
|
2021-05-04 11:40:50 +00:00
|
|
|
const classLists = await findClassListsInRange(state, doc, range, mode, includeCustom)
|
2020-06-12 10:41:39 +00:00
|
|
|
return flatten(classLists.map(getClassNamesInClassList))
|
|
|
|
}
|
|
|
|
|
2020-12-07 15:39:44 +00:00
|
|
|
export async function findClassNamesInDocument(
|
2020-06-12 10:41:39 +00:00
|
|
|
state: State,
|
|
|
|
doc: TextDocument
|
2020-12-07 15:39:44 +00:00
|
|
|
): Promise<DocumentClassName[]> {
|
|
|
|
const classLists = await findClassListsInDocument(state, doc)
|
2020-06-12 10:41:39 +00:00
|
|
|
return flatten(classLists.map(getClassNamesInClassList))
|
2020-04-17 17:59:19 +00:00
|
|
|
}
|
|
|
|
|
2021-05-04 11:40:50 +00:00
|
|
|
export function findClassListsInCssRange(doc: TextDocument, range?: Range): DocumentClassList[] {
|
2020-04-17 17:59:19 +00:00
|
|
|
const text = doc.getText(range)
|
2020-06-17 17:34:53 +00:00
|
|
|
const matches = findAll(
|
|
|
|
/(@apply\s+)(?<classList>[^;}]+?)(?<important>\s*!important)?\s*[;}]/g,
|
|
|
|
text
|
|
|
|
)
|
2020-05-24 17:32:25 +00:00
|
|
|
const globalStart: Position = range ? range.start : { line: 0, character: 0 }
|
2020-04-17 17:59:19 +00:00
|
|
|
|
|
|
|
return matches.map((match) => {
|
|
|
|
const start = indexToPosition(text, match.index + match[1].length)
|
2021-05-04 11:40:50 +00:00
|
|
|
const end = indexToPosition(text, match.index + match[1].length + match.groups.classList.length)
|
2020-04-17 17:59:19 +00:00
|
|
|
return {
|
|
|
|
classList: match.groups.classList,
|
2020-06-17 17:34:53 +00:00
|
|
|
important: Boolean(match.groups.important),
|
2020-04-17 17:59:19 +00:00
|
|
|
range: {
|
|
|
|
start: {
|
2020-05-24 17:32:25 +00:00
|
|
|
line: globalStart.line + start.line,
|
2021-05-04 11:40:50 +00:00
|
|
|
character: (end.line === 0 ? globalStart.character : 0) + start.character,
|
2020-04-17 17:59:19 +00:00
|
|
|
},
|
|
|
|
end: {
|
2020-05-24 17:32:25 +00:00
|
|
|
line: globalStart.line + end.line,
|
2021-05-04 11:40:50 +00:00
|
|
|
character: (end.line === 0 ? globalStart.character : 0) + end.character,
|
2020-04-17 17:59:19 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2020-12-07 15:39:44 +00:00
|
|
|
async function findCustomClassLists(
|
|
|
|
state: State,
|
|
|
|
doc: TextDocument,
|
|
|
|
range?: Range
|
|
|
|
): Promise<DocumentClassList[]> {
|
2021-05-03 17:00:04 +00:00
|
|
|
const settings = await state.editor.getConfiguration(doc.uri)
|
2021-05-04 11:40:50 +00:00
|
|
|
const regexes = dlv(settings, 'tailwindCSS.experimental.classRegex', [])
|
2020-12-07 15:39:44 +00:00
|
|
|
|
|
|
|
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 {
|
2021-05-04 11:40:50 +00:00
|
|
|
let [containerRegex, classRegex] = Array.isArray(regexes[i]) ? regexes[i] : [regexes[i]]
|
2020-12-07 15:39:44 +00:00
|
|
|
|
2022-02-28 13:49:07 +00:00
|
|
|
let containerRegex2 = createMultiRegexp(containerRegex)
|
2020-12-07 15:39:44 +00:00
|
|
|
let containerMatch
|
|
|
|
|
2022-02-28 13:49:07 +00:00
|
|
|
while ((containerMatch = containerRegex2.exec(text)) !== null) {
|
2021-05-04 11:40:50 +00:00
|
|
|
const searchStart = doc.offsetAt(range?.start || { line: 0, character: 0 })
|
2020-12-07 15:39:44 +00:00
|
|
|
const matchStart = searchStart + containerMatch.start
|
|
|
|
const matchEnd = searchStart + containerMatch.end
|
|
|
|
|
|
|
|
if (classRegex) {
|
2022-02-28 13:49:07 +00:00
|
|
|
let classRegex2 = createMultiRegexp(classRegex)
|
2020-12-07 15:39:44 +00:00
|
|
|
let classMatch
|
|
|
|
|
2022-02-28 13:49:07 +00:00
|
|
|
while ((classMatch = classRegex2.exec(containerMatch.match)) !== null) {
|
2020-12-07 15:39:44 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2021-10-08 15:51:14 +00:00
|
|
|
export function matchClassAttributes(text: string, attributes: string[]): RegExpMatchArray[] {
|
|
|
|
const attrs = attributes.filter((x) => typeof x === 'string').flatMap((a) => [a, `\\[${a}\\]`])
|
|
|
|
const re = /(?:\s|:|\()(ATTRS)\s*=\s*['"`{]/
|
|
|
|
return findAll(new RegExp(re.source.replace('ATTRS', attrs.join('|')), 'gi'), text)
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function findClassListsInHtmlRange(
|
|
|
|
state: State,
|
|
|
|
doc: TextDocument,
|
|
|
|
range?: Range
|
|
|
|
): Promise<DocumentClassList[]> {
|
2020-05-17 16:13:14 +00:00
|
|
|
const text = doc.getText(range)
|
2021-10-08 15:51:14 +00:00
|
|
|
|
|
|
|
const matches = matchClassAttributes(
|
|
|
|
text,
|
|
|
|
(await state.editor.getConfiguration(doc.uri)).tailwindCSS.classAttributes
|
|
|
|
)
|
|
|
|
|
2020-05-17 16:13:14 +00:00
|
|
|
const result: DocumentClassList[] = []
|
|
|
|
|
|
|
|
matches.forEach((match) => {
|
2020-06-29 18:06:14 +00:00
|
|
|
const subtext = text.substr(match.index + match[0].length - 1)
|
2020-05-17 16:13:14 +00:00
|
|
|
|
2020-06-12 11:30:12 +00:00
|
|
|
let lexer =
|
2021-10-08 15:51:14 +00:00
|
|
|
match[0][0] === ':' || (match[1].startsWith('[') && match[1].endsWith(']'))
|
2020-06-12 11:30:12 +00:00
|
|
|
? getComputedClassAttributeLexer()
|
|
|
|
: getClassAttributeLexer()
|
2020-05-17 16:13:14 +00:00
|
|
|
lexer.reset(subtext)
|
|
|
|
|
|
|
|
let classLists: { value: string; offset: number }[] = []
|
|
|
|
let token: moo.Token
|
|
|
|
let currentClassList: { value: string; offset: number }
|
|
|
|
|
|
|
|
try {
|
|
|
|
for (let token of lexer) {
|
|
|
|
if (token.type === 'classlist') {
|
|
|
|
if (currentClassList) {
|
|
|
|
currentClassList.value += token.value
|
|
|
|
} else {
|
|
|
|
currentClassList = {
|
|
|
|
value: token.value,
|
|
|
|
offset: token.offset,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if (currentClassList) {
|
|
|
|
classLists.push({
|
|
|
|
value: currentClassList.value,
|
|
|
|
offset: currentClassList.offset,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
currentClassList = undefined
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (_) {}
|
|
|
|
|
|
|
|
if (currentClassList) {
|
|
|
|
classLists.push({
|
|
|
|
value: currentClassList.value,
|
|
|
|
offset: currentClassList.offset,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
result.push(
|
|
|
|
...classLists
|
|
|
|
.map(({ value, offset }) => {
|
|
|
|
if (value.trim() === '') {
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
|
|
|
const before = value.match(/^\s*/)
|
|
|
|
const beforeOffset = before === null ? 0 : before[0].length
|
|
|
|
const after = value.match(/\s*$/)
|
|
|
|
const afterOffset = after === null ? 0 : -after[0].length
|
|
|
|
|
|
|
|
const start = indexToPosition(
|
|
|
|
text,
|
|
|
|
match.index + match[0].length - 1 + offset + beforeOffset
|
|
|
|
)
|
|
|
|
const end = indexToPosition(
|
|
|
|
text,
|
2021-05-04 11:40:50 +00:00
|
|
|
match.index + match[0].length - 1 + offset + value.length + afterOffset
|
2020-05-17 16:13:14 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
return {
|
2020-06-16 10:24:35 +00:00
|
|
|
classList: value.substr(beforeOffset, value.length + afterOffset),
|
2020-05-17 16:13:14 +00:00
|
|
|
range: {
|
|
|
|
start: {
|
2020-06-12 10:41:39 +00:00
|
|
|
line: (range?.start.line || 0) + start.line,
|
2021-05-04 11:40:50 +00:00
|
|
|
character: (end.line === 0 ? range?.start.character || 0 : 0) + start.character,
|
2020-05-17 16:13:14 +00:00
|
|
|
},
|
|
|
|
end: {
|
2020-06-12 10:41:39 +00:00
|
|
|
line: (range?.start.line || 0) + end.line,
|
2021-05-04 11:40:50 +00:00
|
|
|
character: (end.line === 0 ? range?.start.character || 0 : 0) + end.character,
|
2020-05-17 16:13:14 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.filter((x) => x !== null)
|
|
|
|
)
|
|
|
|
})
|
|
|
|
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
2020-12-07 15:39:44 +00:00
|
|
|
export async function findClassListsInRange(
|
|
|
|
state: State,
|
2020-05-17 16:13:14 +00:00
|
|
|
doc: TextDocument,
|
2020-06-12 10:41:39 +00:00
|
|
|
range?: Range,
|
2020-12-07 15:39:44 +00:00
|
|
|
mode?: 'html' | 'css',
|
|
|
|
includeCustom: boolean = true
|
|
|
|
): Promise<DocumentClassList[]> {
|
|
|
|
let classLists: DocumentClassList[]
|
2020-05-17 16:13:14 +00:00
|
|
|
if (mode === 'css') {
|
2020-12-07 15:39:44 +00:00
|
|
|
classLists = findClassListsInCssRange(doc, range)
|
|
|
|
} else {
|
2021-10-08 15:51:14 +00:00
|
|
|
classLists = await findClassListsInHtmlRange(state, doc, range)
|
2020-05-17 16:13:14 +00:00
|
|
|
}
|
2021-05-04 11:40:50 +00:00
|
|
|
return [...classLists, ...(includeCustom ? await findCustomClassLists(state, doc, range) : [])]
|
2020-05-17 16:13:14 +00:00
|
|
|
}
|
|
|
|
|
2020-12-07 15:39:44 +00:00
|
|
|
export async function findClassListsInDocument(
|
2020-05-24 17:32:25 +00:00
|
|
|
state: State,
|
|
|
|
doc: TextDocument
|
2020-12-07 15:39:44 +00:00
|
|
|
): Promise<DocumentClassList[]> {
|
2020-05-24 17:32:25 +00:00
|
|
|
if (isCssDoc(state, doc)) {
|
|
|
|
return findClassListsInCssRange(doc)
|
|
|
|
}
|
|
|
|
|
2020-06-15 10:24:05 +00:00
|
|
|
let boundaries = getLanguageBoundaries(state, doc)
|
|
|
|
if (!boundaries) return []
|
2020-05-24 17:32:25 +00:00
|
|
|
|
2020-06-15 10:24:05 +00:00
|
|
|
return flatten([
|
2021-10-08 15:51:14 +00:00
|
|
|
...(await Promise.all(
|
|
|
|
boundaries.html.map((range) => findClassListsInHtmlRange(state, doc, range))
|
|
|
|
)),
|
2020-06-15 10:24:05 +00:00
|
|
|
...boundaries.css.map((range) => findClassListsInCssRange(doc, range)),
|
2020-12-07 15:39:44 +00:00
|
|
|
await findCustomClassLists(state, doc),
|
2020-06-15 10:24:05 +00:00
|
|
|
])
|
2020-05-24 17:32:25 +00:00
|
|
|
}
|
|
|
|
|
2020-08-12 17:45:36 +00:00
|
|
|
export function findHelperFunctionsInDocument(
|
|
|
|
state: State,
|
|
|
|
doc: TextDocument
|
|
|
|
): DocumentHelperFunction[] {
|
|
|
|
if (isCssDoc(state, doc)) {
|
|
|
|
return findHelperFunctionsInRange(doc)
|
|
|
|
}
|
|
|
|
|
|
|
|
let boundaries = getLanguageBoundaries(state, doc)
|
|
|
|
if (!boundaries) return []
|
|
|
|
|
2021-05-04 11:40:50 +00:00
|
|
|
return flatten(boundaries.css.map((range) => findHelperFunctionsInRange(doc, range)))
|
2020-08-12 17:45:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export function findHelperFunctionsInRange(
|
|
|
|
doc: TextDocument,
|
|
|
|
range?: Range
|
|
|
|
): DocumentHelperFunction[] {
|
|
|
|
const text = doc.getText(range)
|
|
|
|
const matches = findAll(
|
|
|
|
/(?<before>^|\s)(?<helper>theme|config)\((?:(?<single>')([^']+)'|(?<double>")([^"]+)")\)/gm,
|
|
|
|
text
|
|
|
|
)
|
|
|
|
|
|
|
|
return matches.map((match) => {
|
|
|
|
let value = match[4] || match[6]
|
|
|
|
let startIndex = match.index + match.groups.before.length
|
|
|
|
return {
|
|
|
|
full: match[0].substr(match.groups.before.length),
|
|
|
|
value,
|
|
|
|
helper: match.groups.helper === 'theme' ? 'theme' : 'config',
|
|
|
|
quotes: match.groups.single ? "'" : '"',
|
|
|
|
range: resolveRange(
|
|
|
|
{
|
|
|
|
start: indexToPosition(text, startIndex),
|
|
|
|
end: indexToPosition(text, match.index + match[0].length),
|
|
|
|
},
|
|
|
|
range
|
|
|
|
),
|
|
|
|
valueRange: resolveRange(
|
|
|
|
{
|
2021-05-04 11:40:50 +00:00
|
|
|
start: indexToPosition(text, startIndex + match.groups.helper.length + 1),
|
2020-08-12 17:45:36 +00:00
|
|
|
end: indexToPosition(
|
|
|
|
text,
|
|
|
|
startIndex + match.groups.helper.length + 1 + 1 + value.length + 1
|
|
|
|
),
|
|
|
|
},
|
|
|
|
range
|
|
|
|
),
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2020-06-12 15:41:49 +00:00
|
|
|
export function indexToPosition(str: string, index: number): Position {
|
2020-04-17 17:59:19 +00:00
|
|
|
const { line, col } = lineColumn(str + '\n', index)
|
|
|
|
return { line: line - 1, character: col - 1 }
|
|
|
|
}
|
2020-05-17 16:13:14 +00:00
|
|
|
|
2020-12-07 15:39:44 +00:00
|
|
|
export async function findClassNameAtPosition(
|
2020-05-17 16:13:14 +00:00
|
|
|
state: State,
|
|
|
|
doc: TextDocument,
|
|
|
|
position: Position
|
2020-12-07 15:39:44 +00:00
|
|
|
): Promise<DocumentClassName> {
|
2020-05-17 16:13:14 +00:00
|
|
|
let classNames = []
|
2021-08-13 16:59:14 +00:00
|
|
|
const positionOffset = doc.offsetAt(position)
|
|
|
|
const searchRange: Range = {
|
2021-10-08 16:36:17 +00:00
|
|
|
start: doc.positionAt(Math.max(0, positionOffset - 1000)),
|
|
|
|
end: doc.positionAt(positionOffset + 1000),
|
2020-05-17 16:13:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (isCssContext(state, doc, position)) {
|
2020-12-07 15:39:44 +00:00
|
|
|
classNames = await findClassNamesInRange(state, doc, searchRange, 'css')
|
2021-05-04 11:40:50 +00:00
|
|
|
} else if (isHtmlContext(state, doc, position) || isJsContext(state, doc, position)) {
|
2020-12-07 15:39:44 +00:00
|
|
|
classNames = await findClassNamesInRange(state, doc, searchRange, 'html')
|
2020-05-17 16:13:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (classNames.length === 0) {
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
2021-05-04 11:40:50 +00:00
|
|
|
const className = classNames.find(({ range }) => isWithinRange(position, range))
|
2020-05-17 16:13:14 +00:00
|
|
|
|
|
|
|
if (!className) return null
|
|
|
|
|
|
|
|
return className
|
|
|
|
}
|