Skip to content

Commit 8794c0e

Browse files
authored
Fix live undoing (codesandbox#1086)
Fixes codesandbox#699
1 parent fd198e1 commit 8794c0e

File tree

3 files changed

+210
-35
lines changed

3 files changed

+210
-35
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { TextOperation } from 'ot';
2+
import { lineAndColumnToIndex } from './monaco-index-converter';
3+
4+
export default function convertChangeEventToOperation(
5+
changeEvent,
6+
liveOperationCode
7+
) {
8+
let otOperation;
9+
10+
let composedCode = liveOperationCode;
11+
12+
// eslint-disable-next-line no-restricted-syntax
13+
for (const change of [...changeEvent.changes]) {
14+
const newOt = new TextOperation();
15+
const cursorStartOffset = lineAndColumnToIndex(
16+
composedCode.split(/\r?\n/),
17+
change.range.startLineNumber,
18+
change.range.startColumn
19+
);
20+
21+
const retain = cursorStartOffset - newOt.targetLength;
22+
23+
if (retain !== 0) {
24+
newOt.retain(retain);
25+
}
26+
27+
if (change.rangeLength > 0) {
28+
newOt.delete(change.rangeLength);
29+
}
30+
31+
if (change.text) {
32+
const normalizedChangeText = change.text.split(/\r?\n/).join('\n');
33+
newOt.insert(normalizedChangeText);
34+
}
35+
36+
const remaining = composedCode.length - newOt.baseLength;
37+
if (remaining > 0) {
38+
newOt.retain(remaining);
39+
}
40+
41+
otOperation = otOperation ? otOperation.compose(newOt) : newOt;
42+
43+
composedCode = otOperation.apply(liveOperationCode);
44+
}
45+
46+
return {
47+
operation: otOperation,
48+
newCode: composedCode,
49+
};
50+
}
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import eventToTransform from './event-to-transform';
2+
3+
describe('Live', () => {
4+
describe('Monaco Event to OT Transform', () => {
5+
it('correctly transforms undo of adding text', () => {
6+
const changeEvent = {
7+
changes: [
8+
{
9+
range: {
10+
startLineNumber: 13,
11+
startColumn: 15,
12+
endLineNumber: 13,
13+
endColumn: 16,
14+
},
15+
rangeLength: 1,
16+
text: '',
17+
rangeOffset: 255,
18+
forceMoveMarkers: false,
19+
},
20+
{
21+
range: {
22+
startLineNumber: 13,
23+
startColumn: 14,
24+
endLineNumber: 13,
25+
endColumn: 15,
26+
},
27+
rangeLength: 1,
28+
text: '',
29+
rangeOffset: 254,
30+
forceMoveMarkers: false,
31+
},
32+
],
33+
eol: '\n',
34+
versionId: 5,
35+
isUndoing: true,
36+
isRedoing: false,
37+
isFlush: false,
38+
};
39+
40+
const code = `import React from 'react';
41+
import { render } from 'react-dom';
42+
import Hello from './Hello';
43+
44+
const styles = {
45+
fontFamily: 'sans-serif',
46+
textAlign: 'center',
47+
};
48+
49+
const App = () => (
50+
<div style={styles}>
51+
<Hello name="CodeSandbox" />
52+
<h2>Startaa editing to see some magic happen {"\u2728"}</h2>
53+
</div>
54+
);
55+
`;
56+
57+
const { operation, newCode } = eventToTransform(changeEvent, code);
58+
59+
expect(operation.apply(code)).toBe(`import React from 'react';
60+
import { render } from 'react-dom';
61+
import Hello from './Hello';
62+
63+
const styles = {
64+
fontFamily: 'sans-serif',
65+
textAlign: 'center',
66+
};
67+
68+
const App = () => (
69+
<div style={styles}>
70+
<Hello name="CodeSandbox" />
71+
<h2>Start editing to see some magic happen {"\u2728"}</h2>
72+
</div>
73+
);
74+
`);
75+
76+
expect(operation.apply(code)).toBe(newCode);
77+
});
78+
79+
it('correctly transforms undo of removing text', () => {
80+
const changeEvent = {
81+
changes: [
82+
{
83+
range: {
84+
startLineNumber: 13,
85+
startColumn: 12,
86+
endLineNumber: 13,
87+
endColumn: 12,
88+
},
89+
rangeLength: 0,
90+
text: 'r',
91+
rangeOffset: 252,
92+
forceMoveMarkers: false,
93+
},
94+
{
95+
range: {
96+
startLineNumber: 13,
97+
startColumn: 13,
98+
endLineNumber: 13,
99+
endColumn: 13,
100+
},
101+
rangeLength: 0,
102+
text: 't',
103+
rangeOffset: 253,
104+
forceMoveMarkers: false,
105+
},
106+
],
107+
eol: '\n',
108+
versionId: 9,
109+
isUndoing: true,
110+
isRedoing: false,
111+
isFlush: false,
112+
};
113+
114+
const code = `import React from 'react';
115+
import { render } from 'react-dom';
116+
import Hello from './Hello';
117+
118+
const styles = {
119+
fontFamily: 'sans-serif',
120+
textAlign: 'center',
121+
};
122+
123+
const App = () => (
124+
<div style={styles}>
125+
<Hello name="CodeSandbox" />
126+
<h2>Sta editing to see some magic happen {"\u2728"}</h2>
127+
</div>
128+
);
129+
`;
130+
131+
const { operation, newCode } = eventToTransform(changeEvent, code);
132+
133+
expect(operation.apply(code)).toBe(`import React from 'react';
134+
import { render } from 'react-dom';
135+
import Hello from './Hello';
136+
137+
const styles = {
138+
fontFamily: 'sans-serif',
139+
textAlign: 'center',
140+
};
141+
142+
const App = () => (
143+
<div style={styles}>
144+
<Hello name="CodeSandbox" />
145+
<h2>Start editing to see some magic happen {"\u2728"}</h2>
146+
</div>
147+
);
148+
`);
149+
150+
expect(operation.apply(code)).toBe(newCode);
151+
});
152+
});
153+
});

packages/app/src/app/components/CodeEditor/Monaco/index.js

Lines changed: 7 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import LinterWorker from 'worker-loader?publicPath=/&name=monaco-linter.[hash:8]
2525
import TypingsFetcherWorker from 'worker-loader?publicPath=/&name=monaco-typings-ata.[hash:8].worker.js!./workers/fetch-dependency-typings';
2626
/* eslint-enable import/no-webpack-loader-syntax */
2727

28+
import eventToTransform from './event-to-transform';
2829
import MonacoEditorComponent from './MonacoReactComponent';
2930
import FuzzySearch from '../FuzzySearch';
3031
import { Container, CodeContainer } from './elements';
@@ -432,45 +433,16 @@ class MonacoEditor extends React.Component<Props, State> implements Editor {
432433
const { sendTransforms, isLive, onCodeReceived } = this.props;
433434

434435
if (sendTransforms && changeEvent.changes) {
435-
const otOperation = new TextOperation();
436-
// TODO: add a comment explaining what "delta" is
437-
let delta = 0;
438-
439436
this.liveOperationCode =
440437
this.liveOperationCode || this.currentModule.code || '';
441-
// eslint-disable-next-line no-restricted-syntax
442-
for (const change of [...changeEvent.changes].reverse()) {
443-
const cursorStartOffset =
444-
lineAndColumnToIndex(
445-
this.liveOperationCode.split(/\r?\n/),
446-
change.range.startLineNumber,
447-
change.range.startColumn
448-
) + delta;
449-
450-
const retain = cursorStartOffset - otOperation.targetLength;
451-
if (retain > 0) {
452-
otOperation.retain(retain);
453-
}
454-
455-
if (change.rangeLength > 0) {
456-
otOperation.delete(change.rangeLength);
457-
delta -= change.rangeLength;
458-
}
459-
460-
if (change.text) {
461-
const normalizedChangeText = change.text.split(/\r?\n/).join('\n');
462-
otOperation.insert(normalizedChangeText);
463-
delta += normalizedChangeText.length;
464-
}
465-
}
438+
const { operation, newCode } = eventToTransform(
439+
changeEvent,
440+
this.liveOperationCode
441+
);
466442

467-
const remaining = this.liveOperationCode.length - otOperation.baseLength;
468-
if (remaining > 0) {
469-
otOperation.retain(remaining);
470-
}
471-
this.liveOperationCode = otOperation.apply(this.liveOperationCode);
443+
this.liveOperationCode = newCode;
472444

473-
sendTransforms(otOperation);
445+
sendTransforms(operation);
474446

475447
requestAnimationFrame(() => {
476448
this.liveOperationCode = '';

0 commit comments

Comments
 (0)