@neo4j-cypher/language-server 2.0.0-next.4 → 2.0.0-next.40

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/src/linting.ts CHANGED
@@ -1,57 +1,113 @@
1
- import { validateSyntax } from '@neo4j-cypher/language-support';
1
+ import type {
2
+ SymbolTable,
3
+ SyntaxDiagnostic,
4
+ } from '@neo4j-cypher/language-support';
5
+ import {
6
+ clampUnsafePositions,
7
+ isNotParamError,
8
+ } from '@neo4j-cypher/language-support';
9
+ import type { Neo4jSchemaPoller } from '@neo4j-cypher/query-tools';
2
10
  import debounce from 'lodash.debounce';
3
11
  import { join } from 'path';
4
- import { Diagnostic, TextDocumentChangeEvent } from 'vscode-languageserver';
5
- import { TextDocument } from 'vscode-languageserver-textdocument';
12
+ import type { Diagnostic } from 'vscode-languageserver';
13
+ import type { TextDocument } from 'vscode-languageserver-textdocument';
6
14
  import workerpool from 'workerpool';
7
- import { LinterTask, LintWorker } from './lint-worker';
15
+ import type { LinterTask, LintWorker } from '@neo4j-cypher/lint-worker';
16
+ import { convertDbSchema } from '@neo4j-cypher/lint-worker';
17
+ import { languageService } from './server';
8
18
 
