@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/CHANGELOG.md +377 -1
- package/dist/cypher-language-server +2134 -73
- package/dist/server.js +210 -0
- package/package.json +35 -33
- package/src/autocompletion.ts +44 -20
- package/src/descriptionToMarkdown.ts +55 -0
- package/src/formatting.ts +34 -0
- package/src/hoverInfo.ts +29 -0
- package/src/hoverMarkdown.ts +56 -0
- package/src/linting.ts +87 -31
- package/src/server.ts +237 -52
- package/src/signatureHelp.ts +7 -7
- package/src/{syntaxColouring.ts → syntaxHighlighting.ts} +6 -8
- package/src/tests/descriptionToMarkdown.test.ts +157 -0
- package/src/tests/hoverMarkdown.test.ts +232 -0
- package/src/types.ts +24 -4
- package/src/lint-worker.ts +0 -14
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
|
-
"
|
|
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.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
"
|
|
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
|
-
"
|
|
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
|
+
}
|
package/src/autocompletion.ts
CHANGED
|
@@ -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 {
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
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 (
|
|
18
|
-
const textDocument = documents.get(
|
|
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 =
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
+
};
|
package/src/hoverInfo.ts
ADDED
|
@@ -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
|
+
}
|