@neo4j-cypher/react-codemirror 2.0.0-next.41 → 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 (107) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/src/CypherEditor.d.ts +8 -4
  3. package/dist/src/CypherEditor.js +9 -1
  4. package/dist/src/CypherEditor.js.map +1 -1
  5. package/dist/src/CypherEditor.test.js +1 -0
  6. package/dist/src/CypherEditor.test.js.map +1 -1
  7. package/dist/src/index.d.ts +8 -3
  8. package/dist/src/index.js +3 -2
  9. package/dist/src/index.js.map +1 -1
  10. package/dist/src/lang-cypher/createCypherTheme.js +46 -0
  11. package/dist/src/lang-cypher/createCypherTheme.js.map +1 -1
  12. package/dist/src/lang-cypher/hover/hover.d.ts +3 -0
  13. package/dist/src/lang-cypher/hover/hover.js +26 -0
  14. package/dist/src/lang-cypher/hover/hover.js.map +1 -0
  15. package/dist/src/lang-cypher/hover/hoverRender.d.ts +9 -0
  16. package/dist/src/lang-cypher/hover/hoverRender.js +142 -0
  17. package/dist/src/lang-cypher/hover/hoverRender.js.map +1 -0
  18. package/dist/src/lang-cypher/hover/hoverRender.test.js +123 -0
  19. package/dist/src/lang-cypher/hover/hoverRender.test.js.map +1 -0
  20. package/dist/src/lang-cypher/langCypher.js +3 -0
  21. package/dist/src/lang-cypher/langCypher.js.map +1 -1
  22. package/dist/src/lang-cypher/lintWorker.mjs +160 -162
  23. package/dist/src/lang-cypher/signatureHelp.js +13 -23
  24. package/dist/src/lang-cypher/signatureHelp.js.map +1 -1
  25. package/dist/src/lang-cypher/tooltipHighlighting.d.ts +21 -0
  26. package/dist/src/lang-cypher/tooltipHighlighting.js +76 -0
  27. package/dist/src/lang-cypher/tooltipHighlighting.js.map +1 -0
  28. package/dist/src/lang-cypher/tooltipHighlighting.test.js +63 -0
  29. package/dist/src/lang-cypher/tooltipHighlighting.test.js.map +1 -0
  30. package/dist/src/{e2e_tests → tests/e2e}/autoCompletion.spec.js +1 -1
  31. package/dist/src/tests/e2e/autoCompletion.spec.js.map +1 -0
  32. package/dist/src/{e2e_tests → tests/e2e}/configuration.spec.js +1 -1
  33. package/dist/src/tests/e2e/configuration.spec.js.map +1 -0
  34. package/dist/src/{e2e_tests → tests/e2e}/debounce.spec.js +2 -2
  35. package/dist/src/tests/e2e/debounce.spec.js.map +1 -0
  36. package/dist/src/tests/e2e/e2eUtils.js.map +1 -0
  37. package/dist/src/{e2e_tests → tests/e2e}/extraKeybindings.spec.js +1 -1
  38. package/dist/src/tests/e2e/extraKeybindings.spec.js.map +1 -0
  39. package/dist/src/{e2e_tests → tests/e2e}/historyNavigation.spec.js +1 -1
  40. package/dist/src/tests/e2e/historyNavigation.spec.js.map +1 -0
  41. package/dist/src/{e2e_tests → tests/e2e}/sanityChecks.spec.js +1 -1
  42. package/dist/src/tests/e2e/sanityChecks.spec.js.map +1 -0
  43. package/dist/src/{e2e_tests → tests/e2e}/signatureHelp.spec.js +1 -1
  44. package/dist/src/tests/e2e/signatureHelp.spec.js.map +1 -0
  45. package/dist/src/{e2e_tests → tests/e2e}/snippets.spec.js +1 -1
  46. package/dist/src/tests/e2e/snippets.spec.js.map +1 -0
  47. package/dist/src/tests/e2e/syntaxHighlighting.spec.d.ts +1 -0
  48. package/dist/src/{e2e_tests → tests/e2e}/syntaxHighlighting.spec.js +2 -2
  49. package/dist/src/tests/e2e/syntaxHighlighting.spec.js.map +1 -0
  50. package/dist/src/tests/e2e/syntaxValidation.spec.d.ts +1 -0
  51. package/dist/src/{e2e_tests → tests/e2e}/syntaxValidation.spec.js +1 -1
  52. package/dist/src/tests/e2e/syntaxValidation.spec.js.map +1 -0
  53. package/dist/tsconfig.tsbuildinfo +1 -1
  54. package/dist/vitest.config.d.mts +2 -0
  55. package/dist/vitest.config.mjs +16 -0
  56. package/dist/vitest.config.mjs.map +1 -0
  57. package/package.json +6 -5
  58. package/src/CypherEditor.test.tsx +1 -0
  59. package/src/CypherEditor.tsx +26 -3
  60. package/src/index.ts +17 -3
  61. package/src/lang-cypher/createCypherTheme.ts +47 -0
  62. package/src/lang-cypher/hover/hover.ts +32 -0
  63. package/src/lang-cypher/hover/hoverRender.test.ts +176 -0
  64. package/src/lang-cypher/hover/hoverRender.ts +198 -0
  65. package/src/lang-cypher/langCypher.ts +3 -0
  66. package/src/lang-cypher/lintWorker.mjs +160 -162
  67. package/src/lang-cypher/signatureHelp.ts +19 -24
  68. package/src/lang-cypher/tooltipHighlighting.test.ts +78 -0
  69. package/src/lang-cypher/tooltipHighlighting.ts +119 -0
  70. package/src/{e2e_tests → tests/e2e}/autoCompletion.spec.tsx +1 -1
  71. package/src/{e2e_tests → tests/e2e}/configuration.spec.tsx +1 -1
  72. package/src/{e2e_tests → tests/e2e}/debounce.spec.tsx +2 -2
  73. package/src/{e2e_tests → tests/e2e}/extraKeybindings.spec.tsx +1 -1
  74. package/src/{e2e_tests → tests/e2e}/historyNavigation.spec.tsx +1 -1
  75. package/src/{e2e_tests → tests/e2e}/sanityChecks.spec.tsx +1 -1
  76. package/src/{e2e_tests → tests/e2e}/signatureHelp.spec.tsx +1 -1
  77. package/src/{e2e_tests → tests/e2e}/snippets.spec.tsx +1 -1
  78. package/src/{e2e_tests → tests/e2e}/syntaxHighlighting.spec.tsx +2 -2
  79. package/src/{e2e_tests → tests/e2e}/syntaxValidation.spec.tsx +1 -1
  80. package/dist/src/e2e_tests/autoCompletion.spec.js.map +0 -1
  81. package/dist/src/e2e_tests/configuration.spec.js.map +0 -1
  82. package/dist/src/e2e_tests/debounce.spec.js.map +0 -1
  83. package/dist/src/e2e_tests/e2eUtils.js.map +0 -1
  84. package/dist/src/e2e_tests/extraKeybindings.spec.js.map +0 -1
  85. package/dist/src/e2e_tests/historyNavigation.spec.js.map +0 -1
  86. package/dist/src/e2e_tests/performanceTest.spec.d.ts +0 -6
  87. package/dist/src/e2e_tests/performanceTest.spec.js +0 -97
  88. package/dist/src/e2e_tests/performanceTest.spec.js.map +0 -1
  89. package/dist/src/e2e_tests/sanityChecks.spec.js.map +0 -1
  90. package/dist/src/e2e_tests/signatureHelp.spec.js.map +0 -1
  91. package/dist/src/e2e_tests/snippets.spec.js.map +0 -1
  92. package/dist/src/e2e_tests/syntaxHighlighting.spec.js.map +0 -1
  93. package/dist/src/e2e_tests/syntaxValidation.spec.js.map +0 -1
  94. package/src/e2e_tests/performanceTest.spec.tsx +0 -163
  95. /package/dist/src/{e2e_tests/autoCompletion.spec.d.ts → lang-cypher/hover/hoverRender.test.d.ts} +0 -0
  96. /package/dist/src/{e2e_tests/configuration.spec.d.ts → lang-cypher/tooltipHighlighting.test.d.ts} +0 -0
  97. /package/dist/src/{e2e_tests/debounce.spec.d.ts → tests/e2e/autoCompletion.spec.d.ts} +0 -0
  98. /package/dist/src/{e2e_tests/extraKeybindings.spec.d.ts → tests/e2e/configuration.spec.d.ts} +0 -0
  99. /package/dist/src/{e2e_tests/historyNavigation.spec.d.ts → tests/e2e/debounce.spec.d.ts} +0 -0
  100. /package/dist/src/{e2e_tests → tests/e2e}/e2eUtils.d.ts +0 -0
  101. /package/dist/src/{e2e_tests → tests/e2e}/e2eUtils.js +0 -0
  102. /package/dist/src/{e2e_tests/sanityChecks.spec.d.ts → tests/e2e/extraKeybindings.spec.d.ts} +0 -0
  103. /package/dist/src/{e2e_tests/signatureHelp.spec.d.ts → tests/e2e/historyNavigation.spec.d.ts} +0 -0
  104. /package/dist/src/{e2e_tests/snippets.spec.d.ts → tests/e2e/sanityChecks.spec.d.ts} +0 -0
  105. /package/dist/src/{e2e_tests/syntaxHighlighting.spec.d.ts → tests/e2e/signatureHelp.spec.d.ts} +0 -0
  106. /package/dist/src/{e2e_tests/syntaxValidation.spec.d.ts → tests/e2e/snippets.spec.d.ts} +0 -0
  107. /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.41",
