@neo4j-cypher/react-codemirror 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.
Files changed (181) hide show
  1. package/CHANGELOG.md +350 -0
  2. package/README.md +3 -2
  3. package/dist/src/CypherEditor.d.ts +243 -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 +154 -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 +335 -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 +227 -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 +194 -0
  44. package/dist/src/editorActions.js.map +1 -0
  45. package/dist/src/historyNavigation.js +163 -0
  46. package/dist/src/historyNavigation.js.map +1 -0
  47. package/dist/src/hostCallbacks.d.ts +9 -0
  48. package/dist/src/hostCallbacks.js +2 -0
  49. package/dist/src/hostCallbacks.js.map +1 -0
  50. package/dist/{types → src}/icons.d.ts +1 -1
  51. package/dist/src/icons.js +62 -0
  52. package/dist/src/icons.js.map +1 -0
  53. package/dist/src/index.d.ts +6 -0
  54. package/dist/src/index.js +5 -0
  55. package/dist/src/index.js.map +1 -0
  56. package/dist/src/inlinePanel.d.ts +27 -0
  57. package/dist/src/inlinePanel.js +86 -0
  58. package/dist/src/inlinePanel.js.map +1 -0
  59. package/dist/src/lang-cypher/autocomplete.d.ts +6 -0
  60. package/dist/src/lang-cypher/autocomplete.js +116 -0
  61. package/dist/src/lang-cypher/autocomplete.js.map +1 -0
  62. package/dist/{types → src}/lang-cypher/constants.d.ts +13 -0
  63. package/dist/src/lang-cypher/constants.js +74 -0
  64. package/dist/src/lang-cypher/constants.js.map +1 -0
  65. package/dist/src/lang-cypher/contants.test.d.ts +1 -0
  66. package/dist/src/lang-cypher/contants.test.js +103 -0
  67. package/dist/src/lang-cypher/contants.test.js.map +1 -0
  68. package/dist/{types/lang-cypher/create-cypher-theme.d.ts → src/lang-cypher/createCypherTheme.d.ts} +12 -1
  69. package/dist/src/lang-cypher/createCypherTheme.js +202 -0
  70. package/dist/src/lang-cypher/createCypherTheme.js.map +1 -0
  71. package/dist/src/lang-cypher/langCypher.d.ts +14 -0
  72. package/dist/src/lang-cypher/langCypher.js +23 -0
  73. package/dist/src/lang-cypher/langCypher.js.map +1 -0
  74. package/dist/src/lang-cypher/lintWorker.mjs +2035 -0
  75. package/dist/src/lang-cypher/parser-adapter.d.ts +19 -0
  76. package/dist/src/lang-cypher/parser-adapter.js +112 -0
  77. package/dist/src/lang-cypher/parser-adapter.js.map +1 -0
  78. package/dist/src/lang-cypher/signatureHelp.d.ts +4 -0
  79. package/dist/src/lang-cypher/signatureHelp.js +108 -0
  80. package/dist/src/lang-cypher/signatureHelp.js.map +1 -0
  81. package/dist/{types/lang-cypher/syntax-validation.d.ts → src/lang-cypher/syntaxValidation.d.ts} +2 -1
  82. package/dist/src/lang-cypher/syntaxValidation.js +60 -0
  83. package/dist/src/lang-cypher/syntaxValidation.js.map +1 -0
  84. package/dist/src/lang-cypher/themeIcons.js +22 -0
  85. package/dist/src/lang-cypher/themeIcons.js.map +1 -0
  86. package/dist/src/lang-cypher/utils.d.ts +2 -0
  87. package/dist/src/lang-cypher/utils.js +10 -0
  88. package/dist/src/lang-cypher/utils.js.map +1 -0
  89. package/dist/src/ndlTokensCopy.d.ts +570 -0
  90. package/dist/src/ndlTokensCopy.js +571 -0
  91. package/dist/src/ndlTokensCopy.js.map +1 -0
  92. package/dist/src/ndlTokensCopy.test.d.ts +1 -0
  93. package/dist/src/ndlTokensCopy.test.js +12 -0
  94. package/dist/src/ndlTokensCopy.test.js.map +1 -0
  95. package/dist/src/neo4jSetup.d.ts +6 -0
  96. package/dist/src/neo4jSetup.js +120 -0
  97. package/dist/src/neo4jSetup.js.map +1 -0
  98. package/dist/src/richClipboardCopier.d.ts +4 -0
  99. package/dist/src/richClipboardCopier.js +78 -0
  100. package/dist/src/richClipboardCopier.js.map +1 -0
  101. package/dist/src/richClipboardCopier.test.d.ts +1 -0
  102. package/dist/src/richClipboardCopier.test.js +53 -0
  103. package/dist/src/richClipboardCopier.test.js.map +1 -0
  104. package/dist/src/themes.d.ts +5 -0
  105. package/dist/src/themes.js +107 -0
  106. package/dist/src/themes.js.map +1 -0
  107. package/dist/tsconfig.tsbuildinfo +1 -0
  108. package/package.json +62 -62
  109. package/src/CypherEditor.test.tsx +203 -0
  110. package/src/CypherEditor.tsx +584 -48
  111. package/src/constants.ts +1 -0
  112. package/src/diffView.ts +24 -0
  113. package/src/e2e_tests/autoCompletion.spec.tsx +577 -0
  114. package/src/e2e_tests/configuration.spec.tsx +111 -0
  115. package/src/e2e_tests/debounce.spec.tsx +102 -0
  116. package/src/e2e_tests/{e2e-utils.ts → e2eUtils.ts} +41 -3
  117. package/src/e2e_tests/{extra-keybindings.spec.tsx → extraKeybindings.spec.tsx} +1 -3
  118. package/src/e2e_tests/{history-navigation.spec.tsx → historyNavigation.spec.tsx} +137 -18
  119. package/src/e2e_tests/performanceTest.spec.tsx +163 -0
  120. package/src/e2e_tests/{sanity-checks.spec.tsx → sanityChecks.spec.tsx} +28 -20
  121. package/src/e2e_tests/signatureHelp.spec.tsx +443 -0
  122. package/src/e2e_tests/snippets.spec.tsx +19 -0
  123. package/src/e2e_tests/{syntax-highlighting.spec.tsx → syntaxHighlighting.spec.tsx} +26 -24
  124. package/src/e2e_tests/syntaxValidation.spec.tsx +252 -0
  125. package/src/editorActions.ts +253 -0
  126. package/src/{history-navigation.ts → historyNavigation.ts} +1 -1
  127. package/src/hostCallbacks.ts +9 -0
  128. package/src/icons.ts +3 -0
  129. package/src/index.ts +4 -2
  130. package/src/inlinePanel.ts +140 -0
  131. package/src/lang-cypher/autocomplete.ts +98 -18
  132. package/src/lang-cypher/constants.ts +32 -0
  133. package/src/lang-cypher/contants.test.ts +7 -3
  134. package/src/lang-cypher/{create-cypher-theme.ts → createCypherTheme.ts} +85 -23
  135. package/src/lang-cypher/langCypher.ts +46 -0
  136. package/src/lang-cypher/lintWorker.mjs +2035 -0
  137. package/src/lang-cypher/parser-adapter.ts +145 -0
  138. package/src/lang-cypher/signatureHelp.ts +154 -0
  139. package/src/lang-cypher/syntaxValidation.ts +76 -0
  140. package/src/lang-cypher/utils.ts +9 -0
  141. package/src/{ndl-tokens-copy.test.ts → ndlTokensCopy.test.ts} +2 -1
  142. package/src/ndlTokensCopy.ts +570 -0
  143. package/src/{neo4j-setup.tsx → neo4jSetup.tsx} +78 -17
  144. package/src/richClipboardCopier.test.ts +65 -0
  145. package/src/richClipboardCopier.ts +99 -0
  146. package/src/themes.ts +61 -69
  147. package/src/viteEnv.d.ts +1 -0
  148. package/dist/cjs/index.cjs +0 -1441
  149. package/dist/cjs/index.cjs.map +0 -7
  150. package/dist/esm/index.mjs +0 -1464
  151. package/dist/esm/index.mjs.map +0 -7
  152. package/dist/types/CypherEditor.d.ts +0 -113
  153. package/dist/types/e2e_tests/mock-data.d.ts +0 -3779
  154. package/dist/types/index.d.ts +0 -4
  155. package/dist/types/lang-cypher/ParserAdapter.d.ts +0 -14
  156. package/dist/types/lang-cypher/autocomplete.d.ts +0 -3
  157. package/dist/types/lang-cypher/lang-cypher.d.ts +0 -7
  158. package/dist/types/ndl-tokens-copy.d.ts +0 -379
  159. package/dist/types/neo4j-setup.d.ts +0 -2
  160. package/dist/types/themes.d.ts +0 -11
  161. package/dist/types/tsconfig.tsbuildinfo +0 -1
  162. package/src/e2e_tests/auto-completion.spec.tsx +0 -232
  163. package/src/e2e_tests/mock-data.ts +0 -4310
  164. package/src/e2e_tests/performance-test.spec.tsx +0 -71
  165. package/src/e2e_tests/syntax-validation.spec.tsx +0 -156
  166. package/src/lang-cypher/ParserAdapter.ts +0 -92
  167. package/src/lang-cypher/lang-cypher.ts +0 -32
  168. package/src/lang-cypher/syntax-validation.ts +0 -24
  169. package/src/ndl-tokens-copy.ts +0 -379
  170. /package/dist/{types/e2e_tests/auto-completion.spec.d.ts → src/CypherEditor.test.d.ts} +0 -0
  171. /package/dist/{types/e2e_tests/extra-keybindings.spec.d.ts → src/e2e_tests/autoCompletion.spec.d.ts} +0 -0
  172. /package/dist/{types/e2e_tests/history-navigation.spec.d.ts → src/e2e_tests/configuration.spec.d.ts} +0 -0
  173. /package/dist/{types/e2e_tests/performance-test.spec.d.ts → src/e2e_tests/debounce.spec.d.ts} +0 -0
  174. /package/dist/{types/e2e_tests/sanity-checks.spec.d.ts → src/e2e_tests/extraKeybindings.spec.d.ts} +0 -0
  175. /package/dist/{types/e2e_tests/syntax-highlighting.spec.d.ts → src/e2e_tests/historyNavigation.spec.d.ts} +0 -0
  176. /package/dist/{types/e2e_tests/syntax-validation.spec.d.ts → src/e2e_tests/sanityChecks.spec.d.ts} +0 -0
  177. /package/dist/{types/lang-cypher/contants.test.d.ts → src/e2e_tests/signatureHelp.spec.d.ts} +0 -0
  178. /package/dist/{types/ndl-tokens-copy.test.d.ts → src/e2e_tests/snippets.spec.d.ts} +0 -0
  179. /package/dist/{types/history-navigation.d.ts → src/historyNavigation.d.ts} +0 -0
  180. /package/dist/{types/lang-cypher/theme-icons.d.ts → src/lang-cypher/themeIcons.d.ts} +0 -0
  181. /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,16 @@
