From 78a2b439cd81e134a7ebfe2cbe5a874756557551 Mon Sep 17 00:00:00 2001 From: Isak Date: Tue, 28 Oct 2025 13:31:51 +0100 Subject: [PATCH 1/7] adds schema-based completions for nodes too --- .../completionCoreCompletions.ts | 3 +- .../autocompletion/schemaBasedCompletions.ts | 112 +++++++++++++++--- 2 files changed, 99 insertions(+), 16 deletions(-) diff --git a/packages/language-support/src/autocompletion/completionCoreCompletions.ts b/packages/language-support/src/autocompletion/completionCoreCompletions.ts index 481a7160e..26b49ca56 100644 --- a/packages/language-support/src/autocompletion/completionCoreCompletions.ts +++ b/packages/language-support/src/autocompletion/completionCoreCompletions.ts @@ -44,6 +44,7 @@ import { completeRelationshipType, allLabelCompletions, allReltypeCompletions, + completeNodeLabel, } from './schemaBasedCompletions'; import { backtickIfNeeded, uniq } from './autocompletionHelpers'; @@ -705,7 +706,7 @@ export function completionCoreCompletion( } if (topExprParent === CypherParser.RULE_nodePattern) { - return allLabelCompletions(dbSchema); + return completeNodeLabel(dbSchema, parsingResult, symbolsInfo); } if (topExprParent === CypherParser.RULE_relationshipPattern) { diff --git a/packages/language-support/src/autocompletion/schemaBasedCompletions.ts b/packages/language-support/src/autocompletion/schemaBasedCompletions.ts index a58782fdd..374c556cb 100644 --- a/packages/language-support/src/autocompletion/schemaBasedCompletions.ts +++ b/packages/language-support/src/autocompletion/schemaBasedCompletions.ts @@ -10,6 +10,7 @@ import { NodePatternContext, PatternElementContext, QuantifierContext, + RelationshipPatternContext, } from '../generated-parser/CypherCmdParser'; import { ParserRuleContext } from 'antlr4'; import { backtickIfNeeded } from './autocompletionHelpers'; @@ -52,45 +53,47 @@ export const allReltypeCompletions = (dbSchema: DbSchema) => reltypesToCompletions(dbSchema.relationshipTypes); function intersectChildren( - relsFromLabels: Map>, + labelToConnectedLabelsMap: Map>, children: LabelOrCondition[], ): Set { let intersection: Set = undefined; children.forEach((c) => { intersection = intersection ? (intersection = intersection.intersection( - walkLabelTree(relsFromLabels, c), + walkLabelTree(labelToConnectedLabelsMap, c), )) - : walkLabelTree(relsFromLabels, c); + : walkLabelTree(labelToConnectedLabelsMap, c); }); return intersection ?? new Set(); } function uniteChildren( - relsFromLabels: Map>, + labelToConnectedLabelsMap: Map>, children: LabelOrCondition[], ): Set { let union: Set = new Set(); children.forEach( - (c) => (union = union.union(walkLabelTree(relsFromLabels, c))), + (c) => (union = union.union(walkLabelTree(labelToConnectedLabelsMap, c))), ); return union; } +// outgoingLabelsMap is here a map that takes a "label" = Label/ RelType +// and returns the "labels" of rels/nodes going out/in of the node/rel in the graph schema function walkLabelTree( - relsFromLabels: Map>, + labelToConnectedLabelsMap: Map>, labelTree: LabelOrCondition, ): Set { if (isLabelLeaf(labelTree)) { - return relsFromLabels.get(labelTree.value); + return labelToConnectedLabelsMap.get(labelTree.value); } else if (labelTree.andOr == 'and') { - return intersectChildren(relsFromLabels, labelTree.children); + return intersectChildren(labelToConnectedLabelsMap, labelTree.children); } else { - return uniteChildren(relsFromLabels, labelTree.children); + return uniteChildren(labelToConnectedLabelsMap, labelTree.children); } } -function getRelsFromLabelsSet(dbSchema: DbSchema): Map> { +function getRelsFromNodesSet(dbSchema: DbSchema): Map> { if (dbSchema.graphSchema) { const relsFromLabelsSet: Map> = new Map(); dbSchema.graphSchema.forEach((rel) => { @@ -112,16 +115,95 @@ function getRelsFromLabelsSet(dbSchema: DbSchema): Map> { return undefined; } -export function completeRelationshipType( +function getNodesFromRelsSet(dbSchema: DbSchema): Map> { + if (dbSchema.graphSchema) { + const nodesFromRelsSet: Map> = new Map(); + dbSchema.graphSchema.forEach((rel) => { + let currentRelEntry = nodesFromRelsSet.get(rel.relType); + if (!currentRelEntry) { + nodesFromRelsSet.set(rel.relType, new Set()); + currentRelEntry = nodesFromRelsSet.get(rel.relType); + } + currentRelEntry.add(rel.to); + currentRelEntry.add(rel.from); + }); + return nodesFromRelsSet; + } + return undefined; +} + +export function completeNodeLabel( dbSchema: DbSchema, parsingResult: ParsedStatement, symbolsInfo: SymbolsInfo, ): CompletionItem[] { - if (!_internalFeatureFlags.schemaBasedPatternCompletions) { - return allReltypeCompletions(dbSchema); + if ( + !_internalFeatureFlags.schemaBasedPatternCompletions || + dbSchema.graphSchema === undefined + ) { + return allLabelCompletions(dbSchema); } - if (dbSchema.graphSchema === undefined) { + const callContext = findParent( + parsingResult.lastRule.parentCtx, + (x) => x instanceof PatternElementContext, + ); + + if (callContext instanceof PatternElementContext) { + const lastValidElement = callContext.children.toReversed().find((child) => { + if (child instanceof RelationshipPatternContext) { + //For some reason this null check doesnt seem to work the same on nodes -> old check gets current broken node as "lastValid" + if (child.exception === null) { + return true; + } + } + }); + + // limitation: bailing out on quantifiers + if (lastValidElement instanceof QuantifierContext) { + return allLabelCompletions(dbSchema); + } + + if (lastValidElement instanceof RelationshipPatternContext) { + // limitation: not checking anonymous variables + const variable = lastValidElement.variable(); + if (variable === null) { + return allLabelCompletions(dbSchema); + } + + const foundVariable = symbolsInfo?.symbolTables + ?.flat() + .find((entry) => entry.references.includes(variable.start.start)); + + if ( + foundVariable === undefined || + ('children' in foundVariable.labels && + foundVariable.labels.children.length == 0) + ) { + return allLabelCompletions(dbSchema); + } + + // limitation: not direction-aware (ignores <- vs ->) + // limitation: not checking relationship variable reuse + const nodesFromRelsSet = getNodesFromRelsSet(dbSchema); + const rels = walkLabelTree(nodesFromRelsSet, foundVariable.labels); + + return labelsToCompletions(Array.from(rels)); + } + } + + return allLabelCompletions(dbSchema); +} + +export function completeRelationshipType( + dbSchema: DbSchema, + parsingResult: ParsedStatement, + symbolsInfo: SymbolsInfo, +): CompletionItem[] { + if ( + !_internalFeatureFlags.schemaBasedPatternCompletions || + dbSchema.graphSchema === undefined + ) { return allReltypeCompletions(dbSchema); } @@ -169,7 +251,7 @@ export function completeRelationshipType( // limitation: not direction-aware (ignores <- vs ->) // limitation: not checking relationship variable reuse - const relsFromLabelsSet = getRelsFromLabelsSet(dbSchema); + const relsFromLabelsSet = getRelsFromNodesSet(dbSchema); const rels = walkLabelTree(relsFromLabelsSet, foundVariable.labels); return reltypesToCompletions(Array.from(rels)); From c540c5bd0c0754676b18a7d2b3750bd4b5995e41 Mon Sep 17 00:00:00 2001 From: Isak Date: Tue, 28 Oct 2025 14:36:05 +0100 Subject: [PATCH 2/7] adds cursor pos handling, and tests for node SB completions --- .../autocompletion/schemaBasedCompletions.ts | 11 +- .../schemaBasedPatternCompletion.test.ts | 125 ++++++++++++++---- 2 files changed, 104 insertions(+), 32 deletions(-) diff --git a/packages/language-support/src/autocompletion/schemaBasedCompletions.ts b/packages/language-support/src/autocompletion/schemaBasedCompletions.ts index 374c556cb..ae98f0d39 100644 --- a/packages/language-support/src/autocompletion/schemaBasedCompletions.ts +++ b/packages/language-support/src/autocompletion/schemaBasedCompletions.ts @@ -12,7 +12,6 @@ import { QuantifierContext, RelationshipPatternContext, } from '../generated-parser/CypherCmdParser'; -import { ParserRuleContext } from 'antlr4'; import { backtickIfNeeded } from './autocompletionHelpers'; import { _internalFeatureFlags } from '../featureFlags'; @@ -151,7 +150,10 @@ export function completeNodeLabel( if (callContext instanceof PatternElementContext) { const lastValidElement = callContext.children.toReversed().find((child) => { - if (child instanceof RelationshipPatternContext) { + if ( + child instanceof RelationshipPatternContext && + child.stop.stop <= parsingResult.lastRule.stop.stop + ) { //For some reason this null check doesnt seem to work the same on nodes -> old check gets current broken node as "lastValid" if (child.exception === null) { return true; @@ -218,7 +220,10 @@ export function completeRelationshipType( const lastValidElement = patternContext.children .toReversed() .find((child) => { - if (child instanceof ParserRuleContext) { + if ( + child instanceof NodePatternContext && + child.stop.stop <= parsingResult.lastRule.stop.stop + ) { if (child.exception === null) { return true; } diff --git a/packages/language-support/src/tests/autocompletion/schemaBasedPatternCompletion.test.ts b/packages/language-support/src/tests/autocompletion/schemaBasedPatternCompletion.test.ts index f2707b477..2ac4941ed 100644 --- a/packages/language-support/src/tests/autocompletion/schemaBasedPatternCompletion.test.ts +++ b/packages/language-support/src/tests/autocompletion/schemaBasedPatternCompletion.test.ts @@ -64,6 +64,45 @@ describe('completeRelationshipType', () => { }); }); + test('Simple node completion pattern', () => { + const query = 'MATCH (t:Trainer)-[r:CATCHES]->(:'; + + testCompletions({ + query, + dbSchema, + computeSymbolsInfo: true, + expected: [ + { label: 'Pokemon', kind: CompletionItemKind.TypeParameter }, + //Limitation: does not handle direction + { label: 'Trainer', kind: CompletionItemKind.TypeParameter }, + ], + excluded: [ + { label: 'Gym', kind: CompletionItemKind.TypeParameter }, + { label: 'Type', kind: CompletionItemKind.TypeParameter }, + ], + }); + }); + + test('Simple node completion pattern with WHERE', () => { + const query = 'MATCH (n)-[r]->(m) WHERE r:IS_IN MATCH (n)-[r]->(:'; + + testCompletions({ + query, + dbSchema, + computeSymbolsInfo: true, + expected: [ + { label: 'Region', kind: CompletionItemKind.TypeParameter }, + //Limitation: does not handle direction + { label: 'Gym', kind: CompletionItemKind.TypeParameter }, + { label: 'Trainer', kind: CompletionItemKind.TypeParameter }, + ], + excluded: [ + { label: 'Pokemon', kind: CompletionItemKind.TypeParameter }, + { label: 'Type', kind: CompletionItemKind.TypeParameter }, + ], + }); + }); + test('Simple pattern 1', () => { const query = 'MATCH (t:Trainer)-[r:'; @@ -186,6 +225,29 @@ WHERE EXISTS { }); }); + test('Node completion works in pattern expression ', () => { + const query = `MATCH (p:Pokemon)-[r:KNOWS]->(m) +WHERE EXISTS { + (p)-[r]->(:`; + + testCompletions({ + query, + dbSchema, + computeSymbolsInfo: true, + expected: [ + { label: 'Move', kind: CompletionItemKind.TypeParameter }, + //Limitation: does not handle direction + { label: 'Pokemon', kind: CompletionItemKind.TypeParameter }, + ], + excluded: [ + { label: 'Gym', kind: CompletionItemKind.TypeParameter }, + { label: 'Trainer', kind: CompletionItemKind.TypeParameter }, + { label: 'Type', kind: CompletionItemKind.TypeParameter }, + { label: 'Region', kind: CompletionItemKind.TypeParameter }, + ], + }); + }); + test('Works in pattern comprehension ', () => { const query = `MATCH (p:Pokemon) RETURN [(p)-[:`; @@ -315,7 +377,7 @@ RETURN [(p)-[:`; }); }); - test('Handles AND logic with self-referencing ', () => { + test('Handles AND logic with self-referencing', () => { const query = 'MATCH (x) WHERE x:Pokemon OR x:Type MATCH (x)-[:'; testCompletions({ query, @@ -450,26 +512,6 @@ RETURN [(p)-[:`; }); }); - test('Limitation: Does not handle mid query cursor', () => { - const beforeCursor = 'MATCH (p:Pokemon)-[r:'; - const query = beforeCursor + ']-(t:Trainer)--(u:UnrelatedLabel)'; - - testCompletions({ - query, - dbSchema, - computeSymbolsInfo: true, - offset: beforeCursor.length, - expected: [ - // always takes the latest finished node, rather than the correct one - { label: 'UNRELATED_RELTYPE', kind: CompletionItemKind.TypeParameter }, - ], - excluded: [ - { label: 'KNOWS', kind: CompletionItemKind.TypeParameter }, - { label: 'WEAK_TO', kind: CompletionItemKind.TypeParameter }, - ], - }); - }); - test('Limitation: Does not handle direction-aware completions ', () => { const beforeCursor = 'MATCH (p:Pokemon)-[r:'; const query = beforeCursor + ']->(t:Trainer)'; @@ -483,10 +525,10 @@ RETURN [(p)-[:`; // all should be excluded as there is no relationship from pokemon to trainer { label: 'CATCHES', kind: CompletionItemKind.TypeParameter }, { label: 'TRAINS', kind: CompletionItemKind.TypeParameter }, - ], - excluded: [ { label: 'KNOWS', kind: CompletionItemKind.TypeParameter }, { label: 'WEAK_TO', kind: CompletionItemKind.TypeParameter }, + ], + excluded: [ { label: 'UNRELATED_RELTYPE', kind: CompletionItemKind.TypeParameter }, ], }); @@ -512,28 +554,53 @@ RETURN [(p)-[:`; }); }); - test('Handles cursor position', () => { - const query = 'MATCH (t1:Trainer)-[r1:CATCHES]-(p1:Pokemon)-[r2:'; + test('Handles cursor position for rel completion', () => { + const beforeCursor = 'MATCH (t1:Trainer)-[r1:'; + const query = beforeCursor + 'CATCHES]-(p1:Pokemon)-[r2:'; testCompletions({ query, dbSchema, computeSymbolsInfo: true, - offset: 'MATCH (t1:Trainer)-[r1:'.length, + offset: beforeCursor.length, expected: [ { label: 'CATCHES', kind: CompletionItemKind.TypeParameter }, { label: 'TRAINS', kind: CompletionItemKind.TypeParameter }, + { label: 'IS_IN', kind: CompletionItemKind.TypeParameter }, + //Limitation: Only take preceding node into account + { label: 'BATTLES', kind: CompletionItemKind.TypeParameter }, ], excluded: [ { label: 'UNRELATED_RELTYPE', kind: CompletionItemKind.TypeParameter }, - // there's no outoing knows from trainer - // { label: 'KNOWS', kind: CompletionItemKind.TypeParameter }, - // { label: 'WEAK_TO', kind: CompletionItemKind.TypeParameter }, + // there's no in/outoing knows from trainer + { label: 'KNOWS', kind: CompletionItemKind.TypeParameter }, + { label: 'WEAK_TO', kind: CompletionItemKind.TypeParameter }, // there's no BATTLE between trainer and pokemon // { label: 'BATTLES', kind: CompletionItemKind.TypeParameter }, ], }); }); + + test('Handles cursor position for node completion', () => { + const beforeCursor = 'MATCH (t1:Trainer)-[r1:CATCHES]-(p1:'; + const query = beforeCursor + 'Pokemon)-[r2:IS_IN]->(:'; + + testCompletions({ + query, + dbSchema, + computeSymbolsInfo: true, + offset: beforeCursor.length, + expected: [ + { label: 'Pokemon', kind: CompletionItemKind.TypeParameter }, + //limitation: direction + { label: 'Trainer', kind: CompletionItemKind.TypeParameter }, + ], + excluded: [ + { label: 'Region', kind: CompletionItemKind.TypeParameter }, + { label: 'Gym', kind: CompletionItemKind.TypeParameter }, + ], + }); + }); }); From 8021db9d26e8b00ca93584ffa803e642442cde1b Mon Sep 17 00:00:00 2001 From: Isak Date: Tue, 28 Oct 2025 16:51:21 +0100 Subject: [PATCH 3/7] removes check not needed when slicing, slices in testing too --- .../src/autocompletion/schemaBasedCompletions.ts | 11 ++--------- .../autocompletion/completionAssertionHelpers.ts | 3 +++ 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/packages/language-support/src/autocompletion/schemaBasedCompletions.ts b/packages/language-support/src/autocompletion/schemaBasedCompletions.ts index 31c0edf80..d7f171430 100644 --- a/packages/language-support/src/autocompletion/schemaBasedCompletions.ts +++ b/packages/language-support/src/autocompletion/schemaBasedCompletions.ts @@ -150,11 +150,7 @@ export function completeNodeLabel( if (callContext instanceof PatternElementContext) { const lastValidElement = callContext.children.toReversed().find((child) => { - if ( - child instanceof RelationshipPatternContext && - child.stop.stop <= parsingResult.stopNode.stop.stop - ) { - //For some reason this null check doesnt seem to work the same on nodes -> old check gets current broken node as "lastValid" + if (child instanceof RelationshipPatternContext) { if (child.exception === null) { return true; } @@ -220,10 +216,7 @@ export function completeRelationshipType( const lastValidElement = patternContext.children .toReversed() .find((child) => { - if ( - child instanceof NodePatternContext && - child.stop.stop <= parsingResult.stopNode.stop.stop - ) { + if (child instanceof NodePatternContext) { if (child.exception === null) { return true; } diff --git a/packages/language-support/src/tests/autocompletion/completionAssertionHelpers.ts b/packages/language-support/src/tests/autocompletion/completionAssertionHelpers.ts index 33e1d3a3a..67026e87a 100644 --- a/packages/language-support/src/tests/autocompletion/completionAssertionHelpers.ts +++ b/packages/language-support/src/tests/autocompletion/completionAssertionHelpers.ts @@ -38,6 +38,9 @@ export function testCompletions({ manualTrigger?: boolean; computeSymbolsInfo?: boolean; }) { + if (query.length > offset) { + query = query.slice(0, offset); + } if (computeSymbolsInfo) { const result = lintCypherQuery(query, dbSchema); parserWrapper.setSymbolsInfo({ From d08fed114c59f96e277c8d7d3104add474736c71 Mon Sep 17 00:00:00 2001 From: Isak Date: Tue, 4 Nov 2025 15:15:11 +0100 Subject: [PATCH 4/7] remove (extra) slice in testing now that we do it in shared autocompletion method --- .../src/tests/autocompletion/completionAssertionHelpers.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/language-support/src/tests/autocompletion/completionAssertionHelpers.ts b/packages/language-support/src/tests/autocompletion/completionAssertionHelpers.ts index 67026e87a..33e1d3a3a 100644 --- a/packages/language-support/src/tests/autocompletion/completionAssertionHelpers.ts +++ b/packages/language-support/src/tests/autocompletion/completionAssertionHelpers.ts @@ -38,9 +38,6 @@ export function testCompletions({ manualTrigger?: boolean; computeSymbolsInfo?: boolean; }) { - if (query.length > offset) { - query = query.slice(0, offset); - } if (computeSymbolsInfo) { const result = lintCypherQuery(query, dbSchema); parserWrapper.setSymbolsInfo({ From 9c0afcd6d16f479f81165fcc9c60114b8a2abb31 Mon Sep 17 00:00:00 2001 From: Isak Date: Tue, 4 Nov 2025 15:46:58 +0100 Subject: [PATCH 5/7] minor cleanup --- .../autocompletion/schemaBasedCompletions.ts | 29 +++++++++---------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/packages/language-support/src/autocompletion/schemaBasedCompletions.ts b/packages/language-support/src/autocompletion/schemaBasedCompletions.ts index d7f171430..a6f045e3d 100644 --- a/packages/language-support/src/autocompletion/schemaBasedCompletions.ts +++ b/packages/language-support/src/autocompletion/schemaBasedCompletions.ts @@ -52,43 +52,41 @@ export const allReltypeCompletions = (dbSchema: DbSchema) => reltypesToCompletions(dbSchema.relationshipTypes); function intersectChildren( - labelToConnectedLabelsMap: Map>, + connectedLabels: Map>, children: LabelOrCondition[], ): Set { let intersection: Set = undefined; children.forEach((c) => { intersection = intersection ? (intersection = intersection.intersection( - walkLabelTree(labelToConnectedLabelsMap, c), + walkLabelTree(connectedLabels, c), )) - : walkLabelTree(labelToConnectedLabelsMap, c); + : walkLabelTree(connectedLabels, c); }); return intersection ?? new Set(); } function uniteChildren( - labelToConnectedLabelsMap: Map>, + connectedLabels: Map>, children: LabelOrCondition[], ): Set { let union: Set = new Set(); children.forEach( - (c) => (union = union.union(walkLabelTree(labelToConnectedLabelsMap, c))), + (c) => (union = union.union(walkLabelTree(connectedLabels, c))), ); return union; } -// outgoingLabelsMap is here a map that takes a "label" = Label/ RelType -// and returns the "labels" of rels/nodes going out/in of the node/rel in the graph schema function walkLabelTree( - labelToConnectedLabelsMap: Map>, + connectedLabels: Map>, labelTree: LabelOrCondition, ): Set { if (isLabelLeaf(labelTree)) { - return labelToConnectedLabelsMap.get(labelTree.value); + return connectedLabels.get(labelTree.value); } else if (labelTree.andOr == 'and') { - return intersectChildren(labelToConnectedLabelsMap, labelTree.children); + return intersectChildren(connectedLabels, labelTree.children); } else { - return uniteChildren(labelToConnectedLabelsMap, labelTree.children); + return uniteChildren(connectedLabels, labelTree.children); } } @@ -118,11 +116,10 @@ function getNodesFromRelsSet(dbSchema: DbSchema): Map> { if (dbSchema.graphSchema) { const nodesFromRelsSet: Map> = new Map(); dbSchema.graphSchema.forEach((rel) => { - let currentRelEntry = nodesFromRelsSet.get(rel.relType); - if (!currentRelEntry) { + if (!nodesFromRelsSet.has(rel.relType)) { nodesFromRelsSet.set(rel.relType, new Set()); - currentRelEntry = nodesFromRelsSet.get(rel.relType); } + const currentRelEntry = nodesFromRelsSet.get(rel.relType); currentRelEntry.add(rel.to); currentRelEntry.add(rel.from); }); @@ -182,7 +179,7 @@ export function completeNodeLabel( } // limitation: not direction-aware (ignores <- vs ->) - // limitation: not checking relationship variable reuse + // limitation: not checking node label repetition const nodesFromRelsSet = getNodesFromRelsSet(dbSchema); const rels = walkLabelTree(nodesFromRelsSet, foundVariable.labels); @@ -248,7 +245,7 @@ export function completeRelationshipType( } // limitation: not direction-aware (ignores <- vs ->) - // limitation: not checking relationship variable reuse + // limitation: not checking relationship type repetition const relsFromLabelsSet = getRelsFromNodesSet(dbSchema); const rels = walkLabelTree(relsFromLabelsSet, foundVariable.labels); From 501e94d6b92da6a484369ad5fa2257cd53fb75f7 Mon Sep 17 00:00:00 2001 From: Isak Date: Tue, 4 Nov 2025 16:47:37 +0100 Subject: [PATCH 6/7] expanded testing --- .../schemaBasedPatternCompletion.test.ts | 146 +++++++++++++++++- 1 file changed, 140 insertions(+), 6 deletions(-) diff --git a/packages/language-support/src/tests/autocompletion/schemaBasedPatternCompletion.test.ts b/packages/language-support/src/tests/autocompletion/schemaBasedPatternCompletion.test.ts index ac84ca2be..12d5bc993 100644 --- a/packages/language-support/src/tests/autocompletion/schemaBasedPatternCompletion.test.ts +++ b/packages/language-support/src/tests/autocompletion/schemaBasedPatternCompletion.test.ts @@ -64,7 +64,7 @@ describe('completeRelationshipType', () => { }); }); - test('Simple node completion pattern', () => { + test('Simple node completion pattern 1', () => { const query = 'MATCH (t:Trainer)-[r:CATCHES]->(:'; testCompletions({ @@ -83,6 +83,28 @@ describe('completeRelationshipType', () => { }); }); + test('Simple node completion pattern 2', () => { + const query = 'MATCH (g:Gym)<-[r:CHALLENGES]-(:'; + + testCompletions({ + query, + dbSchema, + computeSymbolsInfo: true, + expected: [ + { label: 'Pokemon', kind: CompletionItemKind.TypeParameter }, + { label: 'Trainer', kind: CompletionItemKind.TypeParameter }, + //Limitation: does not handle direciton + { label: 'Gym', kind: CompletionItemKind.TypeParameter }, + ], + excluded: [ + { label: 'Region', kind: CompletionItemKind.TypeParameter }, + { label: 'Type', kind: CompletionItemKind.TypeParameter }, + { label: 'Move', kind: CompletionItemKind.TypeParameter }, + { label: 'UnrelatedLabel', kind: CompletionItemKind.TypeParameter }, + ], + }); + }); + test('Simple node completion pattern with WHERE', () => { const query = 'MATCH (n)-[r]->(m) WHERE r:IS_IN MATCH (n)-[r]->(:'; @@ -103,7 +125,7 @@ describe('completeRelationshipType', () => { }); }); - test('Simple pattern 1', () => { + test('Simple rel completion pattern 1', () => { const query = 'MATCH (t:Trainer)-[r:'; testCompletions({ @@ -123,7 +145,7 @@ describe('completeRelationshipType', () => { }); }); - test('Simple pattern 2', () => { + test('Simple rel completion pattern 2', () => { const query = 'MATCH (p:Pokemon)-[r:'; testCompletions({ @@ -143,7 +165,7 @@ describe('completeRelationshipType', () => { }); }); - test('Longer path pattern', () => { + test('Longer relationship path pattern', () => { const query = 'MATCH (t:Trainer)-[:CATCHES]->(p:Pokemon)-[r:'; testCompletions({ @@ -163,6 +185,50 @@ describe('completeRelationshipType', () => { }); }); + test('Longer node path pattern', () => { + const query = 'MATCH (t:Trainer)-[:CATCHES]->(p:Pokemon)-[r:WEAK_TO]->(:'; + + testCompletions({ + query, + dbSchema, + computeSymbolsInfo: true, + expected: [ + { label: 'Type', kind: CompletionItemKind.TypeParameter }, + //Limitation: Does not handle direction + { label: 'Pokemon', kind: CompletionItemKind.TypeParameter }, + ], + excluded: [ + { label: 'UnrelatedLabel', kind: CompletionItemKind.TypeParameter }, + { label: 'Trainer', kind: CompletionItemKind.TypeParameter }, + { label: 'Gym', kind: CompletionItemKind.TypeParameter }, + { label: 'Region', kind: CompletionItemKind.TypeParameter }, + { label: 'Move', kind: CompletionItemKind.TypeParameter }, + ], + }); + }); + + test('Handles variables overlapping from other statements', () => { + const query = `MATCH (p:Trainer) RETURN p; + MATCH (p:Gym) RETURN p; + MATCH (p:Pokemon)-[r:`; + + testCompletions({ + query, + dbSchema, + computeSymbolsInfo: true, + expected: [ + { label: 'KNOWS', kind: CompletionItemKind.TypeParameter }, + { label: 'WEAK_TO', kind: CompletionItemKind.TypeParameter }, + { label: 'CATCHES', kind: CompletionItemKind.TypeParameter }, + { label: 'TRAINS', kind: CompletionItemKind.TypeParameter }, + ], + excluded: [ + { label: 'BATTLES', kind: CompletionItemKind.TypeParameter }, + { label: 'UNRELATED_RELTYPE', kind: CompletionItemKind.TypeParameter }, + ], + }); + }); + test('Handles scope', () => { const query = `MATCH (t:Trainer) CALL () { @@ -459,7 +525,7 @@ RETURN [(p)-[:`; }); }); - test('Limitation: Does not handle anonymous variables as context ', () => { + test('Limitation: Does not handle anonymous variables as context - relationship completion ', () => { const query = 'MATCH (:Trainer)-[r:'; testCompletions({ @@ -476,6 +542,28 @@ RETURN [(p)-[:`; }); }); + test('Limitation: Does not handle anonymous variables as context - node completion', () => { + const query = 'MATCH (:Trainer)-[:TRAINS]->(:'; + + testCompletions({ + query, + dbSchema, + computeSymbolsInfo: true, + expected: [ + { label: 'Pokemon', kind: CompletionItemKind.TypeParameter }, + //Limitation - does not handle direction + { label: 'Trainer', kind: CompletionItemKind.TypeParameter }, + //Limitation - bails on anon + { label: 'Gym', kind: CompletionItemKind.TypeParameter }, + { label: 'Region', kind: CompletionItemKind.TypeParameter }, + { label: 'Type', kind: CompletionItemKind.TypeParameter }, + { label: 'Move', kind: CompletionItemKind.TypeParameter }, + { label: 'UnrelatedLabel', kind: CompletionItemKind.TypeParameter }, + { label: 'Unconnected', kind: CompletionItemKind.TypeParameter }, + ], + }); + }); + test('Limitation: Does not properly handle quantifiers ', () => { const query = 'MATCH (t:Trainer)-[r:CATCHES*1..3]->(p:Pokemon)-[r2:'; @@ -512,7 +600,7 @@ RETURN [(p)-[:`; }); }); - test('Limitation: Does not handle direction-aware completions ', () => { + test('Limitation: Does not handle direction-aware completions with context after caret ', () => { const beforeCursor = 'MATCH (p:Pokemon)-[r:'; const query = beforeCursor + ']->(t:Trainer)'; @@ -555,6 +643,52 @@ RETURN [(p)-[:`; }); }); + test('Limitation: Does not deduplicate existing node labels in simple "|" pattern ', () => { + const query = 'MATCH (g)<-[r:CHALLENGES]-(p:Pokemon|Gym|'; + + testCompletions({ + query, + dbSchema, + computeSymbolsInfo: true, + expected: [ + { label: 'Trainer', kind: CompletionItemKind.TypeParameter }, + // below should be excluded + { label: 'Pokemon', kind: CompletionItemKind.TypeParameter }, + //Limitation - does not handle direction + { label: 'Gym', kind: CompletionItemKind.TypeParameter }, + ], + excluded: [ + { label: 'Type', kind: CompletionItemKind.TypeParameter }, + { label: 'Region', kind: CompletionItemKind.TypeParameter }, + { label: 'Move', kind: CompletionItemKind.TypeParameter }, + { label: 'UnrelatedLabel', kind: CompletionItemKind.TypeParameter }, + ], + }); + }); + + test('Limitation: Does not deduplicate existing node labels in simple "&" pattern ', () => { + const query = 'MATCH (g)<-[r:CHALLENGES]-(p:Pokemon&Gym&'; + + testCompletions({ + query, + dbSchema, + computeSymbolsInfo: true, + expected: [ + { label: 'Trainer', kind: CompletionItemKind.TypeParameter }, + // below should be excluded + { label: 'Pokemon', kind: CompletionItemKind.TypeParameter }, + //Limitation - does not handle direction + { label: 'Gym', kind: CompletionItemKind.TypeParameter }, + ], + excluded: [ + { label: 'Type', kind: CompletionItemKind.TypeParameter }, + { label: 'Region', kind: CompletionItemKind.TypeParameter }, + { label: 'Move', kind: CompletionItemKind.TypeParameter }, + { label: 'UnrelatedLabel', kind: CompletionItemKind.TypeParameter }, + ], + }); + }); + test('Handles cursor position for rel completion', () => { const beforeCursor = 'MATCH (t1:Trainer)-[r1:'; const query = beforeCursor + 'CATCHES]-(p1:Pokemon)-[r2:'; From e18ee73929762420bb6018f9d3a0e794711d8f15 Mon Sep 17 00:00:00 2001 From: Isak Date: Wed, 5 Nov 2025 15:27:42 +0100 Subject: [PATCH 7/7] adds tests using undirected rels --- .../schemaBasedPatternCompletion.test.ts | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/packages/language-support/src/tests/autocompletion/schemaBasedPatternCompletion.test.ts b/packages/language-support/src/tests/autocompletion/schemaBasedPatternCompletion.test.ts index 12d5bc993..d4759ab42 100644 --- a/packages/language-support/src/tests/autocompletion/schemaBasedPatternCompletion.test.ts +++ b/packages/language-support/src/tests/autocompletion/schemaBasedPatternCompletion.test.ts @@ -462,6 +462,45 @@ RETURN [(p)-[:`; }); }); + test('Handles undirected rels for node completions', () => { + const query = 'MATCH (p:Pokemon)-[r:KNOWS]-(:'; + testCompletions({ + query, + dbSchema, + computeSymbolsInfo: true, + expected: [ + { label: 'Move', kind: CompletionItemKind.TypeParameter }, + { label: 'Pokemon', kind: CompletionItemKind.TypeParameter }, + ], + excluded: [ + { label: 'Trainer', kind: CompletionItemKind.TypeParameter }, + { label: 'Region', kind: CompletionItemKind.TypeParameter }, + { label: 'Type', kind: CompletionItemKind.TypeParameter }, + { label: 'Gym', kind: CompletionItemKind.TypeParameter }, + ], + }); + }); + + test('Handles undirected rels for rel completions', () => { + const query = + 'MATCH (p:Pokemon)-[r:CHALLENGES]-(m:Gym)-[r2:IS_IN]->(reg:Region)-[:'; + testCompletions({ + query, + dbSchema, + computeSymbolsInfo: true, + expected: [{ label: 'IS_IN', kind: CompletionItemKind.TypeParameter }], + excluded: [ + { label: 'UNRELATED_RELTYPE', kind: CompletionItemKind.TypeParameter }, + { label: 'BATTLES', kind: CompletionItemKind.TypeParameter }, + { label: 'WEAK_TO', kind: CompletionItemKind.TypeParameter }, + { label: 'STRONG_AGAINST', kind: CompletionItemKind.TypeParameter }, + { label: 'CATCHES', kind: CompletionItemKind.TypeParameter }, + { label: 'TRAINS', kind: CompletionItemKind.TypeParameter }, + { label: 'CHALLENGES', kind: CompletionItemKind.TypeParameter }, + ], + }); + }); + test('Limitation: Does not handle union types, as they are not yet supported in symbol table ', () => { const query = 'MATCH (x:Pokemon|Trainer)-[r:';