@neo4j-cypher/react-codemirror 2.0.0-next.1 → 2.0.0-next.10
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 +79 -0
- package/README.md +4 -1
- package/dist/{types/CypherEditor.d.ts → CypherEditor.d.ts} +69 -8
- package/dist/CypherEditor.js +300 -0
- package/dist/CypherEditor.js.map +1 -0
- package/dist/CypherEditor.test.js +151 -0
- package/dist/CypherEditor.test.js.map +1 -0
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +2 -0
- package/dist/constants.js.map +1 -0
- package/dist/e2e_tests/autoCompletion.spec.js +267 -0
- package/dist/e2e_tests/autoCompletion.spec.js.map +1 -0
- package/dist/e2e_tests/configuration.spec.js +83 -0
- package/dist/e2e_tests/configuration.spec.js.map +1 -0
- package/dist/e2e_tests/debounce.spec.js +65 -0
- package/dist/e2e_tests/debounce.spec.js.map +1 -0
- package/dist/e2e_tests/e2eUtils.js +60 -0
- package/dist/e2e_tests/e2eUtils.js.map +1 -0
- package/dist/e2e_tests/extraKeybindings.spec.js +43 -0
- package/dist/e2e_tests/extraKeybindings.spec.js.map +1 -0
- package/dist/e2e_tests/historyNavigation.spec.js +227 -0
- package/dist/e2e_tests/historyNavigation.spec.js.map +1 -0
- package/dist/e2e_tests/performanceTest.spec.d.ts +6 -0
- package/dist/e2e_tests/performanceTest.spec.js +96 -0
- package/dist/e2e_tests/performanceTest.spec.js.map +1 -0
- package/dist/e2e_tests/sanityChecks.spec.js +53 -0
- package/dist/e2e_tests/sanityChecks.spec.js.map +1 -0
- package/dist/e2e_tests/signatureHelp.spec.js +179 -0
- package/dist/e2e_tests/signatureHelp.spec.js.map +1 -0
- package/dist/e2e_tests/snippets.spec.js +62 -0
- package/dist/e2e_tests/snippets.spec.js.map +1 -0
- package/dist/e2e_tests/syntaxHighlighting.spec.d.ts +1 -0
- package/dist/e2e_tests/syntaxHighlighting.spec.js +90 -0
- package/dist/e2e_tests/syntaxHighlighting.spec.js.map +1 -0
- package/dist/e2e_tests/syntaxValidation.spec.d.ts +1 -0
- package/dist/e2e_tests/syntaxValidation.spec.js +79 -0
- package/dist/e2e_tests/syntaxValidation.spec.js.map +1 -0
- package/dist/historyNavigation.d.ts +7 -0
- package/dist/historyNavigation.js +163 -0
- package/dist/historyNavigation.js.map +1 -0
- package/dist/{types/icons.d.ts → icons.d.ts} +1 -1
- package/dist/icons.js +62 -0
- package/dist/icons.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/lang-cypher/autocomplete.d.ts +6 -0
- package/dist/lang-cypher/autocomplete.js +124 -0
- package/dist/lang-cypher/autocomplete.js.map +1 -0
- package/dist/{types/lang-cypher → lang-cypher}/constants.d.ts +9 -0
- package/dist/lang-cypher/constants.js +65 -0
- package/dist/lang-cypher/constants.js.map +1 -0
- package/dist/lang-cypher/contants.test.d.ts +1 -0
- package/dist/lang-cypher/contants.test.js +103 -0
- package/dist/lang-cypher/contants.test.js.map +1 -0
- package/dist/lang-cypher/createCypherTheme.js +179 -0
- package/dist/lang-cypher/createCypherTheme.js.map +1 -0
- package/dist/lang-cypher/langCypher.d.ts +14 -0
- package/dist/lang-cypher/langCypher.js +30 -0
- package/dist/lang-cypher/langCypher.js.map +1 -0
- package/dist/lang-cypher/lintWorker.d.ts +8 -0
- package/dist/lang-cypher/lintWorker.js +4 -0
- package/dist/lang-cypher/lintWorker.js.map +1 -0
- package/dist/lang-cypher/parser-adapter.d.ts +19 -0
- package/dist/lang-cypher/parser-adapter.js +113 -0
- package/dist/lang-cypher/parser-adapter.js.map +1 -0
- package/dist/lang-cypher/signatureHelp.d.ts +4 -0
- package/dist/lang-cypher/signatureHelp.js +94 -0
- package/dist/lang-cypher/signatureHelp.js.map +1 -0
- package/dist/lang-cypher/syntaxValidation.d.ts +5 -0
- package/dist/lang-cypher/syntaxValidation.js +69 -0
- package/dist/lang-cypher/syntaxValidation.js.map +1 -0
- package/dist/lang-cypher/themeIcons.js +22 -0
- package/dist/lang-cypher/themeIcons.js.map +1 -0
- package/dist/lang-cypher/utils.d.ts +2 -0
- package/dist/lang-cypher/utils.js +10 -0
- package/dist/lang-cypher/utils.js.map +1 -0
- package/dist/ndlTokensCopy.js +380 -0
- package/dist/ndlTokensCopy.js.map +1 -0
- package/dist/ndlTokensCopy.test.d.ts +1 -0
- package/dist/ndlTokensCopy.test.js +12 -0
- package/dist/ndlTokensCopy.test.js.map +1 -0
- package/dist/neo4jSetup.js +120 -0
- package/dist/neo4jSetup.js.map +1 -0
- package/dist/{types/themes.d.ts → themes.d.ts} +1 -1
- package/dist/themes.js +114 -0
- package/dist/themes.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +30 -31
- package/src/CypherEditor.test.tsx +200 -0
- package/src/CypherEditor.tsx +292 -41
- package/src/constants.ts +1 -0
- package/src/e2e_tests/autoCompletion.spec.tsx +472 -0
- package/src/e2e_tests/configuration.spec.tsx +111 -0
- package/src/e2e_tests/debounce.spec.tsx +101 -0
- package/src/e2e_tests/{e2e-utils.ts → e2eUtils.ts} +11 -1
- package/src/e2e_tests/{extra-keybindings.spec.tsx → extraKeybindings.spec.tsx} +1 -3
- package/src/e2e_tests/historyNavigation.spec.tsx +315 -0
- package/src/e2e_tests/performanceTest.spec.tsx +158 -0
- package/src/e2e_tests/{sanity-checks.spec.tsx → sanityChecks.spec.tsx} +7 -22
- package/src/e2e_tests/signatureHelp.spec.tsx +380 -0
- package/src/e2e_tests/snippets.spec.tsx +92 -0
- package/src/e2e_tests/{syntax-highlighting.spec.tsx → syntaxHighlighting.spec.tsx} +1 -3
- package/src/e2e_tests/{syntax-validation.spec.tsx → syntaxValidation.spec.tsx} +6 -7
- package/src/{repl-mode.ts → historyNavigation.ts} +7 -30
- package/src/icons.ts +3 -0
- package/src/index.ts +5 -2
- package/src/lang-cypher/autocomplete.ts +107 -15
- package/src/lang-cypher/constants.ts +23 -0
- package/src/lang-cypher/contants.test.ts +6 -2
- package/src/lang-cypher/{create-cypher-theme.ts → createCypherTheme.ts} +41 -2
- package/src/lang-cypher/langCypher.ts +53 -0
- package/src/lang-cypher/lintWorker.ts +14 -0
- package/src/lang-cypher/parser-adapter.ts +145 -0
- package/src/lang-cypher/signatureHelp.ts +133 -0
- package/src/lang-cypher/syntaxValidation.ts +96 -0
- package/src/lang-cypher/utils.ts +9 -0
- package/src/{ndl-tokens-copy.test.ts → ndlTokensCopy.test.ts} +2 -1
- package/src/{neo4j-setup.tsx → neo4jSetup.tsx} +51 -1
- package/src/themes.ts +4 -2
- package/src/viteEnv.d.ts +1 -0
- package/dist/cjs/index.cjs +0 -1443
- package/dist/cjs/index.cjs.map +0 -7
- package/dist/esm/index.mjs +0 -1466
- package/dist/esm/index.mjs.map +0 -7
- package/dist/types/e2e_tests/mock-data.d.ts +0 -3779
- package/dist/types/index.d.ts +0 -4
- package/dist/types/lang-cypher/ParserAdapter.d.ts +0 -14
- package/dist/types/lang-cypher/autocomplete.d.ts +0 -3
- package/dist/types/lang-cypher/lang-cypher.d.ts +0 -7
- package/dist/types/lang-cypher/syntax-validation.d.ts +0 -3
- package/dist/types/repl-mode.d.ts +0 -8
- package/dist/types/tsconfig.tsbuildinfo +0 -1
- package/src/e2e_tests/auto-completion.spec.tsx +0 -232
- package/src/e2e_tests/history-navigation.spec.tsx +0 -144
- package/src/e2e_tests/mock-data.ts +0 -4310
- package/src/e2e_tests/performance-test.spec.tsx +0 -71
- package/src/lang-cypher/ParserAdapter.ts +0 -92
- package/src/lang-cypher/lang-cypher.ts +0 -32
- package/src/lang-cypher/syntax-validation.ts +0 -24
- /package/dist/{types/e2e_tests/auto-completion.spec.d.ts → CypherEditor.test.d.ts} +0 -0
- /package/dist/{types/e2e_tests/extra-keybindings.spec.d.ts → e2e_tests/autoCompletion.spec.d.ts} +0 -0
- /package/dist/{types/e2e_tests/history-navigation.spec.d.ts → e2e_tests/configuration.spec.d.ts} +0 -0
- /package/dist/{types/e2e_tests/performance-test.spec.d.ts → e2e_tests/debounce.spec.d.ts} +0 -0
- /package/dist/{types/e2e_tests/e2e-utils.d.ts → e2e_tests/e2eUtils.d.ts} +0 -0
- /package/dist/{types/e2e_tests/sanity-checks.spec.d.ts → e2e_tests/extraKeybindings.spec.d.ts} +0 -0
- /package/dist/{types/e2e_tests/syntax-highlighting.spec.d.ts → e2e_tests/historyNavigation.spec.d.ts} +0 -0
- /package/dist/{types/e2e_tests/syntax-validation.spec.d.ts → e2e_tests/sanityChecks.spec.d.ts} +0 -0
- /package/dist/{types/lang-cypher/contants.test.d.ts → e2e_tests/signatureHelp.spec.d.ts} +0 -0
- /package/dist/{types/ndl-tokens-copy.test.d.ts → e2e_tests/snippets.spec.d.ts} +0 -0
- /package/dist/{types/lang-cypher/create-cypher-theme.d.ts → lang-cypher/createCypherTheme.d.ts} +0 -0
- /package/dist/{types/lang-cypher/theme-icons.d.ts → lang-cypher/themeIcons.d.ts} +0 -0
- /package/dist/{types/ndl-tokens-copy.d.ts → ndlTokensCopy.d.ts} +0 -0
- /package/dist/{types/neo4j-setup.d.ts → neo4jSetup.d.ts} +0 -0
- /package/src/lang-cypher/{theme-icons.ts → themeIcons.ts} +0 -0
- /package/src/{ndl-tokens-copy.ts → ndlTokensCopy.ts} +0 -0
|
@@ -1,8 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
Completion,
|
|
3
|
+
CompletionSource,
|
|
4
|
+
snippet,
|
|
5
|
+
} from '@codemirror/autocomplete';
|
|
2
6
|
import { autocomplete } from '@neo4j-cypher/language-support';
|
|
3
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
CompletionItemKind,
|
|
9
|
+
CompletionItemTag,
|
|
10
|
+
} from 'vscode-languageserver-types';
|
|
4
11
|
import { CompletionItemIcons } from '../icons';
|
|
5
|
-
import type { CypherConfig } from './
|
|
12
|
+
import type { CypherConfig } from './langCypher';
|
|
13
|
+
import { getDocString } from './utils';
|
|
6
14
|
|
|
7
15
|
const completionKindToCodemirrorIcon = (c: CompletionItemKind) => {
|
|
8
16
|
const map: Record<CompletionItemKind, CompletionItemIcons> = {
|
|
@@ -28,7 +36,8 @@ const completionKindToCodemirrorIcon = (c: CompletionItemKind) => {
|
|
|
28
36
|
[CompletionItemKind.EnumMember]: 'EnumMember',
|
|
29
37
|
[CompletionItemKind.Constant]: 'Constant',
|
|
30
38
|
[CompletionItemKind.Struct]: 'Struct',
|
|
31
|
-
|
|
39
|
+
// we're miss-using the enum here as there is no `Console` kind in the predefined list
|
|
40
|
+
[CompletionItemKind.Event]: 'Console',
|
|
32
41
|
[CompletionItemKind.Operator]: 'Operator',
|
|
33
42
|
[CompletionItemKind.TypeParameter]: 'TypeParameter',
|
|
34
43
|
};
|
|
@@ -36,12 +45,22 @@ const completionKindToCodemirrorIcon = (c: CompletionItemKind) => {
|
|
|
36
45
|
return map[c];
|
|
37
46
|
};
|
|
38
47
|
|
|
48
|
+
export const completionStyles: (
|
|
49
|
+
completion: Completion & { deprecated?: boolean },
|
|
50
|
+
) => string = (completion) => {
|
|
51
|
+
if (completion.deprecated) {
|
|
52
|
+
return 'cm-deprecated-completion';
|
|
53
|
+
} else {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
39
58
|
export const cypherAutocomplete: (config: CypherConfig) => CompletionSource =
|
|
40
59
|
(config) => (context) => {
|
|
41
|
-
const
|
|
60
|
+
const documentText = context.state.doc.toString();
|
|
42
61
|
|
|
43
|
-
const triggerCharacters = ['.', ':', '{', '$'];
|
|
44
|
-
const lastCharacter =
|
|
62
|
+
const triggerCharacters = ['.', ':', '{', '$', ')'];
|
|
63
|
+
const lastCharacter = documentText.at(context.pos - 1);
|
|
45
64
|
|
|
46
65
|
const lastWord = context.matchBefore(/\w*/);
|
|
47
66
|
const inWord = lastWord.from !== lastWord.to;
|
|
@@ -49,17 +68,90 @@ export const cypherAutocomplete: (config: CypherConfig) => CompletionSource =
|
|
|
49
68
|
const shouldTriggerCompletion =
|
|
50
69
|
inWord || context.explicit || triggerCharacters.includes(lastCharacter);
|
|
51
70
|
|
|
71
|
+
if (config.useLightVersion && !context.explicit) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
52
75
|
if (!shouldTriggerCompletion) {
|
|
53
76
|
return null;
|
|
54
77
|
}
|
|
55
78
|
|
|
56
|
-
const options = autocomplete(
|
|
79
|
+
const options = autocomplete(
|
|
80
|
+
documentText,
|
|
81
|
+
config.schema ?? {},
|
|
82
|
+
context.pos,
|
|
83
|
+
context.explicit,
|
|
84
|
+
);
|
|
57
85
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
86
|
+
if (config.featureFlags?.signatureInfoOnAutoCompletions) {
|
|
87
|
+
return {
|
|
88
|
+
from: context.matchBefore(/(\w|\$)*$/).from,
|
|
89
|
+
options: options.map((o) => {
|
|
90
|
+
let maybeInfo = {};
|
|
91
|
+
let emptyInfo = true;
|
|
92
|
+
const newDiv = document.createElement('div');
|
|
93
|
+
|
|
94
|
+
if (o.signature) {
|
|
95
|
+
const header = document.createElement('p');
|
|
96
|
+
header.setAttribute('class', 'cm-completionInfo-signature');
|
|
97
|
+
header.textContent = o.signature;
|
|
98
|
+
if (header.textContent.length > 0) {
|
|
99
|
+
emptyInfo = false;
|
|
100
|
+
newDiv.appendChild(header);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (o.documentation) {
|
|
105
|
+
const paragraph = document.createElement('p');
|
|
106
|
+
paragraph.textContent = getDocString(o.documentation);
|
|
107
|
+
if (paragraph.textContent.length > 0) {
|
|
108
|
+
emptyInfo = false;
|
|
109
|
+
newDiv.appendChild(paragraph);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!emptyInfo) {
|
|
114
|
+
maybeInfo = {
|
|
115
|
+
info: () => Promise.resolve(newDiv),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
const deprecated =
|
|
119
|
+
o.tags?.find((tag) => tag === CompletionItemTag.Deprecated) ??
|
|
120
|
+
false;
|
|
121
|
+
// The negative boost moves the deprecation down the list
|
|
122
|
+
// so we offer the user the completions that are
|
|
123
|
+
// deprecated the last
|
|
124
|
+
const maybeDeprecated = deprecated
|
|
125
|
+
? { boost: -99, deprecated: true }
|
|
126
|
+
: {};
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
label: o.label,
|
|
130
|
+
type: completionKindToCodemirrorIcon(o.kind),
|
|
131
|
+
apply:
|
|
132
|
+
o.kind === CompletionItemKind.Snippet
|
|
133
|
+
? // codemirror requires an empty snippet space to be able to tab out of the completion
|
|
134
|
+
snippet((o.insertText ?? o.label) + '${}')
|
|
135
|
+
: undefined,
|
|
136
|
+
detail: o.detail,
|
|
137
|
+
...maybeDeprecated,
|
|
138
|
+
...maybeInfo,
|
|
139
|
+
};
|
|
140
|
+
}),
|
|
141
|
+
};
|
|
142
|
+
} else {
|
|
143
|
+
return {
|
|
144
|
+
from: context.matchBefore(/(\w|\$)*$/).from,
|
|
145
|
+
options: options.map((o) => ({
|
|
146
|
+
label: o.label,
|
|
147
|
+
type: completionKindToCodemirrorIcon(o.kind),
|
|
148
|
+
apply:
|
|
149
|
+
o.kind === CompletionItemKind.Snippet
|
|
150
|
+
? // codemirror requires an empty snippet space to be able to tab out of the completion
|
|
151
|
+
snippet((o.insertText ?? o.label) + '${}')
|
|
152
|
+
: undefined,
|
|
153
|
+
detail: o.detail,
|
|
154
|
+
})),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
65
157
|
};
|
|
@@ -31,8 +31,30 @@ export const cypherTokenTypeToNode = (facet: Facet<unknown>) => ({
|
|
|
31
31
|
none: NodeType.define({ id: 19, name: 'none' }),
|
|
32
32
|
separator: NodeType.define({ id: 20, name: 'separator' }),
|
|
33
33
|
punctuation: NodeType.define({ id: 21, name: 'punctuation' }),
|
|
34
|
+
consoleCommand: NodeType.define({ id: 22, name: 'consoleCommand' }),
|
|
35
|
+
// also include prism token types
|
|
36
|
+
'class-name': NodeType.define({ id: 23, name: 'label' }),
|
|
37
|
+
// this is escaped variables
|
|
38
|
+
identifier: NodeType.define({ id: 24, name: 'variable' }),
|
|
39
|
+
string: NodeType.define({ id: 25, name: 'stringLiteral' }),
|
|
40
|
+
relationship: NodeType.define({ id: 26, name: 'label' }),
|
|
41
|
+
boolean: NodeType.define({ id: 27, name: 'booleanLiteral' }),
|
|
42
|
+
number: NodeType.define({ id: 28, name: 'numberLiteral' }),
|
|
34
43
|
});
|
|
35
44
|
|
|
45
|
+
export type PrismSpecificTokenType =
|
|
46
|
+
| 'class-name'
|
|
47
|
+
| 'identifier'
|
|
48
|
+
| 'string'
|
|
49
|
+
| 'relationship'
|
|
50
|
+
| 'boolean'
|
|
51
|
+
| 'number';
|
|
52
|
+
|
|
53
|
+
export type CodemirrorParseTokenType =
|
|
54
|
+
| CypherTokenType
|
|
55
|
+
| PrismSpecificTokenType
|
|
56
|
+
| 'topNode';
|
|
57
|
+
|
|
36
58
|
export type HighlightedCypherTokenTypes = Exclude<CypherTokenType, 'none'>;
|
|
37
59
|
export const tokenTypeToStyleTag: Record<HighlightedCypherTokenTypes, Tag> = {
|
|
38
60
|
comment: tags.comment,
|
|
@@ -55,6 +77,7 @@ export const tokenTypeToStyleTag: Record<HighlightedCypherTokenTypes, Tag> = {
|
|
|
55
77
|
bracket: tags.bracket,
|
|
56
78
|
punctuation: tags.punctuation,
|
|
57
79
|
separator: tags.separator,
|
|
80
|
+
consoleCommand: tags.macroName,
|
|
58
81
|
};
|
|
59
82
|
|
|
60
83
|
export const parserAdapterNodeSet = (nodes: Record<string, NodeType>) =>
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { tags } from '@lezer/highlight';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
applySyntaxColouring,
|
|
4
|
+
CypherTokenType,
|
|
5
|
+
} from '@neo4j-cypher/language-support';
|
|
6
|
+
import { expect, test } from 'vitest';
|
|
3
7
|
import { tokenTypeToStyleTag } from './constants';
|
|
4
8
|
|
|
5
9
|
const cypherQueryWithAllTokenTypes = `MATCH (variable :Label)-[:REL_TYPE]->()
|
|
@@ -54,7 +58,7 @@ test('correctly parses all cypher token types to style tags', () => {
|
|
|
54
58
|
]);
|
|
55
59
|
|
|
56
60
|
const styleTags = tokenTypes.map((tokenType) => {
|
|
57
|
-
if (tokenType ===
|
|
61
|
+
if (tokenType === CypherTokenType.none) return undefined;
|
|
58
62
|
return tokenTypeToStyleTag[tokenType];
|
|
59
63
|
});
|
|
60
64
|
const correctTags = [
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
} from '@codemirror/language';
|
|
6
6
|
import { Extension } from '@codemirror/state';
|
|
7
7
|
import { EditorView } from '@codemirror/view';
|
|
8
|
+
import { CypherTokenType } from '@neo4j-cypher/language-support';
|
|
8
9
|
import { StyleSpec } from 'style-mod';
|
|
9
10
|
import { HighlightedCypherTokenTypes, tokenTypeToStyleTag } from './constants';
|
|
10
11
|
import {
|
|
@@ -15,7 +16,7 @@ import {
|
|
|
15
16
|
replaceAllSvg,
|
|
16
17
|
replaceSvg,
|
|
17
18
|
upArrowSvg,
|
|
18
|
-
} from './
|
|
19
|
+
} from './themeIcons';
|
|
19
20
|
|
|
20
21
|
export interface ThemeOptions {
|
|
21
22
|
dark: boolean;
|
|
@@ -53,6 +54,9 @@ export const createCypherTheme = ({
|
|
|
53
54
|
color: settings.foreground,
|
|
54
55
|
fontVariantLigatures: 'none',
|
|
55
56
|
},
|
|
57
|
+
'& .cm-snippetField': {
|
|
58
|
+
backgroundColor: settings.autoCompletionPanel.selectedColor,
|
|
59
|
+
},
|
|
56
60
|
'&.cm-focused': {
|
|
57
61
|
outline: 'none',
|
|
58
62
|
},
|
|
@@ -61,8 +65,9 @@ export const createCypherTheme = ({
|
|
|
61
65
|
color: settings.gutterForeground,
|
|
62
66
|
border: 'none',
|
|
63
67
|
},
|
|
64
|
-
'&.cm-editor
|
|
68
|
+
'&.cm-editor': {
|
|
65
69
|
fontFamily: 'Fira Code, Menlo, Monaco, Lucida Console, monospace',
|
|
70
|
+
height: '100%',
|
|
66
71
|
},
|
|
67
72
|
'.cm-content': {
|
|
68
73
|
caretColor: settings.cursor,
|
|
@@ -80,6 +85,9 @@ export const createCypherTheme = ({
|
|
|
80
85
|
'& .cm-selectionMatch': {
|
|
81
86
|
backgroundColor: settings.textMatchingSelection,
|
|
82
87
|
},
|
|
88
|
+
'& .cm-bold': {
|
|
89
|
+
fontWeight: 'bold',
|
|
90
|
+
},
|
|
83
91
|
'& .cm-panels': {
|
|
84
92
|
backgroundColor: settings.searchPanel.background,
|
|
85
93
|
fontFamily: 'Fira Code, Menlo, Monaco, Lucida Console, monospace',
|
|
@@ -93,7 +101,37 @@ export const createCypherTheme = ({
|
|
|
93
101
|
color: settings.autoCompletionPanel.matchingTextColor,
|
|
94
102
|
textDecoration: 'none',
|
|
95
103
|
},
|
|
104
|
+
'& .cm-signature-help-panel': {
|
|
105
|
+
backgroundColor: settings.autoCompletionPanel.backgroundColor,
|
|
106
|
+
maxWidth: '700px',
|
|
107
|
+
maxHeight: '250px',
|
|
108
|
+
fontFamily: 'Fira Code, Menlo, Monaco, Lucida Console, monospace',
|
|
109
|
+
},
|
|
110
|
+
'& .cm-signature-help-panel-contents': {
|
|
111
|
+
overflow: 'auto',
|
|
112
|
+
maxHeight: '250px',
|
|
113
|
+
},
|
|
114
|
+
'& .cm-signature-help-panel-current-argument': {
|
|
115
|
+
color: settings.autoCompletionPanel.matchingTextColor,
|
|
116
|
+
fontWeight: 'bold',
|
|
117
|
+
},
|
|
118
|
+
'& .cm-signature-help-panel-separator': {
|
|
119
|
+
borderBottom: '1px solid #ccc',
|
|
120
|
+
},
|
|
121
|
+
'& .cm-signature-help-panel-name': {
|
|
122
|
+
padding: '5px',
|
|
123
|
+
},
|
|
124
|
+
'& .cm-signature-help-panel-description': {
|
|
125
|
+
padding: '5px',
|
|
126
|
+
},
|
|
127
|
+
'.cm-completionInfo-signature': {
|
|
128
|
+
color: 'darkgrey',
|
|
129
|
+
},
|
|
130
|
+
'.cm-deprecated-completion': {
|
|
131
|
+
'text-decoration': 'line-through',
|
|
132
|
+
},
|
|
96
133
|
'.cm-tooltip-autocomplete': {
|
|
134
|
+
maxWidth: '430px',
|
|
97
135
|
'& > ul > li[aria-selected]': {
|
|
98
136
|
backgroundColor: settings.autoCompletionPanel.selectedColor,
|
|
99
137
|
color: settings.foreground,
|
|
@@ -198,6 +236,7 @@ export const createCypherTheme = ({
|
|
|
198
236
|
([token, color]: [HighlightedCypherTokenTypes, string]): TagStyle => ({
|
|
199
237
|
tag: tokenTypeToStyleTag[token],
|
|
200
238
|
color,
|
|
239
|
+
class: token === CypherTokenType.consoleCommand ? 'cm-bold' : undefined,
|
|
201
240
|
}),
|
|
202
241
|
);
|
|
203
242
|
const highlightStyle = HighlightStyle.define(styles);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { autocompletion } from '@codemirror/autocomplete';
|
|
2
|
+
import {
|
|
3
|
+
defineLanguageFacet,
|
|
4
|
+
Language,
|
|
5
|
+
LanguageSupport,
|
|
6
|
+
} from '@codemirror/language';
|
|
7
|
+
import {
|
|
8
|
+
_internalFeatureFlags,
|
|
9
|
+
type DbSchema,
|
|
10
|
+
} from '@neo4j-cypher/language-support';
|
|
11
|
+
import { completionStyles, cypherAutocomplete } from './autocomplete';
|
|
12
|
+
import { ParserAdapter } from './parser-adapter';
|
|
13
|
+
import { signatureHelpTooltip } from './signatureHelp';
|
|
14
|
+
import { cypherLinter, semanticAnalysisLinter } from './syntaxValidation';
|
|
15
|
+
|
|
16
|
+
const facet = defineLanguageFacet({
|
|
17
|
+
commentTokens: { block: { open: '/*', close: '*/' }, line: '//' },
|
|
18
|
+
closeBrackets: { brackets: ['(', '[', '{', "'", '"', '`'] },
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export type CypherConfig = {
|
|
22
|
+
lint?: boolean;
|
|
23
|
+
showSignatureTooltipBelow?: boolean;
|
|
24
|
+
featureFlags?: {
|
|
25
|
+
signatureInfoOnAutoCompletions?: boolean;
|
|
26
|
+
consoleCommands?: boolean;
|
|
27
|
+
};
|
|
28
|
+
schema?: DbSchema;
|
|
29
|
+
useLightVersion: boolean;
|
|
30
|
+
setUseLightVersion?: (useLightVersion: boolean) => void;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export function cypher(config: CypherConfig) {
|
|
34
|
+
const featureFlags = config.featureFlags;
|
|
35
|
+
// We allow to override the consoleCommands feature flag
|
|
36
|
+
if (featureFlags.consoleCommands !== undefined) {
|
|
37
|
+
_internalFeatureFlags.consoleCommands = featureFlags.consoleCommands;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const parserAdapter = new ParserAdapter(facet, config);
|
|
41
|
+
|
|
42
|
+
const cypherLanguage = new Language(facet, parserAdapter, [], 'cypher');
|
|
43
|
+
|
|
44
|
+
return new LanguageSupport(cypherLanguage, [
|
|
45
|
+
autocompletion({
|
|
46
|
+
override: [cypherAutocomplete(config)],
|
|
47
|
+
optionClass: completionStyles,
|
|
48
|
+
}),
|
|
49
|
+
cypherLinter(config),
|
|
50
|
+
semanticAnalysisLinter(config),
|
|
51
|
+
signatureHelpTooltip(config),
|
|
52
|
+
]);
|
|
53
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { Facet } from '@codemirror/state';
|
|
2
|
+
import { Input, NodeType, Parser, PartialParse, Tree } from '@lezer/common';
|
|
3
|
+
import {
|
|
4
|
+
applySyntaxColouring,
|
|
5
|
+
ParsedCypherToken,
|
|
6
|
+
} from '@neo4j-cypher/language-support';
|
|
7
|
+
|
|
8
|
+
import Prism from 'prismjs';
|
|
9
|
+
import {
|
|
10
|
+
CodemirrorParseTokenType,
|
|
11
|
+
cypherTokenTypeToNode,
|
|
12
|
+
parserAdapterNodeSet,
|
|
13
|
+
} from './constants';
|
|
14
|
+
// This import will load the cypher support in prisma
|
|
15
|
+
import 'prismjs/components/prism-cypher';
|
|
16
|
+
import { CypherConfig } from './langCypher';
|
|
17
|
+
|
|
18
|
+
const DEFAULT_NODE_GROUP_SIZE = 4;
|
|
19
|
+
Prism.manual = true;
|
|
20
|
+
|
|
21
|
+
export class ParserAdapter extends Parser {
|
|
22
|
+
cypherTokenTypeToNode: Record<CodemirrorParseTokenType, NodeType>;
|
|
23
|
+
|
|
24
|
+
constructor(facet: Facet<unknown>, private config: CypherConfig) {
|
|
25
|
+
super();
|
|
26
|
+
this.cypherTokenTypeToNode = cypherTokenTypeToNode(facet);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
createParse(input: Input): PartialParse {
|
|
30
|
+
return this.startParse(input);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* There are more arguments, but since we don't do any incremental parsing, they are not useful */
|
|
34
|
+
startParse(input: string | Input): PartialParse {
|
|
35
|
+
const document =
|
|
36
|
+
typeof input === 'string' ? input : input.read(0, input.length);
|
|
37
|
+
|
|
38
|
+
const tree = this.buildTree(document);
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
stoppedAt: input.length,
|
|
42
|
+
parsedPos: input.length,
|
|
43
|
+
stopAt: () => {
|
|
44
|
+
return undefined;
|
|
45
|
+
},
|
|
46
|
+
advance: () => tree,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private buildTree(document: string) {
|
|
51
|
+
const parse = this.config.useLightVersion
|
|
52
|
+
? this.prismParse(document)
|
|
53
|
+
: this.antlrParse(document);
|
|
54
|
+
|
|
55
|
+
if (parse.tokens.length === 0) {
|
|
56
|
+
return this.createEmptyTree(document);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const buffer =
|
|
60
|
+
parse.type === 'prism'
|
|
61
|
+
? this.createBufferForPrismTokens(parse.tokens)
|
|
62
|
+
: this.createBufferForAntlrTokens(parse.tokens);
|
|
63
|
+
|
|
64
|
+
this.addTopNodeToBuffer(buffer, document);
|
|
65
|
+
|
|
66
|
+
return Tree.build({
|
|
67
|
+
buffer: buffer.flat(),
|
|
68
|
+
nodeSet: parserAdapterNodeSet(this.cypherTokenTypeToNode),
|
|
69
|
+
topID: this.cypherTokenTypeToNode.topNode.id,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private antlrParse(document: string) {
|
|
74
|
+
const startTime = performance.now();
|
|
75
|
+
const tokens = applySyntaxColouring(document);
|
|
76
|
+
const timeTaken = performance.now() - startTime;
|
|
77
|
+
if (timeTaken > 300) {
|
|
78
|
+
this.config.setUseLightVersion?.(true);
|
|
79
|
+
}
|
|
80
|
+
return { type: 'antlr' as const, tokens };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private prismParse(document: string) {
|
|
84
|
+
if (document.length === 0) {
|
|
85
|
+
this.config.setUseLightVersion?.(false);
|
|
86
|
+
}
|
|
87
|
+
const tokens = Prism.tokenize(document, Prism.languages.cypher);
|
|
88
|
+
return {
|
|
89
|
+
type: 'prism' as const,
|
|
90
|
+
tokens,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
private createBufferForAntlrTokens(tokens: ParsedCypherToken[]) {
|
|
94
|
+
return tokens.map((token) => {
|
|
95
|
+
const nodeTypeId = this.cypherTokenTypeToNode[token.tokenType].id;
|
|
96
|
+
const startOffset = token.position.startOffset;
|
|
97
|
+
const endOffset = token.position.startOffset + token.length;
|
|
98
|
+
|
|
99
|
+
return [nodeTypeId, startOffset, endOffset, DEFAULT_NODE_GROUP_SIZE];
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private createBufferForPrismTokens(tokens: (string | Prism.Token)[]) {
|
|
104
|
+
let totalOffset = 0;
|
|
105
|
+
return tokens.map((token) => {
|
|
106
|
+
const tokenType = (
|
|
107
|
+
typeof token === 'string' ? 'variable' : token.type
|
|
108
|
+
) as CodemirrorParseTokenType;
|
|
109
|
+
|
|
110
|
+
const nodeTypeId = this.cypherTokenTypeToNode[tokenType].id;
|
|
111
|
+
const startOffset = totalOffset;
|
|
112
|
+
const endOffset = startOffset + token.length;
|
|
113
|
+
totalOffset = endOffset;
|
|
114
|
+
|
|
115
|
+
return [nodeTypeId, startOffset, endOffset, DEFAULT_NODE_GROUP_SIZE];
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private createEmptyTree(document: string) {
|
|
120
|
+
return Tree.build({
|
|
121
|
+
buffer: [
|
|
122
|
+
this.cypherTokenTypeToNode.topNode.id,
|
|
123
|
+
0,
|
|
124
|
+
document.length,
|
|
125
|
+
DEFAULT_NODE_GROUP_SIZE,
|
|
126
|
+
],
|
|
127
|
+
nodeSet: parserAdapterNodeSet(this.cypherTokenTypeToNode),
|
|
128
|
+
topID: this.cypherTokenTypeToNode.topNode.id,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private addTopNodeToBuffer(buffer: number[][], document: string) {
|
|
133
|
+
const id = this.cypherTokenTypeToNode.topNode.id;
|
|
134
|
+
const startOffset = 0;
|
|
135
|
+
const endOffset = document.length;
|
|
136
|
+
const totalBufferLength = buffer.length * DEFAULT_NODE_GROUP_SIZE;
|
|
137
|
+
|
|
138
|
+
buffer.push([
|
|
139
|
+
id,
|
|
140
|
+
startOffset,
|
|
141
|
+
endOffset,
|
|
142
|
+
totalBufferLength + DEFAULT_NODE_GROUP_SIZE,
|
|
143
|
+
]);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { EditorState, StateField } from '@codemirror/state';
|
|
2
|
+
import { showTooltip, Tooltip } from '@codemirror/view';
|
|
3
|
+
import { signatureHelp } from '@neo4j-cypher/language-support';
|
|
4
|
+
import { SignatureInformation } from 'vscode-languageserver-types';
|
|
5
|
+
import { CypherConfig } from './langCypher';
|
|
6
|
+
import { getDocString } from './utils';
|
|
7
|
+
|
|
8
|
+
function getTriggerCharacter(query: string, caretPosition: number) {
|
|
9
|
+
let i = caretPosition - 1;
|
|
10
|
+
let triggerCharacter = query.at(i);
|
|
11
|
+
|
|
12
|
+
// Discard all space characters. Note that a space can be more than just a ' '
|
|
13
|
+
while (/\s/.test(triggerCharacter) && i > 0) {
|
|
14
|
+
i -= 1;
|
|
15
|
+
triggerCharacter = query.at(i);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return triggerCharacter;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const createSignatureHelpElement =
|
|
22
|
+
({
|
|
23
|
+
signature,
|
|
24
|
+
activeParameter,
|
|
25
|
+
}: {
|
|
26
|
+
signature: SignatureInformation;
|
|
27
|
+
activeParameter: number;
|
|
28
|
+
}) =>
|
|
29
|
+
() => {
|
|
30
|
+
const parameters = signature.parameters;
|
|
31
|
+
const doc = getDocString(signature.documentation);
|
|
32
|
+
const dom = document.createElement('div');
|
|
33
|
+
dom.className = 'cm-signature-help-panel';
|
|
34
|
+
|
|
35
|
+
const contents = document.createElement('div');
|
|
36
|
+
contents.className = 'cm-signature-help-panel-contents';
|
|
37
|
+
dom.appendChild(contents);
|
|
38
|
+
|
|
39
|
+
const signatureLabel = document.createElement('div');
|
|
40
|
+
signatureLabel.className = 'cm-signature-help-panel-name';
|
|
41
|
+
signatureLabel.appendChild(document.createTextNode(`${signature.label}(`));
|
|
42
|
+
|
|
43
|
+
parameters.forEach((param, index) => {
|
|
44
|
+
if (typeof param.documentation === 'string') {
|
|
45
|
+
const span = document.createElement('span');
|
|
46
|
+
span.appendChild(document.createTextNode(param.documentation));
|
|
47
|
+
if (index !== parameters.length - 1) {
|
|
48
|
+
span.appendChild(document.createTextNode(', '));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (index === activeParameter) {
|
|
52
|
+
span.className = 'cm-signature-help-panel-current-argument';
|
|
53
|
+
}
|
|
54
|
+
signatureLabel.appendChild(span);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
signatureLabel.appendChild(document.createTextNode(')'));
|
|
59
|
+
|
|
60
|
+
contents.appendChild(signatureLabel);
|
|
61
|
+
|
|
62
|
+
const separator = document.createElement('div');
|
|
63
|
+
separator.className = 'cm-signature-help-panel-separator';
|
|
64
|
+
|
|
65
|
+
contents.appendChild(separator);
|
|
66
|
+
|
|
67
|
+
const description = document.createElement('div');
|
|
68
|
+
description.className = 'cm-signature-help-panel-description';
|
|
69
|
+
description.appendChild(document.createTextNode(doc));
|
|
70
|
+
|
|
71
|
+
contents.appendChild(description);
|
|
72
|
+
|
|
73
|
+
return { dom };
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
function getSignatureHelpTooltip(
|
|
77
|
+
state: EditorState,
|
|
78
|
+
config: CypherConfig,
|
|
79
|
+
): Tooltip[] {
|
|
80
|
+
let result: Tooltip[] = [];
|
|
81
|
+
const schema = config.schema;
|
|
82
|
+
const ranges = state.selection.ranges;
|
|
83
|
+
const range = ranges.at(0);
|
|
84
|
+
|
|
85
|
+
if (schema && ranges.length === 1 && range.from === range.to) {
|
|
86
|
+
const caretPosition = range.from;
|
|
87
|
+
const query = state.doc.toString();
|
|
88
|
+
|
|
89
|
+
const triggerCharacter = getTriggerCharacter(query, caretPosition);
|
|
90
|
+
|
|
91
|
+
if (triggerCharacter === '(' || triggerCharacter === ',') {
|
|
92
|
+
const signatureHelpInfo = signatureHelp(query, schema, caretPosition);
|
|
93
|
+
const activeSignature = signatureHelpInfo.activeSignature;
|
|
94
|
+
const signatures = signatureHelpInfo.signatures;
|
|
95
|
+
const activeParameter = signatureHelpInfo.activeParameter;
|
|
96
|
+
|
|
97
|
+
if (
|
|
98
|
+
activeSignature !== undefined &&
|
|
99
|
+
activeSignature >= 0 &&
|
|
100
|
+
activeSignature < signatures.length &&
|
|
101
|
+
signatures[activeSignature].documentation !== undefined
|
|
102
|
+
) {
|
|
103
|
+
const signature = signatures[activeSignature];
|
|
104
|
+
const showSignatureTooltipBelow =
|
|
105
|
+
config.showSignatureTooltipBelow ?? true;
|
|
106
|
+
|
|
107
|
+
result = [
|
|
108
|
+
{
|
|
109
|
+
pos: caretPosition,
|
|
110
|
+
above: !showSignatureTooltipBelow,
|
|
111
|
+
arrow: true,
|
|
112
|
+
create: createSignatureHelpElement({ signature, activeParameter }),
|
|
113
|
+
},
|
|
114
|
+
];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function signatureHelpTooltip(config: CypherConfig) {
|
|
123
|
+
return StateField.define<readonly Tooltip[]>({
|
|
124
|
+
create: (state) => getSignatureHelpTooltip(state, config),
|
|
125
|
+
|
|
126
|
+
update(tooltips, tr) {
|
|
127
|
+
if (!tr.docChanged && !tr.selection) return tooltips;
|
|
128
|
+
return getSignatureHelpTooltip(tr.state, config);
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
provide: (f) => showTooltip.computeN([f], (state) => state.field(f)),
|
|
132
|
+
});
|
|
133
|
+
}
|