Skip to content

Commit a7e8835

Browse files
author
Ives van Hoorne
committed
Progress
1 parent b8ce727 commit a7e8835

File tree

20 files changed

+585
-36
lines changed

20 files changed

+585
-36
lines changed

packages/app/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@
151151
"monaco-vue": "^0.2.1",
152152
"normalize.css": "^5.0.0",
153153
"normalizr": "^3.2.3",
154+
"ot": "^0.0.15",
154155
"phoenix": "^1.3.0",
155156
"postcss": "^6.0.9",
156157
"postcss-selector-parser": "^2.2.3",

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

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// @flow
22
import * as React from 'react';
3+
import { TextOperation } from 'ot';
34
import { debounce } from 'lodash';
45
import { getModulePath } from 'common/sandbox/modules';
56

@@ -28,6 +29,25 @@ type State = {
2829
fuzzySearchEnabled: boolean,
2930
};
3031

32+
function indexToLineAndColumn(lines, index) {
33+
let offset = 0;
34+
for (let i = 0; i < lines.length; i++) {
35+
const line = lines[i];
36+
if (offset + line.length + 1 > index) {
37+
return {
38+
lineNumber: i + 1,
39+
column: index - offset + 1,
40+
};
41+
}
42+
43+
// + 1 is for the linebreak character which is not included
44+
offset += line.length + 1;
45+
}
46+
47+
// +2 for column, because +1 for Monaco and +1 for linebreak
48+
return { lineNumber: lines.length, column: lines[lines.length - 1] + 2 };
49+
}
50+
3151
let modelCache = {};
3252

3353
const fontFamilies = (...families) =>
@@ -59,6 +79,7 @@ class MonacoEditor extends React.Component<Props, State> implements Editor {
5979
sizeProbeInterval: ?number;
6080
editor: any;
6181
monaco: any;
82+
receivingCode: ?boolean = false;
6283

6384
constructor(props: Props) {
6485
super(props);
@@ -203,6 +224,59 @@ class MonacoEditor extends React.Component<Props, State> implements Editor {
203224
},
204225
});
205226

227+
editor.onDidChangeModelContent(({ changes }) => {
228+
const { isLive, sendTransforms, onCodeReceived } = this.props;
229+
230+
if (isLive && sendTransforms && !this.receivingCode) {
231+
let code = this.currentModule.code || '';
232+
const t = changes
233+
.map(change => {
234+
const startPos = change.range.getStartPosition();
235+
const lines = code.split('\n');
236+
let index = 0;
237+
const totalLength = code.length;
238+
let currentLine = 0;
239+
240+
while (currentLine + 1 < startPos.lineNumber) {
241+
index += lines[currentLine].length;
242+
index += 1; // Linebreak character
243+
currentLine += 1;
244+
}
245+
246+
index += startPos.column - 1;
247+
248+
const operation = new TextOperation();
249+
if (index) {
250+
operation.retain(index);
251+
}
252+
253+
if (change.rangeLength > 0) {
254+
// Deletion
255+
operation.delete(change.rangeLength);
256+
257+
index += change.rangeLength;
258+
}
259+
if (change.text) {
260+
// Insertion
261+
operation.insert(change.text);
262+
}
263+
264+
operation.retain(Math.max(0, totalLength - index));
265+
266+
if (changes.length > 1) {
267+
code = operation.apply(code);
268+
}
269+
270+
return operation;
271+
})
272+
.reduce((prev, next) => prev.compose(next));
273+
274+
sendTransforms(t);
275+
} else if (onCodeReceived) {
276+
onCodeReceived();
277+
}
278+
});
279+
206280
if (this.props.onInitialized) {
207281
this.disposeInitializer = this.props.onInitialized(this);
208282
}
@@ -253,6 +327,10 @@ class MonacoEditor extends React.Component<Props, State> implements Editor {
253327
});
254328
};
255329

