From bfbba8acc7659f2020be393f94289f24ac2aba98 Mon Sep 17 00:00:00 2001 From: Melvin van Bree Date: Thu, 15 Jan 2026 10:53:16 +0100 Subject: [PATCH 1/7] Added tests the auto completion should adhere to # Conflicts: # test/language-service/language-service.ts --- test/language-service/language-service.ts | 94 +++++++++++++++++++++-- 1 file changed, 88 insertions(+), 6 deletions(-) diff --git a/test/language-service/language-service.ts b/test/language-service/language-service.ts index 8787b9f..cb237d9 100644 --- a/test/language-service/language-service.ts +++ b/test/language-service/language-service.ts @@ -199,13 +199,95 @@ describe('Language Service', () => { position: { line: 0, character: 0 } }); - expect(completions.find(c => c.label === 'numVar')?.detail).toBe('number'); - expect(completions.find(c => c.label === 'strVar')?.detail).toBe('string'); - expect(completions.find(c => c.label === 'arrVar')?.detail).toBe('array'); - expect(completions.find(c => c.label === 'boolVar')?.detail).toBe('boolean'); - expect(completions.find(c => c.label === 'nullVar')?.detail).toBe('null'); + expect(completions.find(c => c.label === 'numVar')?.detail).toBe('number'); + expect(completions.find(c => c.label === 'strVar')?.detail).toBe('string'); + expect(completions.find(c => c.label === 'arrVar')?.detail).toBe('array'); + expect(completions.find(c => c.label === 'boolVar')?.detail).toBe('boolean'); + expect(completions.find(c => c.label === 'nullVar')?.detail).toBe('null'); + }); + + it('should suggest array selector when variable is an array', () => { + const text = 'arr'; + const doc = TextDocument.create('file://test', 'plaintext', 1, text); + + const completions = ls.getCompletions({ + textDocument: doc, + variables: { + arr: [10, 20, 30] + }, + position: { line: 0, character: 3 } + }); + + const arrayItem = completions.find(c => c.label === 'arr[]'); + expect(arrayItem).toBeDefined(); + + // Insert only the selector + expect(arrayItem?.insertTextFormat).toBe(2); // Snippet + expect(arrayItem?.textEdit?.newText).toContain('arr['); + }); + + it('should autocomplete children after indexed array access', () => { + const text = 'arr[0].'; + const doc = TextDocument.create('file://test', 'plaintext', 1, text); + + const completions = ls.getCompletions({ + textDocument: doc, + variables: { + arr: [ + { foo: 1, bar: 2 } + ] + }, + position: { line: 0, character: text.length } + }); + + expect(completions.length).toBeGreaterThan(0); + + const fooItem = completions.find(c => c.label === 'arr[0].foo'); + expect(fooItem).toBeDefined(); + expect(fooItem?.insertText).toBe('foo'); + }); + + it('should support multi-dimensional array selectors', () => { + const text = 'matrix[0][1].'; + const doc = TextDocument.create('file://test', 'plaintext', 1, text); + + const completions = ls.getCompletions({ + textDocument: doc, + variables: { + matrix: [ + [ + { value: 42 } + ] + ] + }, + position: { line: 0, character: text.length } + }); + + const valueItem = completions.find(c => c.label === 'matrix[0][1].value'); + expect(valueItem).toBeDefined(); + expect(valueItem?.insertText).toBe('value'); + }); + + it('should place cursor inside array brackets', () => { + const text = 'arr'; + const doc = TextDocument.create('file://test', 'plaintext', 1, text); + + const completions = ls.getCompletions({ + textDocument: doc, + variables: { + arr: [1, 2, 3] + }, + position: { line: 0, character: 3 } + }); + + const arrayItem = completions.find(c => c.label === 'arr[]'); + const newText = arrayItem?.textEdit?.newText as string | undefined; + + expect(newText).toContain('['); + expect(newText).toContain(']'); + expect(newText).toContain('${1}'); + }); }); - }); describe('getHover', () => { it('should show type information for variables', () => { From 5b1e0699688339752d7d026d2013b0e2759f652f Mon Sep 17 00:00:00 2001 From: Melvin van Bree Date: Thu, 15 Jan 2026 11:15:41 +0100 Subject: [PATCH 2/7] Array completion working --- src/language-service/ls-utils.ts | 3 +- src/language-service/variable-utils.ts | 98 +++++++++++++++++++++++--- 2 files changed, 91 insertions(+), 10 deletions(-) diff --git a/src/language-service/ls-utils.ts b/src/language-service/ls-utils.ts index f626fb3..ec0a05b 100644 --- a/src/language-service/ls-utils.ts +++ b/src/language-service/ls-utils.ts @@ -19,7 +19,8 @@ export function valueTypeName(value: Value): string { } export function isPathChar(ch: string): boolean { - return /[A-Za-z0-9_$.]/.test(ch); + // Include square brackets to keep array selectors within the detected prefix + return /[A-Za-z0-9_$\.\[\]]/.test(ch); } export function extractPathPrefix(text: string, position: number): { start: number; prefix: string } { diff --git a/src/language-service/variable-utils.ts b/src/language-service/variable-utils.ts index 0b5e7e7..a956b39 100644 --- a/src/language-service/variable-utils.ts +++ b/src/language-service/variable-utils.ts @@ -1,5 +1,5 @@ import { TextDocument } from 'vscode-languageserver-textdocument'; -import { Position, Range, MarkupKind, CompletionItem, CompletionItemKind } from 'vscode-languageserver-types'; +import { Position, Range, MarkupKind, CompletionItem, CompletionItemKind, InsertTextFormat } from 'vscode-languageserver-types'; import { Values, Value, ValueObject } from '../types'; import { TNAME, Token } from '../parsing'; import { HoverV2 } from './language-service.types'; @@ -159,6 +159,72 @@ class VarTrie { } } +/** + * Resolve value by a mixed dot/bracket path like foo[0][1].bar starting from a given root. + * For arrays, when an index is accessed, we treat it as the element shape and use the first element if present. + */ +function resolveValueByBracketPath(root: unknown, path: string): unknown { + const isObj = (v: unknown): v is Record => v !== null && typeof v === 'object'; + let node: unknown = root as unknown; + if (!path) return node; + const segments = path.split('.'); + for (const seg of segments) { + if (!isObj(node)) return undefined; + // parse leading name and bracket chains + let i = seg.indexOf('['); + const name = i >= 0 ? seg.slice(0, i) : seg; + let rest = i >= 0 ? seg.slice(i) : ''; + if (name) { + node = (node as Record)[name]; + } + // walk bracket chains, treat any index as the element shape (use first element) + while (rest.startsWith('[')) { + const closeIdx = rest.indexOf(']'); + if (closeIdx < 0) break; // malformed, stop here + rest = rest.slice(closeIdx + 1); + if (Array.isArray(node)) { + node = node.length > 0 ? node[0] : undefined; + } else { + node = undefined; + } + } + } + return node; +} + +/** + * Pushes standard key completion and (if applicable) an array selector snippet completion. + */ +function pushVarKeyCompletions( + items: CompletionItem[], + key: string, + label: string, + detail: string, + val: unknown, + rangePartial?: Range +): void { + // Regular key/variable completion + items.push({ + label, + kind: CompletionItemKind.Variable, + detail, + insertText: key, + textEdit: rangePartial ? { range: rangePartial, newText: key } : undefined + }); + + // If the value is an array, suggest selector snippet as an extra item + if (Array.isArray(val)) { + const snippet = key + '[${1}]'; + items.push({ + label: `${label}[]`, + kind: CompletionItemKind.Variable, + detail: 'array', + insertTextFormat: InsertTextFormat.Snippet, + textEdit: rangePartial ? { range: rangePartial, newText: snippet } : undefined + }); + } +} + /** * Tries to resolve a variable hover using spans. * @param textDocument The document containing the variable name. @@ -257,6 +323,27 @@ export function pathVariableCompletions(vars: Values | undefined, prefix: string const partial = endsWithDot ? '' : prefix.slice(lastDot + 1); const lowerPartial = partial.toLowerCase(); + // If there are bracket selectors anywhere in the basePath, use bracket-aware resolution + if (basePath.includes('[')) { + const baseValue = resolveValueByBracketPath(vars, basePath); + const items: CompletionItem[] = []; + + // If the baseValue is an object, offer its keys + if (baseValue && typeof baseValue === 'object' && !Array.isArray(baseValue)) { + const obj = baseValue as Record; + for (const key of Object.keys(obj)) { + if (partial && !key.toLowerCase().startsWith(lowerPartial)) continue; + const fullLabel = basePath ? `${basePath}.${key}` : key; + const val = obj[key] as Value; + const detail = valueTypeName(val); + pushVarKeyCompletions(items, key, fullLabel, detail, val, rangePartial); + } + } + + return items; + } + + // Dot-only path: use trie for speed and existing behavior const baseNode = trie.search(baseParts); if (!baseNode) { return []; @@ -272,14 +359,7 @@ export function pathVariableCompletions(vars: Values | undefined, prefix: string const child = baseNode.children[key]; const label = [...baseParts, key].join('.'); const detail = child.value !== undefined ? valueTypeName(child.value) : 'object'; - - items.push({ - label, - kind: CompletionItemKind.Variable, - detail, - insertText: key, - textEdit: rangePartial ? { range: rangePartial, newText: key } : undefined - }); + pushVarKeyCompletions(items, key, label, detail, child.value, rangePartial); } return items; From 09c7f144feae3566d1dded41d5d58f6c593f647b Mon Sep 17 00:00:00 2001 From: Melvin van Bree Date: Thu, 15 Jan 2026 12:00:29 +0100 Subject: [PATCH 3/7] Formatted --- .editorconfig | 3 +- test/language-service/language-service.ts | 266 +++++++++++----------- 2 files changed, 135 insertions(+), 134 deletions(-) diff --git a/.editorconfig b/.editorconfig index 70746bc..0878863 100644 --- a/.editorconfig +++ b/.editorconfig @@ -6,4 +6,5 @@ end_of_line = lf charset = utf-8 insert_final_newline = true trim_trailing_whitespace = true - +indent_style = space +indent_size = 2 \ No newline at end of file diff --git a/test/language-service/language-service.ts b/test/language-service/language-service.ts index cb237d9..28c3c9d 100644 --- a/test/language-service/language-service.ts +++ b/test/language-service/language-service.ts @@ -1,7 +1,7 @@ -import { describe, it, expect, beforeEach } from 'vitest'; -import { TextDocument } from 'vscode-languageserver-textdocument'; -import { createLanguageService } from '../../src/language-service/language-service'; -import { CompletionItemKind, MarkupKind } from 'vscode-languageserver-types'; +import {describe, it, expect, beforeEach} from 'vitest'; +import {TextDocument} from 'vscode-languageserver-textdocument'; +import {createLanguageService} from '../../src/language-service/language-service'; +import {CompletionItemKind, MarkupKind} from 'vscode-languageserver-types'; function getContentsValue(contents: any): string { if (typeof contents === 'string') { @@ -29,8 +29,8 @@ describe('Language Service', () => { const doc = TextDocument.create('file://test', 'plaintext', 1, text); const completions = ls.getCompletions({ textDocument: doc, - variables: { foo: 123, bar: 'test' }, - position: { line: 0, character: 3 } + variables: {foo: 123, bar: 'test'}, + position: {line: 0, character: 3} }); const labels = completions.map(c => c.label); expect(labels).toContain('foo'); @@ -41,8 +41,8 @@ describe('Language Service', () => { const doc = TextDocument.create('file://test', 'plaintext', 1, text); const completions = ls.getCompletions({ textDocument: doc, - variables: { max: 10, min: 5, foo: 1 }, - position: { line: 0, character: 2 } + variables: {max: 10, min: 5, foo: 1}, + position: {line: 0, character: 2} }); const labels = completions.map(c => c.label); expect(labels).toContain('max'); @@ -55,8 +55,8 @@ describe('Language Service', () => { const doc = TextDocument.create('file://test', 'plaintext', 1, text); const completions = ls.getCompletions({ textDocument: doc, - variables: { max: 10, foo: 1 }, - position: { line: 0, character: 2 } + variables: {max: 10, foo: 1}, + position: {line: 0, character: 2} }); const labels = completions.map(c => c.label); expect(labels).toContain('max'); @@ -67,8 +67,8 @@ describe('Language Service', () => { const doc = TextDocument.create('file://test', 'plaintext', 1, text); const completions = ls.getCompletions({ textDocument: doc, - variables: { foo: 1, bar: 2 }, - position: { line: 0, character: 0 } + variables: {foo: 1, bar: 2}, + position: {line: 0, character: 0} }); const labels = completions.map(c => c.label); expect(labels).toContain('foo'); @@ -82,7 +82,7 @@ describe('Language Service', () => { const completions = ls.getCompletions({ textDocument: doc, variables: undefined, - position: { line: 0, character: 3 } + position: {line: 0, character: 3} }); const labels = completions.map(c => c.label); expect(labels).not.toContain('foo'); @@ -93,7 +93,7 @@ describe('Language Service', () => { const doc = TextDocument.create('file://test', 'plaintext', 1, text); const completions = ls.getCompletions({ textDocument: doc, - position: { line: 0, character: 3 } + position: {line: 0, character: 3} }); const labels = completions.map(c => c.label); expect(labels).toContain('sin'); @@ -104,7 +104,7 @@ describe('Language Service', () => { const doc = TextDocument.create('file://test', 'plaintext', 1, text); const completions = ls.getCompletions({ textDocument: doc, - position: { line: 0, character: 2 } + position: {line: 0, character: 2} }); const labels = completions.map(c => c.label); expect(labels).toContain('PI'); @@ -115,7 +115,7 @@ describe('Language Service', () => { const doc = TextDocument.create('file://test', 'plaintext', 1, text); const completions = ls.getCompletions({ textDocument: doc, - position: { line: 0, character: 2 } + position: {line: 0, character: 2} }); const labels = completions.map(c => c.label); expect(labels).toContain('case'); @@ -126,8 +126,8 @@ describe('Language Service', () => { const doc = TextDocument.create('file://test', 'plaintext', 1, text); const completions = ls.getCompletions({ textDocument: doc, - variables: { foo: { bar: 1 } }, - position: { line: 0, character: 4 } + variables: {foo: {bar: 1}}, + position: {line: 0, character: 4} }); expect(completions.length).toBeGreaterThan(0); const item = completions.find(c => c.label === 'foo.bar'); @@ -140,8 +140,8 @@ describe('Language Service', () => { const doc = TextDocument.create('file://test', 'plaintext', 1, text); const completions = ls.getCompletions({ textDocument: doc, - variables: { sine: 1 }, - position: { line: 0, character: 2 } + variables: {sine: 1}, + position: {line: 0, character: 2} }); const sinFunc = completions.find(c => c.label === 'sin'); @@ -161,8 +161,8 @@ describe('Language Service', () => { const doc = TextDocument.create('file://test', 'plaintext', 1, text); const completions = ls.getCompletions({ textDocument: doc, - variables: { myVar: 42 }, - position: { line: 0, character: 2 } + variables: {myVar: 42}, + position: {line: 0, character: 2} }); const varCompletion = completions.find(c => c.label === 'myVar'); @@ -176,7 +176,7 @@ describe('Language Service', () => { const doc = TextDocument.create('file://test', 'plaintext', 1, text); const completions = ls.getCompletions({ textDocument: doc, - position: { line: 0, character: 1 } + position: {line: 0, character: 1} }); const eConst = completions.find(c => c.label === 'E'); @@ -196,107 +196,107 @@ describe('Language Service', () => { boolVar: true, nullVar: null }, - position: { line: 0, character: 0 } - }); - - expect(completions.find(c => c.label === 'numVar')?.detail).toBe('number'); - expect(completions.find(c => c.label === 'strVar')?.detail).toBe('string'); - expect(completions.find(c => c.label === 'arrVar')?.detail).toBe('array'); - expect(completions.find(c => c.label === 'boolVar')?.detail).toBe('boolean'); - expect(completions.find(c => c.label === 'nullVar')?.detail).toBe('null'); - }); - - it('should suggest array selector when variable is an array', () => { - const text = 'arr'; - const doc = TextDocument.create('file://test', 'plaintext', 1, text); - - const completions = ls.getCompletions({ - textDocument: doc, - variables: { - arr: [10, 20, 30] - }, - position: { line: 0, character: 3 } - }); - - const arrayItem = completions.find(c => c.label === 'arr[]'); - expect(arrayItem).toBeDefined(); - - // Insert only the selector - expect(arrayItem?.insertTextFormat).toBe(2); // Snippet - expect(arrayItem?.textEdit?.newText).toContain('arr['); - }); - - it('should autocomplete children after indexed array access', () => { - const text = 'arr[0].'; - const doc = TextDocument.create('file://test', 'plaintext', 1, text); - - const completions = ls.getCompletions({ - textDocument: doc, - variables: { - arr: [ - { foo: 1, bar: 2 } - ] - }, - position: { line: 0, character: text.length } - }); - - expect(completions.length).toBeGreaterThan(0); - - const fooItem = completions.find(c => c.label === 'arr[0].foo'); - expect(fooItem).toBeDefined(); - expect(fooItem?.insertText).toBe('foo'); - }); - - it('should support multi-dimensional array selectors', () => { - const text = 'matrix[0][1].'; - const doc = TextDocument.create('file://test', 'plaintext', 1, text); - - const completions = ls.getCompletions({ - textDocument: doc, - variables: { - matrix: [ - [ - { value: 42 } - ] - ] - }, - position: { line: 0, character: text.length } - }); - - const valueItem = completions.find(c => c.label === 'matrix[0][1].value'); - expect(valueItem).toBeDefined(); - expect(valueItem?.insertText).toBe('value'); - }); - - it('should place cursor inside array brackets', () => { - const text = 'arr'; - const doc = TextDocument.create('file://test', 'plaintext', 1, text); - - const completions = ls.getCompletions({ - textDocument: doc, - variables: { - arr: [1, 2, 3] - }, - position: { line: 0, character: 3 } - }); - - const arrayItem = completions.find(c => c.label === 'arr[]'); - const newText = arrayItem?.textEdit?.newText as string | undefined; - - expect(newText).toContain('['); - expect(newText).toContain(']'); - expect(newText).toContain('${1}'); - }); + position: {line: 0, character: 0} + }); + + expect(completions.find(c => c.label === 'numVar')?.detail).toBe('number'); + expect(completions.find(c => c.label === 'strVar')?.detail).toBe('string'); + expect(completions.find(c => c.label === 'arrVar')?.detail).toBe('array'); + expect(completions.find(c => c.label === 'boolVar')?.detail).toBe('boolean'); + expect(completions.find(c => c.label === 'nullVar')?.detail).toBe('null'); + }); + + it('should suggest array selector when variable is an array', () => { + const text = 'arr'; + const doc = TextDocument.create('file://test', 'plaintext', 1, text); + + const completions = ls.getCompletions({ + textDocument: doc, + variables: { + arr: [10, 20, 30] + }, + position: {line: 0, character: 3} + }); + + const arrayItem = completions.find(c => c.label === 'arr[]'); + expect(arrayItem).toBeDefined(); + + // Insert only the selector + expect(arrayItem?.insertTextFormat).toBe(2); // Snippet + expect(arrayItem?.textEdit?.newText).toContain('arr['); }); + it('should autocomplete children after indexed array access', () => { + const text = 'arr[0].'; + const doc = TextDocument.create('file://test', 'plaintext', 1, text); + + const completions = ls.getCompletions({ + textDocument: doc, + variables: { + arr: [ + {foo: 1, bar: 2} + ] + }, + position: {line: 0, character: text.length} + }); + + expect(completions.length).toBeGreaterThan(0); + + const fooItem = completions.find(c => c.label === 'arr[0].foo'); + expect(fooItem).toBeDefined(); + expect(fooItem?.insertText).toBe('foo'); + }); + + it('should support multi-dimensional array selectors', () => { + const text = 'matrix[0][1].'; + const doc = TextDocument.create('file://test', 'plaintext', 1, text); + + const completions = ls.getCompletions({ + textDocument: doc, + variables: { + matrix: [ + [ + {value: 42} + ] + ] + }, + position: {line: 0, character: text.length} + }); + + const valueItem = completions.find(c => c.label === 'matrix[0][1].value'); + expect(valueItem).toBeDefined(); + expect(valueItem?.insertText).toBe('value'); + }); + + it('should place cursor inside array brackets', () => { + const text = 'arr'; + const doc = TextDocument.create('file://test', 'plaintext', 1, text); + + const completions = ls.getCompletions({ + textDocument: doc, + variables: { + arr: [1, 2, 3] + }, + position: {line: 0, character: 3} + }); + + const arrayItem = completions.find(c => c.label === 'arr[]'); + const newText = arrayItem?.textEdit?.newText as string | undefined; + + expect(newText).toContain('['); + expect(newText).toContain(']'); + expect(newText).toContain('${1}'); + }); + }); + describe('getHover', () => { it('should show type information for variables', () => { const text = 'foo'; const doc = TextDocument.create('file://test', 'plaintext', 1, text); const hover = ls.getHover({ textDocument: doc, - position: { line: 0, character: 1 }, - variables: { foo: 42 } + position: {line: 0, character: 1}, + variables: {foo: 42} }); const contents = getContentsValue(hover.contents); @@ -309,7 +309,7 @@ describe('Language Service', () => { const doc = TextDocument.create('file://test', 'plaintext', 1, text); const hover = ls.getHover({ textDocument: doc, - position: { line: 0, character: 1 }, + position: {line: 0, character: 1}, variables: {} }); @@ -322,7 +322,7 @@ describe('Language Service', () => { const doc = TextDocument.create('file://test', 'plaintext', 1, text); const hover = ls.getHover({ textDocument: doc, - position: { line: 0, character: 1 }, + position: {line: 0, character: 1}, variables: {} }); @@ -336,7 +336,7 @@ describe('Language Service', () => { const doc = TextDocument.create('file://test', 'plaintext', 1, text); const hover = ls.getHover({ textDocument: doc, - position: { line: 0, character: 1 }, + position: {line: 0, character: 1}, variables: {} }); @@ -349,7 +349,7 @@ describe('Language Service', () => { const doc = TextDocument.create('file://test', 'plaintext', 1, text); const hover = ls.getHover({ textDocument: doc, - position: { line: 0, character: 2 }, + position: {line: 0, character: 2}, variables: {} }); @@ -363,7 +363,7 @@ describe('Language Service', () => { const doc = TextDocument.create('file://test', 'plaintext', 1, text); const hover = ls.getHover({ textDocument: doc, - position: { line: 0, character: 1 }, + position: {line: 0, character: 1}, variables: {} }); @@ -376,7 +376,7 @@ describe('Language Service', () => { const doc = TextDocument.create('file://test', 'plaintext', 1, text); const hover = ls.getHover({ textDocument: doc, - position: { line: 0, character: 2 }, + position: {line: 0, character: 2}, variables: {} }); @@ -389,7 +389,7 @@ describe('Language Service', () => { const doc = TextDocument.create('file://test', 'plaintext', 1, text); const hover = ls.getHover({ textDocument: doc, - position: { line: 0, character: 0 }, + position: {line: 0, character: 0}, variables: {} }); @@ -401,8 +401,8 @@ describe('Language Service', () => { const doc = TextDocument.create('file://test', 'plaintext', 1, text); const hover = ls.getHover({ textDocument: doc, - position: { line: 0, character: 1 }, - variables: { myFunc: 'my-variable' } + position: {line: 0, character: 1}, + variables: {myFunc: 'my-variable'} }); const contents = getContentsValue(hover.contents); @@ -416,7 +416,7 @@ describe('Language Service', () => { const doc = TextDocument.create('file://test', 'plaintext', 1, text); const hover = ls.getHover({ textDocument: doc, - position: { line: 0, character: 1 } + position: {line: 0, character: 1} }); const contents = hover.contents as any; @@ -434,8 +434,8 @@ describe('Language Service', () => { const doc = TextDocument.create('file://test', 'plaintext', 1, text); const hover = ls.getHover({ textDocument: doc, - position: { line: 0, character: 1 }, - variables: { foo: 42 } + position: {line: 0, character: 1}, + variables: {foo: 42} }); const contents = hover.contents as any; @@ -558,13 +558,13 @@ describe('Language Service', () => { const completions = ls.getCompletions({ textDocument: doc, - position: { line: 0, character: 0 } + position: {line: 0, character: 0} }); expect(Array.isArray(completions)).toBe(true); const hover = ls.getHover({ textDocument: doc, - position: { line: 0, character: 0 } + position: {line: 0, character: 0} }); expect(hover).toBeDefined(); @@ -602,7 +602,7 @@ describe('Language Service', () => { const completions = ls.getCompletions({ textDocument: doc, - position: { line: 0, character: text.length } + position: {line: 0, character: text.length} }); expect(Array.isArray(completions)).toBe(true); }); @@ -613,7 +613,7 @@ describe('Language Service', () => { const completions = ls.getCompletions({ textDocument: doc, - position: { line: 0, character: 0 } + position: {line: 0, character: 0} }); expect(Array.isArray(completions)).toBe(true); }); @@ -630,7 +630,7 @@ describe('Language Service', () => { it('should create service with custom options', () => { const service = createLanguageService({ - operators: { '+': true, '-': false } + operators: {'+': true, '-': false} }); expect(service).toBeDefined(); }); From 136861d90bfda83220e5b6a8d8a730fb0c62b29b Mon Sep 17 00:00:00 2001 From: Melvin van Bree Date: Thu, 15 Jan 2026 12:14:19 +0100 Subject: [PATCH 4/7] Reverted spacing between braces --- .editorconfig | 6 +- test/language-service/language-service.ts | 90 +++++++++++------------ 2 files changed, 50 insertions(+), 46 deletions(-) diff --git a/.editorconfig b/.editorconfig index 0878863..19f3447 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,4 +7,8 @@ charset = utf-8 insert_final_newline = true trim_trailing_whitespace = true indent_style = space -indent_size = 2 \ No newline at end of file +indent_size = 2 + +[*.{js,jsx,ts,tsx}] +ij_javascript_spaces_within_object_literal_braces = true +ij_typescript_spaces_within_object_literal_braces = true \ No newline at end of file diff --git a/test/language-service/language-service.ts b/test/language-service/language-service.ts index 28c3c9d..50fa0a5 100644 --- a/test/language-service/language-service.ts +++ b/test/language-service/language-service.ts @@ -29,8 +29,8 @@ describe('Language Service', () => { const doc = TextDocument.create('file://test', 'plaintext', 1, text); const completions = ls.getCompletions({ textDocument: doc, - variables: {foo: 123, bar: 'test'}, - position: {line: 0, character: 3} + variables: { foo: 123, bar: 'test' }, + position: { line: 0, character: 3 } }); const labels = completions.map(c => c.label); expect(labels).toContain('foo'); @@ -41,8 +41,8 @@ describe('Language Service', () => { const doc = TextDocument.create('file://test', 'plaintext', 1, text); const completions = ls.getCompletions({ textDocument: doc, - variables: {max: 10, min: 5, foo: 1}, - position: {line: 0, character: 2} + variables: { max: 10, min: 5, foo: 1 }, + position: { line: 0, character: 2 } }); const labels = completions.map(c => c.label); expect(labels).toContain('max'); @@ -55,8 +55,8 @@ describe('Language Service', () => { const doc = TextDocument.create('file://test', 'plaintext', 1, text); const completions = ls.getCompletions({ textDocument: doc, - variables: {max: 10, foo: 1}, - position: {line: 0, character: 2} + variables: { max: 10, foo: 1 }, + position: { line: 0, character: 2 } }); const labels = completions.map(c => c.label); expect(labels).toContain('max'); @@ -67,8 +67,8 @@ describe('Language Service', () => { const doc = TextDocument.create('file://test', 'plaintext', 1, text); const completions = ls.getCompletions({ textDocument: doc, - variables: {foo: 1, bar: 2}, - position: {line: 0, character: 0} + variables: { foo: 1, bar: 2 }, + position: { line: 0, character: 0 } }); const labels = completions.map(c => c.label); expect(labels).toContain('foo'); @@ -82,7 +82,7 @@ describe('Language Service', () => { const completions = ls.getCompletions({ textDocument: doc, variables: undefined, - position: {line: 0, character: 3} + position: { line: 0, character: 3 } }); const labels = completions.map(c => c.label); expect(labels).not.toContain('foo'); @@ -93,7 +93,7 @@ describe('Language Service', () => { const doc = TextDocument.create('file://test', 'plaintext', 1, text); const completions = ls.getCompletions({ textDocument: doc, - position: {line: 0, character: 3} + position: { line: 0, character: 3 } }); const labels = completions.map(c => c.label); expect(labels).toContain('sin'); @@ -104,7 +104,7 @@ describe('Language Service', () => { const doc = TextDocument.create('file://test', 'plaintext', 1, text); const completions = ls.getCompletions({ textDocument: doc, - position: {line: 0, character: 2} + position: { line: 0, character: 2 } }); const labels = completions.map(c => c.label); expect(labels).toContain('PI'); @@ -115,7 +115,7 @@ describe('Language Service', () => { const doc = TextDocument.create('file://test', 'plaintext', 1, text); const completions = ls.getCompletions({ textDocument: doc, - position: {line: 0, character: 2} + position: { line: 0, character: 2 } }); const labels = completions.map(c => c.label); expect(labels).toContain('case'); @@ -126,8 +126,8 @@ describe('Language Service', () => { const doc = TextDocument.create('file://test', 'plaintext', 1, text); const completions = ls.getCompletions({ textDocument: doc, - variables: {foo: {bar: 1}}, - position: {line: 0, character: 4} + variables: { foo: { bar: 1 } }, + position: { line: 0, character: 4 } }); expect(completions.length).toBeGreaterThan(0); const item = completions.find(c => c.label === 'foo.bar'); @@ -140,8 +140,8 @@ describe('Language Service', () => { const doc = TextDocument.create('file://test', 'plaintext', 1, text); const completions = ls.getCompletions({ textDocument: doc, - variables: {sine: 1}, - position: {line: 0, character: 2} + variables: { sine: 1 }, + position: { line: 0, character: 2 } }); const sinFunc = completions.find(c => c.label === 'sin'); @@ -161,8 +161,8 @@ describe('Language Service', () => { const doc = TextDocument.create('file://test', 'plaintext', 1, text); const completions = ls.getCompletions({ textDocument: doc, - variables: {myVar: 42}, - position: {line: 0, character: 2} + variables: { myVar: 42 }, + position: { line: 0, character: 2 } }); const varCompletion = completions.find(c => c.label === 'myVar'); @@ -176,7 +176,7 @@ describe('Language Service', () => { const doc = TextDocument.create('file://test', 'plaintext', 1, text); const completions = ls.getCompletions({ textDocument: doc, - position: {line: 0, character: 1} + position: { line: 0, character: 1 } }); const eConst = completions.find(c => c.label === 'E'); @@ -196,7 +196,7 @@ describe('Language Service', () => { boolVar: true, nullVar: null }, - position: {line: 0, character: 0} + position: { line: 0, character: 0 } }); expect(completions.find(c => c.label === 'numVar')?.detail).toBe('number'); @@ -215,7 +215,7 @@ describe('Language Service', () => { variables: { arr: [10, 20, 30] }, - position: {line: 0, character: 3} + position: { line: 0, character: 3 } }); const arrayItem = completions.find(c => c.label === 'arr[]'); @@ -234,10 +234,10 @@ describe('Language Service', () => { textDocument: doc, variables: { arr: [ - {foo: 1, bar: 2} + { foo: 1, bar: 2 } ] }, - position: {line: 0, character: text.length} + position: { line: 0, character: text.length } }); expect(completions.length).toBeGreaterThan(0); @@ -256,11 +256,11 @@ describe('Language Service', () => { variables: { matrix: [ [ - {value: 42} + { value: 42 } ] ] }, - position: {line: 0, character: text.length} + position: { line: 0, character: text.length } }); const valueItem = completions.find(c => c.label === 'matrix[0][1].value'); @@ -277,7 +277,7 @@ describe('Language Service', () => { variables: { arr: [1, 2, 3] }, - position: {line: 0, character: 3} + position: { line: 0, character: 3 } }); const arrayItem = completions.find(c => c.label === 'arr[]'); @@ -295,8 +295,8 @@ describe('Language Service', () => { const doc = TextDocument.create('file://test', 'plaintext', 1, text); const hover = ls.getHover({ textDocument: doc, - position: {line: 0, character: 1}, - variables: {foo: 42} + position: { line: 0, character: 1 }, + variables: { foo: 42 } }); const contents = getContentsValue(hover.contents); @@ -309,7 +309,7 @@ describe('Language Service', () => { const doc = TextDocument.create('file://test', 'plaintext', 1, text); const hover = ls.getHover({ textDocument: doc, - position: {line: 0, character: 1}, + position: { line: 0, character: 1 }, variables: {} }); @@ -322,7 +322,7 @@ describe('Language Service', () => { const doc = TextDocument.create('file://test', 'plaintext', 1, text); const hover = ls.getHover({ textDocument: doc, - position: {line: 0, character: 1}, + position: { line: 0, character: 1 }, variables: {} }); @@ -336,7 +336,7 @@ describe('Language Service', () => { const doc = TextDocument.create('file://test', 'plaintext', 1, text); const hover = ls.getHover({ textDocument: doc, - position: {line: 0, character: 1}, + position: { line: 0, character: 1 }, variables: {} }); @@ -349,7 +349,7 @@ describe('Language Service', () => { const doc = TextDocument.create('file://test', 'plaintext', 1, text); const hover = ls.getHover({ textDocument: doc, - position: {line: 0, character: 2}, + position: { line: 0, character: 2 }, variables: {} }); @@ -363,7 +363,7 @@ describe('Language Service', () => { const doc = TextDocument.create('file://test', 'plaintext', 1, text); const hover = ls.getHover({ textDocument: doc, - position: {line: 0, character: 1}, + position: { line: 0, character: 1 }, variables: {} }); @@ -376,7 +376,7 @@ describe('Language Service', () => { const doc = TextDocument.create('file://test', 'plaintext', 1, text); const hover = ls.getHover({ textDocument: doc, - position: {line: 0, character: 2}, + position: { line: 0, character: 2 }, variables: {} }); @@ -389,7 +389,7 @@ describe('Language Service', () => { const doc = TextDocument.create('file://test', 'plaintext', 1, text); const hover = ls.getHover({ textDocument: doc, - position: {line: 0, character: 0}, + position: { line: 0, character: 0 }, variables: {} }); @@ -401,8 +401,8 @@ describe('Language Service', () => { const doc = TextDocument.create('file://test', 'plaintext', 1, text); const hover = ls.getHover({ textDocument: doc, - position: {line: 0, character: 1}, - variables: {myFunc: 'my-variable'} + position: { line: 0, character: 1 }, + variables: { myFunc: 'my-variable' } }); const contents = getContentsValue(hover.contents); @@ -416,7 +416,7 @@ describe('Language Service', () => { const doc = TextDocument.create('file://test', 'plaintext', 1, text); const hover = ls.getHover({ textDocument: doc, - position: {line: 0, character: 1} + position: { line: 0, character: 1 } }); const contents = hover.contents as any; @@ -434,8 +434,8 @@ describe('Language Service', () => { const doc = TextDocument.create('file://test', 'plaintext', 1, text); const hover = ls.getHover({ textDocument: doc, - position: {line: 0, character: 1}, - variables: {foo: 42} + position: { line: 0, character: 1 }, + variables: { foo: 42 } }); const contents = hover.contents as any; @@ -558,13 +558,13 @@ describe('Language Service', () => { const completions = ls.getCompletions({ textDocument: doc, - position: {line: 0, character: 0} + position: { line: 0, character: 0 } }); expect(Array.isArray(completions)).toBe(true); const hover = ls.getHover({ textDocument: doc, - position: {line: 0, character: 0} + position: { line: 0, character: 0 } }); expect(hover).toBeDefined(); @@ -602,7 +602,7 @@ describe('Language Service', () => { const completions = ls.getCompletions({ textDocument: doc, - position: {line: 0, character: text.length} + position: { line: 0, character: text.length } }); expect(Array.isArray(completions)).toBe(true); }); @@ -613,7 +613,7 @@ describe('Language Service', () => { const completions = ls.getCompletions({ textDocument: doc, - position: {line: 0, character: 0} + position: { line: 0, character: 0 } }); expect(Array.isArray(completions)).toBe(true); }); @@ -630,7 +630,7 @@ describe('Language Service', () => { it('should create service with custom options', () => { const service = createLanguageService({ - operators: {'+': true, '-': false} + operators: { '+': true, '-': false } }); expect(service).toBeDefined(); }); From f1dfbde459e82f09a3fa3739d3f36be1067605a1 Mon Sep 17 00:00:00 2001 From: Melvin van Bree Date: Thu, 15 Jan 2026 12:16:21 +0100 Subject: [PATCH 5/7] Fixed properly with linter fix instead --- .editorconfig | 6 +----- src/language-service/ls-utils.ts | 2 +- src/language-service/variable-utils.ts | 2 +- test/language-service/language-service.ts | 8 ++++---- 4 files changed, 7 insertions(+), 11 deletions(-) diff --git a/.editorconfig b/.editorconfig index 19f3447..0878863 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,8 +7,4 @@ charset = utf-8 insert_final_newline = true trim_trailing_whitespace = true indent_style = space -indent_size = 2 - -[*.{js,jsx,ts,tsx}] -ij_javascript_spaces_within_object_literal_braces = true -ij_typescript_spaces_within_object_literal_braces = true \ No newline at end of file +indent_size = 2 \ No newline at end of file diff --git a/src/language-service/ls-utils.ts b/src/language-service/ls-utils.ts index ec0a05b..bcab320 100644 --- a/src/language-service/ls-utils.ts +++ b/src/language-service/ls-utils.ts @@ -20,7 +20,7 @@ export function valueTypeName(value: Value): string { export function isPathChar(ch: string): boolean { // Include square brackets to keep array selectors within the detected prefix - return /[A-Za-z0-9_$\.\[\]]/.test(ch); + return /[A-Za-z0-9_$.\[\]]/.test(ch); } export function extractPathPrefix(text: string, position: number): { start: number; prefix: string } { diff --git a/src/language-service/variable-utils.ts b/src/language-service/variable-utils.ts index a956b39..31609fe 100644 --- a/src/language-service/variable-utils.ts +++ b/src/language-service/variable-utils.ts @@ -171,7 +171,7 @@ function resolveValueByBracketPath(root: unknown, path: string): unknown { for (const seg of segments) { if (!isObj(node)) return undefined; // parse leading name and bracket chains - let i = seg.indexOf('['); + const i = seg.indexOf('['); const name = i >= 0 ? seg.slice(0, i) : seg; let rest = i >= 0 ? seg.slice(i) : ''; if (name) { diff --git a/test/language-service/language-service.ts b/test/language-service/language-service.ts index 50fa0a5..27bf590 100644 --- a/test/language-service/language-service.ts +++ b/test/language-service/language-service.ts @@ -1,7 +1,7 @@ -import {describe, it, expect, beforeEach} from 'vitest'; -import {TextDocument} from 'vscode-languageserver-textdocument'; -import {createLanguageService} from '../../src/language-service/language-service'; -import {CompletionItemKind, MarkupKind} from 'vscode-languageserver-types'; +import { describe, it, expect, beforeEach } from 'vitest'; +import { TextDocument } from 'vscode-languageserver-textdocument'; +import { createLanguageService } from '../../src/language-service/language-service'; +import { CompletionItemKind, MarkupKind } from 'vscode-languageserver-types'; function getContentsValue(contents: any): string { if (typeof contents === 'string') { From 0bdc0096edb174c5b25cb1501ad3de10b0f9722a Mon Sep 17 00:00:00 2001 From: Melvin van Bree Date: Thu, 15 Jan 2026 12:19:16 +0100 Subject: [PATCH 6/7] Raised package version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3e4fd81..696e768 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@pro-fa/expr-eval", - "version": "6.0.0", + "version": "6.0.1", "description": "Mathematical expression evaluator", "keywords": [ "expression", From 3956ef05dedbb70607723440d6257a95b7063970 Mon Sep 17 00:00:00 2001 From: Sander Toonen Date: Thu, 15 Jan 2026 14:04:31 +0100 Subject: [PATCH 7/7] Update lock file --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8cbff0a..8c136e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@pro-fa/expr-eval", - "version": "6.0.0", + "version": "6.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@pro-fa/expr-eval", - "version": "6.0.0", + "version": "6.0.1", "license": "MIT", "dependencies": { "vscode-languageserver-textdocument": "^1.0.12"