Skip to content

Commit 5b1e069

Browse files
author
Melvin van Bree
committed
Array completion working
1 parent bfbba8a commit 5b1e069

2 files changed

Lines changed: 91 additions & 10 deletions

File tree

src/language-service/ls-utils.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ export function valueTypeName(value: Value): string {
1919
}
2020

2121
export function isPathChar(ch: string): boolean {
22-
return /[A-Za-z0-9_$.]/.test(ch);
22+
// Include square brackets to keep array selectors within the detected prefix
23+
return /[A-Za-z0-9_$\.\[\]]/.test(ch);
2324
}
2425

2526
export function extractPathPrefix(text: string, position: number): { start: number; prefix: string } {

src/language-service/variable-utils.ts

Lines changed: 89 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { 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';
33
import { Values, Value, ValueObject } from '../types';
44
import { TNAME, Token } from '../parsing';
55
import { 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+
let 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

Comments
 (0)