11import { TextDocument } from 'vscode-languageserver-textdocument' ;
2- import { Position , Range , MarkupKind , CompletionItem , CompletionItemKind } from 'vscode-languageserver-types' ;
2+ import { Position , Range , MarkupKind , CompletionItem , CompletionItemKind , InsertTextFormat } from 'vscode-languageserver-types' ;
33import { Values , Value , ValueObject } from '../types' ;
44import { TNAME , Token } from '../parsing' ;
55import { HoverV2 } from './language-service.types' ;
@@ -159,6 +159,72 @@ class VarTrie {
159159 }
160160}
161161
162+ /**
163+ * Resolve value by a mixed dot/bracket path like foo[0][1].bar starting from a given root.
164+ * For arrays, when an index is accessed, we treat it as the element shape and use the first element if present.
165+ */
166+ function resolveValueByBracketPath ( root : unknown , path : string ) : unknown {
167+ const isObj = ( v : unknown ) : v is Record < string , unknown > => v !== null && typeof v === 'object' ;
168+ let node : unknown = root as unknown ;
169+ if ( ! path ) return node ;
170+ const segments = path . split ( '.' ) ;
171+ for ( const seg of segments ) {
172+ if ( ! isObj ( node ) ) return undefined ;
173+ // parse leading name and bracket chains
174+ const i = seg . indexOf ( '[' ) ;
175+ const name = i >= 0 ? seg . slice ( 0 , i ) : seg ;
176+ let rest = i >= 0 ? seg . slice ( i ) : '' ;
177+ if ( name ) {
178+ node = ( node as Record < string , unknown > ) [ name ] ;
179+ }
180+ // walk bracket chains, treat any index as the element shape (use first element)
181+ while ( rest . startsWith ( '[' ) ) {
182+ const closeIdx = rest . indexOf ( ']' ) ;
183+ if ( closeIdx < 0 ) break ; // malformed, stop here
184+ rest = rest . slice ( closeIdx + 1 ) ;
185+ if ( Array . isArray ( node ) ) {
186+ node = node . length > 0 ? node [ 0 ] : undefined ;
187+ } else {
188+ node = undefined ;
189+ }
190+ }
191+ }
192+ return node ;
193+ }
194+
195+ /**
196+ * Pushes standard key completion and (if applicable) an array selector snippet completion.
197+ */
198+ function pushVarKeyCompletions (
199+ items : CompletionItem [ ] ,
200+ key : string ,
201+ label : string ,
202+ detail : string ,
203+ val : unknown ,
204+ rangePartial ?: Range
205+ ) : void {
206+ // Regular key/variable completion
207+ items . push ( {
208+ label,
209+ kind : CompletionItemKind . Variable ,
210+ detail,
211+ insertText : key ,
212+ textEdit : rangePartial ? { range : rangePartial , newText : key } : undefined
213+ } ) ;
214+
215+ // If the value is an array, suggest selector snippet as an extra item
216+ if ( Array . isArray ( val ) ) {
217+ const snippet = key + '[${1}]' ;
218+ items . push ( {
219+ label : `${ label } []` ,
220+ kind : CompletionItemKind . Variable ,
221+ detail : 'array' ,
222+ insertTextFormat : InsertTextFormat . Snippet ,
223+ textEdit : rangePartial ? { range : rangePartial , newText : snippet } : undefined
224+ } ) ;
225+ }
226+ }
227+
162228/**
163229 * Tries to resolve a variable hover using spans.
164230 * @param textDocument The document containing the variable name.
@@ -257,6 +323,27 @@ export function pathVariableCompletions(vars: Values | undefined, prefix: string
257323 const partial = endsWithDot ? '' : prefix . slice ( lastDot + 1 ) ;
258324 const lowerPartial = partial . toLowerCase ( ) ;
259325
326+ // If there are bracket selectors anywhere in the basePath, use bracket-aware resolution
327+ if ( basePath . includes ( '[' ) ) {
328+ const baseValue = resolveValueByBracketPath ( vars , basePath ) ;
329+ const items : CompletionItem [ ] = [ ] ;
330+
331+ // If the baseValue is an object, offer its keys
332+ if ( baseValue && typeof baseValue === 'object' && ! Array . isArray ( baseValue ) ) {
333+ const obj = baseValue as Record < string , unknown > ;
334+ for ( const key of Object . keys ( obj ) ) {
335+ if ( partial && ! key . toLowerCase ( ) . startsWith ( lowerPartial ) ) continue ;
336+ const fullLabel = basePath ? `${ basePath } .${ key } ` : key ;
337+ const val = obj [ key ] as Value ;
338+ const detail = valueTypeName ( val ) ;
339+ pushVarKeyCompletions ( items , key , fullLabel , detail , val , rangePartial ) ;
340+ }
341+ }
342+
343+ return items ;
344+ }
345+
346+ // Dot-only path: use trie for speed and existing behavior
260347 const baseNode = trie . search ( baseParts ) ;
261348 if ( ! baseNode ) {
262349 return [ ] ;
@@ -272,14 +359,7 @@ export function pathVariableCompletions(vars: Values | undefined, prefix: string
272359 const child = baseNode . children [ key ] ;
273360 const label = [ ...baseParts , key ] . join ( '.' ) ;
274361 const detail = child . value !== undefined ? valueTypeName ( child . value ) : 'object' ;
275-
276- items . push ( {
277- label,
278- kind : CompletionItemKind . Variable ,
279- detail,
280- insertText : key ,
281- textEdit : rangePartial ? { range : rangePartial , newText : key } : undefined
282- } ) ;
362+ pushVarKeyCompletions ( items , key , label , detail , child . value , rangePartial ) ;
283363 }
284364
285365 return items ;
0 commit comments