Skip to content

Commit 08f94af

Browse files
nneclaanderson4j
andauthored
adding auto-completion for :auto (#667)
Co-authored-by: Isak <isak.nilsson@neo4j.com>
1 parent 135393f commit 08f94af

8 files changed

Lines changed: 200 additions & 5 deletions

File tree

.changeset/fancy-bushes-travel.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@neo4j-cypher/language-support': patch
3+
---
4+
5+
adding auto-completion for :auto

packages/language-support/src/antlr-grammar/CypherCmdLexer.g4

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,6 @@ PLAY: P L A Y;
2424

2525
ACCESSMODE: A C C E S S '-' M O D E;
2626

27-
HELP: H E L P;
27+
HELP: H E L P;
28+
29+
AUTO: A U T O;

packages/language-support/src/antlr-grammar/CypherCmdParser.g4

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ consoleCommand: COLON (
2222
| playCmd
2323
| accessModeCmd
2424
| helpCmd
25+
| autoCmd
2526
);
2627

2728
paramsCmd: PARAM paramsArgs?;
@@ -58,7 +59,9 @@ accessModeCmd: ACCESSMODE accessModeArgs?;
5859

5960
helpCmd: HELP;
6061

61-
// These rules are needed to distinguish cypher <-> commands, for exapmle `USE` and `:use` in autocompletion
62+
autoCmd: AUTO statement;
63+
64+
// These rules are needed to distinguish cypher <-> commands, for example `USE` and `:use` in autocompletion
6265
listCompletionRule: LIST;
6366

6467
useCompletionRule: USE;

packages/language-support/src/cypherLanguageService.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -574,7 +574,8 @@ export type ParsedCommandNoPosition =
574574
| { type: 'style'; operation?: 'reset' }
575575
| { type: 'play'; guide?: string }
576576
| { type: 'access-mode'; operation?: string }
577-
| { type: 'help' };
577+
| { type: 'help' }
578+
| { type: 'auto'; statement?: string };
578579

579580
export type ParsedCommand = ParsedCommandNoPosition & RuleTokens;
580581

@@ -777,6 +778,21 @@ function parseToCommand(
777778
return { type: 'help', start, stop };
778779
}
779780

781+
const autoCmd = consoleCmd.autoCmd();
782+
if (autoCmd) {
783+
const autoStmt = autoCmd.statement();
784+
if (autoStmt && autoStmt.start && autoStmt.stop) {
785+
//we want autoStmt.start so we skip :auto when calling semantic analysis
786+
//but regular stop, so we include trailing error nodes not parsed as statement
787+
const statement = inputstream.getText(
788+
autoStmt.start.start,
789+
stop.stop,
790+
);
791+
return { type: 'auto', statement, start: autoStmt.start, stop };
792+
}
793+
return { type: 'auto', start, stop };
794+
}
795+
780796
return { type: 'parse-error', start, stop };
781797
}
782798
const stopToken = stop ?? tokens.at(-1);

packages/language-support/src/lexerSymbols.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,7 @@ export const lexerConsoleCmds = [
425425
CypherLexer.PLAY,
426426
CypherLexer.ACCESSMODE,
427427
CypherLexer.HELP,
428+
CypherLexer.AUTO,
428429
];
429430

