@neo4j-cypher/react-codemirror 2.0.0-next.4 → 2.0.0-next.41

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.
Files changed (186) hide show
  1. package/CHANGELOG.md +370 -0
  2. package/README.md +3 -2
  3. package/dist/src/CypherEditor.d.ts +245 -0
  4. package/dist/src/CypherEditor.js +513 -0
  5. package/dist/src/CypherEditor.js.map +1 -0
  6. package/dist/src/CypherEditor.test.js +162 -0
  7. package/dist/src/CypherEditor.test.js.map +1 -0
  8. package/dist/src/constants.d.ts +1 -0
  9. package/dist/src/constants.js +2 -0
  10. package/dist/src/constants.js.map +1 -0
  11. package/dist/src/diffView.d.ts +13 -0
  12. package/dist/src/diffView.js +11 -0
  13. package/dist/src/diffView.js.map +1 -0
  14. package/dist/src/e2e_tests/autoCompletion.spec.js +334 -0
  15. package/dist/src/e2e_tests/autoCompletion.spec.js.map +1 -0
  16. package/dist/src/e2e_tests/configuration.spec.js +83 -0
  17. package/dist/src/e2e_tests/configuration.spec.js.map +1 -0
  18. package/dist/src/e2e_tests/debounce.spec.js +66 -0
  19. package/dist/src/e2e_tests/debounce.spec.js.map +1 -0
  20. package/dist/{types/e2e_tests/e2e-utils.d.ts → src/e2e_tests/e2eUtils.d.ts} +2 -0
  21. package/dist/src/e2e_tests/e2eUtils.js +79 -0
  22. package/dist/src/e2e_tests/e2eUtils.js.map +1 -0
  23. package/dist/src/e2e_tests/extraKeybindings.spec.js +43 -0
  24. package/dist/src/e2e_tests/extraKeybindings.spec.js.map +1 -0
  25. package/dist/src/e2e_tests/historyNavigation.spec.js +227 -0
  26. package/dist/src/e2e_tests/historyNavigation.spec.js.map +1 -0
  27. package/dist/src/e2e_tests/performanceTest.spec.d.ts +6 -0
  28. package/dist/src/e2e_tests/performanceTest.spec.js +97 -0
  29. package/dist/src/e2e_tests/performanceTest.spec.js.map +1 -0
  30. package/dist/src/e2e_tests/sanityChecks.spec.js +64 -0
  31. package/dist/src/e2e_tests/sanityChecks.spec.js.map +1 -0
  32. package/dist/src/e2e_tests/signatureHelp.spec.js +226 -0
  33. package/dist/src/e2e_tests/signatureHelp.spec.js.map +1 -0
  34. package/dist/src/e2e_tests/snippets.spec.js +14 -0
  35. package/dist/src/e2e_tests/snippets.spec.js.map +1 -0
  36. package/dist/src/e2e_tests/syntaxHighlighting.spec.d.ts +1 -0
  37. package/dist/src/e2e_tests/syntaxHighlighting.spec.js +90 -0
  38. package/dist/src/e2e_tests/syntaxHighlighting.spec.js.map +1 -0
  39. package/dist/src/e2e_tests/syntaxValidation.spec.d.ts +1 -0
  40. package/dist/src/e2e_tests/syntaxValidation.spec.js +129 -0
  41. package/dist/src/e2e_tests/syntaxValidation.spec.js.map +1 -0
  42. package/dist/src/editorActions.d.ts +17 -0
  43. package/dist/src/editorActions.js +259 -0
  44. package/dist/src/editorActions.js.map +1 -0
  45. package/dist/{types/history-navigation.d.ts → src/historyNavigation.d.ts} +1 -1
  46. package/dist/src/historyNavigation.js +163 -0
  47. package/dist/src/historyNavigation.js.map +1 -0
  48. package/dist/src/hostCallbacks.d.ts +9 -0
  49. package/dist/src/hostCallbacks.js +2 -0
  50. package/dist/src/hostCallbacks.js.map +1 -0
  51. package/dist/{types → src}/icons.d.ts +1 -1
  52. package/dist/src/icons.js +62 -0
  53. package/dist/src/icons.js.map +1 -0
  54. package/dist/src/index.d.ts +6 -0
  55. package/dist/src/index.js +5 -0
  56. package/dist/src/index.js.map +1 -0
  57. package/dist/src/inlinePanel.d.ts +27 -0
  58. package/dist/src/inlinePanel.js +86 -0
  59. package/dist/src/inlinePanel.js.map +1 -0
  60. package/dist/src/lang-cypher/autocomplete.d.ts +6 -0
  61. package/dist/src/lang-cypher/autocomplete.js +116 -0
  62. package/dist/src/lang-cypher/autocomplete.js.map +1 -0
  63. package/dist/{types → src}/lang-cypher/constants.d.ts +15 -2
  64. package/dist/src/lang-cypher/constants.js +74 -0
  65. package/dist/src/lang-cypher/constants.js.map +1 -0
  66. package/dist/src/lang-cypher/contants.test.d.ts +1 -0
  67. package/dist/src/lang-cypher/contants.test.js +103 -0
  68. package/dist/src/lang-cypher/contants.test.js.map +1 -0
  69. package/dist/{types/lang-cypher/create-cypher-theme.d.ts → src/lang-cypher/createCypherTheme.d.ts} +14 -3
  70. package/dist/src/lang-cypher/createCypherTheme.js +202 -0
  71. package/dist/src/lang-cypher/createCypherTheme.js.map +1 -0
  72. package/dist/src/lang-cypher/langCypher.d.ts +15 -0
  73. package/dist/src/lang-cypher/langCypher.js +23 -0
  74. package/dist/src/lang-cypher/langCypher.js.map +1 -0
  75. package/dist/src/lang-cypher/lintWorker.mjs +2039 -0
  76. package/dist/src/lang-cypher/parser-adapter.d.ts +21 -0
  77. package/dist/src/lang-cypher/parser-adapter.js +115 -0
  78. package/dist/src/lang-cypher/parser-adapter.js.map +1 -0
  79. package/dist/src/lang-cypher/prism-global.d.ts +1 -0
  80. package/dist/src/lang-cypher/prism-global.js +7 -0
  81. package/dist/src/lang-cypher/prism-global.js.map +1 -0
  82. package/dist/src/lang-cypher/signatureHelp.d.ts +4 -0
  83. package/dist/src/lang-cypher/signatureHelp.js +108 -0
  84. package/dist/src/lang-cypher/signatureHelp.js.map +1 -0
  85. package/dist/src/lang-cypher/syntaxValidation.d.ts +4 -0
  86. package/dist/src/lang-cypher/syntaxValidation.js +65 -0
  87. package/dist/src/lang-cypher/syntaxValidation.js.map +1 -0
  88. package/dist/src/lang-cypher/themeIcons.js +22 -0
  89. package/dist/src/lang-cypher/themeIcons.js.map +1 -0
  90. package/dist/src/lang-cypher/utils.d.ts +2 -0
  91. package/dist/src/lang-cypher/utils.js +10 -0
  92. package/dist/src/lang-cypher/utils.js.map +1 -0
  93. package/dist/src/ndlTokensCopy.d.ts +570 -0
  94. package/dist/src/ndlTokensCopy.js +571 -0
  95. package/dist/src/ndlTokensCopy.js.map +1 -0
  96. package/dist/src/ndlTokensCopy.test.d.ts +1 -0
  97. package/dist/src/ndlTokensCopy.test.js +12 -0
  98. package/dist/src/ndlTokensCopy.test.js.map +1 -0
  99. package/dist/src/neo4jSetup.d.ts +6 -0
  100. package/dist/src/neo4jSetup.js +120 -0
  101. package/dist/src/neo4jSetup.js.map +1 -0
  102. package/dist/src/richClipboardCopier.d.ts +4 -0
  103. package/dist/src/richClipboardCopier.js +78 -0
  104. package/dist/src/richClipboardCopier.js.map +1 -0
  105. package/dist/src/richClipboardCopier.test.d.ts +1 -0
  106. package/dist/src/richClipboardCopier.test.js +53 -0
  107. package/dist/src/richClipboardCopier.test.js.map +1 -0
  108. package/dist/src/themes.d.ts +5 -0
  109. package/dist/src/themes.js +107 -0
  110. package/dist/src/themes.js.map +1 -0
  111. package/dist/tsconfig.tsbuildinfo +1 -0
  112. package/package.json +64 -62
  113. package/src/CypherEditor.test.tsx +211 -0
  114. package/src/CypherEditor.tsx +586 -59
  115. package/src/constants.ts +1 -0
  116. package/src/diffView.ts +24 -0
  117. package/src/e2e_tests/autoCompletion.spec.tsx +576 -0
  118. package/src/e2e_tests/configuration.spec.tsx +111 -0
  119. package/src/e2e_tests/debounce.spec.tsx +102 -0
  120. package/src/e2e_tests/{e2e-utils.ts → e2eUtils.ts} +41 -3
  121. package/src/e2e_tests/{extra-keybindings.spec.tsx → extraKeybindings.spec.tsx} +1 -3
  122. package/src/e2e_tests/{history-navigation.spec.tsx → historyNavigation.spec.tsx} +138 -19
  123. package/src/e2e_tests/performanceTest.spec.tsx +163 -0
  124. package/src/e2e_tests/{sanity-checks.spec.tsx → sanityChecks.spec.tsx} +28 -20
  125. package/src/e2e_tests/signatureHelp.spec.tsx +442 -0
  126. package/src/e2e_tests/snippets.spec.tsx +19 -0
  127. package/src/e2e_tests/{syntax-highlighting.spec.tsx → syntaxHighlighting.spec.tsx} +26 -24
  128. package/src/e2e_tests/syntaxValidation.spec.tsx +252 -0
  129. package/src/editorActions.ts +334 -0
  130. package/src/{history-navigation.ts → historyNavigation.ts} +6 -4
  131. package/src/hostCallbacks.ts +9 -0
  132. package/src/icons.ts +3 -0
  133. package/src/index.ts +4 -2
  134. package/src/inlinePanel.ts +140 -0
  135. package/src/lang-cypher/autocomplete.ts +96 -19
  136. package/src/lang-cypher/constants.ts +35 -2
  137. package/src/lang-cypher/contants.test.ts +7 -3
  138. package/src/lang-cypher/{create-cypher-theme.ts → createCypherTheme.ts} +97 -33
  139. package/src/lang-cypher/langCypher.ts +44 -0
  140. package/src/lang-cypher/lintWorker.mjs +2039 -0
  141. package/src/lang-cypher/parser-adapter.ts +146 -0
  142. package/src/lang-cypher/prism-global.ts +7 -0
  143. package/src/lang-cypher/signatureHelp.ts +154 -0
  144. package/src/lang-cypher/syntaxValidation.ts +81 -0
  145. package/src/lang-cypher/utils.ts +9 -0
  146. package/src/{ndl-tokens-copy.test.ts → ndlTokensCopy.test.ts} +2 -1
  147. package/src/ndlTokensCopy.ts +570 -0
  148. package/src/{neo4j-setup.tsx → neo4jSetup.tsx} +79 -18
  149. package/src/richClipboardCopier.test.ts +67 -0
  150. package/src/richClipboardCopier.ts +99 -0
  151. package/src/themes.ts +63 -71
  152. package/src/viteEnv.d.ts +1 -0
  153. package/dist/cjs/index.cjs +0 -1441
  154. package/dist/cjs/index.cjs.map +0 -7
  155. package/dist/esm/index.mjs +0 -1464
  156. package/dist/esm/index.mjs.map +0 -7
  157. package/dist/types/CypherEditor.d.ts +0 -113
  158. package/dist/types/e2e_tests/mock-data.d.ts +0 -3779
  159. package/dist/types/index.d.ts +0 -4
  160. package/dist/types/lang-cypher/ParserAdapter.d.ts +0 -14
  161. package/dist/types/lang-cypher/autocomplete.d.ts +0 -3
  162. package/dist/types/lang-cypher/lang-cypher.d.ts +0 -7
  163. package/dist/types/lang-cypher/syntax-validation.d.ts +0 -3
  164. package/dist/types/ndl-tokens-copy.d.ts +0 -379
  165. package/dist/types/neo4j-setup.d.ts +0 -2
  166. package/dist/types/themes.d.ts +0 -11
  167. package/dist/types/tsconfig.tsbuildinfo +0 -1
  168. package/src/e2e_tests/auto-completion.spec.tsx +0 -232
  169. package/src/e2e_tests/mock-data.ts +0 -4310
  170. package/src/e2e_tests/performance-test.spec.tsx +0 -71
  171. package/src/e2e_tests/syntax-validation.spec.tsx +0 -156
  172. package/src/lang-cypher/ParserAdapter.ts +0 -92
  173. package/src/lang-cypher/lang-cypher.ts +0 -32
  174. package/src/lang-cypher/syntax-validation.ts +0 -24
  175. package/src/ndl-tokens-copy.ts +0 -379
  176. /package/dist/{types/e2e_tests/auto-completion.spec.d.ts → src/CypherEditor.test.d.ts} +0 -0
  177. /package/dist/{types/e2e_tests/extra-keybindings.spec.d.ts → src/e2e_tests/autoCompletion.spec.d.ts} +0 -0
  178. /package/dist/{types/e2e_tests/history-navigation.spec.d.ts → src/e2e_tests/configuration.spec.d.ts} +0 -0
  179. /package/dist/{types/e2e_tests/performance-test.spec.d.ts → src/e2e_tests/debounce.spec.d.ts} +0 -0
  180. /package/dist/{types/e2e_tests/sanity-checks.spec.d.ts → src/e2e_tests/extraKeybindings.spec.d.ts} +0 -0
  181. /package/dist/{types/e2e_tests/syntax-highlighting.spec.d.ts → src/e2e_tests/historyNavigation.spec.d.ts} +0 -0
  182. /package/dist/{types/e2e_tests/syntax-validation.spec.d.ts → src/e2e_tests/sanityChecks.spec.d.ts} +0 -0
  183. /package/dist/{types/lang-cypher/contants.test.d.ts → src/e2e_tests/signatureHelp.spec.d.ts} +0 -0
  184. /package/dist/{types/ndl-tokens-copy.test.d.ts → src/e2e_tests/snippets.spec.d.ts} +0 -0
  185. /package/dist/{types/lang-cypher/theme-icons.d.ts → src/lang-cypher/themeIcons.d.ts} +0 -0
  186. /package/src/lang-cypher/{theme-icons.ts → themeIcons.ts} +0 -0
