tailwind-ctp-intellisense/packages/tailwindcss-language-service/src/completionProvider.ts

1126 lines
33 KiB
TypeScript
Raw Normal View History

import { State } from './util/state'
import type {
2020-04-11 21:20:45 +00:00
CompletionItem,
CompletionItemKind,
Range,
MarkupKind,
CompletionList,
TextDocument,
Position,
2021-05-18 11:22:18 +00:00
CompletionContext,
2020-04-11 21:20:45 +00:00
} from 'vscode-languageserver'
const dlv = require('dlv')
import removeMeta from './util/removeMeta'
import { getColor, getColorFromValue } from './util/color'
import { isHtmlContext } from './util/html'
import { isCssContext } from './util/css'
import { findLast } from './util/find'
import { stringifyConfigValue, stringifyCss } from './util/stringify'
import { stringifyScreen, Screen } from './util/screens'
import isObject from './util/isObject'
2020-05-03 14:57:15 +00:00
import * as emmetHelper from 'vscode-emmet-helper-bundled'
import { isValidLocationForEmmetAbbreviation } from './util/isValidLocationForEmmetAbbreviation'
import { isJsContext } from './util/js'
import { naturalExpand } from './util/naturalExpand'
import semver from 'semver'
import { docsUrl } from './util/docsUrl'
import { ensureArray } from './util/array'
import { getClassAttributeLexer, getComputedClassAttributeLexer } from './util/lexers'
import { validateApply } from './util/validateApply'
import { flagEnabled } from './util/flagEnabled'
import { remToPx } from './util/remToPx'
import { createMultiRegexp } from './util/createMultiRegexp'
import * as jit from './util/jit'
import { TinyColor } from '@ctrl/tinycolor'
import { getVariantsFromClassName } from './util/getVariantsFromClassName'
let isUtil = (className) =>
Array.isArray(className.__info)
? className.__info.some((x) => x.__source === 'utilities')
: className.__info.__source === 'utilities'
2020-04-11 21:20:45 +00:00
2020-11-26 18:27:13 +00:00
export function completionsFromClassList(
2020-04-11 21:20:45 +00:00
state: State,
classList: string,
2020-05-10 12:06:22 +00:00
classListRange: Range,
filter?: (item: CompletionItem) => boolean,
2021-05-18 11:22:18 +00:00
document?: TextDocument,
context?: CompletionContext
2020-04-11 21:20:45 +00:00
): CompletionList {
let classNames = classList.split(/[\s+]/)
const partialClassName = classNames[classNames.length - 1]
2020-04-27 21:23:22 +00:00
let sep = state.separator
2020-04-11 21:20:45 +00:00
let parts = partialClassName.split(sep)
let subset: any
let subsetKey: string[] = []
2020-04-11 21:20:45 +00:00
let isSubset: boolean = false
let replacementRange = {
...classListRange,
start: {
...classListRange.start,
character: classListRange.end.character - partialClassName.length,
},
}
if (state.jit) {
2021-05-18 11:22:18 +00:00
if (
context &&
(context.triggerKind === 1 ||
(context.triggerKind === 2 && context.triggerCharacter === '/')) &&
partialClassName.includes('/')
) {
let beforeSlash = partialClassName.split('/').slice(0, -1).join('/')
let testClass = beforeSlash + '/[0]'
let { rules } = jit.generateRules(state, [testClass])
if (rules.length > 0) {
let opacities = dlv(state.config, 'theme.opacity', {})
if (!isObject(opacities)) {
opacities = {}
}
return {
isIncomplete: false,
items: Object.keys(opacities).map((opacity, index) => {
let className = `${beforeSlash}/${opacity}`
let kind: CompletionItemKind = 21
let documentation: string = null
const color = getColor(state, className)
if (color !== null) {
kind = 16
if (typeof color !== 'string') {
documentation = color.toRgbString().replace(/(^rgba\([^)]+) 0\)$/, '$1 0.001)')
}
}
return {
label: opacity,
detail: stringifyConfigValue(opacities[opacity]),
documentation,
kind,
sortText: naturalExpand(index),
data: [className],
textEdit: {
newText: opacity,
range: {
...replacementRange,
start: {
...replacementRange.start,
character: replacementRange.start.character + beforeSlash.length + 1,
},
},
},
}
}),
}
}
}
let allVariants = Object.keys(state.variants)
let { variants: existingVariants, offset } = getVariantsFromClassName(state, partialClassName)
replacementRange.start.character += offset
let important = partialClassName.substr(offset).startsWith('!')
if (important) {
replacementRange.start.character += 1
}
let items: CompletionItem[] = []
if (!important) {
items.push(
...Object.entries(state.variants)
.filter(([variant]) => !existingVariants.includes(variant))
.map(([variant, definition], index) => {
let resultingVariants = [...existingVariants, variant].sort(
(a, b) => allVariants.indexOf(b) - allVariants.indexOf(a)
)
return {
label: variant + sep,
kind: 9,
detail: definition,
data: 'variant',
command: {
title: '',
command: 'editor.action.triggerSuggest',
},
sortText: '-' + naturalExpand(index),
textEdit: {
newText: resultingVariants[resultingVariants.length - 1] + sep,
range: replacementRange,
},
additionalTextEdits:
resultingVariants.length > 1
? [
{
newText:
resultingVariants.slice(0, resultingVariants.length - 1).join(sep) + sep,
range: {
start: {
...classListRange.start,
character: classListRange.end.character - partialClassName.length,
},
end: {
...replacementRange.start,
character: replacementRange.start.character,
},
},
},
]
: [],
} as CompletionItem
})
)
}
return {
isIncomplete: false,
items: items
.concat(
Object.keys(state.classNames.classNames)
.filter((className) => {
let item = state.classNames.classNames[className]
if (existingVariants.length === 0) {
return item.__info
}
return item.__info && isUtil(item)
})
.map((className, index) => {
let kind: CompletionItemKind = 21
let documentation: string = null
const color = getColor(state, className)
if (color !== null) {
kind = 16
if (typeof color !== 'string' && color.a !== 0) {
documentation = color.toRgbString()
}
}
return {
label: className,
kind,
documentation,
sortText: naturalExpand(index),
data: [...existingVariants, important ? `!${className}` : className],
textEdit: {
newText: className,
range: replacementRange,
},
} as CompletionItem
})
)
.filter((item) => {
if (item === null) {
return false
}
if (filter && !filter(item)) {
return false
}
return true
}),
}
}
2020-04-11 21:20:45 +00:00
for (let i = parts.length - 1; i > 0; i--) {
let keys = parts.slice(0, i).filter(Boolean)
subset = dlv(state.classNames.classNames, keys)
if (typeof subset !== 'undefined' && typeof dlv(subset, ['__info', '__rule']) === 'undefined') {
2020-04-11 21:20:45 +00:00
isSubset = true
subsetKey = keys
2020-04-11 21:20:45 +00:00
replacementRange = {
...replacementRange,
start: {
...replacementRange.start,
character: replacementRange.start.character + keys.join(sep).length + sep.length,
2020-04-11 21:20:45 +00:00
},
}
break
}
}
return {
isIncomplete: false,
2020-05-10 12:06:22 +00:00
items: Object.keys(isSubset ? subset : state.classNames.classNames)
2020-11-19 17:34:59 +00:00
.filter((k) => k !== '__info')
.filter((className) => isContextItem(state, [...subsetKey, className]))
.map(
(className, index): CompletionItem => {
return {
label: className + sep,
kind: 9,
documentation: null,
command: {
title: '',
command: 'editor.action.triggerSuggest',
},
sortText: '-' + naturalExpand(index),
data: [...subsetKey, className],
textEdit: {
newText: className + sep,
range: replacementRange,
},
2020-04-11 21:20:45 +00:00
}
}
2020-11-19 17:34:59 +00:00
)
.concat(
Object.keys(isSubset ? subset : state.classNames.classNames)
.filter((className) =>
dlv(state.classNames.classNames, [...subsetKey, className, '__info'])
2020-11-19 17:34:59 +00:00
)
.map((className, index) => {
let kind: CompletionItemKind = 21
let documentation: string = null
const color = getColor(state, className)
2020-11-19 17:34:59 +00:00
if (color !== null) {
kind = 16
if (typeof color !== 'string' && color.a !== 0) {
documentation = color.toRgbString()
}
}
2020-04-11 21:20:45 +00:00
2020-11-19 17:34:59 +00:00
return {
label: className,
kind,
documentation,
sortText: naturalExpand(index),
data: [...subsetKey, className],
textEdit: {
newText: className,
range: replacementRange,
},
}
})
)
.filter((item) => {
if (item === null) {
return false
2020-04-11 21:20:45 +00:00
}
2020-05-10 12:06:22 +00:00
if (filter && !filter(item)) {
2020-11-19 17:34:59 +00:00
return false
2020-05-10 12:06:22 +00:00
}
2020-11-19 17:34:59 +00:00
return true
}),
2020-04-11 21:20:45 +00:00
}
}
function provideClassAttributeCompletions(
state: State,
document: TextDocument,
2021-05-18 11:22:18 +00:00
position: Position,
context?: CompletionContext
2020-04-11 21:20:45 +00:00
): CompletionList {
let str = document.getText({
2020-04-11 21:20:45 +00:00
start: { line: Math.max(position.line - 10, 0), character: 0 },
end: position,
})
const match = findLast(/(?:\s|:|\()(?:class(?:Name)?|\[ngClass\])=['"`{]/gi, str)
2020-04-11 21:20:45 +00:00
if (match === null) {
return null
}
const lexer =
match[0][0] === ':' || match[0].trim().startsWith('[ngClass]')
? getComputedClassAttributeLexer()
: getClassAttributeLexer()
lexer.reset(str.substr(match.index + match[0].length - 1))
try {
let tokens = Array.from(lexer)
let last = tokens[tokens.length - 1]
if (last.type.startsWith('start') || last.type === 'classlist') {
let classList = ''
for (let i = tokens.length - 1; i >= 0; i--) {
if (tokens[i].type === 'classlist') {
classList = tokens[i].value + classList
} else {
break
}
}
return completionsFromClassList(
state,
classList,
{
start: {
line: position.line,
character: position.character - classList.length,
},
end: position,
2020-04-11 21:20:45 +00:00
},
undefined,
2021-05-18 11:22:18 +00:00
document,
context
)
2020-04-11 21:20:45 +00:00
}
} catch (_) {}
2020-04-11 21:20:45 +00:00
return null
2020-04-11 21:20:45 +00:00
}
async function provideCustomClassNameCompletions(
state: State,
document: TextDocument,
position: Position
): Promise<CompletionList> {
const settings = await state.editor.getConfiguration(document.uri)
2021-05-04 11:40:50 +00:00
const regexes = dlv(settings, 'tailwindCSS.experimental.classRegex', [])
if (regexes.length === 0) return null
const positionOffset = document.offsetAt(position)
const searchRange: Range = {
start: document.positionAt(Math.max(0, positionOffset - 500)),
end: document.positionAt(positionOffset + 500),
}
let str = document.getText(searchRange)
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(str)) !== null) {
const searchStart = document.offsetAt(searchRange.start)
const matchStart = searchStart + containerMatch.start
const matchEnd = searchStart + containerMatch.end
const cursor = document.offsetAt(position)
if (cursor >= matchStart && cursor <= matchEnd) {
let classList
if (classRegex) {
classRegex = createMultiRegexp(classRegex)
let classMatch
while ((classMatch = classRegex.exec(containerMatch.match)) !== null) {
const classMatchStart = matchStart + classMatch.start
const classMatchEnd = matchStart + classMatch.end
if (cursor >= classMatchStart && cursor <= classMatchEnd) {
classList = classMatch.match.substr(0, cursor - classMatchStart)
}
}
if (typeof classList === 'undefined') {
throw Error()
}
} else {
classList = containerMatch.match.substr(0, cursor - matchStart)
}
return completionsFromClassList(state, classList, {
start: {
line: position.line,
character: position.character - classList.length,
},
end: position,
})
}
}
} catch (_) {}
}
return null
}
2020-04-11 21:20:45 +00:00
function provideAtApplyCompletions(
state: State,
document: TextDocument,
position: Position
2020-04-11 21:20:45 +00:00
): CompletionList {
let str = document.getText({
2020-04-11 21:20:45 +00:00
start: { line: Math.max(position.line - 30, 0), character: 0 },
end: position,
})
const match = findLast(/@apply\s+(?<classList>[^;}]*)$/gi, str)
if (match === null) {
return null
}
const classList = match.groups.classList
2020-05-10 12:06:22 +00:00
return completionsFromClassList(
state,
classList,
{
start: {
line: position.line,
character: position.character - classList.length,
},
end: position,
2020-04-11 21:20:45 +00:00
},
2020-05-10 12:06:22 +00:00
(item) => {
if (item.kind === 9) {
2020-11-19 17:34:59 +00:00
return (
semver.gte(state.version, '2.0.0-alpha.1') || flagEnabled(state, 'applyComplexClasses')
2020-11-19 17:34:59 +00:00
)
}
let validated = validateApply(state, item.data)
return validated !== null && validated.isApplyable === true
2020-05-10 12:06:22 +00:00
}
)
2020-04-11 21:20:45 +00:00
}
function provideClassNameCompletions(
state: State,
document: TextDocument,
2021-05-18 11:22:18 +00:00
position: Position,
context?: CompletionContext
2020-04-11 21:20:45 +00:00
): CompletionList {
if (isHtmlContext(state, document, position) || isJsContext(state, document, position)) {
2021-05-18 11:22:18 +00:00
return provideClassAttributeCompletions(state, document, position, context)
2020-04-11 21:20:45 +00:00
}
if (isCssContext(state, document, position)) {
return provideAtApplyCompletions(state, document, position)
2020-04-11 21:20:45 +00:00
}
return null
}
function provideCssHelperCompletions(
state: State,
document: TextDocument,
position: Position
2020-04-11 21:20:45 +00:00
): CompletionList {
if (!isCssContext(state, document, position)) {
2020-04-11 21:20:45 +00:00
return null
}
let text = document.getText({
2020-04-11 21:20:45 +00:00
start: { line: position.line, character: 0 },
// read one extra character so we can see if it's a ] later
end: { line: position.line, character: position.character + 1 },
})
const match = text
.substr(0, text.length - 1) // don't include that extra character from earlier
.match(/\b(?<helper>config|theme)\(['"](?<keys>[^'"]*)$/)
if (match === null) {
return null
}
let base = match.groups.helper === 'config' ? state.config : dlv(state.config, 'theme', {})
2020-04-11 21:20:45 +00:00
let parts = match.groups.keys.split(/([\[\].]+)/)
let keys = parts.filter((_, i) => i % 2 === 0)
let separators = parts.filter((_, i) => i % 2 !== 0)
// let obj =
// keys.length === 1 ? base : dlv(base, keys.slice(0, keys.length - 1), {})
// if (!isObject(obj)) return null
function totalLength(arr: string[]): number {
return arr.reduce((acc, cur) => acc + cur.length, 0)
}
let obj: any
let offset: number = 0
let separator: string = separators.length ? separators[separators.length - 1] : null
2020-04-11 21:20:45 +00:00
if (keys.length === 1) {
obj = base
} else {
for (let i = keys.length - 1; i > 0; i--) {
let o = dlv(base, keys.slice(0, i))
if (isObject(o)) {
obj = o
offset = totalLength(parts.slice(i * 2))
separator = separators[i - 1]
break
}
}
}
if (!obj) return null
return {
isIncomplete: false,
2020-04-27 22:18:28 +00:00
items: Object.keys(obj).map((item, index) => {
let color = getColorFromValue(obj[item])
const replaceDot: boolean = item.indexOf('.') !== -1 && separator && separator.endsWith('.')
2020-04-11 21:20:45 +00:00
const insertClosingBrace: boolean =
text.charAt(text.length - 1) !== ']' &&
(replaceDot || (separator && separator.endsWith('[')))
const detail = stringifyConfigValue(obj[item])
2020-04-11 21:20:45 +00:00
return {
label: item,
filterText: `${replaceDot ? '.' : ''}${item}`,
2020-04-27 22:18:28 +00:00
sortText: naturalExpand(index),
kind: color ? 16 : isObject(obj[item]) ? 9 : 10,
// VS Code bug causes some values to not display in some cases
detail: detail === '0' || detail === 'transparent' ? `${detail} ` : detail,
documentation: color instanceof TinyColor && color.a !== 0 ? color.toRgbString() : null,
2020-04-11 21:20:45 +00:00
textEdit: {
newText: `${replaceDot ? '[' : ''}${item}${insertClosingBrace ? ']' : ''}`,
2020-04-11 21:20:45 +00:00
range: {
start: {
line: position.line,
character:
position.character - keys[keys.length - 1].length - (replaceDot ? 1 : 0) - offset,
2020-04-11 21:20:45 +00:00
},
end: position,
},
},
data: 'helper',
}
}),
}
}
2020-04-27 22:52:31 +00:00
function provideTailwindDirectiveCompletions(
state: State,
document: TextDocument,
position: Position
2020-04-27 22:52:31 +00:00
): CompletionList {
if (!isCssContext(state, document, position)) {
2020-04-27 22:52:31 +00:00
return null
}
let text = document.getText({
2020-04-27 22:52:31 +00:00
start: { line: position.line, character: 0 },
end: position,
})
const match = text.match(/^\s*@tailwind\s+(?<partial>[^\s]*)$/i)
if (match === null) return null
return {
isIncomplete: false,
items: [
semver.gte(state.version, '1.0.0-beta.1')
? {
label: 'base',
documentation: {
kind: 'markdown' as typeof MarkupKind.Markdown,
2020-04-28 20:42:43 +00:00
value: `This injects Tailwinds base styles and any base styles registered by plugins.\n\n[Tailwind CSS Documentation](${docsUrl(
state.version,
'functions-and-directives/#tailwind'
)})`,
},
}
: {
label: 'preflight',
documentation: {
kind: 'markdown' as typeof MarkupKind.Markdown,
2020-04-28 20:42:43 +00:00
value: `This injects Tailwinds base styles, which is a combination of Normalize.css and some additional base styles.\n\n[Tailwind CSS Documentation](${docsUrl(
state.version,
'functions-and-directives/#tailwind'
)})`,
},
},
2020-04-27 22:52:31 +00:00
{
label: 'components',
documentation: {
kind: 'markdown' as typeof MarkupKind.Markdown,
2020-04-28 20:42:43 +00:00
value: `This injects Tailwinds component classes and any component classes registered by plugins.\n\n[Tailwind CSS Documentation](${docsUrl(
state.version,
'functions-and-directives/#tailwind'
)})`,
2020-04-27 22:52:31 +00:00
},
},
{
label: 'utilities',
documentation: {
kind: 'markdown' as typeof MarkupKind.Markdown,
2020-04-28 20:42:43 +00:00
value: `This injects Tailwinds utility classes and any utility classes registered by plugins.\n\n[Tailwind CSS Documentation](${docsUrl(
state.version,
'functions-and-directives/#tailwind'
)})`,
2020-04-27 22:52:31 +00:00
},
},
{
label: 'screens',
documentation: {
kind: 'markdown' as typeof MarkupKind.Markdown,
2020-04-28 20:42:43 +00:00
value: `Use this directive to control where Tailwind injects the responsive variations of each utility.\n\nIf omitted, Tailwind will append these classes to the very end of your stylesheet by default.\n\n[Tailwind CSS Documentation](${docsUrl(
state.version,
'functions-and-directives/#tailwind'
)})`,
2020-04-27 22:52:31 +00:00
},
},
].map((item) => ({
...item,
kind: 21,
data: '@tailwind',
2020-04-27 22:52:31 +00:00
textEdit: {
newText: item.label,
range: {
start: {
line: position.line,
character: position.character - match.groups.partial.length,
},
end: position,
},
},
})),
}
}
2020-04-12 22:48:57 +00:00
function provideVariantsDirectiveCompletions(
state: State,
document: TextDocument,
position: Position
2020-04-12 22:48:57 +00:00
): CompletionList {
if (!isCssContext(state, document, position)) {
2020-04-12 22:48:57 +00:00
return null
}
let text = document.getText({
2020-04-12 22:48:57 +00:00
start: { line: position.line, character: 0 },
end: position,
})
const match = text.match(/^\s*@variants\s+(?<partial>[^}]*)$/i)
if (match === null) return null
const parts = match.groups.partial.split(/\s*,\s*/)
if (/\s+/.test(parts[parts.length - 1])) return null
let possibleVariants = Object.keys(state.variants)
2020-04-12 22:48:57 +00:00
const existingVariants = parts.slice(0, parts.length - 1)
if (state.jit) {
possibleVariants.unshift('responsive')
possibleVariants = possibleVariants.filter((v) => !state.screens.includes(v))
}
2020-04-12 22:48:57 +00:00
return {
isIncomplete: false,
items: possibleVariants
2020-04-12 22:48:57 +00:00
.filter((v) => existingVariants.indexOf(v) === -1)
.map((variant, index) => ({
2020-04-12 22:48:57 +00:00
// TODO: detail
label: variant,
detail: state.variants[variant],
kind: 21,
2020-04-12 22:48:57 +00:00
data: 'variant',
sortText: naturalExpand(index),
2020-04-12 22:48:57 +00:00
textEdit: {
newText: variant,
range: {
start: {
line: position.line,
character: position.character - parts[parts.length - 1].length,
},
end: position,
},
},
})),
}
}
2020-11-27 17:13:46 +00:00
function provideLayerDirectiveCompletions(
state: State,
document: TextDocument,
position: Position
): CompletionList {
if (!isCssContext(state, document, position)) {
return null
}
let text = document.getText({
start: { line: position.line, character: 0 },
end: position,
})
const match = text.match(/^\s*@layer\s+(?<partial>[^\s]*)$/i)
if (match === null) return null
return {
isIncomplete: false,
items: ['base', 'components', 'utilities'].map((layer, index) => ({
label: layer,
kind: 21,
data: 'layer',
sortText: naturalExpand(index),
textEdit: {
newText: layer,
range: {
start: {
line: position.line,
character: position.character - match.groups.partial.length,
},
end: position,
},
},
})),
}
}
2020-04-12 17:11:41 +00:00
function provideScreenDirectiveCompletions(
state: State,
document: TextDocument,
position: Position
2020-04-12 17:11:41 +00:00
): CompletionList {
if (!isCssContext(state, document, position)) {
2020-04-12 17:11:41 +00:00
return null
}
let text = document.getText({
2020-04-12 17:11:41 +00:00
start: { line: position.line, character: 0 },
end: position,
})
const match = text.match(/^\s*@screen\s+(?<partial>[^\s]*)$/i)
if (match === null) return null
const screens = dlv(state.config, ['screens'], dlv(state.config, ['theme', 'screens'], {}))
2020-04-12 17:11:41 +00:00
if (!isObject(screens)) return null
return {
isIncomplete: false,
2020-04-27 22:18:28 +00:00
items: Object.keys(screens).map((screen, index) => ({
2020-04-12 17:11:41 +00:00
label: screen,
kind: 21,
2020-04-27 21:48:30 +00:00
data: 'screen',
2020-04-27 22:18:28 +00:00
sortText: naturalExpand(index),
2020-04-12 17:11:41 +00:00
textEdit: {
newText: screen,
range: {
start: {
line: position.line,
character: position.character - match.groups.partial.length,
},
end: position,
},
},
})),
}
}
2020-04-12 16:55:32 +00:00
function provideCssDirectiveCompletions(
state: State,
document: TextDocument,
position: Position
2020-04-12 16:55:32 +00:00
): CompletionList {
if (!isCssContext(state, document, position)) {
2020-04-12 16:55:32 +00:00
return null
}
let text = document.getText({
2020-04-12 16:55:32 +00:00
start: { line: position.line, character: 0 },
end: position,
})
const match = text.match(/^\s*@(?<partial>[a-z]*)$/i)
if (match === null) return null
const items: CompletionItem[] = [
{
label: '@tailwind',
documentation: {
kind: 'markdown' as typeof MarkupKind.Markdown,
2020-04-28 20:42:43 +00:00
value: `Use the \`@tailwind\` directive to insert Tailwinds \`base\`, \`components\`, \`utilities\` and \`screens\` styles into your CSS.\n\n[Tailwind CSS Documentation](${docsUrl(
state.version,
'functions-and-directives/#tailwind'
)})`,
2020-04-12 16:55:32 +00:00
},
},
{
label: '@variants',
documentation: {
kind: 'markdown' as typeof MarkupKind.Markdown,
2020-04-28 20:42:43 +00:00
value: `You can generate \`responsive\`, \`hover\`, \`focus\`, \`active\`, and \`group-hover\` versions of your own utilities by wrapping their definitions in the \`@variants\` directive.\n\n[Tailwind CSS Documentation](${docsUrl(
state.version,
'functions-and-directives/#variants'
)})`,
2020-04-12 16:55:32 +00:00
},
},
{
label: '@responsive',
documentation: {
kind: 'markdown' as typeof MarkupKind.Markdown,
2020-04-28 20:42:43 +00:00
value: `You can generate responsive variants of your own classes by wrapping their definitions in the \`@responsive\` directive.\n\n[Tailwind CSS Documentation](${docsUrl(
state.version,
'functions-and-directives/#responsive'
)})`,
2020-04-12 16:55:32 +00:00
},
},
{
label: '@screen',
documentation: {
kind: 'markdown' as typeof MarkupKind.Markdown,
2020-04-28 20:42:43 +00:00
value: `The \`@screen\` directive allows you to create media queries that reference your breakpoints by name instead of duplicating their values in your own CSS.\n\n[Tailwind CSS Documentation](${docsUrl(
state.version,
'functions-and-directives/#screen'
)})`,
2020-04-12 16:55:32 +00:00
},
},
{
label: '@apply',
documentation: {
kind: 'markdown' as typeof MarkupKind.Markdown,
2020-04-28 20:42:43 +00:00
value: `Use \`@apply\` to inline any existing utility classes into your own custom CSS.\n\n[Tailwind CSS Documentation](${docsUrl(
state.version,
'functions-and-directives/#apply'
)})`,
2020-04-12 16:55:32 +00:00
},
},
2020-11-27 17:13:46 +00:00
...(semver.gte(state.version, '1.8.0')
? [
{
label: '@layer',
documentation: {
kind: 'markdown' as typeof MarkupKind.Markdown,
value: `Use the \`@layer\` directive to tell Tailwind which "bucket" a set of custom styles belong to. Valid layers are \`base\`, \`components\`, and \`utilities\`.\n\n[Tailwind CSS Documentation](${docsUrl(
state.version,
'functions-and-directives/#layer'
)})`,
},
},
]
: []),
2020-04-12 16:55:32 +00:00
]
return {
isIncomplete: false,
items: items.map((item) => ({
...item,
kind: 14,
2020-04-12 16:55:32 +00:00
data: 'directive',
textEdit: {
newText: item.label,
range: {
start: {
line: position.line,
character: position.character - match.groups.partial.length - 1,
},
end: position,
},
},
})),
}
}
2020-04-16 21:39:16 +00:00
async function provideEmmetCompletions(
state: State,
document: TextDocument,
position: Position
2020-04-16 21:39:16 +00:00
): Promise<CompletionList> {
let settings = await state.editor.getConfiguration(document.uri)
2021-05-04 11:40:50 +00:00
if (settings.tailwindCSS.emmetCompletions !== true) return null
2020-12-10 17:22:06 +00:00
const isHtml = isHtmlContext(state, document, position)
const isJs = !isHtml && isJsContext(state, document, position)
const syntax = isHtml ? 'html' : isJs ? 'jsx' : null
2020-04-16 21:39:16 +00:00
if (syntax === null) {
return null
}
const extractAbbreviationResults = emmetHelper.extractAbbreviation(document, position, true)
2020-04-16 21:39:16 +00:00
if (
!extractAbbreviationResults ||
!emmetHelper.isAbbreviationValid(syntax, extractAbbreviationResults.abbreviation)
2020-04-16 21:39:16 +00:00
) {
return null
}
if (
!isValidLocationForEmmetAbbreviation(document, extractAbbreviationResults.abbreviationRange)
2020-04-16 21:39:16 +00:00
) {
return null
}
2020-12-10 17:22:06 +00:00
if (isJs) {
const abbreviation: string = extractAbbreviationResults.abbreviation
if (abbreviation.startsWith('this.')) {
return null
}
const symbols = await state.editor.getDocumentSymbols(document.uri)
2020-12-10 17:22:06 +00:00
if (
symbols &&
symbols.find(
(symbol) =>
abbreviation === symbol.name ||
(abbreviation.startsWith(symbol.name + '.') && !/>|\*|\+/.test(abbreviation))
2020-12-10 17:22:06 +00:00
)
) {
return null
}
}
const emmetItems = emmetHelper.doComplete(document, position, syntax, {})
2020-04-16 21:39:16 +00:00
if (!emmetItems || !emmetItems.items || emmetItems.items.length !== 1) {
return null
}
// https://github.com/microsoft/vscode/issues/86941
if (emmetItems.items[0].label === 'widows: ;') {
return null
}
const parts = emmetItems.items[0].label.split('.')
if (parts.length < 2) return null
return completionsFromClassList(state, parts[parts.length - 1], {
start: {
line: position.line,
character: position.character - parts[parts.length - 1].length,
},
end: position,
})
}
2021-05-18 11:22:18 +00:00
export async function doComplete(
state: State,
document: TextDocument,
position: Position,
context?: CompletionContext
) {
2020-04-11 21:20:45 +00:00
if (state === null) return { items: [], isIncomplete: false }
2020-04-16 21:39:16 +00:00
const result =
2021-05-18 11:22:18 +00:00
provideClassNameCompletions(state, document, position, context) ||
provideCssHelperCompletions(state, document, position) ||
provideCssDirectiveCompletions(state, document, position) ||
provideScreenDirectiveCompletions(state, document, position) ||
provideVariantsDirectiveCompletions(state, document, position) ||
provideTailwindDirectiveCompletions(state, document, position) ||
2020-11-27 17:13:46 +00:00
provideLayerDirectiveCompletions(state, document, position) ||
(await provideCustomClassNameCompletions(state, document, position))
2020-04-16 21:39:16 +00:00
if (result) return result
return provideEmmetCompletions(state, document, position)
2020-04-11 21:20:45 +00:00
}
export async function resolveCompletionItem(
2020-04-11 21:20:45 +00:00
state: State,
item: CompletionItem
): Promise<CompletionItem> {
if (['helper', 'directive', 'variant', 'layer', '@tailwind'].includes(item.data)) {
2020-04-27 21:48:30 +00:00
return item
}
if (item.data === 'screen') {
let screens = dlv(state.config, ['theme', 'screens'], dlv(state.config, ['screens'], {}))
2020-04-27 21:48:30 +00:00
if (!isObject(screens)) screens = {}
item.detail = stringifyScreen(screens[item.label] as Screen)
2020-04-11 21:20:45 +00:00
return item
}
2021-05-18 11:22:18 +00:00
if (!Array.isArray(item.data)) {
return item
}
if (state.jit) {
if (item.kind === 9) return item
2021-05-18 11:22:18 +00:00
if (item.detail && item.documentation) return item
let { root, rules } = jit.generateRules(state, [item.data.join(state.separator)])
if (rules.length === 0) return item
if (!item.detail) {
if (rules.length === 1) {
item.detail = await jit.stringifyDecls(state, rules[0])
} else {
item.detail = `${rules.length} rules`
}
}
if (!item.documentation) {
item.documentation = {
kind: 'markdown' as typeof MarkupKind.Markdown,
value: ['```css', await jit.stringifyRoot(state, root), '```'].join('\n'),
}
}
return item
}
2020-11-19 17:34:59 +00:00
const className = dlv(state.classNames.classNames, [...item.data, '__info'])
if (item.kind === 9) {
item.detail = state.classNames.context[item.data[item.data.length - 1]].join(', ')
2020-04-11 21:20:45 +00:00
} else {
item.detail = await getCssDetail(state, className)
2020-04-11 21:20:45 +00:00
if (!item.documentation) {
const settings = await state.editor.getConfiguration()
const css = stringifyCss(item.data.join(':'), className, {
2021-05-04 11:40:50 +00:00
tabSize: dlv(settings, 'editor.tabSize', 2),
showPixelEquivalents: dlv(settings, 'tailwindCSS.showPixelEquivalents', true),
rootFontSize: dlv(settings, 'tailwindCSS.rootFontSize', 16),
})
if (css) {
item.documentation = {
kind: 'markdown' as typeof MarkupKind.Markdown,
value: ['```css', css, '```'].join('\n'),
}
2020-04-11 21:20:45 +00:00
}
}
}
return item
}
function isContextItem(state: State, keys: string[]): boolean {
2020-11-19 17:34:59 +00:00
const item = dlv(state.classNames.classNames, [keys])
if (!isObject(item)) {
return false
}
if (!state.classNames.context[keys[keys.length - 1]]) {
return false
}
if (Object.keys(item).filter((x) => x !== '__info').length > 0) {
return true
}
return isObject(item.__info) && !item.__info.__rule
2020-04-11 21:20:45 +00:00
}
function stringifyDecls(
obj: any,
{
showPixelEquivalents = false,
rootFontSize = 16,
}: Partial<{ showPixelEquivalents: boolean; rootFontSize: number }> = {}
): string {
let props = Object.keys(obj)
let nonCustomProps = props.filter((prop) => !prop.startsWith('--'))
if (props.length !== nonCustomProps.length && nonCustomProps.length !== 0) {
props = nonCustomProps
}
return props
.map((prop) =>
ensureArray(obj[prop])
.map((value) => {
const px = showPixelEquivalents ? remToPx(value, rootFontSize) : undefined
return `${prop}: ${value}${px ? `/* ${px} */` : ''};`
})
.join(' ')
)
2020-04-11 21:20:45 +00:00
.join(' ')
}
async function getCssDetail(state: State, className: any): Promise<string> {
2020-04-11 21:20:45 +00:00
if (Array.isArray(className)) {
return `${className.length} rules`
}
if (className.__rule === true) {
const settings = await state.editor.getConfiguration()
return stringifyDecls(removeMeta(className), {
2021-05-04 11:40:50 +00:00
showPixelEquivalents: dlv(settings, 'tailwindCSS.showPixelEquivalents', true),
rootFontSize: dlv(settings, 'tailwindCSS.rootFontSize', 16),
})
2020-04-11 21:20:45 +00:00
}
return null
2020-04-11 21:20:45 +00:00
}