@kerebron/extension-codemirror 0.4.27 → 0.4.29

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 (92) hide show
  1. package/esm/ExtensionCodeMirror.d.ts +17 -0
  2. package/esm/ExtensionCodeMirror.d.ts.map +1 -0
  3. package/esm/ExtensionCodeMirror.js +62 -0
  4. package/esm/ExtensionCodeMirror.js.map +1 -0
  5. package/esm/NodeCodeMirror.d.ts +20 -0
  6. package/esm/NodeCodeMirror.d.ts.map +1 -0
  7. package/esm/NodeCodeMirror.js +96 -0
  8. package/esm/NodeCodeMirror.js.map +1 -0
  9. package/esm/codeMirrorBlockNodeView.d.ts +7 -0
  10. package/esm/codeMirrorBlockNodeView.d.ts.map +1 -0
  11. package/esm/codeMirrorBlockNodeView.js +269 -0
  12. package/esm/codeMirrorBlockNodeView.js.map +1 -0
  13. package/esm/defaults.d.ts +6 -0
  14. package/esm/defaults.d.ts.map +1 -0
  15. package/esm/defaults.js +59 -0
  16. package/esm/defaults.js.map +1 -0
  17. package/esm/languageLoaders.d.ts +5 -0
  18. package/esm/languageLoaders.d.ts.map +1 -0
  19. package/esm/languageLoaders.js +113 -0
  20. package/esm/languageLoaders.js.map +1 -0
  21. package/esm/languages.d.ts +109 -0
  22. package/esm/languages.d.ts.map +1 -0
  23. package/esm/languages.js +111 -0
  24. package/esm/languages.js.map +1 -0
  25. package/esm/lsp/LSPExtension.d.ts +14 -0
  26. package/esm/lsp/LSPExtension.d.ts.map +1 -0
  27. package/esm/lsp/LSPExtension.js +30 -0
  28. package/esm/lsp/LSPExtension.js.map +1 -0
  29. package/esm/lsp/completion.d.ts +8 -0
  30. package/esm/lsp/completion.d.ts.map +1 -0
  31. package/esm/lsp/completion.js +180 -0
  32. package/esm/lsp/completion.js.map +1 -0
  33. package/esm/lsp/hover.d.ts +5 -0
  34. package/esm/lsp/hover.d.ts.map +1 -0
  35. package/esm/lsp/hover.js +82 -0
  36. package/esm/lsp/hover.js.map +1 -0
  37. package/esm/lsp/index.d.ts +7 -0
  38. package/esm/lsp/index.d.ts.map +1 -0
  39. package/esm/lsp/index.js +50 -0
  40. package/esm/lsp/index.js.map +1 -0
  41. package/esm/lsp/plugin.d.ts +37 -0
  42. package/esm/lsp/plugin.d.ts.map +1 -0
  43. package/esm/lsp/plugin.js +87 -0
  44. package/esm/lsp/plugin.js.map +1 -0
  45. package/esm/lsp/pos.d.ts +5 -0
  46. package/esm/lsp/pos.d.ts.map +1 -0
  47. package/esm/lsp/pos.js +9 -0
  48. package/esm/lsp/pos.js.map +1 -0
  49. package/esm/lsp/text.d.ts +2 -0
  50. package/esm/lsp/text.d.ts.map +1 -0
  51. package/esm/lsp/text.js +48 -0
  52. package/esm/lsp/text.js.map +1 -0
  53. package/esm/lsp/theme.d.ts +2 -0
  54. package/esm/lsp/theme.d.ts.map +1 -0
  55. package/esm/lsp/theme.js +77 -0
  56. package/esm/lsp/theme.js.map +1 -0
  57. package/esm/package.json +3 -0
  58. package/esm/remote-selections.d.ts +15 -0
  59. package/esm/remote-selections.d.ts.map +1 -0
  60. package/esm/remote-selections.js +226 -0
  61. package/esm/remote-selections.js.map +1 -0
  62. package/esm/remote-sync.d.ts +11 -0
  63. package/esm/remote-sync.d.ts.map +1 -0
  64. package/esm/remote-sync.js +18 -0
  65. package/esm/remote-sync.js.map +1 -0
  66. package/esm/types.d.ts +30 -0
  67. package/esm/types.d.ts.map +1 -0
  68. package/esm/types.js +2 -0
  69. package/esm/types.js.map +1 -0
  70. package/esm/utils.d.ts +40 -0
  71. package/esm/utils.d.ts.map +1 -0
  72. package/esm/utils.js +211 -0
  73. package/esm/utils.js.map +1 -0
  74. package/package.json +9 -6
  75. package/src/ExtensionCodeMirror.ts +84 -0
  76. package/src/NodeCodeMirror.ts +135 -0
  77. package/src/codeMirrorBlockNodeView.ts +400 -0
  78. package/src/defaults.ts +80 -0
  79. package/src/languageLoaders.ts +401 -0
  80. package/src/languages.ts +109 -0
  81. package/src/lsp/LSPExtension.ts +42 -0
  82. package/src/lsp/completion.ts +231 -0
  83. package/src/lsp/hover.ts +100 -0
  84. package/src/lsp/index.ts +55 -0
  85. package/src/lsp/plugin.ts +128 -0
  86. package/src/lsp/pos.ts +12 -0
  87. package/src/lsp/text.ts +63 -0
  88. package/src/lsp/theme.ts +81 -0
  89. package/src/remote-selections.ts +263 -0
  90. package/src/remote-sync.ts +23 -0
  91. package/src/types.ts +55 -0
  92. package/src/utils.ts +336 -0
