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..c3a323ea4 100644 --- a/packages/language-server/src/server.ts +++ b/packages/language-server/src/server.ts @@ -32,6 +32,7 @@ import { import workerpool from 'workerpool'; import { join } from 'path'; import { LintWorker } from '@neo4j-cypher/lint-worker'; +import { shouldBail } from './helpers'; class SymbolFetcher { private processing = false; @@ -45,8 +46,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..d4bfeda22 100644 --- a/packages/language-server/tsconfig.json +++ b/packages/language-server/tsconfig.json @@ -2,7 +2,8 @@ "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "dist", - "rootDir": "src" + "rootDir": "src", + "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..24d81c5a8 --- /dev/null +++ b/packages/language-server/tsconfig.node.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["vitest.config.mts"], + "compilerOptions": { + "noEmit": true + } +} 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, + }, +});