@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/dist/server.js ADDED
@@ -0,0 +1,210 @@
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
+ exports.languageService = void 0;
7
+ const node_1 = require("vscode-languageserver/node");
8
+ const vscode_languageserver_textdocument_1 = require("vscode-languageserver-textdocument");
9
+ const language_support_1 = require("@neo4j-cypher/language-support");
10
+ const query_tools_1 = require("@neo4j-cypher/query-tools");
11
+ const autocompletion_1 = require("./autocompletion");
12
+ const formatting_1 = require("./formatting");
13
+ const linting_1 = require("./linting");
14
+ const signatureHelp_1 = require("./signatureHelp");
15
+ const syntaxHighlighting_1 = require("./syntaxHighlighting");
16
+ const workerpool_1 = __importDefault(require("workerpool"));
17
+ const lint_worker_1 = require("@neo4j-cypher/lint-worker");
18
+ const path_1 = require("path");
19
+ const hoverInfo_js_1 = require("./hoverInfo.js");
20
+ const defaultWorkerPath = (0, path_1.join)(__dirname, 'lintWorker.cjs');
21
+ let workerPath = defaultWorkerPath;
22
+ exports.languageService = new language_support_1.CypherLanguageService({
23
+ consoleCommandsEnabled: false,
24
+ });
25
+ class SymbolFetcher {
26
+ processing = false;
27
+ nextJob;
28
+ symbolTablePool = workerpool_1.default.pool(defaultWorkerPath, {
29
+ maxWorkers: 1,
30
+ workerTerminateTimeout: 0,
31
+ });
32
+ linterVersion;
33
+ setLintWorker(lintWorkerPath = defaultWorkerPath, linterVersion) {
34
+ this.linterVersion = linterVersion;
35
+ this.symbolTablePool = workerpool_1.default.pool(lintWorkerPath, {
36
+ maxWorkers: 1,
37
+ workerTerminateTimeout: 0,
38
+ });
39
+ }
40
+ queueSymbolJob(query, uri, schema) {
41
+ this.nextJob = { query, uri, schema };
42
+ if (!this.processing) {
43
+ void this.processJobQueue();
44
+ }
45
+ }
46
+ async processJobQueue() {
47
+ this.processing = true;
48
+ while (this.nextJob) {
49
+ try {
50
+ const query = this.nextJob.query;
51
+ const dbSchema = this.nextJob.schema;
52
+ const docUri = this.nextJob.uri;
53
+ this.nextJob = undefined;
54
+ const proxyWorker = (await this.symbolTablePool.proxy());
55
+ const fixedDbSchema = (0, lint_worker_1.convertDbSchema)(dbSchema, this.linterVersion);
56
+ const result = await proxyWorker.lintCypherQuery(query, fixedDbSchema);
57
+ if (
58
+ //if this.nextJob has new doc, our result is no longer valid
59
+ result.symbolTables &&
60
+ !(this.nextJob && this.nextJob.uri != docUri)) {
61
+ exports.languageService.setSymbolsInfo({
62
+ query,
63
+ symbolTables: result.symbolTables,
64
+ }, async (symbolTables) => await connection.sendNotification('symbolTableDone', {
65
+ symbolTables,
66
+ }));
67
+ }
68
+ }
69
+ catch {
70
+ //eslint-disable-next-line
71
+ console.log('Symbol table calculation failed');
72
+ }
73
+ }
74
+ this.processing = false;
75
+ }
76
+ }
77
+ const connection = (0, node_1.createConnection)(node_1.ProposedFeatures.all);
78
+ let settings = undefined;
79
+ // Create a simple text document manager.
80
+ const documents = new node_1.TextDocuments(vscode_languageserver_textdocument_1.TextDocument);
81
+ const neo4jSchemaPoller = new query_tools_1.Neo4jSchemaPoller();
82
+ const symbolFetcher = new SymbolFetcher();
83
+ async function lintSingleDocument(document) {
84
+ symbolFetcher.queueSymbolJob(document.getText(), document.uri, neo4jSchemaPoller?.metadata?.dbSchema);
85
+ if (settings?.features?.linting) {
86
+ return (0, linting_1.lintDocument)(document, (diagnostics) => {
87
+ void connection.sendDiagnostics({
88
+ uri: document.uri,
89
+ diagnostics,
90
+ });
91
+ }, async (symbolTables) => await connection.sendNotification('symbolTableDone', {
92
+ symbolTables: symbolTables,
93
+ }), neo4jSchemaPoller);
94
+ }
95
+ else {
96
+ void connection.sendDiagnostics({
97
+ uri: document.uri,
98
+ diagnostics: [],
99
+ });
100
+ }
101
+ }
102
+ function relintAllDocuments() {
103
+ void documents.all().map(lintSingleDocument);
104
+ }
105
+ connection.onInitialize(() => {
106
+ const result = {
107
+ capabilities: {
108
+ textDocumentSync: node_1.TextDocumentSyncKind.Full,
109
+ // Tell the client what features does the server support
110
+ completionProvider: {
111
+ resolveProvider: false,
112
+ triggerCharacters: ['.', ':', '{', '$', ')', ' ', ']', '-', '<'],
113
+ },
114
+ semanticTokensProvider: {
115
+ documentSelector: [{ language: 'cypher' }],
116
+ legend: language_support_1.syntaxHighlightingLegend,
117
+ range: false,
118
+ full: {
119
+ delta: false,
120
+ },
121
+ },
122
+ signatureHelpProvider: {
123
+ triggerCharacters: ['(', ',', ')'],
124
+ },
125
+ hoverProvider: true,
126
+ documentFormattingProvider: true,
127
+ },
128
+ };
129
+ return result;
130
+ });
131
+ connection.onInitialized(() => {
132
+ void connection.client.register(node_1.DidChangeConfigurationNotification.type, {
133
+ section: 'neo4j',
134
+ });
135
+ const registrationOptions = {
136
+ documentSelector: [{ language: 'cypher' }],
137
+ legend: language_support_1.syntaxHighlightingLegend,
138
+ range: false,
139
+ full: {
140
+ delta: false,
141
+ },
142
+ };
143
+ void connection.client.register(node_1.SemanticTokensRegistrationType.type, registrationOptions);
144
+ });
145
+ connection.onDidChangeConfiguration((params) => {
146
+ settings = params.settings?.neo4j;
147
+ relintAllDocuments();
148
+ });
149
+ documents.onDidChangeContent((change) => lintSingleDocument(change.document));
150
+ // Trigger the syntax highlighting
151
+ connection.languages.semanticTokens.on((0, syntaxHighlighting_1.highlightSyntaxForDocument)(documents));
152
+ // Trigger the signature help, providing info about functions / procedures
153
+ connection.onSignatureHelp((0, signatureHelp_1.doSignatureHelp)(documents, neo4jSchemaPoller));
154
+ // Trigger the auto completion
155
+ connection.onCompletion((0, autocompletion_1.doAutoCompletion)(documents, neo4jSchemaPoller));
156
+ connection.onNotification('updateLintWorker', (linterSettings) => {
157
+ const lintWorkerPath = linterSettings.lintWorkerPath;
158
+ const linterVersion = linterSettings.linterVersion;
159
+ if (lintWorkerPath !== workerPath) {
160
+ workerPath = lintWorkerPath;
161
+ void (async () => {
162
+ symbolFetcher.setLintWorker(workerPath, linterVersion);
163
+ await (0, linting_1.setLintWorker)(workerPath, linterVersion);
164
+ relintAllDocuments();
165
+ })();
166
+ }
167
+ });
168
+ connection.onNotification('connectionUpdated', (connectionSettings) => {
169
+ changeConnection(connectionSettings);
170
+ neo4jSchemaPoller.events.once('schemaFetched', relintAllDocuments);
171
+ });
172
+ connection.onHover((params) => {
173
+ return (0, hoverInfo_js_1.doHoverInfo)(documents, neo4jSchemaPoller, params);
174
+ });
175
+ connection.onNotification('fetchSymbolTable', (params) => {
176
+ neo4jSchemaPoller.events.once('schemaFetched', () => symbolFetcher.queueSymbolJob(params.query, params.uri, params.schema));
177
+ });
178
+ connection.onNotification('updateParameters', (parameters) => {
179
+ neo4jSchemaPoller.setParameters(parameters);
180
+ relintAllDocuments();
181
+ });
182
+ connection.onDocumentFormatting((params) => {
183
+ return (0, formatting_1.formatDocument)(params, documents);
184
+ });
185
+ connection.onNotification('connectionDisconnected', () => {
186
+ disconnect();
187
+ relintAllDocuments();
188
+ });
189
+ documents.listen(connection);
190
+ connection.listen();
191
+ connection.onExit(() => {
192
+ void (0, linting_1.cleanupWorkers)();
193
+ });
194
+ const changeConnection = (connectionSettings) => {
195
+ disconnect();
196
+ if (neo4jSchemaPoller.connection === undefined &&
197
+ connectionSettings.connect &&
198
+ connectionSettings.password &&
199
+ connectionSettings.connectURL &&
200
+ connectionSettings.user) {
201
+ void neo4jSchemaPoller.persistentConnect(connectionSettings.connectURL, {
202
+ username: connectionSettings.user,
203
+ password: connectionSettings.password,
204
+ }, { appName: 'cypher-language-server' }, connectionSettings.database);
205
+ }
206
+ };
207
+ const disconnect = () => {
208
+ neo4jSchemaPoller.disconnect();
209
+ };
210
+ //# 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.40",
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,45 +27,31 @@
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.4",
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"
25
- },
26
- "bugs": {
27
- "url": "https://github.com/neo4j/cypher-language-support/issues"
28
- },
29
- "engineStrict": true,
30
- "engines": {
31
- "node": ">=18.18.2"
32
- },
33
- "bin": {
34
- "cypher-language-server": "./dist/cypher-language-server"
35
- },
36
32
  "dependencies": {
37
- "@neo4j-cypher/language-support": "2.0.0-next.3",
38
33
  "lodash.debounce": "^4.0.8",
39
- "neo4j-driver": "^5.3.0",
40
34
  "vscode-languageserver": "^8.1.0",
41
35
  "vscode-languageserver-textdocument": "^1.0.8",
42
- "workerpool": "^9.0.4"
43
- },
44
- "scripts": {
45
- "build": "tsc -b && npm run bundle && npm run make-executable",
46
- "bundle": "esbuild ./src/server.ts --bundle --format=cjs --platform=node --outfile=dist/cypher-language-server.js --minify",
47
- "make-executable": "cd dist && echo '#!/usr/bin/env node' > cypher-language-server && cat cypher-language-server.js >> cypher-language-server",
48
- "clean": "rm -rf dist",
49
- "watch": "tsc -b -w"
36
+ "workerpool": "^9.3.3",
37
+ "@neo4j-cypher/lint-worker": "1.10.1-next.16",
38
+ "@neo4j-cypher/language-support": "2.0.0-next.39",
39
+ "@neo4j-cypher/query-tools": "2.0.0-next.39"
50
40
  },
