@neo4j-cypher/language-server 2.0.0-next.3 → 2.0.0-next.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js ADDED
@@ -0,0 +1,204 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const node_1 = require("vscode-languageserver/node");
7
+ const vscode_languageserver_textdocument_1 = require("vscode-languageserver-textdocument");
8
+ const language_support_1 = require("@neo4j-cypher/language-support");
9
+ const query_tools_1 = require("@neo4j-cypher/query-tools");
10
+ const autocompletion_1 = require("./autocompletion");
11
+ const formatting_1 = require("./formatting");
12
+ const linting_1 = require("./linting");
13
+ const signatureHelp_1 = require("./signatureHelp");
14
+ const syntaxColouring_1 = require("./syntaxColouring");
15
+ const workerpool_1 = __importDefault(require("workerpool"));
16
+ const lint_worker_1 = require("@neo4j-cypher/lint-worker");
17
+ const path_1 = require("path");
18
+ const defaultWorkerPath = (0, path_1.join)(__dirname, 'lintWorker.cjs');
19
+ let workerPath = defaultWorkerPath;
20
+ class SymbolFetcher {
21
+ processing = false;
22
+ nextJob;
23
+ symbolTablePool = workerpool_1.default.pool(defaultWorkerPath, {
24
+ maxWorkers: 1,
25
+ workerTerminateTimeout: 0,
26
+ });
27
+ linterVersion;
28
+ setLintWorker(lintWorkerPath = defaultWorkerPath, linterVersion) {
29
+ this.linterVersion = linterVersion;
30
+ this.symbolTablePool = workerpool_1.default.pool(lintWorkerPath, {
31
+ maxWorkers: 1,
32
+ workerTerminateTimeout: 0,
33
+ });
34
+ }
35
+ queueSymbolJob(query, uri, schema) {
36
+ this.nextJob = { query, uri, schema };
37
+ if (!this.processing) {
38
+ void this.processJobQueue();
39
+ }
40
+ }
41
+ async processJobQueue() {
42
+ this.processing = true;
43
+ while (this.nextJob) {
44
+ try {
45
+ const proxyWorker = (await this.symbolTablePool.proxy());
46
+ const query = this.nextJob.query;
47
+ const dbSchema = this.nextJob.schema;
48
+ const docUri = this.nextJob.uri;
49
+ this.nextJob = undefined;
50
+ const fixedDbSchema = (0, lint_worker_1.convertDbSchema)(dbSchema, this.linterVersion);
51
+ const result = await proxyWorker.lintCypherQuery(query, fixedDbSchema);
52
+ if (
53
+ //if this.nextJob has new doc, our result is no longer valid
54
+ result.symbolTables &&
55
+ !(this.nextJob && this.nextJob.uri != docUri)) {
56
+ language_support_1.parserWrapper.setSymbolsInfo({
57
+ query,
58
+ symbolTables: result.symbolTables,
59
+ }, async (symbolTables) => await connection.sendNotification('symbolTableDone', {
60
+ symbolTables,
61
+ }));
62
+ }
63
+ }
64
+ catch {
65
+ //eslint-disable-next-line
66
+ console.log('Symbol table calculation failed');
67
+ }
68
+ }
69
+ this.processing = false;
70
+ }
71
+ }
72
+ const connection = (0, node_1.createConnection)(node_1.ProposedFeatures.all);
73
+ let settings = undefined;
74
+ // Create a simple text document manager.
75
+ const documents = new node_1.TextDocuments(vscode_languageserver_textdocument_1.TextDocument);
76
+ const neo4jSchemaPoller = new query_tools_1.Neo4jSchemaPoller();
77
+ const symbolFetcher = new SymbolFetcher();
78
+ async function lintSingleDocument(document) {
79
+ symbolFetcher.queueSymbolJob(document.getText(), document.uri, neo4jSchemaPoller?.metadata?.dbSchema);
80
+ if (settings?.features?.linting) {
81
+ return (0, linting_1.lintDocument)(document, (diagnostics) => {
82
+ void connection.sendDiagnostics({
83
+ uri: document.uri,
84
+ diagnostics,
85
+ });
86
+ }, async (symbolTables) => await connection.sendNotification('symbolTableDone', {
87
+ symbolTables: symbolTables,
88
+ }), neo4jSchemaPoller);
89
+ }
90
+ else {
91
+ void connection.sendDiagnostics({
92
+ uri: document.uri,
93
+ diagnostics: [],
94
+ });
95
+ }
96
+ }
97
+ function relintAllDocuments() {
98
+ void documents.all().map(lintSingleDocument);
99
+ }
100
+ connection.onInitialize(() => {
101
+ const result = {
102
+ capabilities: {
103
+ textDocumentSync: node_1.TextDocumentSyncKind.Full,
104
+ // Tell the client what features does the server support
105
+ completionProvider: {
106
+ resolveProvider: false,
107
+ triggerCharacters: ['.', ':', '{', '$', ')', ' ', ']', '-', '<'],
108
+ },
109
+ semanticTokensProvider: {
110
+ documentSelector: [{ language: 'cypher' }],
111
+ legend: language_support_1.syntaxColouringLegend,
112
+ range: false,
113
+ full: {
114
+ delta: false,
115
+ },
116
+ },
117
+ signatureHelpProvider: {
118
+ triggerCharacters: ['(', ',', ')'],
119
+ },
120
+ documentFormattingProvider: true,
121
+ },
122
+ };
123
+ return result;
124
+ });
125
+ connection.onInitialized(() => {
126
+ void connection.client.register(node_1.DidChangeConfigurationNotification.type, {
127
+ section: 'neo4j',
128
+ });
129
+ const registrationOptions = {
130
+ documentSelector: [{ language: 'cypher' }],
131
+ legend: language_support_1.syntaxColouringLegend,
132
+ range: false,
133
+ full: {
134
+ delta: false,
135
+ },
136
+ };
137
+ void connection.client.register(node_1.SemanticTokensRegistrationType.type, registrationOptions);
138
+ });
139
+ connection.onDidChangeConfiguration((params) => {
140
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
141
+ settings = params.settings?.neo4j;
142
+ relintAllDocuments();
143
+ });
144
+ documents.onDidChangeContent((change) => lintSingleDocument(change.document));
145
+ // Trigger the syntax colouring
146
+ connection.languages.semanticTokens.on((0, syntaxColouring_1.applySyntaxColouringForDocument)(documents));
147
+ // Trigger the signature help, providing info about functions / procedures
148
+ connection.onSignatureHelp((0, signatureHelp_1.doSignatureHelp)(documents, neo4jSchemaPoller));
149
+ // Trigger the auto completion
150
+ connection.onCompletion((0, autocompletion_1.doAutoCompletion)(documents, neo4jSchemaPoller));
151
+ connection.onNotification('updateLintWorker', (linterSettings) => {
152
+ const lintWorkerPath = linterSettings.lintWorkerPath;
153
+ const linterVersion = linterSettings.linterVersion;
154
+ if (lintWorkerPath !== workerPath) {
155
+ workerPath = lintWorkerPath;
156
+ void (async () => {
157
+ symbolFetcher.setLintWorker(workerPath, linterVersion);
158
+ await (0, linting_1.setLintWorker)(workerPath, linterVersion);
159
+ relintAllDocuments();
160
+ })();
161
+ }
162
+ });
163
+ connection.onNotification('connectionUpdated', (connectionSettings) => {
164
+ changeConnection(connectionSettings);
165
+ neo4jSchemaPoller.events.once('schemaFetched', relintAllDocuments);
166
+ });
167
+ connection.onNotification('fetchSymbolTable', (params) => {
168
+ neo4jSchemaPoller.events.once('schemaFetched',
169
+ // oxlint-disable-next-line typescript-eslint/no-meaningless-void-operator
170
+ void symbolFetcher.queueSymbolJob(params.query, params.uri, params.schema));
171
+ });
172
+ connection.onNotification('updateParameters', (parameters) => {
173
+ neo4jSchemaPoller.setParameters(parameters);
174
+ relintAllDocuments();
175
+ });
176
+ connection.onDocumentFormatting((params) => {
177
+ return (0, formatting_1.formatDocument)(params, documents);
178
+ });
179
+ connection.onNotification('connectionDisconnected', () => {
180
+ disconnect();
181
+ relintAllDocuments();
182
+ });
183
+ documents.listen(connection);
184
+ connection.listen();
185
+ connection.onExit(() => {
186
+ void (0, linting_1.cleanupWorkers)();
187
+ });
188
+ const changeConnection = (connectionSettings) => {
189
+ disconnect();
190
+ if (neo4jSchemaPoller.connection === undefined &&
191
+ connectionSettings.connect &&
192
+ connectionSettings.password &&
193
+ connectionSettings.connectURL &&
194
+ connectionSettings.user) {
195
+ void neo4jSchemaPoller.persistentConnect(connectionSettings.connectURL, {
196
+ username: connectionSettings.user,
197
+ password: connectionSettings.password,
198
+ }, { appName: 'cypher-language-server' }, connectionSettings.database);
199
+ }
200
+ };
201
+ const disconnect = () => {
202
+ neo4jSchemaPoller.disconnect();
203
+ };
204
+ //# sourceMappingURL=server.js.map
package/package.json CHANGED
@@ -1,8 +1,24 @@
1
1
  {
2
2
  "name": "@neo4j-cypher/language-server",
3
+ "version": "2.0.0-next.31",
3
4
  "description": "Cypher Language Server",
4
- "author": "Neo4j Inc.",
5
+ "keywords": [
6
+ "cypher",
7
+ "language server",
8
+ "neo4j"
9
+ ],
10
+ "bugs": {
11
+ "url": "https://github.com/neo4j/cypher-language-support/issues"
12
+ },
5
13
  "license": "Apache-2.0",
14
+ "author": "Neo4j Inc.",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git://github.com/neo4j/cypher-language-support.git"
18
+ },
19
+ "bin": {
20
+ "cypher-language-server": "./dist/cypher-language-server"
21
+ },
6
22
  "files": [
7
23
  "./dist/cypher-language-server",
8
24
  "./src",
@@ -11,42 +27,33 @@
11
27
  "LICENSE.md",
12
28
  "CHANGELOG.md"
13
29
  ],
