Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/src/packages/frontend/codemirror/extensions/set-value-nojump.ts
Views: 687
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45import * as CodeMirror from "codemirror";6import { diff_main } from "@cocalc/sync/editor/generic/util";78/*9Try to set the value of the buffer to something new by replacing just the ranges10that changed, so that the viewport/history/etc. doesn't get messed up.11Setting scroll_last to true sets cursor to last changed position and puts cursors12there; this is used for undo/redo.1314**NOTE:** there are no guarantees, since if the patch to transform from current to15value involves a "huge" (at least 500) number of chunks, then we just set the value16directly since apply thousands of chunks will lock the cpu for seconds. This will17slightly mess up the scroll position, undo position, etc. It's worth it. We noticed18this performance edge case mostly in running prettier.1920**NOTE 2:** To me this "set value without jumping" problem seems like one of the21most important basic problems to solve for collaborative editing. I just checked22(Dec 2020) and shockingly, Google docs does _not_ solve this problem! They just23let the screen drastically jump around in response to another editor. Surprising,24though for them maybe it is harder due to pagination.25*/2627CodeMirror.defineExtension(28"setValueNoJump",29function (value: string, scroll_last: boolean = false) {30// @ts-ignore31const cm: any = this;3233if (value == null) {34// Special case -- trying to set to value=undefined. This is the sort of thing35// that might rarely happen right as the document opens or closes, for which36// there is no meaningful thing to do but "do nothing". We detected this periodically37// by catching user stacktraces in production... See38// https://github.com/sagemathinc/cocalc/issues/176839// Obviously, this is something that typescript should in theory prevent (but we can't40// risk it).41return;42}43const current_value = cm.getValue();44if (value === current_value) {45// Special case: nothing to do46return;47}4849const r = cm.getOption("readOnly");50if (!r) {51// temporarily set editor to readOnly to prevent any potential changes.52// This code is synchronous so I'm not sure why this is needed (I really53// can't remember why I did this, unfortunately).54cm.setOption("readOnly", true);55}56// We do the following, so the cursor events that happen as a direct result57// of this setValueNoJump know that this is what is causing them.58cm._setValueNoJump = true;5960// Determine information so we can restore the scroll position61const t = cm.getScrollInfo().top;62const b = cm.setBookmark({ line: cm.lineAtHeight(t, "local") });63const before = cm.heightAtLine(cm.lineAtHeight(t, "local"));6465// Compute patch that transforms current_value to new value:66const diff = diff_main(current_value, value);67let last_pos: CodeMirror.Position | undefined = undefined;68if (diff.length >= 500) {69// special case -- this is a "weird" change that will take70// an enormous amount of time to apply using diffApply.71// For example, something that changes every line in a file72// slightly could do this, e.g., changing from 4 space to 2 space73// indentation, which prettier might do. In this case, instead of74// blocking the user browser for several seconds, we just take the75// hit and possibly unset the cursor.76const scroll =77cm.getScrollInfo().top - (before - cm.heightAtLine(b.find().line));78cm.setValue(value);79cm.scrollTo(undefined, scroll); // make some attempt to fix scroll.80} else {81// Change the buffer in place by applying the diffs as we go; this avoids replacing the entire buffer,82// which would cause total chaos.83last_pos = cm.diffApply(diff);84}8586// Now, if possible, restore the exact scroll position using our bookmark.87const n = b.find()?.line;88if (n != null) {89cm.scrollTo(90undefined,91cm.getScrollInfo().top - (before - cm.heightAtLine(b.find().line))92);93b.clear();94}9596// Just do a possibly expensive double check that the above worked. I have no reason97// to believe the above could ever fail... but maybe it does in some very rare98// cases, and if it did, the results would be *corruption*, which is not acceptable.99// So... we just brutally do the set directly (messing up the cursor) if it fails, thus100// preventing any possibility of corruption. This will mess up cursors, etc., but that's101// a reasonable price to pay for correctness.102// I can't remember if this ever happens, or if I was just overly paranoid.103if (value !== cm.getValue()) {104console.warn("setValueNoJump failed -- setting value directly");105cm.setValue(value);106}107108if (!r) {109// Also restore readOnly state.110cm.setOption("readOnly", false);111if (scroll_last && last_pos != null) {112cm.scrollIntoView(last_pos);113cm.setCursor(last_pos);114}115}116117delete cm._setValueNoJump;118}119);120121122