77 type CompletionList ,
88 type Position ,
99 type CompletionContext ,
10+ InsertTextFormat ,
1011} from 'vscode-languageserver'
1112import type { TextDocument } from 'vscode-languageserver-textdocument'
1213import dlv from 'dlv'
@@ -18,7 +19,7 @@ import { findLast, matchClassAttributes } from './util/find'
1819import { stringifyConfigValue , stringifyCss } from './util/stringify'
1920import { stringifyScreen , Screen } from './util/screens'
2021import isObject from './util/isObject'
21- import braceLevel from './util/braceLevel'
22+ import { braceLevel , parenLevel } from './util/braceLevel'
2223import * as emmetHelper from 'vscode-emmet-helper-bundled'
2324import { isValidLocationForEmmetAbbreviation } from './util/isValidLocationForEmmetAbbreviation'
2425import { isJsDoc , isJsxContext } from './util/js'
@@ -41,6 +42,9 @@ import { IS_SCRIPT_SOURCE, IS_TEMPLATE_SOURCE } from './metadata/extensions'
4142import * as postcss from 'postcss'
4243import { findFileDirective } from './completions/file-paths'
4344import type { ThemeEntry } from './util/v4'
45+ import { posix } from 'node:path/win32'
46+ import { segment } from './util/segment'
47+ import { resolveKnownThemeKeys , resolveKnownThemeNamespaces } from './util/v4/theme-keys'
4448
4549let isUtil = ( className ) =>
4650 Array . isArray ( className . __info )
@@ -1097,6 +1101,172 @@ function provideCssHelperCompletions(
10971101 )
10981102}
10991103
1104+ function getCsstUtilityNameAtPosition (
1105+ state : State ,
1106+ document : TextDocument ,
1107+ position : Position ,
1108+ ) : { root : string ; kind : 'static' | 'functional' } | null {
1109+ if ( ! isCssContext ( state , document , position ) ) return null
1110+ if ( ! isInsideAtRule ( 'utility' , document , position ) ) return null
1111+
1112+ let text = document . getText ( {
1113+ start : { line : 0 , character : 0 } ,
1114+ end : position ,
1115+ } )
1116+
1117+ // Make sure we're in a functional utility block
1118+ let block = text . lastIndexOf ( `@utility` )
1119+ if ( block === - 1 ) return null
1120+
1121+ let curly = text . indexOf ( '{' , block )
1122+ if ( curly === - 1 ) return null
1123+
1124+ let root = text . slice ( block + 8 , curly ) . trim ( )
1125+
1126+ if ( root . length === 0 ) return null
1127+
1128+ if ( root . endsWith ( '-*' ) ) {
1129+ root = root . slice ( 0 , - 2 )
1130+
1131+ if ( root . length === 0 ) return null
1132+
1133+ return { root, kind : 'functional' }
1134+ }
1135+
1136+ return { root : root , kind : 'static' }
1137+ }
1138+
1139+ function provideUtilityFunctionCompletions (
1140+ state : State ,
1141+ document : TextDocument ,
1142+ position : Position ,
1143+ ) : CompletionList {
1144+ let utilityName = getCsstUtilityNameAtPosition ( state , document , position )
1145+ if ( ! utilityName ) return null
1146+
1147+ let text = document . getText ( {
1148+ start : { line : position . line , character : 0 } ,
1149+ end : position ,
1150+ } )
1151+
1152+ // Make sure we're in "value position"
1153+ // e.g. --foo: <cursor>
1154+ let pattern = / ^ [ ^ : ] + : [ ^ ; ] * $ /
1155+ if ( ! pattern . test ( text ) ) return null
1156+
1157+ return withDefaults (
1158+ {
1159+ isIncomplete : false ,
1160+ items : [
1161+ {
1162+ label : '--value()' ,
1163+ textEditText : '--value($1)' ,
1164+ sortText : '-00000' ,
1165+ insertTextFormat : InsertTextFormat . Snippet ,
1166+ kind : CompletionItemKind . Function ,
1167+ documentation : {
1168+ kind : 'markdown' as typeof MarkupKind . Markdown ,
1169+ value : 'Reference a value based on the name of the utility. e.g. the `md` in `text-md`' ,
1170+ } ,
1171+ command : { command : 'editor.action.triggerSuggest' , title : '' } ,
1172+ } ,
1173+ {
1174+ label : '--modifier()' ,
1175+ textEditText : '--modifier($1)' ,
1176+ sortText : '-00001' ,
1177+ insertTextFormat : InsertTextFormat . Snippet ,
1178+ kind : CompletionItemKind . Function ,
1179+ documentation : {
1180+ kind : 'markdown' as typeof MarkupKind . Markdown ,
1181+ value : "Reference a value based on the utility's modifier. e.g. the `6` in `text-md/6`" ,
1182+ } ,
1183+ } ,
1184+ ] ,
1185+ } ,
1186+ {
1187+ data : {
1188+ ...( state . completionItemData ?? { } ) ,
1189+ } ,
1190+ range : {
1191+ start : position ,
1192+ end : position ,
1193+ } ,
1194+ } ,
1195+ state . editor . capabilities . itemDefaults ,
1196+ )
1197+ }
1198+
1199+ async function provideUtilityFunctionArgumentCompletions (
1200+ state : State ,
1201+ document : TextDocument ,
1202+ position : Position ,
1203+ ) : Promise < CompletionList | null > {
1204+ let utilityName = getCsstUtilityNameAtPosition ( state , document , position )
1205+ if ( ! utilityName ) return null
1206+
1207+ let text = document . getText ( {
1208+ start : { line : position . line , character : 0 } ,
1209+ end : position ,
1210+ } )
1211+
1212+ // Look to see if we're inside --value() or --modifier()
1213+ let fn = null
1214+ let fnStart = 0
1215+ let valueIdx = text . lastIndexOf ( '--value(' )
1216+ let modifierIdx = text . lastIndexOf ( '--modifier(' )
1217+ let fnIdx = Math . max ( valueIdx , modifierIdx )
1218+ if ( fnIdx === - 1 ) return null
1219+
1220+ if ( fnIdx === valueIdx ) {
1221+ fn = '--value'
1222+ } else if ( fnIdx === modifierIdx ) {
1223+ fn = '--modifier'
1224+ }
1225+
1226+ fnStart = fnIdx + fn . length + 1
1227+
1228+ // Make sure we're actaully inside the function
1229+ if ( parenLevel ( text . slice ( fnIdx ) ) === 0 ) return null
1230+
1231+ let args = Array . from ( await knownUtilityFunctionArguments ( state , fn ) )
1232+
1233+ let parts = segment ( text . slice ( fnStart ) , ',' ) . map ( ( s ) => s . trim ( ) )
1234+
1235+ // Only suggest at the start of the argument
1236+ if ( parts . at ( - 1 ) !== '' ) return null
1237+
1238+ // Remove items that are already used
1239+ args = args . filter ( ( arg ) => ! parts . includes ( arg . name ) )
1240+
1241+ let items : CompletionItem [ ] = args . map ( ( arg , idx ) => ( {
1242+ label : arg . name ,
1243+ insertText : arg . name ,
1244+ kind : CompletionItemKind . Constant ,
1245+ sortText : naturalExpand ( idx , args . length ) ,
1246+ documentation : {
1247+ kind : 'markdown' as typeof MarkupKind . Markdown ,
1248+ value : arg . description . replace ( / \{ u t i l i t y \} - / g, `${ utilityName . root } -` ) ,
1249+ } ,
1250+ } ) )
1251+
1252+ return withDefaults (
1253+ {
1254+ isIncomplete : true ,
1255+ items,
1256+ } ,
1257+ {
1258+ data : {
1259+ ...( state . completionItemData ?? { } ) ,
1260+ } ,
1261+ range : {
1262+ start : position ,
1263+ end : position ,
1264+ } ,
1265+ } ,
1266+ state . editor . capabilities . itemDefaults ,
1267+ )
1268+ }
1269+
11001270function provideTailwindDirectiveCompletions (
11011271 state : State ,
11021272 document : TextDocument ,
@@ -1871,6 +2041,8 @@ export async function doComplete(
18712041 const result =
18722042 ( await provideClassNameCompletions ( state , document , position , context ) ) ||
18732043 ( await provideThemeDirectiveCompletions ( state , document , position ) ) ||
2044+ provideUtilityFunctionArgumentCompletions ( state , document , position ) ||
2045+ provideUtilityFunctionCompletions ( state , document , position ) ||
18742046 provideCssHelperCompletions ( state , document , position ) ||
18752047 provideCssDirectiveCompletions ( state , document , position ) ||
18762048 provideScreenDirectiveCompletions ( state , document , position ) ||
@@ -2039,3 +2211,87 @@ async function getCssDetail(state: State, className: any): Promise<string> {
20392211 }
20402212 return null
20412213}
2214+
2215+ type UtilityFn = '--value' | '--modifier'
2216+
2217+ interface UtilityFnArg {
2218+ name : string
2219+ description : string
2220+ }
2221+
2222+ async function knownUtilityFunctionArguments ( state : State , fn : UtilityFn ) : Promise < UtilityFnArg [ ] > {
2223+ if ( ! state . designSystem ) return [ ]
2224+
2225+ let args : UtilityFnArg [ ] = [ ]
2226+
2227+ let namespaces = resolveKnownThemeNamespaces ( state . designSystem )
2228+
2229+ for ( let ns of namespaces ) {
2230+ args . push ( {
2231+ name : `${ ns } -*` ,
2232+ description : `Support theme values from \`${ ns } -*\`` ,
2233+ } )
2234+ }
2235+
2236+ args . push ( {
2237+ name : 'integer' ,
2238+ description : 'Support integer values, e.g. `{utility}-6`' ,
2239+ } )
2240+
2241+ args . push ( {
2242+ name : 'number' ,
2243+ description :
2244+ 'Support numeric values in increments of 0.25, e.g. `{utility}-6` and `{utility}-7.25`' ,
2245+ } )
2246+
2247+ args . push ( {
2248+ name : 'percentage' ,
2249+ description : 'Support integer percentage values, e.g. `{utility}-50%` and `{utility}-21%`' ,
2250+ } )
2251+
2252+ if ( fn === '--value' ) {
2253+ args . push ( {
2254+ name : 'ratio' ,
2255+ description : 'Support fractions, e.g. `{utility}-1/5` and `{utility}-16/9`' ,
2256+ } )
2257+ }
2258+
2259+ args . push ( {
2260+ name : '[integer]' ,
2261+ description : 'Support arbitrary integer values, e.g. `{utility}-[123]`' ,
2262+ } )
2263+
2264+ args . push ( {
2265+ name : '[number]' ,
2266+ description : 'Support arbitrary numeric values, e.g. `{utility}-[10]` and `{utility}-[10.234]`' ,
2267+ } )
2268+
2269+ args . push ( {
2270+ name : '[percentage]' ,
2271+ description :
2272+ 'Support arbitrary percentage values, e.g. `{utility}-[10%]` and `{utility}-[10.234%]`' ,
2273+ } )
2274+
2275+ args . push ( {
2276+ name : '[ratio]' ,
2277+ description : 'Support arbitrary fractions, e.g. `{utility}-[1/5]` and `{utility}-[16/9]`' ,
2278+ } )
2279+
2280+ args . push ( {
2281+ name : '[color]' ,
2282+ description :
2283+ 'Support arbitrary color values, e.g. `{utility}-[#639]` and `{utility}-[oklch(44.03% 0.1603 303.37)]`' ,
2284+ } )
2285+
2286+ args . push ( {
2287+ name : '[angle]' ,
2288+ description : 'Support arbitrary angle, e.g. `{utility}-[12deg]` and `{utility}-[0.21rad]`' ,
2289+ } )
2290+
2291+ args . push ( {
2292+ name : '[url]' ,
2293+ description : "Support arbitrary URL functions, e.g. `{utility}-['url(…)']`" ,
2294+ } )
2295+
2296+ return args
2297+ }
0 commit comments