@neo4j-cypher/react-codemirror 2.0.0-next.40 → 2.0.0-next.42

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 (146) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/src/CypherEditor.d.ts +12 -6
  3. package/dist/src/CypherEditor.js +11 -3
  4. package/dist/src/CypherEditor.js.map +1 -1
  5. package/dist/src/CypherEditor.test.js +20 -11
  6. package/dist/src/CypherEditor.test.js.map +1 -1
  7. package/dist/src/editorActions.js +83 -18
  8. package/dist/src/editorActions.js.map +1 -1
  9. package/dist/src/historyNavigation.d.ts +1 -1
  10. package/dist/src/historyNavigation.js.map +1 -1
  11. package/dist/src/index.d.ts +8 -3
  12. package/dist/src/index.js +3 -2
  13. package/dist/src/index.js.map +1 -1
  14. package/dist/src/lang-cypher/autocomplete.d.ts +1 -1
  15. package/dist/src/lang-cypher/autocomplete.js +1 -1
  16. package/dist/src/lang-cypher/autocomplete.js.map +1 -1
  17. package/dist/src/lang-cypher/constants.d.ts +2 -2
  18. package/dist/src/lang-cypher/constants.js.map +1 -1
  19. package/dist/src/lang-cypher/createCypherTheme.d.ts +2 -2
  20. package/dist/src/lang-cypher/createCypherTheme.js +47 -1
  21. package/dist/src/lang-cypher/createCypherTheme.js.map +1 -1
  22. package/dist/src/lang-cypher/hover/hover.d.ts +3 -0
  23. package/dist/src/lang-cypher/hover/hover.js +26 -0
  24. package/dist/src/lang-cypher/hover/hover.js.map +1 -0
  25. package/dist/src/lang-cypher/hover/hoverRender.d.ts +9 -0
  26. package/dist/src/lang-cypher/hover/hoverRender.js +142 -0
  27. package/dist/src/lang-cypher/hover/hoverRender.js.map +1 -0
  28. package/dist/src/lang-cypher/hover/hoverRender.test.js +123 -0
  29. package/dist/src/lang-cypher/hover/hoverRender.test.js.map +1 -0
  30. package/dist/src/lang-cypher/langCypher.d.ts +2 -1
  31. package/dist/src/lang-cypher/langCypher.js +3 -0
  32. package/dist/src/lang-cypher/langCypher.js.map +1 -1
  33. package/dist/src/lang-cypher/lintWorker.mjs +168 -166
  34. package/dist/src/lang-cypher/parser-adapter.d.ts +6 -4
  35. package/dist/src/lang-cypher/parser-adapter.js +4 -1
  36. package/dist/src/lang-cypher/parser-adapter.js.map +1 -1
  37. package/dist/src/lang-cypher/prism-global.js +7 -0
  38. package/dist/src/lang-cypher/prism-global.js.map +1 -0
  39. package/dist/src/lang-cypher/signatureHelp.d.ts +2 -2
  40. package/dist/src/lang-cypher/signatureHelp.js +14 -24
  41. package/dist/src/lang-cypher/signatureHelp.js.map +1 -1
  42. package/dist/src/lang-cypher/syntaxValidation.d.ts +1 -1
  43. package/dist/src/lang-cypher/syntaxValidation.js +7 -2
  44. package/dist/src/lang-cypher/syntaxValidation.js.map +1 -1
  45. package/dist/src/lang-cypher/tooltipHighlighting.d.ts +21 -0
  46. package/dist/src/lang-cypher/tooltipHighlighting.js +76 -0
  47. package/dist/src/lang-cypher/tooltipHighlighting.js.map +1 -0
  48. package/dist/src/lang-cypher/tooltipHighlighting.test.js +63 -0
  49. package/dist/src/lang-cypher/tooltipHighlighting.test.js.map +1 -0
  50. package/dist/src/neo4jSetup.d.ts +1 -1
  51. package/dist/src/neo4jSetup.js.map +1 -1
  52. package/dist/src/richClipboardCopier.js +2 -2
  53. package/dist/src/richClipboardCopier.js.map +1 -1
  54. package/dist/src/richClipboardCopier.test.js.map +1 -1
  55. package/dist/src/{e2e_tests → tests/e2e}/autoCompletion.spec.js +1 -2
  56. package/dist/src/tests/e2e/autoCompletion.spec.js.map +1 -0
  57. package/dist/src/{e2e_tests → tests/e2e}/configuration.spec.js +1 -1
  58. package/dist/src/tests/e2e/configuration.spec.js.map +1 -0
  59. package/dist/src/{e2e_tests → tests/e2e}/debounce.spec.js +2 -2
  60. package/dist/src/tests/e2e/debounce.spec.js.map +1 -0
  61. package/dist/src/tests/e2e/e2eUtils.js.map +1 -0
  62. package/dist/src/{e2e_tests → tests/e2e}/extraKeybindings.spec.js +1 -1
  63. package/dist/src/tests/e2e/extraKeybindings.spec.js.map +1 -0
  64. package/dist/src/{e2e_tests → tests/e2e}/historyNavigation.spec.js +1 -1
  65. package/dist/src/tests/e2e/historyNavigation.spec.js.map +1 -0
  66. package/dist/src/{e2e_tests → tests/e2e}/sanityChecks.spec.js +1 -1
  67. package/dist/src/tests/e2e/sanityChecks.spec.js.map +1 -0
  68. package/dist/src/{e2e_tests → tests/e2e}/signatureHelp.spec.js +1 -2
  69. package/dist/src/tests/e2e/signatureHelp.spec.js.map +1 -0
  70. package/dist/src/tests/e2e/snippets.spec.d.ts +1 -0
  71. package/dist/src/{e2e_tests → tests/e2e}/snippets.spec.js +1 -1
  72. package/dist/src/tests/e2e/snippets.spec.js.map +1 -0
  73. package/dist/src/tests/e2e/syntaxHighlighting.spec.d.ts +1 -0
  74. package/dist/src/{e2e_tests → tests/e2e}/syntaxHighlighting.spec.js +2 -2
  75. package/dist/src/tests/e2e/syntaxHighlighting.spec.js.map +1 -0
  76. package/dist/src/tests/e2e/syntaxValidation.spec.d.ts +1 -0
  77. package/dist/src/{e2e_tests → tests/e2e}/syntaxValidation.spec.js +1 -1
  78. package/dist/src/tests/e2e/syntaxValidation.spec.js.map +1 -0
  79. package/dist/src/themes.d.ts +2 -2
  80. package/dist/src/themes.js.map +1 -1
  81. package/dist/tsconfig.tsbuildinfo +1 -1
  82. package/dist/vitest.config.d.mts +2 -0
  83. package/dist/vitest.config.mjs +16 -0
  84. package/dist/vitest.config.mjs.map +1 -0
  85. package/package.json +15 -12
  86. package/src/CypherEditor.test.tsx +28 -19
  87. package/src/CypherEditor.tsx +33 -19
  88. package/src/editorActions.ts +103 -22
  89. package/src/historyNavigation.ts +5 -3
  90. package/src/index.ts +17 -3
  91. package/src/lang-cypher/autocomplete.ts +3 -6
  92. package/src/lang-cypher/constants.ts +3 -2
  93. package/src/lang-cypher/createCypherTheme.ts +59 -10
  94. package/src/lang-cypher/hover/hover.ts +32 -0
  95. package/src/lang-cypher/hover/hoverRender.test.ts +176 -0
  96. package/src/lang-cypher/hover/hoverRender.ts +198 -0
  97. package/src/lang-cypher/langCypher.ts +5 -4
  98. package/src/lang-cypher/lintWorker.mjs +168 -166
  99. package/src/lang-cypher/parser-adapter.ts +10 -9
  100. package/src/lang-cypher/prism-global.ts +7 -0
  101. package/src/lang-cypher/signatureHelp.ts +26 -31
  102. package/src/lang-cypher/syntaxValidation.ts +9 -4
  103. package/src/lang-cypher/tooltipHighlighting.test.ts +78 -0
  104. package/src/lang-cypher/tooltipHighlighting.ts +119 -0
  105. package/src/neo4jSetup.tsx +3 -3
  106. package/src/richClipboardCopier.test.ts +7 -5
  107. package/src/richClipboardCopier.ts +2 -2
  108. package/src/{e2e_tests → tests/e2e}/autoCompletion.spec.tsx +1 -2
  109. package/src/{e2e_tests → tests/e2e}/configuration.spec.tsx +1 -1
  110. package/src/{e2e_tests → tests/e2e}/debounce.spec.tsx +2 -2
  111. package/src/{e2e_tests → tests/e2e}/extraKeybindings.spec.tsx +1 -1
  112. package/src/{e2e_tests → tests/e2e}/historyNavigation.spec.tsx +2 -2
  113. package/src/{e2e_tests → tests/e2e}/sanityChecks.spec.tsx +1 -1
  114. package/src/{e2e_tests → tests/e2e}/signatureHelp.spec.tsx +2 -3
  115. package/src/{e2e_tests → tests/e2e}/snippets.spec.tsx +1 -1
  116. package/src/{e2e_tests → tests/e2e}/syntaxHighlighting.spec.tsx +2 -2
  117. package/src/{e2e_tests → tests/e2e}/syntaxValidation.spec.tsx +1 -1
  118. package/src/themes.ts +2 -2
  119. package/dist/src/e2e_tests/autoCompletion.spec.js.map +0 -1
  120. package/dist/src/e2e_tests/configuration.spec.js.map +0 -1
  121. package/dist/src/e2e_tests/debounce.spec.js.map +0 -1
  122. package/dist/src/e2e_tests/e2eUtils.js.map +0 -1
  123. package/dist/src/e2e_tests/extraKeybindings.spec.js.map +0 -1
  124. package/dist/src/e2e_tests/historyNavigation.spec.js.map +0 -1
  125. package/dist/src/e2e_tests/performanceTest.spec.d.ts +0 -6
  126. package/dist/src/e2e_tests/performanceTest.spec.js +0 -97
  127. package/dist/src/e2e_tests/performanceTest.spec.js.map +0 -1
  128. package/dist/src/e2e_tests/sanityChecks.spec.js.map +0 -1
  129. package/dist/src/e2e_tests/signatureHelp.spec.js.map +0 -1
  130. package/dist/src/e2e_tests/snippets.spec.js.map +0 -1
  131. package/dist/src/e2e_tests/syntaxHighlighting.spec.js.map +0 -1
  132. package/dist/src/e2e_tests/syntaxValidation.spec.js.map +0 -1
  133. package/src/e2e_tests/performanceTest.spec.tsx +0 -163
  134. /package/dist/src/{e2e_tests/autoCompletion.spec.d.ts → lang-cypher/hover/hoverRender.test.d.ts} +0 -0
  135. /package/dist/src/{e2e_tests/configuration.spec.d.ts → lang-cypher/prism-global.d.ts} +0 -0
  136. /package/dist/src/{e2e_tests/debounce.spec.d.ts → lang-cypher/tooltipHighlighting.test.d.ts} +0 -0
  137. /package/dist/src/{e2e_tests/extraKeybindings.spec.d.ts → tests/e2e/autoCompletion.spec.d.ts} +0 -0
  138. /package/dist/src/{e2e_tests/historyNavigation.spec.d.ts → tests/e2e/configuration.spec.d.ts} +0 -0
  139. /package/dist/src/{e2e_tests/sanityChecks.spec.d.ts → tests/e2e/debounce.spec.d.ts} +0 -0
  140. /package/dist/src/{e2e_tests → tests/e2e}/e2eUtils.d.ts +0 -0
  141. /package/dist/src/{e2e_tests → tests/e2e}/e2eUtils.js +0 -0
  142. /package/dist/src/{e2e_tests/signatureHelp.spec.d.ts → tests/e2e/extraKeybindings.spec.d.ts} +0 -0
  143. /package/dist/src/{e2e_tests/snippets.spec.d.ts → tests/e2e/historyNavigation.spec.d.ts} +0 -0
  144. /package/dist/src/{e2e_tests/syntaxHighlighting.spec.d.ts → tests/e2e/sanityChecks.spec.d.ts} +0 -0
  145. /package/dist/src/{e2e_tests/syntaxValidation.spec.d.ts → tests/e2e/signatureHelp.spec.d.ts} +0 -0
  146. /package/src/{e2e_tests → tests/e2e}/e2eUtils.ts +0 -0
