@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.
@@ -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 { LinterTask } from '@neo4j-cypher/lint-worker';
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: 2,
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 (lastSemanticJob !== undefined && !lastSemanticJob.resolved) {
31
- void lastSemanticJob.cancel();
28
+ if (pool.stats().busyWorkers > 0) {
29
+ await pool.terminate(true);
32
30
  }
33
-
34
- lastSemanticJob = pool.exec("lintCypherQuery", [query, config.schema ?? {}, config.featureFlags ?? {}])
35
- const result = await lastSemanticJob;
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 instanceof workerpool.Promise.CancellationError)) {
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
+ });