Skip to content

Commit 8cc77c6

Browse files
Console command support (:use, :params, :clear, history) (#165)
* create gramamr for client commands * some basic commands * consider * add test file * mellan * rename * add unit tests * cleanuptodos * todos * fix syntax highlighting for console commands * add tests * fix completions * add errors when cmd disabled * semantic analysis handles multiple queries * comment * self review * unused variable * fix unit tests * self review * rename parser * rename rules * rephrase comment * fix crash and add tests * remove todo * mellan * properly merge main * improve error messages for console commands * add changeset * fix bad merge
1 parent 87817aa commit 8cc77c6

27 files changed

+1232
-147
lines changed

.changeset/hungry-beans-eat.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@neo4j-cypher/language-support': patch
3+
'@neo4j-cypher/react-codemirror': patch
4+
'@neo4j-cypher/language-server': patch
5+
---
6+
7+
Add support for console commands

packages/language-server/src/linting.ts

+2-8
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
1-
import {
2-
findEndPosition,
3-
parserWrapper,
4-
validateSyntax,
5-
} from '@neo4j-cypher/language-support';
1+
import { validateSyntax } from '@neo4j-cypher/language-support';
62
import debounce from 'lodash.debounce';
73
import { join } from 'path';
84
import { Diagnostic, TextDocumentChangeEvent } from 'vscode-languageserver';
@@ -42,9 +38,7 @@ async function rawLintDocument(
4238
lastSemanticJob = proxyWorker.validateSemantics(query);
4339
const result = await lastSemanticJob;
4440

45-
sendDiagnostics(
46-
result.map((el) => findEndPosition(el, parserWrapper.parsingResult)),
47-
);
41+
sendDiagnostics(result);
4842
} catch (err) {
4943
if (!(err instanceof workerpool.Promise.CancellationError)) {
5044
console.error(err);

packages/language-support/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
"vscode-languageserver-types": "^3.17.3"
4848
},
4949
"scripts": {
50-
"gen-parser": "antlr4 -Dlanguage=TypeScript -visitor src/antlr-grammar/CypherParser.g4 src/antlr-grammar/CypherLexer.g4 -o src/generated-parser/ -Xexact-output-dir",
50+
"gen-parser": "antlr4 -Dlanguage=TypeScript -visitor src/antlr-grammar/CypherCmdLexer.g4 src/antlr-grammar/CypherCmdParser.g4 -o src/generated-parser/ -Xexact-output-dir",
5151
"build": "npm run gen-parser && concurrently 'npm:build-types' 'npm:build-esm' 'npm:build-commonjs'",
5252
"build-types": "tsc --emitDeclarationOnly --outDir dist/types",
5353
"build-esm": "esbuild ./src/index.ts --bundle --format=esm --sourcemap --outfile=dist/esm/index.mjs",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
lexer grammar CypherCmdLexer;
2+
3+
import CypherLexer;
4+
5+
PARAM : P A R A M S?;
6+
7+
CLEAR: C L E A R;
8+
9+
HISTORY: H I S T O R Y;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
parser grammar CypherCmdParser;
2+
3+
import CypherParser;
4+
5+
options { tokenVocab = CypherCmdLexer; }
6+
7+
statementsOrCommands: statementOrCommand (SEMICOLON statementOrCommand)* SEMICOLON? EOF;
8+
9+
statementOrCommand: (statement | consoleCommand);
10+
11+
consoleCommand: COLON (clearCmd | historyCmd | useCmd | paramsCmd);
12+
13+
paramsCmd: PARAM paramsArgs?;
14+
15+
paramsArgs: (CLEAR | listCompletionRule | map | lambda);
16+
17+
lambda: unescapedSymbolicNameString EQ GT expression;
18+
19+
clearCmd: CLEAR;
20+
21+
historyCmd: HISTORY;
22+
23+
useCmd: useCompletionRule symbolicAliasName?;
24+
25+
// These rules are needed to distinguish cypher <-> commands, for exapmle `USE` and `:use` in autocompletion
26+
listCompletionRule: LIST;
27+
28+
useCompletionRule: USE;

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

+61-20
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,22 @@ import {
66
CompletionItemKind,
77
} from 'vscode-languageserver-types';
88
import { DbSchema } from '../dbSchema';
9-
import CypherLexer from '../generated-parser/CypherLexer';
9+
import CypherLexer from '../generated-parser/CypherCmdLexer';
1010
import CypherParser, {
1111
Expression2Context,
12-
} from '../generated-parser/CypherParser';
12+
} from '../generated-parser/CypherCmdParser';
1313
import { rulesDefiningVariables } from '../helpers';
1414
import {
1515
CypherTokenType,
1616
lexerKeywords,
1717
lexerSymbols,
1818
tokenNames,
1919
} from '../lexerSymbols';
20-
import { EnrichedParsingResult, ParsingResult } from '../parserWrapper';
20+
import {
21+
consoleCommandEnabled,
22+
EnrichedParsingResult,
23+
ParsingResult,
24+
} from '../parserWrapper';
2125

2226
const uniq = <T>(arr: T[]) => Array.from(new Set(arr));
2327

@@ -125,17 +129,27 @@ const namespacedCompletion = (
125129
}
126130
};
127131

128-
function getTokenCandidates(
132+
function getTokenCompletions(
129133
candidates: CandidatesCollection,
130134
ignoredTokens: Set<number>,
131-
) {
135+
): CompletionItem[] {
132136
const tokenEntries = candidates.tokens.entries();
133137

134-
const tokenCandidates = Array.from(tokenEntries).flatMap((value) => {
138+
const completions = Array.from(tokenEntries).flatMap((value) => {
135139
const [tokenNumber, followUpList] = value;
136140

137141
if (!ignoredTokens.has(tokenNumber)) {
138-
const firstToken = tokenNames[tokenNumber];
142+
const isConsoleCommand =
143+
lexerSymbols[tokenNumber] === CypherTokenType.consoleCommand;
144+
145+
const kind = isConsoleCommand
146+
? CompletionItemKind.Event
147+
: CompletionItemKind.Keyword;
148+
149+
const firstToken = isConsoleCommand
150+
? tokenNames[tokenNumber].toLowerCase()
151+
: tokenNames[tokenNumber];
152+
139153
const followUpIndexes = followUpList.indexes;
140154
const firstIgnoredToken = followUpIndexes.findIndex((t) =>
141155
ignoredTokens.has(t),
@@ -151,21 +165,28 @@ function getTokenCandidates(
151165
if (firstToken === undefined) {
152166
return [];
153167
} else if (followUpString === '') {
154-
return [firstToken];
168+
return [{ label: firstToken, kind }];
155169
} else {
156-
const followUp = firstToken + ' ' + followUpString;
170+
const followUp =
171+
firstToken +
172+
' ' +
173+
(isConsoleCommand ? followUpString.toLowerCase() : followUpString);
174+
157175
if (followUpList.optional) {
158-
return [firstToken, followUp];
176+
return [
177+
{ label: firstToken, kind },
178+
{ label: followUp, kind },
179+
];
159180
}
160181

161-
return [followUp];
182+
return [{ label: followUp, kind }];
162183
}
163184
} else {
164185
return [];
165186
}
166187
});
167188

168-
return tokenCandidates;
189+
return completions;
169190
}
170191

171192
const parameterCompletions = (
@@ -306,6 +327,15 @@ export function completionCoreCompletion(
306327
CypherParser.RULE_propertyKeyName,
307328
CypherParser.RULE_variable,
308329

330+
// Either enable the helper rules for lexer clashes,
331+
// or collect all console commands like below with symbolicNameString
332+
...(consoleCommandEnabled()
333+
? [
334+
CypherParser.RULE_useCompletionRule,
335+
CypherParser.RULE_listCompletionRule,
336+
]
337+
: [CypherParser.RULE_consoleCommand]),
338+
309339
// Because of the overlap of keywords and identifiers in cypher
310340
// We will suggest keywords when users type identifiers as well
311341
// To avoid this we want custom completion for identifiers
@@ -317,7 +347,11 @@ export function completionCoreCompletion(
317347
// Keep only keywords as suggestions
318348
const ignoredTokens = new Set<number>(
319349
Object.entries(lexerSymbols)
320-
.filter(([, type]) => type !== CypherTokenType.keyword)
350+
.filter(
351+
([, type]) =>
352+
type !== CypherTokenType.keyword &&
353+
type !== CypherTokenType.consoleCommand,
354+
)
321355
.map(([token]) => Number(token)),
322356
);
323357

@@ -427,17 +461,24 @@ export function completionCoreCompletion(
427461
];
428462
}
429463
}
464+
465+
// These are simple tokens that get completed as the wrong kind, due to a lexer conflict
466+
if (ruleNumber === CypherParser.RULE_useCompletionRule) {
467+
return [{ label: 'use', kind: CompletionItemKind.Event }];
468+
}
469+
470+
if (ruleNumber === CypherParser.RULE_listCompletionRule) {
471+
return [{ label: 'list', kind: CompletionItemKind.Event }];
472+
}
473+
430474
return [];
431475
},
432476
);
433477

434-
const tokenCandidates = getTokenCandidates(candidates, ignoredTokens);
435-
const tokenCompletions: CompletionItem[] = tokenCandidates.map((t) => ({
436-
label: t,
437-
kind: CompletionItemKind.Keyword,
438-
}));
439-
440-
return [...ruleCompletions, ...tokenCompletions];
478+
return [
479+
...ruleCompletions,
480+
...getTokenCompletions(candidates, ignoredTokens),
481+
];
441482
}
442483

443484
type CompletionHelperArgs = {

packages/language-support/src/helpers.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ import antlrDefaultExport, {
66
ParseTree,
77
Token,
88
} from 'antlr4';
9-
import CypherLexer from './generated-parser/CypherLexer';
9+
import CypherLexer from './generated-parser/CypherCmdLexer';
1010
import CypherParser, {
1111
NodePatternContext,
1212
RelationshipPatternContext,
13-
StatementsContext,
14-
} from './generated-parser/CypherParser';
13+
StatementsOrCommandsContext,
14+
} from './generated-parser/CypherCmdParser';
1515
import { ParsingResult } from './parserWrapper';
1616

1717
/* In antlr we have
@@ -31,7 +31,7 @@ export type EnrichedParseTree = ParseTree & {
3131
parentCtx: ParserRuleContext | undefined;
3232
};
3333

34-
export function findStopNode(root: StatementsContext) {
34+
export function findStopNode(root: StatementsOrCommandsContext) {
3535
let children = root.children;
3636
let current: ParserRuleContext = root;
3737

packages/language-support/src/highlighting/syntaxColouring/syntaxColouring.ts

+43-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
AnyExpressionContext,
66
ArrowLineContext,
77
BooleanLiteralContext,
8+
ConsoleCommandContext,
89
FunctionNameContext,
910
KeywordLiteralContext,
1011
LabelNameContext,
@@ -14,6 +15,7 @@ import {
1415
NoneExpressionContext,
1516
NumberLiteralContext,
1617
ParameterContext,
18+
ParamsArgsContext,
1719
ProcedureNameContext,
1820
ProcedureResultItemContext,
1921
PropertyKeyNameContext,
@@ -23,14 +25,15 @@ import {
2325
StringsLiteralContext,
2426
StringTokenContext,
2527
SymbolicNameStringContext,
28+
UseCompletionRuleContext,
2629
VariableContext,
27-
} from '../../generated-parser/CypherParser';
30+
} from '../../generated-parser/CypherCmdParser';
2831

2932
import {
3033
SemanticTokensLegend,
3134
SemanticTokenTypes,
3235
} from 'vscode-languageserver-types';
33-
import CypherParserListener from '../../generated-parser/CypherParserListener';
36+
import CypherParserListener from '../../generated-parser/CypherCmdParserListener';
3437
import { CypherTokenType } from '../../lexerSymbols';
3538
import { parserWrapper } from '../../parserWrapper';
3639
import {
@@ -74,6 +77,7 @@ export function mapCypherToSemanticTokenIndex(
7477
[CypherTokenType.label]: SemanticTokenTypes.type,
7578
[CypherTokenType.variable]: SemanticTokenTypes.variable,
7679
[CypherTokenType.symbolicName]: SemanticTokenTypes.variable,
80+
[CypherTokenType.consoleCommand]: SemanticTokenTypes.macro,
7781
};
7882

7983
const semanticTokenType = tokenMappings[cypherTokenType];
@@ -229,6 +233,43 @@ class SyntaxHighlighter extends CypherParserListener {
229233
exitSymbolicNameString = (ctx: SymbolicNameStringContext) => {
230234
this.addToken(ctx.start, CypherTokenType.symbolicName, ctx.getText());
231235
};
236+
237+
// Fix coloring of colon in console commands (operator -> consoleCommand)
238+
exitConsoleCommand = (ctx: ConsoleCommandContext) => {
239+
const colon = ctx.COLON();
240+
this.addToken(
241+
colon.symbol,
242+
CypherTokenType.consoleCommand,
243+
colon.getText(),
244+
);
245+
};
246+
247+
// console commands that clash with cypher keywords
248+
exitUseCompletionRule = (ctx: UseCompletionRuleContext) => {
249+
const use = ctx.USE();
250+
251+
this.addToken(use.symbol, CypherTokenType.consoleCommand, use.getText());
252+
};
253+
254+
exitParamsArgs = (ctx: ParamsArgsContext) => {
255+
const clear = ctx.CLEAR();
256+
if (clear) {
257+
this.addToken(
258+
clear.symbol,
259+
CypherTokenType.consoleCommand,
260+
clear.getText(),
261+
);
262+
}
263+
264+
const list = ctx.listCompletionRule()?.LIST();
265+
if (list) {
266+
this.addToken(
267+
list.symbol,
268+
CypherTokenType.consoleCommand,
269+
list.getText(),
270+
);
271+
}
272+
};
232273
}
233274

234275
function colourLexerTokens(tokens: Token[]) {

packages/language-support/src/highlighting/syntaxColouring/syntaxColouringHelpers.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { SemanticTokenTypes } from 'vscode-languageserver-types';
22

33
import { Token } from 'antlr4';
44

5-
import CypherLexer from '../../generated-parser/CypherLexer';
5+
import CypherLexer from '../../generated-parser/CypherCmdLexer';
66

77
import { CypherTokenType, lexerSymbols } from '../../lexerSymbols';
88

0 commit comments

Comments
 (0)