330+
setReceivingCode = (receiving: boolean) => {
331+
this.receivingCode = receiving;
332+
};
333+
256334
setTSConfig = (config: Object) => {
257335
this.tsconfig = config;
258336

@@ -321,9 +399,56 @@ class MonacoEditor extends React.Component<Props, State> implements Editor {
321399
this.currentModule.title,
322400
this.editor.getModel().getVersionId()
323401
);
402+
this.lint(
403+
code,
404+
this.currentModule.title,
405+
this.editor.getModel().getVersionId()
406+
);
324407
}
325408
};
326409

410+
applyOperations = ops => {
411+
const monacoEditOperations = [];
412+
413+
ops.forEach(operation => {
414+
const lines = this.editor.getModel().getLinesContent();
415+
416+
let index = 0;
417+
for (let i = 0; i < operation.ops.length; i++) {
418+
let op = operation.ops[i];
419+
if (TextOperation.isRetain(op)) {
420+
index += op;
421+
} else if (TextOperation.isInsert(op)) {
422+
const { lineNumber, column } = indexToLineAndColumn(lines, index);
423+
monacoEditOperations.push({
424+
range: new this.monaco.Range(
425+
lineNumber,
426+
column,
427+
lineNumber,
428+
column
429+
),
430+
text: op,
431+
});
432+
index += op.length;
433+
} else if (TextOperation.isDelete(op)) {
434+
const from = indexToLineAndColumn(lines, index);
435+
const to = indexToLineAndColumn(lines, index - op);
436+
monacoEditOperations.push({
437+
range: new this.monaco.Range(
438+
from.lineNumber,
439+
from.column,
440+
to.lineNumber,
441+
to.column
442+
),
443+
text: '',
444+
});
445+
}
446+
}
447+
});
448+
449+
this.editor.getModel().applyEdits(monacoEditOperations);
450+
};
451+
327452
changeDependencies = (
328453
dependencies: ?$PropertyType<Props, 'dependencies'>
329454
) => {

packages/app/src/app/components/CodeEditor/types.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ export interface Editor {
3838
changeCode?: (code: string) => any;
3939
currentModule?: Module;
4040
setTSConfig?: (tsConfig: Object) => void;
41+
setReceivingCode?: (receivingCode: boolean) => void;
42+
applyOperations?: (operations: Array<any>) => void;
4143
}
4244

4345
export type Props = {
@@ -56,4 +58,8 @@ export type Props = {
5658
highlightedLines?: Array<number>,
5759
tsconfig?: Object,
5860
readOnly?: boolean,
61+
isLive: boolean,
62+
sendTransforms?: (transform: any) => void,
63+
receivingCode?: boolean,
64+
onCodeReceived?: () => void,
5965
};

packages/app/src/app/pages/Sandbox/Editor/Content/index.js

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as React from 'react';
33
import { ThemeProvider } from 'styled-components';
44
import { Prompt } from 'react-router-dom';
55
import { reaction } from 'mobx';
6+
import { TextOperation } from 'ot';
67
import { inject, observer } from 'mobx-react';
78
import getTemplateDefinition from 'common/templates';
89
import type { ModuleError } from 'common/types';
@@ -198,6 +199,31 @@ class EditorPreview extends React.Component<Props, State> {
198199
}
199200
}
200201
);
202+
const disposeLiveHandler = reaction(
203+
() => store.live.receivingCode,
204+
() => {
205+
if (editor.setReceivingCode) {
206+
editor.setReceivingCode(store.live.receivingCode);
207+
}
208+
}
209+
);
210+
211+
const disposeOperationsToApplyHandler = reaction(
212+
() => store.editor.operationsToApply.map(x => x),
213+
() => {
214+
if (editor.applyOperations) {
215+
editor.applyOperations(
216+
store.editor.operationsToApply.map(TextOperation.fromJSON)
217+
);
218+
219+
this.props.signals.live.onOperationsApplied();
220+
} else {
221+
console.log('not applying; setting code manually');
222+
// TODO apply logic itself and call `editor.changeCode` manually
223+
}
224+
}
225+
);
226+
console.log(disposeOperationsToApplyHandler);
201227
const disposeModuleHandler = reaction(
202228
() => [store.editor.currentModule, store.editor.currentModule.code],
203229
([newModule]) => {
@@ -235,6 +261,8 @@ class EditorPreview extends React.Component<Props, State> {
235261
disposeToggleDevtools();
236262
disposeResizeHandler();
237263
disposeGlyphsHandler();
264+
disposeLiveHandler();
265+
disposeOperationsToApplyHandler();
238266
};
239267
};
240268

@@ -252,6 +280,15 @@ class EditorPreview extends React.Component<Props, State> {
252280
);
253281
};
254282