3
+ "version": "2.0.0-next.42",
4
4
  "keywords": [
5
5
  "codemirror",
6
6
  "codemirror 6",
@@ -48,8 +48,8 @@
48
48
  "style-mod": "^4.1.2",
49
49
  "vscode-languageserver-types": "^3.17.3",
50
50
  "workerpool": "^9.3.3",
51
- "@neo4j-cypher/language-support": "2.0.0-next.38",
52
- "@neo4j-cypher/lint-worker": "1.10.1-next.15"
51
+ "@neo4j-cypher/language-support": "2.0.0-next.39",
52
+ "@neo4j-cypher/lint-worker": "1.10.1-next.16"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@neo4j-ndl/base": "^3.2.10",
@@ -77,10 +77,11 @@
77
77
  "dev": "tsc --watch",
78
78
  "build": "pnpm copy-lint-worker && tsc --declaration --outDir dist/",
79
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",
80
+ "check-api-report": "tsx checkApiReport.ts",
81
+ "gen-api-report": "pnpm build && pnpm check-api-report",
80
82
  "clean": "rm -rf dist tsconfig*.tsbuildinfo",
81
83
  "test": "vitest run",
82
84
  "test:e2e": "playwright test -c playwright-ct.config.ts",
83
- "test:e2e-ui": "playwright test -c playwright-ct.config.ts --ui",
84
- "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"
85
86
  }
