Skip to content

Commit

Permalink
move to monaco
Browse files Browse the repository at this point in the history
  • Loading branch information
frontstall committed Sep 2, 2021
1 parent c698418 commit 167929a
Show file tree
Hide file tree
Showing 8 changed files with 82 additions and 117 deletions.
5 changes: 2 additions & 3 deletions app/packs/src/lesson/components/ControlBox.jsx
Expand Up @@ -19,8 +19,7 @@ import EntityContext from '../EntityContext.js';

const ControlBox = () => {
const { t } = useTranslation();
const { checkInfo, lessonInfo, editor } = useSelector((state) => ({
editor: state.editorSlice,
const { checkInfo, lessonInfo } = useSelector((state) => ({
checkInfo: state.checkInfoSlice,
lessonInfo: state.lessonSlice,
}));
Expand All @@ -31,7 +30,7 @@ const ControlBox = () => {
} = useContext(EntityContext);

const handleRunCheck = () => {
dispatch(actions.runCheck({ lessonVersion, editor }));
dispatch(actions.runCheck({ lessonVersion }));
};

const handleReset = () => {
Expand Down
136 changes: 47 additions & 89 deletions app/packs/src/lesson/components/Editor.jsx
@@ -1,126 +1,84 @@
/* eslint-disable no-bitwise */
// @ts-check

import React, { useContext, useEffect, useState } from 'react';
import React, { useContext, useEffect, useRef } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { UnControlled as CodeMirror } from 'react-codemirror2';
import MonacoEditor from '@monaco-editor/react';
import { useLocalStorage } from '@rehooks/local-storage';
import { actions } from '../slices/index.js';
import { getLanguageForEditor, getTabSize, shouldReplaceTabsWithSpaces } from '../utils/editorUtils.js';
import { getTabSize, shouldReplaceTabsWithSpaces } from '../utils/editorUtils.js';

import EntityContext from '../EntityContext.js';

import 'codemirror/mode/htmlmixed/htmlmixed.js';
import 'codemirror/mode/javascript/javascript.js';
import 'codemirror/mode/css/css.js';
import 'codemirror/mode/yaml/yaml.js';
import 'codemirror/mode/shell/shell.js';
import 'codemirror/mode/jsx/jsx.js';
import 'codemirror/mode/markdown/markdown.js';
import 'codemirror/mode/ruby/ruby.js';
import 'codemirror/mode/erlang/erlang.js';
import 'codemirror/mode/python/python.js';
import 'codemirror/mode/scheme/scheme.js';
import 'codemirror/mode/php/php.js';
import 'codemirror/mode/sass/sass.js';
import 'codemirror/mode/pug/pug.js';
import 'codemirror/mode/clike/clike.js';
import 'codemirror/mode/go/go.js';
import 'codemirror-mode-elixir';

import 'codemirror/lib/codemirror.css';
import 'codemirror/addon/scroll/simplescrollbars.css';
import 'codemirror/addon/dialog/dialog.css';

import 'codemirror/keymap/sublime.js';

import 'codemirror/addon/edit/closebrackets.js';
import 'codemirror/addon/edit/matchtags.js';
import 'codemirror/addon/edit/matchbrackets.js';
import 'codemirror/addon/edit/closetag.js';
import 'codemirror/addon/fold/xml-fold.js';
import 'codemirror/addon/comment/comment.js';
import 'codemirror/addon/scroll/simplescrollbars.js';
import 'codemirror/addon/search/searchcursor.js';
import 'codemirror/addon/search/search.js';
import 'codemirror/addon/search/jump-to-line.js';
import 'codemirror/addon/dialog/dialog.js';

const commonOptions = {
autoCloseBrackets: true,
autoCloseTags: true,
autofocus: true,
keyMap: 'sublime',
matchBrackets: true,
matchTags: true,
scrollbarStyle: 'overlay',
lineNumbers: true,
fontSize: 14,
scrollBeyondLastLine: false,
minimap: {
enabled: false,
},
hover: {
delay: 1000,
},
renderWhitespace: 'trailing',
formatOnPaste: true,
renderLineHighlight: false,
};

const Editor = () => {
const { language, lessonVersion } = useContext(EntityContext);
const { content, focusesCount } = useSelector((state) => state.editorSlice);
const dispatch = useDispatch();
const [editor, setEditor] = useState(null);
const editorRef = useRef(null);

const localStorageKey = `lesson-version-${lessonVersion.id}`;
const [localStorageContent, setContent] = useLocalStorage(localStorageKey);

useEffect(() => {
editor?.focus();
}, [editor, focusesCount]);
editorRef.current?.focus();
}, [focusesCount]);

const onMount = (self) => {
setEditor(self);
self.focus();
self.refresh();
if (localStorageContent) {
self.getDoc().setValue(localStorageContent);
}
const handleRunCheck = () => {
dispatch(actions.runCheck({ lessonVersion }));
};

const onContentChange = (_editor, _data, newContent) => {
setContent(newContent);
dispatch(actions.changeContent({ content: newContent }));
const editorOptions = {
tabSize: getTabSize(language),
insertSpaces: shouldReplaceTabsWithSpaces(language),
};

const replaceTab = (cm) => {
const space = ' ';
const spaces = space.repeat(cm.getOption('indentUnit'));
cm.replaceSelection(spaces);
};
const onMount = (editor, monaco) => {
editorRef.current = editor;
const model = editor.getModel();
model.updateOptions(editorOptions);
model.pushEOL(0);

const handleRunCheck = (e) => {
const value = e.getValue();
dispatch(actions.runCheck({ lessonVersion, editor: { content: value } }));
};

const extraKeys = {
'Ctrl-Enter': handleRunCheck,
};
editorRef.current.focus();

const indentWithTabs = !shouldReplaceTabsWithSpaces(language);
const indentWithSpaces = !indentWithTabs;
const extraKeys = [
{
key: monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter,
action: handleRunCheck,
},
];

if (indentWithSpaces) {
extraKeys.Tab = replaceTab;
}
extraKeys.forEach(({ key, action }) => {
editorRef.current.addCommand(key, action);
});
};

const options = {
autofocus: true,
...commonOptions,
mode: getLanguageForEditor(language),
indentUnit: getTabSize(language),
indentWithTabs,
extraKeys,
const onContentChange = (newContent) => {
setContent(newContent);
dispatch(actions.changeContent({ content: newContent }));
};

return (
<CodeMirror
<MonacoEditor
defaultValue={localStorageContent || ''}
value={content}
options={options}
options={commonOptions}
defaultLanguage={language}
onChange={onContentChange}
detach
editorDidMount={onMount}
onMount={onMount}
className="w-100 h-100"
/>
);
Expand Down
5 changes: 4 additions & 1 deletion app/packs/src/lesson/index.jsx
Expand Up @@ -40,9 +40,12 @@ export default async () => {

const isFinished = gon.lesson_member.state === lessonMemberStates.finished;

const localStorageKey = `lesson-version-${gon.lesson_version.id}`;
const locallySavedContent = localStorage.getItem(localStorageKey);

const preloadedState = {
editorSlice: {
content: gon.lesson_version.prepared_code || '',
content: locallySavedContent || gon.lesson_version.prepared_code || '',
focusesCount: 1,
},
solutionSlice: {
Expand Down
5 changes: 3 additions & 2 deletions app/packs/src/lesson/slices/checkInfoSlice.js
Expand Up @@ -9,13 +9,14 @@ import hexletAxios from 'lib/hexlet-axios.js';
import routes from 'vendor/appRoutes.js';
import { checkInfoStates } from '../utils/maps.js';

const runCheck = createAsyncThunk('runCheck', async ({ lessonVersion, editor }) => {
const runCheck = createAsyncThunk('runCheck', async ({ lessonVersion }, { getState }) => {
const { editorSlice: { content } } = getState();
const checkLessonPath = routes.checkApiLessonPath(lessonVersion.lesson_id);
const response = await hexletAxios.post(checkLessonPath, {
version_id: lessonVersion.id,
data: {
attributes: {
code: editor.content,
code: content,
},
},
});
Expand Down
5 changes: 3 additions & 2 deletions app/packs/src/lesson/utils/editorUtils.js
Expand Up @@ -8,8 +8,8 @@ const languageMapping = {

const editorMapping = {
racket: 'scheme',
java: 'text/x-java',
html: 'text/html',
// java: 'text/x-java',
// html: 'text/html',
};

const langToTabSizeMapping = {
Expand Down Expand Up @@ -41,6 +41,7 @@ export const langToSpacesMapping = {
'text/html': true,
html: true,
css: true,
elixir: true,
};

const defaultTabSize = 4;
Expand Down
2 changes: 1 addition & 1 deletion app/views/web/languages/lessons/show.html.slim
Expand Up @@ -77,4 +77,4 @@
/ - else
.d-flex.flex-column.x-h-md-100#basics-lesson-container data-testid="basics-lesson-container"
- append_javascript_packs 'lesson'
= stylesheet_pack_tag 'lesson', media: 'all'
/ = stylesheet_pack_tag 'lesson', media: 'all'
4 changes: 1 addition & 3 deletions package.json
Expand Up @@ -5,6 +5,7 @@
"pre-push": "make app-lint"
},
"dependencies": {
"@monaco-editor/react": "^4.2.2",
"@popperjs/core": "^2.9.2",
"@rails/actioncable": "^6.1.4",
"@rails/activestorage": "^6.1.4",
Expand All @@ -19,8 +20,6 @@
"bootstrap-icons": "^1.5.0",
"caniuse-lite": "^1.0.30001246",
"classnames": "^2.3.1",
"codemirror": "^5.62.2",
"codemirror-mode-elixir": "^1.1.2",
"compression-webpack-plugin": "^8.0.1",
"core-js": "^3.15.2",
"date-fns": "^2.23.0",
Expand All @@ -32,7 +31,6 @@
"prop-types": "^15.7.2",
"react": "^17.0.2",
"react-bootstrap": "^2.0.0-beta.4",
"react-codemirror2": "^7.2.1",
"react-countdown": "^2.3.2",
"react-dom": "^17.0.2",
"react-fast-highlight": "^4.1.0",
Expand Down
37 changes: 21 additions & 16 deletions yarn.lock
Expand Up @@ -1206,6 +1206,21 @@
"@types/yargs" "^16.0.0"
chalk "^4.0.0"

"@monaco-editor/loader@^1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@monaco-editor/loader/-/loader-1.1.1.tgz#37db648c81a86946d0febd391de00df9c28a0a3d"
integrity sha512-mkT4r4xDjIyOG9o9M6rJDSzEIeonwF80sYErxEvAAL4LncFVdcbNli8Qv6NDqF6nyv6sunuKkDzo4iFjxPL+uQ==
dependencies:
state-local "^1.0.6"

"@monaco-editor/react@^4.2.2":
version "4.2.2"
resolved "https://registry.yarnpkg.com/@monaco-editor/react/-/react-4.2.2.tgz#636e5b8eb9519ef62f475f9a4a50f62ee0c493a8"
integrity sha512-yDDct+f/mZ946tJEXudvmMC8zXDygkELNujpJGjqJ0gS3WePZmS/IwBBsuJ8JyKQQC3Dy/+Ivg1sSpW+UvCv9g==
dependencies:
"@monaco-editor/loader" "^1.1.1"
prop-types "^15.7.2"

"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz"
Expand Down Expand Up @@ -2476,16 +2491,6 @@ co@^4.6.0:
resolved "https://registry.npmjs.org/co/-/co-4.6.0.tgz"
integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=

codemirror-mode-elixir@^1.1.2:
version "1.1.2"
resolved "https://registry.npmjs.org/codemirror-mode-elixir/-/codemirror-mode-elixir-1.1.2.tgz"
integrity sha512-1oIuRVHyUhLv0Za9sEIsI7urAj06EohwO/yVj10bg7aHnimHQ964Wk3uuoPH0Yn8L38EkOd+SwULYpDiCQtoTA==

codemirror@^5.62.2:
version "5.62.2"
resolved "https://registry.npmjs.org/codemirror/-/codemirror-5.62.2.tgz"
integrity sha512-tVFMUa4J3Q8JUd1KL9yQzQB0/BJt7ZYZujZmTPgo/54Lpuq3ez4C8x/ATUY/wv7b7X3AUq8o3Xd+2C5ZrCGWHw==

collect-v8-coverage@^1.0.0:
version "1.0.1"
resolved "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz"
Expand Down Expand Up @@ -6509,11 +6514,6 @@ react-bootstrap@^2.0.0-beta.4:
uncontrollable "^7.2.1"
warning "^4.0.3"

react-codemirror2@^7.2.1:
version "7.2.1"
resolved "https://registry.npmjs.org/react-codemirror2/-/react-codemirror2-7.2.1.tgz"
integrity sha512-t7YFmz1AXdlImgHXA9Ja0T6AWuopilub24jRaQdPVbzUJVNKIYuy3uCFZYa7CE5S3UW6SrSa5nAqVQvtzRF9gw==

react-countdown@^2.3.2:
version "2.3.2"
resolved "https://registry.npmjs.org/react-countdown/-/react-countdown-2.3.2.tgz"
Expand Down Expand Up @@ -7346,6 +7346,11 @@ stack-utils@^2.0.3:
dependencies:
escape-string-regexp "^2.0.0"

state-local@^1.0.6:
version "1.0.7"
resolved "https://registry.yarnpkg.com/state-local/-/state-local-1.0.7.tgz#da50211d07f05748d53009bee46307a37db386d5"
integrity sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==

static-extend@^0.1.1:
version "0.1.2"
resolved "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz"
Expand Down Expand Up @@ -7829,7 +7834,7 @@ urix@^0.1.0:

url-parse@^1.4.3, url-parse@^1.5.1:
version "1.5.3"
resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.3.tgz#71c1303d38fb6639ade183c2992c8cc0686df862"
resolved "https://registry.npmjs.org/url-parse/-/url-parse-1.5.3.tgz"
integrity sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ==
dependencies:
querystringify "^2.1.1"
Expand Down

0 comments on commit 167929a

Please sign in to comment.