diff --git a/packages/language-server/src/lintWorker.ts b/packages/language-server/src/lintWorker.ts
index 2ab986f66..67aa41606 100644
--- a/packages/language-server/src/lintWorker.ts
+++ b/packages/language-server/src/lintWorker.ts
@@ -8,9 +8,12 @@ import workerpool from 'workerpool';
function lintCypherQuery(
query: string,
dbSchema: DbSchema,
- featureFlags: { cypher25?: boolean } = {},
+ featureFlags: { consoleCommands?: boolean; cypher25?: boolean } = {},
) {
// We allow to override the consoleCommands feature flag
+ if (featureFlags.consoleCommands !== undefined) {
+ _internalFeatureFlags.consoleCommands = featureFlags.consoleCommands;
+ }
if (featureFlags.cypher25 !== undefined) {
_internalFeatureFlags.cypher25 = featureFlags.cypher25;
}
diff --git a/packages/language-support/src/helpers.ts b/packages/language-support/src/helpers.ts
index 0e6d38949..77e17da13 100644
--- a/packages/language-support/src/helpers.ts
+++ b/packages/language-support/src/helpers.ts
@@ -7,6 +7,7 @@ import antlrDefaultExport, {
Token,
} from 'antlr4';
import { DbSchema } from './dbSchema';
+import { _internalFeatureFlags } from './featureFlags';
import CypherLexer from './generated-parser/CypherCmdLexer';
import CypherParser, {
NodePatternContext,
@@ -209,11 +210,15 @@ export function resolveCypherVersion(
parsedVersion: CypherVersion | undefined,
dbSchema: DbSchema,
) {
- const cypherVersion: CypherVersion =
- parsedVersion ??
- (dbSchema.defaultLanguage ? dbSchema.defaultLanguage : 'cypher 5');
-
- return cypherVersion;
+ if (_internalFeatureFlags.cypher25) {
+ const cypherVersion: CypherVersion =
+ parsedVersion ??
+ (dbSchema.defaultLanguage ? dbSchema.defaultLanguage : 'cypher 5');
+
+ return cypherVersion;
+ } else {
+ return 'cypher 5';
+ }
}
export const rulesDefiningVariables = [
diff --git a/packages/react-codemirror-playground/src/App.tsx b/packages/react-codemirror-playground/src/App.tsx
index f39abbdd1..0ce4bf3a4 100644
--- a/packages/react-codemirror-playground/src/App.tsx
+++ b/packages/react-codemirror-playground/src/App.tsx
@@ -112,7 +112,6 @@ export function App() {
theme={darkMode ? 'dark' : 'light'}
history={Object.values(demos)}
schema={schema}
- featureFlags={{ signatureInfoOnAutoCompletions: true }}
ariaLabel="Cypher Editor"
/>
diff --git a/packages/react-codemirror/src/CypherEditor.tsx b/packages/react-codemirror/src/CypherEditor.tsx
index e5e63156c..58a1e2c50 100644
--- a/packages/react-codemirror/src/CypherEditor.tsx
+++ b/packages/react-codemirror/src/CypherEditor.tsx
@@ -13,7 +13,10 @@ import {
placeholder,
ViewUpdate,
} from '@codemirror/view';
-import { type DbSchema } from '@neo4j-cypher/language-support';
+import {
+ _internalFeatureFlags,
+ type DbSchema,
+} from '@neo4j-cypher/language-support';
import debounce from 'lodash.debounce';
import { Component, createRef } from 'react';
import { DEBOUNCE_TIME } from './constants';
@@ -99,7 +102,7 @@ export interface CypherEditorProps {
*/
featureFlags?: {
consoleCommands?: boolean;
- signatureInfoOnAutoCompletions?: boolean;
+ cypher25?: boolean;
};
/**
* The schema to use for autocompletion and linting.
@@ -346,6 +349,10 @@ export class CypherEditor extends Component<
newLineOnEnter,
} = this.props;
+ if (featureFlags.cypher25) {
+ _internalFeatureFlags.cypher25 = featureFlags.cypher25;
+ }
+
this.schemaRef.current = {
schema,
lint,
diff --git a/packages/react-codemirror/src/e2e_tests/autoCompletion.spec.tsx b/packages/react-codemirror/src/e2e_tests/autoCompletion.spec.tsx
index d98c6e2bc..e52bca1c4 100644
--- a/packages/react-codemirror/src/e2e_tests/autoCompletion.spec.tsx
+++ b/packages/react-codemirror/src/e2e_tests/autoCompletion.spec.tsx
@@ -312,14 +312,7 @@ test('shows signature help information on auto-completion for procedures', async
page,
mount,
}) => {
- await mount(
- ,
- );
+ await mount();
const procName = 'apoc.periodic.iterate';
const procedure = testData.mockSchema.procedures['cypher 5'][procName];
@@ -337,14 +330,7 @@ test('shows signature help information on auto-completion for functions', async
page,
mount,
}) => {
- await mount(
- ,
- );
+ await mount();
const fnName = 'apoc.coll.combinations';
const fn = testData.mockSchema.functions['cypher 5'][fnName];
@@ -373,9 +359,6 @@ test('shows deprecated procedures as strikethrough on auto-completion', async ({
},
},
}}
- featureFlags={{
- signatureInfoOnAutoCompletions: true,
- }}
/>,
);
const textField = page.getByRole('textbox');
@@ -401,9 +384,6 @@ test('shows deprecated function as strikethrough on auto-completion', async ({
},
},
}}
- featureFlags={{
- signatureInfoOnAutoCompletions: true,
- }}
/>,
);
const textField = page.getByRole('textbox');
@@ -418,14 +398,7 @@ test('does not signature help information on auto-completion if docs and signatu
page,
mount,
}) => {
- await mount(
- ,
- );
+ await mount();
const textField = page.getByRole('textbox');
await textField.fill('C');
@@ -452,9 +425,6 @@ test('shows signature help information on auto-completion if description is not
},
},
}}
- featureFlags={{
- signatureInfoOnAutoCompletions: true,
- }}
/>,
);
@@ -483,9 +453,6 @@ test('shows signature help information on auto-completion if signature is not em
},
},
}}
- featureFlags={{
- signatureInfoOnAutoCompletions: true,
- }}
/>,
);
@@ -495,3 +462,41 @@ test('shows signature help information on auto-completion if signature is not em
await expect(page.locator('.cm-tooltip-autocomplete')).toBeVisible();
await expect(page.locator('.cm-completionInfo')).toBeVisible();
});
+
+test('completions are cypher version dependant', async ({ page, mount }) => {
+ await mount(
+ ,
+ );
+
+ const textField = page.getByRole('textbox');
+
+ await textField.fill('CYPHER 5 RETURN cypher');
+
+ await expect(
+ page.locator('.cm-tooltip-autocomplete').getByText('cypher5Function'),
+ ).toBeVisible();
+
+ await textField.fill('CYPHER 25 RETURN cypher');
+
+ await expect(
+ page.locator('.cm-tooltip-autocomplete').getByText('cypher25Function'),
+ ).toBeVisible();
+});
diff --git a/packages/react-codemirror/src/lang-cypher/langCypher.ts b/packages/react-codemirror/src/lang-cypher/langCypher.ts
index 43160009d..47fdc6faa 100644
--- a/packages/react-codemirror/src/lang-cypher/langCypher.ts
+++ b/packages/react-codemirror/src/lang-cypher/langCypher.ts
@@ -20,6 +20,7 @@ export type CypherConfig = {
showSignatureTooltipBelow?: boolean;
featureFlags?: {
consoleCommands?: boolean;
+ cypher25?: boolean;
};
schema?: DbSchema;
useLightVersion: boolean;
diff --git a/packages/react-codemirror/src/lang-cypher/lintWorker.ts b/packages/react-codemirror/src/lang-cypher/lintWorker.ts
index d2c819e71..67aa41606 100644
--- a/packages/react-codemirror/src/lang-cypher/lintWorker.ts
+++ b/packages/react-codemirror/src/lang-cypher/lintWorker.ts
@@ -8,12 +8,15 @@ import workerpool from 'workerpool';
function lintCypherQuery(
query: string,
dbSchema: DbSchema,
- featureFlags: { consoleCommands?: boolean } = {},
+ featureFlags: { consoleCommands?: boolean; cypher25?: boolean } = {},
) {
// We allow to override the consoleCommands feature flag
if (featureFlags.consoleCommands !== undefined) {
_internalFeatureFlags.consoleCommands = featureFlags.consoleCommands;
}
+ if (featureFlags.cypher25 !== undefined) {
+ _internalFeatureFlags.cypher25 = featureFlags.cypher25;
+ }
return _lintCypherQuery(query, dbSchema);
}
diff --git a/packages/vscode-extension/package.json b/packages/vscode-extension/package.json
index 50c013804..cff1757c0 100644
--- a/packages/vscode-extension/package.json
+++ b/packages/vscode-extension/package.json
@@ -305,8 +305,8 @@
"build": "tsc -b && npm run gen-textmate && npm run bundle-extension && npm run bundle-language-server && npm run bundle-webview-controllers",
"build:dev": "tsc -b && npm run gen-textmate && npm run bundle-extension:dev && npm run bundle-language-server && npm run bundle-webview-controllers",
"clean": "rm -rf dist",
- "test:e2e": "npm run build && npm run test:apiAndUnit && npm run test:webviews",
- "test:apiAndUnit": "rm -rf .vscode-test/user-data && node ./dist/tests/runApiAndUnitTests.js",
+ "test:e2e": "npm run build:dev && npm run test:apiAndUnit && npm run test:webviews",
+ "test:apiAndUnit": "npm run build:dev && rm -rf .vscode-test/user-data && node ./dist/tests/runApiAndUnitTests.js",
"test:webviews": "wdio run ./dist/tests/runWebviewTests.js"
},
"dependencies": {
diff --git a/packages/vscode-extension/tests/helpers.ts b/packages/vscode-extension/tests/helpers.ts
index 9f26f2e8b..b783de65d 100644
--- a/packages/vscode-extension/tests/helpers.ts
+++ b/packages/vscode-extension/tests/helpers.ts
@@ -1,4 +1,5 @@
import * as path from 'path';
+import * as vscode from 'vscode';
import { TextDocument, Uri, window, workspace } from 'vscode';
import { Connection } from '../src/connectionService';
import { getNonce } from '../src/getNonce';
@@ -21,6 +22,8 @@ export async function newUntitledFileWithContent(
// The language server will not be activated automatically
const document = await workspace.openTextDocument({ content: content });
await window.showTextDocument(document);
+ const editor = vscode.window.activeTextEditor;
+ await vscode.languages.setTextDocumentLanguage(editor.document, 'cypher');
return document;
} catch (e) {
console.error(e);
@@ -85,3 +88,31 @@ export function getNeo4jConfiguration() {
password: process.env.NEO4J_PASSWORD || 'password',
};
}
+
+export function rangeToString(range: vscode.Range) {
+ return `${range.start.line}:${range.start.character} to ${range.end.line}:${range.end.character}`;
+}
+
+export function documentationToString(
+ doc: string | vscode.MarkdownString | undefined,
+) {
+ if (typeof doc === 'string') {
+ return doc;
+ } else if (typeof doc === 'undefined') {
+ return 'undefined';
+ } else {
+ return doc.value;
+ }
+}
+
+export function tagsToString(doc: readonly vscode.CompletionItemTag[]) {
+ return doc.map((tag) => tag.toString()).join(', ');
+}
+
+export function parameterLabelToString(label: string | [number, number]) {
+ if (Array.isArray(label)) {
+ return `${label[0]}:${label[1]}`;
+ } else {
+ return label;
+ }
+}
diff --git a/packages/vscode-extension/tests/specs/api/autoCompletion.spec.ts b/packages/vscode-extension/tests/specs/api/autoCompletion.spec.ts
index f19deb450..29e7ff842 100644
--- a/packages/vscode-extension/tests/specs/api/autoCompletion.spec.ts
+++ b/packages/vscode-extension/tests/specs/api/autoCompletion.spec.ts
@@ -2,10 +2,17 @@ import { testData } from '@neo4j-cypher/language-support';
import * as assert from 'assert';
import * as vscode from 'vscode';
import { CompletionItemTag } from 'vscode-languageclient';
-import { eventually, getDocumentUri, openDocument } from '../../helpers';
+import {
+ documentationToString,
+ eventually,
+ getDocumentUri,
+ newUntitledFileWithContent,
+ openDocument,
+ tagsToString,
+} from '../../helpers';
type InclusionTestArgs = {
- textFile: string;
+ textFile: string | vscode.Uri;
position: vscode.Position;
expected: vscode.CompletionItem[];
};
@@ -16,10 +23,14 @@ export async function testCompletionContains({
textFile,
position,
expected,
-}: InclusionTestArgs) {
- const docUri = getDocumentUri(textFile);
-
- await openDocument(docUri);
+}: InclusionTestArgs): Promise {
+ let docUri: vscode.Uri;
+ if (typeof textFile === 'string') {
+ docUri = getDocumentUri(textFile);
+ await openDocument(docUri);
+ } else {
+ docUri = textFile;
+ }
await eventually(async () => {
const actualCompletionList: vscode.CompletionList =
@@ -36,10 +47,30 @@ export async function testCompletionContains({
value.label === expectedItem.label,
);
- assert.equal(found !== undefined, true);
- assert.equal(found.detail, expectedItem.detail);
- assert.equal(found.documentation, expectedItem.documentation);
- assert.deepStrictEqual(found.tags, expectedItem.tags);
+ assert.equal(
+ found !== undefined,
+ true,
+ `Expected item not found by kind and label`,
+ );
+ assert.equal(
+ found.detail,
+ expectedItem.detail,
+ `Detail does not match. Actual: ${found.detail}, expected: ${expectedItem.detail}`,
+ );
+ assert.equal(
+ found.documentation,
+ expectedItem.documentation,
+ `Documentation does not match. Actual: ${documentationToString(
+ found.documentation,
+ )}, expected: ${documentationToString(expectedItem.documentation)}`,
+ );
+ assert.deepStrictEqual(
+ found.tags,
+ expectedItem.tags,
+ `Tags do not match. Actual: ${tagsToString(
+ found.tags,
+ )}, expected: ${tagsToString(expectedItem.tags)}`,
+ );
});
});
}
@@ -145,4 +176,41 @@ suite('Auto completion spec', () => {
expected: expected,
});
});
+
+ test('Completions are Cypher version dependant', async () => {
+ const textDocument = await newUntitledFileWithContent(`
+ CYPHER 5 RETURN ;
+ CYPHER 25 RETURN
+ `);
+ const cypher5Position = new vscode.Position(1, 22);
+ const cypher5Expected: vscode.CompletionItem[] = [
+ {
+ label: 'apoc.create.uuid',
+ kind: vscode.CompletionItemKind.Function,
+ detail:
+ '(function) ' + functions['cypher 5']['apoc.create.uuid'].signature,
+ documentation: functions['cypher 5']['apoc.create.uuid'].description,
+ tags: [CompletionItemTag.Deprecated],
+ },
+ ];
+ await testCompletionContains({
+ textFile: textDocument.uri,
+ position: cypher5Position,
+ expected: cypher5Expected,
+ });
+
+ const cypher25Position = new vscode.Position(2, 23);
+
+ // TODO Using assert.rejects is not ideal but I couldn't find
+ // a procedure that was specifically added in Cypher 25
+ // In next apoc releases, apoc.refactor.deleteAndReconnect
+ // will be deprecated in Cypher 25, so we could improve this test
+ await assert.rejects(
+ testCompletionContains({
+ textFile: textDocument.uri,
+ position: cypher25Position,
+ expected: cypher5Expected,
+ }),
+ );
+ });
});
diff --git a/packages/vscode-extension/tests/specs/api/signatureHelp.spec.ts b/packages/vscode-extension/tests/specs/api/signatureHelp.spec.ts
index 884accfcb..d18d6307e 100644
--- a/packages/vscode-extension/tests/specs/api/signatureHelp.spec.ts
+++ b/packages/vscode-extension/tests/specs/api/signatureHelp.spec.ts
@@ -4,10 +4,17 @@ import {
} from '@neo4j-cypher/language-support';
import * as assert from 'assert';
import * as vscode from 'vscode';
-import { eventually, getDocumentUri, openDocument } from '../../helpers';
+import {
+ documentationToString,
+ eventually,
+ getDocumentUri,
+ newUntitledFileWithContent,
+ openDocument,
+ parameterLabelToString,
+} from '../../helpers';
type InclusionTestArgs = {
- textFile: string;
+ textFile: string | vscode.Uri;
position: vscode.Position;
expected: vscode.SignatureHelp;
};
@@ -17,9 +24,13 @@ export async function testSignatureHelp({
position,
expected,
}: InclusionTestArgs) {
- const docUri = getDocumentUri(textFile);
-
- await openDocument(docUri);
+ let docUri: vscode.Uri;
+ if (typeof textFile === 'string') {
+ docUri = getDocumentUri(textFile);
+ await openDocument(docUri);
+ } else {
+ docUri = textFile;
+ }
await eventually(async () => {
const signatureHelp: vscode.SignatureHelp =
@@ -29,7 +40,11 @@ export async function testSignatureHelp({
position,
);
- assert.equal(signatureHelp.activeParameter, expected.activeParameter);
+ assert.equal(
+ signatureHelp.activeParameter,
+ expected.activeParameter,
+ `Active parameter does not match. Actual: ${signatureHelp.activeParameter}, expected: ${expected.activeParameter}`,
+ );
expected.signatures.forEach((expectedSignature) => {
const foundSignature = signatureHelp.signatures.find((signature) => {
@@ -39,6 +54,11 @@ export async function testSignatureHelp({
assert.equal(
foundSignature.documentation,
expectedSignature.documentation,
+ `Documentation for the signature does not match. Actual: ${documentationToString(
+ foundSignature.documentation,
+ )}, expected: ${documentationToString(
+ expectedSignature.documentation,
+ )}`,
);
expectedSignature.parameters.forEach((expectedParameter) => {
@@ -47,8 +67,15 @@ export async function testSignatureHelp({
);
assert.equal(
- foundParameter.documentation,
+ foundParameter?.documentation,
expectedParameter.documentation,
+ `Documentation for the parameter ${parameterLabelToString(
+ expectedParameter.label,
+ )} does not match. Actual: ${documentationToString(
+ foundParameter?.documentation,
+ )}, expected: ${documentationToString(
+ expectedParameter.documentation,
+ )}`,
);
});
});
@@ -159,4 +186,44 @@ suite('Signature help spec', () => {
expected: expected,
});
});
+
+ test('Signature help is cypher version dependant', async () => {
+ const textDocument = await newUntitledFileWithContent(`
+ CYPHER 5 RETURN apoc.create.uuid( ;
+ CYPHER 25 RETURN apoc.create.uuid(
+ `);
+ const cypher5Position = new vscode.Position(1, 43);
+ const cypher25Position = new vscode.Position(2, 44);
+
+ const cypher5Expected: vscode.SignatureHelp = {
+ // This is what would make it show only the function description
+ // since there are only 3 arguments in the signature and the last index is 2
+ activeParameter: 0,
+ activeSignature: undefined,
+ signatures: [
+ toSignatureInformation(
+ testData.mockSchema.functions['cypher 5']['apoc.create.uuid'],
+ ) as vscode.SignatureInformation,
+ ],
+ };
+
+ await testSignatureHelp({
+ textFile: textDocument.uri,
+ position: cypher5Position,
+ expected: cypher5Expected,
+ });
+
+ // TODO Using assert.rejects is not ideal but I couldn't find
+ // a procedure that was specifically added in Cypher 25
+ // In next apoc releases, apoc.cypher.runTimeboxed
+ // will add an extra config argument in Cypher 25,
+ // so we could improve this test
+ await assert.rejects(
+ testSignatureHelp({
+ textFile: textDocument.uri,
+ position: cypher25Position,
+ expected: cypher5Expected,
+ }),
+ );
+ });
});
diff --git a/packages/vscode-extension/tests/specs/api/syntaxValidation.spec.ts b/packages/vscode-extension/tests/specs/api/syntaxValidation.spec.ts
index be9244980..725afb172 100644
--- a/packages/vscode-extension/tests/specs/api/syntaxValidation.spec.ts
+++ b/packages/vscode-extension/tests/specs/api/syntaxValidation.spec.ts
@@ -7,6 +7,7 @@ import {
getNeo4jConfiguration,
newUntitledFileWithContent,
openDocument,
+ rangeToString,
} from '../../helpers';
import {
connectDefault,
@@ -19,10 +20,6 @@ type InclusionTestArgs = {
expected: vscode.Diagnostic[];
};
-function rangeToString(range: vscode.Range) {
- return `${range.start.line}:${range.start.character} to ${range.end.line}:${range.end.character}`;
-}
-
export async function testSyntaxValidation({
docUri,
expected,
@@ -298,9 +295,6 @@ suite('Syntax validation spec', () => {
CYPHER 5 CALL apoc.create.uuids(5);
CYPHER 25 CALL apoc.create.uuids(5)
`);
- const editor = vscode.window.activeTextEditor;
- await vscode.languages.setTextDocumentLanguage(editor.document, 'cypher');
-
// We need to wait here because diagnostics are eventually
// consistent i.e. they don't show up immediately
await testSyntaxValidation({