430431
function toTokentypeObject(arr: number[], tokenType: CypherTokenType) {

packages/language-support/src/syntaxValidation/completionCoreErrors.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ export function completionCoreErrormessage(
6969

7070
// If we can complete only a statement, we don't want to suggest that
7171
// We want to be using the database errors stack instead
72+
// Exception: inside console commands (e.g. :auto) we do want to report
73+
// statement-level errors since there's no database error stack for those
7274
if (
7375
ruleCandidates.length === 1 &&
7476
ruleCandidates[0] === CypherParser.RULE_statement

packages/language-support/src/syntaxValidation/syntaxValidation.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,10 @@ export function lintCypherQuery(
347347
const statements = resolvedParsingResult.statementsParsing;
348348
const result = statements.map((current) => {
349349
const cmd = current.command;
350-
if (cmd.type === 'cypher' && cmd.statement.length > 0) {
350+
if (
351+
(cmd.type === 'cypher' || cmd.type === 'auto') &&
352+
cmd.statement.length > 0
353+
) {
351354
if (current.cypherVersionError)
352355
return { diagnostics: [current.cypherVersionError], symbolTable: [] };
353356

packages/language-support/src/tests/consoleCommands.test.ts

Lines changed: 164 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ import {
55
import { testData } from './testData.js';
66
import { highlightSyntax } from '../syntaxHighlighting/syntaxHighlighting.js';
77
import { autocomplete } from '../autocompletion/autocompletion.js';
8+
import {
9+
getDiagnosticsForQuery,
10+
getSymbolTablesForQuery,
11+
} from './syntaxValidation/helpers.js';
812

913
function expectParsedCommands(
1014
query: string,
@@ -224,12 +228,28 @@ describe('sanity checks', () => {
224228
tokenType: 'consoleCommand',
225229
},
226230
]);
231+
232+
expect(highlightSyntax(':auto')).toEqual([
233+
{
234+
length: 1,
235+
position: { line: 0, startCharacter: 0, startOffset: 0 },
236+
token: ':',
237+
tokenType: 'consoleCommand',
238+
},
239+
{
240+
length: 4,
241+
position: { line: 0, startCharacter: 1, startOffset: 1 },
242+
token: 'auto',
243+
tokenType: 'consoleCommand',
244+
},
245+
]);
227246
});
228247

229248
test('completes basic console cmds on :', () => {
230249
expect(autocomplete(':', {})).toEqual([
231250
{ kind: 23, label: 'server' },
232251
{ kind: 23, label: 'use' },
252+
{ kind: 23, label: 'auto' },
233253
{ kind: 23, label: 'help' },
234254
{ kind: 23, label: 'access-mode' },
235255
{ kind: 23, label: 'play' },
@@ -277,7 +297,7 @@ describe('sanity checks', () => {
277297
test('handles misspelled or non-existing command', () => {
278298
expectErrorMessage(
279299
':foo',
280-
'Expected any of help, access-mode, play, style, sysinfo, welcome, disconnect, connect, param, history, clear, server or use',
300+
'Expected any of auto, help, access-mode, play, style, sysinfo, welcome, disconnect, connect, param, history, clear, server or use',
281301
);
282302

283303
expectErrorMessage(':clea', 'Unexpected token. Did you mean clear?');
@@ -785,6 +805,149 @@ describe('server', () => {
785805
});
786806
});
787807

808+
describe('auto', () => {
809+
test('parses without arg', () => {
810+
expectParsedCommands(':auto', [{ type: 'auto', statement: '' }]);
811+
});
812+
813+
test('parses with valid statement', () => {
814+
expectParsedCommands(':auto RETURN 1', [
815+
{ type: 'auto', statement: 'RETURN 1' },
816+
]);
817+
});
818+
819+
test('gives errors on invalid statement', () => {
820+
const linting = getDiagnosticsForQuery({
821+
query: ':auto asdf',
822+
consoleCommandsEnabled: true,
823+
});
824+
expect(linting).toEqual([
825+
{
826+
offsets: {
827+
end: 10,
828+
start: 6,
829+
},
830+
message:
831+
"Invalid input 'asdf': expected 'ALTER', 'ORDER BY', 'CALL', 'USING PERIODIC COMMIT', 'CREATE', 'LOAD CSV', 'START DATABASE', 'STOP DATABASE', 'DEALLOCATE', 'DELETE', 'DENY', 'DETACH', 'DROP', 'DRYRUN', 'FINISH', 'FOREACH', 'GRANT', 'INSERT', 'LIMIT', 'MATCH', 'MERGE', 'NODETACH', 'OFFSET', 'OPTIONAL', 'REALLOCATE', 'REMOVE', 'RENAME', 'RETURN', 'REVOKE', 'ENABLE SERVER', 'SET', 'SHOW', 'SKIP', 'TERMINATE', 'UNWIND', 'USE' or 'WITH'",
832+
range: {
833+
end: {
834+
character: 10,
835+
line: 0,
836+
},
837+
start: {
838+
character: 6,
839+
line: 0,
840+
},
841+
},
842+
severity: 1,
843+
},
844+
]);
845+
});
846+
847+
test('gives errors on trailing garbage after valid statement', () => {
848+
const linting = getDiagnosticsForQuery({
849+
query: ':auto RETURN 1 asdf',
850+
consoleCommandsEnabled: true,
851+
});
852+
expect(linting).toEqual([
853+
{
854+
offsets: {
855+
end: 19,
856+
start: 15,
857+
},
858+
message:
859+
"Invalid input 'asdf': expected an expression, ',', 'AS', 'ORDER BY', 'CALL', 'CREATE', 'LOAD CSV', 'DELETE', 'DETACH', 'FINISH', 'FOREACH', 'INSERT', 'LIMIT', 'MATCH', 'MERGE', 'NODETACH', 'OFFSET', 'OPTIONAL', 'REMOVE', 'RETURN', 'SET', 'SKIP', 'UNION', 'UNWIND', 'USE', 'WITH' or <EOF>",
860+
range: {
861+
end: {
862+
character: 19,
863+
line: 0,
864+
},
865+
start: {
866+
character: 15,
867+
line: 0,
868+
},
869+
},
870+
severity: 1,
871+
},
872+
]);
873+
});
874+
875+
test('provides symbol tables with correct positions', () => {
876+
const symbolTables = getSymbolTablesForQuery({
877+
query: ':auto MATCH (n) RETURN n',
878+
consoleCommandsEnabled: true,
879+
});
880+
expect(symbolTables).toEqual([
881+
[
882+
{
883+
variable: 'n',
884+
definitionPosition: 13,
885+
types: ['Node'],
886+
references: [13, 23],
887+
labels: {
888+
condition: 'and',
889+
children: [],
890+
},
891+
},
892+
],
893+
]);
894+
});
895+
896+
test('provides semantic errors', () => {
897+
const linting = getDiagnosticsForQuery({
898+
query: ':auto RETURN n',
899+
consoleCommandsEnabled: true,
900+
});
901+
expect(linting).toEqual([
902+
{
903+
offsets: {
904+
end: 14,
905+
start: 13,
906+
},
907+
message: 'Variable `n` not defined',
908+
range: {
909+
end: {
910+
character: 14,
911+
line: 0,
912+
},
913+
start: {
914+
character: 13,
915+
line: 0,
916+
},
917+
},
918+
severity: 1,
919+
},
920+
]);
921+
});
922+
923+
test('handles multiple statements', () => {
924+
const linting = getDiagnosticsForQuery({
925+
query: ':auto RETURN n; MATCH (n) RETURN n',
926+
consoleCommandsEnabled: true,
927+
});
928+
expect(linting).toEqual([
929+
{
930+
offsets: {
931+
end: 14,
932+
start: 13,
933+
},
934+
message: 'Variable `n` not defined',
935+
range: {
936+
end: {
937+
character: 14,
938+
line: 0,
939+
},
940+
start: {
941+
character: 13,
942+
line: 0,
943+
},
944+
},
945+
severity: 1,
946+
},
947+
]);
948+
});
949+
});
950+
788951
describe('command parser also handles cypher', () => {
789952
test('parses cypher', () => {
790953
expectParsedCommands('MATCH (n) RETURN n', [

0 commit comments

Comments
 (0)