Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/frontend/editors/slate/operations.ts
1691 views
1
/*
2
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
import { Editor, Operation, Point } from "slate";
6
import { isEqual } from "lodash";
7
import type { SlateEditor } from "./editable-markdown";
8
import { getScrollState, setScrollState } from "./scroll";
9
10
export function applyOperations(
11
editor: SlateEditor,
12
operations: Operation[]
13
): void {
14
if (operations.length == 0) return;
15
16
// window.operations = operations;
17
18
// const t0 = Date.now();
19
20
// This cursor gets mutated during the for loop below!
21
const cursor: { focus: Point | null } = {
22
focus: editor.selection?.focus ?? null,
23
};
24
25
try {
26
editor.applyingOperations = true; // TODO: not sure if this is at all necessary...
27
28
try {
29
Editor.withoutNormalizing(editor, () => {
30
for (const op of operations) {
31
// Should skip due to just removing whitespace right
32
// before the user's cursor?
33
if (skipCursor(cursor, op)) continue;
34
try {
35
// This can rarely throw an error in production
36
// if somehow the op isn't valid. Instead of
37
// crashing, we print a warning, and document
38
// "applyOperations" above as "best effort".
39
// The document *should* converge
40
// when the next diff/patch round occurs.
41
editor.apply(op);
42
} catch (err) {
43
console.warn(
44
`WARNING: Slate issue -- unable to apply an operation to the document -- err=${err}, op=${op}`
45
);
46
}
47
}
48
});
49
} catch (err) {
50
console.warn(
51
`WARNING: Slate issue -- unable to apply operations to the document -- err=${err} -- could create invalid state`
52
);
53
}
54
55
/* console.log(
56
`time: apply ${operations.length} operations`,
57
Date.now() - t0,
58
"ms"
59
);*/
60
} finally {
61
editor.applyingOperations = false;
62
}
63
}
64
65
/*
66
There is a special case that is unavoidable without making the
67
plain text file really ugly. If you type "foo " in slate (with the space),
68
this converts to "foo " in Markdown (*with* the space). But
69
markdown-it converts this back to [...{text:"foo"}]
70
without the space at the end of the line! Without modifying
71
how we apply diffs, the only solution to this problem would
72
be to emit "foo " which technically works, but is REALLY ugly.
73
So if we do not do the following operation in some cases
74
when the path is to the focused cursor.
75
76
{type: "remove_text", text:"[whitespace]", path, offset}
77
78
NOTE: not doing this transform doesn't mess up paths of
79
subsequent ops since all this did was change some whitespace
80
in a single text node, hence doesn't mutate any paths.
81
82
Similarly we do not delete empty paragraphs if the cursor
83
is in it. This comes up when moving the cursor next to voids,
84
where we have to make an empty paragraph to make it possible to
85
type something there (e.g., between two code blocks).
86
*/
87
function skipCursor(cursor: { focus: Point | null }, op): boolean {
88
const { focus } = cursor;
89
if (focus == null) return false;
90
if (
91
op.type == "remove_text" &&
92
isEqual(focus.path, op.path) &&
93
op.text.trim() == "" &&
94
op.text.length + op.offset == focus.offset
95
) {
96
return true;
97
}
98
if (
99
op.type == "remove_node" &&
100
isEqual(op.node, { type: "paragraph", children: [{ text: "" }] }) &&
101
isEqual(op.path, focus.path.slice(0, op.path.length))
102
) {
103
return true;
104
}
105
106
cursor.focus = Point.transform(focus, op);
107
return false;
108
}
109
110
// This only has an impact with windowing enabled, which is the only situation where
111
// scrolling should be happening anyways.
112
export function preserveScrollPosition(
113
editor: SlateEditor,
114
operations: Operation[]
115
): void {
116
const scroll = getScrollState(editor);
117
if (scroll == null) return;
118
const { index, offset } = scroll;
119
120
let point: Point | null = { path: [index], offset: 0 };
121
// transform point via the operations.
122
for (const op of operations) {
123
point = Point.transform(point, op);
124
if (point == null) break;
125
}
126
127
const newStartIndex = point?.path[0];
128
if (newStartIndex == null) return;
129
130
setScrollState(editor, { index: newStartIndex, offset });
131
}
132
133