@neo4j-cypher/react-codemirror 2.0.0-next.26.5 → 2.0.0-next.28
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 +24 -0
- package/LICENSE.md +200 -200
- package/dist/src/CypherEditor.js +10 -3
- package/dist/src/CypherEditor.js.map +1 -1
- package/dist/src/lang-cypher/autocomplete.js +1 -3
- package/dist/src/lang-cypher/autocomplete.js.map +1 -1
- package/dist/src/lang-cypher/lintWorker.mjs +126 -117
- package/dist/src/lang-cypher/syntaxValidation.js +6 -7
- package/dist/src/lang-cypher/syntaxValidation.js.map +1 -1
- package/dist/src/richClipboardCopier.d.ts +4 -0
- package/dist/src/richClipboardCopier.js +78 -0
- package/dist/src/richClipboardCopier.js.map +1 -0
- package/dist/src/richClipboardCopier.test.d.ts +1 -0
- package/dist/src/richClipboardCopier.test.js +53 -0
- package/dist/src/richClipboardCopier.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +15 -15
- package/src/CypherEditor.tsx +30 -26
- package/src/icons.ts +90 -90
- package/src/lang-cypher/autocomplete.ts +1 -2
- package/src/lang-cypher/lintWorker.mjs +126 -117
- package/src/lang-cypher/syntaxValidation.ts +12 -10
- package/src/richClipboardCopier.test.ts +65 -0
- package/src/richClipboardCopier.ts +99 -0
|
@@ -3,19 +3,17 @@ import { Extension } from '@codemirror/state';
|
|
|
3
3
|
import { DiagnosticSeverity, DiagnosticTag } from 'vscode-languageserver-types';
|
|
4
4
|
import workerpool from 'workerpool';
|
|
5
5
|
import type { CypherConfig } from './langCypher';
|
|
6
|
-
import type {
|
|
6
|
+
import type { LintWorker } from '@neo4j-cypher/lint-worker';
|
|
7
7
|
import { parserWrapper } from '@neo4j-cypher/language-support';
|
|
8
8
|
|
|
9
9
|
const WorkerURL = new URL('./lintWorker.mjs', import.meta.url).pathname;
|
|
10
10
|
|
|
11
11
|
const pool = workerpool.pool(WorkerURL, {
|
|
12
|
-
minWorkers:
|
|
12
|
+
minWorkers: 1,
|
|
13
13
|
workerOpts: { type: 'module' },
|
|
14
14
|
workerTerminateTimeout: 2000,
|
|
15
15
|
});
|
|
16
16
|
|
|
17
|
-
let lastSemanticJob: LinterTask | undefined;
|
|
18
|
-
|
|
19
17
|
export const cypherLinter: (config: CypherConfig) => Extension = (config) =>
|
|
20
18
|
linter(async (view) => {
|
|
21
19
|
if (!config.lint) {
|
|
@@ -27,12 +25,16 @@ export const cypherLinter: (config: CypherConfig) => Extension = (config) =>
|
|
|
27
25
|
}
|
|
28
26
|
|
|
29
27
|
try {
|
|
30
|
-
if (
|
|
31
|
-
|
|
28
|
+
if (pool.stats().busyWorkers > 0) {
|
|
29
|
+
await pool.terminate(true);
|
|
32
30
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const result = await
|
|
31
|
+
|
|
32
|
+
const proxyWorker = (await pool.proxy()) as unknown as LintWorker;
|
|
33
|
+
const result = await proxyWorker.lintCypherQuery(
|
|
34
|
+
query,
|
|
35
|
+
config.schema ?? {},
|
|
36
|
+
config.featureFlags ?? {},
|
|
37
|
+
);
|
|
36
38
|
|
|
37
39
|
if (result.symbolTables) {
|
|
38
40
|
parserWrapper.setSymbolsInfo({
|
|
@@ -58,7 +60,7 @@ export const cypherLinter: (config: CypherConfig) => Extension = (config) =>
|
|
|
58
60
|
});
|
|
59
61
|
return a;
|
|
60
62
|
} catch (err) {
|
|
61
|
-
if (!(err
|
|
63
|
+
if (!String(err).includes('Worker terminated')) {
|
|
62
64
|
console.error(String(err) + ' ' + query);
|
|
63
65
|
}
|
|
64
66
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
3
|
+
import { EditorView } from '@codemirror/view';
|
|
4
|
+
import { getHTML, getCSSStyleForClass } from './richClipboardCopier';
|
|
5
|
+
|
|
6
|
+
function createStyleSheet(rules: string): CSSStyleSheet {
|
|
7
|
+
const styleEl = document.createElement('style');
|
|
8
|
+
styleEl.textContent = rules;
|
|
9
|
+
document.head.appendChild(styleEl);
|
|
10
|
+
return styleEl.sheet;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe('getCSSStyleForClass', () => {
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
document.head.innerHTML = '';
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('returns cssText for a given class', () => {
|
|
19
|
+
createStyleSheet('.foo { color: red; font-weight: bold; }');
|
|
20
|
+
const css = getCSSStyleForClass('foo');
|
|
21
|
+
expect(css).toContain('color: red');
|
|
22
|
+
expect(css).toContain('font-weight: bold');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('returns empty string if no matching class exists', () => {
|
|
26
|
+
createStyleSheet('.bar { color: blue; }');
|
|
27
|
+
const css = getCSSStyleForClass('nonexistent');
|
|
28
|
+
expect(css).toBe('');
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('getHTML', () => {
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
document.head.innerHTML = '';
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('wraps selected DOM content with monospace wrapper and inlines class styles', () => {
|
|
38
|
+
createStyleSheet(
|
|
39
|
+
'.foo { color: red; font-weight: bold; } .bar { background: yellow; }',
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const div = document.createElement('div');
|
|
43
|
+
div.className = 'foo bar';
|
|
44
|
+
div.textContent = 'Hello';
|
|
45
|
+
const wrapper = document.createElement('div');
|
|
46
|
+
wrapper.appendChild(div);
|
|
47
|
+
document.body.appendChild(wrapper);
|
|
48
|
+
|
|
49
|
+
const mockView = {
|
|
50
|
+
domAtPos: vi.fn((pos: number) => ({
|
|
51
|
+
node: wrapper,
|
|
52
|
+
offset: pos === 0 ? 0 : 1,
|
|
53
|
+
})),
|
|
54
|
+
} as unknown as EditorView;
|
|
55
|
+
|
|
56
|
+
const html = getHTML(mockView, 0, 1);
|
|
57
|
+
|
|
58
|
+
expect(html).toContain('font-family: monospace;');
|
|
59
|
+
expect(html).toContain('color: red');
|
|
60
|
+
expect(html).toContain('font-weight: bold');
|
|
61
|
+
expect(html).toContain('background: yellow');
|
|
62
|
+
expect(html).not.toContain('class=');
|
|
63
|
+
expect(html).toContain('Hello');
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { EditorView } from '@codemirror/view';
|
|
2
|
+
|
|
3
|
+
export function getCSSStyleForClass(className: string): string {
|
|
4
|
+
return Array.from(document.styleSheets).reduce((cssText, sheet) => {
|
|
5
|
+
try {
|
|
6
|
+
const rules = sheet.cssRules ?? [];
|
|
7
|
+
for (const rule of Array.from(rules)) {
|
|
8
|
+
if (
|
|
9
|
+
rule instanceof CSSStyleRule &&
|
|
10
|
+
rule.selectorText
|
|
11
|
+
.split(',')
|
|
12
|
+
.some((sel) => sel.trim() === `.${className}`)
|
|
13
|
+
) {
|
|
14
|
+
cssText += rule.style.cssText;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
} catch {
|
|
18
|
+
// Ignore CORS-protected stylesheets
|
|
19
|
+
}
|
|
20
|
+
return cssText;
|
|
21
|
+
}, '');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function replaceClassWithStyle(element: HTMLElement): void {
|
|
25
|
+
const classNames = element.className.split(/\s+/).filter(Boolean);
|
|
26
|
+
const computed = getComputedStyle(element);
|
|
27
|
+
|
|
28
|
+
let styleText = computed.color ? `color: ${computed.color};` : '';
|
|
29
|
+
|
|
30
|
+
for (const cls of classNames) {
|
|
31
|
+
styleText += getCSSStyleForClass(cls);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (styleText) {
|
|
35
|
+
element.setAttribute('style', styleText);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
element.removeAttribute('class');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function getHTML(view: EditorView, from: number, to: number): string {
|
|
42
|
+
const range = document.createRange();
|
|
43
|
+
const fromInfo = view.domAtPos(from);
|
|
44
|
+
const toInfo = view.domAtPos(to);
|
|
45
|
+
|
|
46
|
+
range.setStart(fromInfo.node, fromInfo.offset);
|
|
47
|
+
range.setEnd(toInfo.node, toInfo.offset);
|
|
48
|
+
|
|
49
|
+
const commonAncestor = range.commonAncestorContainer;
|
|
50
|
+
const wrapperElement =
|
|
51
|
+
commonAncestor instanceof Text
|
|
52
|
+
? commonAncestor.parentElement
|
|
53
|
+
: (commonAncestor as HTMLElement);
|
|
54
|
+
|
|
55
|
+
const wrapper = document.createElement('div');
|
|
56
|
+
|
|
57
|
+
if (wrapperElement) {
|
|
58
|
+
const style = wrapperElement.getAttribute('style');
|
|
59
|
+
const cls = wrapperElement.getAttribute('class');
|
|
60
|
+
if (style) wrapper.setAttribute('style', style);
|
|
61
|
+
if (cls) wrapper.setAttribute('class', cls);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
wrapper.appendChild(range.cloneContents());
|
|
65
|
+
|
|
66
|
+
replaceClassWithStyle(wrapper);
|
|
67
|
+
wrapper
|
|
68
|
+
.querySelectorAll<HTMLElement>('[class]')
|
|
69
|
+
.forEach(replaceClassWithStyle);
|
|
70
|
+
|
|
71
|
+
const editorElement = document.querySelector('.cm-editor');
|
|
72
|
+
const editorStyles = editorElement ? getComputedStyle(editorElement) : null;
|
|
73
|
+
|
|
74
|
+
return `<div style="font-family: monospace; ${
|
|
75
|
+
editorStyles?.color ? `color: ${editorStyles.color};` : ''
|
|
76
|
+
} ${
|
|
77
|
+
editorStyles?.backgroundColor
|
|
78
|
+
? `background-color: ${editorStyles.backgroundColor};`
|
|
79
|
+
: ''
|
|
80
|
+
}">${wrapper.outerHTML}</div>`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const richClipboardCopier = EditorView.domEventHandlers({
|
|
84
|
+
copy: (event, view) => {
|
|
85
|
+
event.preventDefault();
|
|
86
|
+
|
|
87
|
+
const { from, to } = view.state.selection.main;
|
|
88
|
+
const selectedText = view.state.doc.sliceString(from, to);
|
|
89
|
+
|
|
90
|
+
if (!selectedText) return;
|
|
91
|
+
|
|
92
|
+
if (event.clipboardData) {
|
|
93
|
+
event.clipboardData.setData('text/plain', selectedText);
|
|
94
|
+
if (selectedText.length < 1000) {
|
|
95
|
+
event.clipboardData.setData('text/html', getHTML(view, from, to));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
});
|