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({