Skip to content

Commit ce14c2d

Browse files
committed
upgrades and fix cursor selection after changing editor options
1 parent fc972a3 commit ce14c2d

File tree

15 files changed

+341
-281
lines changed

15 files changed

+341
-281
lines changed

services/app/apps/codebattle/assets/js/__tests__/RootContainer.test.jsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import waitingRoom from '../widgets/machines/waitingRoom';
1919
import RootContainer from '../widgets/pages/RoomWidget';
2020
import reducers from '../widgets/slices';
2121

22+
jest.mock('../widgets/initEditor.js', () => ({}));
23+
2224
jest.mock('../widgets/pages/game/TaskDescriptionMarkdown', () => () => (<>Examples: </>));
2325

2426
jest.mock('@fortawesome/react-fontawesome', () => ({
@@ -77,7 +79,7 @@ jest.mock(
7779

7880
jest.mock(
7981
'../widgets/utils/useStayScrolled',
80-
() => () => ({ stayScrolled: () => {} }),
82+
() => () => ({ stayScrolled: () => { } }),
8183
{ virtual: true },
8284
);
8385

@@ -105,7 +107,7 @@ jest.mock(
105107

106108
return channel;
107109
}),
108-
connect: jest.fn(() => {}),
110+
connect: jest.fn(() => { }),
109111
})),
110112
};
111113
},
@@ -161,8 +163,8 @@ const preloadedState = {
161163
},
162164
},
163165
usersInfo: {
164-
1: { },
165-
2: { },
166+
1: {},
167+
2: {},
166168
},
167169
chat: {
168170
users: Object.values(players),

services/app/apps/codebattle/assets/js/widgets/components/Editor.jsx

Lines changed: 33 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,47 @@
11
import React, { memo } from 'react';
22

3-
import MonacoEditor, { loader } from '@monaco-editor/react';
3+
import '../initEditor';
4+
import MonacoEditor from '@monaco-editor/react';
45
import PropTypes from 'prop-types';
56

6-
import haskellProvider from '../config/editor/haskell';
7-
import sassProvider from '../config/editor/sass';
8-
import stylusProvider from '../config/editor/stylus';
97
import languages from '../config/languages';
108
import useEditor from '../utils/useEditor';
119

1210
import EditorLoading from './EditorLoading';
1311

14-
const monacoVersion = '0.52.0';
15-
16-
loader.config({
17-
paths: {
18-
vs: `https://cdn.jsdelivr.net/npm/monaco-editor@${monacoVersion}/min/vs`,
19-
},
20-
});
21-
22-
loader.init().then(monaco => {
23-
monaco.languages.register({ id: 'haskell', aliases: ['haskell'] });
24-
monaco.languages.setMonarchTokensProvider('haskell', haskellProvider);
25-
26-
monaco.languages.register({ id: 'stylus', aliases: ['stylus'] });
27-
monaco.languages.setMonarchTokensProvider('stylus', stylusProvider);
28-
29-
monaco.languages.register({ id: 'scss', aliases: ['scss'] });
30-
monaco.languages.setMonarchTokensProvider('scss', sassProvider);
31-
});
32-
3312
function Editor(props) {
34-
const {
35-
value,
36-
syntax,
37-
onChange,
38-
theme,
39-
loading = false,
40-
} = props;
41-
const mappedSyntax = languages[syntax];
13+
const {
14+
value,
15+
syntax,
16+
onChange,
17+
theme,
18+
loading = false,
19+
} = props;
20+
const mappedSyntax = languages[syntax];
4221

43-
const {
44-
options,
45-
handleEditorDidMount,
46-
handleEditorWillMount,
47-
} = useEditor(props);
22+
const {
23+
options,
24+
handleEditorDidMount,
25+
handleEditorWillMount,
26+
} = useEditor(props);
4827

49-
return (
50-
<>
51-
<MonacoEditor
52-
theme={theme}
53-
options={options}
54-
width="100%"
55-
height="100%"
56-
language={mappedSyntax}
57-
beforeMount={handleEditorWillMount}
58-
onMount={handleEditorDidMount}
59-
value={value}
60-
onChange={onChange}
61-
data-guide-id="Editor"
62-
/>
63-
<EditorLoading loading={loading} />
64-
</>
65-
);
28+
return (
29+
<>
30+
<MonacoEditor
31+
theme={theme}
32+
options={options}
33+
width="100%"
34+
height="100%"
35+
language={mappedSyntax}
36+
beforeMount={handleEditorWillMount}
37+
onMount={handleEditorDidMount}
38+
value={value}
39+
onChange={onChange}
40+
data-guide-id="Editor"
41+
/>
42+
<EditorLoading loading={loading} />
43+
</>
44+
);
6645
}
6746

6847
Editor.propTypes = {
@@ -75,7 +54,7 @@ Editor.propTypes = {
7554
lineNumbers: PropTypes.string,
7655
fontSize: PropTypes.number,
7756
editable: PropTypes.bool,
78-
gameMode: PropTypes.string.isRequired,
57+
roomMode: PropTypes.string.isRequired,
7958
checkResult: PropTypes.func.isRequired,
8059
toggleMuteSound: PropTypes.func.isRequired,
8160
mute: PropTypes.bool.isRequired,

services/app/apps/codebattle/assets/js/widgets/components/ExtendedEditor.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ const mapStateToProps = state => {
9898
const locked = gameLockedSelector(state);
9999
return {
100100
gameId,
101-
gameMode,
101+
roomMode: gameMode,
102102
locked,
103103
mute: state.user.settings.mute,
104104
};
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { loader } from '@monaco-editor/react';
2+
import * as monacoLib from 'monaco-editor';
3+
4+
import haskellProvider from './config/editor/haskell';
5+
import sassProvider from './config/editor/sass';
6+
import stylusProvider from './config/editor/stylus';
7+
8+
// const monacoVersion = '0.52.0';
9+
10+
loader.config({
11+
monaco: monacoLib,
12+
// paths: {
13+
// vs: `https://cdn.jsdelivr.net/npm/monaco-editor@${monacoVersion}/min/vs`,
14+
// },
15+
});
16+
17+
loader.init().then(monaco => {
18+
monaco.languages.register({ id: 'haskell', aliases: ['haskell'] });
19+
monaco.languages.setMonarchTokensProvider('haskell', haskellProvider);
20+
21+
monaco.languages.register({ id: 'stylus', aliases: ['stylus'] });
22+
monaco.languages.setMonarchTokensProvider('stylus', stylusProvider);
23+
24+
monaco.languages.register({ id: 'scss', aliases: ['scss'] });
25+
monaco.languages.setMonarchTokensProvider('scss', sassProvider);
26+
});

services/app/apps/codebattle/assets/js/widgets/middlewares/Room.js

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -327,9 +327,19 @@ export const resetTextToTemplateAndSend = langSlug => (dispatch, getState) => {
327327

328328
export const soundNotification = notification();
329329

330-
export const addCursorListeners = (userId, onChangePosition, onChangeSelection) => {
331-
if (!userId || isRecord) {
332-
return () => {};
330+
export const addCursorListeners = (params, onChangePosition, onChangeSelection) => {
331+
const {
332+
roomMode,
333+
userId,
334+
} = params;
335+
336+
const isBuilder = roomMode === GameRoomModes.builder;
337+
const isHistory = roomMode === GameRoomModes.history;
338+
339+
const canReceivedRemoteCursor = !isBuilder && !isHistory && !!userId && !isRecord;
340+
341+
if (!canReceivedRemoteCursor) {
342+
return () => { };
333343
}
334344

335345
const handleNewCursorPosition = debounce(data => {
@@ -1039,7 +1049,7 @@ export const changePlaybookSolution = method => dispatch => {
10391049
export const storedEditorReady = service => {
10401050
service.send('load_stored_editor');
10411051

1042-
return () => {};
1052+
return () => { };
10431053
};
10441054

10451055
export const downloadPlaybook = service => dispatch => {

services/app/apps/codebattle/assets/js/widgets/pages/game/DarkModeButton.jsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React from 'react';
22

3+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
34
import cn from 'classnames';
45
import { useDispatch, useSelector } from 'react-redux';
56

@@ -15,7 +16,7 @@ function DakModeButton() {
1516
const isDarkMode = currentTheme === editorThemes.dark;
1617
const mode = isDarkMode ? editorThemes.light : editorThemes.dark;
1718

18-
const classNames = cn('btn btn-sm mr-2 border rounded', {
19+
const className = cn('btn mr-2 border rounded', {
1920
'btn-light': isDarkMode,
2021
'btn-secondary': !isDarkMode,
2122
});
@@ -25,8 +26,8 @@ function DakModeButton() {
2526
};
2627

2728
return (
28-
<button type="button" className={classNames} onClick={handleToggleDarkMode}>
29-
{isDarkMode ? 'Light' : 'Dark'}
29+
<button type="button" className={className} onClick={handleToggleDarkMode}>
30+
<FontAwesomeIcon icon={isDarkMode ? 'sun' : 'moon'} />
3031
</button>
3132
);
3233
}

services/app/apps/codebattle/assets/js/widgets/pages/game/EditorContainer.jsx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,13 @@ const useEditorChannelSubscription = (mainService, editorService, player) => {
4747

4848
useEffect(() => {
4949
if (isPreview) {
50-
return () => {};
50+
return () => { };
5151
}
5252

5353
if (inTestingRoom) {
5454
editorService.send('load_testing_editor');
5555

56-
return () => {};
56+
return () => { };
5757
}
5858

5959
const clearEditorListeners = GameActions.connectToEditor(editorService, player?.isBanned)(dispatch);
@@ -199,19 +199,17 @@ function EditorContainer({
199199
const canSendCursor = canChange && !inTestingRoom && !inBuilderRoom;
200200
const updateEditor = editorCurrent.context.editorState === 'testing' ? updateEditorValue : updateAndSendEditorValue;
201201
const onChange = canChange ? updateEditor : noop;
202-
const onChangeCursorSelection = canSendCursor ? GameActions.sendEditorCursorSelection : noop;
203-
const onChangeCursorPosition = canSendCursor ? GameActions.sendEditorCursorPosition : noop;
204202

205203
const editorParams = {
204+
roomMode: tournamentId ? GameModeCodes.tournament : gameMode,
206205
userId: id,
207206
wordWrap: 'off',
208207
lineNumbers: 'on',
209208
hidingPanelControls: false,
210209
userType: type,
211210
syntax: editorState?.currentLangSlug || 'js',
212211
onChange,
213-
onChangeCursorSelection,
214-
onChangeCursorPosition,
212+
canSendCursor,
215213
checkResult,
216214
value: isRestricted ? restrictedText : editorState?.text,
217215
editorHeight,
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import {
2+
useMemo, useState, useEffect, useCallback,
3+
} from 'react';
4+
5+
import pick from 'lodash/pick';
6+
7+
import editorUserTypes from '../config/editorUserTypes';
8+
import * as RoomActions from '../middlewares/Room';
9+
10+
const useCursorUpdates = (editor, monaco, props) => {
11+
const params = useMemo(
12+
() => pick(props, ['userId', 'roomMode']),
13+
14+
// eslint-disable-next-line react-hooks/exhaustive-deps
15+
[props.userId, props.roomMode],
16+
);
17+
const [, setRemoteKeys] = useState([]);
18+
const [remote, setRemote] = useState({
19+
cursor: {},
20+
selection: {},
21+
});
22+
23+
const updateRemoteCursorPosition = useCallback(offset => {
24+
const { readOnly, userType } = editor.getRawOptions();
25+
26+
const position = editor.getModel().getPositionAt(offset);
27+
const userClassName = userType === editorUserTypes.opponent
28+
? 'cb-remote-opponent'
29+
: 'cb-remote-player';
30+
31+
if (readOnly) {
32+
const cursor = {
33+
range: new monaco.Range(
34+
position.lineNumber,
35+
position.column,
36+
position.lineNumber,
37+
position.column,
38+
),
39+
options: { className: `cb-editor-remote-cursor ${userClassName}` },
40+
};
41+
42+
setRemote(oldRemote => ({
43+
...oldRemote,
44+
cursor,
45+
}));
46+
}
47+
}, [setRemote, editor, monaco]);
48+
49+
const updateRemoteCursorSelection = useCallback((startOffset, endOffset) => {
50+
const { readOnly, userType } = editor.getRawOptions();
51+
52+
const userClassName = userType === editorUserTypes.opponent
53+
? 'cb-remote-opponent'
54+
: 'cb-remote-player';
55+
56+
if (readOnly) {
57+
const startPosition = editor.getModel().getPositionAt(startOffset);
58+
const endPosition = editor.getModel().getPositionAt(endOffset);
59+
60+
const startColumn = startPosition.column;
61+
const startLineNumber = startPosition.lineNumber;
62+
const endColumn = endPosition.column;
63+
const endLineNumber = endPosition.lineNumber;
64+
65+
const selection = {
66+
range: new monaco.Range(
67+
startLineNumber,
68+
startColumn,
69+
endLineNumber,
70+
endColumn,
71+
),
72+
options: { className: `cb-editor-remote-selection ${userClassName}` },
73+
};
74+
75+
setRemote(prevRemote => ({
76+
...prevRemote,
77+
selection,
78+
}));
79+
}
80+
}, [setRemote, editor, monaco]);
81+
82+
useEffect(() => {
83+
if (remote.cursor.range && remote.selection.range) {
84+
setRemoteKeys(oldRemoteKeys => (
85+
editor.deltaDecorations(oldRemoteKeys, Object.values(remote))
86+
));
87+
}
88+
}, [editor, remote, setRemoteKeys]);
89+
90+
useEffect(() => {
91+
const clearCursorListeners = RoomActions.addCursorListeners(
92+
params,
93+
updateRemoteCursorPosition,
94+
updateRemoteCursorSelection,
95+
);
96+
97+
return clearCursorListeners;
98+
}, [
99+
params,
100+
updateRemoteCursorPosition,
101+
updateRemoteCursorSelection,
102+
]);
103+
};
104+
105+
export default useCursorUpdates;

0 commit comments

Comments
 (0)