CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/frontend/codemirror/extensions/set-value-nojump.ts
Views: 687
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 { diff_main } from "@cocalc/sync/editor/generic/util";
8
9
/*
10
Try to set the value of the buffer to something new by replacing just the ranges
11
that changed, so that the viewport/history/etc. doesn't get messed up.
12
Setting scroll_last to true sets cursor to last changed position and puts cursors
13
there; this is used for undo/redo.
14
15
**NOTE:** there are no guarantees, since if the patch to transform from current to
16
value involves a "huge" (at least 500) number of chunks, then we just set the value
17
directly since apply thousands of chunks will lock the cpu for seconds. This will
18
slightly mess up the scroll position, undo position, etc. It's worth it. We noticed
19
this performance edge case mostly in running prettier.
20
21
**NOTE 2:** To me this "set value without jumping" problem seems like one of the
22
most important basic problems to solve for collaborative editing. I just checked
23
(Dec 2020) and shockingly, Google docs does _not_ solve this problem! They just
24
let the screen drastically jump around in response to another editor. Surprising,
25
though for them maybe it is harder due to pagination.
26
*/
27
28
CodeMirror.defineExtension(
29
"setValueNoJump",
30
function (value: string, scroll_last: boolean = false) {
31
// @ts-ignore
32
const cm: any = this;
33
34
if (value == null) {
35
// Special case -- trying to set to value=undefined. This is the sort of thing
36
// that might rarely happen right as the document opens or closes, for which
37
// there is no meaningful thing to do but "do nothing". We detected this periodically
38
// by catching user stacktraces in production... See
39
// https://github.com/sagemathinc/cocalc/issues/1768
40
// Obviously, this is something that typescript should in theory prevent (but we can't
41
// risk it).
42
return;
43
}
44
const current_value = cm.getValue();
45
if (value === current_value) {
46
// Special case: nothing to do
47
return;
48
}
49
50
const r = cm.getOption("readOnly");
51
if (!r) {
52
// temporarily set editor to readOnly to prevent any potential changes.
53
// This code is synchronous so I'm not sure why this is needed (I really
54
// can't remember why I did this, unfortunately).
55
cm.setOption("readOnly", true);
56
}
57
// We do the following, so the cursor events that happen as a direct result
58
// of this setValueNoJump know that this is what is causing them.
59
cm._setValueNoJump = true;
60
61
// Determine information so we can restore the scroll position
62
const t = cm.getScrollInfo().top;
63
const b = cm.setBookmark({ line: cm.lineAtHeight(t, "local") });
64
const before = cm.heightAtLine(cm.lineAtHeight(t, "local"));
65
66
// Compute patch that transforms current_value to new value:
67
const diff = diff_main(current_value, value);
68
let last_pos: CodeMirror.Position | undefined = undefined;
69
if (diff.length >= 500) {
70
// special case -- this is a "weird" change that will take
71
// an enormous amount of time to apply using diffApply.
72
// For example, something that changes every line in a file
73
// slightly could do this, e.g., changing from 4 space to 2 space
74
// indentation, which prettier might do. In this case, instead of
75
// blocking the user browser for several seconds, we just take the
76
// hit and possibly unset the cursor.
77
const scroll =
78
cm.getScrollInfo().top - (before - cm.heightAtLine(b.find().line));
79
cm.setValue(value);
80
cm.scrollTo(undefined, scroll); // make some attempt to fix scroll.
81
} else {
82
// Change the buffer in place by applying the diffs as we go; this avoids replacing the entire buffer,
83
// which would cause total chaos.
84
last_pos = cm.diffApply(diff);
85
}
86
87
// Now, if possible, restore the exact scroll position using our bookmark.
88
const n = b.find()?.line;
89
if (n != null) {
90
cm.scrollTo(
91
undefined,
92
cm.getScrollInfo().top - (before - cm.heightAtLine(b.find().line))
93
);
94
b.clear();
95
}
96
97
// Just do a possibly expensive double check that the above worked. I have no reason
98
// to believe the above could ever fail... but maybe it does in some very rare
99
// cases, and if it did, the results would be *corruption*, which is not acceptable.
100
// So... we just brutally do the set directly (messing up the cursor) if it fails, thus
101
// preventing any possibility of corruption. This will mess up cursors, etc., but that's
102
// a reasonable price to pay for correctness.
103
// I can't remember if this ever happens, or if I was just overly paranoid.
104
if (value !== cm.getValue()) {
105
console.warn("setValueNoJump failed -- setting value directly");
106
cm.setValue(value);
107
}
108
109
if (!r) {
110
// Also restore readOnly state.
111
cm.setOption("readOnly", false);
112
if (scroll_last && last_pos != null) {
113
cm.scrollIntoView(last_pos);
114
cm.setCursor(last_pos);
115
}
116
}
117
118
delete cm._setValueNoJump;
119
}
120
);
121
122