Skip to content

Commit b0170f9

Browse files
authored
Add triggering of path segment completions on start of path (#655)
1 parent 58d31f8 commit b0170f9

7 files changed

Lines changed: 299 additions & 131 deletions

File tree

.changeset/hungry-buses-work.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@neo4j-cypher/language-support': patch
3+
'@neo4j-cypher/language-server': patch
4+
---
5+
6+
Increase path segment completion triggering

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: 155 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import CypherParser, {
88
CallClauseContext,
99
Expression2Context,
1010
NodePatternContext,
11+
PatternElementContext,
1112
RelationshipPatternContext,
1213
} from '../generated-parser/CypherCmdParser';
1314
import {
@@ -499,6 +500,7 @@ export function completionCoreCompletion(
499500
caretIndex--;
500501
}
501502

503+
//Rules that should be returned as candidates instead of their constituent tokens
502504
codeCompletion.preferredRules = new Set<number>([
503505
CypherParser.RULE_functionName,
504506
CypherParser.RULE_procedureName,
@@ -507,16 +509,13 @@ export function completionCoreCompletion(
507509
CypherParser.RULE_parameter,
508510
CypherParser.RULE_propertyKeyName,
509511
CypherParser.RULE_variable,
510-
CypherParser.RULE_leftArrow,
511-
CypherParser.RULE_arrowLine,
512512
// this rule is used for usernames and roles.
513513
CypherParser.RULE_commandNameExpression,
514514
CypherParser.RULE_procedureResultItem,
515515
CypherParser.RULE_cypherVersion,
516516
CypherParser.RULE_cypher,
517517
CypherParser.RULE_labelType,
518518
CypherParser.RULE_relType,
519-
520519
// Either enable the helper rules for lexer clashes,
521520
// or collect all console commands like below with symbolicNameString
522521
...(_internalFeatureFlags.consoleCommands
@@ -765,74 +764,180 @@ export function completionCoreCompletion(
765764
return [{ label: 'write', kind: CompletionItemKind.Event }];
766765
}
767766

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-
786-
if (ruleNumber === CypherParser.RULE_leftArrow) {
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-
}
805-
}
806-
807767
return [];
808768
},
809769
);
810770

771+
const snippetCompletions: CompletionItem[] = getSnippetCompletions(
772+
parsingResult,
773+
dbSchema,
774+
tokens,
775+
candidates,
776+
symbolsInfo,
777+
);
778+
const snippetTriggers = [
779+
CypherParser.RPAREN,
780+
CypherParser.RBRACKET,
781+
CypherParser.ARROW_LINE,
782+
CypherParser.ARROW_LEFT_HEAD,
783+
];
784+
811785
// if the completion was automatically triggered by a snippet trigger character
812786
// we should only return snippet completions
813-
if (
814-
(CypherLexer.RPAREN === previousToken?.type ||
815-
CypherLexer.RBRACKET === previousToken?.type) &&
816-
!manualTrigger
817-
) {
818-
return ruleCompletions.filter(
819-
(completion) => completion.kind === CompletionItemKind.Snippet,
820-
);
787+
if (snippetTriggers.includes(previousToken?.type) && !manualTrigger) {
788+
return snippetCompletions;
821789
}
822790

823791
return [
824792
...ruleCompletions,
825793
...getTokenCompletions(candidates, ignoredTokens),
794+
...snippetCompletions,
826795
];
827796
}
828797

798+
function getSnippetCompletions(
799+
parsingResult: ParsedStatement,
800+
dbSchema: DbSchema,
801+
tokens: Token[],
802+
candidates: CandidatesCollection,
803+
symbolsInfo: SymbolsInfo,
804+
) {
805+
let snippetCompletions: CompletionItem[] = [];
806+
if (
807+
tokens.length > 4 &&
808+
candidates.tokens
809+
.keys()
810+
.toArray()
811+
.some((x) =>
812+
[
813+
CypherParser.ARROW_LINE,
814+
CypherParser.LPAREN,
815+
CypherParser.ARROW_RIGHT_HEAD,
816+
CypherParser.LBRACKET,
817+
].includes(x),
818+
)
819+
) {
820+
const parent = findParent(
821+
parsingResult.stopNode,
822+
(x) => x.parentCtx instanceof PatternElementContext,
823+
)?.parentCtx;
824+
825+
const lastNode: RelationshipPatternContext | NodePatternContext =
826+
parent?.children.findLast(
827+
(x) =>
828+
(x instanceof RelationshipPatternContext ||
829+
x instanceof NodePatternContext) &&
830+
x.exception === null,
831+
) as RelationshipPatternContext | NodePatternContext;
832+
const finalNonEofToken = tokens.at(-2);
833+
834+
if (
835+
lastNode &&
836+
[
837+
CypherParser.LT,
838+
CypherParser.ARROW_LEFT_HEAD,
839+
CypherParser.ARROW_LINE,
840+
CypherParser.MINUS,
841+
].includes(finalNonEofToken.type)
842+
) {
843+
const secondLastToken = tokens.at(-3);
844+
// case: ]-
845+
if (
846+
secondLastToken.type === CypherParser.RBRACKET &&
847+
lastNode instanceof RelationshipPatternContext
848+
) {
849+
const shortPathCompletions: CompletionItem[] = getShortPathCompletions(
850+
lastNode,
851+
symbolsInfo,
852+
dbSchema,
853+
);
854+
const cutSnippets = shortPathCompletions
855+
.filter((x) => x.label[0] === finalNonEofToken.text)
856+
.map((x) => {
857+
return {
858+
...x,
859+
insertText: x?.insertText.slice(1, x.insertText.length),
860+
label: x.label.slice(1, x.label.length),
861+
};
862+
});
863+
snippetCompletions = cutSnippets;
864+
// cases:
865+
// )<
866+
// )-
867+
} else if (
868+
secondLastToken.type === CypherParser.RPAREN &&
869+
lastNode instanceof NodePatternContext
870+
) {
871+
const pathCompletions: CompletionItem[] = getPathCompletions(
872+
lastNode,
873+
symbolsInfo,
874+
dbSchema,
875+
);
876+
const cutSnippets = pathCompletions
877+
.filter((x) => x.label[0] === finalNonEofToken.text)
878+
.map((x) => {
879+
return {
880+
...x,
881+
insertText: x?.insertText.slice(1, x.insertText.length),
882+
label: x.label.slice(1, x.label.length),
883+
};
884+
});
885+
snippetCompletions = cutSnippets;
886+
// case: )<-
887+
// Needed because the "-" will re-trigger completions
888+
} else if (
889+
tokens.at(-4).type === CypherParser.RPAREN &&
890+
[CypherParser.ARROW_LEFT_HEAD, CypherParser.LT].includes(
891+
secondLastToken.type,
892+
) &&
893+
[CypherParser.ARROW_LINE, CypherParser.MINUS].includes(
894+
finalNonEofToken.type,
895+
) &&
896+
lastNode instanceof NodePatternContext
897+
) {
898+
const pathCompletions: CompletionItem[] = getPathCompletions(
899+
lastNode,
900+
symbolsInfo,
901+
dbSchema,
902+
);
903+
const cutSnippets = pathCompletions
904+
.filter((x) => x.label[0] === '<')
905+
.map((x) => {
906+
return {
907+
...x,
908+
insertText: x?.insertText.slice(2, x.insertText.length),
909+
label: x.label.slice(2, x.label.length),
910+
};
911+
});
912+
snippetCompletions = cutSnippets;
913+
}
914+
} else if (
915+
lastNode instanceof RelationshipPatternContext &&
916+
finalNonEofToken.text === ']'
917+
) {
918+
snippetCompletions = getShortPathCompletions(
919+
lastNode,
920+
symbolsInfo,
921+
dbSchema,
922+
);
923+
} else if (
924+
lastNode instanceof NodePatternContext &&
925+
finalNonEofToken.text === ')'
926+
) {
927+
snippetCompletions = getPathCompletions(lastNode, symbolsInfo, dbSchema);
928+
}
929+
}
930+
return snippetCompletions;
931+
}
932+
829933
type CompletionHelperArgs = {
830934
parsingResult: ParsedStatement;
831935
dbSchema: DbSchema;
832936
previousToken?: Token;
833937
tokens: Token[];
834938
candidateRule: CandidateRule;
835939
};
940+
836941
function completeAliasName({
837942
candidateRule,
838943
dbSchema,

packages/language-support/src/helpers.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,8 @@ export function findStopNode(root: StatementsOrCommandsContext) {
6060
export function findParent(
6161
leaf: EnrichedParseTree | undefined,
6262
condition: (node: EnrichedParseTree) => boolean,
63-
): EnrichedParseTree {
64-
let current: EnrichedParseTree | undefined = leaf;
63+
): EnrichedParseTree | null {
64+
let current: EnrichedParseTree | null = leaf;
6565

6666
while (current && !condition(current)) {
6767
current = current.parentCtx;

0 commit comments

Comments
 (0)