14
- "keywords": [
15
- "neo4j",
16
- "cypher",
17
- "language server"
18
- ],
19
- "version": "2.0.0-next.3",
20
30
  "main": "./dist/server.js",
21
31
  "types": "src/server.ts",
22
- "repository": {
23
- "type": "git",
24
- "url": "git://github.com/neo4j/cypher-language-support.git"
32
+ "dependencies": {
33
+ "axios": "^1.9.0",
34
+ "lodash.debounce": "^4.0.8",
35
+ "neo4j-driver": "6.0.1",
36
+ "vscode-languageserver": "^8.1.0",
37
+ "vscode-languageserver-textdocument": "^1.0.8",
38
+ "workerpool": "^9.3.3",
39
+ "@neo4j-cypher/query-tools": "2.0.0-next.30",
40
+ "@neo4j-cypher/language-support": "2.0.0-next.30",
41
+ "@neo4j-cypher/lint-worker": "1.10.1-next.7"
25
42
  },
26
- "bugs": {
27
- "url": "https://github.com/neo4j/cypher-language-support/issues"
43
+ "devDependencies": {
44
+ "@types/lodash.debounce": "^4.0.9",
45
+ "copyfiles": "^2.4.1"
28
46
  },
29
- "engineStrict": true,
30
47
  "engines": {
31
- "node": ">=18.18.2"
32
- },
33
- "bin": {
34
- "cypher-language-server": "./dist/cypher-language-server"
35
- },
36
- "dependencies": {
37
- "@neo4j-cypher/language-support": "2.0.0-next.2",
38
- "neo4j-driver": "^5.3.0",
39
- "vscode-languageserver": "^8.1.0",
40
- "vscode-languageserver-textdocument": "^1.0.8"
48
+ "node": ">=24.11.1"
41
49
  },
50
+ "engineStrict": true,
42
51
  "scripts": {
43
- "build": "tsc -b && npm run bundle && npm run make-executable",
44
- "bundle": "esbuild ./src/server.ts --bundle --format=cjs --platform=node --outfile=dist/cypher-language-server.js --minify",
52
+ "build": "tsc -b && pnpm bundle && pnpm make-executable && pnpm copy-lint-worker",
53
+ "copy-lint-worker": "copyfiles -u 4 ../lint-worker/dist/cjs/lintWorker.cjs dist/",
54
+ "bundle": "esbuild ./src/server.ts --bundle --format=cjs --platform=node --outfile=dist/cypher-language-server.js --minify --conditions=require",
55
+ "dev": "tsc --watch",
45
56
  "make-executable": "cd dist && echo '#!/usr/bin/env node' > cypher-language-server && cat cypher-language-server.js >> cypher-language-server",
46
- "clean": "rm -rf dist",
47
- "watch": "tsc -b -w"
48
- },
49
- "devDependencies": {
50
- "esbuild": "^0.19.4"
57
+ "clean": "rm -rf {dist,tsconfig.tsbuildinfo}"
51
58
  }
52
- }
59
+ }
@@ -1,34 +1,55 @@
1
1
  import {
2
+ CompletionParams,
3
+ CompletionTriggerKind,
2
4
  Position,
3
- TextDocumentPositionParams,
4
5
  TextDocuments,
5
6
  } from 'vscode-languageserver/node';