@@ -0,0 +1,140 @@
1
+ import { StateEffect, StateField, type Extension } from '@codemirror/state';
2
+ import {
3
+ Decoration,
4
+ type DecorationSet,
5
+ EditorView,
6
+ WidgetType,
7
+ } from '@codemirror/view';
8
+
9
+ import type { HostPortalCallbacks } from './hostCallbacks';
10
+
11
+ /** Lifecycle callbacks for an inline panel. See {@link HostPortalCallbacks}. */
12
+ export type InlinePanelCallbacks = HostPortalCallbacks;
13
+
14
+ type InlinePanelShowOptions = {
15
+ /** Document position the panel anchors to. */
16
+ pos: number;
17
+ /**
18
+ * Where to render the panel relative to the line at `pos`. `'above'`
19
+ * places it before the line, `'below'` after it.
20
+ *
21
+ * @default 'above'
22
+ */
23
+ placement?: 'above' | 'below';
24
+ } & InlinePanelCallbacks;
25
+
26
+ type ShowPayload = InlinePanelShowOptions | null;
27
+
28
+ export type InlinePanelController = {
29
+ /** CodeMirror extension to register on the editor. */
30
+ extension: Extension;
31
+ /** Build an effect that mounts the panel with the given options. */
32
+ show: (options: InlinePanelShowOptions) => StateEffect<ShowPayload>;
33
+ /** Build an effect that unmounts the panel. */
34
+ hide: () => StateEffect<ShowPayload>;
35
+ updateCallbacks: (callbacks: InlinePanelCallbacks) => void;
36
+ };
37
+
38
+ export function createInlinePanelController(): InlinePanelController {
39
+ const showEffect = StateEffect.define<ShowPayload>();
40
+
41
+ let callbacksRef: InlinePanelCallbacks | null = null;
42
+
43
+ class InlinePanelWidget extends WidgetType {
44
+ private resizeObserver: ResizeObserver | null = null;
45
+
46
+ constructor(readonly options: InlinePanelShowOptions) {
47
+ super();
48
+ }
49
+
50
+ toDOM(view: EditorView): HTMLElement {
51
+ const container = document.createElement('div');
52
+ container.className = 'cm-inline-panel';
53
+ this.resizeObserver = new ResizeObserver(() => view.requestMeasure());
54
+ this.resizeObserver.observe(container);
55
+ callbacksRef?.onMount(container);
56
+ return container;
57
+ }
58
+
59
+ destroy(): void {
60
+ this.resizeObserver?.disconnect();
61
+ this.resizeObserver = null;
62
+ callbacksRef?.onUnmount();
63
+ }
64
+
65
+ eq(other: InlinePanelWidget): boolean {
66
+ return (
67
+ other.options.pos === this.options.pos &&
68
+ other.options.placement === this.options.placement
69
+ );
70
+ }
71
+
72
+ ignoreEvent(): boolean {
73
+ return true;
74
+ }
75
+ }
76
+
77
+ const buildDecoration = (options: InlinePanelShowOptions): DecorationSet =>
78
+ Decoration.set([
79
+ Decoration.widget({
80
+ widget: new InlinePanelWidget(options),
81
+ block: true,
82
+ side: options.placement === 'below' ? 1 : -1,
83
+ }).range(options.pos),
84
+ ]);
85
+
86
+ const readOptions = (
87
+ decorations: DecorationSet,
88
+ ): InlinePanelShowOptions | null => {
89
+ const widget = decorations.iter().value?.spec?.widget;
90
+ return widget instanceof InlinePanelWidget ? widget.options : null;
91
+ };
92
+
93
+ const field = StateField.define<DecorationSet>({
94
+ create() {
95
+ return Decoration.none;
96
+ },
97
+ update(decorations, transaction) {
98
+ for (const effect of transaction.effects) {
99
+ if (effect.is(showEffect)) {
100
+ return effect.value === null
101
+ ? Decoration.none
102
+ : buildDecoration(effect.value);
103
+ }
104
+ }
105
+
106
+ // Nothing to do when the panel is closed, or when the document is unchanged
107
+ if (decorations.size === 0 || !transaction.docChanged) {
108
+ return decorations.map(transaction.changes);
109
+ }
110
+
111
+ // Re-anchor manually because CodeMirror's default mapping silently drops
112
+ // block-widget decorations when their anchor line is deleted
113
+ const options = readOptions(decorations);
114
+ if (options === null) {
115
+ return decorations.map(transaction.changes);
116
+ }
117
+ const side = options.placement === 'below' ? 1 : -1;
118
+ const lineNumber = Math.min(
119
+ transaction.startState.doc.lineAt(options.pos).number,
120
+ transaction.newDoc.lines,
121
+ );
122
+ const line = transaction.newDoc.line(lineNumber);
123
+
124
+ return buildDecoration({
125
+ ...options,
126
+ pos: side === 1 ? line.to : line.from,
127
+ });
128
+ },
129
+ provide: (f) => EditorView.decorations.from(f),
130
+ });
131
+
132
+ return {
133
+ extension: field,
134
+ show: (options) => showEffect.of(options),
135
+ hide: () => showEffect.of(null),
136
+ updateCallbacks: (callbacks) => {
137
+ callbacksRef = callbacks;
138
+ },
139
+ };
140
+ }
@@ -1,8 +1,13 @@
1
- import { CompletionSource } from '@codemirror/autocomplete';
2
- import { autocomplete } from '@neo4j-cypher/language-support';
3
- import { CompletionItemKind } from 'vscode-languageserver-types';
4
- import { CompletionItemIcons } from '../icons';
5
- import type { CypherConfig } from './lang-cypher';
1
+ import type { Completion, CompletionSource } from '@codemirror/autocomplete';
2
+ import { snippet } from '@codemirror/autocomplete';
3
+ import { shouldAutoCompleteYield } from '@neo4j-cypher/language-support';
4
+ import {
5
+ CompletionItemKind,
6
+ CompletionItemTag,
7
+ } from 'vscode-languageserver-types';
8
+ import type { CompletionItemIcons } from '../icons';
9
+ import type { CypherConfig } from './langCypher';
10
+ import { getDocString } from './utils';
6
11
 
