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

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neo4j-cypher/react-codemirror",
3
- "version": "2.0.0-next.39",
3
+ "version": "2.0.0-next.40",
4
4
  "keywords": [
5
5
  "codemirror",
6
6
  "codemirror 6",
@@ -41,15 +41,13 @@
41
41
  "@lezer/common": "^1.0.2",
42
42
  "@lezer/highlight": "^1.1.3",
43
43
  "@types/prismjs": "^1.26.3",
44
- "@types/workerpool": "^6.4.7",
45
- "fastest-levenshtein": "^1.0.16",
46
44
  "lodash.debounce": "^4.0.8",
47
45
  "prismjs": "^1.29.0",
48
46
  "style-mod": "^4.1.2",
49
47
  "vscode-languageserver-types": "^3.17.3",
50
48
  "workerpool": "^9.3.3",
51
- "@neo4j-cypher/lint-worker": "1.10.1-next.13",
52
- "@neo4j-cypher/language-support": "2.0.0-next.36"
49
+ "@neo4j-cypher/language-support": "2.0.0-next.37",
50
+ "@neo4j-cypher/lint-worker": "1.10.1-next.14"
53
51
  },
54
52
  "devDependencies": {
55
53
  "@neo4j-ndl/base": "^3.2.10",
@@ -61,7 +59,6 @@
61
59
  "@vitejs/plugin-react": "^4.3.1",
62
60
  "copyfiles": "^2.4.1",
63
61
  "jsdom": "^24.1.1",
64
- "lodash": "^4.17.21",
65
62
  "playwright": "^1.55.1",
66
63
  "react": "^18.2.0",
67
64
  "react-dom": "^18.2.0",
@@ -46,6 +46,19 @@ import { LintWorker } from '@neo4j-cypher/lint-worker';
46
46
  import workerpool from 'workerpool';
47
47
 
48
48
  type DomEventHandlers = Parameters<typeof EditorView.domEventHandlers>[0];
49
+
50
+ /**
51
+ * Normalize CRLF line endings to LF, which is what CodeMirror expects.
52
+ * CodeMirror collapses `\r\n` into a single line break, so a raw `value`
53
+ * string with CRLF endings is longer than the resulting document. Normalizing
54
+ * up front keeps the document length in sync with the value length used for
55
+ * cursor placement, and avoids "Selection points outside of document" errors.
56
+ * https://codemirror.net/docs/ref/#state.EditorState^lineSeparator
57
+ */
58
+ function normalizeLineEndings(value: string): string {
59
+ return value.replace(/\r\n/g, '\n');
60
+ }
61
+
49
62
  export interface CypherEditorProps {
50
63
  /**
51
64
  * The prompt to show on single line editors
@@ -438,8 +451,17 @@ export class CypherEditor extends Component<
438
451
  * For example, to move the cursor to the end of the editor, use `value.length`
439
452
  */
440
453
  updateCursorPosition(position: number) {
441
- this.editorView.current?.dispatch({
442
- selection: { anchor: position, head: position },
454
+ const view = this.editorView.current;
455
+ if (!view) {
456
+ return;
457
+ }
458
+
459
+ // Clamp to the doc length; `position` may come from a raw value longer than
460
+ // the doc (e.g. CRLF endings), which would throw "Selection points outside
461
+ // of document".
462
+ const clamped = Math.max(0, Math.min(position, view.state.doc.length));
463
+ view.dispatch({
464
+ selection: { anchor: clamped, head: clamped },
443
465
  });
444
466
  }
445
467
 
@@ -448,10 +470,7 @@ export class CypherEditor extends Component<
448
470
  */
449
471
  setValueAndFocus(value = '') {
450
472
  const currentCmValue = this.editorView.current.state?.doc.toString() ?? '';
451
- // Normalize line endings to LF that CM expects.
452
- // Prevents issues with inserted values that contain CRLF line endings.
453
- // https://codemirror.net/docs/ref/?utm_source=chatgpt.com#state.EditorState^lineSeparator
454
- const normalizedValue = value.replace(/\r\n/g, '\n');
473
+ const normalizedValue = normalizeLineEndings(value);
455
474
  this.editorView.current.dispatch({
456
475
  changes: {
457
476
  from: 0,
@@ -603,7 +622,7 @@ export class CypherEditor extends Component<
603
622
  ),
604
623
  this.editorActionsController.extension,
605
624
  ],
606
- doc: this.props.value,
625
+ doc: normalizeLineEndings(this.props.value ?? ''),
607
626
  });
608
627
 
609
628
  this.editorView.current = new EditorView({
@@ -701,7 +720,7 @@ export class CypherEditor extends Component<
701
720
  changes: {
702
721
  from: 0,
703
722
  to: currentCmValue.length,
704
- insert: this.props.value ?? '',
723
+ insert: normalizeLineEndings(this.props.value ?? ''),
705
724
  },
706
725
  annotations: [ExternalEdit.of(true)],
707
726
  });
@@ -42,6 +42,29 @@ test('the editor can report changes to the text ', async ({ mount, page }) => {
42
42
  }).toPass({ intervals: [300, 300, 1000] });
43
43
  });
44
44
 
45
+ test('can mount an autofocused editor with CRLF line endings without crashing', async ({
46
+ mount,
47
+ }) => {
48
+ const value = 'MATCH (n)\r\nRETURN n;';
49
+
50
+ const component = await mount(<CypherEditor value={value} autofocus />);
51
+
52
+ await expect(component).toContainText('MATCH (n)');
53
+ await expect(component).toContainText('RETURN n;');
54
+ });
55
+
56
+ test('can externally update to a value with CRLF line endings without crashing', async ({
57
+ mount,
58
+ }) => {
59
+ const component = await mount(<CypherEditor value="MATCH (n)" autofocus />);
60
+
61
+ await component.update(
62
+ <CypherEditor value={'MATCH (n)\r\nRETURN n;'} autofocus />,
63
+ );
64
+
65
+ await expect(component).toContainText('RETURN n;');
66
+ });
67
+
45
68
  test('can complete RETURN', async ({ page, mount }) => {
46
69
  await mount(<CypherEditor />);
47
70
  const textField = page.getByRole('textbox');
@@ -82,7 +82,7 @@ export function createEditorActionsController(): EditorActionsController {
82
82
  const spacer = EditorView.decorations.compute(
83
83
  ['doc', activeField],
84
84
  (state) => {
85
- if (!state.field(activeField) || state.doc.length === 0) {
85
+ if (!state.field(activeField)) {
86
86
  return Decoration.none;
87
87
  }
88
88
  return Decoration.set([
@@ -230,6 +230,9 @@ export function createEditorActionsController(): EditorActionsController {
230
230
  WebkitUserSelect: 'none',
231
231
  pointerEvents: 'none',
232
232
  },
233
+ '.cm-placeholder': {
234
+ display: 'inline',
235
+ },
233
236
  '.cm-editor-actions': {
234
237
  position: 'absolute',
235
238
  top: 'var(--cm-editor-actions-top, 0px)',
@@ -11,7 +11,7 @@ import type { HostPortalCallbacks } from './hostCallbacks';
11
11
  /** Lifecycle callbacks for an inline panel. See {@link HostPortalCallbacks}. */
12
12
  export type InlinePanelCallbacks = HostPortalCallbacks;
13
13
 
14
- export type InlinePanelShowOptions = {
14
+ type InlinePanelShowOptions = {
15
15
  /** Document position the panel anchors to. */
16
16
  pos: number;
17
17
  /**
@@ -42,9 +42,13 @@ export const cypherTokenTypeToNode = (facet: Facet<unknown>) => ({
42
42
  number: NodeType.define({ id: 28, name: 'numberLiteral' }),
43
43
  setting: NodeType.define({ id: 29, name: 'setting' }),
44
44
  settingValue: NodeType.define({ id: 30, name: 'settingValue' }),
45
+ interpolationDelimiter: NodeType.define({
46
+ id: 31,
47
+ name: 'interpolationDelimiter',
48
+ }),
45
49
  });
46
50
 
47
- export type PrismSpecificTokenType =
51
+ type PrismSpecificTokenType =
48
52
  | 'class-name'
49
53
  | 'identifier'
50
54
  | 'string'
@@ -68,6 +72,7 @@ export const tokenTypeToStyleTag: Record<HighlightedCypherTokenTypes, Tag> = {
68
72
  variable: tags.variableName,
69
73
  paramDollar: tags.atom,
70
74
  paramValue: tags.atom,
75
+ interpolationDelimiter: tags.atom,
71
76
  symbolicName: tags.variableName,
72
77
  operator: tags.operator,
73
78
  stringLiteral: tags.string,