6
7
 
7
- import { Range } from 'vscode-languageserver-types';
8
-
9
- import { autocomplete } from '@neo4j-cypher/language-support';
10
- import { Neo4jSchemaPoller } from '@neo4j-cypher/schema-poller';
8
+ import type { CompletionItem } from '@neo4j-cypher/language-support';
9
+ import {
10
+ autocomplete,
11
+ shouldAutoCompleteYield,
12
+ } from '@neo4j-cypher/language-support';
13
+ import { Neo4jSchemaPoller } from '@neo4j-cypher/query-tools';
11
14
  import { TextDocument } from 'vscode-languageserver-textdocument';
12
15
 
13
16
  export function doAutoCompletion(
14
17
  documents: TextDocuments<TextDocument>,
15
18
  neo4j: Neo4jSchemaPoller,
16
19
  ) {
17
- return (textDocumentPosition: TextDocumentPositionParams) => {
18
- const textDocument = documents.get(textDocumentPosition.textDocument.uri);
20
+ return (completionParams: CompletionParams) => {
21
+ const textDocument = documents.get(completionParams.textDocument.uri);
19
22
  if (textDocument === undefined) return [];
20
23
 
21
- const position: Position = textDocumentPosition.position;
22
- const range: Range = {
23
- // TODO Nacho: We are parsing from the begining of the file.
24
- // Do we need to parse from the begining of the current query?
25
- start: Position.create(0, 0),
26
- end: position,
27
- };
28
-
29
- return autocomplete(
30
- textDocument.getText(range),
31
- neo4j.metadata?.dbSchema ?? {},
24
+ const position: Position = completionParams.position;
25
+ const offset = textDocument.offsetAt(position);
26
+ const yieldTriggered = shouldAutoCompleteYield(
27
+ textDocument.getText(),
28
+ offset,
32
29
  );
30
+ const manualOrCharacterOrInwordTriggered =
31
+ completionParams.context?.triggerCharacter !== ' ';
32
+ if (yieldTriggered || manualOrCharacterOrInwordTriggered) {
33
+ const completions: CompletionItem[] = autocomplete(
34
+ textDocument.getText(),
35
+ neo4j.metadata?.dbSchema ?? {},
36
+ offset,
37
+ completionParams.context.triggerKind === CompletionTriggerKind.Invoked,
38
+ );
39
+
40
+ const result = completions.map((item) => {
41
+ if (item.signature) {
42
+ return {
43
+ ...item,
44
+ detail: item.detail + ' ' + item.signature,
45
+ signature: undefined,
46
+ };
47
+ } else {
48
+ return item;
49
+ }
50
+ });
51
+
52
+ return result;
53
+ }
33
54
  };
34
55
  }
@@ -0,0 +1,34 @@
1
+ import { formatQuery } from '@neo4j-cypher/language-support';
2
+ import { TextDocument } from 'vscode-languageserver-textdocument';
3
+ import {
4
+ DocumentFormattingParams,
5
+ TextDocuments,
6
+ TextEdit,
7
+ } from 'vscode-languageserver/node';
8
+
9
+ export const formatDocument = (
10
+ params: DocumentFormattingParams,
11
+ documents: TextDocuments<TextDocument>,
12
+ ): TextEdit[] => {
13
+ const document = documents.get(params.textDocument.uri);
14
+ if (!document) {
15
+ return [];
16
+ }
17
+
18
+ const text = document.getText();
19
+ const formattedText = formatQuery(text).formattedQuery;
20
+
21
+ if (text === formattedText) {
22
+ return [];
23
+ }
24
+
25
+ return [
26
+ TextEdit.replace(
27
+ {
28
+ start: { line: 0, character: 0 },
29
+ end: { line: document.lineCount, character: 0 },
30
+ },
31
+ formattedText,
32
+ ),
33
+ ];
34
+ };
package/src/linting.ts ADDED
@@ -0,0 +1,105 @@
1
+ import {
2
+ _internalFeatureFlags,
3
+ clampUnsafePositions,
4
+ parserWrapper,
5
+ SymbolTable,
6
+ } from '@neo4j-cypher/language-support';
7
+ import { Neo4jSchemaPoller } from '@neo4j-cypher/query-tools';
8
+ import debounce from 'lodash.debounce';
9
+ import { join } from 'path';
10
+ import { Diagnostic } from 'vscode-languageserver';
11
+ import { TextDocument } from 'vscode-languageserver-textdocument';
12
+ import workerpool from 'workerpool';
13
+ import {
14
+ convertDbSchema,
15
+ LinterTask,
16
+ LintWorker,
17
+ } from '@neo4j-cypher/lint-worker';
18
+
19
+ const defaultWorkerPath = join(__dirname, 'lintWorker.cjs');
20
+
21
+ let pool = workerpool.pool(defaultWorkerPath, {
22
+ minWorkers: 2,
23
+ workerTerminateTimeout: 0,
24
+ });
25
+ let linterVersion: string | undefined = undefined;
26
+ let lastSemanticJob: LinterTask | undefined;
27
+
28
+ /**Sets the lintworker to the one specified by the given path, reverting to default if the path is undefined */
29
+ export async function setLintWorker(
30
+ lintWorkerPath: string | undefined = defaultWorkerPath,
31
+ linter: string | undefined,
32
+ ) {
33
+ await cleanupWorkers();
34
+ linterVersion = linter;
35
+ pool = workerpool.pool(lintWorkerPath, {
36
+ minWorkers: 2,
37
+ workerTerminateTimeout: 0,
38
+ });
39
+ }
40
+
41
+ async function rawLintDocument(
42
+ document: TextDocument,
43
+ sendDiagnostics: (diagnostics: Diagnostic[]) => void,
44
+ notifySymbolTableDone: (symbolTable: SymbolTable[]) => Promise<void>,
45
+ neo4j: Neo4jSchemaPoller,
46
+ ) {
47
+ const query = document.getText();
48
+ if (query.length === 0) {
49
+ sendDiagnostics([]);
50
+ return;
51
+ }
52
+
53
+ const dbSchema = neo4j.metadata?.dbSchema ?? {};
54
+ try {
55
+ if (lastSemanticJob !== undefined && !lastSemanticJob.resolved) {
56
+ void lastSemanticJob.cancel();
57
+ }
58
+
59
+ const proxyWorker = (await pool.proxy()) as unknown as LintWorker;
60
+
61
+ const fixedDbSchema = convertDbSchema(dbSchema, linterVersion);
62
+ lastSemanticJob = proxyWorker.lintCypherQuery(
63
+ query,
64
+ fixedDbSchema,
65
+ _internalFeatureFlags,
66
+ );
67
+ const result = await lastSemanticJob;
68
+
69
+ //marks the entire text if any position is negative
70
+ const positionSafeResult = clampUnsafePositions(
71
+ result.diagnostics,
72
+ document,
73
+ );
74
+
75
+ // Pass the computed symbol tables to the parser
76
+ if (result.symbolTables) {
77
+ parserWrapper.setSymbolsInfo(
78
+ {
79
+ query,
80
+ symbolTables: result.symbolTables,
81
+ },
82
+ notifySymbolTableDone,
83
+ );
84
+ }
85
+
86
+ sendDiagnostics(positionSafeResult);
87
+ } catch (err) {
88
+ if (!(err instanceof workerpool.Promise.CancellationError)) {
89
+ console.error(err);
90
+ }
91
+ }
92
+ }
93
+
94
+ export const lintDocument: typeof rawLintDocument = debounce(
95
+ rawLintDocument,
96
+ 600,
97
+ {
98
+ leading: false,
99
+ trailing: true,
100
+ },
101
+ );
102
+
103
+ export const cleanupWorkers = async () => {
104
+ await pool.terminate();
105
+ };