86
87
  }
@@ -18,6 +18,7 @@ const onChange = vi.fn<(v: string) => void>((v) => {
18
18
  rerender();
19
19
  });
20
20
 
21
+ // oxlint-disable-next-line typescript/no-explicit-any
21
22
  (global as any).IS_REACT_ACT_ENVIRONMENT = true;
22
23
  const DEBOUNCE_TIME_WITH_MARGIN = DEBOUNCE_TIME + 100;
23
24
 
@@ -36,7 +36,9 @@ import { richClipboardCopier } from './richClipboardCopier';
36
36
  import type { LintWorker } from '@neo4j-cypher/lint-worker';
37
37
  import workerpool from 'workerpool';
38
38
 
39
- type DomEventHandlers = Parameters<typeof EditorView.domEventHandlers>[0];
39
+ export type DomEventHandlers = Parameters<
40
+ typeof EditorView.domEventHandlers
41
+ >[0];
40
42
 
41
43
  /**
42
44
  * Normalize CRLF line endings to LF, which is what CodeMirror expects.
@@ -187,6 +189,12 @@ export interface CypherEditorProps {
187
189
  */
188
190
  ariaLabel?: string;
189
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
+
190
198
  /**
191
199
  * Whether keybindings for inserting indents with the Tab key should be disabled.
192
200
  *
@@ -314,6 +322,7 @@ const readOnlyCompartment = new Compartment();
314
322
  const placeholderCompartment = new Compartment();
315
323
  const domEventHandlerCompartment = new Compartment();
316
324
  const diffCompartment = new Compartment();
325
+ const contentAttributesCompartment = new Compartment();
317
326
 
318
327
  const formatLineNumber =
319
328
  (prompt?: string) => (a: number, state: EditorState) => {
@@ -324,7 +333,7 @@ const formatLineNumber =
324
333
  return a.toString();
325
334
  };
326
335
 
327
- type CypherEditorState = {
336
+ export type CypherEditorState = {
328
337
  editorActionsContainer: HTMLElement | null;
329
338
  };
330
339
 
@@ -332,7 +341,7 @@ const ExternalEdit = Annotation.define<boolean>();
332
341
  const WorkerURL = new URL('./lang-cypher/lintWorker.mjs', import.meta.url)
333
342
  .pathname;
334
343
 
335
- class CodemirrorSymbolFetcher {
344
+ export class CodemirrorSymbolFetcher {
336
345
  constructor(languageService: CypherLanguageService) {
337
346
  this.languageService = languageService;
338
347
  }
@@ -601,6 +610,9 @@ export class CypherEditor extends Component<
601
610
  'aria-label': this.props.ariaLabel,
602
611
  })
603
612
  : [],
613
+ contentAttributesCompartment.of(
614
+ EditorView.contentAttributes.of(this.props.contentAttributes ?? {}),
615
+ ),
604
616
  !this.props.moveFocusOnTab
605
617
  ? EditorView.contentAttributes.of({
606
618
  'aria-description':
@@ -747,6 +759,17 @@ export class CypherEditor extends Component<
747
759
  });
748
760
  }
749
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
+
750
773
  if (prevProps.readonly !== this.props.readonly) {
751
774
  this.editorView.current.dispatch({
752
775
  effects: readOnlyCompartment.reconfigure(
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';
@@ -139,6 +139,53 @@ export const createCypherTheme = ({
139
139
  '& .cm-signature-help-panel-description': {
140
140
  padding: '5px',
141
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
+ },
142
189
  '.cm-completionInfo-signature': {
143
190
  color: 'darkgrey',
144
191
  },
@@ -0,0 +1,32 @@
1
+ import type { HoverTooltipSource } from '@codemirror/view';
2
+ import type { CypherConfig } from '../langCypher';
3
+ import { renderHoverInfo } from './hoverRender';
4
+ import { tooltipCypherHighlighter } from '../tooltipHighlighting.js';
5
+
6
+ export function getHoverSource(cfg: CypherConfig): HoverTooltipSource {
7
+ const hoverSource: HoverTooltipSource = (view, pos) => {
8
+ const doc = view.state.doc.toString();
9
+ const hoverInfo = cfg.languageService.hoverInfo(doc, {
10
+ caretPosition: pos,
11
+ dbSchema: cfg.schema ?? {},
12
+ });
13
+
14
+ if (!hoverInfo) {
15
+ return null;
16
+ }
17
+
18
+ return {
19
+ pos: pos,
20
+ above: true,
21
+ create() {
22
+ const dom = document.createElement('div');
23
+ dom.className = 'cm-hover-tooltip';
24
+ dom.appendChild(
25
+ renderHoverInfo(hoverInfo, tooltipCypherHighlighter(view.state, cfg)),
26
+ );
27
+ return { dom };
28
+ },
29
+ };
30
+ };
31
+ return hoverSource;
32
+ }
@@ -0,0 +1,176 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import type { HoverInfo } from '@neo4j-cypher/language-support';
4
+ import {
5
+ CypherLanguageService,
6
+ testData,
7
+ } from '@neo4j-cypher/language-support';
8
+ import { describe, expect, test } from 'vitest';
9
+ import type { CypherFragmentKind } from '../tooltipHighlighting.js';
10
+ import { renderHoverInfo } from './hoverRender';
11
+
12
+ function methodHover(query: string): HoverInfo {
13
+ const languageService = new CypherLanguageService();
14
+ return languageService.hoverInfo(query, {
15
+ caretPosition: query.indexOf('(') - 1,
16
+ dbSchema: testData.mockSchema,
17
+ });
18
+ }
19
+
20
+ function renderMethodHover(query: string): HTMLElement {
21
+ return renderHoverInfo(methodHover(query));
22
+ }
23
+
24
+ describe('hover info html rendering', () => {
25
+ function highlightedFragments(
26
+ info: HoverInfo,
27
+ ): [string, CypherFragmentKind][] {
28
+ const seen: [string, CypherFragmentKind][] = [];
29
+ renderHoverInfo(info, (code, kind, target) => {
30
+ seen.push([code, kind]);
31
+ target.textContent = code;
32
+ });
33
+ return seen;
34
+ }
35
+
36
+ test('passes the fragment kind of each cypher block to the highlighter', () => {
37
+ expect(
38
+ highlightedFragments({
39
+ kind: 'variable',
40
+ variable: 'n',
41
+ types: ['Node'],
42
+ labelExpression: '(Person | Pet)',
43
+ }),
44
+ ).toEqual([['(Person | Pet)', 'labelExpression']]);
45
+
46
+ expect(highlightedFragments(methodHover('RETURN abs(1)'))).toEqual([
47
+ ['abs(input :: INTEGER | FLOAT) :: INTEGER | FLOAT', 'function'],
48
+ ]);
49
+
50
+ expect(highlightedFragments(methodHover('CALL db.labels()'))).toEqual([
51
+ ['db.labels() :: (label :: STRING)', 'procedure'],
52
+ ]);
53
+ });
54
+
55
+ test('renders a variable hover', () => {
56
+ expect(
57
+ renderHoverInfo({
58
+ kind: 'variable',
59
+ variable: 'n',
60
+ types: ['Node'],
61
+ labelExpression: '(Person | Pet)',
62
+ }).innerHTML,
63
+ ).toBe('<p><code>n: Node</code></p><pre><code>(Person | Pet)</code></pre>');
64
+
65
+ expect(
66
+ renderHoverInfo({ kind: 'variable', variable: 'x', types: ['Integer'] })
67
+ .innerHTML,
68
+ ).toBe('<p><code>x: Integer</code></p>');
69
+ });
70
+
71
+ test('renders a function hover', () => {
72
+ expect(renderMethodHover('RETURN toFloat(1)').innerHTML).toBe(
73
+ [
74
+ '<pre><code>toFloat(input :: STRING | INTEGER | FLOAT) :: FLOAT</code></pre>',
75
+ '<p>Converts a <code>STRING</code>, <code>INTEGER</code> or <code>FLOAT</code> value to a <code>FLOAT</code> value.</p>',
76
+ '<p><strong>Parameters</strong></p>',
77
+ '<ul><li><code>input</code> - A value to be converted into a float.</li></ul>',
78
+ '<p><strong>Returns:</strong> <code>FLOAT</code></p>',
79
+ ].join(''),
80
+ );
81
+ });
82
+
83
+ test('renders a procedure hover', () => {
84
+ expect(renderMethodHover('CALL db.labels()').innerHTML).toBe(
85
+ [
86
+ '<pre><code>db.labels() :: (label :: STRING)</code></pre>',
87
+ "<p>List all labels attached to nodes within a database according to the user's access rights. The procedure returns empty results if the user is not authorized to view those labels.</p>",
88
+ '<p><strong>Returns</strong></p>',
89
+ '<ul><li><code>label</code> - A label within the database.</li></ul>',
90
+ ].join(''),
91
+ );
92
+ });
93
+
94
+ test('italicises the deprecation marker of a deprecated method', () => {
95
+ expect(
96
+ renderMethodHover('CYPHER 5 RETURN apoc.create.uuid()').innerHTML,
97
+ ).toContain('<p>(<em>deprecated</em>) Returns a UUID.</p>');
98
+ });
99
+
100
+ // The wildcards of dbms.queryJmx' descriptions are shown as written, both
101
+ // in prose and inside a code span
102
+ test('renders the wildcards of a procedure description', () => {
103
+ const html = renderMethodHover('CALL dbms.queryJmx()').innerHTML;
104
+
105
+ expect(html).toContain(
106
+ '<p>Query JMX management data by domain and name. For instance, use <code>*:*</code> to find all JMX beans.</p>',
107
+ );
108
+ expect(html).toContain(
109
+ "<li><code>query</code> - A query for MBeans on this MBeanServer (e.g. '*:*,name=*neo4j*' for all metrics in neo4j database).</li>",
110
+ );
111
+ });
112
+
113
+ test('renders the paragraphs, bullet list and italics of a procedure description', () => {
114
+ const html = renderMethodHover(
115
+ 'CALL db.index.fulltext.queryNodes()',
116
+ ).innerHTML;
117
+
118
+ expect(html).toContain('Valid <em>key: value</em> pairs');
119
+ expect(html).toContain(
120
+ [
121
+ '<ul>',
122
+ "<li>'skip' -- to skip the top N results.</li>",
123
+ "<li>'limit' -- to limit the number of results returned.</li>",
124
+ "<li>'analyzer' -- to use the specified analyzer as a search analyzer for this query.</li>",
125
+ '</ul>',
126
+ '<p>The <code>options</code> map and any of the keys are optional.',
127
+ ].join(''),
128
+ );
129
+ });
130
+
131
+ test('renders a parameter that carries its own layout as a code block', () => {
132
+ expect(renderMethodHover('CALL apoc.export.csv.all()').innerHTML).toContain(
133
+ [
134
+ '<ul><li><code>file</code> - The name of the file to which the data will be exported.</li>',
135
+ '<li><code>config</code> -<pre><code>{\n',
136
+ ' stream = false :: BOOLEAN,\n',
137
+ ].join(''),
138
+ );
139
+ });
140
+
141
+ // Schema descriptions list values such as 'AS_PATH_LIST', which must not
142
+ // turn into emphasis
143
+ test('leaves the underscores inside words alone', () => {
144
+ const description =
145
+ "JSON path options: ('ALWAYS_RETURN_LIST', 'AS_PATH_LIST', 'DEFAULT_PATH_LEAF_TO_NULL').";
146
+
147
+ const dom = renderHoverInfo({
148
+ kind: 'function',
149
+ signature: 'f(pathOptions :: LIST<STRING>) :: ANY',
150
+ description: '',
151
+ isDeprecated: false,
152
+ parameters: [{ name: 'pathOptions', description }],
153
+ returnType: 'ANY',
154
+ });
155
+
156
+ expect(dom.querySelector('em')).toBeNull();
157
+ expect(dom.querySelector('li').textContent).toBe(
158
+ `pathOptions - ${description}`,
159
+ );
160
+ });
161
+
162
+ test('never renders descriptions as html', () => {
163
+ const dom = renderHoverInfo({
164
+ kind: 'procedure',
165
+ signature: 'p()',
166
+ description: '<img src=x onerror=alert(1)>',
167
+ isDeprecated: false,
168
+ parameters: [],
169
+ returnValues: [{ name: 'r', description: '<b>bold</b>' }],
170
+ });
171
+
172
+ expect(dom.querySelector('img')).toBeNull();
173
+ expect(dom.querySelector('b')).toBeNull();
174
+ expect(dom.textContent).toContain('<img src=x onerror=alert(1)>');
175
+ });
176
+ });
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Renders language-support's HoverInfo into the hover tooltip.
3
+ *
4
+ * Everything is inserted as text nodes, so schema-provided descriptions can
5
+ * never inject HTML into the editor.
6
+ */
7
+
8
+ import type {
9
+ HoverDescription,
10
+ HoverInfo,
11
+ } from '@neo4j-cypher/language-support';
12
+ import type {
13
+ CypherFragmentKind,
14
+ TooltipCypherHighlighter,
15
+ } from '../tooltipHighlighting.js';
16
+
17
+ // The schema writes the bullet lists of its descriptions with *
18
+ const bullet = '* ';
19
+
20
+ export function renderHoverInfo(
21
+ info: HoverInfo,
22
+ highlightHoverCode?: TooltipCypherHighlighter,
23
+ ): HTMLElement {
24
+ const dom = document.createElement('div');
25
+ dom.className = 'cm-hover-info';
26
+
27
+ if (info.kind === 'variable') {
28
+ const typeLine = document.createElement('p');
29
+ typeLine.appendChild(
30
+ element('code', `${info.variable}: ${info.types.join(', ')}`),
31
+ );
32
+ dom.appendChild(typeLine);
33
+
34
+ if (info.labelExpression !== undefined) {
35
+ dom.appendChild(
36
+ codeBlock(info.labelExpression, 'labelExpression', highlightHoverCode),
37
+ );
38
+ }
39
+ return dom;
40
+ }
41
+
42
+ dom.appendChild(codeBlock(info.signature, info.kind, highlightHoverCode));
43
+
44
+ const deprecatedMarker = info.isDeprecated
45
+ ? [
46
+ document.createTextNode('('),
47
+ element('em', 'deprecated'),
48
+ document.createTextNode(') '),
49
+ ]
50
+ : [];
51
+ renderDescription(dom, info.description, deprecatedMarker);
52
+
53
+ appendDescribedList(dom, 'Parameters', info.parameters);
54
+
55
+ if (info.kind === 'function') {
56
+ if (info.returnType) {
57
+ const returns = document.createElement('p');
58
+ returns.append(
59
+ element('strong', 'Returns:'),
60
+ ' ',
61
+ element('code', info.returnType),
62
+ );
63
+ dom.appendChild(returns);
64
+ }
65
+ } else {
66
+ appendDescribedList(dom, 'Returns', info.returnValues);
67
+ }
68
+
69
+ return dom;
70
+ }
71
+
72
+ function element(tag: string, text: string): HTMLElement {
73
+ const created = document.createElement(tag);
74
+ created.textContent = text;
75
+ return created;
76
+ }
77
+
78
+ function codeBlock(
79
+ code: string,
80
+ kind: CypherFragmentKind | undefined,
81
+ highlightHoverCode?: TooltipCypherHighlighter,
82
+ ): HTMLElement {
83
+ const pre = document.createElement('pre');
84
+ const codeElement = document.createElement('code');
85
+
86
+ if (kind && highlightHoverCode) {
87
+ highlightHoverCode(code, kind, codeElement);
88
+ } else {
89
+ codeElement.textContent = code;
90
+ }
91
+
92
+ pre.appendChild(codeElement);
93
+ return pre;
94
+ }
95
+
96
+ function appendDescribedList(
97
+ dom: HTMLElement,
98
+ heading: string,
99
+ descriptions: HoverDescription[],
100
+ ) {
101
+ if (descriptions.length === 0) {
102
+ return;
103
+ }
104
+
105
+ const headingParagraph = document.createElement('p');
106
+ headingParagraph.appendChild(element('strong', heading));
107
+ dom.appendChild(headingParagraph);
108
+
109
+ const list = document.createElement('ul');
110
+ descriptions.forEach(({ name, description }) => {
111
+ const item = document.createElement('li');
112
+ item.append(element('code', name), ' -');
113
+
114
+ // Map-style descriptions (often configs) are laid out over several lines
115
+ const trimmed = description.trimEnd();
116
+ if (trimmed.includes('\n')) {
117
+ item.appendChild(codeBlock(trimmed, undefined));
118
+ } else {
119
+ item.append(' ');
120
+ renderInline(item, description);
121
+ }
122
+ list.appendChild(item);
123
+ });
124
+ dom.appendChild(list);
125
+ }
126
+
127
+ /* A schema description is prose that can bring paragraphs and * bullet lists.
128
+ `lead` goes in front of its first paragraph. */
129
+ function renderDescription(
130
+ dom: HTMLElement,
131
+ description: string,
132
+ lead: Node[],
133
+ ) {
134
+ const lines = description.split('\n');
135
+ let paragraph: string[] = [];
136
+
137
+ const flushParagraph = () => {
138
+ if (paragraph.length === 0 && lead.length === 0) return;
139
+ const p = document.createElement('p');
140
+ p.append(...lead.splice(0));
141
+ renderInline(p, paragraph.join('\n'));
142
+ dom.appendChild(p);
143
+ paragraph = [];
144
+ };
145
+
146
+ let i = 0;
147
+ while (i < lines.length) {
148
+ if (lines[i].startsWith(bullet)) {
149
+ flushParagraph();
150
+ const list = document.createElement('ul');
151
+ while (i < lines.length && lines[i].startsWith(bullet)) {
152
+ const item = document.createElement('li');
153
+ renderInline(item, lines[i].slice(bullet.length));
154
+ list.appendChild(item);
155
+ i++;
156
+ }
157
+ dom.appendChild(list);
158
+ continue;
159
+ }
160
+
161
+ if (lines[i].trim() === '') {
162
+ if (paragraph.length > 0) flushParagraph();
163
+ } else {
164
+ paragraph.push(lines[i]);
165
+ }
166
+ i++;
167
+ }
168
+
169
+ flushParagraph();
170
+ }
171
+
172
+ /* Code spans and italics, the markup descriptions write. Underscores only
173
+ count as emphasis outside a word, so that descriptions listing values like
174
+ 'DEFAULT_PATH_LEAF_TO_NULL' keep them - the same rule CommonMark
175
+ (https://spec.commonmark.org/0.31.2/#example-374) follows. */
176
+ const inlineMarkup = /`([^`]+)`|(?<!\w)_([^_\n]+)_(?!\w)/g;
177
+
178
+ function renderInline(target: HTMLElement, text: string) {
179
+ let lastEnd = 0;
180
+
181
+ for (const match of text.matchAll(inlineMarkup)) {
182
+ const [matched, code, italic] = match;
183
+
184
+ if (match.index > lastEnd) {
185
+ target.append(text.slice(lastEnd, match.index));
186
+ }
187
+
188
+ target.appendChild(
189
+ code !== undefined ? element('code', code.trim()) : element('em', italic),
190
+ );
191
+
192
+ lastEnd = match.index + matched.length;
193
+ }
194
+
195
+ if (lastEnd < text.length) {
196
+ target.append(text.slice(lastEnd));
197
+ }
198
+ }
@@ -10,6 +10,8 @@ import { completionStyles, cypherAutocomplete } from './autocomplete';
10
10
  import { ParserAdapter } from './parser-adapter';
11
11
  import { signatureHelpTooltip } from './signatureHelp';
12
12
  import { cypherLinter } from './syntaxValidation';
13
+ import { hoverTooltip } from '@codemirror/view';
14
+ import { getHoverSource } from './hover/hover.js';
13
15
 
14
16
  const facet = defineLanguageFacet({
15
17
  commentTokens: { block: { open: '/*', close: '*/' }, line: '//' },
@@ -40,5 +42,6 @@ export function cypher(config: CypherConfig) {
40
42
  }),
41
43
  cypherLinter(config),
42
44
  signatureHelpTooltip(config),
45
+ hoverTooltip(getHoverSource(config)),
43
46
  ]);
44
47
  }