From 281a4ca0932bb8c145efb5776a9a380aed106455 Mon Sep 17 00:00:00 2001 From: Isak Date: Fri, 17 Oct 2025 16:10:41 +0200 Subject: [PATCH 1/2] adds bailing heuristic for symbol table calc + test --- packages/language-server/package.json | 3 +- packages/language-server/src/helpers.ts | 32 +++++++++++++ packages/language-server/src/server.ts | 42 ++++++++++++++++- .../language-server/src/tests/misc.test.ts | 47 +++++++++++++++++++ packages/language-server/tsconfig.json | 7 ++- packages/language-server/tsconfig.node.json | 8 ++++ packages/language-server/vitest.config.mts | 7 +++ 7 files changed, 143 insertions(+), 3 deletions(-) create mode 100644 packages/language-server/src/helpers.ts create mode 100644 packages/language-server/src/tests/misc.test.ts create mode 100644 packages/language-server/tsconfig.node.json create mode 100644 packages/language-server/vitest.config.mts diff --git a/packages/language-server/package.json b/packages/language-server/package.json index 769eded27..d2a36a9f9 100644 --- a/packages/language-server/package.json +++ b/packages/language-server/package.json @@ -50,7 +50,8 @@ "bundle": "esbuild ./src/server.ts --bundle --format=cjs --platform=node --outfile=dist/cypher-language-server.js --minify --conditions=require", "dev": "tsc --watch", "make-executable": "cd dist && echo '#!/usr/bin/env node' > cypher-language-server && cat cypher-language-server.js >> cypher-language-server", - "clean": "rm -rf {dist,tsconfig.tsbuildinfo}" + "clean": "rm -rf {dist,tsconfig.tsbuildinfo}", + "test": "vitest run" }, "devDependencies": { "@types/lodash.debounce": "^4.0.9", diff --git a/packages/language-server/src/helpers.ts b/packages/language-server/src/helpers.ts new file mode 100644 index 000000000..b1176e78c --- /dev/null +++ b/packages/language-server/src/helpers.ts @@ -0,0 +1,32 @@ +/**Checks if we should bail on the job, which would be if we have only typed a letter/number/underscore */ +export function shouldBail(query: string, oldQuery: string) { + //Trying to determine if we typed in a single character or copy-pasted, and if single char, if this new char is a non number/letter + + let shouldBail = false; + if (query.length === oldQuery.length + 1) { + let newCharCandidateIndex: number = undefined; + for (let i = 0; i < oldQuery.length; i++) { + //if we just consider typing, we only have 1 diff, the inserted symbol + //if diff, the symbol was inserted into newquery here, if we never do the new symbol is the last + //if we copypaste, we could do to equally long -> in that case, removing the first diff symbol would not yield + //the same query + if (query[i] != oldQuery[i]) { + newCharCandidateIndex = i; + break; + } + } + newCharCandidateIndex = newCharCandidateIndex ?? query.length - 1; + const oldifiedNewQuery = + query.slice(0, newCharCandidateIndex) + + query.slice(newCharCandidateIndex + 1, query.length); + if (oldifiedNewQuery === oldQuery) { + const newChar = query[newCharCandidateIndex]; + const letterOrNumber = /^\w/; + const isLetterOrNumber = newChar.match(letterOrNumber); + if (isLetterOrNumber) { + shouldBail = true; + } + } + } + return shouldBail; +} diff --git a/packages/language-server/src/server.ts b/packages/language-server/src/server.ts index 0fd3a768c..2449abadc 100644 --- a/packages/language-server/src/server.ts +++ b/packages/language-server/src/server.ts @@ -33,7 +33,40 @@ import workerpool from 'workerpool'; import { join } from 'path'; import { LintWorker } from '@neo4j-cypher/lint-worker'; -class SymbolFetcher { +/**Checks if we should bail on the job, which would be if we have only typed a letter/number/underscore */ +export function shouldBail(query: string, oldQuery: string) { + //Trying to determine if we typed in a single character or copy-pasted, and if single char, if this new char is a non number/letter + + let shouldBail = false; + if (query.length === oldQuery.length + 1) { + let newCharCandidateIndex: number = undefined; + for (let i = 0; i < oldQuery.length; i++) { + //if we just consider typing, we only have 1 diff, the inserted symbol + //if diff, the symbol was inserted into newquery here, if we never do the new symbol is the last + //if we copypaste, we could do to equally long -> in that case, removing the first diff symbol would not yield + //the same query + if (query[i] != oldQuery[i]) { + newCharCandidateIndex = i; + break; + } + } + newCharCandidateIndex = newCharCandidateIndex ?? query.length - 1; + const oldifiedNewQuery = + query.slice(0, newCharCandidateIndex) + + query.slice(newCharCandidateIndex + 1, query.length); + if (oldifiedNewQuery === oldQuery) { + const newChar = query[newCharCandidateIndex]; + const letterOrNumber = /^\w/; + const isLetterOrNumber = newChar.match(letterOrNumber); + if (isLetterOrNumber) { + shouldBail = true; + } + } + } + return shouldBail; +} + +export class SymbolFetcher { private processing = false; private nextJob: { query: string; @@ -45,8 +78,15 @@ class SymbolFetcher { maxWorkers: 1, workerTerminateTimeout: 0, }); + private lastQuery: string = ''; public queueSymbolJob(query: string, uri: string, schema: DbSchema) { + const bailEarly = shouldBail(query, this.lastQuery); + this.lastQuery = query; + if (bailEarly) { + return; + } + this.nextJob = { query, uri, schema }; if (!this.processing) { void this.processJobQueue(); diff --git a/packages/language-server/src/tests/misc.test.ts b/packages/language-server/src/tests/misc.test.ts new file mode 100644 index 000000000..8c3f0ee49 --- /dev/null +++ b/packages/language-server/src/tests/misc.test.ts @@ -0,0 +1,47 @@ +import { shouldBail } from '../helpers'; + +const bailingCases = [ + { + name: 'Typing letters in end', + lastQuery: 'MATCH (m', + nextQuery: 'MATCH (mn', + shouldBail: true, + }, + { + name: 'Typing symbol in end', + lastQuery: 'MATCH (mnm', + nextQuery: 'MATCH (mnm:', + shouldBail: false, + }, + //Here we would prefer to update the symbol table, but this heuristic would skip it (staying at "mn:Node" even after we rename to "mnm") + { + name: 'Typing letters in middle', + lastQuery: 'MATCH (mn)-[:', + nextQuery: 'MATCH (mnm)-[:', + shouldBail: true, + }, + { + name: 'Typing symbol in middle', + lastQuery: 'MATCH (mnm)-[:', + nextQuery: 'MATCH (mnm )-[:', + shouldBail: false, + }, + { + name: 'Pasting in multiple symbols at once', + lastQuery: 'RETURN 50', + nextQuery: 'MATCH (n) RETURN n', + shouldBail: false, + }, +]; + +describe('Misc tests for the language server', () => { + test('Bailing logic for symbol table calculation', () => { + bailingCases.forEach((c) => { + expect( + shouldBail(c.nextQuery, c.lastQuery), + "Failed on test '" + c.name + "'", + ).toBe(c.shouldBail); + }); + //expect(false).toBe(true); + }); +}); diff --git a/packages/language-server/tsconfig.json b/packages/language-server/tsconfig.json index 5a24989cd..06fc72903 100644 --- a/packages/language-server/tsconfig.json +++ b/packages/language-server/tsconfig.json @@ -2,7 +2,12 @@ "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "dist", - "rootDir": "src" + "rootDir": "src", + "declaration": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "types": ["vitest/globals"] }, "include": ["src"] } diff --git a/packages/language-server/tsconfig.node.json b/packages/language-server/tsconfig.node.json new file mode 100644 index 000000000..208898cc2 --- /dev/null +++ b/packages/language-server/tsconfig.node.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["vitest.config.mts"], + "compilerOptions": { + "noEmit": true, + "declaration": false + } +} diff --git a/packages/language-server/vitest.config.mts b/packages/language-server/vitest.config.mts new file mode 100644 index 000000000..7382f40e7 --- /dev/null +++ b/packages/language-server/vitest.config.mts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + }, +}); From 2f6a56a3e2776d78159d24e340b29bf002251861 Mon Sep 17 00:00:00 2001 From: Isak Date: Fri, 17 Oct 2025 16:31:59 +0200 Subject: [PATCH 2/2] cleanup --- packages/language-server/src/server.ts | 36 ++------------------- packages/language-server/tsconfig.json | 4 --- packages/language-server/tsconfig.node.json | 3 +- 3 files changed, 3 insertions(+), 40 deletions(-) diff --git a/packages/language-server/src/server.ts b/packages/language-server/src/server.ts index 2449abadc..c3a323ea4 100644 --- a/packages/language-server/src/server.ts +++ b/packages/language-server/src/server.ts @@ -32,41 +32,9 @@ import { import workerpool from 'workerpool'; import { join } from 'path'; import { LintWorker } from '@neo4j-cypher/lint-worker'; +import { shouldBail } from './helpers'; -/**Checks if we should bail on the job, which would be if we have only typed a letter/number/underscore */ -export function shouldBail(query: string, oldQuery: string) { - //Trying to determine if we typed in a single character or copy-pasted, and if single char, if this new char is a non number/letter - - let shouldBail = false; - if (query.length === oldQuery.length + 1) { - let newCharCandidateIndex: number = undefined; - for (let i = 0; i < oldQuery.length; i++) { - //if we just consider typing, we only have 1 diff, the inserted symbol - //if diff, the symbol was inserted into newquery here, if we never do the new symbol is the last - //if we copypaste, we could do to equally long -> in that case, removing the first diff symbol would not yield - //the same query - if (query[i] != oldQuery[i]) { - newCharCandidateIndex = i; - break; - } - } - newCharCandidateIndex = newCharCandidateIndex ?? query.length - 1; - const oldifiedNewQuery = - query.slice(0, newCharCandidateIndex) + - query.slice(newCharCandidateIndex + 1, query.length); - if (oldifiedNewQuery === oldQuery) { - const newChar = query[newCharCandidateIndex]; - const letterOrNumber = /^\w/; - const isLetterOrNumber = newChar.match(letterOrNumber); - if (isLetterOrNumber) { - shouldBail = true; - } - } - } - return shouldBail; -} - -export class SymbolFetcher { +class SymbolFetcher { private processing = false; private nextJob: { query: string; diff --git a/packages/language-server/tsconfig.json b/packages/language-server/tsconfig.json index 06fc72903..d4bfeda22 100644 --- a/packages/language-server/tsconfig.json +++ b/packages/language-server/tsconfig.json @@ -3,10 +3,6 @@ "compilerOptions": { "outDir": "dist", "rootDir": "src", - "declaration": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "node", "types": ["vitest/globals"] }, "include": ["src"] diff --git a/packages/language-server/tsconfig.node.json b/packages/language-server/tsconfig.node.json index 208898cc2..24d81c5a8 100644 --- a/packages/language-server/tsconfig.node.json +++ b/packages/language-server/tsconfig.node.json @@ -2,7 +2,6 @@ "extends": "../../tsconfig.base.json", "include": ["vitest.config.mts"], "compilerOptions": { - "noEmit": true, - "declaration": false + "noEmit": true } }