1
- import { CompletionSource } from '@codemirror/autocomplete';
2
- import { autocomplete } from '@neo4j-cypher/language-support';
3
- import { CompletionItemKind } from 'vscode-languageserver-types';
1
+ import {
2
+ Completion,
3
+ CompletionSource,
4
+ snippet,
5
+ } from '@codemirror/autocomplete';
6
+ import { shouldAutoCompleteYield } from '@neo4j-cypher/language-support';
7
+ import {
8
+ CompletionItemKind,
9
+ CompletionItemTag,
10
+ } from 'vscode-languageserver-types';
4
11
  import { CompletionItemIcons } from '../icons';
5
- import type { CypherConfig } from './lang-cypher';
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
- [CompletionItemKind.Event]: 'Event',
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,30 +45,101 @@ 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-element';
53
+ } else {
54
+ return null;
55
+ }
56
+ };
57
+
39
58
  export const cypherAutocomplete: (config: CypherConfig) => CompletionSource =
40
59
  (config) => (context) => {
41
- const textUntilCursor = context.state.doc.toString().slice(0, context.pos);
42
-
43
- const triggerCharacters = ['.', ':', '{', '$'];
44
- const lastCharacter = textUntilCursor.slice(-1);
45
-
60
+ const documentText = context.state.doc.toString();
61
+ const offset = context.pos;
62
+ const triggerCharacters = ['.', ':', '{', '$', ')', ']', '-', '<'];
63
+ const lastCharacter = documentText.at(offset - 1);
64
+ const yieldTriggered = shouldAutoCompleteYield(documentText, offset);
46
65
  const lastWord = context.matchBefore(/\w*/);
47
66
  const inWord = lastWord.from !== lastWord.to;
48
-
49
67
  const shouldTriggerCompletion =
50
- inWord || context.explicit || triggerCharacters.includes(lastCharacter);
68
+ inWord ||
69
+ context.explicit ||
70
+ triggerCharacters.includes(lastCharacter) ||
71
+ yieldTriggered;
72
+
73
+ if (config.useLightVersion && !context.explicit) {
74
+ return null;
75
+ }
51
76
 
52
77
  if (!shouldTriggerCompletion) {
53
78
  return null;
54
79
  }
55
80
 
56
- const options = autocomplete(textUntilCursor, config.schema ?? {});
81
+ const options = config.languageService.autocomplete(
82
+ documentText,
83
+ config.schema ?? {},
84
+ {
85
+ caretPosition: offset,
86
+ manual: context.explicit,
87
+ },
88
+ );
57
89
 
58
90
  return {
59
- from: context.matchBefore(/(\w|\$)*$/).from,
60
- options: options.map((o) => ({
61
- label: o.label,
62
- type: completionKindToCodemirrorIcon(o.kind),
63
- })),
91
+ from: context.matchBefore(/(\w)*$/).from,
92
+ options: options.map((o) => {
93
+ let maybeInfo = {};
94
+ let emptyInfo = true;
95
+ const newDiv = document.createElement('div');
96
+
97
+ if (o.signature) {
98
+ const header = document.createElement('p');
99
+ header.setAttribute('class', 'cm-completionInfo-signature');
100
+ header.textContent = o.signature;
101
+ if (header.textContent.length > 0) {
102
+ emptyInfo = false;
103
+ newDiv.appendChild(header);
104
+ }
105
+ }
106
+
107
+ if (o.documentation) {
108
+ const paragraph = document.createElement('p');
109
+ paragraph.textContent = getDocString(o.documentation);
110
+ if (paragraph.textContent.length > 0) {
111
+ emptyInfo = false;
112
+ newDiv.appendChild(paragraph);
113
+ }
114
+ }
115
+
116
+ if (!emptyInfo) {
117
+ maybeInfo = {
118
+ info: () => Promise.resolve(newDiv),
119
+ };
120
+ }
121
+ const deprecated =
122
+ o.tags?.find((tag) => tag === CompletionItemTag.Deprecated) ?? false;
123
+ // The negative boost moves the deprecation down the list
124
+ // so we offer the user the completions that are
125
+ // deprecated the last
126
+ const maybeDeprecated = deprecated
127
+ ? { boost: -99, deprecated: true }
128
+ : {};
129
+
130
+ return {
131
+ label: o.insertText ? o.insertText : o.label,
132
+ displayLabel: o.label,
133
+ type: completionKindToCodemirrorIcon(o.kind),
134
+ apply:
135
+ o.kind === CompletionItemKind.Snippet
136
+ ? // codemirror requires an empty snippet space to be able to tab out of the completion
137
+ snippet((o.insertText ?? o.label) + '${}')
138
+ : undefined,
139
+ detail: o.detail,
140
+ ...maybeDeprecated,
141
+ ...maybeInfo,
142
+ };
143
+ }),
64
144
  };
65
145
  };