9
- const pool = workerpool.pool(join(__dirname, 'lint-worker.js'), {
19
+ const defaultWorkerPath = join(__dirname, 'lintWorker.cjs');
20
+
21
+ let pool = workerpool.pool(defaultWorkerPath, {
10
22
  minWorkers: 2,
11
- workerTerminateTimeout: 2000,
23
+ workerTerminateTimeout: 0,
12
24
  });
13
-
25
+ let linterVersion: string | undefined = undefined;
14
26
  let lastSemanticJob: LinterTask | undefined;
15
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
+
16
41
  async function rawLintDocument(
17
- change: TextDocumentChangeEvent<TextDocument>,
42
+ document: TextDocument,
18
43
  sendDiagnostics: (diagnostics: Diagnostic[]) => void,
44
+ notifySymbolTableDone: (symbolTable: SymbolTable[]) => Promise<void>,
45
+ neo4j: Neo4jSchemaPoller,
19
46
  ) {
20
- const { document } = change;
21
-
22
47
  const query = document.getText();
23
48
  if (query.length === 0) {
49
+ sendDiagnostics([]);
24
50
  return;
25
51
  }
26
52
 
27
- const syntaxErrors = validateSyntax(query, {});
53
+ const dbSchema = neo4j.metadata?.dbSchema ?? {};
54
+ try {
55
+ if (lastSemanticJob !== undefined && !lastSemanticJob.resolved) {
56
+ void lastSemanticJob.cancel();
57
+ }
28
58
 
29
- sendDiagnostics(syntaxErrors);
59
+ const proxyWorker = (await pool.proxy()) as unknown as LintWorker;
30
60
 
31
- if (syntaxErrors.length === 0) {
32
- try {
33
- if (lastSemanticJob !== undefined && !lastSemanticJob.resolved) {
34
- void lastSemanticJob.cancel();
35
- }
61
+ const fixedDbSchema = convertDbSchema(dbSchema, linterVersion);
62
+ lastSemanticJob = proxyWorker.lintCypherQuery(query, fixedDbSchema, {
63
+ consoleCommands: false,
64
+ });
65
+ const result = await lastSemanticJob;
36
66
 
37
- const proxyWorker = (await pool.proxy()) as unknown as LintWorker;
38
- lastSemanticJob = proxyWorker.validateSemantics(query);
39
- const result = await lastSemanticJob;
67
+ // Pass the computed symbol tables to the parser
68
+ if (result.symbolTables) {
69
+ languageService.setSymbolsInfo(
70
+ {
71
+ query,
72
+ symbolTables: result.symbolTables,
73
+ },
74
+ notifySymbolTableDone,
75
+ );
76
+ }
40
77
 
41
- sendDiagnostics(result);
42
- } catch (err) {
43
- if (!(err instanceof workerpool.Promise.CancellationError)) {
44
- console.error(err);
45
- }
78
+ sendDiagnostics(
79
+ filterLintResult(
80
+ result.diagnostics,
81
+ dbSchema?.databaseNames?.length ? false : true,
82
+ document,
83
+ ),
84
+ );
85
+ } catch (err) {
86
+ if (!(err instanceof workerpool.Promise.CancellationError)) {
87
+ console.error(err);
46
88
  }
47
89
  }
48
90
  }
49
91
 
50
- export const lintDocument = debounce(rawLintDocument, 600, {
51
- leading: false,
52
- trailing: true,
53
- });
92
+ function filterLintResult(
93
+ diagnostics: SyntaxDiagnostic[],
94
+ dontWarnOnParams: boolean,
95
+ document: TextDocument,
96
+ ) {
97
+ return dontWarnOnParams
98
+ ? clampUnsafePositions(diagnostics.filter(isNotParamError), document)
99
+ : clampUnsafePositions(diagnostics, document);
100
+ }
101
+
102
+ export const lintDocument: typeof rawLintDocument = debounce(
103
+ rawLintDocument,
104
+ 600,
105
+ {
106
+ leading: false,
107
+ trailing: true,
108
+ },
109
+ );
54
110
 
55
- export const cleanupWorkers = () => {
56
- void pool.terminate();
111
+ export const cleanupWorkers = async () => {
112
+ await pool.terminate();
57
113
  };
package/src/server.ts CHANGED
@@ -1,32 +1,161 @@
1
+ import type {
2
+ Diagnostic,
3
+ Hover,
4
+ HoverParams,
5
+ InitializeResult,
6
+ SemanticTokensRegistrationOptions,
7
+ } from 'vscode-languageserver/node';
1
8
  import {
2
9
  createConnection,
3
- Diagnostic,
4
10
  DidChangeConfigurationNotification,
5
- InitializeResult,
6
11
  ProposedFeatures,
7
- SemanticTokensRegistrationOptions,
8
12
  SemanticTokensRegistrationType,
9
13
  TextDocuments,
10
14
  TextDocumentSyncKind,
11
15
  } from 'vscode-languageserver/node';
12
16
 
13
17
  import { TextDocument } from 'vscode-languageserver-textdocument';
14
-
15
- import { syntaxColouringLegend } from '@neo4j-cypher/language-support';
16
- import { Neo4jSchemaPoller } from '@neo4j-cypher/schema-poller';
18
+ import type { DbSchema, SymbolTable } from '@neo4j-cypher/language-support';
19
+ import {
20
+ syntaxHighlightingLegend,
21
+ CypherLanguageService,
22
+ } from '@neo4j-cypher/language-support';
23
+ import { Neo4jSchemaPoller } from '@neo4j-cypher/query-tools';
17
24
  import { doAutoCompletion } from './autocompletion';
25
+ import { formatDocument } from './formatting';
26
+ import { cleanupWorkers, lintDocument, setLintWorker } from './linting';
18
27
  import { doSignatureHelp } from './signatureHelp';
19
- import { applySyntaxColouringForDocument } from './syntaxColouring';
20
- import { Neo4jSettings } from './types';
28
+ import { highlightSyntaxForDocument } from './syntaxHighlighting';
29
+ import type {
30
+ LintWorkerSettings,
31
+ Neo4jConnectionSettings,
32
+ Neo4jParameters,
33
+ Neo4jSettings,
34
+ } from './types';
35
+ import workerpool from 'workerpool';
36
+ import type { LintWorker } from '@neo4j-cypher/lint-worker';
37
+ import { convertDbSchema } from '@neo4j-cypher/lint-worker';
38
+ import { join } from 'path';
39
+ import { doHoverInfo } from './hoverInfo.js';
21
40
 
22
- const connection = createConnection(ProposedFeatures.all);
41
+ const defaultWorkerPath: string = join(__dirname, 'lintWorker.cjs');
42
+ let workerPath = defaultWorkerPath;
43
+ export const languageService = new CypherLanguageService({
44
+ consoleCommandsEnabled: false,
45
+ });
23
46
 
24
- import { cleanupWorkers, lintDocument } from './linting';
47
+ class SymbolFetcher {
48
+ private processing = false;
49
+ private nextJob:
50
+ | {
51
+ query: string;
52
+ uri: string;
53
+ schema: DbSchema;
54
+ }
55
+ | undefined;
56
+ private symbolTablePool = workerpool.pool(defaultWorkerPath, {
57
+ maxWorkers: 1,
58
+ workerTerminateTimeout: 0,
59
+ });
60
+ private linterVersion: string | undefined;
61
+
62
+ public setLintWorker(
63
+ lintWorkerPath: string | undefined = defaultWorkerPath,
64
+ linterVersion: string | undefined,
65
+ ) {
66
+ this.linterVersion = linterVersion;
67
+ this.symbolTablePool = workerpool.pool(lintWorkerPath, {
68
+ maxWorkers: 1,
69
+ workerTerminateTimeout: 0,
70
+ });
71
+ }
72
+
73
+ public queueSymbolJob(query: string, uri: string, schema: DbSchema) {
74
+ this.nextJob = { query, uri, schema };
75
+ if (!this.processing) {
76
+ void this.processJobQueue();
77
+ }
78
+ }
79
+
80
+ private async processJobQueue() {
81
+ this.processing = true;
82
+ while (this.nextJob) {
83
+ try {
84
+ const query = this.nextJob.query;
85
+ const dbSchema = this.nextJob.schema;
86
+ const docUri = this.nextJob.uri;
87
+ this.nextJob = undefined;
88
+ const proxyWorker =
89
+ (await this.symbolTablePool.proxy()) as unknown as LintWorker;
90
+ const fixedDbSchema = convertDbSchema(dbSchema, this.linterVersion);
91
+
92
+ const result = await proxyWorker.lintCypherQuery(query, fixedDbSchema);
93
+
94
+ if (
95
+ //if this.nextJob has new doc, our result is no longer valid
96
+ result.symbolTables &&
97
+ !(this.nextJob && this.nextJob.uri != docUri)
98
+ ) {
99
+ languageService.setSymbolsInfo(
100
+ {
101
+ query,
102
+ symbolTables: result.symbolTables,
103
+ },
104
+ async (symbolTables: SymbolTable[]) =>
105
+ await connection.sendNotification('symbolTableDone', {
106
+ symbolTables,
107
+ }),
108
+ );
109
+ }
110
+ } catch {
111
+ //eslint-disable-next-line
112
+ console.log('Symbol table calculation failed');
113
+ }
114
+ }
115
+ this.processing = false;
116
+ }
117
+ }
118
+
119
+ const connection = createConnection(ProposedFeatures.all);
120
+ let settings: Neo4jSettings | undefined = undefined;
25
121
 
26
122
  // Create a simple text document manager.
27
123
  const documents: TextDocuments<TextDocument> = new TextDocuments(TextDocument);
124
+ const neo4jSchemaPoller = new Neo4jSchemaPoller();
125
+ const symbolFetcher = new SymbolFetcher();
126
+
127
+ async function lintSingleDocument(document: TextDocument): Promise<void> {
128
+ symbolFetcher.queueSymbolJob(
129
+ document.getText(),
130
+ document.uri,
131
+ neo4jSchemaPoller?.metadata?.dbSchema,
132
+ );
133
+ if (settings?.features?.linting) {
134
+ return lintDocument(
135
+ document,
136
+ (diagnostics: Diagnostic[]) => {
137
+ void connection.sendDiagnostics({
138
+ uri: document.uri,
139
+ diagnostics,
140
+ });
141
+ },
142
+ async (symbolTables: SymbolTable[]) =>
143
+ await connection.sendNotification('symbolTableDone', {
144
+ symbolTables: symbolTables,
145
+ }),
146
+ neo4jSchemaPoller,
147
+ );
148
+ } else {
149
+ void connection.sendDiagnostics({
150
+ uri: document.uri,
151
+ diagnostics: [],
152
+ });
153
+ }
154
+ }
28
155
 
29
- const neo4jSdk = new Neo4jSchemaPoller();
156
+ function relintAllDocuments() {
157
+ void documents.all().map(lintSingleDocument);
158
+ }
30
159
 
31
160
  connection.onInitialize(() => {
32
161
  const result: InitializeResult = {
@@ -35,11 +164,11 @@ connection.onInitialize(() => {
35
164
  // Tell the client what features does the server support
36
165
  completionProvider: {
37
166
  resolveProvider: false,
38
- triggerCharacters: ['.'],
167
+ triggerCharacters: ['.', ':', '{', '$', ')', ' ', ']', '-', '<'],
39
168
  },
40
169
  semanticTokensProvider: {
41
- documentSelector: null,
42
- legend: syntaxColouringLegend,
170
+ documentSelector: [{ language: 'cypher' }],
171
+ legend: syntaxHighlightingLegend,
43
172
  range: false,
44
173
  full: {
45
174
  delta: false,
@@ -48,6 +177,8 @@ connection.onInitialize(() => {
48
177
  signatureHelpProvider: {
49
178
  triggerCharacters: ['(', ',', ')'],
50
179
  },
180
+ hoverProvider: true,
181
+ documentFormattingProvider: true,
51
182
  },
52
183
  };
53
184
 
@@ -60,8 +191,8 @@ connection.onInitialized(() => {
60
191
  });
61
192
 
62
193
  const registrationOptions: SemanticTokensRegistrationOptions = {
63
- documentSelector: null,
64
- legend: syntaxColouringLegend,
194
+ documentSelector: [{ language: 'cypher' }],
195
+ legend: syntaxHighlightingLegend,
65
196
  range: false,
66
197
  full: {
67
198
  delta: false,
@@ -73,53 +204,107 @@ connection.onInitialized(() => {
73
204
  );
74
205
  });
75
206
 
76
- documents.onDidChangeContent((change) =>
77
- lintDocument(change, (diagnostics: Diagnostic[]) => {
78
- void connection.sendDiagnostics({
79
- uri: change.document.uri,
80
- diagnostics,
81
- });
82
- }),
83
- );
207
+ connection.onDidChangeConfiguration((params) => {
208
+ settings = params.settings?.neo4j as Neo4jSettings;
209
+ relintAllDocuments();
210
+ });
84
211
 
85
- // Trigger the syntax colouring
86
- connection.languages.semanticTokens.on(
87
- applySyntaxColouringForDocument(documents),
88
- );
212
+ documents.onDidChangeContent((change) => lintSingleDocument(change.document));
213
+
214
+ // Trigger the syntax highlighting
215
+ connection.languages.semanticTokens.on(highlightSyntaxForDocument(documents));
89
216
 
90
217
  // Trigger the signature help, providing info about functions / procedures
91
- connection.onSignatureHelp(doSignatureHelp(documents, neo4jSdk));
218
+ connection.onSignatureHelp(doSignatureHelp(documents, neo4jSchemaPoller));
92
219
  // Trigger the auto completion
93
- connection.onCompletion(doAutoCompletion(documents, neo4jSdk));
94
-
95
- connection.onDidChangeConfiguration(
96
- (params: { settings: { neo4j: Neo4jSettings } }) => {
97
- neo4jSdk.disconnect();
98
-
99
- const neo4jConfig = params.settings.neo4j;
100
- if (
101
- neo4jSdk.connection === undefined &&
102
- neo4jConfig.connect &&
103
- neo4jConfig.password &&
104
- neo4jConfig.URL &&
105
- neo4jConfig.user
106
- ) {
107
- void neo4jSdk.persistentConnect(
108
- neo4jConfig.URL,
109
- {
110
- username: neo4jConfig.user,
111
- password: neo4jConfig.password,
112
- },
113
- { appName: 'cypher-language-server' },
114
- );
220
+ connection.onCompletion(doAutoCompletion(documents, neo4jSchemaPoller));
221
+
222
+ connection.onNotification(
223
+ 'updateLintWorker',
224
+ (linterSettings: LintWorkerSettings) => {
225
+ const lintWorkerPath = linterSettings.lintWorkerPath;
226
+ const linterVersion = linterSettings.linterVersion;
227
+ if (lintWorkerPath !== workerPath) {
228
+ workerPath = lintWorkerPath;
229
+ void (async () => {
230
+ symbolFetcher.setLintWorker(workerPath, linterVersion);
231
+ await setLintWorker(workerPath, linterVersion);
232
+ relintAllDocuments();
233
+ })();
115
234
  }
116
235
  },
117
236
  );
118
237
 
238
+ connection.onNotification(
239
+ 'connectionUpdated',
240
+ (connectionSettings: Neo4jConnectionSettings) => {
241
+ changeConnection(connectionSettings);
242
+ neo4jSchemaPoller.events.once('schemaFetched', relintAllDocuments);
243
+ },
244
+ );
245
+
246
+ connection.onHover((params: HoverParams): Hover | null => {
247
+ return doHoverInfo(documents, neo4jSchemaPoller, params);
248
+ });
249
+
250
+ connection.onNotification(
251
+ 'fetchSymbolTable',
252
+ (params: {
253
+ query: string;
254
+ uri: string;
255
+ version: number;
256
+ schema: DbSchema;
257
+ }) => {
258
+ neo4jSchemaPoller.events.once('schemaFetched', () =>
259
+ symbolFetcher.queueSymbolJob(params.query, params.uri, params.schema),
260
+ );
261
+ },
262
+ );
263
+
264
+ connection.onNotification('updateParameters', (parameters: Neo4jParameters) => {
265
+ neo4jSchemaPoller.setParameters(parameters);
266
+ relintAllDocuments();
267
+ });
268
+
269
+ connection.onDocumentFormatting((params) => {
270
+ return formatDocument(params, documents);
271
+ });
272
+
273
+ connection.onNotification('connectionDisconnected', () => {
274
+ disconnect();
275
+ relintAllDocuments();
276
+ });
277
+
119
278
  documents.listen(connection);
120
279
 
121
280
  connection.listen();
122
281
 
123
282
  connection.onExit(() => {
124
- cleanupWorkers();
283
+ void cleanupWorkers();
125
284
  });
285
+
286
+ const changeConnection = (connectionSettings: Neo4jConnectionSettings) => {
287
+ disconnect();
288
+
289
+ if (
290
+ neo4jSchemaPoller.connection === undefined &&
291
+ connectionSettings.connect &&
292
+ connectionSettings.password &&
293
+ connectionSettings.connectURL &&
294
+ connectionSettings.user
295
+ ) {
296
+ void neo4jSchemaPoller.persistentConnect(
297
+ connectionSettings.connectURL,
298
+ {
299
+ username: connectionSettings.user,
300
+ password: connectionSettings.password,
301
+ },
302
+ { appName: 'cypher-language-server' },
303
+ connectionSettings.database,
304
+ );
305
+ }
306
+ };
307
+
308
+ const disconnect = () => {
309
+ neo4jSchemaPoller.disconnect();
310
+ };
@@ -1,14 +1,14 @@
1
- import { signatureHelp } from '@neo4j-cypher/language-support';
2
- import {
1
+ import type {
3
2
  SignatureHelp,
4
3
  SignatureHelpParams,
5
4
  TextDocuments,
6
5
  } from 'vscode-languageserver/node';
7
6
 
8
- import { Neo4jSchemaPoller } from '@neo4j-cypher/schema-poller';
9
- import { TextDocument } from 'vscode-languageserver-textdocument';
7
+ import type { Neo4jSchemaPoller } from '@neo4j-cypher/query-tools';
8
+ import type { TextDocument } from 'vscode-languageserver-textdocument';
9
+ import { languageService } from './server';
10
10
 
11
- export const emptyResult: SignatureHelp = {
11
+ const emptyResult: SignatureHelp = {
12
12
  signatures: [],
13
13
  activeSignature: undefined,
14
14
  activeParameter: undefined,
@@ -26,10 +26,10 @@ export function doSignatureHelp(
26
26
  const position = params.position;
27
27
  const offset = textDocument.offsetAt(position);
28
28
 
29
- return signatureHelp(
29
+ return languageService.getSignatureHelp(
30
30
  textDocument.getText(),
31
31
  neo4j.metadata?.dbSchema ?? {},
32
- offset,
32
+ { caretPosition: offset },
33
33
  );
34
34
  };
35
35
  }
@@ -1,22 +1,20 @@
1
- import {
2
- applySyntaxColouring,
3
- mapCypherToSemanticTokenIndex,
4
- } from '@neo4j-cypher/language-support';
5
- import {
6
- SemanticTokensBuilder,
1
+ import { mapCypherToSemanticTokenIndex } from '@neo4j-cypher/language-support';
2
+ import type {
7
3
  SemanticTokensParams,
8
4
  TextDocument,
9
5
  TextDocuments,
10
6
  } from 'vscode-languageserver';
7
+ import { SemanticTokensBuilder } from 'vscode-languageserver';
8
+ import { languageService } from './server';
11
9
 
12
- export function applySyntaxColouringForDocument(
10
+ export function highlightSyntaxForDocument(
13
11
  documents: TextDocuments<TextDocument>,
14
12
  ) {
15
13
  return (params: SemanticTokensParams) => {
16
14
  const textDocument = documents.get(params.textDocument.uri);
17
15
  if (textDocument === undefined) return { data: [] };
18
16
 
19
- const tokens = applySyntaxColouring(textDocument.getText());
17
+ const tokens = languageService.highlightSyntax(textDocument.getText());
20
18
 
21
19
  const builder = new SemanticTokensBuilder();
22
20