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,8 @@ 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'
44
47
45
48
let isUtil = ( className ) =>
46
49
Array . isArray ( className . __info )
@@ -1097,6 +1100,219 @@ function provideCssHelperCompletions(
1097
1100
)
1098
1101
}
1099
1102
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
+
1100
1316
function provideTailwindDirectiveCompletions (
1101
1317
state : State ,
1102
1318
document : TextDocument ,
@@ -1871,6 +2087,8 @@ export async function doComplete(
1871
2087
const result =
1872
2088
( await provideClassNameCompletions ( state , document , position , context ) ) ||
1873
2089
( await provideThemeDirectiveCompletions ( state , document , position ) ) ||
2090
+ provideUtilityFunctionArgumentCompletions ( state , document , position ) ||
2091
+ provideUtilityFunctionCompletions ( state , document , position ) ||
1874
2092
provideCssHelperCompletions ( state , document , position ) ||
1875
2093
provideCssDirectiveCompletions ( state , document , position ) ||
1876
2094
provideScreenDirectiveCompletions ( state , document , position ) ||
0 commit comments