@@ -31,8 +31,36 @@ 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' }),
43
+ setting: NodeType.define({ id: 29, name: 'setting' }),
44
+ settingValue: NodeType.define({ id: 30, name: 'settingValue' }),
45
+ interpolationDelimiter: NodeType.define({
46
+ id: 31,
47
+ name: 'interpolationDelimiter',
48
+ }),
34
49
  });
35
50
 
51
+ type PrismSpecificTokenType =
52
+ | 'class-name'
53
+ | 'identifier'
54
+ | 'string'
55
+ | 'relationship'
56
+ | 'boolean'
57
+ | 'number';
58
+
59
+ export type CodemirrorParseTokenType =
60
+ | CypherTokenType
61
+ | PrismSpecificTokenType
62
+ | 'topNode';
63
+
36
64
  export type HighlightedCypherTokenTypes = Exclude<CypherTokenType, 'none'>;
37
65
  export const tokenTypeToStyleTag: Record<HighlightedCypherTokenTypes, Tag> = {
38
66
  comment: tags.comment,
@@ -44,6 +72,7 @@ export const tokenTypeToStyleTag: Record<HighlightedCypherTokenTypes, Tag> = {
44
72
  variable: tags.variableName,
45
73
  paramDollar: tags.atom,
46
74
  paramValue: tags.atom,
75
+ interpolationDelimiter: tags.atom,
47
76
  symbolicName: tags.variableName,
48
77
  operator: tags.operator,
49
78
  stringLiteral: tags.string,
@@ -55,6 +84,9 @@ export const tokenTypeToStyleTag: Record<HighlightedCypherTokenTypes, Tag> = {
55
84
  bracket: tags.bracket,
56
85
  punctuation: tags.punctuation,
57
86
  separator: tags.separator,
87
+ consoleCommand: tags.macroName,
88
+ setting: tags.attributeName,
89
+ settingValue: tags.attributeValue,
58
90
  };
59
91
 
60
92
  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 = [
@@ -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,18 @@ import {
15
16
  replaceAllSvg,
16
17
  replaceSvg,
17
18
  upArrowSvg,
18
- } from './theme-icons';
19
+ } from './themeIcons';
20
+
21
+ export interface DiffColors {
22
+ /** Background fill for the whole inserted/changed line. */
23
+ insertedLine: string;
24
+ /** Highlight for the exact inserted text within a changed line. */
25
+ insertedText: string;
26
+ /** Background fill for the whole deleted line widget. */
27
+ deletedLine: string;
28
+ /** Highlight for the exact deleted text within a deleted chunk. */
29
+ deletedText: string;
30
+ }
19
31
 
20
32
  export interface ThemeOptions {
21
33
  dark: boolean;
@@ -39,6 +51,7 @@ export interface ThemeOptions {
39
51
  };
40
52
  highlightStyles: Partial<Record<HighlightedCypherTokenTypes, string>>;
41
53
  inheritBgColor?: boolean;
54
+ diffColors?: DiffColors;
42
55
  }
43
56
 
44
57
  export const createCypherTheme = ({
@@ -46,6 +59,7 @@ export const createCypherTheme = ({
46
59
  editorSettings: settings,
47
60
  highlightStyles,
48
61
  inheritBgColor,
62
+ diffColors,
49
63
  }: ThemeOptions): Extension => {
50
64
  const themeOptions: Record<string, StyleSpec> = {
51
65
  '&': {
@@ -53,6 +67,9 @@ export const createCypherTheme = ({
53
67
  color: settings.foreground,
54
68
  fontVariantLigatures: 'none',
55
69
  },
70
+ '& .cm-snippetField': {
71
+ backgroundColor: settings.autoCompletionPanel.selectedColor,
72
+ },
56
73
  '&.cm-focused': {
57
74
  outline: 'none',
58
75
  },
@@ -61,8 +78,9 @@ export const createCypherTheme = ({
61
78
  color: settings.gutterForeground,
62
79
  border: 'none',
63
80
  },
64
- '&.cm-editor .cm-scroller': {
81
+ '&.cm-editor': {
65
82
  fontFamily: 'Fira Code, Menlo, Monaco, Lucida Console, monospace',
83
+ height: '100%',
66
84
  },
67
85
  '.cm-content': {
68
86
  caretColor: settings.cursor,
@@ -80,9 +98,13 @@ export const createCypherTheme = ({
80
98
  '& .cm-selectionMatch': {
81
99
  backgroundColor: settings.textMatchingSelection,
82
100
  },
101
+ '& .cm-bold': {
102
+ fontWeight: 'bold',
103
+ },
83
104
  '& .cm-panels': {
84
105
  backgroundColor: settings.searchPanel.background,
85
106
  fontFamily: 'Fira Code, Menlo, Monaco, Lucida Console, monospace',
107
+ zIndex: 0,
86
108
  },
87
109
  '& .cm-completionLabel': {
88
110
  fontFamily: 'Fira Code, Menlo, Monaco, Lucida Console, monospace',
@@ -93,7 +115,40 @@ export const createCypherTheme = ({
93
115
  color: settings.autoCompletionPanel.matchingTextColor,
94
116
  textDecoration: 'none',
95
117
  },
118
+ '& .cm-signature-help-panel': {
119
+ backgroundColor: settings.autoCompletionPanel.backgroundColor,
120
+ maxWidth: '700px',
121
+ maxHeight: '250px',
122
+ fontFamily: 'Fira Code, Menlo, Monaco, Lucida Console, monospace',
123
+ },
124
+ '& .cm-signature-help-panel-contents': {
125
+ overflow: 'auto',
126
+ maxHeight: '250px',
127
+ },
128
+ '& .cm-signature-help-panel-current-argument': {
129
+ color: settings.autoCompletionPanel.matchingTextColor,
130
+ fontWeight: 'bold',
131
+ },
132
+ '& .cm-signature-help-panel-separator': {
133
+ borderBottom: '1px solid #ccc',
134
+ },
135
+ '& .cm-signature-help-panel-name': {
136
+ padding: '5px',
137
+ },
138
+ '& .cm-signature-help-panel-arg-description': {
139
+ padding: '5px',
140
+ },
141
+ '& .cm-signature-help-panel-description': {
142
+ padding: '5px',
143
+ },
144
+ '.cm-completionInfo-signature': {
145
+ color: 'darkgrey',
146
+ },
147
+ '.cm-deprecated-element': {
148
+ 'text-decoration': 'line-through',
149
+ },
96
150
  '.cm-tooltip-autocomplete': {
151
+ maxWidth: '430px',
97
152
  '& > ul > li[aria-selected]': {
98
153
  backgroundColor: settings.autoCompletionPanel.selectedColor,
99
154
  color: settings.foreground,
@@ -118,24 +173,16 @@ export const createCypherTheme = ({
118
173
  border: 'none',
119
174
  verticalAlign: 'middle',
120
175
  '&[name=next]::before': {
121
- content: `url("data:image/svg+xml;base64,${window.btoa(
122
- downArrowSvg,
123
- )}")`,
176
+ content: `url("data:image/svg+xml;base64,${window.btoa(downArrowSvg)}")`,
124
177
  },
125
178
  '&[name=prev]::before': {
126
- content: `url("data:image/svg+xml;base64,${window.btoa(
127
- upArrowSvg,
128
- )}")`,
179
+ content: `url("data:image/svg+xml;base64,${window.btoa(upArrowSvg)}")`,
129
180
  },
130
181
  '&[name=replace]::before': {
131
- content: `url("data:image/svg+xml;base64,${window.btoa(
132
- replaceSvg,
133
- )}")`,
182
+ content: `url("data:image/svg+xml;base64,${window.btoa(replaceSvg)}")`,
134
183
  },
135
184
  '&[name=replaceAll]::before': {
136
- content: `url("data:image/svg+xml;base64,${window.btoa(
137
- replaceAllSvg,
138
- )}")`,
185
+ content: `url("data:image/svg+xml;base64,${window.btoa(replaceAllSvg)}")`,
139
186
  },
140
187
  width: '20px',
141
188
  height: '20px',
@@ -167,19 +214,13 @@ export const createCypherTheme = ({
167
214
  borderRadius: '4px',
168
215
 
169
216
  '&[name=case]::before': {
170
- content: `url("data:image/svg+xml;base64,${window.btoa(
171
- caseSensitiveSvg,
172
- )}")`,
217
+ content: `url("data:image/svg+xml;base64,${window.btoa(caseSensitiveSvg)}")`,
173
218
  },
174
219
  '&[name=re]::before': {
175
- content: `url("data:image/svg+xml;base64,${window.btoa(
176
- regexSvg,
177
- )}")`,
220
+ content: `url("data:image/svg+xml;base64,${window.btoa(regexSvg)}")`,
178
221
  },
179
222
  '&[name=word]::before': {
180
- content: `url("data:image/svg+xml;base64,${window.btoa(
181
- byWordSvg,
182
- )}")`,
223
+ content: `url("data:image/svg+xml;base64,${window.btoa(byWordSvg)}")`,
183
224
  },
184
225
  '&:hover': {
185
226
  backgroundColor: settings.searchPanel.buttonHoverBackground,
@@ -190,6 +231,26 @@ export const createCypherTheme = ({
190
231
  },
191
232
  },
192
233
  },
234
+ ...(diffColors && {
235
+ '&.cm-merge-b .cm-changedLine': {
236
+ backgroundColor: diffColors.insertedLine,
237
+ },
238
+ '&.cm-merge-b .cm-changedText': {
239
+ background: diffColors.insertedText,
240
+ },
241
+ '&.cm-merge-b .cm-deletedChunk': {
242
+ backgroundColor: diffColors.deletedLine,
243
+ },
244
+ '&.cm-merge-b .cm-deletedChunk .cm-deletedText': {
245
+ background: diffColors.deletedText,
246
+ },
247
+ // Hide the empty deletion widget that unifiedMergeView renders
248
+ // when the original document is empty (its only line is an empty <del>).
249
+ '&.cm-merge-b .cm-deletedChunk:has(> .cm-deletedLine:only-child > del > br:only-child)':
250
+ {
251
+ display: 'none',
252
+ },
253
+ }),
193
254
  };
194
255
 
195
256
  const themeExtension = EditorView.theme(themeOptions, { dark });
@@ -198,6 +259,7 @@ export const createCypherTheme = ({
198
259
  ([token, color]: [HighlightedCypherTokenTypes, string]): TagStyle => ({
199
260
  tag: tokenTypeToStyleTag[token],
200
261
  color,
262
+ class: token === CypherTokenType.consoleCommand ? 'cm-bold' : undefined,
201
263
  }),
202
264
  );
203
265
  const highlightStyle = HighlightStyle.define(styles);
@@ -0,0 +1,46 @@
1
+ import { autocompletion } from '@codemirror/autocomplete';
2
+ import {
3
+ defineLanguageFacet,
4
+ Language,
5
+ LanguageSupport,
6
+ } from '@codemirror/language';
7
+ import {
8
+ CypherLanguageService,
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 } from './syntaxValidation';
15
+
16
+ const facet = defineLanguageFacet({
17
+ commentTokens: { block: { open: '/*', close: '*/' }, line: '//' },
18
+ closeBrackets: { brackets: ['(', '[', '{', "'", '"', '`'] },
19
+ });
20
+
21
+ export type CypherConfig = {
22
+ languageService: CypherLanguageService;
23
+ lint?: boolean;
24
+ showSignatureTooltipBelow?: boolean;
25
+ featureFlags?: {
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 parserAdapter = new ParserAdapter(facet, config);
35
+
36
+ const cypherLanguage = new Language(facet, parserAdapter, [], 'cypher');
37
+
38
+ return new LanguageSupport(cypherLanguage, [
39
+ autocompletion({
40
+ override: [cypherAutocomplete(config)],
41
+ optionClass: completionStyles,
42
+ }),
43
+ cypherLinter(config),
44
+ signatureHelpTooltip(config),
45
+ ]);
46
+ }