Skip to content

Commit 87817aa

Browse files
authored
Avoids reparses in the signature help (#168)
1 parent 2eff826 commit 87817aa

File tree

6 files changed

+405
-226
lines changed

6 files changed

+405
-226
lines changed

packages/language-server/src/signatureHelp.ts

+3-7
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import { signatureHelp } from '@neo4j-cypher/language-support';
22
import {
3-
Position,
4-
Range,
53
SignatureHelp,
64
SignatureHelpParams,
75
TextDocuments,
@@ -26,14 +24,12 @@ export function doSignatureHelp(
2624
if (textDocument === undefined || endOfTriggerHelp) return emptyResult;
2725

2826
const position = params.position;
29-
const range: Range = {
30-
start: Position.create(0, 0),
31-
end: position,
32-
};
27+
const offset = textDocument.offsetAt(position);
3328

3429
return signatureHelp(
35-
textDocument.getText(range),
30+
textDocument.getText(),
3631
neo4j.metadata?.dbSchema ?? {},
32+
offset,
3733
);
3834
};
3935
}

packages/language-support/src/helpers.ts

+22-4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import antlrDefaultExport, {
44
CommonTokenStream,
55
ParserRuleContext,
6+
ParseTree,
67
Token,
78
} from 'antlr4';
89
import CypherLexer from './generated-parser/CypherLexer';
@@ -13,6 +14,23 @@ import CypherParser, {
1314
} from './generated-parser/CypherParser';
1415
import { ParsingResult } from './parserWrapper';
1516

17+
/* In antlr we have
18+
19+
ParseTree
20+
/ \
21+
/ \
22+
TerminalNode RuleContext
23+
\
24+
ParserRuleContext
25+
26+
Both TerminalNode and RuleContext have parentCtx, but ParseTree doesn't
27+
This type fixes that because it's what we need to traverse the tree most
28+
of the time
29+
*/
30+
export type EnrichedParseTree = ParseTree & {
31+
parentCtx: ParserRuleContext | undefined;
32+
};
33+
1634
export function findStopNode(root: StatementsContext) {
1735
let children = root.children;
1836
let current: ParserRuleContext = root;
@@ -38,10 +56,10 @@ export function findStopNode(root: StatementsContext) {
3856
}
3957

4058
export function findParent(
41-
leaf: ParserRuleContext | undefined,
42-
condition: (node: ParserRuleContext) => boolean,
43-
) {
44-
let current: ParserRuleContext | undefined = leaf;
59+
leaf: EnrichedParseTree | undefined,
60+
condition: (node: EnrichedParseTree) => boolean,
61+
): EnrichedParseTree {
62+
let current: EnrichedParseTree | undefined = leaf;
4563

4664
while (current && !condition(current)) {
4765
current = current.parentCtx;

packages/language-support/src/signatureHelp.ts

+134-135
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,17 @@ import {
33
SignatureInformation,
44
} from 'vscode-languageserver-types';
55

6-
import { ParserRuleContext, ParseTree } from 'antlr4';
7-
import {
6+
import { ParseTreeWalker } from 'antlr4';
7+
import CypherParser, {
88
CallClauseContext,
99
ExpressionContext,
1010
FunctionInvocationContext,
11-
StatementsContext,
1211
} from './generated-parser/CypherParser';
1312

13+
import { Token } from 'antlr4-c3';
1414
import { DbSchema } from './dbSchema';
15-
import { findParent } from './helpers';
15+
import CypherParserListener from './generated-parser/CypherParserListener';
16+
import { isDefined } from './helpers';
1617
import { parserWrapper } from './parserWrapper';
1718

1819
export const emptyResult: SignatureHelp = {
@@ -21,163 +22,161 @@ export const emptyResult: SignatureHelp = {
2122
activeParameter: undefined,
2223
};
2324

25+
export enum MethodType {
26+
function = 'function',
27+
procedure = 'procedure',
28+
}
2429
interface ParsedMethod {
2530
methodName: string;
26-
numProcedureArgs: number;
27-
}
28-
29-
function tryParseProcedure(
30-
currentNode: ParserRuleContext,
31-
): ParsedMethod | undefined {
32-
const callClause = findParent(
33-
currentNode,
34-
(node) => node instanceof CallClauseContext,
35-
);
36-
37-
if (callClause) {
38-
const ctx = callClause as CallClauseContext;
39-
40-
const methodName = ctx.procedureName().getText();
41-
const numProcedureArgs = ctx.procedureArgument_list().length;
42-
return {
43-
methodName: methodName,
44-
numProcedureArgs: numProcedureArgs,
45-
};
46-
} else {
47-
return undefined;
48-
}
31+
activeParameter: number;
32+
methodType: MethodType;
4933
}
5034

51-
/*
52-
RETURN apoc.do.when( gets parsed as:
53-
54-
statements
55-
/ | \
56-
statement ( EOF
57-
/ \
58-
RETURN expression
59-
60-
61-
62-
rather than:
63-
64-
65-
statements
66-
/ \
67-
statement EOF
68-
/ \
69-
RETURN functionInvocation
70-
/ \
71-
functionName (
72-
35+
function toSignatureHelp(
36+
methodSignatures: Record<string, SignatureInformation>,
37+
parsedMethod: ParsedMethod,
38+
) {
39+
const methodName = parsedMethod.methodName;
40+
const method = methodSignatures[methodName];
41+
const signatures = method ? [method] : [];
7342

74-
so we need to treat that case differently because we cannot modify the
75-
relative priority of functionInvocation vs expression
43+
const signatureHelp: SignatureHelp = {
44+
signatures: signatures,
45+
activeSignature: method ? 0 : undefined,
46+
activeParameter: parsedMethod.activeParameter,
47+
};
48+
return signatureHelp;
49+
}
7650

77-
RETURN apoc.do.when(x gets parsed correctly (when we've started the first argument),
78-
because the parser has enough information to recognize that case
51+
class SignatureHelper extends CypherParserListener {
52+
result: ParsedMethod;
53+
constructor(private tokens: Token[], private caretToken: Token) {
54+
super();
55+
}
7956

80-
*/
81-
function findRightmostPreviousExpression(
82-
currentNode: ParserRuleContext,
83-
): ParserRuleContext | undefined {
84-
const parentChildren = currentNode.parentCtx.children;
85-
let result: ParserRuleContext | undefined = undefined;
57+
exitExpression = (ctx: ExpressionContext) => {
58+
// If the caret is at (
59+
if (this.caretToken.type === CypherParser.LPAREN) {
60+
/* We need to compute the next token that is not
61+
a space following the expression
62+
63+
Example: in the case 'RETURN apoc.do.when (' the
64+
expression finishes before the ( and we would have a
65+
collection of spaces between apoc.do.when and the left parenthesis
66+
*/
67+
let index = ctx.stop.tokenIndex + 1;
68+
let nextToken = this.tokens[index];
69+
70+
while (
71+
nextToken.type === CypherParser.SPACE &&
72+
index < this.tokens.length
73+
) {
74+
index++;
75+
nextToken = this.tokens[index];
76+
}
77+
78+
if (
79+
this.caretToken.start === nextToken?.start &&
80+
this.caretToken.stop === nextToken?.stop
81+
) {
82+
const methodName = ctx.getText();
83+
const numMethodArgs = 0;
84+
this.result = {
85+
methodName: methodName,
86+
activeParameter: numMethodArgs,
87+
methodType: MethodType.function,
88+
};
89+
}
90+
}
91+
};
8692

87-
if (parentChildren && parentChildren.length > 2) {
88-
let current: ParseTree | undefined =
89-
parentChildren[parentChildren.length - 3];
90-
let expressionFound = false;
93+
exitFunctionInvocation = (ctx: FunctionInvocationContext) => {
94+
if (
95+
ctx.start.start <= this.caretToken.start &&
96+
this.caretToken.stop <= ctx.stop.stop &&
97+
// We need to check we have opened the left parenthesis
98+
// and we won't offer the signature help on just the name
99+
isDefined(ctx.LPAREN())
100+
) {
101+
const methodName = ctx.functionName().getText();
102+
const previousArguments = ctx.COMMA_list().filter((arg) => {
103+
return arg.symbol.stop <= this.caretToken.start;
104+
});
105+
106+
this.result = {
107+
methodName: methodName,
108+
activeParameter: previousArguments.length,
109+
methodType: MethodType.function,
110+
};
111+
}
112+
};
91113

92-
while (
93-
current instanceof ParserRuleContext &&
94-
current.children &&
95-
current.children.length > 0 &&
96-
!expressionFound
114+
exitCallClause = (ctx: CallClauseContext) => {
115+
if (
116+
ctx.start.start <= this.caretToken.start &&
117+
this.caretToken.stop <= ctx.stop.stop &&
118+
// We need to check we have opened the left parenthesis
119+
// and we won't offer the signature help on just the name
120+
isDefined(ctx.LPAREN())
97121
) {
98-
const children = current.children;
99-
current = children[children.length - 1];
100-
expressionFound = current instanceof ExpressionContext;
122+
const methodName = ctx.procedureName().getText();
123+
const previousArguments = ctx.COMMA_list().filter((arg) => {
124+
return arg.symbol.stop <= this.caretToken.start;
125+
});
126+
127+
this.result = {
128+
methodName: methodName,
129+
activeParameter: previousArguments.length,
130+
methodType: MethodType.procedure,
131+
};
101132
}
133+
};
134+
}
102135

103-
result = expressionFound ? (current as ParserRuleContext) : undefined;
104-
}
136+
function findCaretToken(tokens: Token[], caretPosition: number): Token {
137+
let i = 0;
138+
let result: Token;
139+
let keepLooking = true;
105140

106-
return result;
107-
}
141+
while (i < tokens.length && keepLooking) {
142+
const currentToken = tokens[i];
143+
keepLooking = currentToken.start < caretPosition;
108144

109-
function tryParseFunction(
110-
currentNode: ParserRuleContext,
111-
): ParsedMethod | undefined {
112-
let result: ParsedMethod | undefined = undefined;
113-
const functionInvocation = findParent(
114-
currentNode,
115-
(node) => node instanceof FunctionInvocationContext,
116-
);
117-
118-
if (functionInvocation) {
119-
const ctx = functionInvocation as FunctionInvocationContext;
120-
const methodName = ctx.functionName().getText();
121-
const numMethodArgs = ctx.functionArgument_list().length;
122-
123-
result = {
124-
methodName: methodName,
125-
numProcedureArgs: numMethodArgs,
126-
};
127-
} else if (
128-
currentNode.getText() === '(' &&
129-
currentNode.parentCtx instanceof StatementsContext
130-
) {
131-
// If we finish in an expression followed by (,
132-
// take the expression text as method name
133-
const prevExpresion = findRightmostPreviousExpression(currentNode);
134-
135-
if (prevExpresion) {
136-
result = {
137-
methodName: prevExpresion.getText(),
138-
numProcedureArgs: 0,
139-
};
145+
if (currentToken.channel === 0 && keepLooking) {
146+
result = currentToken;
140147
}
148+
149+
i++;
141150
}
142151

143152
return result;
144153
}
145154

146-
function toSignatureHelp(
147-
methodSignatures: Record<string, SignatureInformation>,
148-
parsedMethod: ParsedMethod,
149-
) {
150-
const methodName = parsedMethod.methodName;
151-
const numMethodArgs = parsedMethod.numProcedureArgs;
152-
const method = methodSignatures[methodName];
153-
const signatures = method ? [method] : [];
154-
const argPosition =
155-
numMethodArgs !== undefined ? Math.max(numMethodArgs - 1, 0) : undefined;
156-
157-
const signatureHelp: SignatureHelp = {
158-
signatures: signatures,
159-
activeSignature: method ? 0 : undefined,
160-
activeParameter: argPosition,
161-
};
162-
return signatureHelp;
163-
}
164-
165155
export function signatureHelp(
166-
textUntilPosition: string,
156+
fullQuery: string,
167157
dbSchema: DbSchema,
158+
caretPosition: number,
168159
): SignatureHelp {
169-
const parserResult = parserWrapper.parse(textUntilPosition);
170-
const stopNode = parserResult.stopNode;
171160
let result: SignatureHelp = emptyResult;
172161

173-
const parsedProc = tryParseProcedure(stopNode);
174-
if (parsedProc && dbSchema.procedureSignatures) {
175-
result = toSignatureHelp(dbSchema.procedureSignatures, parsedProc);
176-
} else {
177-
const parsedFunc = tryParseFunction(stopNode);
162+
if (caretPosition > 0) {
163+
const parserResult = parserWrapper.parse(fullQuery);
164+
165+
const caretToken = findCaretToken(parserResult.tokens, caretPosition);
166+
const signatureHelper = new SignatureHelper(
167+
parserResult.tokens,
168+
caretToken,
169+
);
170+
171+
ParseTreeWalker.DEFAULT.walk(signatureHelper, parserResult.result);
172+
const method = signatureHelper.result;
178173

179-
if (parsedFunc && dbSchema.functionSignatures) {
180-
result = toSignatureHelp(dbSchema.functionSignatures, parsedFunc);
174+
if (method !== undefined) {
175+
if (method.methodType === MethodType.function) {
176+
result = toSignatureHelp(dbSchema.functionSignatures, method);
177+
} else {
178+
result = toSignatureHelp(dbSchema.procedureSignatures, method);
179+
}
181180
}
182181
}
183182

0 commit comments

Comments
 (0)