Path: blob/master/src/packages/frontend/editors/slate/slate-diff/diff.ts
1698 views
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45import { Node, Operation } from "slate";6import { diff_main } from "@cocalc/sync/editor/generic/util";7import { StringCharMapping } from "@cocalc/util/misc";8import { handleChangeOneNode } from "./handle-change-one-node";9import { handleChangeTextNodes, isAllText } from "./handle-change-text-nodes";1011// We could instead use12// import stringify from "json-stable-stringify";13// which might sometimes avoid a safe "false positive" (i.e., slightly14// less efficient patch), but is significantly slower.15const stringify = JSON.stringify;1617function docToStrings(doc: Node[]): string[] {18const v: string[] = [];19for (const node of doc) {20v.push(stringify(node));21}22return v;23}2425export function slateDiff(26doc0: Node[],27doc1: Node[],28path: number[] = []29): Operation[] {30// const t0 = path.length == 0 ? Date.now() : 0;31const string_mapping = new StringCharMapping();32const s0 = docToStrings(doc0);33const s1 = docToStrings(doc1);34const m0 = string_mapping.to_string(s0);35const m1 = string_mapping.to_string(s1);36const diff = diff_main(m0, m1);37const operations: Operation[] = [];38//console.log({ diff, to_string: string_mapping._to_string });3940function letterToNode(x: string): Node {41const node = JSON.parse(string_mapping._to_string[x]);42if (node == null) {43throw Error("letterToNode: bug");44}45return node;46}4748function stringToNodes(s: string): Node[] {49const nodes: Node[] = [];50for (const x of s) {51nodes.push(letterToNode(x));52}53return nodes;54}5556let index = 0;57let i = 0;58while (i < diff.length) {59const chunk = diff[i];60const op = chunk[0]; // -1 = delete, 0 = leave unchanged, 1 = insert61const val = chunk[1];62if (op === 0) {63// skip over context diff nodes64index += val.length;65i += 1;66continue;67}68const nodes = stringToNodes(val);69if (op === -1) {70if (i < diff.length - 1 && diff[i + 1][0] == 1) {71// next one is an insert, so this is really a "replace".72const nextVal = diff[i + 1][1];73const nextNodes = stringToNodes(nextVal);74if (isAllText(nodes) && isAllText(nextNodes)) {75// Every single node involved is a text node. This can be done76// via modifying and splitting and joining.77for (const op of handleChangeTextNodes(78nodes,79nextNodes,80path.concat([index])81)) {82operations.push(op);83}84index += nextNodes.length;85i += 2; // this consumed two entries from the diff array.86continue;87}88while (nodes.length > 0 && nextNodes.length > 0) {89// replace corresponding node90for (const op of handleChangeOneNode(91nodes[0],92nextNodes[0],93path.concat([index])94)) {95operations.push(op);96}97index += 1;98nodes.shift();99nextNodes.shift();100}101// delete anything left in nodes102for (const node of nodes) {103operations.push({104type: "remove_node",105path: path.concat([index]),106node,107} as Operation);108}109// insert anything left in nextNodes110for (const node of nextNodes) {111operations.push({112type: "insert_node",113path: path.concat([index]),114node,115} as Operation);116index += 1;117}118i += 2; // this consumed two entries from the diff array.119continue;120} else {121// Plain delete of some nodes (with no insert immediately after)122for (const node of nodes) {123operations.push({124type: "remove_node",125path: path.concat([index]),126node,127} as Operation);128}129i += 1; // consumes only one entry from diff array.130continue;131}132}133if (op === 1) {134// insert new nodes.135for (const node of nodes) {136operations.push({137type: "insert_node",138path: path.concat([index]),139node,140});141index += 1;142}143i += 1;144continue;145}146throw Error("BUG");147}148/* if (path.length == 0) {149console.log("time: slateDiff", Date.now() - t0, "ms");150}*/151152return operations;153}154155156