7
12
  const completionKindToCodemirrorIcon = (c: CompletionItemKind) => {
8
13
  const map: Record<CompletionItemKind, CompletionItemIcons> = {
@@ -28,7 +33,8 @@ const completionKindToCodemirrorIcon = (c: CompletionItemKind) => {
28
33
  [CompletionItemKind.EnumMember]: 'EnumMember',
29
34
  [CompletionItemKind.Constant]: 'Constant',
30
35
  [CompletionItemKind.Struct]: 'Struct',
31
- [CompletionItemKind.Event]: 'Event',
36
+ // we're miss-using the enum here as there is no `Console` kind in the predefined list
37
+ [CompletionItemKind.Event]: 'Console',
32
38
  [CompletionItemKind.Operator]: 'Operator',
33
39
  [CompletionItemKind.TypeParameter]: 'TypeParameter',
34
40
  };
@@ -36,30 +42,101 @@ const completionKindToCodemirrorIcon = (c: CompletionItemKind) => {
36
42
  return map[c];
37
43
  };
38
44
 
45
+ export const completionStyles: (
46
+ completion: Completion & { deprecated?: boolean },
47
+ ) => string = (completion) => {
48
+ if (completion.deprecated) {
49
+ return 'cm-deprecated-element';
50
+ } else {
51
+ return null;
52
+ }
53
+ };
54
+
39
55
  export const cypherAutocomplete: (config: CypherConfig) => CompletionSource =
40
56
  (config) => (context) => {
41
- const textUntilCursor = context.state.doc.toString().slice(0, context.pos);
42
-
43
- const triggerCharacters = ['.', ':', '{', '$'];
44
- const lastCharacter = textUntilCursor.slice(-1);
45
-
57
+ const documentText = context.state.doc.toString();
58
+ const offset = context.pos;
59
+ const triggerCharacters = ['.', ':', '{', '$', ')', ']', '-', '<'];
60
+ const lastCharacter = documentText.at(offset - 1);
61
+ const yieldTriggered = shouldAutoCompleteYield(documentText, offset);
46
62
  const lastWord = context.matchBefore(/\w*/);
47
63
  const inWord = lastWord.from !== lastWord.to;
48
-
49
64
  const shouldTriggerCompletion =
50
- inWord || context.explicit || triggerCharacters.includes(lastCharacter);
65
+ inWord ||
66
+ context.explicit ||
67
+ triggerCharacters.includes(lastCharacter) ||
68
+ yieldTriggered;
69
+
70
+ if (config.useLightVersion && !context.explicit) {
71
+ return null;
72
+ }
51
73
 
52
74
  if (!shouldTriggerCompletion) {
53
75
  return null;
54
76
  }
55
77
 
56
- const options = autocomplete(textUntilCursor, config.schema ?? {});
78
+ const options = config.languageService.autocomplete(
79
+ documentText,
80
+ config.schema ?? {},
81
+ {
82
+ caretPosition: offset,
83
+ manual: context.explicit,
84
+ },
85
+ );
57
86
 
58
87
  return {
59
- from: context.matchBefore(/(\w|\$)*$/).from,
60
- options: options.map((o) => ({
61
- label: o.label,
62
- type: completionKindToCodemirrorIcon(o.kind),
63
- })),
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) ?? false;
120
+ // The negative boost moves the deprecation down the list
121
+ // so we offer the user the completions that are
122
+ // deprecated the last
123
+ const maybeDeprecated = deprecated
124
+ ? { boost: -99, deprecated: true }
125
+ : {};
126
+
127
+ return {
128
+ label: o.insertText ? o.insertText : o.label,
129
+ displayLabel: 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
+ }),
64
141
  };
65
142
  };
