Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/frontend/editors/slate/sync.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
6
import * as CodeMirror from "codemirror";
7
import { Descendant, Editor, Point } from "slate";
8
import { ReactEditor } from "./slate-react";
9
import { slate_to_markdown } from "./slate-to-markdown";
10
import { markdown_to_slate } from "./markdown-to-slate";
11
import { isWhitespaceParagraph } from "./padding";
12
const SENTINEL = "\uFE30";
13
import { SlateEditor } from "./editable-markdown";
14
15
export function slatePointToMarkdownPosition(
16
editor: SlateEditor,
17
point: Point | undefined
18
): CodeMirror.Position | undefined {
19
if (point == null) return undefined; // easy special case not handled below.
20
const { index, markdown } = slatePointToMarkdown(editor, point);
21
if (index == -1) return;
22
return indexToPosition({ index, markdown });
23
}
24
25
// Given a location in a slatejs document, return the
26
// corresponding index into the corresponding markdown document,
27
// along with the version of the markdown file that was used for
28
// this determination.
29
// Returns index of -1 if it fails to work for some reason, e.g.,
30
// the point doesn't exist in the document.
31
// TODO/BUG: This can still be slightly wrong because we don't use caching on the top-level
32
// block that contains the cursor. Thus, e.g., in a big nested list with various markdown
33
// that isn't canonical this could make things be slightly off.
34
export function slatePointToMarkdown(
35
editor: SlateEditor,
36
point: Point
37
): { index: number; markdown: string } {
38
let node;
39
try {
40
[node] = Editor.node(editor, point);
41
} catch (err) {
42
// console.warn(`slate -- invalid point ${JSON.stringify(point)} -- ${err}`);
43
// There is no guarantee that point is valid when this is called.
44
return { index: -1, markdown: "" };
45
}
46
47
let markdown = slate_to_markdown(editor.children, {
48
cache: editor.syncCache,
49
noCache: new Set([point.path[0]]),
50
hook: (elt) => {
51
if (elt !== node) return;
52
return (s) => s.slice(0, point.offset) + SENTINEL + s.slice(point.offset);
53
},
54
});
55
const index = markdown.indexOf(SENTINEL);
56
if (index != -1) {
57
markdown = markdown.slice(0, index) + markdown.slice(index + 1);
58
}
59
return { markdown, index };
60
}
61
62
export function indexToPosition({
63
index,
64
markdown,
65
}: {
66
index: number;
67
markdown: string;
68
}): CodeMirror.Position | undefined {
69
let n = 0;
70
const lines = markdown.split("\n");
71
for (let line = 0; line < lines.length; line++) {
72
const len = lines[line].length + 1; // +1 for the newlines.
73
const next = n + len;
74
if (index >= n && index < next) {
75
// in this line
76
return { line, ch: index - n };
77
}
78
n = next;
79
}
80
// not found...?
81
return undefined; // just being explicit here.
82
}
83
84
function insertSentinel(pos: CodeMirror.Position, markdown: string): string {
85
const v = markdown.split("\n");
86
const s = v[pos.line];
87
if (s == null) {
88
return markdown + SENTINEL;
89
}
90
v[pos.line] = s.slice(0, pos.ch) + SENTINEL + s.slice(pos.ch);
91
return v.join("\n");
92
}
93
94
function findSentinel(doc: any[]): Point | undefined {
95
let j = 0;
96
for (const node of doc) {
97
if (node.text != null) {
98
const offset = node.text.indexOf(SENTINEL);
99
if (offset != -1) {
100
return { path: [j], offset };
101
}
102
}
103
if (node.children != null) {
104
const x = findSentinel(node.children);
105
if (x != null) {
106
return { path: [j].concat(x.path), offset: x.offset };
107
}
108
}
109
j += 1;
110
}
111
}
112
113
// Convert a markdown string and point in it (in codemirror {line,ch})
114
// to corresponding slate editor coordinates.
115
// TODO/Huge CAVEAT -- right now we add in some blank paragraphs to the
116
// slate document to make it possible to do something things with the cursor,
117
// get before the first bullet point or code block in a document. These paragraphs
118
// are unknown to this conversion function... so if there are any then things are
119
// off as a result. Obviously, we need to get rid of the code (in control.ts) that
120
// adds these and come up with a better approach to make cursors and source<-->editable sync
121
// work perfectly.
122
export function markdownPositionToSlatePoint({
123
markdown,
124
pos,
125
editor,
126
}: {
127
markdown: string;
128
pos: CodeMirror.Position | undefined;
129
editor: SlateEditor;
130
}): Point | undefined {
131
if (pos == null) return undefined;
132
const m = insertSentinel(pos, markdown);
133
if (m == null) {
134
return undefined;
135
}
136
const doc: Descendant[] = markdown_to_slate(m, false);
137
let point = findSentinel(doc);
138
if (point != null) return normalizePoint(editor, doc, point);
139
if (pos.ch == 0) return undefined;
140
141
// try again at beginning of line, e.g., putting a sentinel
142
// in middle of an html fragment not likely to work, but beginning
143
// of line highly likely to work.
144
return markdownPositionToSlatePoint({
145
markdown,
146
pos: { line: pos.line, ch: 0 },
147
editor,
148
});
149
}
150
151
export async function scrollIntoView(
152
editor: ReactEditor,
153
point: Point
154
): Promise<void> {
155
const scrollIntoView = () => {
156
try {
157
const [node] = Editor.node(editor, point);
158
const elt = ReactEditor.toDOMNode(editor, node);
159
elt.scrollIntoView({ block: "center" });
160
} catch (_err) {
161
// There is no guarantee the point is valid, or that
162
// the DOM node exists.
163
}
164
};
165
if (!ReactEditor.isUsingWindowing(editor)) {
166
scrollIntoView();
167
} else {
168
// TODO: this below makes it so the top of the top-level block containing
169
// the point is displayed. However, that block could be big, and we
170
// really need to somehow move down to it via some scroll offset.
171
// There is an offset option to scrollToIndex (see use in preserveScrollPosition),
172
// and that might be very helpful.
173
const index = point.path[0];
174
editor.windowedListRef.current?.virtuosoRef.current?.scrollToIndex({
175
index,
176
align: "center",
177
});
178
setTimeout(scrollIntoView, 0);
179
requestAnimationFrame(() => {
180
scrollIntoView();
181
setTimeout(scrollIntoView, 0);
182
});
183
}
184
}
185
186
function normalizePoint(
187
editor: Editor,
188
doc: Descendant[],
189
point: Point
190
): Point | undefined {
191
// On the slate side at the top level we create blank paragraph to make it possible to
192
// move the cursor before/after various block elements. In practice this seems to nicely
193
// workaround a lot of maybe fundamental bugs/issues with Slate, like those
194
// hinted at here: https://github.com/ianstormtaylor/slate/issues/3469
195
// But it means we also have to account for this when mapping from markdown
196
// coordinates to slate coordinates, or other user cursors and forward search
197
// will be completely broken. These disappear when generating markdown from
198
// slate, so cause no trouble in the other direction.
199
if (doc.length < editor.children.length) {
200
// only an issue when lengths are different; in the common special case they
201
// are the same (e.g., maybe slate only used to view, not edit), then this
202
// can't be an issue.
203
let i = 0,
204
j = 0;
205
while (i <= point.path[0]) {
206
if (
207
isWhitespaceParagraph(editor.children[j]) &&
208
!isWhitespaceParagraph(doc[i])
209
) {
210
point.path[0] += 1;
211
j += 1;
212
continue;
213
}
214
i += 1;
215
j += 1;
216
}
217
}
218
219
// If position is at the very end of a line with marking our process to find it
220
// creates a new text node, so cursor gets lost, so we move back 1 position
221
// and try that. This is a heuristic to make one common edge case work.
222
try {
223
Editor.node(editor, point);
224
} catch (_err) {
225
point.path[point.path.length - 1] -= 1;
226
if (point.path[point.path.length - 1] >= 0) {
227
try {
228
// this goes to the end of it or raises an exception if
229
// there's no point here.
230
return Editor.after(editor, point, { unit: "line" });
231
} catch (_err) {
232
return undefined;
233
}
234
}
235
}
236
return point;
237
}
238
239