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,8 @@ 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'
4447
4548let isUtil = ( className ) =>
4649 Array . isArray ( className . __info )
@@ -1097,6 +1100,219 @@ function provideCssHelperCompletions(
10971100 )
10981101}
10991102
1103+ function getCsstUtilityNameAtPosition (
1104+ state : State ,
1105+ document : TextDocument ,
1106+ position : Position ,
1107+ ) : { root : string ; kind : 'static' | 'functional' } | null {
1108+ if ( ! isCssContext ( state , document , position ) ) return null
1109+ if ( ! isInsideAtRule ( 'utility' , document , position ) ) return null
1110+
1111+ let text = document . getText ( {
1112+ start : { line : 0 , character : 0 } ,
1113+ end : position ,
1114+ } )
1115+
1116+ // Make sure we're in a functional utility block
1117+ let block = text . lastIndexOf ( `@utility` )
1118+ if ( block === - 1 ) return null
1119+
1120+ let curly = text . indexOf ( '{' , block )
1121+ if ( curly === - 1 ) return null
1122+
1123+ let root = text . slice ( block + 8 , curly ) . trim ( )
1124+
1125+ if ( root . length === 0 ) return null
1126+
1127+ if ( root . endsWith ( '-*' ) ) {
1128+ root = root . slice ( 0 , - 2 )
1129+
1130+ if ( root . length === 0 ) return null
1131+
1132+ return { root, kind : 'functional' }
1133+ }
1134+
1135+ return { root : root , kind : 'static' }
1136+ }
1137+
1138+ function provideUtilityFunctionCompletions (
1139+ state : State ,
1140+ document : TextDocument ,
1141+ position : Position ,
1142+ ) : CompletionList {
1143+ let utilityName = getCsstUtilityNameAtPosition ( state , document , position )
1144+ if ( ! utilityName ) return null
1145+
1146+ let text = document . getText ( {
1147+ start : { line : position . line , character : 0 } ,
1148+ end : position ,
1149+ } )
1150+
1151+ // Make sure we're in "value position"
1152+ // e.g. --foo: <cursor>
1153+ let pattern = / ^ [ ^ : ] + : [ ^ ; ] * $ /
1154+ if ( ! pattern . test ( text ) ) return null
1155+
1156+ return withDefaults (
1157+ {
1158+ isIncomplete : false ,
1159+ items : [
1160+ {
1161+ label : '--value()' ,
1162+ textEditText : '--value($1)' ,
1163+ sortText : '-00000' ,
1164+ insertTextFormat : InsertTextFormat . Snippet ,
1165+ kind : CompletionItemKind . Function ,
1166+ documentation : {
1167+ kind : 'markdown' as typeof MarkupKind . Markdown ,
1168+ value : 'Reference a value based on the name of the utility. e.g. the `md` in `text-md`' ,
1169+ } ,
1170+ command : { command : 'editor.action.triggerSuggest' , title : '' } ,
1171+ } ,
1172+ {
1173+ label : '--modifier()' ,
1174+ textEditText : '--modifier($1)' ,
1175+ sortText : '-00001' ,
1176+ insertTextFormat : InsertTextFormat . Snippet ,
1177+ kind : CompletionItemKind . Function ,
1178+ documentation : {
1179+ kind : 'markdown' as typeof MarkupKind . Markdown ,
1180+ value : "Reference a value based on the utility's modifier. e.g. the `6` in `text-md/6`" ,
1181+ } ,
1182+ } ,
1183+ ] ,
1184+ } ,
1185+ {
1186+ data : {
1187+ ...( state . completionItemData ?? { } ) ,
1188+ } ,
1189+ range : {
1190+ start : position ,
1191+ end : position ,
1192+ } ,
1193+ } ,
1194+ state . editor . capabilities . itemDefaults ,
1195+ )
1196+ }
1197+
1198+ function provideUtilityFunctionArgumentCompletions (
1199+ state : State ,
1200+ document : TextDocument ,
1201+ position : Position ,
1202+ ) : CompletionList {
1203+ let utilityName = getCsstUtilityNameAtPosition ( state , document , position )
1204+ if ( ! utilityName ) return null
1205+
1206+ let text = document . getText ( {
1207+ start : { line : position . line , character : 0 } ,
1208+ end : position ,
1209+ } )
1210+
1211+ // Look to see if we're inside --value() or --modifier()
1212+ let fn = null
1213+ let fnStart = 0
1214+ let valueIdx = text . lastIndexOf ( '--value(' )
1215+ let modifierIdx = text . lastIndexOf ( '--modifier(' )
1216+ let fnIdx = Math . max ( valueIdx , modifierIdx )
1217+ if ( fnIdx === - 1 ) return null
1218+
1219+ if ( fnIdx === valueIdx ) {
1220+ fn = '--value'
1221+ } else if ( fnIdx === modifierIdx ) {
1222+ fn = '--modifier'
1223+ }
1224+
1225+ fnStart = fnIdx + fn . length + 1
1226+
1227+ // Make sure we're actaully inside the function
1228+ if ( parenLevel ( text . slice ( fnIdx ) ) === 0 ) return null
1229+
1230+ let items : CompletionItem [ ] = [
1231+ {
1232+ label : 'integer' ,
1233+ insertText : `integer` ,
1234+ kind : CompletionItemKind . Constant ,
1235+ documentation : {
1236+ kind : 'markdown' as typeof MarkupKind . Markdown ,
1237+ value : 'Support integer values, e.g. `placeholder-6`' ,
1238+ } ,
1239+ } ,
1240+ {
1241+ label : 'number' ,
1242+ insertText : `number` ,
1243+ kind : CompletionItemKind . Constant ,
1244+ documentation : {
1245+ kind : 'markdown' as typeof MarkupKind . Markdown ,
1246+ value :
1247+ 'Support numeric values in increments of 0.25, e.g. `placeholder-6` and `placeholder-7.25`' ,
1248+ } ,
1249+ } ,
1250+ {
1251+ label : 'percentage' ,
1252+ insertText : `percentage` ,
1253+ kind : CompletionItemKind . Constant ,
1254+ documentation : {
1255+ kind : 'markdown' as typeof MarkupKind . Markdown ,
1256+ value : 'Support integer percentage values, e.g. `placeholder-50%` and `placeholder-21%`' ,
1257+ } ,
1258+ } ,
1259+ ]
1260+
1261+ if ( fn === '--value' ) {
1262+ items . push ( {
1263+ label : 'ratio' ,
1264+ insertText : `ratio` ,
1265+ kind : CompletionItemKind . Constant ,
1266+ documentation : {
1267+ kind : 'markdown' as typeof MarkupKind . Markdown ,
1268+ value : 'Support fractions, e.g. `placeholder-1/5` and `placeholder-16/9`' ,
1269+ } ,
1270+ } )
1271+ }
1272+
1273+ let parts = segment ( text . slice ( fnStart ) , ',' ) . map ( ( s ) => s . trim ( ) )
1274+
1275+ // Only suggest at the start of the argument
1276+ if ( parts . at ( - 1 ) !== '' ) return null
1277+
1278+ // Remove items that are already used
1279+ items = items . filter ( ( item ) => ! parts . includes ( item . label ) )
1280+
1281+ for ( let [ idx , item ] of items . entries ( ) ) {
1282+ item . sortText = naturalExpand ( idx , items . length )
1283+
1284+ if ( typeof item . documentation === 'string' ) {
1285+ item . documentation = item . documentation . replace ( / p l a c e h o l d e r - / g, `${ utilityName . root } -` )
1286+ } else {
1287+ item . documentation . value = item . documentation . value . replace (
1288+ / p l a c e h o l d e r - / g,
1289+ `${ utilityName . root } -` ,
1290+ )
1291+ }
1292+
1293+ // TODO: Add a `, ` prefix to additional arguments automatically
1294+ // Doing so requires using `textEditText` + bookkeeping to make sure the
1295+ // output isn't mangled when the user has typed part of the argument
1296+ }
1297+
1298+ return withDefaults (
1299+ {
1300+ isIncomplete : true ,
1301+ items,
1302+ } ,
1303+ {
1304+ data : {
1305+ ...( state . completionItemData ?? { } ) ,
1306+ } ,
1307+ range : {
1308+ start : position ,
1309+ end : position ,
1310+ } ,
1311+ } ,
1312+ state . editor . capabilities . itemDefaults ,
1313+ )
1314+ }
1315+
11001316function provideTailwindDirectiveCompletions (
11011317 state : State ,
11021318 document : TextDocument ,
@@ -1871,6 +2087,8 @@ export async function doComplete(
18712087 const result =
18722088 ( await provideClassNameCompletions ( state , document , position , context ) ) ||
18732089 ( await provideThemeDirectiveCompletions ( state , document , position ) ) ||
2090+ provideUtilityFunctionArgumentCompletions ( state , document , position ) ||
2091+ provideUtilityFunctionCompletions ( state , document , position ) ||
18742092 provideCssHelperCompletions ( state , document , position ) ||
18752093 provideCssDirectiveCompletions ( state , document , position ) ||
18762094 provideScreenDirectiveCompletions ( state , document , position ) ||
0 commit comments