@@ -1,8 +1,9 @@
1
1
  import { languageDataProp } from '@codemirror/language';
2
2
  import type { Facet } from '@codemirror/state';
3
3
  import { NodeSet, NodeType } from '@lezer/common';
4
- import { styleTags, Tag, tags } from '@lezer/highlight';
5
- import { CypherTokenType } from '@neo4j-cypher/language-support';
4
+ import type { Tag } from '@lezer/highlight';
5
+ import { styleTags, tags } from '@lezer/highlight';
6
+ import type { CypherTokenType } from '@neo4j-cypher/language-support';
6
7
 
7
8
  export const cypherTokenTypeToNode = (facet: Facet<unknown>) => ({
8
9
  topNode: NodeType.define({
@@ -31,8 +32,36 @@ export const cypherTokenTypeToNode = (facet: Facet<unknown>) => ({
31
32
  none: NodeType.define({ id: 19, name: 'none' }),
32
33
  separator: NodeType.define({ id: 20, name: 'separator' }),
33
34
  punctuation: NodeType.define({ id: 21, name: 'punctuation' }),
35
+ consoleCommand: NodeType.define({ id: 22, name: 'consoleCommand' }),
36
+ // also include prism token types
37
+ 'class-name': NodeType.define({ id: 23, name: 'label' }),
38
+ // this is escaped variables
39
+ identifier: NodeType.define({ id: 24, name: 'variable' }),
40
+ string: NodeType.define({ id: 25, name: 'stringLiteral' }),
41
+ relationship: NodeType.define({ id: 26, name: 'label' }),
42
+ boolean: NodeType.define({ id: 27, name: 'booleanLiteral' }),
43
+ number: NodeType.define({ id: 28, name: 'numberLiteral' }),
44
+ setting: NodeType.define({ id: 29, name: 'setting' }),
45
+ settingValue: NodeType.define({ id: 30, name: 'settingValue' }),
46
+ interpolationDelimiter: NodeType.define({
47
+ id: 31,
48
+ name: 'interpolationDelimiter',
49
+ }),
34
50
  });
35
51
 
52
+ type PrismSpecificTokenType =
53
+ | 'class-name'
54
+ | 'identifier'
55
+ | 'string'
56
+ | 'relationship'
57
+ | 'boolean'
58
+ | 'number';
59
+
60
+ export type CodemirrorParseTokenType =
61
+ | CypherTokenType
62
+ | PrismSpecificTokenType
63
+ | 'topNode';
64
+
36
65
  export type HighlightedCypherTokenTypes = Exclude<CypherTokenType, 'none'>;
37
66
  export const tokenTypeToStyleTag: Record<HighlightedCypherTokenTypes, Tag> = {
38
67
  comment: tags.comment,
@@ -44,6 +73,7 @@ export const tokenTypeToStyleTag: Record<HighlightedCypherTokenTypes, Tag> = {
44
73
  variable: tags.variableName,
45
74
  paramDollar: tags.atom,
46
75
  paramValue: tags.atom,
76
+ interpolationDelimiter: tags.atom,
47
77
  symbolicName: tags.variableName,
48
78
  operator: tags.operator,
49
79
  stringLiteral: tags.string,
@@ -55,6 +85,9 @@ export const tokenTypeToStyleTag: Record<HighlightedCypherTokenTypes, Tag> = {
55
85
  bracket: tags.bracket,
56
86
  punctuation: tags.punctuation,
57
87
  separator: tags.separator,
88
+ consoleCommand: tags.macroName,
89
+ setting: tags.attributeName,
90
+ settingValue: tags.attributeValue,
58
91
  };
59
92
 
60
93
  export const parserAdapterNodeSet = (nodes: Record<string, NodeType>) =>
@@ -1,5 +1,9 @@
1
1
  import { tags } from '@lezer/highlight';
2
- import { applySyntaxColouring } from '@neo4j-cypher/language-support';
2
+ import {
3
+ highlightSyntax,
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]->()
@@ -10,7 +14,7 @@ WHERE variable.property = "String"
10
14
  RETURN variable;`;
11
15
 
12
16
  test('correctly parses all cypher token types to style tags', () => {
13
- const tokens = applySyntaxColouring(cypherQueryWithAllTokenTypes);
17
+ const tokens = highlightSyntax(cypherQueryWithAllTokenTypes);
14
18
  const tokenTypes = tokens.map((token) => token.tokenType);
15
19
  expect(tokenTypes).toEqual([
16
20
  'keyword',
@@ -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 === 'none') return undefined;
61
+ if (tokenType === CypherTokenType.none) return undefined;
58
62
  return tokenTypeToStyleTag[tokenType];
59
63
  });
60
64
  const correctTags = [
@@ -1,12 +1,11 @@
1
- import {
2
- HighlightStyle,
3
- syntaxHighlighting,
4
- TagStyle,
5
- } from '@codemirror/language';
6
- import { Extension } from '@codemirror/state';
1
+ import type { TagStyle } from '@codemirror/language';
2
+ import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
3
+ import type { Extension } from '@codemirror/state';
7
4
  import { EditorView } from '@codemirror/view';
8
- import { StyleSpec } from 'style-mod';
9
- import { HighlightedCypherTokenTypes, tokenTypeToStyleTag } from './constants';
5
+ import { CypherTokenType } from '@neo4j-cypher/language-support';
6
+ import type { StyleSpec } from 'style-mod';
7
+ import type { HighlightedCypherTokenTypes } from './constants';
8
+ import { tokenTypeToStyleTag } from './constants';
10
9
  import {
11
10
  byWordSvg,
12
11
  caseSensitiveSvg,
@@ -15,7 +14,18 @@ import {
15
14
  replaceAllSvg,
16
15
  replaceSvg,
17
16
  upArrowSvg,
18
- } from './theme-icons';
17
+ } from './themeIcons';
18
+
19
+ export interface DiffColors {
20
+ /** Background fill for the whole inserted/changed line. */
21
+ insertedLine: string;
22
+ /** Highlight for the exact inserted text within a changed line. */
23
+ insertedText: string;
24
+ /** Background fill for the whole deleted line widget. */
25
+ deletedLine: string;
26
+ /** Highlight for the exact deleted text within a deleted chunk. */
27
+ deletedText: string;
28
+ }
19
29
 
20
30
  export interface ThemeOptions {
21
31
  dark: boolean;
@@ -39,6 +49,7 @@ export interface ThemeOptions {
39
49
  };
40
50
  highlightStyles: Partial<Record<HighlightedCypherTokenTypes, string>>;
41
51
  inheritBgColor?: boolean;
52
+ diffColors?: DiffColors;
42
53
  }
43
54
 
44
55
  export const createCypherTheme = ({
@@ -46,6 +57,7 @@ export const createCypherTheme = ({
46
57
  editorSettings: settings,
47
58
  highlightStyles,
48
59
  inheritBgColor,
60
+ diffColors,
49
61
  }: ThemeOptions): Extension => {
50
62
  const themeOptions: Record<string, StyleSpec> = {
51
63
  '&': {
@@ -53,6 +65,9 @@ export const createCypherTheme = ({
53
65
  color: settings.foreground,
54
66
  fontVariantLigatures: 'none',
55
67
  },
68
+ '& .cm-snippetField': {
69
+ backgroundColor: settings.autoCompletionPanel.selectedColor,
70
+ },
56
71
  '&.cm-focused': {
57
72
  outline: 'none',
58
73
  },
@@ -61,8 +76,9 @@ export const createCypherTheme = ({
61
76
  color: settings.gutterForeground,
62
77
  border: 'none',
63
78
  },
64
- '&.cm-editor .cm-scroller': {
79
+ '&.cm-editor': {
65
80
  fontFamily: 'Fira Code, Menlo, Monaco, Lucida Console, monospace',
81
+ height: '100%',
66
82
  },
67
83
  '.cm-content': {
68
84
  caretColor: settings.cursor,
@@ -80,9 +96,13 @@ export const createCypherTheme = ({
80
96
  '& .cm-selectionMatch': {
81
97
  backgroundColor: settings.textMatchingSelection,
82
98
  },
99
+ '& .cm-bold': {
100
+ fontWeight: 'bold',
101
+ },
83
102
  '& .cm-panels': {
84
103
  backgroundColor: settings.searchPanel.background,
85
104
  fontFamily: 'Fira Code, Menlo, Monaco, Lucida Console, monospace',
105
+ zIndex: 0,
86
106
  },
87
107
  '& .cm-completionLabel': {
88
108
  fontFamily: 'Fira Code, Menlo, Monaco, Lucida Console, monospace',
@@ -93,7 +113,40 @@ export const createCypherTheme = ({
93
113
  color: settings.autoCompletionPanel.matchingTextColor,
94
114
  textDecoration: 'none',
95
115
  },
116
+ '& .cm-signature-help-panel': {
117
+ backgroundColor: settings.autoCompletionPanel.backgroundColor,
118
+ maxWidth: '700px',
119
+ maxHeight: '250px',
120
+ fontFamily: 'Fira Code, Menlo, Monaco, Lucida Console, monospace',
121
+ },
122
+ '& .cm-signature-help-panel-contents': {
123
+ overflow: 'auto',
124
+ maxHeight: '250px',
125
+ },
126
+ '& .cm-signature-help-panel-current-argument': {
127
+ color: settings.autoCompletionPanel.matchingTextColor,
128
+ fontWeight: 'bold',
129
+ },
130
+ '& .cm-signature-help-panel-separator': {
131
+ borderBottom: '1px solid #ccc',
132
+ },
133
+ '& .cm-signature-help-panel-name': {
134
+ padding: '5px',
135
+ },
136
+ '& .cm-signature-help-panel-arg-description': {
137
+ padding: '5px',
138
+ },
139
+ '& .cm-signature-help-panel-description': {
140
+ padding: '5px',
141
+ },
142
+ '.cm-completionInfo-signature': {
143
+ color: 'darkgrey',
144
+ },
145
+ '.cm-deprecated-element': {
146
+ 'text-decoration': 'line-through',
147
+ },
96
148
  '.cm-tooltip-autocomplete': {
149
+ maxWidth: '430px',
97
150
  '& > ul > li[aria-selected]': {
98
151
  backgroundColor: settings.autoCompletionPanel.selectedColor,
99
152
  color: settings.foreground,
@@ -118,24 +171,16 @@ export const createCypherTheme = ({
118
171
  border: 'none',
119
172
  verticalAlign: 'middle',
120
173
  '&[name=next]::before': {
121
- content: `url("data:image/svg+xml;base64,${window.btoa(
122
- downArrowSvg,
123
- )}")`,
174
+ content: `url("data:image/svg+xml;base64,${window.btoa(downArrowSvg)}")`,
124
175
  },
125
176
  '&[name=prev]::before': {
126
- content: `url("data:image/svg+xml;base64,${window.btoa(
127
- upArrowSvg,
128
- )}")`,
177
+ content: `url("data:image/svg+xml;base64,${window.btoa(upArrowSvg)}")`,
129
178
  },
130
179
  '&[name=replace]::before': {
131
- content: `url("data:image/svg+xml;base64,${window.btoa(
132
- replaceSvg,
133
- )}")`,
180
+ content: `url("data:image/svg+xml;base64,${window.btoa(replaceSvg)}")`,
134
181
  },
135
182
  '&[name=replaceAll]::before': {
136
- content: `url("data:image/svg+xml;base64,${window.btoa(
137
- replaceAllSvg,
138
- )}")`,
183
+ content: `url("data:image/svg+xml;base64,${window.btoa(replaceAllSvg)}")`,
139
184
  },
140
185
  width: '20px',
141
186
  height: '20px',
@@ -167,19 +212,13 @@ export const createCypherTheme = ({
167
212
  borderRadius: '4px',
168
213
 
169
214
  '&[name=case]::before': {
170
- content: `url("data:image/svg+xml;base64,${window.btoa(
171
- caseSensitiveSvg,
172
- )}")`,
215
+ content: `url("data:image/svg+xml;base64,${window.btoa(caseSensitiveSvg)}")`,
173
216
  },
174
217
  '&[name=re]::before': {
175
- content: `url("data:image/svg+xml;base64,${window.btoa(
176
- regexSvg,
177
- )}")`,
218
+ content: `url("data:image/svg+xml;base64,${window.btoa(regexSvg)}")`,
178
219
  },
179
220
  '&[name=word]::before': {
180
- content: `url("data:image/svg+xml;base64,${window.btoa(
181
- byWordSvg,
182
- )}")`,
221
+ content: `url("data:image/svg+xml;base64,${window.btoa(byWordSvg)}")`,
183
222
  },
184
223
  '&:hover': {
185
224
  backgroundColor: settings.searchPanel.buttonHoverBackground,
@@ -190,14 +229,39 @@ export const createCypherTheme = ({
190
229
  },
191
230
  },
192
231
  },
232
+ ...(diffColors && {
233
+ '&.cm-merge-b .cm-changedLine': {
234
+ backgroundColor: diffColors.insertedLine,
235
+ },
236
+ '&.cm-merge-b .cm-changedText': {
237
+ background: diffColors.insertedText,
238
+ },
239
+ '&.cm-merge-b .cm-deletedChunk': {
240
+ backgroundColor: diffColors.deletedLine,
241
+ },
242
+ '&.cm-merge-b .cm-deletedChunk .cm-deletedText': {
243
+ background: diffColors.deletedText,
244
+ },
245
+ // Hide the empty deletion widget that unifiedMergeView renders
246
+ // when the original document is empty (its only line is an empty <del>).
247
+ '&.cm-merge-b .cm-deletedChunk:has(> .cm-deletedLine:only-child > del > br:only-child)':
248
+ {
249
+ display: 'none',
250
+ },
251
+ }),
193
252
  };
194
253
 
195
254
  const themeExtension = EditorView.theme(themeOptions, { dark });
196
255
 
197
- const styles = Object.entries(highlightStyles).map(
198
- ([token, color]: [HighlightedCypherTokenTypes, string]): TagStyle => ({
256
+ const styles = (
257
+ Object.entries(highlightStyles) as Array<
258
+ [HighlightedCypherTokenTypes, string]
259
+ >
260
+ ).map(
261
+ ([token, color]): TagStyle => ({
199
262
  tag: tokenTypeToStyleTag[token],
200
263
  color,
264
+ class: token === CypherTokenType.consoleCommand ? 'cm-bold' : undefined,
201
265
  }),
202
266
  );
203
267
  const highlightStyle = HighlightStyle.define(styles);
@@ -0,0 +1,44 @@
1
+ import { autocompletion } from '@codemirror/autocomplete';
2
+ import {
3
+ defineLanguageFacet,
4
+ Language,
5
+ LanguageSupport,
6
+ } from '@codemirror/language';
7
+ import type { CypherLanguageService } from '@neo4j-cypher/language-support';
8
+ import { type DbSchema } from '@neo4j-cypher/language-support';
9
+ import { completionStyles, cypherAutocomplete } from './autocomplete';
10
+ import { ParserAdapter } from './parser-adapter';
11
+ import { signatureHelpTooltip } from './signatureHelp';
12
+ import { cypherLinter } from './syntaxValidation';
13
+
14
+ const facet = defineLanguageFacet({
15
+ commentTokens: { block: { open: '/*', close: '*/' }, line: '//' },
16
+ closeBrackets: { brackets: ['(', '[', '{', "'", '"', '`'] },
17
+ });
18
+
19
+ export type CypherConfig = {
20
+ languageService: CypherLanguageService;
21
+ lint?: boolean;
22
+ showSignatureTooltipBelow?: boolean;
23
+ featureFlags?: {
24
+ consoleCommands?: boolean;
25
+ };
26
+ schema?: DbSchema;
27
+ useLightVersion: boolean;
28
+ setUseLightVersion?: (useLightVersion: boolean) => void;
29
+ };
30
+
31
+ export function cypher(config: CypherConfig) {
32
+ const parserAdapter = new ParserAdapter(facet, config);
33
+
34
+ const cypherLanguage = new Language(facet, parserAdapter, [], 'cypher');
35
+
36
+ return new LanguageSupport(cypherLanguage, [
37
+ autocompletion({
38
+ override: [cypherAutocomplete(config)],
39
+ optionClass: completionStyles,
40
+ }),
41
+ cypherLinter(config),
42
+ signatureHelpTooltip(config),
43
+ ]);
44
+ }