283+
sendTransforms = operation => {
284+
const currentModuleShortid = this.props.store.editor.currentModuleShortid;
285+
286+
this.props.signals.live.onTransformMade({
287+
moduleShortid: currentModuleShortid,
288+
operation: operation.toJSON(),
289+
});
290+
};
291+
255292
render() {
256293
const { signals, store } = this.props;
257294
const currentModule = store.editor.currentModule;
@@ -326,7 +363,13 @@ class EditorPreview extends React.Component<Props, State> {
326363
width={editorWidth}
327364
height={editorHeight}
328365
settings={settings(store)}
329-
readOnly={store.live.isLive && !store.live.isCurrentEditor}
366+
sendTransforms={this.sendTransforms}
367+
readOnly={
368+
false
369+
// (store.live.isLive && !store.live.isCurrentEditor)
370+
}
371+
isLive={store.live.isLive}
372+
onCodeReceived={signals.live.onCodeReceived}
330373
onNpmDependencyAdded={name => {
331374
if (sandbox.owned) {
332375
signals.editor.addNpmDependency({ name, isDev: true });
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import React from 'react';
2+
import styled, { css } from 'styled-components';
3+
4+
import RecordIcon from 'react-icons/lib/md/fiber-manual-record';
5+
6+
const styles = css`
7+
display: flex;
8+
align-items: center;
9+
justify-content: center;
10+
11+
outline: none;
12+
border: none;
13+
padding: 0.5rem;
14+
15+
background-color: #fd2439b8;
16+
17+
width: 100%;
18+
color: white;
19+
border-radius: 4px;
20+
font-weight: 800;
21+
22+
border: 2px solid #fd2439b8;
23+
`;
24+
25+
const Button = styled.button`
26+
transition: 0.3s ease all;
27+
${styles};
28+
cursor: pointer;
29+
30+
svg {
31+
margin-right: 0.25rem;
32+
}
33+
34+
&:hover {
35+
background-color: #fd2439fa;
36+
}
37+
`;
38+
39+
const LoadingDiv = styled.div`
40+
${styles};
41+
`;
42+
43+
const AnimatedRecordIcon = styled(RecordIcon)`
44+
transition: 0.3s ease opacity;
45+
`;
46+
47+
export default class LiveButton extends React.PureComponent {
48+
state = {
49+
hovering: false,
50+
showIcon: true,
51+
};
52+
53+
timer: ?number;
54+
55+
componentDidUpdate() {
56+
if (this.state.hovering && !this.timer) {
57+
this.timer = setInterval(() => {
58+
this.setState({ showIcon: !this.state.showIcon });
59+
}, 1000);
60+
} else if (!this.state.hovering && this.timer) {
61+
clearInterval(this.timer);
62+
this.timer = null;
63+
64+
this.setState({ showIcon: true });
65+
}
66+
}
67+
68+
startHovering = () => {
69+
this.setState({ hovering: true });
70+
};
71+
72+
stopHovering = () => {
73+
this.setState({ hovering: false });
74+
};
75+
76+
render() {
77+
const { onClick, isLoading } = this.props;
78+
79+
if (isLoading) {
80+
return <LoadingDiv>Creating Session</LoadingDiv>;
81+
}
82+
83+
return (
84+
<Button
85+
onMouseEnter={this.startHovering}
86+
onMouseLeave={this.stopHovering}
87+
onClick={onClick}
88+
>
89+
<AnimatedRecordIcon style={{ opacity: this.state.showIcon ? 1 : 0 }} />{' '}
90+
Go Live
91+
</Button>
92+
);
93+
}
94+
}

0 commit comments

Comments
 (0)