@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.
- package/CHANGELOG.md +14 -0
- package/dist/src/CypherEditor.d.ts +8 -4
- package/dist/src/CypherEditor.js +9 -1
- package/dist/src/CypherEditor.js.map +1 -1
- package/dist/src/CypherEditor.test.js +1 -0
- package/dist/src/CypherEditor.test.js.map +1 -1
- package/dist/src/index.d.ts +8 -3
- package/dist/src/index.js +3 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/lang-cypher/createCypherTheme.js +46 -0
- package/dist/src/lang-cypher/createCypherTheme.js.map +1 -1
- package/dist/src/lang-cypher/hover/hover.d.ts +3 -0
- package/dist/src/lang-cypher/hover/hover.js +26 -0
- package/dist/src/lang-cypher/hover/hover.js.map +1 -0
- package/dist/src/lang-cypher/hover/hoverRender.d.ts +9 -0
- package/dist/src/lang-cypher/hover/hoverRender.js +142 -0
- package/dist/src/lang-cypher/hover/hoverRender.js.map +1 -0
- package/dist/src/lang-cypher/hover/hoverRender.test.js +123 -0
- package/dist/src/lang-cypher/hover/hoverRender.test.js.map +1 -0
- package/dist/src/lang-cypher/langCypher.js +3 -0
- package/dist/src/lang-cypher/langCypher.js.map +1 -1
- package/dist/src/lang-cypher/lintWorker.mjs +160 -162
- package/dist/src/lang-cypher/signatureHelp.js +13 -23
- package/dist/src/lang-cypher/signatureHelp.js.map +1 -1
- package/dist/src/lang-cypher/tooltipHighlighting.d.ts +21 -0
- package/dist/src/lang-cypher/tooltipHighlighting.js +76 -0
- package/dist/src/lang-cypher/tooltipHighlighting.js.map +1 -0
- package/dist/src/lang-cypher/tooltipHighlighting.test.js +63 -0
- package/dist/src/lang-cypher/tooltipHighlighting.test.js.map +1 -0
- package/dist/src/{e2e_tests → tests/e2e}/autoCompletion.spec.js +1 -1
- package/dist/src/tests/e2e/autoCompletion.spec.js.map +1 -0
- package/dist/src/{e2e_tests → tests/e2e}/configuration.spec.js +1 -1
- package/dist/src/tests/e2e/configuration.spec.js.map +1 -0
- package/dist/src/{e2e_tests → tests/e2e}/debounce.spec.js +2 -2
- package/dist/src/tests/e2e/debounce.spec.js.map +1 -0
- package/dist/src/tests/e2e/e2eUtils.js.map +1 -0
- package/dist/src/{e2e_tests → tests/e2e}/extraKeybindings.spec.js +1 -1
- package/dist/src/tests/e2e/extraKeybindings.spec.js.map +1 -0
- package/dist/src/{e2e_tests → tests/e2e}/historyNavigation.spec.js +1 -1
- package/dist/src/tests/e2e/historyNavigation.spec.js.map +1 -0
- package/dist/src/{e2e_tests → tests/e2e}/sanityChecks.spec.js +1 -1
- package/dist/src/tests/e2e/sanityChecks.spec.js.map +1 -0
- package/dist/src/{e2e_tests → tests/e2e}/signatureHelp.spec.js +1 -1
- package/dist/src/tests/e2e/signatureHelp.spec.js.map +1 -0
- package/dist/src/{e2e_tests → tests/e2e}/snippets.spec.js +1 -1
- package/dist/src/tests/e2e/snippets.spec.js.map +1 -0
- package/dist/src/tests/e2e/syntaxHighlighting.spec.d.ts +1 -0
- package/dist/src/{e2e_tests → tests/e2e}/syntaxHighlighting.spec.js +2 -2
- package/dist/src/tests/e2e/syntaxHighlighting.spec.js.map +1 -0
- package/dist/src/tests/e2e/syntaxValidation.spec.d.ts +1 -0
- package/dist/src/{e2e_tests → tests/e2e}/syntaxValidation.spec.js +1 -1
- package/dist/src/tests/e2e/syntaxValidation.spec.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/vitest.config.d.mts +2 -0
- package/dist/vitest.config.mjs +16 -0
- package/dist/vitest.config.mjs.map +1 -0
- package/package.json +6 -5
- package/src/CypherEditor.test.tsx +1 -0
- package/src/CypherEditor.tsx +26 -3
- package/src/index.ts +17 -3
- package/src/lang-cypher/createCypherTheme.ts +47 -0
- package/src/lang-cypher/hover/hover.ts +32 -0
- package/src/lang-cypher/hover/hoverRender.test.ts +176 -0
- package/src/lang-cypher/hover/hoverRender.ts +198 -0
- package/src/lang-cypher/langCypher.ts +3 -0
- package/src/lang-cypher/lintWorker.mjs +160 -162
- package/src/lang-cypher/signatureHelp.ts +19 -24
- package/src/lang-cypher/tooltipHighlighting.test.ts +78 -0
- package/src/lang-cypher/tooltipHighlighting.ts +119 -0
- package/src/{e2e_tests → tests/e2e}/autoCompletion.spec.tsx +1 -1
- package/src/{e2e_tests → tests/e2e}/configuration.spec.tsx +1 -1
- package/src/{e2e_tests → tests/e2e}/debounce.spec.tsx +2 -2
- package/src/{e2e_tests → tests/e2e}/extraKeybindings.spec.tsx +1 -1
- package/src/{e2e_tests → tests/e2e}/historyNavigation.spec.tsx +1 -1
- package/src/{e2e_tests → tests/e2e}/sanityChecks.spec.tsx +1 -1
- package/src/{e2e_tests → tests/e2e}/signatureHelp.spec.tsx +1 -1
- package/src/{e2e_tests → tests/e2e}/snippets.spec.tsx +1 -1
- package/src/{e2e_tests → tests/e2e}/syntaxHighlighting.spec.tsx +2 -2
- package/src/{e2e_tests → tests/e2e}/syntaxValidation.spec.tsx +1 -1
- package/dist/src/e2e_tests/autoCompletion.spec.js.map +0 -1
- package/dist/src/e2e_tests/configuration.spec.js.map +0 -1
- package/dist/src/e2e_tests/debounce.spec.js.map +0 -1
- package/dist/src/e2e_tests/e2eUtils.js.map +0 -1
- package/dist/src/e2e_tests/extraKeybindings.spec.js.map +0 -1
- package/dist/src/e2e_tests/historyNavigation.spec.js.map +0 -1
- package/dist/src/e2e_tests/performanceTest.spec.d.ts +0 -6
- package/dist/src/e2e_tests/performanceTest.spec.js +0 -97
- package/dist/src/e2e_tests/performanceTest.spec.js.map +0 -1
- package/dist/src/e2e_tests/sanityChecks.spec.js.map +0 -1
- package/dist/src/e2e_tests/signatureHelp.spec.js.map +0 -1
- package/dist/src/e2e_tests/snippets.spec.js.map +0 -1
- package/dist/src/e2e_tests/syntaxHighlighting.spec.js.map +0 -1
- package/dist/src/e2e_tests/syntaxValidation.spec.js.map +0 -1
- package/src/e2e_tests/performanceTest.spec.tsx +0 -163
- /package/dist/src/{e2e_tests/autoCompletion.spec.d.ts → lang-cypher/hover/hoverRender.test.d.ts} +0 -0
- /package/dist/src/{e2e_tests/configuration.spec.d.ts → lang-cypher/tooltipHighlighting.test.d.ts} +0 -0
- /package/dist/src/{e2e_tests/debounce.spec.d.ts → tests/e2e/autoCompletion.spec.d.ts} +0 -0
- /package/dist/src/{e2e_tests/extraKeybindings.spec.d.ts → tests/e2e/configuration.spec.d.ts} +0 -0
- /package/dist/src/{e2e_tests/historyNavigation.spec.d.ts → tests/e2e/debounce.spec.d.ts} +0 -0
- /package/dist/src/{e2e_tests → tests/e2e}/e2eUtils.d.ts +0 -0
- /package/dist/src/{e2e_tests → tests/e2e}/e2eUtils.js +0 -0
- /package/dist/src/{e2e_tests/sanityChecks.spec.d.ts → tests/e2e/extraKeybindings.spec.d.ts} +0 -0
- /package/dist/src/{e2e_tests/signatureHelp.spec.d.ts → tests/e2e/historyNavigation.spec.d.ts} +0 -0
- /package/dist/src/{e2e_tests/snippets.spec.d.ts → tests/e2e/sanityChecks.spec.d.ts} +0 -0
- /package/dist/src/{e2e_tests/syntaxHighlighting.spec.d.ts → tests/e2e/signatureHelp.spec.d.ts} +0 -0
- /package/dist/src/{e2e_tests/syntaxValidation.spec.d.ts → tests/e2e/snippets.spec.d.ts} +0 -0
- /package/src/{e2e_tests → tests/e2e}/e2eUtils.ts +0 -0
|
@@ -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.
|
|
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.
|
|
52
|
-
"@neo4j-cypher/lint-worker": "1.10.1-next.
|
|
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
|
}
|
package/src/CypherEditor.tsx
CHANGED
|
@@ -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<
|
|
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
|
-
|
|
2
|
-
export {
|
|
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
|
}
|