Skip to content

Commit e158c38

Browse files
authored
Add path segment completions (#641)
1 parent bfdc70e commit e158c38

9 files changed

Lines changed: 726 additions & 211 deletions

File tree

packages/language-server/src/server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ connection.onInitialize(() => {
154154
// Tell the client what features does the server support
155155
completionProvider: {
156156
resolveProvider: false,
157-
triggerCharacters: ['.', ':', '{', '$', ')', ' '],
157+
triggerCharacters: ['.', ':', '{', '$', ')', ' ', ']'],
158158
},
159159
semanticTokensProvider: {
160160
documentSelector: [{ language: 'cypher' }],

packages/language-support/src/autocompletion/completionCoreCompletions.ts

Lines changed: 72 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import {
22
CompletionItemKind,
33
CompletionItemTag,
4-
InsertTextFormat,
54
} from 'vscode-languageserver-types';
65
import { DbSchema } from '../dbSchema';
76
import CypherLexer from '../generated-parser/CypherCmdLexer';
87
import CypherParser, {
98
CallClauseContext,
109
Expression2Context,
10+
NodePatternContext,
11+
RelationshipPatternContext,
1112
} from '../generated-parser/CypherCmdParser';
1213
import {
1314
findParent,
@@ -43,8 +44,10 @@ import {
4344
import {
4445
completeRelationshipType,
4546
allLabelCompletions,
46-
allReltypeCompletions,
4747
completeNodeLabel,
48+
getPathCompletions,
49+
getShortPathCompletions,
50+
allReltypeCompletions,
4851
} from './schemaBasedCompletions';
4952
import { backtickIfNeeded, uniq } from './autocompletionHelpers';
5053

@@ -505,11 +508,14 @@ export function completionCoreCompletion(
505508
CypherParser.RULE_propertyKeyName,
506509
CypherParser.RULE_variable,
507510
CypherParser.RULE_leftArrow,
511+
CypherParser.RULE_arrowLine,
508512
// this rule is used for usernames and roles.
509513
CypherParser.RULE_commandNameExpression,
510514
CypherParser.RULE_procedureResultItem,
511515
CypherParser.RULE_cypherVersion,
512516
CypherParser.RULE_cypher,
517+
CypherParser.RULE_labelType,
518+
CypherParser.RULE_relType,
513519

514520
// Either enable the helper rules for lexer clashes,
515521
// or collect all console commands like below with symbolicNameString
@@ -689,6 +695,29 @@ export function completionCoreCompletion(
689695
parsingResult,
690696
});
691697
}
698+
if (
699+
ruleNumber === CypherParser.RULE_symbolicNameString &&
700+
candidateRule.ruleList.at(-1) ===
701+
CypherParser.RULE_multiLabelNodePattern
702+
) {
703+
return completeNodeLabel(dbSchema, parsingResult, symbolsInfo);
704+
}
705+
706+
if (
707+
ruleNumber === CypherParser.RULE_symbolicNameString &&
708+
candidateRule.ruleList.at(-1) ===
709+
CypherParser.RULE_multiRelTypeRelPattern
710+
) {
711+
return completeRelationshipType(dbSchema, parsingResult, symbolsInfo);
712+
}
713+
714+
if (ruleNumber === CypherParser.RULE_labelType) {
715+
return completeNodeLabel(dbSchema, parsingResult, symbolsInfo);
716+
}
717+
718+
if (ruleNumber === CypherParser.RULE_relType) {
719+
return completeRelationshipType(dbSchema, parsingResult, symbolsInfo);
720+
}
692721

693722
if (ruleNumber === CypherParser.RULE_labelExpression1) {
694723
const topExprIndex = candidateRule.ruleList.indexOf(
@@ -736,36 +765,43 @@ export function completionCoreCompletion(
736765
return [{ label: 'write', kind: CompletionItemKind.Event }];
737766
}
738767

768+
if (ruleNumber === CypherParser.RULE_arrowLine) {
769+
const possiblePatternElementParent =
770+
parsingResult.stopNode?.parentCtx?.parentCtx;
771+
const lastNode: RelationshipPatternContext | NodePatternContext =
772+
possiblePatternElementParent?.children.findLast(
773+
(x) =>
774+
(x instanceof RelationshipPatternContext ||
775+
x instanceof NodePatternContext) &&
776+
x.exception === null,
777+
) as RelationshipPatternContext | NodePatternContext;
778+
779+
const shortPathCompletions: CompletionItem[] =
780+
lastNode instanceof RelationshipPatternContext
781+
? getShortPathCompletions(lastNode, symbolsInfo, dbSchema)
782+
: [];
783+
return shortPathCompletions;
784+
}
785+
739786
if (ruleNumber === CypherParser.RULE_leftArrow) {
740-
return [
741-
{
742-
label: '-[]->()',
743-
kind: CompletionItemKind.Snippet,
744-
insertTextFormat: InsertTextFormat.Snippet,
745-
insertText: '-[${1: }]->(${2: })',
746-
detail: 'path template',
747-
// vscode does not call the completion provider for every single character
748-
// after the second character is typed (i.e) `MATCH ()-[` the completion is no longer valid
749-
// it'd insert `MATCH ()-[[]->()` which is not valid. Hence we filter it out by using the filterText
750-
filterText: '',
751-
},
752-
{
753-
label: '-[]-()',
754-
kind: CompletionItemKind.Snippet,
755-
insertTextFormat: InsertTextFormat.Snippet,
756-
insertText: '-[${1: }]-(${2: })',
757-
detail: 'path template',
758-
filterText: '',
759-
},
760-
{
761-
label: '<-[]-()',
762-
kind: CompletionItemKind.Snippet,
763-
insertTextFormat: InsertTextFormat.Snippet,
764-
insertText: '<-[${1: }]-(${2: })',
765-
detail: 'path template',
766-
filterText: '',
767-
},
768-
];
787+
const possiblePatternElementParent =
788+
parsingResult?.stopNode?.parentCtx?.parentCtx;
789+
if (possiblePatternElementParent) {
790+
const lastNodePattern = possiblePatternElementParent.children
791+
.toReversed()
792+
.find((child) => {
793+
if (child instanceof NodePatternContext) {
794+
if (child.exception === null) {
795+
return true;
796+
}
797+
}
798+
});
799+
const availablePaths =
800+
lastNodePattern instanceof NodePatternContext
801+
? getPathCompletions(lastNodePattern, symbolsInfo, dbSchema)
802+
: [];
803+
return availablePaths;
804+
}
769805
}
770806

771807
return [];
@@ -774,7 +810,11 @@ export function completionCoreCompletion(
774810

775811
// if the completion was automatically triggered by a snippet trigger character
776812
// we should only return snippet completions
777-
if (CypherLexer.RPAREN === previousToken?.type && !manualTrigger) {
813+
if (
814+
(CypherLexer.RPAREN === previousToken?.type ||
815+
CypherLexer.RBRACKET === previousToken?.type) &&
816+
!manualTrigger
817+
) {
778818
return ruleCompletions.filter(
779819
(completion) => completion.kind === CompletionItemKind.Snippet,
780820
);

packages/language-support/src/autocompletion/schemaBasedCompletions.ts

Lines changed: 127 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
CompletionItem,
33
CompletionItemKind,
4+
InsertTextFormat,
45
} from 'vscode-languageserver-types';
56
import { DbSchema } from '../dbSchema';
67
import { ParsedStatement } from '../parserWrapper';
@@ -21,6 +22,131 @@ import {
2122
import { backtickIfNeeded } from './autocompletionHelpers';
2223
import { convertToCNF } from '../labelTreeRewriting';
2324

25+
export function getShortPathCompletions(
26+
lastNode: RelationshipPatternContext,
27+
symbolsInfo: SymbolsInfo,
28+
dbSchema: DbSchema,
29+
): CompletionItem[] {
30+
const snippetCompletions: CompletionItem[] = [];
31+
const lastVariable = findLastVariable(lastNode, symbolsInfo);
32+
if (
33+
!lastVariable ||
34+
!dbSchema.graphSchema ||
35+
isLabelLeaf(lastVariable.labels) ||
36+
lastVariable.labels.children.length === 0
37+
) {
38+
return [];
39+
}
40+
41+
const { toRels: nodesToRelsSet, fromRels: nodesFromRelsSet } =
42+
getNodesFromRelsSet(dbSchema);
43+
const assumedDirection = lastNode.leftArrow() ? 'outgoing' : 'incoming';
44+
45+
let cnfTree: LabelOrCondition;
46+
try {
47+
cnfTree = convertToCNF(lastVariable.labels);
48+
} catch (e) {
49+
return [];
50+
}
51+
const { inLabels, outLabels } = walkCNFTree(
52+
nodesToRelsSet,
53+
nodesFromRelsSet,
54+
cnfTree,
55+
);
56+
57+
if (assumedDirection === 'outgoing') {
58+
for (const outLabel of outLabels) {
59+
snippetCompletions.push({
60+
label: '-(:' + outLabel + ')',
61+
kind: CompletionItemKind.Snippet,
62+
insertTextFormat: InsertTextFormat.Snippet,
63+
insertText: '-(${1: }:' + outLabel + ')${2:}',
64+
detail: 'path template',
65+
});
66+
}
67+
} else {
68+
for (const inLabel of inLabels) {
69+
snippetCompletions.push({
70+
label: '->(:' + inLabel + ')',
71+
kind: CompletionItemKind.Snippet,
72+
insertTextFormat: InsertTextFormat.Snippet,
73+
insertText: '->(${1: }:' + inLabel + ')${2:}',
74+
detail: 'path template',
75+
});
76+
}
77+
}
78+
79+
return snippetCompletions;
80+
}
81+
82+
export function getPathCompletions(
83+
lastNode: NodePatternContext,
84+
symbolsInfo: SymbolsInfo,
85+
dbSchema: DbSchema,
86+
): CompletionItem[] {
87+
const snippetCompletions: CompletionItem[] = [];
88+
89+
const lastVariable = findLastVariable(lastNode, symbolsInfo);
90+
if (
91+
!lastVariable ||
92+
!dbSchema.graphSchema ||
93+
isLabelLeaf(lastVariable.labels) ||
94+
lastVariable.labels.children.length === 0
95+
) {
96+
return [];
97+
}
98+
const { toNodes: relsToNodesSet, fromNodes: relsFromNodesSet } =
99+
getRelsFromNodesSets(dbSchema);
100+
let cnfTree: LabelOrCondition;
101+
try {
102+
cnfTree = convertToCNF(lastVariable.labels);
103+
} catch (e) {
104+
return [];
105+
}
106+
const { inLabels: inRelTypes, outLabels: outRelTypes } = walkCNFTree(
107+
relsToNodesSet,
108+
relsFromNodesSet,
109+
cnfTree,
110+
);
111+
const { toRels: nodesToRelsSet, fromRels: nodesFromRelsSet } =
112+
getNodesFromRelsSet(dbSchema);
113+
for (const inRelType of inRelTypes) {
114+
const { inLabels } = walkCNFTree(nodesToRelsSet, nodesFromRelsSet, {
115+
condition: 'and',
116+
children: [{ value: inRelType, validFrom: 0 }],
117+
});
118+
for (const inLabel of inLabels) {
119+
snippetCompletions.push({
120+
label: '-[:' + inRelType + ']->(:' + inLabel + ')',
121+
kind: CompletionItemKind.Snippet,
122+
insertTextFormat: InsertTextFormat.Snippet,
123+
insertText:
124+
'-[${1: }:' + inRelType + ']->(${2: }:' + inLabel + ')${3:}',
125+
detail: 'path template',
126+
});
127+
}
128+
}
129+
130+
for (const outRelType of outRelTypes) {
131+
const { outLabels } = walkCNFTree(nodesToRelsSet, nodesFromRelsSet, {
132+
condition: 'and',
133+
children: [{ value: outRelType, validFrom: 0 }],
134+
});
135+
for (const outLabel of outLabels) {
136+
snippetCompletions.push({
137+
label: '<-[:' + outRelType + ']-(:' + outLabel + ')',
138+
kind: CompletionItemKind.Snippet,
139+
insertTextFormat: InsertTextFormat.Snippet,
140+
insertText:
141+
'<-[${1: }:' + outRelType + ']-(${2: }:' + outLabel + ')${3:}',
142+
detail: 'path template',
143+
});
144+
}
145+
}
146+
147+
return snippetCompletions;
148+
}
149+
24150
export const labelsToCompletions = (labelNames: string[] = []) =>
25151
labelNames.map((labelName) => {
26152
const backtickedName = backtickIfNeeded(labelName, 'label');
@@ -39,7 +165,7 @@ export const labelsToCompletions = (labelNames: string[] = []) =>
39165
export const allLabelCompletions = (dbSchema: DbSchema) =>
40166
labelsToCompletions(dbSchema.labels);
41167

42-
export const reltypesToCompletions = (reltypes: string[] = []) =>
168+
const reltypesToCompletions = (reltypes: string[] = []) =>
43169
reltypes.map((relType) => {
44170
const backtickedName = backtickIfNeeded(relType, 'relType');
45171
const maybeInsertText = backtickedName

packages/language-support/src/tests/autocompletion/patternCompletion.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -640,4 +640,61 @@ describe('Auto-completion works correctly inside nodes and relationship patterns
640640
expected: [{ label: 'WHERE', kind: CompletionItemKind.Keyword }],
641641
});
642642
});
643+
644+
test('Correctly completes label inside "CREATE INDEX" command', () => {
645+
const query = 'CREATE FULLTEXT INDEX IF NOT EXISTS FOR (c:C';
646+
647+
testCompletions({
648+
query,
649+
dbSchema: {
650+
labels: ['Cat', 'Person', 'Dog'],
651+
relationshipTypes: ['KNOWS', 'HAS', 'COSTS'],
652+
},
653+
expected: [{ label: 'Cat', kind: CompletionItemKind.TypeParameter }],
654+
excluded: [{ label: 'COSTS', kind: CompletionItemKind.TypeParameter }],
655+
});
656+
});
657+
658+
test('Correctly completes reltype inside "CREATE INDEX" command', () => {
659+
const query = 'CREATE FULLTEXT INDEX IF NOT EXISTS FOR ()-[c:C';
660+
661+
testCompletions({
662+
query,
663+
dbSchema: {
664+
labels: ['Cat', 'Person', 'Dog'],
665+
relationshipTypes: ['KNOWS', 'HAS', 'COSTS'],
666+
},
667+
expected: [{ label: 'COSTS', kind: CompletionItemKind.TypeParameter }],
668+
excluded: [{ label: 'Cat', kind: CompletionItemKind.TypeParameter }],
669+
});
670+
});
671+
672+
test('Correctly completes label inside "CREATE CONSTRAINT" command', () => {
673+
const query = `CREATE CONSTRAINT sequels
674+
FOR (x:C`;
675+
676+
testCompletions({
677+
query,
678+
dbSchema: {
679+
labels: ['Cat', 'Person', 'Dog'],
680+
relationshipTypes: ['KNOWS', 'HAS', 'COSTS'],
681+
},
682+
expected: [{ label: 'Cat', kind: CompletionItemKind.TypeParameter }],
683+
excluded: [{ label: 'COSTS', kind: CompletionItemKind.TypeParameter }],
684+
});
685+
});
686+
test('Correctly completes reltype inside "CREATE CONSTRAINT" command', () => {
687+
const query = `CREATE CONSTRAINT sequels
688+
FOR ()-[x:C`;
689+
690+
testCompletions({
691+
query,
692+
dbSchema: {
693+
labels: ['Cat', 'Person', 'Dog'],
694+
relationshipTypes: ['KNOWS', 'HAS', 'COSTS'],
695+
},
696+
expected: [{ label: 'COSTS', kind: CompletionItemKind.TypeParameter }],
697+
excluded: [{ label: 'Cat', kind: CompletionItemKind.TypeParameter }],
698+
});
699+
});
643700
});

0 commit comments

Comments
 (0)