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 ,
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 { getDocumentSettings } from './util/getDocumentSettings'
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'
2020-05-16 18:10:17 +00:00
import {
getClassAttributeLexer ,
getComputedClassAttributeLexer ,
2020-10-08 15:20:54 +00:00
} from './util/lexers'
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'
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
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 ,
} ,
}
for ( let i = parts . length - 1 ; i > 0 ; i -- ) {
let keys = parts . slice ( 0 , i ) . filter ( Boolean )
subset = dlv ( state . classNames . classNames , keys )
2020-11-19 17:34:59 +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 ,
character :
replacementRange . start . character +
keys . join ( sep ) . length +
sep . length ,
} ,
}
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' ,
] )
)
. 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 ( )
}
}
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 ,
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 - 10 , 0 ) , character : 0 } ,
end : position ,
} )
2021-01-08 15:53:02 +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
}
}
2020-04-11 21:20:45 +00:00
return completionsFromClassList ( state , classList , {
start : {
line : position.line ,
character : position.character - classList . length ,
} ,
end : position ,
} )
}
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 > {
const settings = await getDocumentSettings ( state , document )
const regexes = dlv ( settings , 'experimental.classRegex' , [ ] )
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 {
2020-12-01 16:35:46 +00:00
let [ containerRegex , classRegex ] = Array . isArray ( regexes [ i ] )
? regexes [ i ]
: [ regexes [ i ] ]
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
while (
2020-12-02 13:52:09 +00:00
( 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 (
semver . gte ( state . version , '2.0.0-alpha.1' ) ||
flagEnabled ( state , 'applyComplexClasses' )
)
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 ,
position : Position
2020-04-11 21:20:45 +00:00
) : CompletionList {
2020-04-16 21:39:16 +00:00
if (
2020-10-08 15:20:54 +00:00
isHtmlContext ( state , document , position ) ||
isJsContext ( state , document , position )
2020-04-16 21:39:16 +00:00
) {
2020-10-08 15:20:54 +00:00
return provideClassAttributeCompletions ( state , document , position )
2020-04-11 21:20:45 +00:00
}
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
}
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
}
let base =
match . groups . helper === 'config'
? state . config
: dlv ( state . config , 'theme' , { } )
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
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 ] )
2020-04-11 21:20:45 +00:00
const replaceDot : boolean =
item . indexOf ( '.' ) !== - 1 && separator && separator . endsWith ( '.' )
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 ,
2020-04-27 23:30:32 +00:00
// VS Code bug causes '0' to not display in some cases
detail : detail === '0' ? '0 ' : detail ,
2020-04-11 21:20:45 +00:00
documentation : color ,
textEdit : {
newText : ` ${ replaceDot ? '[' : '' } ${ item } ${
insertClosingBrace ? ']' : ''
} ` ,
range : {
start : {
line : position.line ,
character :
position . character -
keys [ keys . length - 1 ] . length -
( replaceDot ? 1 : 0 ) -
offset ,
} ,
end : position ,
} ,
} ,
data : 'helper' ,
}
} ) ,
}
}
2020-04-27 23:06:47 +00:00
// TODO: vary docs links based on Tailwind version
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
const existingVariants = parts . slice ( 0 , parts . length - 1 )
return {
isIncomplete : false ,
2020-04-16 21:44:52 +00:00
items : state.variants
2020-04-12 22:48:57 +00:00
. filter ( ( v ) = > existingVariants . indexOf ( v ) === - 1 )
. map ( ( variant ) = > ( {
// TODO: detail
label : variant ,
2020-10-08 15:20:54 +00:00
kind : 21 ,
2020-04-12 22:48:57 +00:00
data : 'variant' ,
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
const screens = dlv (
state . config ,
[ 'screens' ] ,
dlv ( state . config , [ 'theme' , 'screens' ] , { } )
)
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 > {
2020-10-08 15:20:54 +00:00
let settings = await getDocumentSettings ( state , document )
2020-06-15 13:31:04 +00:00
if ( settings . 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 (
2020-10-08 15:20:54 +00:00
document ,
2020-04-16 21:39:16 +00:00
position ,
true
)
if (
! extractAbbreviationResults ||
! emmetHelper . isAbbreviationValid (
syntax ,
extractAbbreviationResults . abbreviation
)
) {
return null
}
if (
! isValidLocationForEmmetAbbreviation (
2020-10-08 15:20:54 +00:00
document ,
2020-04-16 21:39:16 +00:00
extractAbbreviationResults . abbreviationRange
)
) {
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 . emitter . emit ( 'getDocumentSymbols' , {
uri : document.uri ,
} )
if (
symbols &&
symbols . find (
( symbol ) = >
abbreviation === symbol . name ||
( abbreviation . startsWith ( symbol . name + '.' ) &&
! />|\*|\+/ . test ( abbreviation ) )
)
) {
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 ,
} )
}
2020-10-08 15:20:54 +00:00
export async function doComplete (
2020-04-11 21:20:45 +00:00
state : State ,
2020-10-08 15:20:54 +00:00
document : TextDocument ,
position : Position
) {
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 =
2020-10-08 15:20:54 +00:00
provideClassNameCompletions ( state , document , position ) ||
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 > {
2020-11-27 17:13:46 +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' ) {
let screens = dlv (
state . config ,
[ 'theme' , 'screens' ] ,
dlv ( state . config , [ 'screens' ] , { } )
)
if ( ! isObject ( screens ) ) screens = { }
item . detail = stringifyScreen ( screens [ item . label ] as Screen )
2020-04-11 21:20:45 +00:00
return item
}
2020-11-19 17:34:59 +00:00
const className = dlv ( state . classNames . classNames , [ . . . item . data , '__info' ] )
if ( item . kind === 9 ) {
2020-04-13 00:44:43 +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 ) {
2020-12-01 19:05:58 +00:00
const settings = await getDocumentSettings ( state )
const css = stringifyCss ( item . data . join ( ':' ) , className , {
tabSize : dlv ( settings , 'tabSize' ) ,
showPixelValues : dlv ( settings , 'experimental.showPixelValues' ) ,
} )
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 ,
{ showPixelValues = false } : Partial < { showPixelValues : boolean } > = { }
) : 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 ) = > {
const px = showPixelValues ? remToPx ( value ) : undefined
return ` ${ prop } : ${ value } ${ px ? ` /* ${ px } */ ` : '' } ; `
} )
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 ) {
2020-12-01 19:05:58 +00:00
const settings = await getDocumentSettings ( state )
return stringifyDecls ( removeMeta ( className ) , {
showPixelValues : dlv ( settings , 'experimental.showPixelValues' , false ) ,
} )
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
}