@@ -2,6 +2,7 @@ import * as fs from "fs";
2
2
import * as path from "path" ;
3
3
import Ajv from "ajv/dist/2020.js" ;
4
4
import addFormats from "ajv-formats" ;
5
+ import * as jsoncParser from "jsonc-parser" ;
5
6
import {
6
7
Diagnostic ,
7
8
DiagnosticSeverity ,
@@ -247,7 +248,10 @@ export function validateConfig(document: TextDocument): Diagnostic[] {
247
248
}
248
249
}
249
250
250
- export function getConfigCompletions ( document : TextDocument ) : CompletionItem [ ] {
251
+ export function getConfigCompletions (
252
+ document : TextDocument ,
253
+ position ?: Position ,
254
+ ) : CompletionItem [ ] {
251
255
const filePath = document . uri ;
252
256
let fsPath : string ;
253
257
try {
@@ -266,6 +270,189 @@ export function getConfigCompletions(document: TextDocument): CompletionItem[] {
266
270
return [ ] ;
267
271
}
268
272
273
+ // If no position provided, fall back to top-level completions
274
+ if ( ! position ) {
275
+ return getTopLevelCompletions ( schemaInfo ) ;
276
+ }
277
+
278
+ const content = document . getText ( ) ;
279
+ const offset = document . offsetAt ( position ) ;
280
+
281
+ // Parse the document with jsonc-parser (handles incomplete JSON)
282
+ const errors : jsoncParser . ParseError [ ] = [ ] ;
283
+ const root = jsoncParser . parseTree ( content , errors ) ;
284
+ if ( ! root ) {
285
+ return getTopLevelCompletions ( schemaInfo ) ;
286
+ }
287
+
288
+ // Get the location at the cursor
289
+ const location = jsoncParser . getLocation ( content , offset ) ;
290
+
291
+ // Find the nearest object node that contains the cursor
292
+ const currentObjectNode = findContainingObjectNode ( root , offset ) ;
293
+ if ( ! currentObjectNode ) {
294
+ return getTopLevelCompletions ( schemaInfo ) ;
295
+ }
296
+
297
+ // Get the JSON path to this object
298
+ const path = getPathToNode ( root , currentObjectNode ) ;
299
+ if ( ! path ) {
300
+ return getTopLevelCompletions ( schemaInfo ) ;
301
+ }
302
+
303
+ // Resolve the schema for this path
304
+ const schemaAtPath = resolveSchemaForPath ( schemaInfo . schema , path ) ;
305
+ if ( ! schemaAtPath || ! schemaAtPath . properties ) {
306
+ return getTopLevelCompletions ( schemaInfo ) ;
307
+ }
308
+
309
+ // Get existing keys in the current object
310
+ const existingKeys = getExistingKeys ( currentObjectNode ) ;
311
+
312
+ // Build completion items for available properties
313
+ const completions = Object . entries ( schemaAtPath . properties )
314
+ . filter ( ( [ key ] ) => ! existingKeys . includes ( key ) )
315
+ . map ( ( [ key , prop ] : [ string , any ] ) => {
316
+ const item : CompletionItem = {
317
+ label : key ,
318
+ kind : CompletionItemKind . Property ,
319
+ detail : prop . description || key ,
320
+ insertText : `"${ key } ": ` ,
321
+ } ;
322
+
323
+ if ( prop . type === "boolean" ) {
324
+ item . insertText = `"${ key } ": ${ prop . default !== undefined ? prop . default : false } ` ;
325
+ } else if ( prop . type === "array" && prop . items ?. enum ) {
326
+ item . insertText = `"${ key } ": [\n ${ prop . items . enum . map ( ( v : string ) => `"${ v } "` ) . join ( ",\n " ) } \n]` ;
327
+ } else if ( prop . enum ) {
328
+ item . insertText = `"${ key } ": "${ prop . default || prop . enum [ 0 ] } "` ;
329
+ }
330
+
331
+ return item ;
332
+ } ) ;
333
+
334
+ return completions . length > 0 ? completions : getTopLevelCompletions ( schemaInfo ) ;
335
+ }
336
+
337
+ // Helper functions for jsonc-parser based completion
338
+
339
+ function findContainingObjectNode ( node : jsoncParser . Node | undefined , offset : number ) : jsoncParser . Node | undefined {
340
+ if ( ! node ) {
341
+ return undefined ;
342
+ }
343
+
344
+ let bestMatch : jsoncParser . Node | undefined = undefined ;
345
+
346
+ // If this node is an object and contains the offset, it's a potential match
347
+ if ( node . type === 'object' && node . offset <= offset && node . offset + node . length >= offset ) {
348
+ bestMatch = node ;
349
+ }
350
+
351
+ // If this node has children, search them recursively
352
+ if ( node . children ) {
353
+ for ( const child of node . children ) {
354
+ const result = findContainingObjectNode ( child , offset ) ;
355
+ if ( result ) {
356
+ // Prefer deeper/more specific matches
357
+ if ( ! bestMatch || ( result . offset > bestMatch . offset && result . length < bestMatch . length ) ) {
358
+ bestMatch = result ;
359
+ }
360
+ }
361
+ }
362
+ }
363
+
364
+ return bestMatch ;
365
+ }
366
+
367
+ function getPathToNode ( root : jsoncParser . Node , targetNode : jsoncParser . Node ) : string [ ] | undefined {
368
+ function buildPath ( node : jsoncParser . Node , currentPath : string [ ] ) : string [ ] | undefined {
369
+ if ( node === targetNode ) {
370
+ return currentPath ;
371
+ }
372
+
373
+ if ( node . children ) {
374
+ for ( const child of node . children ) {
375
+ let newPath = [ ...currentPath ] ;
376
+
377
+ // If this child is a property node, add its key to the path
378
+ if ( child . type === 'property' && child . children && child . children . length >= 2 ) {
379
+ const keyNode = child . children [ 0 ] ;
380
+ if ( keyNode . type === 'string' ) {
381
+ const key = jsoncParser . getNodeValue ( keyNode ) ;
382
+ if ( typeof key === 'string' ) {
383
+ newPath = [ ...newPath , key ] ;
384
+ }
385
+ }
386
+ }
387
+
388
+ const result = buildPath ( child , newPath ) ;
389
+ if ( result ) {
390
+ return result ;
391
+ }
392
+ }
393
+ }
394
+
395
+ return undefined ;
396
+ }
397
+
398
+ return buildPath ( root , [ ] ) ;
399
+ }
400
+
401
+ function getExistingKeys ( objectNode : jsoncParser . Node ) : string [ ] {
402
+ const keys : string [ ] = [ ] ;
403
+
404
+ if ( objectNode . type === 'object' && objectNode . children ) {
405
+ for ( const child of objectNode . children ) {
406
+ if ( child . type === 'property' && child . children && child . children . length >= 1 ) {
407
+ const keyNode = child . children [ 0 ] ;
408
+ if ( keyNode . type === 'string' ) {
409
+ const key = jsoncParser . getNodeValue ( keyNode ) ;
410
+ if ( typeof key === 'string' ) {
411
+ keys . push ( key ) ;
412
+ }
413
+ }
414
+ }
415
+ }
416
+ }
417
+
418
+ return keys ;
419
+ }
420
+
421
+ function resolveSchemaForPath ( schema : any , path : string [ ] ) : any {
422
+ let current = schema ;
423
+
424
+ for ( const segment of path ) {
425
+ if ( current . properties && current . properties [ segment ] ) {
426
+ const prop = current . properties [ segment ] ;
427
+
428
+ // Handle $ref
429
+ if ( prop . $ref ) {
430
+ const refPath = prop . $ref . replace ( "#/" , "" ) . split ( "/" ) ;
431
+ let resolved = schema ;
432
+ for ( const refSegment of refPath ) {
433
+ resolved = resolved [ refSegment ] ;
434
+ if ( ! resolved ) {
435
+ return null ;
436
+ }
437
+ }
438
+ current = resolved ;
439
+ } else if ( prop . type === "object" && prop . properties ) {
440
+ current = prop ;
441
+ } else {
442
+ return null ;
443
+ }
444
+ } else {
445
+ return null ;
446
+ }
447
+ }
448
+
449
+ return current ;
450
+ }
451
+
452
+ function getTopLevelCompletions ( schemaInfo : SchemaInfo ) : CompletionItem [ ] {
453
+ if ( ! schemaInfo . schema . properties ) {
454
+ return [ ] ;
455
+ }
269
456
return Object . entries ( schemaInfo . schema . properties ) . map (
270
457
( [ key , prop ] : [ string , any ] ) => {
271
458
const item : CompletionItem = {
0 commit comments