@@ -2,6 +2,7 @@ import * as fs from "fs";
22import * as path from "path" ;
33import Ajv from "ajv/dist/2020.js" ;
44import addFormats from "ajv-formats" ;
5+ import * as jsoncParser from "jsonc-parser" ;
56import {
67 Diagnostic ,
78 DiagnosticSeverity ,
@@ -247,7 +248,10 @@ export function validateConfig(document: TextDocument): Diagnostic[] {
247248 }
248249}
249250
250- export function getConfigCompletions ( document : TextDocument ) : CompletionItem [ ] {
251+ export function getConfigCompletions (
252+ document : TextDocument ,
253+ position ?: Position ,
254+ ) : CompletionItem [ ] {
251255 const filePath = document . uri ;
252256 let fsPath : string ;
253257 try {
@@ -266,6 +270,189 @@ export function getConfigCompletions(document: TextDocument): CompletionItem[] {
266270 return [ ] ;
267271 }
268272
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+ }
269456 return Object . entries ( schemaInfo . schema . properties ) . map (
270457 ( [ key , prop ] : [ string , any ] ) => {
271458 const item : CompletionItem = {
0 commit comments