7
7
type CompletionList ,
8
8
type Position ,
9
9
type CompletionContext ,
10
+ InsertTextFormat ,
10
11
} from 'vscode-languageserver'
11
12
import type { TextDocument } from 'vscode-languageserver-textdocument'
12
13
import dlv from 'dlv'
@@ -18,7 +19,7 @@ import { findLast, matchClassAttributes } from './util/find'
18
19
import { stringifyConfigValue , stringifyCss } from './util/stringify'
19
20
import { stringifyScreen , Screen } from './util/screens'
20
21
import isObject from './util/isObject'
21
- import braceLevel from './util/braceLevel'
22
+ import { braceLevel , parenLevel } from './util/braceLevel'
22
23
import * as emmetHelper from 'vscode-emmet-helper-bundled'
23
24
import { isValidLocationForEmmetAbbreviation } from './util/isValidLocationForEmmetAbbreviation'
24
25
import { isJsDoc , isJsxContext } from './util/js'
@@ -41,6 +42,9 @@ import { IS_SCRIPT_SOURCE, IS_TEMPLATE_SOURCE } from './metadata/extensions'
41
42
import * as postcss from 'postcss'
42
43
import { findFileDirective } from './completions/file-paths'
43
44
import 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'
44
48
45
49
let isUtil = ( className ) =>
46
50
Array . isArray ( className . __info )
@@ -1097,6 +1101,172 @@ function provideCssHelperCompletions(
1097
1101
)
1098
1102
}
1099
1103
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
+
1100
1270
function provideTailwindDirectiveCompletions (
1101
1271
state : State ,
1102
1272
document : TextDocument ,
@@ -1871,6 +2041,8 @@ export async function doComplete(
1871
2041
const result =
1872
2042
( await provideClassNameCompletions ( state , document , position , context ) ) ||
1873
2043
( await provideThemeDirectiveCompletions ( state , document , position ) ) ||
2044
+ provideUtilityFunctionArgumentCompletions ( state , document , position ) ||
2045
+ provideUtilityFunctionCompletions ( state , document , position ) ||
1874
2046
provideCssHelperCompletions ( state , document , position ) ||
1875
2047
provideCssDirectiveCompletions ( state , document , position ) ||
1876
2048
provideScreenDirectiveCompletions ( state , document , position ) ||
@@ -2039,3 +2211,87 @@ async function getCssDetail(state: State, className: any): Promise<string> {
2039
2211
}
2040
2212
return null
2041
2213
}
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