51
41
  "devDependencies": {
52
42
  "@types/lodash.debounce": "^4.0.9",
53
- "esbuild": "^0.19.4"
43
+ "copyfiles": "^2.4.1"
44
+ },
45
+ "engines": {
46
+ "node": ">=24.11.1"
47
+ },
48
+ "scripts": {
49
+ "build": "tsc -b && pnpm bundle && pnpm make-executable && pnpm copy-lint-worker",
50
+ "copy-lint-worker": "copyfiles -u 4 ../lint-worker/dist/cjs/lintWorker.cjs dist/",
51
+ "bundle": "esbuild ./src/server.ts --bundle --format=cjs --platform=node --outfile=dist/cypher-language-server.js --minify --conditions=require",
52
+ "dev": "tsc --watch",
53
+ "test": "vitest run",
54
+ "make-executable": "cd dist && echo '#!/usr/bin/env node' > cypher-language-server && cat cypher-language-server.js >> cypher-language-server",
55
+ "clean": "rm -rf dist tsconfig*.tsbuildinfo"
54
56
  }
55
- }
57
+ }
@@ -1,34 +1,58 @@
1
- import {
1
+ import type {
2
+ CompletionParams,
2
3
  Position,
3
- TextDocumentPositionParams,
4
4
  TextDocuments,
5
5
  } from 'vscode-languageserver/node';
6
+ import { CompletionTriggerKind } 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';
11
- import { TextDocument } from 'vscode-languageserver-textdocument';
8
+ import type { CompletionItem } from '@neo4j-cypher/language-support';
9
+ import { shouldAutoCompleteYield } from '@neo4j-cypher/language-support';
10
+ import type { Neo4jSchemaPoller } from '@neo4j-cypher/query-tools';
11
+ import type { TextDocument } from 'vscode-languageserver-textdocument';
12
+ import { languageService } from './server';
12
13
 
13
14
  export function doAutoCompletion(
14
15
  documents: TextDocuments<TextDocument>,
15
16
  neo4j: Neo4jSchemaPoller,
16
17
  ) {
17
- return (textDocumentPosition: TextDocumentPositionParams) => {
18
- const textDocument = documents.get(textDocumentPosition.textDocument.uri);
18
+ return (completionParams: CompletionParams): CompletionItem[] | undefined => {
19
+ const textDocument = documents.get(completionParams.textDocument.uri);
19
20
  if (textDocument === undefined) return [];
20
21
 
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 ?? {},
22
+ const position: Position = completionParams.position;
23
+ const offset = textDocument.offsetAt(position);
24
+ const yieldTriggered = shouldAutoCompleteYield(
25
+ textDocument.getText(),
26
+ offset,
32
27
  );
28
+ const manualOrCharacterOrInwordTriggered =
29
+ completionParams.context?.triggerCharacter !== ' ';
30
+ if (yieldTriggered || manualOrCharacterOrInwordTriggered) {
31
+ const completions: CompletionItem[] = languageService.autocomplete(
32
+ textDocument.getText(),
33
+ neo4j.metadata?.dbSchema ?? {},
34
+ {
35
+ caretPosition: offset,
36
+ manual:
37
+ completionParams.context.triggerKind ===
38
+ CompletionTriggerKind.Invoked,
39
+ },
40
+ );
41
+
42
+ const result = completions.map((item) => {
43
+ if (item.signature) {
44
+ return {
45
+ ...item,
46
+ detail: item.detail + ' ' + item.signature,
47
+ signature: undefined,
48
+ };
49
+ } else {
50
+ return item;
51
+ }
52
+ });
53
+
54
+ return result;
55
+ }
56
+ return undefined;
33
57
  };
34
58
  }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Descriptions in hover info are rendered as markdown in VS Code, which eats
3
+ * the wildcard '*' usages some of them have, as in dbms.queryJmx' "(e.g.
4
+ * '*:*,name=*neo4j*' for all metrics)", so we escape those. Not the asterisks a
5
+ * description means as markup though - in a code span, or as a bullet - where
6
+ * the backslash would show up instead. Backslashes are escaped along with them.
7
+ *
8
+ * Some descriptions are map-style objects (often configs) rather than prose.
9
+ * Markdown would put those on one line, so we tag them as 'text' code blocks
10
+ * and let their own layout through.
11
+ */
12
+
13
+ const bulletMarker = '* ';
14
+
15
+ /* A code span, or the prose up to the next backtick. Pairing left to right, as
16
+ markdown does, leaves the stray backtick a few descriptions have
17
+ (apoc.nodes.rels', apoc.path.expandConfig') as prose. */
18
+ const codeSpanOrProse = /(`[^`]*`)|([^`]+|`)/g;
19
+
20
+ export function escapeDescriptionForMarkdown(description: string): string {
21
+ // Line by line, so that an unclosed backtick cannot reach the lines below
22
+ return description.split('\n').map(escapeLine).join('\n');
23
+ }
24
+
25
+ function escapeLine(line: string): string {
26
+ const bullet = line.startsWith(bulletMarker) ? bulletMarker : '';
27
+
28
+ return (
29
+ bullet +
30
+ line.slice(bullet.length).replace(
31
+ codeSpanOrProse,
32
+ /* Escaping inside a code span would show the backslash, and descriptions
33
+ put wildcards there too, e.g. 'use `*:*` to find all JMX beans'. */
34
+ (_matched, codeSpan: string | undefined, prose: string) =>
35
+ codeSpan ?? prose.replace(/[\\*]/g, '\\$&'),
36
+ )
37
+ );
38
+ }
39
+
40
+ /* For example a map-style description, laid out over several lines */
41
+ function hasInnerNewlines(description: string): boolean {
42
+ const lines = description.trimEnd().split('\n');
43
+ return lines.length > 1;
44
+ }
45
+
46
+ export function formatLineForMarkdown(
47
+ name: string,
48
+ description: string,
49
+ ): string[] {
50
+ const bullet = `- \`${name}\` -`;
51
+
52
+ return hasInnerNewlines(description)
53
+ ? [bullet, '```text', description.trimEnd(), '```']
54
+ : [`${bullet} ${escapeLine(description)}`];
55
+ }
@@ -0,0 +1,34 @@
1
+ import { formatQuery } from '@neo4j-cypher/language-support';
2
+ import type { TextDocument } from 'vscode-languageserver-textdocument';
3
+ import type {
4
+ DocumentFormattingParams,
5
+ TextDocuments,
6
+ } from 'vscode-languageserver/node';
7
+ import { TextEdit } 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
+ };
@@ -0,0 +1,29 @@
1
+ import type { Hover, HoverParams, TextDocuments } from 'vscode-languageserver';
2
+ import { MarkupKind } from 'vscode-languageserver';
3
+ import type { TextDocument } from 'vscode-languageserver-textdocument';
4
+ import { languageService } from './server.js';
5
+ import type { Neo4jSchemaPoller } from '@neo4j-cypher/query-tools';
6
+ import { hoverInfoToMarkdown } from './hoverMarkdown.js';
7
+
8
+ export function doHoverInfo(
9
+ documents: TextDocuments<TextDocument>,
10
+ neo4jSchemaPoller: Neo4jSchemaPoller,
11
+ params: HoverParams,
12
+ ): Hover | null {
13
+ const textDocument = documents.get(params.textDocument.uri);
14
+ if (textDocument === undefined) return null;
15
+ const position = params.position;
16
+ const offset = textDocument.offsetAt(position);
17
+ const hoverInfo = languageService.hoverInfo(textDocument.getText(), {
18
+ caretPosition: offset,
19
+ dbSchema: neo4jSchemaPoller.metadata?.dbSchema ?? {},
20
+ });
21
+ if (!hoverInfo) return null;
22
+
23
+ return {
24
+ contents: {
25
+ kind: MarkupKind.Markdown,
26
+ value: hoverInfoToMarkdown(hoverInfo),
27
+ },
28
+ };
29
+ }
@@ -0,0 +1,56 @@
1
+ import type {
2
+ HoverDescription,
3
+ HoverInfo,
4
+ } from '@neo4j-cypher/language-support';
5
+ import {
6
+ escapeDescriptionForMarkdown,
7
+ formatLineForMarkdown,
8
+ } from './descriptionToMarkdown.js';
9
+
10
+ function cypherCodeBlock(fragment: string): string[] {
11
+ return ['```cypher', fragment, '```'];
12
+ }
13
+
14
+ export function hoverInfoToMarkdown(info: HoverInfo): string {
15
+ if (info.kind === 'variable') {
16
+ const typeLine = ['`', `${info.variable}: ${info.types.join(', ')}`, '`'];
17
+ return (
18
+ info.labelExpression === undefined
19
+ ? typeLine
20
+ : [...typeLine, '', ...cypherCodeBlock(info.labelExpression)]
21
+ ).join('\n');
22
+ }
23
+
24
+ return [
25
+ ...cypherCodeBlock(info.signature),
26
+ `${info.isDeprecated ? '(_deprecated_) ' : ''}${escapeDescriptionForMarkdown(
27
+ info.description,
28
+ )}`,
29
+ '',
30
+ ...descriptionsToMarkdown('**Parameters**', info.parameters),
31
+ '',
32
+ ...(info.kind === 'function'
33
+ ? functionReturn(info.returnType)
34
+ : descriptionsToMarkdown('**Returns**', info.returnValues)),
35
+ ].join('\n');
36
+ }
37
+
38
+ function descriptionsToMarkdown(
39
+ heading: string,
40
+ descriptions: HoverDescription[],
41
+ ): string[] {
42
+ if (descriptions.length === 0) {
43
+ return [];
44
+ }
45
+
46
+ return [
47
+ heading,
48
+ ...descriptions.flatMap(({ name, description }) =>
49
+ formatLineForMarkdown(name, description),
50
+ ),
51
+ ];
52
+ }
53
+
54
+ function functionReturn(returnType: string): string[] {
55
+ return returnType ? [`**Returns:** \`${returnType}\``] : [];
56
+ }