@@ -0,0 +1,2 @@
1
+ declare const _default: import("vitest/config").UserWorkspaceConfig;
2
+ export default _default;
@@ -0,0 +1,16 @@
1
+ import { defineProject } from 'vitest/config';
2
+ export default defineProject({
3
+ test: {
4
+ globals: true,
5
+ projects: [
6
+ {
7
+ test: {
8
+ name: 'unit',
9
+ include: ['src/**/*.test.{ts,tsx}'],
10
+ exclude: ['src/tests/e2e/**'],
11
+ },
12
+ },
13
+ ],
14
+ },
15
+ });
16
+ //# sourceMappingURL=vitest.config.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vitest.config.mjs","sourceRoot":"","sources":["../vitest.config.mts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAE9C,eAAe,aAAa,CAAC;IAC3B,IAAI,EAAE;QACJ,OAAO,EAAE,IAAI;QAEb,QAAQ,EAAE;YACR;gBACE,IAAI,EAAE;oBACJ,IAAI,EAAE,MAAM;oBACZ,OAAO,EAAE,CAAC,wBAAwB,CAAC;oBACnC,OAAO,EAAE,CAAC,kBAAkB,CAAC;iBAC9B;aACF;SACF;KACF;CACF,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neo4j-cypher/react-codemirror",
3
- "version": "2.0.0-next.40",
3
+ "version": "2.0.0-next.42",
4
4
  "keywords": [
5
5
  "codemirror",
6
6
  "codemirror 6",
@@ -26,7 +26,9 @@
26
26
  "CHANGELOG.md"
27
27
  ],
