@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.
@@ -0,0 +1,157 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import {
3
+ formatLineForMarkdown,
4
+ escapeDescriptionForMarkdown,
5
+ } from '../descriptionToMarkdown.js';
6
+
7
+ /* Uses descriptions from real procedures and
8
+ functions named in the test titles */
9
+
10
+ describe('escapes the wildcards a description writes as prose', () => {
11
+ test('dbms.queryJmxs query parameter', () => {
12
+ expect(
13
+ escapeDescriptionForMarkdown(
14
+ "A query for MBeans on this MBeanServer (e.g. '*:*,name=*neo4j*' for all metrics in neo4j database).",
15
+ ),
16
+ ).toBe(
17
+ "A query for MBeans on this MBeanServer (e.g. '\\*:\\*,name=\\*neo4j\\*' for all metrics in neo4j database).",
18
+ );
19
+ });
20
+
21
+ test('apoc.math.sigmoidPrime description', () => {
22
+ expect(
23
+ escapeDescriptionForMarkdown(
24
+ 'Returns the sigmoid prime [ sigmoid(val) * (1 - sigmoid(val)) ] of the given value.',
25
+ ),
26
+ ).toBe(
27
+ 'Returns the sigmoid prime [ sigmoid(val) \\* (1 - sigmoid(val)) ] of the given value.',
28
+ );
29
+ });
30
+
31
+ test('apoc.algo.aStar description', () => {
32
+ expect(escapeDescriptionForMarkdown('Runs the A* search algorithm.')).toBe(
33
+ 'Runs the A\\* search algorithm.',
34
+ );
35
+ });
36
+ });
37
+
38
+ describe('leaves the wildcards inside a code span alone', () => {
39
+ test('dbms.queryJmx description', () => {
40
+ const description =
41
+ 'Query JMX management data by domain and name. For instance, use `*:*` to find all JMX beans.';
42
+
43
+ expect(escapeDescriptionForMarkdown(description)).toBe(description);
44
+ });
45
+
46
+ test('gds.graph.nodeProperties.streams nodeLabels parameter', () => {
47
+ const listed =
48
+ "The node labels to aggregate over. Use `['*']` to indicate all.";
49
+
50
+ expect(escapeDescriptionForMarkdown(listed)).toBe(listed);
51
+ });
52
+ });
53
+
54
+ describe('escaping descriptions', () => {
55
+ test('escapes the backslashes, so that the escaping can be undone', () => {
56
+ expect(escapeDescriptionForMarkdown('a \\ b')).toBe('a \\\\ b');
57
+ expect(escapeDescriptionForMarkdown('already \\* escaped')).toBe(
58
+ 'already \\\\\\* escaped',
59
+ );
60
+ });
61
+
62
+ test('leaves the underscores of a description alone - apoc.json.path pathOptions parameter', () => {
63
+ const values =
64
+ "JSON path options: ('ALWAYS_RETURN_LIST', 'DEFAULT_PATH_LEAF_TO_NULL').";
65
+
66
+ expect(escapeDescriptionForMarkdown(values)).toBe(values);
67
+ });
68
+
69
+ test('leaves the underscores of a description alone - db.index.fulltext.queryNodes', () => {
70
+ const italics = 'Valid _key: value_ pairs for the `options` map are:';
71
+
72
+ expect(escapeDescriptionForMarkdown(italics)).toBe(italics);
73
+ });
74
+
75
+ test('leaves a backtick that is never closed alone - apoc.node.relationships.exist relTypes parameter', () => {
76
+ expect(
77
+ escapeDescriptionForMarkdown('syntax; `[<]TYPE[>]|.... with a * in it'),
78
+ ).toBe('syntax; `[<]TYPE[>]|.... with a \\* in it');
79
+ });
80
+
81
+ test('leaves a backtick that is never closed alone - apoc.path.expandConfig startNode parameter', () => {
82
+ const description =
83
+ 'The node to start the algorithm from. `startNode` can be of type `STRING` (elementId()), `INTEGER` (id()), `NODE`, or `LIST<STRING | INTEGER | NODE>.';
84
+
85
+ expect(escapeDescriptionForMarkdown(description)).toBe(description);
86
+ });
87
+
88
+ test('pairs the backticks within a line, not across the description', () => {
89
+ expect(escapeDescriptionForMarkdown('a ` b\nc * d')).toBe('a ` b\nc \\* d');
90
+ });
91
+
92
+ test('keeps the newlines a description brings, including a trailing one', () => {
93
+ expect(escapeDescriptionForMarkdown('first\n\nsecond\n')).toBe(
94
+ 'first\n\nsecond\n',
95
+ );
96
+ });
97
+
98
+ test('handles empty description', () => {
99
+ expect(escapeDescriptionForMarkdown('')).toBe('');
100
+ });
101
+ });
102
+
103
+ describe('description bullets', () => {
104
+ test('keeps the bullet markers of a description that writes a list - dbms.cluster.switchDiscoveryServiceVersion', () => {
105
+ const description = [
106
+ 'Possible values are:',
107
+ '',
108
+ '* `V1_ONLY` -- it runs only discovery service v1.',
109
+ '* `V2_ONLY` -- it runs only discovery service v2.',
110
+ ].join('\n');
111
+
112
+ expect(escapeDescriptionForMarkdown(description)).toBe(description);
113
+ });
114
+
115
+ test('escapes an asterisk that only looks like a bullet marker', () => {
116
+ // An indented bullet is not one in react-codemirror's renderer
117
+ expect(escapeDescriptionForMarkdown(' * indented')).toBe(' \\* indented');
118
+ // And neither is a bare asterisk without the space
119
+ expect(escapeDescriptionForMarkdown('*emphasised*')).toBe(
120
+ '\\*emphasised\\*',
121
+ );
122
+ // The marker is kept, the wildcard on the same line is not
123
+ expect(escapeDescriptionForMarkdown("* 'skip' -- skips *:*")).toBe(
124
+ "* 'skip' -- skips \\*:\\*",
125
+ );
126
+ });
127
+
128
+ test('writes a prose description inline', () => {
129
+ expect(
130
+ formatLineForMarkdown('query', "A query for MBeans (e.g. '*:*')."),
131
+ ).toEqual(["- `query` - A query for MBeans (e.g. '\\*:\\*')."]);
132
+ });
133
+
134
+ test('accepts a description that is several lines of prose as a textblock', () => {
135
+ expect(
136
+ formatLineForMarkdown('sort', 'Sorts the list.\nTo sort ascending.'),
137
+ ).toEqual([
138
+ '- `sort` -',
139
+ '```text',
140
+ `Sorts the list.
141
+ To sort ascending.`,
142
+ '```',
143
+ ]);
144
+ });
145
+
146
+ test('writes a description that carries its own layout as a code block - apoc.export.csv.all config parameter', () => {
147
+ const description =
148
+ '{\n delimiter = "," :: STRING,\n skipLines = 1 :: INTEGER\n}\n';
149
+
150
+ expect(formatLineForMarkdown('config', description)).toEqual([
151
+ '- `config` -',
152
+ '```text',
153
+ '{\n delimiter = "," :: STRING,\n skipLines = 1 :: INTEGER\n}',
154
+ '```',
155
+ ]);
156
+ });
157
+ });
@@ -0,0 +1,232 @@
1
+ import {
2
+ CypherLanguageService,
3
+ testData,
4
+ } from '@neo4j-cypher/language-support';
5
+ import { describe, expect, test } from 'vitest';
6
+ import { hoverInfoToMarkdown } from '../hoverMarkdown.js';
7
+
8
+ const dbSchema = testData.mockSchema;
9
+ const languageService = new CypherLanguageService();
10
+
11
+ function hoverMarkdown(query: string, hovered: string): string {
12
+ const hoverInfo = languageService.hoverInfo(query, {
13
+ caretPosition: query.indexOf(hovered) + 1,
14
+ dbSchema,
15
+ });
16
+ return hoverInfoToMarkdown(hoverInfo);
17
+ }
18
+
19
+ function variableHoverMarkdown(query: string, caretPosition: number): string {
20
+ const { symbolTables } = languageService.lint(query, dbSchema);
21
+ languageService.setSymbolsInfo({ query, symbolTables });
22
+ return hoverInfoToMarkdown(
23
+ languageService.hoverInfo(query, { caretPosition, dbSchema }),
24
+ );
25
+ }
26
+
27
+ describe('Function hover markdown', () => {
28
+ test('writes the signature, description, parameters and return type', () => {
29
+ expect(hoverMarkdown('CYPHER 25 RETURN abs(1,2)', 'abs')).toBe(
30
+ `\`\`\`cypher
31
+ abs(input :: INTEGER | FLOAT) :: INTEGER | FLOAT
32
+ \`\`\`
33
+ Returns the absolute value of an \`INTEGER\` or \`FLOAT\`.
34
+
35
+ **Parameters**
36
+ - \`input\` - A numeric value from which the absolute number will be returned.
37
+
38
+ **Returns:** \`INTEGER | FLOAT\``,
39
+ );
40
+ });
41
+
42
+ test('marks deprecated functions as deprecated', () => {
43
+ expect(
44
+ hoverMarkdown('CYPHER 5 RETURN apoc.create.uuid()', 'apoc.create.uuid'),
45
+ ).toBe(
46
+ `\`\`\`cypher
47
+ apoc.create.uuid() :: STRING
48
+ \`\`\`
49
+ (_deprecated_) Returns a UUID.
50
+
51
+
52
+ **Returns:** \`STRING\``,
53
+ );
54
+ });
55
+
56
+ test('escapes the asterisk a description writes as prose, so its not interpreted as emphasis in markdown', () => {
57
+ expect(
58
+ hoverMarkdown(
59
+ 'RETURN apoc.math.sigmoidPrime(1.0)',
60
+ 'apoc.math.sigmoidPrime',
61
+ ),
62
+ ).toBe(
63
+ `\`\`\`cypher
64
+ apoc.math.sigmoidPrime(value :: FLOAT) :: FLOAT
65
+ \`\`\`
66
+ Returns the sigmoid prime [ sigmoid(val) \\* (1 - sigmoid(val)) ] of the given value.
67
+
68
+ **Parameters**
69
+ - \`value\` - An angle in radians.
70
+
71
+ **Returns:** \`FLOAT\``,
72
+ );
73
+ });
74
+ });
75
+
76
+ describe('Procedure hover markdown', () => {
77
+ test('writes the return values of a procedure without parameters', () => {
78
+ expect(hoverMarkdown('CALL db.labels()', 'db.labels')).toBe(
79
+ `\`\`\`cypher
80
+ db.labels() :: (label :: STRING)
81
+ \`\`\`
82
+ List all labels attached to nodes within a database according to the user's access rights. The procedure returns empty results if the user is not authorized to view those labels.
83
+
84
+
85
+ **Returns**
86
+ - \`label\` - A label within the database.`,
87
+ );
88
+ });
89
+
90
+ test('writes the parameters of a procedure without return values', () => {
91
+ expect(
92
+ hoverMarkdown('CALL db.awaitIndex("MyIndex", 300)', 'db.awaitIndex'),
93
+ ).toBe(
94
+ `\`\`\`cypher
95
+ db.awaitIndex(indexName :: STRING, timeOutSeconds = 300 :: INTEGER)
96
+ \`\`\`
97
+ Wait for an index to come online (for example: CALL db.awaitIndex("MyIndex", 300)).
98
+
99
+ **Parameters**
100
+ - \`indexName\` - The name of the awaited index.
101
+ - \`timeOutSeconds\` - The maximum time to wait in seconds.
102
+ `,
103
+ );
104
+ });
105
+
106
+ test('marks deprecated procedures as deprecated', () => {
107
+ expect(
108
+ hoverMarkdown(
109
+ 'CYPHER 5 CALL db.create.setVectorProperty(n, "prop", [1.0])',
110
+ 'db.create.setVectorProperty',
111
+ ),
112
+ ).toBe(
113
+ `\`\`\`cypher
114
+ db.create.setVectorProperty(node :: NODE, key :: STRING, vector :: ANY) :: (node :: NODE)
115
+ \`\`\`
116
+ (_deprecated_) Set a vector property on a given node in a more space efficient representation than Cypher's SET.
117
+
118
+ **Parameters**
119
+ - \`node\` - The node on which the new property will be stored.
120
+ - \`key\` - The name of the new property.
121
+ - \`vector\` - The object containing the embedding.
122
+
123
+ **Returns**
124
+ - \`node\` - The node on which the vector property was set.`,
125
+ );
126
+ });
127
+
128
+ test('escapes the wildcards a description writes, but not the ones in its code spans', () => {
129
+ expect(hoverMarkdown('CALL dbms.queryJmx("*:*")', 'dbms.queryJmx')).toBe(
130
+ `\`\`\`cypher
131
+ dbms.queryJmx(query :: STRING) :: (name :: STRING, description :: STRING, attributes :: MAP)
132
+ \`\`\`
133
+ Query JMX management data by domain and name. For instance, use \`*:*\` to find all JMX beans.
134
+
135
+ **Parameters**
136
+ - \`query\` - A query for MBeans on this MBeanServer (e.g. '\\*:\\*,name=\\*neo4j\\*' for all metrics in neo4j database).
137
+
138
+ **Returns**
139
+ - \`name\` - The name of the metric.
140
+ - \`description\` - The description of the metric.
141
+ - \`attributes\` - A collection with the attributes (values) of that metric.`,
142
+ );
143
+ });
144
+
145
+ test('keeps the bullet list and the italics of a description', () => {
146
+ expect(
147
+ hoverMarkdown(
148
+ 'CALL db.index.fulltext.queryNodes("index", "query")',
149
+ 'db.index.fulltext.queryNodes',
150
+ ),
151
+ ).toBe(
152
+ `\`\`\`cypher
153
+ db.index.fulltext.queryNodes(indexName :: STRING, queryString :: STRING, options = {} :: MAP) :: (node :: NODE, score :: FLOAT)
154
+ \`\`\`
155
+ Query the given full-text index. Returns the matching nodes and their Lucene query score, ordered by score.
156
+ Valid _key: value_ pairs for the \`options\` map are:
157
+
158
+ * 'skip' -- to skip the top N results.
159
+ * 'limit' -- to limit the number of results returned.
160
+ * 'analyzer' -- to use the specified analyzer as a search analyzer for this query.
161
+
162
+ The \`options\` map and any of the keys are optional.
163
+ An example of the \`options\` map: \`{skip: 30, limit: 10, analyzer: 'whitespace'}\`
164
+
165
+
166
+ **Parameters**
167
+ - \`indexName\` - The name of the full-text index.
168
+ - \`queryString\` - The string to find approximate matches for.
169
+ - \`options\` - {skip :: INTEGER, limit :: INTEGER, analyzer :: STRING}
170
+
171
+ **Returns**
172
+ - \`node\` - A node which contains a property similar to the query string.
173
+ - \`score\` - The score measuring how similar the node property is to the query string.`,
174
+ );
175
+ });
176
+
177
+ /* Some parameters are described with a map type rather than with prose.
178
+ Markdown would collapse the layout of those into one run-on line. */
179
+ test('writes a parameter that carries its own layout as a code block', () => {
180
+ expect(
181
+ hoverMarkdown(
182
+ 'CALL apoc.export.csv.all("file.csv", {})',
183
+ 'apoc.export.csv.all',
184
+ ),
185
+ ).toContain(
186
+ `**Parameters**
187
+ - \`file\` - The name of the file to which the data will be exported.
188
+ - \`config\` -
189
+ \`\`\`text
190
+ {
191
+ stream = false :: BOOLEAN,
192
+ batchSize = 20000 :: INTEGER,
193
+ bulkImport = false :: BOOLEAN,
194
+ timeoutSeconds = 100 :: INTEGER,
195
+ compression = 'None' :: STRING,
196
+ charset = 'UTF_8' :: STRING,
197
+ quotes = 'always' :: ['always', 'none', 'ifNeeded'],
198
+ differentiateNulls = false :: BOOLEAN,
199
+ sampling = false :: BOOLEAN,
200
+ samplingConfig :: MAP
201
+ }
202
+ \`\`\``,
203
+ );
204
+ });
205
+ });
206
+
207
+ describe('Variable hover markdown', () => {
208
+ test('writes the type of a variable without labels', () => {
209
+ expect(variableHoverMarkdown('MATCH (n) RETURN n', 'MATCH ('.length)).toBe(
210
+ `\`
211
+ n: Node
212
+ \``,
213
+ );
214
+ });
215
+
216
+ test('writes the label expression of a labeled variable', () => {
217
+ expect(
218
+ variableHoverMarkdown(
219
+ 'MATCH (n:Person|Pet) WHERE n:Neighbour RETURN ',
220
+ 'MATCH (n:Person|Pet) WHERE '.length,
221
+ ),
222
+ ).toBe(
223
+ `\`
224
+ n: Node
225
+ \`
226
+
227
+ \`\`\`cypher
228
+ ((Person | Pet) & Neighbour)
229
+ \`\`\``,
230
+ );
231
+ });
232
+ });
package/src/types.ts CHANGED
@@ -1,10 +1,30 @@
1
+ import type { DbSchema } from '@neo4j-cypher/language-support';
2
+
1
3
  // These settings are defined in the package.json
