Path: blob/master/src/packages/frontend/editors/slate/operations.ts
1691 views
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/4import { Editor, Operation, Point } from "slate";5import { isEqual } from "lodash";6import type { SlateEditor } from "./editable-markdown";7import { getScrollState, setScrollState } from "./scroll";89export function applyOperations(10editor: SlateEditor,11operations: Operation[]12): void {13if (operations.length == 0) return;1415// window.operations = operations;1617// const t0 = Date.now();1819// This cursor gets mutated during the for loop below!20const cursor: { focus: Point | null } = {21focus: editor.selection?.focus ?? null,22};2324try {25editor.applyingOperations = true; // TODO: not sure if this is at all necessary...2627try {28Editor.withoutNormalizing(editor, () => {29for (const op of operations) {30// Should skip due to just removing whitespace right31// before the user's cursor?32if (skipCursor(cursor, op)) continue;33try {34// This can rarely throw an error in production35// if somehow the op isn't valid. Instead of36// crashing, we print a warning, and document37// "applyOperations" above as "best effort".38// The document *should* converge39// when the next diff/patch round occurs.40editor.apply(op);41} catch (err) {42console.warn(43`WARNING: Slate issue -- unable to apply an operation to the document -- err=${err}, op=${op}`44);45}46}47});48} catch (err) {49console.warn(50`WARNING: Slate issue -- unable to apply operations to the document -- err=${err} -- could create invalid state`51);52}5354/* console.log(55`time: apply ${operations.length} operations`,56Date.now() - t0,57"ms"58);*/59} finally {60editor.applyingOperations = false;61}62}6364/*65There is a special case that is unavoidable without making the66plain text file really ugly. If you type "foo " in slate (with the space),67this converts to "foo " in Markdown (*with* the space). But68markdown-it converts this back to [...{text:"foo"}]69without the space at the end of the line! Without modifying70how we apply diffs, the only solution to this problem would71be to emit "foo " which technically works, but is REALLY ugly.72So if we do not do the following operation in some cases73when the path is to the focused cursor.7475{type: "remove_text", text:"[whitespace]", path, offset}7677NOTE: not doing this transform doesn't mess up paths of78subsequent ops since all this did was change some whitespace79in a single text node, hence doesn't mutate any paths.8081Similarly we do not delete empty paragraphs if the cursor82is in it. This comes up when moving the cursor next to voids,83where we have to make an empty paragraph to make it possible to84type something there (e.g., between two code blocks).85*/86function skipCursor(cursor: { focus: Point | null }, op): boolean {87const { focus } = cursor;88if (focus == null) return false;89if (90op.type == "remove_text" &&91isEqual(focus.path, op.path) &&92op.text.trim() == "" &&93op.text.length + op.offset == focus.offset94) {95return true;96}97if (98op.type == "remove_node" &&99isEqual(op.node, { type: "paragraph", children: [{ text: "" }] }) &&100isEqual(op.path, focus.path.slice(0, op.path.length))101) {102return true;103}104105cursor.focus = Point.transform(focus, op);106return false;107}108109// This only has an impact with windowing enabled, which is the only situation where110// scrolling should be happening anyways.111export function preserveScrollPosition(112editor: SlateEditor,113operations: Operation[]114): void {115const scroll = getScrollState(editor);116if (scroll == null) return;117const { index, offset } = scroll;118119let point: Point | null = { path: [index], offset: 0 };120// transform point via the operations.121for (const op of operations) {122point = Point.transform(point, op);123if (point == null) break;124}125126const newStartIndex = point?.path[0];127if (newStartIndex == null) return;128129setScrollState(editor, { index: newStartIndex, offset });130}131132133