28
28
  "type": "module",
29
- "sideEffects": false,
29
+ "sideEffects": [
30
+ "**/lang-cypher/prism-global.js"
31
+ ],
30
32
  "main": "./dist/src/index.js",
31
33
  "types": "./dist/src/index.d.ts",
32
34
  "dependencies": {
@@ -46,23 +48,23 @@
46
48
  "style-mod": "^4.1.2",
47
49
  "vscode-languageserver-types": "^3.17.3",
48
50
  "workerpool": "^9.3.3",
49
- "@neo4j-cypher/language-support": "2.0.0-next.37",
50
- "@neo4j-cypher/lint-worker": "1.10.1-next.14"
51
+ "@neo4j-cypher/language-support": "2.0.0-next.39",
52
+ "@neo4j-cypher/lint-worker": "1.10.1-next.16"
51
53
  },
52
54
  "devDependencies": {
53
55
  "@neo4j-ndl/base": "^3.2.10",
54
- "@playwright/experimental-ct-react": "^1.55.1",
55
- "@playwright/test": "^1.55.1",
56
+ "@playwright/experimental-ct-react": "1.62.1",
57
+ "@playwright/test": "1.62.1",
56
58
  "@types/lodash.debounce": "^4.0.9",
57
59
  "@types/react": "^18.0.28",
58
60
  "@types/react-dom": "^18.0.11",
59
- "@vitejs/plugin-react": "^4.3.1",
61
+ "@vitejs/plugin-react": "^6.1.1",
60
62
  "copyfiles": "^2.4.1",
61
63
  "jsdom": "^24.1.1",
62
- "playwright": "^1.55.1",
64
+ "playwright": "1.62.1",
63
65
  "react": "^18.2.0",
64
66
  "react-dom": "^18.2.0",
65
- "vite": "^4.5.10"
67
+ "vite": "^8.2.2"
66
68
  },
