From ce14c2d6e9203d78c457635ad1c7fece7b535d64 Mon Sep 17 00:00:00 2001 From: ReDBrother Date: Wed, 23 Oct 2024 00:27:05 +0300 Subject: [PATCH] upgrades and fix cursor selection after changing editor options --- .../js/__tests__/RootContainer.test.jsx | 10 +- .../assets/js/widgets/components/Editor.jsx | 87 ++++------ .../js/widgets/components/ExtendedEditor.jsx | 2 +- .../assets/js/widgets/initEditor.js | 26 +++ .../assets/js/widgets/middlewares/Room.js | 18 ++- .../js/widgets/pages/game/DarkModeButton.jsx | 7 +- .../js/widgets/pages/game/EditorContainer.jsx | 10 +- .../js/widgets/utils/useCursorUpdates.js | 105 ++++++++++++ .../assets/js/widgets/utils/useEditor.js | 118 +++++++------- .../js/widgets/utils/useEditorCursor.js | 50 ++++++ .../js/widgets/utils/useRemoteCursor.js | 150 ------------------ .../js/widgets/utils/useResizeListener.js | 27 ++++ services/app/apps/codebattle/package.json | 1 + .../codebattle/webpack/webpack.base.config.js | 2 + services/app/apps/codebattle/yarn.lock | 9 +- 15 files changed, 341 insertions(+), 281 deletions(-) create mode 100644 services/app/apps/codebattle/assets/js/widgets/initEditor.js create mode 100644 services/app/apps/codebattle/assets/js/widgets/utils/useCursorUpdates.js create mode 100644 services/app/apps/codebattle/assets/js/widgets/utils/useEditorCursor.js delete mode 100644 services/app/apps/codebattle/assets/js/widgets/utils/useRemoteCursor.js create mode 100644 services/app/apps/codebattle/assets/js/widgets/utils/useResizeListener.js diff --git a/services/app/apps/codebattle/assets/js/__tests__/RootContainer.test.jsx b/services/app/apps/codebattle/assets/js/__tests__/RootContainer.test.jsx index 186f4f2c0..142b6e4d8 100644 --- a/services/app/apps/codebattle/assets/js/__tests__/RootContainer.test.jsx +++ b/services/app/apps/codebattle/assets/js/__tests__/RootContainer.test.jsx @@ -19,6 +19,8 @@ import waitingRoom from '../widgets/machines/waitingRoom'; import RootContainer from '../widgets/pages/RoomWidget'; import reducers from '../widgets/slices'; +jest.mock('../widgets/initEditor.js', () => ({})); + jest.mock('../widgets/pages/game/TaskDescriptionMarkdown', () => () => (<>Examples: )); jest.mock('@fortawesome/react-fontawesome', () => ({ @@ -77,7 +79,7 @@ jest.mock( jest.mock( '../widgets/utils/useStayScrolled', - () => () => ({ stayScrolled: () => {} }), + () => () => ({ stayScrolled: () => { } }), { virtual: true }, ); @@ -105,7 +107,7 @@ jest.mock( return channel; }), - connect: jest.fn(() => {}), + connect: jest.fn(() => { }), })), }; }, @@ -161,8 +163,8 @@ const preloadedState = { }, }, usersInfo: { - 1: { }, - 2: { }, + 1: {}, + 2: {}, }, chat: { users: Object.values(players), diff --git a/services/app/apps/codebattle/assets/js/widgets/components/Editor.jsx b/services/app/apps/codebattle/assets/js/widgets/components/Editor.jsx index 4a7861145..39325bc5f 100644 --- a/services/app/apps/codebattle/assets/js/widgets/components/Editor.jsx +++ b/services/app/apps/codebattle/assets/js/widgets/components/Editor.jsx @@ -1,68 +1,47 @@ import React, { memo } from 'react'; -import MonacoEditor, { loader } from '@monaco-editor/react'; +import '../initEditor'; +import MonacoEditor from '@monaco-editor/react'; import PropTypes from 'prop-types'; -import haskellProvider from '../config/editor/haskell'; -import sassProvider from '../config/editor/sass'; -import stylusProvider from '../config/editor/stylus'; import languages from '../config/languages'; import useEditor from '../utils/useEditor'; import EditorLoading from './EditorLoading'; -const monacoVersion = '0.52.0'; - -loader.config({ - paths: { - vs: `https://cdn.jsdelivr.net/npm/monaco-editor@${monacoVersion}/min/vs`, - }, -}); - -loader.init().then(monaco => { - monaco.languages.register({ id: 'haskell', aliases: ['haskell'] }); - monaco.languages.setMonarchTokensProvider('haskell', haskellProvider); - - monaco.languages.register({ id: 'stylus', aliases: ['stylus'] }); - monaco.languages.setMonarchTokensProvider('stylus', stylusProvider); - - monaco.languages.register({ id: 'scss', aliases: ['scss'] }); - monaco.languages.setMonarchTokensProvider('scss', sassProvider); -}); - function Editor(props) { - const { - value, - syntax, - onChange, - theme, - loading = false, - } = props; - const mappedSyntax = languages[syntax]; + const { + value, + syntax, + onChange, + theme, + loading = false, + } = props; + const mappedSyntax = languages[syntax]; - const { - options, - handleEditorDidMount, - handleEditorWillMount, - } = useEditor(props); + const { + options, + handleEditorDidMount, + handleEditorWillMount, + } = useEditor(props); - return ( - <> - - - - ); + return ( + <> + + + + ); } Editor.propTypes = { @@ -75,7 +54,7 @@ Editor.propTypes = { lineNumbers: PropTypes.string, fontSize: PropTypes.number, editable: PropTypes.bool, - gameMode: PropTypes.string.isRequired, + roomMode: PropTypes.string.isRequired, checkResult: PropTypes.func.isRequired, toggleMuteSound: PropTypes.func.isRequired, mute: PropTypes.bool.isRequired, diff --git a/services/app/apps/codebattle/assets/js/widgets/components/ExtendedEditor.jsx b/services/app/apps/codebattle/assets/js/widgets/components/ExtendedEditor.jsx index 6fc029d29..116936fe4 100644 --- a/services/app/apps/codebattle/assets/js/widgets/components/ExtendedEditor.jsx +++ b/services/app/apps/codebattle/assets/js/widgets/components/ExtendedEditor.jsx @@ -98,7 +98,7 @@ const mapStateToProps = state => { const locked = gameLockedSelector(state); return { gameId, - gameMode, + roomMode: gameMode, locked, mute: state.user.settings.mute, }; diff --git a/services/app/apps/codebattle/assets/js/widgets/initEditor.js b/services/app/apps/codebattle/assets/js/widgets/initEditor.js new file mode 100644 index 000000000..832e66520 --- /dev/null +++ b/services/app/apps/codebattle/assets/js/widgets/initEditor.js @@ -0,0 +1,26 @@ +import { loader } from '@monaco-editor/react'; +import * as monacoLib from 'monaco-editor'; + +import haskellProvider from './config/editor/haskell'; +import sassProvider from './config/editor/sass'; +import stylusProvider from './config/editor/stylus'; + +// const monacoVersion = '0.52.0'; + +loader.config({ + monaco: monacoLib, + // paths: { + // vs: `https://cdn.jsdelivr.net/npm/monaco-editor@${monacoVersion}/min/vs`, + // }, +}); + +loader.init().then(monaco => { + monaco.languages.register({ id: 'haskell', aliases: ['haskell'] }); + monaco.languages.setMonarchTokensProvider('haskell', haskellProvider); + + monaco.languages.register({ id: 'stylus', aliases: ['stylus'] }); + monaco.languages.setMonarchTokensProvider('stylus', stylusProvider); + + monaco.languages.register({ id: 'scss', aliases: ['scss'] }); + monaco.languages.setMonarchTokensProvider('scss', sassProvider); +}); diff --git a/services/app/apps/codebattle/assets/js/widgets/middlewares/Room.js b/services/app/apps/codebattle/assets/js/widgets/middlewares/Room.js index 5db8e33d4..546ab8f6b 100644 --- a/services/app/apps/codebattle/assets/js/widgets/middlewares/Room.js +++ b/services/app/apps/codebattle/assets/js/widgets/middlewares/Room.js @@ -327,9 +327,19 @@ export const resetTextToTemplateAndSend = langSlug => (dispatch, getState) => { export const soundNotification = notification(); -export const addCursorListeners = (userId, onChangePosition, onChangeSelection) => { - if (!userId || isRecord) { - return () => {}; +export const addCursorListeners = (params, onChangePosition, onChangeSelection) => { + const { + roomMode, + userId, + } = params; + + const isBuilder = roomMode === GameRoomModes.builder; + const isHistory = roomMode === GameRoomModes.history; + + const canReceivedRemoteCursor = !isBuilder && !isHistory && !!userId && !isRecord; + + if (!canReceivedRemoteCursor) { + return () => { }; } const handleNewCursorPosition = debounce(data => { @@ -1039,7 +1049,7 @@ export const changePlaybookSolution = method => dispatch => { export const storedEditorReady = service => { service.send('load_stored_editor'); - return () => {}; + return () => { }; }; export const downloadPlaybook = service => dispatch => { diff --git a/services/app/apps/codebattle/assets/js/widgets/pages/game/DarkModeButton.jsx b/services/app/apps/codebattle/assets/js/widgets/pages/game/DarkModeButton.jsx index f9a3459cd..68001e102 100644 --- a/services/app/apps/codebattle/assets/js/widgets/pages/game/DarkModeButton.jsx +++ b/services/app/apps/codebattle/assets/js/widgets/pages/game/DarkModeButton.jsx @@ -1,5 +1,6 @@ import React from 'react'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import cn from 'classnames'; import { useDispatch, useSelector } from 'react-redux'; @@ -15,7 +16,7 @@ function DakModeButton() { const isDarkMode = currentTheme === editorThemes.dark; const mode = isDarkMode ? editorThemes.light : editorThemes.dark; - const classNames = cn('btn btn-sm mr-2 border rounded', { + const className = cn('btn mr-2 border rounded', { 'btn-light': isDarkMode, 'btn-secondary': !isDarkMode, }); @@ -25,8 +26,8 @@ function DakModeButton() { }; return ( - ); } diff --git a/services/app/apps/codebattle/assets/js/widgets/pages/game/EditorContainer.jsx b/services/app/apps/codebattle/assets/js/widgets/pages/game/EditorContainer.jsx index ea3cbad5e..4b9a5cd9d 100644 --- a/services/app/apps/codebattle/assets/js/widgets/pages/game/EditorContainer.jsx +++ b/services/app/apps/codebattle/assets/js/widgets/pages/game/EditorContainer.jsx @@ -47,13 +47,13 @@ const useEditorChannelSubscription = (mainService, editorService, player) => { useEffect(() => { if (isPreview) { - return () => {}; + return () => { }; } if (inTestingRoom) { editorService.send('load_testing_editor'); - return () => {}; + return () => { }; } const clearEditorListeners = GameActions.connectToEditor(editorService, player?.isBanned)(dispatch); @@ -199,10 +199,9 @@ function EditorContainer({ const canSendCursor = canChange && !inTestingRoom && !inBuilderRoom; const updateEditor = editorCurrent.context.editorState === 'testing' ? updateEditorValue : updateAndSendEditorValue; const onChange = canChange ? updateEditor : noop; - const onChangeCursorSelection = canSendCursor ? GameActions.sendEditorCursorSelection : noop; - const onChangeCursorPosition = canSendCursor ? GameActions.sendEditorCursorPosition : noop; const editorParams = { + roomMode: tournamentId ? GameModeCodes.tournament : gameMode, userId: id, wordWrap: 'off', lineNumbers: 'on', @@ -210,8 +209,7 @@ function EditorContainer({ userType: type, syntax: editorState?.currentLangSlug || 'js', onChange, - onChangeCursorSelection, - onChangeCursorPosition, + canSendCursor, checkResult, value: isRestricted ? restrictedText : editorState?.text, editorHeight, diff --git a/services/app/apps/codebattle/assets/js/widgets/utils/useCursorUpdates.js b/services/app/apps/codebattle/assets/js/widgets/utils/useCursorUpdates.js new file mode 100644 index 000000000..daae433ff --- /dev/null +++ b/services/app/apps/codebattle/assets/js/widgets/utils/useCursorUpdates.js @@ -0,0 +1,105 @@ +import { + useMemo, useState, useEffect, useCallback, +} from 'react'; + +import pick from 'lodash/pick'; + +import editorUserTypes from '../config/editorUserTypes'; +import * as RoomActions from '../middlewares/Room'; + +const useCursorUpdates = (editor, monaco, props) => { + const params = useMemo( + () => pick(props, ['userId', 'roomMode']), + + // eslint-disable-next-line react-hooks/exhaustive-deps + [props.userId, props.roomMode], + ); + const [, setRemoteKeys] = useState([]); + const [remote, setRemote] = useState({ + cursor: {}, + selection: {}, + }); + + const updateRemoteCursorPosition = useCallback(offset => { + const { readOnly, userType } = editor.getRawOptions(); + + const position = editor.getModel().getPositionAt(offset); + const userClassName = userType === editorUserTypes.opponent + ? 'cb-remote-opponent' + : 'cb-remote-player'; + + if (readOnly) { + const cursor = { + range: new monaco.Range( + position.lineNumber, + position.column, + position.lineNumber, + position.column, + ), + options: { className: `cb-editor-remote-cursor ${userClassName}` }, + }; + + setRemote(oldRemote => ({ + ...oldRemote, + cursor, + })); + } + }, [setRemote, editor, monaco]); + + const updateRemoteCursorSelection = useCallback((startOffset, endOffset) => { + const { readOnly, userType } = editor.getRawOptions(); + + const userClassName = userType === editorUserTypes.opponent + ? 'cb-remote-opponent' + : 'cb-remote-player'; + + if (readOnly) { + const startPosition = editor.getModel().getPositionAt(startOffset); + const endPosition = editor.getModel().getPositionAt(endOffset); + + const startColumn = startPosition.column; + const startLineNumber = startPosition.lineNumber; + const endColumn = endPosition.column; + const endLineNumber = endPosition.lineNumber; + + const selection = { + range: new monaco.Range( + startLineNumber, + startColumn, + endLineNumber, + endColumn, + ), + options: { className: `cb-editor-remote-selection ${userClassName}` }, + }; + + setRemote(prevRemote => ({ + ...prevRemote, + selection, + })); + } + }, [setRemote, editor, monaco]); + + useEffect(() => { + if (remote.cursor.range && remote.selection.range) { + setRemoteKeys(oldRemoteKeys => ( + editor.deltaDecorations(oldRemoteKeys, Object.values(remote)) + )); + } + }, [editor, remote, setRemoteKeys]); + + useEffect(() => { + const clearCursorListeners = RoomActions.addCursorListeners( + params, + updateRemoteCursorPosition, + updateRemoteCursorSelection, + ); + + return clearCursorListeners; + }, [ + params, + updateRemoteCursorPosition, + updateRemoteCursorSelection, + ]); +}; + +export default useCursorUpdates; diff --git a/services/app/apps/codebattle/assets/js/widgets/utils/useEditor.js b/services/app/apps/codebattle/assets/js/widgets/utils/useEditor.js index f5b9e85d3..5acd6577f 100644 --- a/services/app/apps/codebattle/assets/js/widgets/utils/useEditor.js +++ b/services/app/apps/codebattle/assets/js/widgets/utils/useEditor.js @@ -10,25 +10,32 @@ import GameRoomModes from '../config/gameModes'; import sound from '../lib/sound'; import getLanguageTabSize, { shouldReplaceTabsWithSpaces } from './editor'; -import useRemoteCursor from './useRemoteCursor'; +import useCursorUpdates from './useCursorUpdates'; +import useEditorCursor from './useEditorCursor'; +import useResizeListener from './useResizeListener'; -const editorPlaceholder = 'Please! Help me!!!'; +const defaultEditorPlaceholder = 'Please! Help me!!!'; let editorClipboard = ''; /** + * @param {object} editor * @param {{ + * canSendCursor: boolean, * wordWrap: string, * lineNumbers: string, * fontSize: number, * editable: boolean, - * gameMode: string, + * roomMode: string, * checkResult: Function, * toggleMuteSound: Function, * mute: boolean, + * userType: string, * }} props */ -const useOption = ({ +const useOption = (editor, { + userType, + canSendCursor, wordWrap, lineNumbers, syntax, @@ -37,7 +44,7 @@ const useOption = ({ loading, }) => { const options = useMemo(() => ({ - placeholder: editorPlaceholder, + placeholder: defaultEditorPlaceholder, wordWrap, lineNumbers, stickyScroll: { @@ -67,7 +74,13 @@ const useOption = ({ horizontalScrollbarSize: 17, arrowSize: 30, }, + + // Custom options for codebattle editor callbacks + userType, + canSendCursor, }), [ + userType, + canSendCursor, wordWrap, lineNumbers, syntax, @@ -76,6 +89,12 @@ const useOption = ({ loading, ]); + useEffect(() => { + if (editor) { + editor.updateOptions(options); + } + }, [editor, options]); + return options; }; @@ -85,7 +104,7 @@ const useOption = ({ * lineNumbers: string, * fontSize: number, * editable: boolean, - * gameMode: string, + * roomMode: string, * checkResult: Function, * toggleMuteSound: Function, * mute: boolean, @@ -102,14 +121,22 @@ const useEditor = props => { // this.statusBarHeight = lineHeight = current fontSize * 1.5 // this.statusBarHeight = convertRemToPixels(1) * 1.5; - useRemoteCursor(editor, monaco, props); - const options = useOption(props); + const options = useOption(editor, props); + useCursorUpdates(editor, monaco, props); + useEditorCursor(editor); + useResizeListener(editor, props); - const handleResize = useCallback(() => { - if (editor) { - editor.layout(); - } - }, [editor]); + const handleEnterCtrPlusS = useCallback(e => { + if (e.key === 's' && (e.metaKey || e.ctrlKey)) e.preventDefault(); + }, []); + + useEffect(() => { + window.addEventListener('keydown', handleEnterCtrPlusS); + + return () => { + window.removeEventListener('keydown', handleEnterCtrPlusS); + }; + }, [handleEnterCtrPlusS]); // if (editor) { // const model = editor.getModel(); @@ -118,44 +145,19 @@ const useEditor = props => { // model.forceTokenization(model.getLineCount()); // } - useEffect(() => { - if (editor) { - editor.updateOptions(options); - } - }, [editor, options]); + const handleEditorWillMount = () => { }; - useEffect(() => { - handleResize(); - }, [props.locked, handleResize]); - - useEffect(() => { - const ctrPlusS = e => { - if (e.key === 's' && (e.metaKey || e.ctrlKey)) e.preventDefault(); - }; - - window.addEventListener('resize', handleResize); - - return () => { - window.removeEventListener('resize', handleResize); - window.removeEventListener('keydown', ctrPlusS); - }; - }, [handleResize]); - - const handleEditorWillMount = () => {}; - - const handleEditorDidMount = (newEditor, newMonaco) => { - setEditor(newEditor); - setMonaco(newMonaco); + const handleEditorDidMount = (currentEditor, currentMonaco) => { + setEditor(currentEditor); + setMonaco(currentMonaco); const { editable, - gameMode, + roomMode, checkResult, toggleMuteSound, } = props; - const isBuilder = gameMode === GameRoomModes.builder; - // Handle copy event // editor.onDidCopyText(event => { // // Custom copy logic @@ -164,23 +166,23 @@ const useEditor = props => { // event.preventDefault(); // }); - newEditor.onKeyDown(e => { + currentEditor.onKeyDown(e => { // Custom Copy Event - if ((e.ctrlKey || e.metaKey) && e.keyCode === newMonaco.KeyCode.KEY_C) { - const selection = newEditor.getModel().getValueInRange(newEditor.getSelection()); + if ((e.ctrlKey || e.metaKey) && e.keyCode === currentMonaco.KeyCode.KEY_C) { + const selection = currentEditor.getModel().getValueInRange(currentEditor.getSelection()); editorClipboard = `___CUSTOM_COPIED_TEXT___${selection}`; e.preventDefault(); } // Custom Paste Event - if ((e.ctrlKey || e.metaKey) && e.keyCode === newMonaco.KeyCode.KEY_V) { + if ((e.ctrlKey || e.metaKey) && e.keyCode === currentMonaco.KeyCode.KEY_V) { if (editorClipboard.startsWith('___CUSTOM_COPIED_TEXT___')) { const customText = editorClipboard.replace('___CUSTOM_COPIED_TEXT___', ''); - newEditor.executeEdits('custom-paste', [ + currentEditor.executeEdits('custom-paste', [ { - range: newEditor.getSelection(), + range: currentEditor.getSelection(), text: customText, forceMoveMarkers: true, }, @@ -191,32 +193,32 @@ const useEditor = props => { } }); - if (editable && !isBuilder) { - newEditor.focus(); + if (editable && roomMode !== GameRoomModes.builder) { + currentEditor.focus(); } if (checkResult) { - newEditor.addAction({ + currentEditor.addAction({ id: 'codebattle-check-keys', label: 'Codebattle check start', - keybindings: [newMonaco.KeyMod.CtrlCmd | newMonaco.KeyCode.Enter], + keybindings: [currentMonaco.KeyMod.CtrlCmd | currentMonaco.KeyCode.Enter], run: () => { - if (!newEditor.getOptions().readOnly) { + if (!currentEditor.getOptions().readOnly) { checkResult(); } }, }); } else { - newEditor.addCommand( - newMonaco.KeyMod.CtrlCmd | newMonaco.KeyCode.Enter, + currentEditor.addCommand( + currentMonaco.KeyMod.CtrlCmd | currentMonaco.KeyCode.Enter, () => null, ); } - newEditor.addAction({ + currentEditor.addAction({ id: 'codebattle-mute-keys', label: 'Codebattle mute sound', - keybindings: [newMonaco.KeyMod.CtrlCmd | newMonaco.KeyCode.KEY_M], + keybindings: [currentMonaco.KeyMod.CtrlCmd | currentMonaco.KeyCode.KEY_M], run: () => { const { mute } = props; sound.toggle(mute ? undefined : 0); diff --git a/services/app/apps/codebattle/assets/js/widgets/utils/useEditorCursor.js b/services/app/apps/codebattle/assets/js/widgets/utils/useEditorCursor.js new file mode 100644 index 000000000..e82da448f --- /dev/null +++ b/services/app/apps/codebattle/assets/js/widgets/utils/useEditorCursor.js @@ -0,0 +1,50 @@ +import { useEffect, useCallback } from 'react'; + +import * as GameActions from '../middlewares/Room'; + +/** + * @param {object} editor +*/ +const useEditorCursor = editor => { + const handleChangeCursorSelection = useCallback(e => { + const { readOnly, canSendCursor } = editor.getRawOptions(); + + if (!canSendCursor) { + return; + } + + if (readOnly) { + const { column, lineNumber } = editor.getPosition(); + + editor.setPosition({ lineNumber, column }); + } else { + const startOffset = editor.getModel().getOffsetAt(e.selection.getStartPosition()); + const endOffset = editor.getModel().getOffsetAt(e.selection.getEndPosition()); + + GameActions.sendEditorCursorSelection(startOffset, endOffset); + } + }, [editor]); + + const handleChangeCursorPosition = useCallback(e => { + const { readOnly, canSendCursor } = editor.getRawOptions(); + + if (!canSendCursor) { + return; + } + + if (!readOnly) { + const offset = editor.getModel().getOffsetAt(e.position); + + GameActions.sendEditorCursorPosition(offset); + } + }, [editor]); + + useEffect(() => { + if (editor) { + editor.onDidChangeCursorSelection(handleChangeCursorSelection); + editor.onDidChangeCursorPosition(handleChangeCursorPosition); + } + }, [editor, handleChangeCursorSelection, handleChangeCursorPosition]); +}; + +export default useEditorCursor; diff --git a/services/app/apps/codebattle/assets/js/widgets/utils/useRemoteCursor.js b/services/app/apps/codebattle/assets/js/widgets/utils/useRemoteCursor.js deleted file mode 100644 index 81fba5fc1..000000000 --- a/services/app/apps/codebattle/assets/js/widgets/utils/useRemoteCursor.js +++ /dev/null @@ -1,150 +0,0 @@ -import { useState, useEffect, useCallback } from 'react'; - -import editorUserTypes from '../config/editorUserTypes'; -import GameRoomModes from '../config/gameModes'; -import { addCursorListeners } from '../middlewares/Room'; - -/** - * @param {object} editor - * @param {object} monaco - * @param {{ - * editable: boolean, - * gameMode: @type {import("../config/gameModes.js).default}, - * checkResult: Function, - * toggleMuteSound: Function, - * mute: boolean, - * userType: string, - * userId: string, - * onChangeCursorSelection: Function, - * onChangeCursorPosition: Function, - * }} props -*/ -const useRemoteCursor = (editor, monaco, props) => { - const { - gameMode, - editable, - userType, - userId, - onChangeCursorSelection, - onChangeCursorPosition, - } = props; - const [, setRemoteKeys] = useState([]); - const [remote, setRemote] = useState({ - cursor: {}, - selection: {}, - }); - - const isBuilder = gameMode === GameRoomModes.builder; - const isHistory = gameMode === GameRoomModes.history; - - const needSubscribeCursorUpdates = !isBuilder && !isHistory; - - const updateRemoteCursorPosition = useCallback(offset => { - const position = editor.getModel().getPositionAt(offset); - const userClassName = userType === editorUserTypes.opponent - ? 'cb-remote-opponent' - : 'cb-remote-player'; - - if (!editable) { - const cursor = { - range: new monaco.Range( - position.lineNumber, - position.column, - position.lineNumber, - position.column, - ), - options: { className: `cb-editor-remote-cursor ${userClassName}` }, - }; - - setRemote(oldRemote => ({ - ...oldRemote, - cursor, - })); - } - }, [setRemote, editor, monaco, editable, userType]); - - const updateRemoteCursorSelection = useCallback((startOffset, endOffset) => { - const userClassName = userType === editorUserTypes.opponent - ? 'cb-remote-opponent' - : 'cb-remote-player'; - - if (!editable) { - const startPosition = editor.getModel().getPositionAt(startOffset); - const endPosition = editor.getModel().getPositionAt(endOffset); - - const startColumn = startPosition.column; - const startLineNumber = startPosition.lineNumber; - const endColumn = endPosition.column; - const endLineNumber = endPosition.lineNumber; - - const selection = { - range: new monaco.Range( - startLineNumber, - startColumn, - endLineNumber, - endColumn, - ), - options: { className: `cb-editor-remote-selection ${userClassName}` }, - }; - - setRemote(prevRemote => ({ - ...prevRemote, - selection, - })); - } - }, [setRemote, editor, monaco, editable, userType]); - - const handleChangeCursorSelection = useCallback(e => { - if (!editable) { - const { column, lineNumber } = editor.getPosition(); - editor.setPosition({ lineNumber, column }); - } else if (editable && onChangeCursorSelection) { - const startOffset = editor.getModel().getOffsetAt(e.selection.getStartPosition()); - const endOffset = editor.getModel().getOffsetAt(e.selection.getEndPosition()); - onChangeCursorSelection(startOffset, endOffset); - } - }, [editor, editable, onChangeCursorSelection]); - - const handleChangeCursorPosition = useCallback(e => { - if (editable && onChangeCursorPosition) { - const offset = editor.getModel().getOffsetAt(e.position); - onChangeCursorPosition(offset); - } - }, [editor, editable, onChangeCursorPosition]); - - useEffect(() => { - if (remote.cursor.range && remote.selection.range) { - setRemoteKeys(oldRemoteKeys => ( - editor.deltaDecorations(oldRemoteKeys, Object.values(remote)) - )); - } - }, [editor, remote, setRemoteKeys]); - - useEffect(() => { - if (editor) { - editor.onDidChangeCursorSelection(handleChangeCursorSelection); - editor.onDidChangeCursorPosition(handleChangeCursorPosition); - } - }, [editor, handleChangeCursorSelection, handleChangeCursorPosition]); - - useEffect(() => { - if (needSubscribeCursorUpdates) { - const clearCursorListeners = addCursorListeners( - userId, - updateRemoteCursorPosition, - updateRemoteCursorSelection, - ); - - return clearCursorListeners; - } - - return () => {}; - }, [ - userId, - needSubscribeCursorUpdates, - updateRemoteCursorPosition, - updateRemoteCursorSelection, - ]); -}; - -export default useRemoteCursor; diff --git a/services/app/apps/codebattle/assets/js/widgets/utils/useResizeListener.js b/services/app/apps/codebattle/assets/js/widgets/utils/useResizeListener.js new file mode 100644 index 000000000..7a87d3b2c --- /dev/null +++ b/services/app/apps/codebattle/assets/js/widgets/utils/useResizeListener.js @@ -0,0 +1,27 @@ +import { useCallback, useEffect } from 'react'; + +const useResizeListener = (editor, props) => { + const handleResize = useCallback(() => { + if (editor) { + editor.layout(); + } + }, [editor]); + + useEffect(() => { + handleResize(); + }, [props.locked, handleResize]); + + useEffect(() => { + window.addEventListener('resize', handleResize); + + return () => { + window.removeEventListener('resize', handleResize); + }; + }, [handleResize]); + + return { + handleResize, + }; +}; + +export default useResizeListener; diff --git a/services/app/apps/codebattle/package.json b/services/app/apps/codebattle/package.json index e8537e74e..3b1dab018 100644 --- a/services/app/apps/codebattle/package.json +++ b/services/app/apps/codebattle/package.json @@ -73,6 +73,7 @@ "mini-css-extract-plugin": "^2.7.3", "moment": "^2.29.4", "monaco-editor": "^0.52.0", + "monaco-editor-webpack-plugin": "^7.1.0", "monaco-themes": "^0.4.4", "nprogress": "^0.2.0", "path-browserify": "^1.0.1", diff --git a/services/app/apps/codebattle/webpack/webpack.base.config.js b/services/app/apps/codebattle/webpack/webpack.base.config.js index eef0d06a9..70079c368 100644 --- a/services/app/apps/codebattle/webpack/webpack.base.config.js +++ b/services/app/apps/codebattle/webpack/webpack.base.config.js @@ -2,6 +2,7 @@ const path = require('path'); const CopyWebpackPlugin = require('copy-webpack-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); +const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin'); const webpack = require('webpack'); // const env = process.env.NODE_ENV || 'development'; @@ -89,6 +90,7 @@ module.exports = { }, }, plugins: [ + new MonacoWebpackPlugin(), new webpack.ProvidePlugin({ process: 'process/browser', }), diff --git a/services/app/apps/codebattle/yarn.lock b/services/app/apps/codebattle/yarn.lock index 1395a5619..fa29d54ad 100644 --- a/services/app/apps/codebattle/yarn.lock +++ b/services/app/apps/codebattle/yarn.lock @@ -8717,7 +8717,7 @@ loader-runner@^4.2.0: resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== -loader-utils@^2.0.0, loader-utils@^2.0.4: +loader-utils@^2.0.0, loader-utils@^2.0.2, loader-utils@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.4.tgz#8b5cb38b5c34a9a018ee1fc0e6a066d1dfcc528c" integrity sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw== @@ -9409,6 +9409,13 @@ moment@>=1.6.0, moment@^2.29.4: resolved "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz" integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== +monaco-editor-webpack-plugin@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/monaco-editor-webpack-plugin/-/monaco-editor-webpack-plugin-7.1.0.tgz#16f265c2b5dbb5fe08681b6b3b7d00d3c5b2ee97" + integrity sha512-ZjnGINHN963JQkFqjjcBtn1XBtUATDZBMgNQhDQwd78w2ukRhFXAPNgWuacaQiDZsUr4h1rWv5Mv6eriKuOSzA== + dependencies: + loader-utils "^2.0.2" + monaco-editor@^0.52.0: version "0.52.0" resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.52.0.tgz#d47c02b191eae208d68878d679b3ee7456031be7"