diff --git a/packages/language-server/src/linting.ts b/packages/language-server/src/linting.ts index fe3df0f6b..823b2a703 100644 --- a/packages/language-server/src/linting.ts +++ b/packages/language-server/src/linting.ts @@ -46,7 +46,6 @@ async function rawLintDocument( document: TextDocument, sendDiagnostics: (diagnostics: Diagnostic[]) => void, neo4j: Neo4jSchemaPoller, - versionedLinters: boolean, ) { const query = document.getText(); if (query.length === 0) { @@ -62,9 +61,7 @@ async function rawLintDocument( const proxyWorker = (await pool.proxy()) as unknown as LintWorker; - const fixedDbSchema = versionedLinters - ? convertDbSchema(dbSchema, linterVersion) - : dbSchema; + const fixedDbSchema = convertDbSchema(dbSchema, linterVersion); lastSemanticJob = proxyWorker.lintCypherQuery( query, fixedDbSchema, diff --git a/packages/language-server/src/server.ts b/packages/language-server/src/server.ts index 2303cb125..0c8107580 100644 --- a/packages/language-server/src/server.ts +++ b/packages/language-server/src/server.ts @@ -43,7 +43,6 @@ async function lintSingleDocument(document: TextDocument): Promise { }); }, neo4jSchemaPoller, - settings?.features?.useVersionedLinters, ); } else { void connection.sendDiagnostics({ diff --git a/packages/language-server/src/types.ts b/packages/language-server/src/types.ts index c894f4b31..2ec5e8640 100644 --- a/packages/language-server/src/types.ts +++ b/packages/language-server/src/types.ts @@ -16,7 +16,7 @@ export type Neo4jSettings = { trace: { server: 'off' | 'messages' | 'verbose'; }; - features: { linting: boolean; useVersionedLinters?: boolean }; + features: { linting: boolean }; }; export type Neo4jParameters = Record; diff --git a/packages/lint-worker/package.json b/packages/lint-worker/package.json index 86bb3178d..0cd97cd33 100644 --- a/packages/lint-worker/package.json +++ b/packages/lint-worker/package.json @@ -18,7 +18,7 @@ "cypher", "lint worker" ], - "version": "0.1.0-next.0", + "version": "1.10.0", "repository": { "type": "git", "url": "git://github.com/neo4j/cypher-language-support.git" @@ -34,7 +34,6 @@ "@neo4j-cypher/language-support": "workspace:*", "@neo4j-cypher/query-tools": "workspace:*", "languageSupport-next.13": "npm:@neo4j-cypher/language-support@2.0.0-next.13", - "languageSupport-next.8": "npm:@neo4j-cypher/language-support@2.0.0-next.8", "vscode-languageserver": "^8.1.0", "vscode-languageserver-types": "^3.17.3", "workerpool": "^9.0.4", diff --git a/packages/lint-worker/src/helpers.ts b/packages/lint-worker/src/helpers.ts index 1f5b4affa..aae3ed779 100644 --- a/packages/lint-worker/src/helpers.ts +++ b/packages/lint-worker/src/helpers.ts @@ -7,28 +7,26 @@ import { import axios from 'axios'; import { DbSchema as DbSchemaV1 } from 'languageSupport-next.13'; -const oldLinter = '5.20.0'; - // for older versions of the language support, the dbschema was not the same, // meaning old linters need conversion of the new schema export function convertDbSchema( originalSchema: DbSchemaV2, linterVersion: string, ): DbSchemaV2 | DbSchemaV1 { - let oldFunctions: Record = {}; - let oldProcedures: Record = {}; if (!originalSchema) { return originalSchema; } - if ( - originalSchema.functions['CYPHER 5'] && - originalSchema.procedures['CYPHER 5'] - ) { - oldFunctions = originalSchema.functions['CYPHER 5']; - oldProcedures = originalSchema.procedures['CYPHER 5']; - } - if (compareMajorMinorVersions(linterVersion, oldLinter) <= 0) { + if (compareMajorMinorVersions(linterVersion, '2025.01') < 0) { + let oldFunctions: Record | undefined = undefined; + let oldProcedures: Record | undefined = undefined; + if (originalSchema.functions && originalSchema.functions['CYPHER 5']) { + oldFunctions = originalSchema.functions['CYPHER 5']; + } + + if (originalSchema.procedures && originalSchema.procedures['CYPHER 5']) { + oldProcedures = originalSchema.procedures['CYPHER 5']; + } const dbSchemaOld: DbSchemaV1 = { ...originalSchema, functions: oldFunctions, @@ -40,12 +38,30 @@ export function convertDbSchema( } } -export function serverVersionToLinter(serverVersion: string) { - let candidate: string = 'Latest'; - if (compareMajorMinorVersions(serverVersion, oldLinter) <= 0) { - candidate = oldLinter; +export function serverVersionToLinter(serverVersion: string): { + linterVersion: string; + notResolved?: boolean; + notSupported?: boolean; +} { + // Extract only the major and minor + const versionRegex = /^\d+\.\d+/; + const linterVersion = serverVersion.match(versionRegex)?.[0]; + + // If we have a version lower than 5.23, use that linter + if (compareMajorMinorVersions(serverVersion, '5.23') < 0) { + return { linterVersion: '5.23', notSupported: true }; + // Unfortunately 2025.01, 2025.02 and 2025.03 all return 5.27 + // so we have to assume we are on the most modern database from all those + // + // This case should never happen though because we should have cleaned up the + // version by the moment we call this method + } else if (compareMajorMinorVersions(serverVersion, '5.27') === 0) { + return { linterVersion: '2025.03' }; + } else if (linterVersion) { + return { linterVersion: linterVersion }; } - return candidate; + + return { linterVersion: 'Default', notResolved: true }; } export function linterFileToServerVersion(fileName: string) { @@ -64,7 +80,7 @@ export type NpmRelease = { }; export const npmTagToLinterVersion = (tag: string) => - tag.match(/^neo4j-([\d.]+)$/)?.[1]; + tag.match(/^neo4j-(\d+\.\d+)$/)?.[1]; export async function getTaggedRegistryVersions(): Promise { const registryUrl = 'https://registry.npmjs.org/@neo4j-cypher/lint-worker'; @@ -74,7 +90,7 @@ export async function getTaggedRegistryVersions(): Promise { const taggedVersions: { tag: string; version: string }[] = []; if (data !== null && data['dist-tags'] !== null) { for (const [tag, version] of Object.entries(data['dist-tags'])) { - if (typeof tag === 'string' && typeof version === 'string') { + if (npmTagToLinterVersion(tag)) { taggedVersions.push({ tag, version }); } } diff --git a/packages/lint-worker/src/lintWorker.ts b/packages/lint-worker/src/lintWorker.ts index 970de4a2f..412ddf2cf 100644 --- a/packages/lint-worker/src/lintWorker.ts +++ b/packages/lint-worker/src/lintWorker.ts @@ -13,7 +13,7 @@ function lintCypherQuery( query: string, dbSchema, featureFlags: { consoleCommands?: boolean } = {}, -): { diagnostics: SyntaxDiagnostic[]; symbolTables: SymbolTable[] } { +): { diagnostics: SyntaxDiagnostic[]; symbolTables?: SymbolTable[] } { // We allow to override the consoleCommands feature flag if (featureFlags.consoleCommands !== undefined) { _internalFeatureFlags.consoleCommands = featureFlags.consoleCommands; diff --git a/packages/lint-worker/src/tests/version.test.ts b/packages/lint-worker/src/tests/version.test.ts index add90a7f6..74a7f9991 100644 --- a/packages/lint-worker/src/tests/version.test.ts +++ b/packages/lint-worker/src/tests/version.test.ts @@ -2,6 +2,14 @@ import { compareMajorMinorVersions } from '../version'; describe('Version comparison', () => { test('Comparing versions for major/minor version', () => { + expect(compareMajorMinorVersions('5.24.0', '2025.01')).toBe(-1); + expect(compareMajorMinorVersions('5.22.0', '5.23')).toBe(-1); + expect(compareMajorMinorVersions('5.24', '2025.01')).toBe(-1); + expect(compareMajorMinorVersions('2025.01', '5.24.0')).toBe(1); + expect(compareMajorMinorVersions('2025.01', '5.24')).toBe(1); + expect(compareMajorMinorVersions('2025.01', '2025.02')).toBe(-1); + expect(compareMajorMinorVersions('2025.01', '2025.01')).toBe(0); + expect(compareMajorMinorVersions('5.24.0', '5.25.0')).toBe(-1); expect(compareMajorMinorVersions('5.24.0', '5.25.1')).toBe(-1); expect(compareMajorMinorVersions('5.24.5', '5.25.3')).toBe(-1); @@ -29,11 +37,5 @@ describe('Version comparison', () => { expect(compareMajorMinorVersions('4.4.1', '3.5.2000')).toBe(1); expect(compareMajorMinorVersions('2024.5.0', '2023.500.30')).toBe(1); expect(compareMajorMinorVersions('4.5.0-alpha', '3.5.0-beta')).toBe(1); - - expect(compareMajorMinorVersions('2025.06.3-alpha', '3.y.3-beta')).toBe( - undefined, - ); - expect(compareMajorMinorVersions('5.02.y', '3.01.y')).toBe(undefined); - expect(compareMajorMinorVersions('bla.bla.bla', '5.25.0')).toBe(undefined); }); }); diff --git a/packages/lint-worker/src/version.ts b/packages/lint-worker/src/version.ts index c19d510eb..8ef9910a0 100644 --- a/packages/lint-worker/src/version.ts +++ b/packages/lint-worker/src/version.ts @@ -13,14 +13,17 @@ export function compareMajorMinorVersions( ): integer | undefined { const semVer1: semver.SemVer | null = semver.coerce(version1, { includePrerelease: false, + loose: true, }); const semVer2: semver.SemVer | null = semver.coerce(version2, { includePrerelease: false, + loose: true, }); if (semVer1 && semVer2) { semVer1.patch = 0; semVer2.patch = 0; - return semver.compare(semVer1, semVer2); + const result = semver.compare(semVer1, semVer2, true); + return result; } return undefined; } diff --git a/packages/query-tools/src/schemaPoller.ts b/packages/query-tools/src/schemaPoller.ts index 11e370946..75ec07a5e 100644 --- a/packages/query-tools/src/schemaPoller.ts +++ b/packages/query-tools/src/schemaPoller.ts @@ -207,12 +207,24 @@ export class Neo4jSchemaPoller { const { query: serverVersionQuery, queryConfig: serverVersionQueryConfig } = getVersion(); - const { serverVersion } = await this.driver.executeQuery( + let { serverVersion } = await this.driver.executeQuery( serverVersionQuery, {}, serverVersionQueryConfig, ); + // If the server version is 5.27.0 we have to use the agent to + // really know whether we are in 2025.01, 2025.02 or 2025.03 + if (serverVersion?.startsWith('5.27.0')) { + const agent = summary.server.agent; + if (agent) { + const matchedVersion = agent.match(/\d+\.\d+\.\d+/); + if (matchedVersion) { + serverVersion = matchedVersion[0]; + } + } + } + this.connection.serverVersion = serverVersion; this.metadata = new ConnectedMetadataPoller( diff --git a/packages/vscode-extension/CHANGELOG.md b/packages/vscode-extension/CHANGELOG.md index 9e3794587..c0f52b4ab 100644 --- a/packages/vscode-extension/CHANGELOG.md +++ b/packages/vscode-extension/CHANGELOG.md @@ -1,6 +1,7 @@ # neo4j-for-vscode ## 1.13.0 +- Adjusts linting automatically depending on the neo4j version. - Updates grammar and semantic analysis ## 1.12.0 diff --git a/packages/vscode-extension/README.md b/packages/vscode-extension/README.md index fe8a33ca2..c30735ba8 100644 --- a/packages/vscode-extension/README.md +++ b/packages/vscode-extension/README.md @@ -123,11 +123,21 @@ Once you've written your desired query (for example `CREATE (n)-[r:Rel]->(m) RET ![demo-execution](https://github.com/neo4j/cypher-language-support/blob/main/packages/vscode-extension/resources/images/demo-execution.png?raw=true) -## Upcoming features +## Version tailored linting -We're working on adding more features to the extension, such as: +Our aim is to provide an experience that suits the different neo4j versions you could be connected to. When connecting to a database, a linter that matches that version of the database will be automatically downloaded. -- Dynamically adjusting the language server depending on the neo4j version. +![demo-linter-automatic-adjusting](https://github.com/neo4j/cypher-language-support/blob/main/packages/vscode-extension/resources/images/demo-linter-automatic-adjusting.png?raw=true). + +![demo-linter-5](https://github.com/neo4j/cypher-language-support/blob/main/packages/vscode-extension/resources/images/demo-linter-5.png?raw=true). + +![demo-linter-2025](https://github.com/neo4j/cypher-language-support/blob/main/packages/vscode-extension/resources/images/demo-linter-2025.png?raw=true). + +We can match a neo4j version from 5.23 onwards. If connected to an older database, the 5.23 version will be used. If the version cannot be resolved from your neo4j instance for any reason, the `Default` linter (the one packaged with the current version of the VSCode extension) will be used. + +The linter can be manually adjusted either on the bottom menu or using the `Neo4j: Select Cypher linter version` command from the Command Palette. + +![demo-linter-manual-adjusting](https://github.com/neo4j/cypher-language-support/blob/main/packages/vscode-extension/resources/images/demo-linter-manual-adjusting.png?raw=true). ## Extension settings diff --git a/packages/vscode-extension/package.json b/packages/vscode-extension/package.json index 7a9263c65..0691698c0 100644 --- a/packages/vscode-extension/package.json +++ b/packages/vscode-extension/package.json @@ -40,7 +40,7 @@ { "command": "neo4j.switchLinter", "category": "Neo4j", - "title": "Pick linter" + "title": "Select Cypher linter version" }, { "command": "neo4j.editConnection", @@ -160,8 +160,7 @@ ], "commandPalette": [ { - "command": "neo4j.switchLinter", - "when": "neo4j:useVersionedLinters" + "command": "neo4j.switchLinter" }, { "command": "neo4j.deleteConnection", @@ -445,16 +444,6 @@ ], "default": true, "description": "Enable linting errors for Cypher queries" - }, - "neo4j.features.useVersionedLinters": { - "scope": "window", - "type": "boolean", - "enum": [ - true, - false - ], - "default": false, - "description": "Enable neo4j version specifc linting" } } }, @@ -478,9 +467,9 @@ "dev": "tsc -b && pnpm gen-textmate && concurrently 'node --experimental-strip-types esbuild-extension.mts' 'pnpm bundle-webview-controllers --watch' ", "clean": "rm -rf {dist,tsconfig.tsbuildinfo}", "package": "pnpm vsce package --pre-release --no-dependencies", - "test:e2e": "DEBUG_VSCODE_TESTS=false && pnpm build:dev && pnpm test:apiAndUnit && pnpm test:webviews", - "test:apiAndUnit": "DEBUG_VSCODE_TESTS=false && pnpm build:dev && rm -rf .vscode-test/user-data && node ./dist/tests/runApiAndUnitTests.js", - "test:webviews": "DEBUG_VSCODE_TESTS=false && pnpm build:dev && wdio run ./dist/tests/runWebviewTests.js" + "test:e2e": "pnpm build:dev && cross-env DEBUG_VSCODE_TESTS=false pnpm test:apiAndUnit && cross-env DEBUG_VSCODE_TESTS=false pnpm test:webviews", + "test:apiAndUnit": "pnpm build:dev && rm -rf .vscode-test/user-data && cross-env DEBUG_VSCODE_TESTS=false node ./dist/tests/runApiAndUnitTests.js", + "test:webviews": "pnpm build:dev && cross-env DEBUG_VSCODE_TESTS=false wdio run ./dist/tests/runWebviewTests.js" }, "dependencies": { "@neo4j-cypher/language-server": "workspace:*", diff --git a/packages/vscode-extension/resources/images/demo-linter-2025.png b/packages/vscode-extension/resources/images/demo-linter-2025.png new file mode 100644 index 000000000..5f222ba7b Binary files /dev/null and b/packages/vscode-extension/resources/images/demo-linter-2025.png differ diff --git a/packages/vscode-extension/resources/images/demo-linter-5.png b/packages/vscode-extension/resources/images/demo-linter-5.png new file mode 100644 index 000000000..a56b302b6 Binary files /dev/null and b/packages/vscode-extension/resources/images/demo-linter-5.png differ diff --git a/packages/vscode-extension/resources/images/demo-linter-automatic-adjusting.png b/packages/vscode-extension/resources/images/demo-linter-automatic-adjusting.png new file mode 100644 index 000000000..1f493ae79 Binary files /dev/null and b/packages/vscode-extension/resources/images/demo-linter-automatic-adjusting.png differ diff --git a/packages/vscode-extension/resources/images/demo-linter-manual-adjusting.png b/packages/vscode-extension/resources/images/demo-linter-manual-adjusting.png new file mode 100644 index 000000000..915aa8dee Binary files /dev/null and b/packages/vscode-extension/resources/images/demo-linter-manual-adjusting.png differ diff --git a/packages/vscode-extension/src/commandHandlers/linters.ts b/packages/vscode-extension/src/commandHandlers/linters.ts index c1eacd729..482e6e7a3 100644 --- a/packages/vscode-extension/src/commandHandlers/linters.ts +++ b/packages/vscode-extension/src/commandHandlers/linters.ts @@ -1,4 +1,5 @@ import { + compareMajorMinorVersions, getTaggedRegistryVersions, linterFileToServerVersion, npmTagToLinterVersion, @@ -21,12 +22,16 @@ export async function manuallyAdjustLinter(): Promise { const existingVersions = fileNames.map((name) => linterFileToServerVersion(name), ); - existingVersions.push('Latest'); const allVersions = new Set( existingVersions.concat(npmLinterVersions).filter((v) => v !== undefined), ); - const picked = await window.showQuickPick(Array.from(allVersions), { - placeHolder: 'Select Linter version', + // This is to show Default on top and then the versions in decreasing order + const sanitizedVersions = [ + 'Default', + ...Array.from(allVersions).sort(compareMajorMinorVersions).reverse(), + ]; + const picked = await window.showQuickPick(sanitizedVersions, { + placeHolder: 'Select Cypher linter version', }); //closing the quickpick menu will return undefined if (picked === undefined) { diff --git a/packages/vscode-extension/src/connectionService.ts b/packages/vscode-extension/src/connectionService.ts index 2f599f4c9..5a3caa070 100644 --- a/packages/vscode-extension/src/connectionService.ts +++ b/packages/vscode-extension/src/connectionService.ts @@ -12,8 +12,7 @@ import * as schemaPollerEventHandlers from './schemaPollerEventHandlers'; import { connectionTreeDataProvider } from './treeviews/connectionTreeDataProvider'; import { databaseInformationTreeDataProvider } from './treeviews/databaseInformationTreeDataProvider'; import { displayMessageForConnectionResult } from './uiUtils'; -import * as vscode from 'vscode'; -import { dynamicallyAdjustLinter } from './linterSwitching'; +import { dynamicallyAdjustLinter, switchToLinter } from './linterSwitching'; export type Scheme = | 'neo4j' @@ -422,10 +421,23 @@ async function connectToDatabaseAndNotifyLanguageClient( ? await sendNotificationToLanguageClient('connectionUpdated', settings) : await sendNotificationToLanguageClient('connectionDisconnected'); - const config = vscode.workspace.getConfiguration('neo4j.features'); - const versionedLintersEnabled = config.get('useVersionedLinters', false); - if (result.success && versionedLintersEnabled) { - await dynamicallyAdjustLinter(); + // Note the e2e tests are always going to be on an older neo4j version (a docker container) + // We want for all of the tests to run with the latest version of the linter, + // not an older one (which would not make sense to debug them for example) + // + // except for the tests that are specifically about switching the linter + if (result.success) { + if (process.env.DEBUG_VSCODE_TESTS !== undefined) { + // tests code + if (process.env.LINTER_SWITCHING_TESTS === 'true') { + await dynamicallyAdjustLinter(); + } else { + await switchToLinter('Default', []); + } + } else { + // production code + await dynamicallyAdjustLinter(); + } } await saveConnection({ diff --git a/packages/vscode-extension/src/constants.ts b/packages/vscode-extension/src/constants.ts index e7e54dcdc..226eaab69 100644 --- a/packages/vscode-extension/src/constants.ts +++ b/packages/vscode-extension/src/constants.ts @@ -27,6 +27,12 @@ export const CONSTANTS = { GLOBALSTORAGE_READ_FAILED: 'Failed to read neo4j globalStorage directory.', LINTER_DOWNLOAD_FAILED: 'Linter download failed, reverting to best match from currently downloaded linter versions.', + LINTER_VERSION_NOT_AVAILABLE: + 'Required linter not available for download, default linting experience will be used.', + LINTER_SERVER_NOT_RESOLVED: + 'Neo4j version could not be resolved, default linting experience will be used.', + LINTER_SERVER_NOT_SUPPORTED: + 'Neo4j version is lower than 5.23, your will have a degraded linting experience.', CONNECTED_MESSAGE: 'Connected to Neo4j.', DISCONNECTED_MESSAGE: 'Disconnected from Neo4j.', RECONNECTED_MESSAGE: 'Reconnected to Neo4j.', diff --git a/packages/vscode-extension/src/extension.ts b/packages/vscode-extension/src/extension.ts index ff2d114e1..dfbf9a696 100644 --- a/packages/vscode-extension/src/extension.ts +++ b/packages/vscode-extension/src/extension.ts @@ -37,15 +37,6 @@ export async function activate(context: ExtensionContext) { path.join('..', 'language-server', 'dist', 'server.js'), ); - //only show linter picking command with feature flag active - const config = workspace.getConfiguration('neo4j.features'); - const useVersionedLinters = config.get('useVersionedLinters', false); - await commands.executeCommand( - 'setContext', - 'neo4j:useVersionedLinters', - useVersionedLinters, - ); - // If the extension is launched in debug mode then the debug server options are used // Otherwise the run options are used const serverOptions: ServerOptions = { diff --git a/packages/vscode-extension/src/linterSwitching.ts b/packages/vscode-extension/src/linterSwitching.ts index f7f23e0ec..8f07c8025 100644 --- a/packages/vscode-extension/src/linterSwitching.ts +++ b/packages/vscode-extension/src/linterSwitching.ts @@ -32,7 +32,7 @@ export async function switchWorkerOnLanguageServer( lintWorkerPath: linterPath, linterVersion: linterVersion, }); - linterStatusBarItem.text = linterVersion ? linterVersion : 'Latest'; + linterStatusBarItem.text = linterVersion ? linterVersion : 'Default'; } export async function getFilesInExtensionStorage(): Promise { @@ -83,7 +83,7 @@ export async function downloadLintWorker( npmReleases: NpmRelease[], ): Promise { void vscode.window.showInformationMessage( - 'Downloading linter for your server', + `Downloading linter ${linterVersion} for your server`, ); const newestLegacyLinter = npmReleases?.find( @@ -91,7 +91,7 @@ export async function downloadLintWorker( ); if (!newestLegacyLinter) { void vscode.window.showErrorMessage( - CONSTANTS.MESSAGES.LINTER_DOWNLOAD_FAILED, + CONSTANTS.MESSAGES.LINTER_VERSION_NOT_AVAILABLE, ); return false; } @@ -147,14 +147,20 @@ export async function dynamicallyAdjustLinter(): Promise { const serverVersion = poller.connection?.serverVersion; if (serverVersion) { - //removes zero padding on month of new versions - const sanitizedServerVersion = serverVersion.replace( - /(\.0+)(?=\d)/g, - '.', - ); - //since not every release has a linter release - const linterVersion = serverVersionToLinter(sanitizedServerVersion); + const { linterVersion, notResolved, notSupported } = + serverVersionToLinter(serverVersion); + + if (notResolved) { + void vscode.window.showWarningMessage( + CONSTANTS.MESSAGES.LINTER_SERVER_NOT_RESOLVED, + ); + } else if (notSupported) { + void vscode.window.showWarningMessage( + CONSTANTS.MESSAGES.LINTER_SERVER_NOT_SUPPORTED, + ); + } + const npmReleases = await getTaggedRegistryVersions(); await switchToLinter(linterVersion, npmReleases); } @@ -179,29 +185,34 @@ export async function switchToLinter( linterVersion: string, npmReleases: NpmRelease[], ): Promise { - if (linterVersion === 'Latest') { - return await switchWorkerOnLanguageServer(); - } - if (npmReleases.length === 0) { - await switchToLocalLinter(linterVersion); - } else { - const storageUri = await getStorageUri(); - const { expectedFileName, isExpectedLinterDownloaded } = - await expectedLinterExists(linterVersion, npmReleases, storageUri); - if (isExpectedLinterDownloaded) { - await switchWorkerOnLanguageServer(expectedFileName, storageUri); + try { + if (linterVersion === 'Default') { + return await switchWorkerOnLanguageServer(); + } + if (npmReleases.length === 0) { + await switchToLocalLinter(linterVersion); } else { - const success = await downloadLintWorker( - linterVersion, - storageUri, - npmReleases, - ); - if (success) { + const storageUri = await getStorageUri(); + const { expectedFileName, isExpectedLinterDownloaded } = + await expectedLinterExists(linterVersion, npmReleases, storageUri); + if (isExpectedLinterDownloaded) { await switchWorkerOnLanguageServer(expectedFileName, storageUri); } else { - await switchToLocalLinter(linterVersion); + const success = await downloadLintWorker( + linterVersion, + storageUri, + npmReleases, + ); + if (success) { + await switchWorkerOnLanguageServer(expectedFileName, storageUri); + } else { + await switchToLocalLinter(linterVersion); + } } } + } catch (e) { + // In case of error use default linter (i.e. the one included with the language server) + await switchWorkerOnLanguageServer(); } } diff --git a/packages/vscode-extension/src/registrationService.ts b/packages/vscode-extension/src/registrationService.ts index 6ebfcef5a..29a9d2a4c 100644 --- a/packages/vscode-extension/src/registrationService.ts +++ b/packages/vscode-extension/src/registrationService.ts @@ -1,4 +1,4 @@ -import { commands, Disposable, window, workspace } from 'vscode'; +import { commands, Disposable, window } from 'vscode'; import { createConnectionPanel, cypherFileFromSelection, @@ -42,21 +42,18 @@ export function registerDisposables(): Disposable[] { const disposables = Array(); const queryDetailsProvider = new Neo4jQueryDetailsProvider(); const queryVisualizationProvider = new Neo4jQueryVisualizationProvider(); - const config = workspace.getConfiguration('neo4j.features'); - const versionedLintersEnabled = config.get('useVersionedLinters', false); - if (versionedLintersEnabled) { - linterStatusBarItem.command = CONSTANTS.COMMANDS.SWITCH_LINTWORKER_COMMAND; - linterStatusBarItem.text = 'Latest'; - linterStatusBarItem.tooltip = 'Current Cypher Linter. Click to switch'; - linterStatusBarItem.show(); - disposables.push( - commands.registerCommand( - CONSTANTS.COMMANDS.SWITCH_LINTWORKER_COMMAND, - manuallyAdjustLinter, - ), - linterStatusBarItem, - ); - } + + linterStatusBarItem.command = CONSTANTS.COMMANDS.SWITCH_LINTWORKER_COMMAND; + linterStatusBarItem.text = 'Default'; + linterStatusBarItem.tooltip = 'Current Cypher Linter. Click to switch'; + linterStatusBarItem.show(); + disposables.push( + commands.registerCommand( + CONSTANTS.COMMANDS.SWITCH_LINTWORKER_COMMAND, + manuallyAdjustLinter, + ), + linterStatusBarItem, + ); disposables.push( window.registerWebviewViewProvider( diff --git a/packages/vscode-extension/syntaxes/cypher.json b/packages/vscode-extension/syntaxes/cypher.json index 301982a54..17b77cf5b 100644 --- a/packages/vscode-extension/syntaxes/cypher.json +++ b/packages/vscode-extension/syntaxes/cypher.json @@ -38,7 +38,7 @@ "name": "keyword.operator" }, { - "match": "(?i)\\b(ACCESS|ACTIVE|ADD|ADMIN|ADMINISTRATOR|ALIAS|ALIASES|ALL|ALLREDUCE|allShortestPaths|ALTER|AND|ANY|ARRAY|AS|ASC|ASCENDING|ASSIGN|AT|AUTH|BINDINGS|BOOL|BOOLEAN|BOOSTED|BOTH|BREAK|BUILT|BY|CALL|CASCADE|CASE|CIDR|CHANGE|COLLECT|COMMAND|COMMANDS|COMPOSITE|CONSTRAINT|CONSTRAINTS|CONTAINS|CONTINUE|COPY|COUNT|CREATE|CSV|CONCURRENT|CURRENT|DATA|DATABASE|DATABASES|DATE|DATETIME|DBMS|DEALLOCATE|DEFAULT|DEFINED|DELETE|DENY|DESC|DESCENDING|DESTROY|DETACH|DIFFERENT|DISTINCT|DRIVER|DROP|DRYRUN|DUMP|DURATION|EACH|EDGE|ELEMENT|ELEMENTS|ELSE|ENABLE|ENCRYPTED|END|ENDS|ERROR|EXECUTABLE|EXECUTE|EXIST|EXISTENCE|EXISTS|EXTENDED_IDENTIFIER|FAIL|FALSE|FIELDTERMINATOR|FILTER|FINISH|FLOAT|FLOAT32|FLOAT64|FOR|FOREACH|FROM|FULLTEXT|FUNCTION|FUNCTIONS|GRANT|GRAPH|GRAPHS|GROUP|GROUPS|HEADERS|HOME|ID|IF|IMMUTABLE|IMPERSONATE|IMPLIES|IN|INDEX|INDEXES|INF|INFINITY|INSERT|INT|INT8|INT16|INT32|INT64|INTEGER|INTEGER8|INTEGER16|INTEGER32|INTEGER64|IS|JOIN|KEY|LABEL|LABELS|LANGUAGE|LEADING|LET|LIMIT|LIST|LOAD|LOCAL|LOOKUP|MANAGEMENT|MAP|MATCH|MERGE|NAME|NAMES|NAN|NEW|NEXT|NFC|NFD|NFKC|NFKD|NODE|NODETACH|NODES|NONE|NORMALIZE|NORMALIZED|NOT|NOTHING|NOWAIT|NULL|OF|OFFSET|ON|ONLY|OPTION|OPTIONAL|OPTIONS|OR|ORDER|PASSWORD|PASSWORDS|PATH|PATHS|PLAINTEXT|POINT|POPULATED|PRIMARY|PRIMARIES|PRIVILEGE|PRIVILEGES|PROCEDURE|PROCEDURES|PROPERTIES|PROPERTY|PROVIDER|PROVIDERS|RANGE|READ|REALLOCATE|REDUCE|REL|RELATIONSHIP|RELATIONSHIPS|REMOVE|RENAME|REPEATABLE|REPLACE|REPLICA|REPLICAS|REPORT|REQUIRE|REQUIRED|RESTRICT|RETRY|RETURN|REVOKE|ROLE|ROLES|ROW|ROWS|SCAN|SECONDARY|SECONDARIES|SEC|SECOND|SECONDS|SEEK|SERVER|SERVERS|SET|SETTING|SETTINGS|SHARD|SHARDS|SHORTEST|shortestPath|SHOW|SIGNED|SINGLE|SKIP|START|STARTS|STATUS|STOP|VARCHAR|STRING|SUPPORTED|SUSPENDED|TARGET|TERMINATE|TEXT|THEN|TIME|TIMESTAMP|TIMEZONE|TO|TOPOLOGY|TRAILING|TRANSACTION|TRANSACTIONS|TRAVERSE|TRIM|TRUE|TYPE|TYPED|TYPES|UNION|UNIQUE|UNIQUENESS|UNWIND|URL|USE|USER|USERS|USING|VALUE|VECTOR|VERTEX|WAIT|WHEN|WHERE|WITH|WITHOUT|WRITE|XOR|YIELD|ZONE|ZONED|EXPLAIN|PROFILE|CYPHER)\\b", + "match": "(?i)\\b(ACCESS|ACTIVE|ADD|ADMIN|ADMINISTRATOR|ALIAS|ALIASES|ALL|ALLREDUCE|allShortestPaths|ALTER|AND|ANY|ARRAY|AS|ASC|ASCENDING|ASSIGN|AT|AUTH|BINDINGS|BOOL|BOOLEAN|BOOSTED|BOTH|BREAK|BUILT|BY|CALL|CASCADE|CASE|CIDR|CHANGE|COLLECT|COMMAND|COMMANDS|COMPOSITE|CONSTRAINT|CONSTRAINTS|CONTAINS|CONTINUE|COPY|COSINE|COUNT|CREATE|CSV|CONCURRENT|CURRENT|DATA|DATABASE|DATABASES|DATE|DATETIME|DBMS|DEALLOCATE|DEFAULT|DEFINED|DELETE|DENY|DESC|DESCENDING|DESTROY|DETACH|DIFFERENT|DISTINCT|DOT|DRIVER|DROP|DRYRUN|DUMP|DURATION|EACH|EDGE|ELEMENT|ELEMENTS|ELSE|ENABLE|ENCRYPTED|END|ENDS|ERROR|EUCLIDEAN|EUCLIDEAN_SQUARED|EXECUTABLE|EXECUTE|EXIST|EXISTENCE|EXISTS|EXTENDED_IDENTIFIER|FAIL|FALSE|FIELDTERMINATOR|FILTER|FINISH|FLOAT|FLOAT32|FLOAT64|FOR|FOREACH|FROM|FULLTEXT|FUNCTION|FUNCTIONS|GRANT|GRAPH|GRAPHS|GROUP|GROUPS|HAMMING|HEADERS|HOME|ID|IF|IMMUTABLE|IMPERSONATE|IMPLIES|IN|INDEX|INDEXES|INF|INFINITY|INSERT|INT|INT8|INT16|INT32|INT64|INTEGER|INTEGER8|INTEGER16|INTEGER32|INTEGER64|IS|JOIN|KEY|LABEL|LABELS|LANGUAGE|LEADING|LET|LIMIT|LIST|LOAD|LOCAL|LOOKUP|MANAGEMENT|MANHATTAN|MAP|MATCH|MERGE|NAME|NAMES|NAN|NEW|NEXT|NFC|NFD|NFKC|NFKD|NODE|NODETACH|NODES|NONE|NORMALIZE|NORMALIZED|NOT|NOTHING|NOWAIT|NULL|OF|OFFSET|ON|ONLY|OPTION|OPTIONAL|OPTIONS|OR|ORDER|PASSWORD|PASSWORDS|PATH|PATHS|PLAINTEXT|POINT|POPULATED|PRIMARY|PRIMARIES|PRIVILEGE|PRIVILEGES|PROCEDURE|PROCEDURES|PROPERTIES|PROPERTY|PROVIDER|PROVIDERS|RANGE|READ|REALLOCATE|REDUCE|REL|RELATIONSHIP|RELATIONSHIPS|REMOVE|RENAME|REPEATABLE|REPLACE|REPLICA|REPLICAS|REPORT|REQUIRE|REQUIRED|RESTRICT|RETRY|RETURN|REVOKE|ROLE|ROLES|ROW|ROWS|SCAN|SECONDARY|SECONDARIES|SEC|SECOND|SECONDS|SEEK|SERVER|SERVERS|SET|SETTING|SETTINGS|SHARD|SHARDS|SHORTEST|shortestPath|SHOW|SIGNED|SINGLE|SKIP|START|STARTS|STATUS|STOP|VARCHAR|STRING|SUPPORTED|SUSPENDED|TARGET|TERMINATE|TEXT|THEN|TIME|TIMESTAMP|TIMEZONE|TO|TOPOLOGY|TRAILING|TRANSACTION|TRANSACTIONS|TRAVERSE|TRIM|TRUE|TYPE|TYPED|TYPES|UNION|UNIQUE|UNIQUENESS|UNWIND|URL|USE|USER|USERS|USING|VALUE|VECTOR|VECTOR_DISTANCE|VECTOR_NORM|VERTEX|WAIT|WHEN|WHERE|WITH|WITHOUT|WRITE|XOR|YIELD|ZONE|ZONED|EXPLAIN|PROFILE|CYPHER)\\b", "name": "keyword" } ] diff --git a/packages/vscode-extension/tests/helpers.ts b/packages/vscode-extension/tests/helpers.ts index 6d0f277fe..07367fc8c 100644 --- a/packages/vscode-extension/tests/helpers.ts +++ b/packages/vscode-extension/tests/helpers.ts @@ -140,15 +140,6 @@ export async function toggleLinting(value: boolean) { await config.update('linting', value, vscode.ConfigurationTarget.Global); } -export async function toggleVersionedLinters(value: boolean) { - const config = vscode.workspace.getConfiguration('neo4j.features'); - await config.update( - 'useVersionedLinters', - value, - vscode.ConfigurationTarget.Global, - ); -} - export function getExtensionStoragePath(): string { const extensionId = 'neo4j-extensions.neo4j-for-vscode'; const platform = os.platform(); diff --git a/packages/vscode-extension/tests/specs/api/versionSpecificLinting.spec.ts b/packages/vscode-extension/tests/specs/api/versionSpecificLinting.spec.ts index 98cbe492b..601bd62bc 100644 --- a/packages/vscode-extension/tests/specs/api/versionSpecificLinting.spec.ts +++ b/packages/vscode-extension/tests/specs/api/versionSpecificLinting.spec.ts @@ -3,20 +3,33 @@ import { getDocumentUri, getExtensionStoragePath, openDocument, - toggleVersionedLinters, } from '../../helpers'; -import { connectDefault } from '../../suiteSetup'; +import { connectDefault, disconnectDefault } from '../../suiteSetup'; import { rmSync } from 'fs'; import { testSyntaxValidation } from './syntaxValidation.spec'; import { after, before } from 'mocha'; +// Note these tests do not work with the VSCode debugger +// Because the VSCode debugger seems to sandbox the editor +// it spins up, so globalStorage is a temp folder, not the +// one getExtensionContext().globalStorageUri returns suite('Neo4j version specific linting spec', () => { before(async () => { - await toggleVersionedLinters(true); + process.env.LINTER_SWITCHING_TESTS = 'true'; + // We need to reconnect to neo4j so that the switching + // linter action takes place, otherwise we would be + // using the one packaged with the VSCode extension + await disconnectDefault({ version: 'neo4j 2025' }); + await connectDefault({ version: 'neo4j 2025' }); }); after(async () => { - await toggleVersionedLinters(false); + process.env.LINTER_SWITCHING_TESTS = undefined; + // We need to reconnect to neo4j so that the switching + // linter action is disabled and we go back to be using + // the one packaged with the VSCode extension + await disconnectDefault({ version: 'neo4j 2025' }); + await connectDefault({ version: 'neo4j 2025' }); }); async function testNeo4jSpecificLinting() { @@ -48,7 +61,7 @@ suite('Neo4j version specific linting spec', () => { new vscode.Position(5, 7), new vscode.Position(5, 12), ), - "The query used a deprecated function. ('id' has been replaced by 'elementId or consider using an application-generated id')", + "The query used a deprecated function. ('id' has been replaced by 'elementId or an application-generated id')", vscode.DiagnosticSeverity.Warning, ), ], diff --git a/packages/vscode-extension/tests/specs/unit/connectionCommandHandlers.spec.ts b/packages/vscode-extension/tests/specs/unit/connectionCommandHandlers.spec.ts index 8ee0cb396..21c3b861c 100644 --- a/packages/vscode-extension/tests/specs/unit/connectionCommandHandlers.spec.ts +++ b/packages/vscode-extension/tests/specs/unit/connectionCommandHandlers.spec.ts @@ -46,17 +46,13 @@ suite('Command handlers spec', () => { key: 'schemas', }); - sandbox.assert.calledOnceWithExactly( - sendNotificationSpy, - 'connectionUpdated', - { - connect: true, - connectURL: 'neo4j://localhost:7687', - database: 'schemas', - user: 'neo4j', - password: null, - }, - ); + sandbox.assert.calledWith(sendNotificationSpy, 'connectionUpdated', { + connect: true, + connectURL: 'neo4j://localhost:7687', + database: 'schemas', + user: 'neo4j', + password: null, + }); }); test('Switching to a bad database should notify the language server with connectionDisconnected', async () => { diff --git a/packages/vscode-extension/tests/specs/unit/connectionService.spec.ts b/packages/vscode-extension/tests/specs/unit/connectionService.spec.ts index b03e68e87..34b5460e7 100644 --- a/packages/vscode-extension/tests/specs/unit/connectionService.spec.ts +++ b/packages/vscode-extension/tests/specs/unit/connectionService.spec.ts @@ -326,17 +326,13 @@ suite('Connection service spec', () => { 'mock-password', ); - sandbox.assert.calledOnceWithExactly( - sendNotificationSpy, - 'connectionUpdated', - { - connect: true, - connectURL: 'neo4j://localhost:7687', - database: 'neo4j', - user: 'neo4j', - password: 'mock-password', - }, - ); + sandbox.assert.calledWith(sendNotificationSpy, 'connectionUpdated', { + connect: true, + connectURL: 'neo4j://localhost:7687', + database: 'neo4j', + user: 'neo4j', + password: 'mock-password', + }); }); test('Should not notify language server when initializeDatabaseConnection returns a non success status', async () => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a05a8ebb7..3339b996f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -142,9 +142,6 @@ importers: languageSupport-next.13: specifier: npm:@neo4j-cypher/language-support@2.0.0-next.13 version: '@neo4j-cypher/language-support@2.0.0-next.13' - languageSupport-next.8: - specifier: npm:@neo4j-cypher/language-support@2.0.0-next.8 - version: '@neo4j-cypher/language-support@2.0.0-next.8' vscode-languageserver: specifier: ^8.1.0 version: 8.1.0 @@ -1410,10 +1407,6 @@ packages: resolution: {integrity: sha512-csi1nmY3PfEJEpGrxALmSi6LU8fewgTCMRHYk5yGWT5IFUh2q/b2Q3XqAIblbgAkRoggzkA/9dRoiGq76Ug6eA==} engines: {node: '>=18.18.2'} - '@neo4j-cypher/language-support@2.0.0-next.8': - resolution: {integrity: sha512-dl4kNU+i8hbMgKSIGWX/Lzeg5vEst9wDPz172OA3XQ3HNGkwlSBxAqauVgA9EUIX+7Ujhgptzz49cZMuAe5KtQ==} - engines: {node: '>=18.18.2'} - '@neo4j-devtools/word-color@0.0.8': resolution: {integrity: sha512-0fC2PXU1M0wL72lVil/2JnUccpEoPaiNJsNAc8fgRUysVeZ/OLKFvZAeOnufJ4X5OKkImaL1lvoGyhfKfOKfzw==} @@ -8533,13 +8526,6 @@ snapshots: fastest-levenshtein: 1.0.16 vscode-languageserver-types: 3.17.5 - '@neo4j-cypher/language-support@2.0.0-next.8': - dependencies: - antlr4: 4.13.2 - antlr4-c3: 3.4.4 - fastest-levenshtein: 1.0.16 - vscode-languageserver-types: 3.17.5 - '@neo4j-devtools/word-color@0.0.8': dependencies: '@types/chroma-js': 2.1.4