67
69
  "peerDependencies": {
68
70
  "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
@@ -75,10 +77,11 @@
75
77
  "dev": "tsc --watch",
76
78
  "build": "pnpm copy-lint-worker && tsc --declaration --outDir dist/",
77
79
  "copy-lint-worker": "copyfiles -u 4 ../lint-worker/dist/esm/lintWorker.mjs dist/src/lang-cypher && copyfiles -u 4 ../lint-worker/dist/esm/lintWorker.mjs src/lang-cypher",
78
- "clean": "rm -rf {dist,tsconfig.tsbuildinfo}",
80
+ "check-api-report": "tsx checkApiReport.ts",
81
+ "gen-api-report": "pnpm build && pnpm check-api-report",
82
+ "clean": "rm -rf dist tsconfig*.tsbuildinfo",
79
83
  "test": "vitest run",
80
84
  "test:e2e": "playwright test -c playwright-ct.config.ts",
81
- "test:e2e-ui": "playwright test -c playwright-ct.config.ts --ui",
82
- "benchmark": "BENCHMARKING=true playwright test -c playwright-ct.config.ts -g \"benchmarking & performance test session\""
85
+ "test:e2e-ui": "playwright test -c playwright-ct.config.ts --ui"
83
86
  }
84
87
  }
@@ -1,6 +1,6 @@
1
1
  // @vitest-environment jsdom
2
2
 
3
- import { EditorView } from '@codemirror/view';
3
+ import type { EditorView } from '@codemirror/view';
4
4
  import { createRef } from 'react';
5
5
  import { createRoot } from 'react-dom/client';
6
6
  import { act } from 'react-dom/test-utils';
@@ -13,22 +13,25 @@ let root: ReturnType<typeof createRoot>;
13
13
 
14
14
  const ref = createRef<CypherEditor>();
15
15
  let value = '';