4
+ export type Neo4jConnectionSettings = {
5
+ connect?: boolean;
6
+ user?: string;
7
+ password?: string;
8
+ connectURL?: string;
9
+ database?: string;
10
+ };
11
+
12
+ export type LintWorkerSettings = {
13
+ lintWorkerPath: string;
14
+ linterVersion: string;
15
+ };
16
+
17
+ export type SymbolFetchingParams = {
18
+ query: string;
19
+ uri: string;
20
+ schema: DbSchema;
21
+ };
22
+
2
23
  export type Neo4jSettings = {
3
24
  trace: {
4
25
  server: 'off' | 'messages' | 'verbose';
5
26
  };
6
- connect?: boolean;
7
- password?: string;
8
- URL?: string;
9
- user?: string;
27
+ features: { linting: boolean };
10
28
  };
29
+
30
+ export type Neo4jParameters = Record<string, unknown>;
@@ -1,14 +0,0 @@
1
- import { validateSemantics } from '@neo4j-cypher/language-support';
2
- import workerpool from 'workerpool';
3
-
4
- workerpool.worker({ validateSemantics });
5
-
6
- type LinterArgs = Parameters<typeof validateSemantics>;
7
-
8
- export type LinterTask = workerpool.Promise<
9
- ReturnType<typeof validateSemantics>
10
- >;
11
-
12
- export type LintWorker = {
13
- validateSemantics: (...args: LinterArgs) => LinterTask;
14
- };