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 38e8ae44e..221b55c69 100644
--- a/services/app/apps/codebattle/assets/js/widgets/components/Editor.jsx
+++ b/services/app/apps/codebattle/assets/js/widgets/components/Editor.jsx
@@ -1,9 +1,10 @@
-import React, { memo } from 'react';
+import React, { memo, useRef, useEffect } from 'react';
-import '../initEditor';
import MonacoEditor from '@monaco-editor/react';
+import { initVimMode } from 'monaco-vim';
import PropTypes from 'prop-types';
+import '../initEditor';
import languages from '../config/languages';
import useEditor from '../utils/useEditor';
@@ -11,20 +12,55 @@ import EditorLoading from './EditorLoading';
function Editor(props) {
const {
- value,
- syntax,
- onChange,
- theme,
- loading = false,
- } = props;
+ value, syntax, onChange, theme, loading = false, mode,
+} = props;
+
+ // Map your custom language key to an actual Monaco recognized language
const mappedSyntax = languages[syntax];
+ // Hooks from your custom editor config
const {
options,
- handleEditorDidMount,
+ handleEditorDidMount: originalEditorDidMount,
handleEditorWillMount,
} = useEditor(props);
+ // Create a ref for the actual Monaco editor instance
+ const editorRef = useRef(null);
+ // Create a ref for the Vim status element
+ const vimStatusRef = useRef(null);
+ // Create a ref to hold the vimMode controller so we can dispose if needed
+ const vimModeRef = useRef(null);
+
+ // Wrap your existing "didMount" to store editor and call original if needed
+ function handleEditorDidMount(editor, monaco) {
+ editorRef.current = editor;
+
+ if (typeof originalEditorDidMount === 'function') {
+ originalEditorDidMount(editor, monaco);
+ }
+ }
+ // Whenever `mode` changes, enable or disable vimMode
+ useEffect(() => {
+ // If we haven't mounted the editor yet, exit
+ if (!editorRef.current) return;
+
+ if (mode === 'vim') {
+ // If not already in Vim mode, enable it
+ if (!vimModeRef.current) {
+ vimModeRef.current = initVimMode(
+ editorRef.current,
+ vimStatusRef.current,
+ );
+ }
+ } else if (vimModeRef.current) {
+ // If we're switching away from Vim mode, dispose it
+ vimModeRef.current.dispose();
+ vimModeRef.current = null;
+ }
+ /* eslint-disable react-hooks/exhaustive-deps */
+ }, [mode, editorRef.current]);
+
return (
<>
+
+ {/* This is for displaying normal/insert mode status in Vim */}
+
+
>
);
@@ -49,6 +96,7 @@ Editor.propTypes = {
syntax: PropTypes.string,
onChange: PropTypes.func.isRequired,
theme: PropTypes.string.isRequired,
+ mode: PropTypes.string.isRequired,
loading: PropTypes.bool,
wordWrap: PropTypes.string,
lineNumbers: PropTypes.string,
diff --git a/services/app/apps/codebattle/assets/js/widgets/config/editorModes.js b/services/app/apps/codebattle/assets/js/widgets/config/editorModes.js
index 871635aa6..8463d20ba 100644
--- a/services/app/apps/codebattle/assets/js/widgets/config/editorModes.js
+++ b/services/app/apps/codebattle/assets/js/widgets/config/editorModes.js
@@ -1,4 +1,4 @@
export default {
default: 'default',
- // vim: 'vim',
+ vim: 'vim',
};
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 4b9a5cd9d..b77747d5f 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
@@ -1,8 +1,5 @@
import React, {
- useEffect,
- useContext,
- useCallback,
- useRef,
+ useEffect, useContext, useCallback, useRef,
} from 'react';
import { useInterpret } from '@xstate/react';
@@ -42,21 +39,27 @@ const restrictedText = '\n\n\n\t"Only for Premium subscribers"';
const useEditorChannelSubscription = (mainService, editorService, player) => {
const dispatch = useDispatch();
- const inTestingRoom = useMachineStateSelector(mainService, inTestingRoomSelector);
+ const inTestingRoom = useMachineStateSelector(
+ mainService,
+ inTestingRoomSelector,
+ );
const isPreview = useMachineStateSelector(mainService, inPreviewRoomSelector);
useEffect(() => {
if (isPreview) {
- return () => { };
+ return () => {};
}
if (inTestingRoom) {
editorService.send('load_testing_editor');
- return () => { };
+ return () => {};
}
- const clearEditorListeners = GameActions.connectToEditor(editorService, player?.isBanned)(dispatch);
+ const clearEditorListeners = GameActions.connectToEditor(
+ editorService,
+ player?.isBanned,
+ )(dispatch);
return clearEditorListeners;
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -86,58 +89,89 @@ function EditorContainer({
const subscriptionType = useSelector(selectors.subscriptionTypeSelector);
const currentUserId = useSelector(selectors.currentUserIdSelector);
- const currentEditorLangSlug = useSelector(selectors.userLangSelector(currentUserId));
+ const currentEditorLangSlug = useSelector(
+ selectors.userLangSelector(currentUserId),
+ );
- const updateEditorValue = useCallback(data => dispatch(GameActions.updateEditorText(data)), [dispatch]);
- const updateAndSendEditorValue = useCallback(data => {
- dispatch(GameActions.updateEditorText(data));
- dispatch(GameActions.sendEditorText(data));
- }, [dispatch]);
+ const updateEditorValue = useCallback(
+ data => dispatch(GameActions.updateEditorText(data)),
+ [dispatch],
+ );
+ const updateAndSendEditorValue = useCallback(
+ data => {
+ dispatch(GameActions.updateEditorText(data));
+ dispatch(GameActions.sendEditorText(data));
+ },
+ [dispatch],
+ );
const { mainService } = useContext(RoomContext);
const isPreview = useMachineStateSelector(mainService, inPreviewRoomSelector);
- const isRestricted = useMachineStateSelector(mainService, isRestrictedContentSelector);
- const inTestingRoom = useMachineStateSelector(mainService, inTestingRoomSelector);
- const inBuilderRoom = useMachineStateSelector(mainService, inBuilderRoomSelector);
- const isActiveGame = useMachineStateSelector(mainService, isGameActiveSelector);
+ const isRestricted = useMachineStateSelector(
+ mainService,
+ isRestrictedContentSelector,
+ );
+ const inTestingRoom = useMachineStateSelector(
+ mainService,
+ inTestingRoomSelector,
+ );
+ const inBuilderRoom = useMachineStateSelector(
+ mainService,
+ inBuilderRoomSelector,
+ );
+ const isActiveGame = useMachineStateSelector(
+ mainService,
+ isGameActiveSelector,
+ );
const isGameOver = useMachineStateSelector(mainService, isGameOverSelector);
- const openedReplayer = useMachineStateSelector(mainService, openedReplayerSelector);
+ const openedReplayer = useMachineStateSelector(
+ mainService,
+ openedReplayerSelector,
+ );
const isTournamentGame = !!tournamentId;
const context = { userId: id, type, subscriptionType };
- const editorService = useInterpret(
- editorMachine,
- {
- context,
- devTools: true,
- id: `editor_${id}`,
- actions: {
- userSendSolution: ctx => {
- if (ctx.editorState === 'active') {
- dispatch(GameActions.checkGameSolution());
- }
- },
- handleTimeoutFailureChecking: ctx => {
- dispatch(actions.updateExecutionOutput({
+ const editorService = useInterpret(editorMachine, {
+ context,
+ devTools: true,
+ id: `editor_${id}`,
+ actions: {
+ userSendSolution: ctx => {
+ if (ctx.editorState === 'active') {
+ dispatch(GameActions.checkGameSolution());
+ }
+ },
+ handleTimeoutFailureChecking: ctx => {
+ dispatch(
+ actions.updateExecutionOutput({
userId: ctx.userId,
status: 'client_timeout',
output: '',
result: {},
asserts: [],
- }));
+ }),
+ );
- dispatch(actions.updateCheckStatus({ [ctx.userId]: false }));
- },
+ dispatch(actions.updateCheckStatus({ [ctx.userId]: false }));
},
},
- );
+ });
- const editorCurrent = useMachineStateSelector(editorService, editorStateSelector);
+ const editorCurrent = useMachineStateSelector(
+ editorService,
+ editorStateSelector,
+ );
- const checkActiveTaskSolution = useCallback(() => editorService.send('user_check_solution'), [editorService]);
- const checkTestTaskSolution = useCallback(() => dispatch(GameActions.checkTaskSolution(editorService)), [dispatch, editorService]);
+ const checkActiveTaskSolution = useCallback(
+ () => editorService.send('user_check_solution'),
+ [editorService],
+ );
+ const checkTestTaskSolution = useCallback(
+ () => dispatch(GameActions.checkTaskSolution(editorService)),
+ [dispatch, editorService],
+ );
const checkResult = inTestingRoom
? checkTestTaskSolution
@@ -164,7 +198,7 @@ function EditorContainer({
};
}
- return () => { };
+ return () => {};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [inTestingRoom]);
@@ -195,9 +229,13 @@ function EditorContainer({
};
const canChange = userSettings.type === editorUserTypes.currentUser && !openedReplayer;
- const editable = !openedReplayer && userSettings.editable && userSettings.editorState !== 'banned';
+ const editable = !openedReplayer
+ && userSettings.editable
+ && userSettings.editorState !== 'banned';
const canSendCursor = canChange && !inTestingRoom && !inBuilderRoom;
- const updateEditor = editorCurrent.context.editorState === 'testing' ? updateEditorValue : updateAndSendEditorValue;
+ const updateEditor = editorCurrent.context.editorState === 'testing'
+ ? updateEditorValue
+ : updateAndSendEditorValue;
const onChange = canChange ? updateEditor : noop;
const editorParams = {
@@ -228,13 +266,19 @@ function EditorContainer({
'bg-winner': isGameOver && editorCurrent.matches('idle') && isWon,
});
- const gameRoomEditorStylesVersion2 = { minHeight: `calc(100vh - 92px - ${toolbarRef.current?.clientHeight || 0}px)` };
+ const gameRoomEditorStylesVersion2 = {
+ minHeight: `calc(100vh - 92px - ${toolbarRef.current?.clientHeight || 0}px)`,
+ };
return (
(
(
role="group"
aria-label="Editor mode"
>
+
);
diff --git a/services/app/apps/codebattle/assets/js/widgets/pages/game/VimModeButton.jsx b/services/app/apps/codebattle/assets/js/widgets/pages/game/VimModeButton.jsx
index 8b9f55456..0b5d02b56 100644
--- a/services/app/apps/codebattle/assets/js/widgets/pages/game/VimModeButton.jsx
+++ b/services/app/apps/codebattle/assets/js/widgets/pages/game/VimModeButton.jsx
@@ -1,6 +1,7 @@
import React from 'react';
import cn from 'classnames';
+// import { Col } from 'react-bootstrap';
import { useDispatch, useSelector } from 'react-redux';
import editorModes from '../../config/editorModes';
@@ -10,23 +11,32 @@ import { actions } from '../../slices';
function VimModeButton() {
const dispatch = useDispatch();
const currentMode = useSelector(editorsModeSelector);
-
const isVimMode = currentMode === editorModes.vim;
- const mode = isVimMode ? editorModes.default : editorModes.vim;
+ const handleToggleVimMode = () => {
+ dispatch(
+ actions.setEditorsMode(isVimMode ? editorModes.default : editorModes.vim),
+ );
+ };
+
+ // Use meaningful text, not just color, to indicate state
+ const buttonText = isVimMode ? 'Vim' : 'Vim';
+ // Keep styling if desired, but ensure text clarifies the mode
const classNames = cn('btn btn-sm rounded-left', {
'btn-light': !isVimMode,
'btn-secondary': isVimMode,
});
- const handleToggleVimMode = () => {
- dispatch(actions.setEditorsMode(mode));
- };
-
return (
-
- Vim
+
+ {buttonText}
);
}
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 0264536d9..801ef7445 100644
--- a/services/app/apps/codebattle/assets/js/widgets/utils/useEditor.js
+++ b/services/app/apps/codebattle/assets/js/widgets/utils/useEditor.js
@@ -285,6 +285,38 @@ const useEditor = props => {
toggleMuteSound();
},
});
+
+ domNode.addEventListener(
+ 'wheel',
+ e => {
+ const scrollTop = currentEditor.getScrollTop();
+ const scrollHeight = currentEditor.getScrollHeight();
+ const clientHeight = currentEditor.getLayoutInfo().height;
+
+ const { deltaY } = e;
+
+ const atTop = scrollTop <= 0;
+ const atBottom = scrollTop + clientHeight >= scrollHeight - 1;
+
+ const scrollingDown = deltaY > 0;
+ const scrollingUp = deltaY < 0;
+
+ const shouldBubble = (scrollingUp && atTop) || (scrollingDown && atBottom);
+
+ if (shouldBubble) {
+ // Prevent Monaco from swallowing the event
+ e.preventDefault();
+
+ // Forward the scroll to the window (including momentum scroll)
+ window.scrollBy({
+ top: deltaY,
+ left: 0,
+ behavior: 'auto', // "smooth" breaks momentum feel from touchpad
+ });
+ }
+ },
+ { passive: false }, // Needed so we can call preventDefault()
+ );
};
return {
diff --git a/services/app/apps/codebattle/package.json b/services/app/apps/codebattle/package.json
index be4927ea4..c2af9b1f7 100644
--- a/services/app/apps/codebattle/package.json
+++ b/services/app/apps/codebattle/package.json
@@ -75,6 +75,7 @@
"monaco-editor": "^0.52.2",
"monaco-editor-webpack-plugin": "^7.1.0",
"monaco-themes": "^0.4.4",
+ "monaco-vim": "^0.4.2",
"nprogress": "^0.2.0",
"path-browserify": "^1.0.1",
"phoenix": "^1.6.6",
diff --git a/services/app/apps/codebattle/yarn.lock b/services/app/apps/codebattle/yarn.lock
index 2b4e693f1..53215f96c 100644
--- a/services/app/apps/codebattle/yarn.lock
+++ b/services/app/apps/codebattle/yarn.lock
@@ -9421,6 +9421,11 @@ monaco-themes@^0.4.4:
dependencies:
fast-plist "^0.1.3"
+monaco-vim@^0.4.2:
+ version "0.4.2"
+ resolved "https://registry.yarnpkg.com/monaco-vim/-/monaco-vim-0.4.2.tgz#b56a6bbe2332c987391b3d04000134e0c645da19"
+ integrity sha512-rdbQC3O2rmpwX2Orzig/6gZjZfH7q7TIeB+uEl49sa+QyNm3jCKJOw5mwxBdFzTqbrPD+URfg6A2lEkuL5kymw==
+
mozjpeg@^7.0.0:
version "7.1.1"
resolved "https://registry.yarnpkg.com/mozjpeg/-/mozjpeg-7.1.1.tgz#dfb61953536e66fcabd4ae795e7a312d42a51f18"