16
- const onChange = vi.fn((v: string) => {
16
+ const onChange = vi.fn<(v: string) => void>((v) => {
17
17
  value = v;
18
18
  rerender();
19
19
  });
20
20
 
21
- global.IS_REACT_ACT_ENVIRONMENT = true;
21
+ // oxlint-disable-next-line typescript/no-explicit-any
22
+ (global as any).IS_REACT_ACT_ENVIRONMENT = true;
22
23
  const DEBOUNCE_TIME_WITH_MARGIN = DEBOUNCE_TIME + 100;
23
24
 
24
25
  /** Avoids crash in test environment */
25
26
  function mockEditorView(editorView: EditorView) {
26
- editorView.coordsAtPos = vi.fn().mockReturnValue({
27
- left: 0,
28
- top: 0,
29
- right: 0,
30
- bottom: 0,
31
- });
27
+ editorView.coordsAtPos = vi
28
+ .fn<() => { left: number; top: number; right: number; bottom: number }>()
29
+ .mockReturnValue({
30
+ left: 0,
31
+ top: 0,
32
+ right: 0,
33
+ bottom: 0,
34
+ });
32
35
  }
33
36
 
34
37
  async function debounce() {
@@ -107,24 +110,28 @@ test('props.value set to undefined preserves editorValue', () => {
107
110
  expect(getEditorValue()).toBe('initial');
108
111
  });
109
112
 
110
- // value updates from outside onExecute are overwritten by pending updates
111
- test.fails('new props.value should cancel onChange', async () => {
113
+ // This test documents unexpected behaviour:
114
+ // value updates (from outside onExecute) are overwritten by pending updates.
115
+ test('new props.value should cancel onChange', async () => {
112
116
  // 1. value is updated internally
113
117
  ref.current.setValueAndFocus('update');
114
118
 
115
119
  // 2. editor is rerendered with a new value while a value update is still pending
120
+ // the update to 'new external value' is overwritten by the pending update to 'update'
116
121
  value = 'new external value';
117
122
  rerender();
118
123
 
119
124
  await debounce();
120
125
 
121
- // expect(onChange).not.toHaveBeenCalled();
122
- expect(getEditorValue()).toBe('new external value');
123
- expect(value).toBe('new external value');
126
+ // NOTE: These assertions currently check for the WRONG behavior (they are inverted).
127
+ expect(onChange).toHaveBeenCalled();
128
+ expect(getEditorValue()).not.toBe('new external value');
129
+ expect(value).not.toBe('new external value');
124
130
  });
125
131
 
126
- // value updates from outside onExecute are overwritten by pending updates
127
- test.fails('new props.value set to same value should cancel onChange', async () => {
132
+ // This test documents unexpected behaviour:
133
+ // value updates (from outside onExecute) are overwritten by pending updates
134
+ test('new props.value set to same value should cancel onChange', async () => {
128
135
  // 1. value is set initially
129
136
  value = 'same value';
130
137
  rerender();
@@ -133,14 +140,16 @@ test.fails('new props.value set to same value should cancel onChange', async ()
133
140
  ref.current.setValueAndFocus('update');
134
141
 
135
142
  // 3. editor is rerendered with a new value while a value update is still pending
143
+ // the update to 'same value' is overwritten by the pending update to 'update'
136
144
  value = 'same value';
137
145
  rerender();
138
146
 
139
147
  await debounce();
140
148
 
141
- // expect(onChange).not.toHaveBeenCalled();
142
- expect(getEditorValue()).toBe('same value');
143
- expect(value).toBe('same value');
149
+ // NOTE: These assertions currently check for the WRONG behavior (they are inverted).
150
+ expect(onChange).toHaveBeenCalled();
151
+ expect(getEditorValue()).not.toBe('same value');
152
+ expect(value).not.toBe('same value');
144
153
  });
145
154
 
146
155
  test('rerender should not cancel onChange', async () => {
@@ -1,18 +1,8 @@
1
1
  import { insertNewline } from '@codemirror/commands';
2
- import {
3
- Annotation,
4
- Compartment,
5
- EditorState,
6
- Extension,
7
- } from '@codemirror/state';
8
- import {
9
- EditorView,
10
- KeyBinding,
11
- keymap,
12
- lineNumbers,
13
- placeholder,
14
- ViewUpdate,
15
- } from '@codemirror/view';
2
+ import type { Extension } from '@codemirror/state';
3
+ import { Annotation, Compartment, EditorState } from '@codemirror/state';
4
+ import type { KeyBinding, ViewUpdate } from '@codemirror/view';
5
+ import { EditorView, keymap, lineNumbers, placeholder } from '@codemirror/view';
16
6
  import {
17
7
  createInlinePanelController,
18
8
  type InlinePanelCallbacks,
@@ -37,15 +27,18 @@ import {
37
27
  replaceHistory,
38
28
  replMode as historyNavigation,
39
29
  } from './historyNavigation';
40
- import { cypher, CypherConfig } from './lang-cypher/langCypher';
30
+ import type { CypherConfig } from './lang-cypher/langCypher';
31
+ import { cypher } from './lang-cypher/langCypher';
41
32
  import { cleanupWorkers } from './lang-cypher/syntaxValidation';
42
33
  import { basicNeo4jSetup } from './neo4jSetup';
43
34
  import { getThemeExtension } from './themes';
44
35
  import { richClipboardCopier } from './richClipboardCopier';
45
- import { LintWorker } from '@neo4j-cypher/lint-worker';
36
+ import type { LintWorker } from '@neo4j-cypher/lint-worker';
46
37
  import workerpool from 'workerpool';
47
38
 
48
- type DomEventHandlers = Parameters<typeof EditorView.domEventHandlers>[0];
39
+ export type DomEventHandlers = Parameters<
40
+ typeof EditorView.domEventHandlers
41
+ >[0];
49
42
 
50
43
  /**
51
44
  * Normalize CRLF line endings to LF, which is what CodeMirror expects.
@@ -196,6 +189,12 @@ export interface CypherEditorProps {
196
189
  */
197
190
  ariaLabel?: string;
198
191
 
192
+ /**
193
+ * Extra attributes to set on the editable content element, e.g. `aria-invalid` and
194
+ * `aria-describedby` to associate the editor with an error message rendered outside of it.
195
+ */
196
+ contentAttributes?: Record<string, string>;
197
+
199
198
  /**
200
199
  * Whether keybindings for inserting indents with the Tab key should be disabled.
201
200
  *
@@ -323,6 +322,7 @@ const readOnlyCompartment = new Compartment();
323
322
  const placeholderCompartment = new Compartment();
324
323
  const domEventHandlerCompartment = new Compartment();
325
324
  const diffCompartment = new Compartment();
325
+ const contentAttributesCompartment = new Compartment();
326
326
 
327
327
  const formatLineNumber =
328
328
  (prompt?: string) => (a: number, state: EditorState) => {
@@ -333,7 +333,7 @@ const formatLineNumber =
333
333
  return a.toString();
334
334
  };
335
335
 
336
- type CypherEditorState = {
336
+ export type CypherEditorState = {
337
337
  editorActionsContainer: HTMLElement | null;
338
338
  };
339
339
 
@@ -341,7 +341,7 @@ const ExternalEdit = Annotation.define<boolean>();
341
341
  const WorkerURL = new URL('./lang-cypher/lintWorker.mjs', import.meta.url)
342
342
  .pathname;
343
343
 
344
- class CodemirrorSymbolFetcher {
344
+ export class CodemirrorSymbolFetcher {
345
345
  constructor(languageService: CypherLanguageService) {
346
346
  this.languageService = languageService;
347
347
  }
@@ -610,6 +610,9 @@ export class CypherEditor extends Component<
610
610
  'aria-label': this.props.ariaLabel,
611
611
  })
612
612
  : [],
613
+ contentAttributesCompartment.of(
614
+ EditorView.contentAttributes.of(this.props.contentAttributes ?? {}),
615
+ ),
613
616
  !this.props.moveFocusOnTab
614
617
  ? EditorView.contentAttributes.of({
615
618
  'aria-description':
@@ -756,6 +759,17 @@ export class CypherEditor extends Component<
756
759
  });
757
760
  }
758
761
 
762
+ if (
763
+ JSON.stringify(prevProps.contentAttributes) !==
764
+ JSON.stringify(this.props.contentAttributes)
765
+ ) {
766
+ this.editorView.current.dispatch({
767
+ effects: contentAttributesCompartment.reconfigure(
768
+ EditorView.contentAttributes.of(this.props.contentAttributes ?? {}),
769
+ ),
770
+ });
771
+ }
772
+
759
773
  if (prevProps.readonly !== this.props.readonly) {
760
774
  this.editorView.current.dispatch({
761
775
  effects: readOnlyCompartment.reconfigure(
@@ -2,6 +2,7 @@ import { StateEffect, StateField, type Extension } from '@codemirror/state';
2
2
  import {
3
3
  Decoration,
4
4
  EditorView,
5
+ type Rect,
5
6
  ViewPlugin,
6
7
  type ViewUpdate,
7
8
  WidgetType,
@@ -40,10 +41,23 @@ export type EditorActionsController = {
40
41
  * `absolute` on the editor element (which does not scroll). It never moves
41
42
  * when the document grows, when an inline panel opens above the first line,
42
43
  * or when the content scrolls — content simply scrolls behind it.
44
+ *
45
+ * The overlay is measured against the scroller's visible box rather than
46
+ * against the spacer or the content: the spacer moves down the document
47
+ * whenever something renders above the first line (an inline panel, the
48
+ * deleted lines of a diff), and the content reaches past the right edge as
49
+ * soon as it overflows.
50
+ * Vertically the cluster is centred on the first line.
43
51
  */
44
52
 
45
53
  const SPACER_BOTTOM_GAP = 2;
46
54
  const SPACER_LEFT_GAP = 4;
55
+ /**
56
+ * Floor for the overlay's inset from the visible right edge, so it stays off
57
+ * the content's last pixel even if a theme zeroes `.cm-line`'s right padding
58
+ * (the base theme's 2px, which is what normally provides the inset).
59
+ */
60
+ const MIN_OVERLAY_RIGHT_INSET = 2;
47
61
 
48
62
  export function createEditorActionsController(): EditorActionsController {
49
63
  const setActiveEffect = StateEffect.define<boolean>();
@@ -74,6 +88,38 @@ export function createEditorActionsController(): EditorActionsController {
74
88
  return true;
75
89
  }
76
90
 
91
+ /**
92
+ * The spacer floats right, so CodeMirror's default — the widget's own box —
93
+ * puts the caret at the right edge of the cluster whenever the spacer is
94
+ * the only thing it can measure on the first line (an empty document).
95
+ * Report the start of the line's inline content instead: the node after
96
+ * the spacer (text, a placeholder, or CodeMirror's trailing `<br>`).
97
+ */
98
+ coordsAt(dom: HTMLElement): Rect | null {
99
+ const next = dom.nextSibling;
100
+ if (!next) {
101
+ return null;
102
+ }
103
+ let rects: DOMRectList;
104
+ if (next instanceof Element) {
105
+ rects = next.getClientRects();
106
+ } else {
107
+ const range = document.createRange();
108
+ range.selectNodeContents(next);
109
+ rects = range.getClientRects();
110
+ }
111
+ const rect = rects[0];
112
+ if (!rect) {
113
+ return null;
114
+ }
115
+ return {
116
+ left: rect.left,
117
+ right: rect.left,
118
+ top: rect.top,
119
+ bottom: rect.bottom,
120
+ };
121
+ }
122
+
77
123
  ignoreEvent(): boolean {
78
124
  return true;
79
125
  }
@@ -136,13 +182,11 @@ export function createEditorActionsController(): EditorActionsController {
136
182
  `${container.offsetWidth}px`,
137
183
  );
138
184
  view.dom.style.setProperty(
139
- '--cm-editor-actions-height',
140
- `${container.offsetHeight}px`,
141
- );
142
- view.dom.style.setProperty(
143
- '--cm-editor-actions-content-min-height',
144
- `${container.offsetHeight + SPACER_BOTTOM_GAP}px`,
185
+ '--cm-editor-actions-reserved-width',
186
+ `${container.offsetWidth + SPACER_LEFT_GAP}px`,
145
187
  );
188
+ // The spacer's height follows the overlay's position, so `align`
189
+ // owns it.
146
190
  this.align(view);
147
191
  });
148
192
  this.resizeObserver.observe(container);
@@ -153,28 +197,52 @@ export function createEditorActionsController(): EditorActionsController {
153
197
  }
154
198
 
155
199
  private align(view: EditorView): void {
156
- view.requestMeasure<{ top: number; right: number } | null>({
200
+ view.requestMeasure<{
201
+ top: number;
202
+ right: number;
203
+ spacerHeight: number;
204
+ contentMinHeight: number;
205
+ } | null>({
157
206
  read: () => {
158
207
  if (!this.dom) {
159
208
  return null;
160
209
  }
161
210
  const editorRect = view.dom.getBoundingClientRect();
162
- const spacer = view.dom.querySelector('.cm-editor-actions-spacer');
163
- let topClient: number;
164
- let rightClient: number;
165
- if (spacer) {
166
- const rect = spacer.getBoundingClientRect();
167
- topClient = rect.top;
168
- rightClient = rect.right;
169
- } else {
170
- const rect = view.contentDOM.getBoundingClientRect();
171
- const style = window.getComputedStyle(view.contentDOM);
172
- topClient = rect.top + (parseFloat(style.paddingTop) || 0);
173
- rightClient = rect.right - (parseFloat(style.paddingRight) || 0);
174
- }
211
+ const scrollerRect = view.scrollDOM.getBoundingClientRect();
212
+ const style = window.getComputedStyle(view.contentDOM);
213
+ const visibleRight = scrollerRect.left + view.scrollDOM.clientWidth;
214
+ // Match the inset `.cm-line`'s right padding gives the spacer, so
215
+ // the overlay lands on the space the spacer reserves and stays off
216
+ // `contentRect.right - 1`, the pixel CodeMirror hit-tests to
217
+ // measure a wrapped line's selection. Cover it and the first
218
+ // line's highlight collapses to zero width — hence the floor.
219
+ const line = view.contentDOM.querySelector('.cm-line');
220
+ const lineRightPadding = line
221
+ ? parseFloat(window.getComputedStyle(line).paddingRight) || 0
222
+ : 0;
223
+
224
+ const contentPaddingTop = parseFloat(style.paddingTop) || 0;
225
+ const height = this.dom.offsetHeight;
226
+ // How far the cluster has to rise above the first line's top to
227
+ // sit centred on it, limited to the padding it can rise into.
228
+ const lift = Math.min(
229
+ Math.max((height - view.defaultLineHeight) / 2, 0),
230
+ contentPaddingTop,
231
+ );
232
+
175
233
  return {
176
- top: topClient - editorRect.top + view.scrollDOM.scrollTop,
177
- right: editorRect.right - rightClient - view.scrollDOM.scrollLeft,
234
+ top: scrollerRect.top - editorRect.top + contentPaddingTop - lift,
235
+ right:
236
+ editorRect.right -
237
+ visibleRight +
238
+ (parseFloat(style.paddingRight) || 0) +
239
+ Math.max(lineRightPadding, MIN_OVERLAY_RIGHT_INSET),
240
+ // The spacer starts at the first line's top, so it only needs to
241
+ // clear the part of the cluster that hangs below it.
242
+ spacerHeight: height - lift,
243
+ // A floor under the content, so an empty document is still tall
244
+ // enough to hold the overlay.
245
+ contentMinHeight: contentPaddingTop - lift + height,
178
246
  };
179
247
  },
180
248
  write: (pos) => {
@@ -189,6 +257,14 @@ export function createEditorActionsController(): EditorActionsController {
189
257
  '--cm-editor-actions-right',
190
258
  `${pos.right}px`,
191
259
  );
260
+ view.dom.style.setProperty(
261
+ '--cm-editor-actions-height',
262
+ `${pos.spacerHeight}px`,
263
+ );
264
+ view.dom.style.setProperty(
265
+ '--cm-editor-actions-content-min-height',
266
+ `${pos.contentMinHeight}px`,
267
+ );
192
268
  },
193
269
  });
194
270
  }
@@ -206,6 +282,7 @@ export function createEditorActionsController(): EditorActionsController {
206
282
  root.style.removeProperty('--cm-editor-actions-width');
207
283
  root.style.removeProperty('--cm-editor-actions-height');
208
284
  root.style.removeProperty('--cm-editor-actions-content-min-height');
285
+ root.style.removeProperty('--cm-editor-actions-reserved-width');
209
286
  root.style.removeProperty('--cm-editor-actions-top');
210
287
  root.style.removeProperty('--cm-editor-actions-right');
211
288
  }
@@ -230,6 +307,10 @@ export function createEditorActionsController(): EditorActionsController {
230
307
  WebkitUserSelect: 'none',
231
308
  pointerEvents: 'none',
232
309
  },
310
+ // The spacer only clears the first line, so a panel above it makes its own room.
311
+ '.cm-inline-panel': {
312
+ paddingRight: 'var(--cm-editor-actions-reserved-width, 0px)',
313
+ },
233
314
  '.cm-placeholder': {
234
315
  display: 'inline',
235
316
  },
@@ -1,5 +1,7 @@
1
- import { Extension, StateEffect } from '@codemirror/state';
2
- import { EditorView, keymap } from '@codemirror/view';
1
+ import type { Extension } from '@codemirror/state';
2
+ import { StateEffect } from '@codemirror/state';
3
+ import type { EditorView } from '@codemirror/view';
4
+ import { keymap } from '@codemirror/view';
3
5
 
4
6
  import { StateField } from '@codemirror/state';
5
7
 
@@ -10,7 +12,7 @@ type HistoryState = {
10
12
  documentUpdate: string | null;
11
13
  };
12
14
  const DRAFT_ENTRY_INDEX = -1;
13
- const historyInitialState = {
15
+ const historyInitialState: HistoryState = {
14
16
  history: [],
15
17
  index: DRAFT_ENTRY_INDEX,
16
18
  documentUpdate: null,
package/src/index.ts CHANGED
@@ -1,6 +1,20 @@
1
- export * as LanguageSupport from '@neo4j-cypher/language-support';
2
- export { CypherEditor } from './CypherEditor';
1
+ import * as LanguageSupport from '@neo4j-cypher/language-support';
2
+ export { LanguageSupport };
3
+ export {
4
+ CypherEditor,
5
+ CypherEditorProps,
6
+ CypherEditorState,
7
+ CodemirrorSymbolFetcher,
8
+ DomEventHandlers,
9
+ } from './CypherEditor';
3
10
  export type { InlinePanelProps } from './CypherEditor';
11
+ export type { InlinePanelCallbacks } from './inlinePanel.js';
4
12
  export type { DiffProps } from './diffView';
5
- export { cypher } from './lang-cypher/langCypher';
13
+ export { cypher, CypherConfig } from './lang-cypher/langCypher';
6
14
  export { darkThemeConstants, lightThemeConstants } from './themes';
15
+ export type {
16
+ ThemeOptions,
17
+ DiffColors,
18
+ } from './lang-cypher/createCypherTheme.js';
19
+ export type { HostPortalCallbacks } from './hostCallbacks.js';
20
+ export type { HighlightedCypherTokenTypes } from './lang-cypher/constants.js';
@@ -1,14 +1,11 @@
1
- import {
2
- Completion,
3
- CompletionSource,
4
- snippet,
5
- } from '@codemirror/autocomplete';
1
+ import type { Completion, CompletionSource } from '@codemirror/autocomplete';
2
+ import { snippet } from '@codemirror/autocomplete';
6
3
  import { shouldAutoCompleteYield } from '@neo4j-cypher/language-support';
7
4
  import {
8
5
  CompletionItemKind,
9
6
  CompletionItemTag,
10
7
  } from 'vscode-languageserver-types';
11
- import { CompletionItemIcons } from '../icons';
8
+ import type { CompletionItemIcons } from '../icons';
12
9
  import type { CypherConfig } from './langCypher';
13
10
  import { getDocString } from './utils';
14
11
 
@@ -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({
@@ -1,13 +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
5
  import { CypherTokenType } from '@neo4j-cypher/language-support';
9
- import { StyleSpec } from 'style-mod';
10
- import { HighlightedCypherTokenTypes, tokenTypeToStyleTag } from './constants';
6
+ import type { StyleSpec } from 'style-mod';
7
+ import type { HighlightedCypherTokenTypes } from './constants';
8
+ import { tokenTypeToStyleTag } from './constants';
11
9
  import {
12
10
  byWordSvg,
13
11
  caseSensitiveSvg,
@@ -141,6 +139,53 @@ export const createCypherTheme = ({
141
139
  '& .cm-signature-help-panel-description': {
142
140
  padding: '5px',
143
141
  },
142
+ // Shared between both hover tooltips and hover errors
143
+ '& .cm-tooltip-hover': {
144
+ maxWidth: '800px',
145
+ },
146
+ '& .cm-hover-tooltip': {
147
+ backgroundColor: settings.autoCompletionPanel.backgroundColor,
148
+ color: settings.foreground,
149
+ maxHeight: '300px',
150
+ overflow: 'auto',
151
+ padding: '2px 10px',
152
+ },
153
+ '& .cm-hover-info': {
154
+ fontFamily:
155
+ '-apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif',
156
+ fontSize: '13px',
157
+ lineHeight: '1.5',
158
+ '& p': {
159
+ margin: '6px 0',
160
+ },
161
+ '& ul': {
162
+ margin: '6px 0',
163
+ paddingLeft: '18px',
164
+ },
165
+ '& li': {
166
+ margin: '2px 0',
167
+ },
168
+ '& code': {
169
+ fontFamily: 'Fira Code, Menlo, Monaco, Lucida Console, monospace',
170
+ fontSize: '12px',
171
+ },
172
+ '& pre': {
173
+ margin: '6px 0',
174
+ padding: '6px 8px',
175
+ borderRadius: '3px',
176
+ whiteSpace: 'pre-wrap',
177
+ overflowWrap: 'break-word',
178
+ backgroundColor: dark
179
+ ? 'rgba(255, 255, 255, 0.08)'
180
+ : 'rgba(0, 0, 0, 0.05)',
181
+ },
182
+ '& > *:first-child': {
183
+ marginTop: '0',
184
+ },
185
+ '& > *:last-child': {
186
+ marginBottom: '0',
187
+ },
188
+ },
144
189
  '.cm-completionInfo-signature': {
145
190
  color: 'darkgrey',
146
191
  },
@@ -255,8 +300,12 @@ export const createCypherTheme = ({
255
300
 
256
301
  const themeExtension = EditorView.theme(themeOptions, { dark });
257
302
 
258
- const styles = Object.entries(highlightStyles).map(
259
- ([token, color]: [HighlightedCypherTokenTypes, string]): TagStyle => ({
303
+ const styles = (
304
+ Object.entries(highlightStyles) as Array<
305
+ [HighlightedCypherTokenTypes, string]
306
+ >
307
+ ).map(
308
+ ([token, color]): TagStyle => ({
260
309
  tag: tokenTypeToStyleTag[token],
261
310
  color,
262
311
  class: token === CypherTokenType.consoleCommand ? 'cm-bold' : undefined,