2020-10-08 15:20:54 +00:00
import { State } from './util/state'
import type {
2020-04-11 21:20:45 +00:00
CompletionItem ,
CompletionItemKind ,
Range ,
MarkupKind ,
CompletionList ,
2020-10-08 15:20:54 +00:00
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' )
2020-10-08 15:20:54 +00:00
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'
2020-10-08 15:20:54 +00:00
import { isValidLocationForEmmetAbbreviation } from './util/isValidLocationForEmmetAbbreviation'
import { isJsContext } from './util/js'
import { naturalExpand } from './util/naturalExpand'
2020-04-27 23:06:47 +00:00
import semver from 'semver'
2020-10-08 15:20:54 +00:00
import { docsUrl } from './util/docsUrl'
import { ensureArray } from './util/array'
2021-05-03 17:00:04 +00:00
import { getClassAttributeLexer , getComputedClassAttributeLexer } from './util/lexers'
2020-10-08 15:20:54 +00:00
import { validateApply } from './util/validateApply'
import { flagEnabled } from './util/flagEnabled'
2020-12-01 19:05:58 +00:00
import { remToPx } from './util/remToPx'
2020-12-07 15:39:44 +00:00
import { createMultiRegexp } from './util/createMultiRegexp'
2021-05-03 17:00:04 +00:00
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 ,
2021-05-03 17:00:04 +00:00
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
2020-04-13 00:44:43 +00:00
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 ,
} ,
}
2021-05-03 17:00:04 +00:00
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 ,
} ,
} ,
} ,
}
} ) ,
}
}
}
2021-05-03 17:00:04 +00:00
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 )
2021-05-03 17:00:04 +00:00
if ( typeof subset !== 'undefined' && typeof dlv ( subset , [ '__info' , '__rule' ] ) === 'undefined' ) {
2020-04-11 21:20:45 +00:00
isSubset = true
2020-04-13 00:44:43 +00:00
subsetKey = keys
2020-04-11 21:20:45 +00:00
replacementRange = {
. . . replacementRange ,
start : {
. . . replacementRange . start ,
2021-05-03 17:00:04 +00:00
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 ) = >
2021-05-03 17:00:04 +00:00
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
2021-05-03 17:00:04 +00:00
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 ,
2020-10-08 15:20:54 +00:00
document : TextDocument ,
2021-05-18 11:22:18 +00:00
position : Position ,
context? : CompletionContext
2020-04-11 21:20:45 +00:00
) : CompletionList {
2020-10-08 15:20:54 +00:00
let str = document . getText ( {
2020-04-11 21:20:45 +00:00
start : { line : Math.max ( position . line - 10 , 0 ) , character : 0 } ,
end : position ,
} )
2021-05-03 17:00:04 +00:00
const match = findLast ( /(?:\s|:|\()(?:class(?:Name)?|\[ngClass\])=['"`{]/gi , str )
2020-04-11 21:20:45 +00:00
if ( match === null ) {
return null
}
2020-05-16 18:10:17 +00:00
const lexer =
2020-11-30 18:15:31 +00:00
match [ 0 ] [ 0 ] === ':' || match [ 0 ] . trim ( ) . startsWith ( '[ngClass]' )
2020-05-16 18:10:17 +00:00
? 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
}
}
2021-05-03 17:00:04 +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
} ,
2021-05-03 17:00:04 +00:00
undefined ,
2021-05-18 11:22:18 +00:00
document ,
context
2021-05-03 17:00:04 +00:00
)
2020-04-11 21:20:45 +00:00
}
2020-05-16 18:10:17 +00:00
} catch ( _ ) { }
2020-04-11 21:20:45 +00:00
2020-05-16 18:10:17 +00:00
return null
2020-04-11 21:20:45 +00:00
}
2020-11-27 16:56:19 +00:00
async function provideCustomClassNameCompletions (
state : State ,
document : TextDocument ,
position : Position
) : Promise < CompletionList > {
2021-05-03 17:00:04 +00:00
const settings = await state . editor . getConfiguration ( document . uri )
2021-05-04 11:40:50 +00:00
const regexes = dlv ( settings , 'tailwindCSS.experimental.classRegex' , [ ] )
2020-11-27 16:56:19 +00:00
if ( regexes . length === 0 ) return null
2020-12-01 16:35:46 +00:00
const positionOffset = document . offsetAt ( position )
const searchRange : Range = {
start : document.positionAt ( Math . max ( 0 , positionOffset - 500 ) ) ,
end : document.positionAt ( positionOffset + 500 ) ,
2020-11-27 16:56:19 +00:00
}
let str = document . getText ( searchRange )
for ( let i = 0 ; i < regexes . length ; i ++ ) {
try {
2021-05-03 17:00:04 +00:00
let [ containerRegex , classRegex ] = Array . isArray ( regexes [ i ] ) ? regexes [ i ] : [ regexes [ i ] ]
2020-12-01 16:35:46 +00:00
2020-12-02 13:52:09 +00:00
containerRegex = createMultiRegexp ( containerRegex )
2020-12-01 16:35:46 +00:00
let containerMatch
2020-11-27 16:56:19 +00:00
2020-12-02 13:52:09 +00:00
while ( ( containerMatch = containerRegex . exec ( str ) ) !== null ) {
2020-12-01 16:35:46 +00:00
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 ) {
2020-12-02 13:52:09 +00:00
classRegex = createMultiRegexp ( classRegex )
2020-12-01 16:35:46 +00:00
let classMatch
2021-05-03 17:00:04 +00:00
while ( ( classMatch = classRegex . exec ( containerMatch . match ) ) !== null ) {
2020-12-01 16:35:46 +00:00
const classMatchStart = matchStart + classMatch . start
const classMatchEnd = matchStart + classMatch . end
if ( cursor >= classMatchStart && cursor <= classMatchEnd ) {
classList = classMatch . match . substr ( 0 , cursor - classMatchStart )
}
2020-11-27 16:56:19 +00:00
}
2020-12-01 16:35:46 +00:00
if ( typeof classList === 'undefined' ) {
throw Error ( )
}
} else {
classList = containerMatch . match . substr ( 0 , cursor - matchStart )
2020-11-27 16:56:19 +00:00
}
2020-12-01 16:35:46 +00:00
return completionsFromClassList ( state , classList , {
start : {
line : position.line ,
character : position.character - classList . length ,
} ,
end : position ,
} )
}
2020-11-27 16:56:19 +00:00
}
} catch ( _ ) { }
}
return null
}
2020-04-11 21:20:45 +00:00
function provideAtApplyCompletions (
state : State ,
2020-10-08 15:20:54 +00:00
document : TextDocument ,
position : Position
2020-04-11 21:20:45 +00:00
) : CompletionList {
2020-10-08 15:20:54 +00:00
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 ) = > {
2020-10-08 15:20:54 +00:00
if ( item . kind === 9 ) {
2020-11-19 17:34:59 +00:00
return (
2021-05-03 17:00:04 +00:00
semver . gte ( state . version , '2.0.0-alpha.1' ) || flagEnabled ( state , 'applyComplexClasses' )
2020-11-19 17:34:59 +00:00
)
2020-08-21 14:30:29 +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 ,
2020-10-08 15:20:54 +00:00
document : TextDocument ,
2021-05-18 11:22:18 +00:00
position : Position ,
context? : CompletionContext
2020-04-11 21:20:45 +00:00
) : CompletionList {
2020-10-08 15:20:54 +00:00
if ( isCssContext ( state , document , position ) ) {
return provideAtApplyCompletions ( state , document , position )
2020-04-11 21:20:45 +00:00
}
2021-05-20 15:50:10 +00:00
if ( isHtmlContext ( state , document , position ) || isJsContext ( state , document , position ) ) {
return provideClassAttributeCompletions ( state , document , position , context )
}
2020-04-11 21:20:45 +00:00
return null
}
function provideCssHelperCompletions (
state : State ,
2020-10-08 15:20:54 +00:00
document : TextDocument ,
position : Position
2020-04-11 21:20:45 +00:00
) : CompletionList {
2020-10-08 15:20:54 +00:00
if ( ! isCssContext ( state , document , position ) ) {
2020-04-11 21:20:45 +00:00
return null
}
2020-10-08 15:20:54 +00:00
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
}
2021-05-03 17:00:04 +00:00
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
2021-05-03 17:00:04 +00:00
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 ) = > {
2020-05-03 19:52:14 +00:00
let color = getColorFromValue ( obj [ item ] )
2021-05-03 17:00:04 +00:00
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 ( '[' ) ) )
2020-04-27 23:30:32 +00:00
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 ) ,
2020-10-08 15:20:54 +00:00
kind : color ? 16 : isObject ( obj [ item ] ) ? 9 : 10 ,
2021-05-03 17:00:04 +00:00
// 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 : {
2021-05-03 17:00:04 +00:00
newText : ` ${ replaceDot ? '[' : '' } ${ item } ${ insertClosingBrace ? ']' : '' } ` ,
2020-04-11 21:20:45 +00:00
range : {
start : {
line : position.line ,
character :
2021-05-03 17:00:04 +00:00
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 ,
2020-10-08 15:20:54 +00:00
document : TextDocument ,
position : Position
2020-04-27 22:52:31 +00:00
) : CompletionList {
2020-10-08 15:20:54 +00:00
if ( ! isCssContext ( state , document , position ) ) {
2020-04-27 22:52:31 +00:00
return null
}
2020-10-08 15:20:54 +00:00
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 : [
2020-04-27 23:06:47 +00:00
semver . gte ( state . version , '1.0.0-beta.1' )
? {
label : 'base' ,
documentation : {
2020-10-08 15:20:54 +00:00
kind : 'markdown' as typeof MarkupKind . Markdown ,
2020-04-28 20:42:43 +00:00
value : ` This injects Tailwind’ s base styles and any base styles registered by plugins. \ n \ n[Tailwind CSS Documentation]( ${ docsUrl (
state . version ,
'functions-and-directives/#tailwind'
) } ) ` ,
2020-04-27 23:06:47 +00:00
} ,
}
: {
label : 'preflight' ,
documentation : {
2020-10-08 15:20:54 +00:00
kind : 'markdown' as typeof MarkupKind . Markdown ,
2020-04-28 20:42:43 +00:00
value : ` This injects Tailwind’ s 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 23:06:47 +00:00
} ,
} ,
2020-04-27 22:52:31 +00:00
{
label : 'components' ,
documentation : {
2020-10-08 15:20:54 +00:00
kind : 'markdown' as typeof MarkupKind . Markdown ,
2020-04-28 20:42:43 +00:00
value : ` This injects Tailwind’ s 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 : {
2020-10-08 15:20:54 +00:00
kind : 'markdown' as typeof MarkupKind . Markdown ,
2020-04-28 20:42:43 +00:00
value : ` This injects Tailwind’ s 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 : {
2020-10-08 15:20:54 +00:00
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 ,
2020-10-08 15:20:54 +00:00
kind : 21 ,
2020-04-27 23:07:07 +00:00
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 ,
2020-10-08 15:20:54 +00:00
document : TextDocument ,
position : Position
2020-04-12 22:48:57 +00:00
) : CompletionList {
2020-10-08 15:20:54 +00:00
if ( ! isCssContext ( state , document , position ) ) {
2020-04-12 22:48:57 +00:00
return null
}
2020-10-08 15:20:54 +00:00
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
2021-05-17 11:38:52 +00:00
let possibleVariants = Object . keys ( state . variants )
2020-04-12 22:48:57 +00:00
const existingVariants = parts . slice ( 0 , parts . length - 1 )
2021-05-17 11:38:52 +00:00
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 ,
2021-05-17 11:38:52 +00:00
items : possibleVariants
2020-04-12 22:48:57 +00:00
. filter ( ( v ) = > existingVariants . indexOf ( v ) === - 1 )
2021-05-03 17:00:04 +00:00
. map ( ( variant , index ) = > ( {
2020-04-12 22:48:57 +00:00
// TODO: detail
label : variant ,
2021-05-03 17:00:04 +00:00
detail : state.variants [ variant ] ,
2020-10-08 15:20:54 +00:00
kind : 21 ,
2020-04-12 22:48:57 +00:00
data : 'variant' ,
2021-05-03 17:00:04 +00:00
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 ,
2020-10-08 15:20:54 +00:00
document : TextDocument ,
position : Position
2020-04-12 17:11:41 +00:00
) : CompletionList {
2020-10-08 15:20:54 +00:00
if ( ! isCssContext ( state , document , position ) ) {
2020-04-12 17:11:41 +00:00
return null
}
2020-10-08 15:20:54 +00:00
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
2021-05-03 17:00:04 +00:00
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 ,
2020-10-08 15:20:54 +00:00
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 ,
2020-10-08 15:20:54 +00:00
document : TextDocument ,
position : Position
2020-04-12 16:55:32 +00:00
) : CompletionList {
2020-10-08 15:20:54 +00:00
if ( ! isCssContext ( state , document , position ) ) {
2020-04-12 16:55:32 +00:00
return null
}
2020-10-08 15:20:54 +00:00
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 : {
2020-10-08 15:20:54 +00:00
kind : 'markdown' as typeof MarkupKind . Markdown ,
2020-04-28 20:42:43 +00:00
value : ` Use the \` @tailwind \` directive to insert Tailwind’ s \` 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 : {
2020-10-08 15:20:54 +00:00
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 : {
2020-10-08 15:20:54 +00:00
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 : {
2020-10-08 15:20:54 +00:00
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 : {
2020-10-08 15:20:54 +00:00
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 ,
2020-10-08 15:20:54 +00:00
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 ,
2020-10-08 15:20:54 +00:00
document : TextDocument ,
position : Position
2020-04-16 21:39:16 +00:00
) : Promise < CompletionList > {
2021-05-03 17:00:04 +00:00
let settings = await state . editor . getConfiguration ( document . uri )
2021-05-04 11:40:50 +00:00
if ( settings . tailwindCSS . emmetCompletions !== true ) return null
2020-06-15 13:31:04 +00:00
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
}
2021-05-03 17:00:04 +00:00
const extractAbbreviationResults = emmetHelper . extractAbbreviation ( document , position , true )
2020-04-16 21:39:16 +00:00
if (
! extractAbbreviationResults ||
2021-05-03 17:00:04 +00:00
! emmetHelper . isAbbreviationValid ( syntax , extractAbbreviationResults . abbreviation )
2020-04-16 21:39:16 +00:00
) {
return null
}
if (
2021-05-03 17:00:04 +00:00
! 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
}
2021-05-03 17:00:04 +00:00
const symbols = await state . editor . getDocumentSymbols ( document . uri )
2020-12-10 17:22:06 +00:00
if (
symbols &&
symbols . find (
( symbol ) = >
abbreviation === symbol . name ||
2021-05-03 17:00:04 +00:00
( abbreviation . startsWith ( symbol . name + '.' ) && ! />|\*|\+/ . test ( abbreviation ) )
2020-12-10 17:22:06 +00:00
)
) {
return null
}
}
2020-10-08 15:20:54 +00:00
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 ) ||
2020-10-08 15:20:54 +00:00
provideCssHelperCompletions ( state , document , position ) ||
provideCssDirectiveCompletions ( state , document , position ) ||
provideScreenDirectiveCompletions ( state , document , position ) ||
provideVariantsDirectiveCompletions ( state , document , position ) ||
2020-11-27 16:56:19 +00:00
provideTailwindDirectiveCompletions ( state , document , position ) ||
2020-11-27 17:13:46 +00:00
provideLayerDirectiveCompletions ( state , document , position ) ||
2020-11-27 16:56:19 +00:00
( await provideCustomClassNameCompletions ( state , document , position ) )
2020-04-16 21:39:16 +00:00
if ( result ) return result
2020-10-08 15:20:54 +00:00
return provideEmmetCompletions ( state , document , position )
2020-04-11 21:20:45 +00:00
}
2020-11-26 20:07:39 +00:00
export async function resolveCompletionItem (
2020-04-11 21:20:45 +00:00
state : State ,
item : CompletionItem
2020-11-26 20:07:39 +00:00
) : Promise < CompletionItem > {
2021-05-03 17:00:04 +00:00
if ( [ 'helper' , 'directive' , 'variant' , 'layer' , '@tailwind' ] . includes ( item . data ) ) {
2020-04-27 21:48:30 +00:00
return item
}
if ( item . data === 'screen' ) {
2021-05-03 17:00:04 +00:00
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
}
2021-05-03 17:00:04 +00:00
if ( state . jit ) {
if ( item . kind === 9 ) return item
2021-05-18 11:22:18 +00:00
if ( item . detail && item . documentation ) return item
2021-05-03 17:00:04 +00:00
let { root , rules } = jit . generateRules ( state , [ item . data . join ( state . separator ) ] )
if ( rules . length === 0 ) return item
if ( ! item . detail ) {
if ( rules . length === 1 ) {
2021-05-20 12:24:16 +00:00
item . detail = await jit . stringifyDecls ( state , rules [ 0 ] )
2021-05-03 17:00:04 +00:00
} 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 ) {
2021-05-03 17:00:04 +00:00
item . detail = state . classNames . context [ item . data [ item . data . length - 1 ] ] . join ( ', ' )
2020-04-11 21:20:45 +00:00
} else {
2020-12-01 19:05:58 +00:00
item . detail = await getCssDetail ( state , className )
2020-04-11 21:20:45 +00:00
if ( ! item . documentation ) {
2021-05-03 17:00:04 +00:00
const settings = await state . editor . getConfiguration ( )
2020-12-01 19:05:58 +00:00
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 ) ,
2020-12-01 19:05:58 +00:00
} )
2020-04-13 00:44:43 +00:00
if ( css ) {
item . documentation = {
2020-10-08 15:20:54 +00:00
kind : 'markdown' as typeof MarkupKind . Markdown ,
2020-04-13 00:44:43 +00:00
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
}
2020-12-01 19:05:58 +00:00
function stringifyDecls (
obj : any ,
2021-02-05 14:49:06 +00:00
{
showPixelEquivalents = false ,
rootFontSize = 16 ,
} : Partial < { showPixelEquivalents : boolean ; rootFontSize : number } > = { }
2020-12-01 19:05:58 +00:00
) : string {
2020-05-02 12:18:30 +00:00
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 ] )
2020-12-01 19:05:58 +00:00
. map ( ( value ) = > {
2021-05-03 17:00:04 +00:00
const px = showPixelEquivalents ? remToPx ( value , rootFontSize ) : undefined
2021-02-07 20:58:41 +00:00
return ` ${ prop } : ${ value } ${ px ? ` /* ${ px } */ ` : '' } ; `
2020-12-01 19:05:58 +00:00
} )
2020-05-02 12:18:30 +00:00
. join ( ' ' )
)
2020-04-11 21:20:45 +00:00
. join ( ' ' )
}
2020-12-01 19:05:58 +00:00
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 `
}
2020-04-13 00:44:43 +00:00
if ( className . __rule === true ) {
2021-05-03 17:00:04 +00:00
const settings = await state . editor . getConfiguration ( )
2020-12-01 19:05:58 +00:00
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-12-01 19:05:58 +00:00
} )
2020-04-11 21:20:45 +00:00
}
2020-04-13 00:44:43 +00:00
return null
2020-04-11 21:20:45 +00:00
}