@@ -0,0 +1,263 @@
1
+ import * as cmView from '@codemirror/view';
2
+
3
+ import * as cmState from '@codemirror/state';
4
+ import * as dom from 'lib0/dom';
5
+ import * as pair from 'lib0/pair';
6
+ import * as math from 'lib0/math';
7
+
8
+ import { RemoteSyncConfig, remoteSyncFacet } from './remote-sync.js';
9
+ import type { CoreEditor } from '@kerebron/editor';
10
+
11
+ import type { ExtensionRemoteSelection } from '@kerebron/extension-basic-editor/ExtensionRemoteSelection';
12
+
13
+ export const yRemoteSelectionsTheme = cmView.EditorView.baseTheme({
14
+ '.cm-rSelection': {},
15
+ '.cm-rLineSelection': {
16
+ padding: 0,
17
+ margin: '0px 2px 0px 4px',
18
+ },
19
+ '.cm-rSelectionCaret': {
20
+ position: 'relative',
21
+ borderLeft: '1px solid black',
22
+ borderRight: '1px solid black',
23
+ marginLeft: '-1px',
24
+ marginRight: '-1px',
25
+ boxSizing: 'border-box',
26
+ display: 'inline',
27
+ },
28
+ '.cm-rSelectionCaretDot': {
29
+ borderRadius: '50%',
30
+ position: 'absolute',
31
+ width: '.4em',
32
+ height: '.4em',
33
+ top: '-.2em',
34
+ left: '-.2em',
35
+ backgroundColor: 'inherit',
36
+ transition: 'transform .3s ease-in-out',
37
+ boxSizing: 'border-box',
38
+ },
39
+ '.cm-rSelectionCaret:hover > .cm-rSelectionCaretDot': {
40
+ transformOrigin: 'bottom center',
41
+ transform: 'scale(0)',
42
+ },
43
+ '.cm-rSelectionInfo': {
44
+ position: 'absolute',
45
+ top: '-1.05em',
46
+ left: '-1px',
47
+ fontSize: '.75em',
48
+ fontFamily: 'serif',
49
+ fontStyle: 'normal',
50
+ fontWeight: 'normal',
51
+ lineHeight: 'normal',
52
+ userSelect: 'none',
53
+ color: 'white',
54
+ paddingLeft: '2px',
55
+ paddingRight: '2px',
56
+ zIndex: 101,
57
+ transition: 'opacity .3s ease-in-out',
58
+ backgroundColor: 'inherit',
59
+ // these should be separate
60
+ opacity: 0,
61
+ transitionDelay: '0s',
62
+ whiteSpace: 'nowrap',
63
+ },
64
+ '.cm-rSelectionCaret:hover > .cm-rSelectionInfo': {
65
+ opacity: 1,
66
+ transitionDelay: '0s',
67
+ },
68
+ });
69
+
70
+ const yRemoteSelectionsAnnotation: cmState.AnnotationType<Array<number>> =
71
+ cmState.Annotation.define();
72
+
73
+ class YRemoteCaretWidget extends cmView.WidgetType {
74
+ constructor(public color: string, public name: string) {
75
+ super();
76
+ }
77
+
78
+ toDOM(): HTMLElement {
79
+ return <HTMLElement> (dom.element('span', [
80
+ pair.create('class', 'kb-yjs__cursor kb-widget'),
81
+ pair.create('style', `border-color: ${this.color};`),
82
+ ], [
83
+ dom.text('\u2060'),
84
+ dom.element('div', [
85
+ pair.create('style', `background-color: ${this.color}`),
86
+ ], [
87
+ dom.text(this.name),
88
+ ]),
89
+ dom.text('\u2060'),
90
+ ]));
91
+ }
92
+
93
+ override eq(widget: YRemoteCaretWidget) {
94
+ return widget.color === this.color;
95
+ }
96
+
97
+ compare(widget: YRemoteCaretWidget) {
98
+ return widget.color === this.color;
99
+ }
100
+
101
+ override updateDOM() {
102
+ return false;
103
+ }
104
+
105
+ override get estimatedHeight() {
106
+ return -1;
107
+ }
108
+
109
+ override ignoreEvent() {
110
+ return true;
111
+ }
112
+ }
113
+
114
+ export class YRemoteSelectionsPluginValue {
115
+ conf: RemoteSyncConfig;
116
+ private editor: CoreEditor;
117
+ decorations: cmView.DecorationSet;
118
+ private _remoteSelectionChange: () => void;
119
+
120
+ constructor(view: cmView.EditorView) {
121
+ this.conf = view.state.facet(remoteSyncFacet);
122
+ this.editor = this.conf.editor;
123
+ this.decorations = cmState.RangeSet.of([]);
124
+
125
+ this._remoteSelectionChange = () => {
126
+ view.dispatch({ annotations: [yRemoteSelectionsAnnotation.of([])] });
127
+ };
128
+ this.editor.addEventListener(
129
+ 'remoteSelectionChange',
130
+ this._remoteSelectionChange,
131
+ );
132
+ }
133
+
134
+ destroy() {
135
+ this.editor.removeEventListener(
136
+ 'remoteSelectionChange',
137
+ this._remoteSelectionChange,
138
+ );
139
+ }
140
+
141
+ update(update: cmView.ViewUpdate) {
142
+ const decorations: cmState.Range<cmView.Decoration>[] = [];
143
+
144
+ const extension: ExtensionRemoteSelection = this.editor.getExtension(
145
+ 'remote-selection',
146
+ )!;
147
+
148
+ const remoteStates = extension.getRemoteStates();
149
+ for (const state of remoteStates) {
150
+ const clientId = state.clientId;
151
+
152
+ const cursor = state.cursor;
153
+ if (cursor?.anchor == null || cursor?.head == null) {
154
+ return;
155
+ }
156
+
157
+ const nodeAnchor = this.conf.getPmPos();
158
+ if ('undefined' !== typeof nodeAnchor) {
159
+ const nodeHead = nodeAnchor + this.conf.getNode().nodeSize;
160
+
161
+ if (
162
+ cursor.anchor >= nodeAnchor && cursor.anchor < nodeHead &&
163
+ cursor.head >= nodeAnchor && cursor.head < nodeHead
164
+ ) {
165
+ const anchor = { index: cursor.anchor - nodeAnchor };
166
+ const head = { index: cursor.head - nodeAnchor };
167
+
168
+ try {
169
+ const { color = '#ffa500', name = `User: ${clientId}` } =
170
+ state.user || {};
171
+ const colorLight = (state.user && state.user.colorLight) ||
172
+ color + '33';
173
+ const start = math.min(anchor.index, head.index);
174
+ const end = math.max(anchor.index, head.index);
175
+ const startLine = update.view.state.doc.lineAt(start);
176
+ const endLine = update.view.state.doc.lineAt(end);
177
+ if (startLine.number === endLine.number) {
178
+ // selected content in a single line.
179
+ decorations.push({
180
+ from: start,
181
+ to: end,
182
+ value: cmView.Decoration.mark({
183
+ attributes: { style: `background-color: ${colorLight}` },
184
+ class: 'cm-rSelection',
185
+ }),
186
+ });
187
+ } else {
188
+ // selected content in multiple lines
189
+ // first, render text-selection in the first line
190
+ decorations.push({
191
+ from: start,
192
+ to: startLine.from + startLine.length,
193
+ value: cmView.Decoration.mark({
194
+ attributes: { style: `background-color: ${colorLight}` },
195
+ class: 'cm-rSelection',
196
+ }),
197
+ });
198
+ // render text-selection in the last line
199
+ decorations.push({
200
+ from: endLine.from,
201
+ to: end,
202
+ value: cmView.Decoration.mark({
203
+ attributes: { style: `background-color: ${colorLight}` },
204
+ class: 'cm-rSelection',
205
+ }),
206
+ });
207
+ for (let i = startLine.number + 1; i < endLine.number; i++) {
208
+ const linePos = update.view.state.doc.line(i).from;
209
+ decorations.push({
210
+ from: linePos,
211
+ to: linePos,
212
+ value: cmView.Decoration.line({
213
+ attributes: {
214
+ style: `background-color: ${colorLight}`,
215
+ class: 'cm-rLineSelection',
216
+ },
217
+ }),
218
+ });
219
+ }
220
+ }
221
+ decorations.push({
222
+ from: head.index,
223
+ to: head.index,
224
+ value: cmView.Decoration.widget({
225
+ side: head.index - anchor.index > 0 ? -1 : 1, // the local cursor should be rendered outside the remote selection
226
+ block: false,
227
+ widget: new YRemoteCaretWidget(color, name),
228
+ }),
229
+ });
230
+ } catch (err) {
231
+ console.warn(err, `User: ${clientId}`);
232
+ }
233
+ }
234
+ }
235
+ }
236
+
237
+ this.decorations = cmView.Decoration.set(decorations, true);
238
+
239
+ const hasFocus = update.view.hasFocus &&
240
+ update.view.dom.ownerDocument.hasFocus();
241
+ const sel = hasFocus ? update.state.selection.main : null;
242
+ const nodePos = this.conf.getPmPos();
243
+ if (sel != null && 'undefined' !== typeof nodePos) {
244
+ const anchor = nodePos + sel.anchor;
245
+ const head = nodePos + sel.head;
246
+
247
+ const event = new CustomEvent('localPositionChanged', {
248
+ detail: {
249
+ anchor,
250
+ head,
251
+ },
252
+ });
253
+ this.editor.dispatchEvent(event);
254
+ }
255
+ }
256
+ }
257
+
258
+ export const yRemoteSelections = cmView.ViewPlugin.fromClass(
259
+ YRemoteSelectionsPluginValue,
260
+ {
261
+ decorations: (v) => v.decorations,
262
+ },
263
+ );
@@ -0,0 +1,23 @@
1
+ import * as cmState from '@codemirror/state';
2
+
3
+ import { Node } from 'prosemirror-model';
4
+ import type { CoreEditor } from '@kerebron/editor';
5
+
6
+ export class RemoteSyncConfig {
7
+ constructor(
8
+ public getNode: () => Node,
9
+ public getPmPos: () => number | undefined,
10
+ public editor: CoreEditor,
11
+ ) {
12
+ }
13
+ }
14
+
15
+ export const remoteSyncFacet: cmState.Facet<
16
+ RemoteSyncConfig,
17
+ RemoteSyncConfig
18
+ > = cmState.Facet
19
+ .define({
20
+ combine(inputs) {
21
+ return inputs[inputs.length - 1];
22
+ },
23
+ });
package/src/types.ts ADDED
@@ -0,0 +1,55 @@
1
+ import { EditorState, Transaction } from 'prosemirror-state';
2
+ import { EditorView as CodemirrorView } from '@codemirror/view';
3
+ import { Node } from 'prosemirror-model';
4
+ import { EditorView } from 'prosemirror-view';
5
+ import { LanguageSupport } from '@codemirror/language';
6
+ import { Extension as CmExtension } from '@codemirror/state';
7
+ import { type Transport } from '@kerebron/extension-lsp';
8
+
9
+ export type LanguageLoaders = Record<string, () => Promise<LanguageSupport>>;
10
+
11
+ export type ThemeItem = { extension: CmExtension; name: string };
12
+
13
+ export type CodeBlockSettings = {
14
+ createSelect: (
15
+ settings: CodeBlockSettings,
16
+ dom: HTMLElement,
17
+ node: Node,
18
+ view: EditorView,
19
+ getPos: () => number | undefined,
20
+ ) => () => void;
21
+ updateSelect: (
22
+ settings: CodeBlockSettings,
23
+ dom: HTMLElement,
24
+ node: Node,
25
+ view: EditorView,
26
+ getPos: () => number | undefined,
27
+ oldNode: Node,
28
+ ) => void;
29
+ createCopyButton: (
30
+ settings: CodeBlockSettings,
31
+ dom: HTMLElement,
32
+ node: Node,
33
+ view: EditorView,
34
+ codeMirrorView: CodemirrorView,
35
+ getPos: () => number | undefined,
36
+ ) => () => void;
37
+ stopEvent: (
38
+ e: Event,
39
+ node: Node,
40
+ getPos: () => number | undefined,
41
+ view: EditorView,
42
+ dom: HTMLElement,
43
+ ) => boolean;
44
+ languageLoaders?: LanguageLoaders;
45
+ languageNameMap?: Record<string, string>;
46
+ languageWhitelist?: string[];
47
+ undo?: (state: EditorState, dispatch: (tr: Transaction) => void) => void;
48
+ redo?: (state: EditorState, dispatch: (tr: Transaction) => void) => void;
49
+ theme?: CmExtension[];
50
+ themes: ThemeItem[];
51
+ readOnly?: boolean;
52
+ getCurrentTheme?: () => string;
53
+ codeBlockName?: string;
54
+ lspTransport?: Transport;
55
+ };
package/src/utils.ts ADDED
@@ -0,0 +1,336 @@
1
+ // From prosemirror guide
2
+ import {
3
+ Command,
4
+ EditorState,
5
+ Selection,
6
+ TextSelection,
7
+ Transaction,
8
+ } from 'prosemirror-state';
9
+ import { EditorView as PMEditorView } from 'prosemirror-view';
10
+ import { Node } from 'prosemirror-model';
11
+
12
+ import { EditorView } from '@codemirror/view';
13
+ import { Compartment } from '@codemirror/state';
14
+
15
+ import type { CodeBlockSettings, ThemeItem } from './types.js';
16
+ import type { CoreEditor } from '@kerebron/editor';
17
+ import { LSPPlugin } from './lsp/plugin.js';
18
+
19
+ export const CodeBlockNodeName = 'code_block';
20
+
21
+ function nonEmpty<TValue>(value: TValue | null | undefined): value is TValue {
22
+ return value !== null && value !== undefined;
23
+ }
24
+
25
+ export function computeChange(oldVal: string, newVal: string) {
26
+ if (oldVal === newVal) return null;
27
+ let start = 0;
28
+ let oldEnd = oldVal.length;
29
+ let newEnd = newVal.length;
30
+ while (
31
+ start < oldEnd &&
32
+ oldVal.charCodeAt(start) === newVal.charCodeAt(start)
33
+ ) {
34
+ start += 1;
35
+ }
36
+ while (
37
+ oldEnd > start &&
38
+ newEnd > start &&
39
+ oldVal.charCodeAt(oldEnd - 1) === newVal.charCodeAt(newEnd - 1)
40
+ ) {
41
+ oldEnd -= 1;
42
+ newEnd -= 1;
43
+ }
44
+ return { from: start, to: oldEnd, text: newVal.slice(start, newEnd) };
45
+ }
46
+
47
+ export const asProseMirrorSelection = (
48
+ pmDoc: Node,
49
+ cmView: EditorView,
50
+ getPos: () => number | undefined,
51
+ ) => {
52
+ const offset = (typeof getPos === 'function' ? getPos() || 0 : 0) + 1;
53
+ const anchor = cmView.state.selection.main.from + offset;
54
+ const head = cmView.state.selection.main.to + offset;
55
+ return TextSelection.create(pmDoc, anchor, head);
56
+ };
57
+
58
+ export const forwardSelection = (
59
+ cmView: EditorView,
60
+ pmView: PMEditorView,
61
+ getPos: () => number | undefined,
62
+ ) => {
63
+ if (!cmView.hasFocus) return;
64
+ const selection = asProseMirrorSelection(pmView.state.doc, cmView, getPos);
65
+ if (!selection.eq(pmView.state.selection)) {
66
+ pmView.dispatch(pmView.state.tr.setSelection(selection));
67
+ }
68
+ };
69
+
70
+ export const valueChanged = (
71
+ textUpdate: string,
72
+ node: Node,
73
+ getPos: () => number | undefined,
74
+ view: PMEditorView,
75
+ ) => {
76
+ const change = computeChange(node.textContent, textUpdate);
77
+
78
+ if (change) {
79
+ const pos = getPos();
80
+ if ('undefined' !== typeof pos) {
81
+ const start = pos + 1;
82
+
83
+ let pmTr = view.state.tr;
84
+ pmTr = pmTr.replaceWith(
85
+ start + change.from,
86
+ start + change.to,
87
+ change.text ? view.state.schema.text(change.text) : [],
88
+ );
89
+ view.dispatch(pmTr);
90
+ }
91
+ }
92
+ };
93
+
94
+ export const maybeEscape = (
95
+ unit: 'char' | 'line',
96
+ dir: -1 | 1,
97
+ cm: EditorView,
98
+ view: PMEditorView,
99
+ getPos: () => number | undefined,
100
+ ) => {
101
+ const sel = cm.state.selection.main;
102
+ const line = cm.state.doc.lineAt(sel.from);
103
+ const lastLine = cm.state.doc.lines;
104
+ if (
105
+ sel.to !== sel.from ||
106
+ line.number !== (dir < 0 ? 1 : lastLine) ||
107
+ (unit === 'char' && sel.from !== (dir < 0 ? 0 : line.to)) ||
108
+ typeof getPos !== 'function'
109
+ ) {
110
+ return false;
111
+ }
112
+ view.focus();
113
+ const pos = getPos();
114
+ if (!pos) {
115
+ return false;
116
+ }
117
+ const node = view.state.doc.nodeAt(pos);
118
+ if (!node) return false;
119
+ const targetPos = pos + (dir < 0 ? 0 : node.nodeSize);
120
+ const selection = Selection.near(view.state.doc.resolve(targetPos), dir);
121
+ view.dispatch(view.state.tr.setSelection(selection).scrollIntoView());
122
+ view.focus();
123
+ return true;
124
+ };
125
+
126
+ export const backspaceHandler = (
127
+ pmView: PMEditorView,
128
+ view: EditorView,
129
+ editor: CoreEditor,
130
+ ) => {
131
+ const { selection } = view.state;
132
+ if (selection.main.empty && selection.main.from === 0) {
133
+ editor.commandFactories.setBlockType(pmView.state.schema.nodes.paragraph)(
134
+ pmView.state,
135
+ pmView.dispatch,
136
+ );
137
+ setTimeout(() => pmView.focus(), 20);
138
+ return true;
139
+ }
140
+ return false;
141
+ };
142
+
143
+ export const setMode = async (
144
+ lang: string,
145
+ cmView: EditorView,
146
+ settings: CodeBlockSettings,
147
+ languageConf: Compartment,
148
+ ) => {
149
+ const support = await settings.languageLoaders?.[lang]?.();
150
+ if (support) {
151
+ cmView.dispatch({
152
+ effects: languageConf.reconfigure(support),
153
+ });
154
+ }
155
+
156
+ const lspPlugin = LSPPlugin.get(cmView);
157
+ if (lspPlugin) {
158
+ lspPlugin.setLang(lang);
159
+ }
160
+ };
161
+
162
+ const isTheme = (theme: Array<ThemeItem | undefined>): theme is ThemeItem[] => {
163
+ if (!Array.isArray(theme)) {
164
+ return false;
165
+ }
166
+ return theme.every(
167
+ (item) =>
168
+ item !== undefined &&
169
+ typeof item.extension === 'object' && // or whatever type Extension is
170
+ typeof item.name === 'string',
171
+ );
172
+ };
173
+
174
+ export const setTheme = async (
175
+ cmView: EditorView,
176
+ themeConfig: Compartment,
177
+ theme: Array<ThemeItem | undefined>,
178
+ ) => {
179
+ if (isTheme(theme)) {
180
+ cmView.dispatch({
181
+ effects: themeConfig.reconfigure(theme),
182
+ });
183
+ }
184
+ };
185
+
186
+ const arrowHandler: (dir: 'left' | 'right' | 'up' | 'down') => Command =
187
+ (dir) =>
188
+ (
189
+ state: EditorState,
190
+ dispatch: ((tr: Transaction) => void) | undefined,
191
+ view?: PMEditorView,
192
+ ): boolean => {
193
+ if (state.selection.empty && view?.endOfTextblock(dir)) {
194
+ const side = dir === 'left' || dir === 'up' ? -1 : 1;
195
+ const { $head } = state.selection;
196
+ const nextPos = Selection.near(
197
+ state.doc.resolve(side > 0 ? $head.after() : $head.before()),
198
+ side,
199
+ );
200
+ if (
201
+ nextPos.$head &&
202
+ nextPos.$head.parent.type.name === CodeBlockNodeName
203
+ ) {
204
+ dispatch?.(state.tr.setSelection(nextPos));
205
+ return true;
206
+ }
207
+ }
208
+ return false;
209
+ };
210
+
211
+ export const createCodeBlock = (
212
+ state: EditorState,
213
+ dispatch: ((tr: Transaction) => void) | undefined,
214
+ attributes: object,
215
+ ) => {
216
+ const { $from, $to } = state.selection;
217
+ //if we are in a CodeBlock node we do nothing
218
+ const parentNode = $from.node($from.depth);
219
+ if (parentNode && parentNode.type.name === CodeBlockNodeName) {
220
+ return false;
221
+ }
222
+ //if from and to in the same paragraph
223
+ if (
224
+ $from.parentOffset === $to.parentOffset &&
225
+ $from.parent.type.name === 'paragraph'
226
+ ) {
227
+ const text = $from.parent.textContent;
228
+ const tr = state.tr;
229
+ const newNode = state.schema.nodes[CodeBlockNodeName].createAndFill(
230
+ attributes,
231
+ text ? [state.schema.text($from.parent.textContent)] : [],
232
+ );
233
+ if (newNode && dispatch) {
234
+ const pos = $from.before($from.depth);
235
+ tr.delete(pos, pos + $from.parent.nodeSize - 1);
236
+ tr.insert(pos, newNode);
237
+ tr.setSelection(TextSelection.create(tr.doc, $from.pos));
238
+ dispatch(tr);
239
+ }
240
+ return true;
241
+ }
242
+
243
+ if (dispatch) {
244
+ const tr = state.tr;
245
+
246
+ const slice = state.doc.slice(
247
+ $from.before($from.depth),
248
+ $to.after($to.depth),
249
+ true,
250
+ );
251
+ const content = slice.content.textBetween(0, slice.content.size, '\n');
252
+ const newNode = state.schema.nodes[CodeBlockNodeName].createAndFill(
253
+ attributes,
254
+ state.schema.text(content),
255
+ );
256
+
257
+ if (newNode) {
258
+ tr.delete(
259
+ $from.before(slice.openStart + 1),
260
+ $to.after(slice.openEnd + 1),
261
+ );
262
+ tr.insert($from.before(slice.openStart + 1), newNode);
263
+ tr.setSelection(
264
+ TextSelection.create(
265
+ tr.doc,
266
+ $from.pos,
267
+ $from.pos + newNode.nodeSize - 2,
268
+ ),
269
+ );
270
+ dispatch(tr);
271
+ }
272
+ }
273
+ return true;
274
+ };
275
+
276
+ export const removeCodeBlock = (
277
+ state: EditorState,
278
+ dispatch: ((tr: Transaction) => void) | undefined,
279
+ ) => {
280
+ const { $from } = state.selection;
281
+ const parentNode = $from.node($from.depth);
282
+ if (parentNode && parentNode.type.name === CodeBlockNodeName) {
283
+ const children: Node[] = [];
284
+ parentNode.forEach((child) => {
285
+ children.push(child);
286
+ });
287
+ const childrenNodes = children
288
+ .map((child) => {
289
+ return state.schema.nodes.paragraph.createAndFill({}, [child]);
290
+ })
291
+ .filter(nonEmpty);
292
+ const tr = state.tr;
293
+ const pos = $from.before($from.depth);
294
+ tr.delete(pos, pos + parentNode.nodeSize - 1);
295
+ tr.insert(pos, childrenNodes);
296
+ tr.setSelection(TextSelection.create(tr.doc, $from.pos - 1));
297
+ if (dispatch) {
298
+ dispatch(tr.scrollIntoView());
299
+ }
300
+ }
301
+ return false;
302
+ };
303
+
304
+ export const toggleCodeBlock = (
305
+ state: EditorState,
306
+ dispatch: ((tr: Transaction) => void) | undefined,
307
+ attributes: object,
308
+ ) => {
309
+ const { $from } = state.selection;
310
+ if ($from.pos === 0) {
311
+ return false;
312
+ }
313
+ const parentNode = $from.node($from.depth);
314
+
315
+ if (parentNode && parentNode.type.name === CodeBlockNodeName) {
316
+ return removeCodeBlock(state, dispatch);
317
+ } else {
318
+ return createCodeBlock(state, dispatch, attributes);
319
+ }
320
+ };
321
+
322
+ export const codeBlockArrowHandlers = {
323
+ ArrowLeft: arrowHandler('left'),
324
+ ArrowRight: arrowHandler('right'),
325
+ ArrowUp: arrowHandler('up'),
326
+ ArrowDown: arrowHandler('down'),
327
+ };
328
+
329
+ export const codeBlockToggleShortcut = {
330
+ 'Mod-Alt-c': toggleCodeBlock as Command,
331
+ };
332
+
333
+ export const codeBlockKeymap = {
334
+ ...codeBlockToggleShortcut,
335
+ ...codeBlockArrowHandlers,
336
+ };