Skip to content

Commit aee0bf2

Browse files
HKalbasishepmaster
authored andcommitted
Add the Monaco editor
1 parent e1e8c08 commit aee0bf2

13 files changed

+321
-11
lines changed

tests/spec/features/editor_types_spec.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
before { visit '/' }
88

99
scenario "using the simple editor" do
10-
in_config_menu { choose("simple") }
10+
in_config_menu { select("simple") }
1111

1212
fill_in('editor-simple', with: simple_editor_code)
1313

ui/frontend/ConfigMenu.tsx

+24-7
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,14 @@ interface ConfigMenuProps {
2121
close: () => void;
2222
}
2323

24+
const MONACO_THEMES = [
25+
'vs', 'vs-dark', 'vscode-dark-plus',
26+
];
27+
2428
const ConfigMenu: React.SFC<ConfigMenuProps> = () => {
2529
const keybinding = useSelector((state: State) => state.configuration.ace.keybinding);
2630
const aceTheme = useSelector((state: State) => state.configuration.ace.theme);
31+
const monacoTheme = useSelector((state: State) => state.configuration.monaco.theme);
2732
const orientation = useSelector((state: State) => state.configuration.orientation);
2833
const editorStyle = useSelector((state: State) => state.configuration.editor);
2934
const pairCharacters = useSelector((state: State) => state.configuration.ace.pairCharacters);
@@ -33,6 +38,7 @@ const ConfigMenu: React.SFC<ConfigMenuProps> = () => {
3338

3439
const dispatch = useDispatch();
3540
const changeAceTheme = useCallback((t) => dispatch(actions.changeAceTheme(t)), [dispatch]);
41+
const changeMonacoTheme = useCallback((t) => dispatch(actions.changeMonacoTheme(t)), [dispatch]);
3642
const changeKeybinding = useCallback((k) => dispatch(actions.changeKeybinding(k)), [dispatch]);
3743
const changeOrientation = useCallback((o) => dispatch(actions.changeOrientation(o)), [dispatch]);
3844
const changeEditorStyle = useCallback((e) => dispatch(actions.changeEditor(e)), [dispatch]);
@@ -44,14 +50,14 @@ const ConfigMenu: React.SFC<ConfigMenuProps> = () => {
4450
return (
4551
<Fragment>
4652
<MenuGroup title="Editor">
47-
<EitherConfig
48-
id="editor-style"
49-
name="Style"
50-
a={Editor.Simple}
51-
b={Editor.Ace}
53+
<SelectConfig
54+
name="Editor"
5255
value={editorStyle}
53-
onChange={changeEditorStyle} />
54-
56+
onChange={changeEditorStyle}
57+
>
58+
{[Editor.Simple, Editor.Ace, Editor.Monaco]
59+
.map(k => <option key={k} value={k}>{k}</option>)}
60+
</SelectConfig>
5561
{editorStyle === Editor.Ace && (
5662
<Fragment>
5763
<SelectConfig
@@ -79,6 +85,17 @@ const ConfigMenu: React.SFC<ConfigMenuProps> = () => {
7985
onChange={changePairCharacters} />
8086
</Fragment>
8187
)}
88+
{editorStyle === Editor.Monaco && (
89+
<Fragment>
90+
<SelectConfig
91+
name="Theme"
92+
value={monacoTheme}
93+
onChange={changeMonacoTheme}
94+
>
95+
{MONACO_THEMES.map(t => <option key={t} value={t}>{t}</option>)}
96+
</SelectConfig>
97+
</Fragment>
98+
)}
8299
</MenuGroup>
83100

84101
<MenuGroup title="UI">

ui/frontend/actions.ts

+5
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ export enum ActionType {
6464
ChangeEditor = 'CHANGE_EDITOR',
6565
ChangeKeybinding = 'CHANGE_KEYBINDING',
6666
ChangeAceTheme = 'CHANGE_ACE_THEME',
67+
ChangeMonacoTheme = 'CHANGE_MONACO_THEME',
6768
ChangePairCharacters = 'CHANGE_PAIR_CHARACTERS',
6869
ChangeOrientation = 'CHANGE_ORIENTATION',
6970
ChangeAssemblyFlavor = 'CHANGE_ASSEMBLY_FLAVOR',
@@ -141,6 +142,9 @@ export const changeKeybinding = (keybinding: string) =>
141142
export const changeAceTheme = (theme: string) =>
142143
createAction(ActionType.ChangeAceTheme, { theme });
143144

145+
export const changeMonacoTheme = (theme: string) =>
146+
createAction(ActionType.ChangeMonacoTheme, { theme });
147+
144148
export const changePairCharacters = (pairCharacters: PairCharacters) =>
145149
createAction(ActionType.ChangePairCharacters, { pairCharacters });
146150

@@ -817,6 +821,7 @@ export type Action =
817821
| ReturnType<typeof changePrimaryAction>
818822
| ReturnType<typeof changeProcessAssembly>
819823
| ReturnType<typeof changeAceTheme>
824+
| ReturnType<typeof changeMonacoTheme>
820825
| ReturnType<typeof requestExecute>
821826
| ReturnType<typeof receiveExecuteSuccess>
822827
| ReturnType<typeof receiveExecuteFailure>

ui/frontend/editor/Editor.module.css

+10-2
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,20 @@
33
position: relative;
44
}
55

6-
.ace {
6+
.-advanced {
77
composes: -bodyMonospace -autoSize from '../shared.module.css';
88
position: absolute;
99
}
1010

11+
.ace {
12+
composes: -advanced;
13+
}
14+
15+
.monaco {
16+
composes: -advanced;
17+
}
18+
1119
.simple {
12-
composes: ace;
20+
composes: -advanced;
1321
border: none;
1422
}

ui/frontend/editor/Editor.tsx

+8-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { CommonEditorProps, Editor as EditorType, Position, Selection } from '..
77
import { State } from '../reducers';
88

99
import styles from './Editor.module.css';
10+
import MonacoEditor from './MonacoEditor';
1011

1112
class CodeByteOffsets {
1213
readonly code: string;
@@ -107,6 +108,12 @@ class SimpleEditor extends React.PureComponent<CommonEditorProps> {
107108
}
108109
}
109110

111+
const editorMap = {
112+
[EditorType.Simple]: SimpleEditor,
113+
[EditorType.Ace]: AceEditor,
114+
[EditorType.Monaco]: MonacoEditor,
115+
};
116+
110117
const Editor: React.SFC = () => {
111118
const code = useSelector((state: State) => state.code);
112119
const editor = useSelector((state: State) => state.configuration.editor);
@@ -118,7 +125,7 @@ const Editor: React.SFC = () => {
118125
const execute = useCallback(() => dispatch(actions.performPrimaryAction()), [dispatch]);
119126
const onEditCode = useCallback((c) => dispatch(actions.editCode(c)), [dispatch]);
120127

121-
const SelectedEditor = editor === EditorType.Simple ? SimpleEditor : AceEditor;
128+
const SelectedEditor = editorMap[editor];
122129

123130
return (
124131
<div className={styles.container}>

ui/frontend/editor/MonacoEditor.tsx

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import React, { Suspense } from 'react';
2+
3+
import { CommonEditorProps } from '../types';
4+
5+
const MonacoEditorLazy = React.lazy(() => import('./MonacoEditorCore'));
6+
7+
const MonacoEditor: React.SFC<CommonEditorProps> = props => (
8+
<Suspense fallback={'Loading'}>
9+
<MonacoEditorLazy {...props} />
10+
</Suspense>
11+
)
12+
13+
export default MonacoEditor;
+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import React from 'react';
2+
import { CommonEditorProps } from '../types';
3+
import MonacoEditor, { EditorWillMount } from 'react-monaco-editor';
4+
import { useSelector } from 'react-redux';
5+
import State from '../state';
6+
import { config, grammar, themeVsDarkPlus } from './rust_monaco_def';
7+
8+
import styles from './Editor.module.css';
9+
10+
const MODE_ID = 'my-rust';
11+
12+
const initMonaco: EditorWillMount = (monaco) => {
13+
monaco.editor.defineTheme('vscode-dark-plus', themeVsDarkPlus);
14+
15+
monaco.languages.register({
16+
id: MODE_ID,
17+
});
18+
19+
monaco.languages.onLanguage(MODE_ID, async () => {
20+
monaco.languages.setLanguageConfiguration(MODE_ID, config);
21+
monaco.languages.setMonarchTokensProvider(MODE_ID, grammar);
22+
});
23+
};
24+
25+
const MonacoEditorCore: React.SFC<CommonEditorProps> = props => {
26+
const theme = useSelector((s: State) => s.configuration.monaco.theme);
27+
28+
return (
29+
<MonacoEditor
30+
language={MODE_ID}
31+
theme={theme}
32+
className={styles.monaco}
33+
value={props.code}
34+
onChange={props.onEditCode}
35+
editorWillMount={initMonaco}
36+
options={{ automaticLayout: true }}
37+
/>
38+
);
39+
}
40+
41+
export default MonacoEditorCore;

ui/frontend/editor/rust_monaco_def.ts

+163
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import { languages, editor } from 'monaco-editor';
2+
3+
export const config: languages.LanguageConfiguration = {
4+
comments: {
5+
lineComment: '//',
6+
blockComment: ['/*', '*/'],
7+
},
8+
brackets: [
9+
['{', '}'],
10+
['[', ']'],
11+
['(', ')'],
12+
],
13+
autoClosingPairs: [
14+
{ open: '[', close: ']' },
15+
{ open: '{', close: '}' },
16+
{ open: '(', close: ')' },
17+
{ open: '"', close: '"', notIn: ['string'] },
18+
],
19+
surroundingPairs: [
20+
{ open: '{', close: '}' },
21+
{ open: '[', close: ']' },
22+
{ open: '(', close: ')' },
23+
{ open: '"', close: '"' },
24+
{ open: '\'', close: '\'' },
25+
],
26+
folding: {
27+
markers: {
28+
start: new RegExp('^\\s*#pragma\\s+region\\b'),
29+
end: new RegExp('^\\s*#pragma\\s+endregion\\b'),
30+
},
31+
},
32+
};
33+
34+
export const grammar: languages.IMonarchLanguage = {
35+
// Set defaultToken to invalid to see what you do not tokenize yet
36+
// defaultToken: 'invalid',
37+
38+
keywords: [
39+
'as', 'break', 'const', 'crate', 'enum', 'extern', 'false', 'fn', 'impl', 'in',
40+
'let', 'mod', 'move', 'mut', 'pub', 'ref', 'return', 'self', 'Self', 'static',
41+
'struct', 'super', 'trait', 'true', 'type', 'unsafe', 'use', 'where',
42+
'macro_rules',
43+
],
44+
45+
controlFlowKeywords: [
46+
'continue', 'else', 'for', 'if', 'while', 'loop', 'match',
47+
],
48+
49+
typeKeywords: [
50+
'Self', 'm32', 'm64', 'm128', 'f80', 'f16', 'f128', 'int', 'uint', 'float', 'char',
51+
'bool', 'u8', 'u16', 'u32', 'u64', 'f32', 'f64', 'i8', 'i16', 'i32', 'i64', 'str',
52+
'Option', 'Either', 'c_float', 'c_double', 'c_void', 'FILE', 'fpos_t', 'DIR', 'dirent',
53+
'c_char', 'c_schar', 'c_uchar', 'c_short', 'c_ushort', 'c_int', 'c_uint', 'c_long', 'c_ulong',
54+
'size_t', 'ptrdiff_t', 'clock_t', 'time_t', 'c_longlong', 'c_ulonglong', 'intptr_t',
55+
'uintptr_t', 'off_t', 'dev_t', 'ino_t', 'pid_t', 'mode_t', 'ssize_t',
56+
],
57+
58+
operators: [
59+
'=', '>', '<', '!', '~', '?', ':', '==', '<=', '>=', '!=',
60+
'&&', '||', '++', '--', '+', '-', '*', '/', '&', '|', '^', '%',
61+
'<<', '>>', '>>>', '+=', '-=', '*=', '/=', '&=', '|=', '^=',
62+
'%=', '<<=', '>>=', '>>>=',
63+
],
64+
65+
// we include these common regular expressions
66+
symbols: /[=><!~?:&|+\-*\/\^%]+/,
67+
68+
// for strings
69+
escapes: /\\(?:[abfnrtv\\"']|x[0-9A-Fa-f]{1,4}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})/,
70+
71+
// The main tokenizer for our languages
72+
tokenizer: {
73+
root: [
74+
// identifiers and keywords
75+
[/[a-z_$][\w$]*/, {
76+
cases: {
77+
'@typeKeywords': 'type.identifier',
78+
'@keywords': {
79+
cases: {
80+
'fn': { token: 'keyword', next: '@func_decl' },
81+
'@default': 'keyword',
82+
},
83+
},
84+
'@controlFlowKeywords': 'keyword.control',
85+
'@default': 'variable',
86+
},
87+
}],
88+
[/[A-Z][\w\$]*/, 'type.identifier'], // to show class names nicely
89+
90+
// whitespace
91+
{ include: '@whitespace' },
92+
93+
// delimiters and operators
94+
[/[{}()\[\]]/, '@brackets'],
95+
[/[<>](?!@symbols)/, '@brackets'],
96+
[/@symbols/, {
97+
cases: {
98+
'@operators': 'operator',
99+
'@default': '',
100+
},
101+
}],
102+
103+
// @ annotations.
104+
// As an example, we emit a debugging log message on these tokens.
105+
// Note: message are supressed during the first load -- change some lines to see them.
106+
[/@\s*[a-zA-Z_\$][\w\$]*/, { token: 'annotation', log: 'annotation token: $0' }],
107+
108+
// numbers
109+
[/\d*\.\d+([eE][\-+]?\d+)?/, 'number.float'],
110+
[/0[xX][0-9a-fA-F]+/, 'number.hex'],
111+
[/\d+/, 'number'],
112+
113+
// delimiter: after number because of .\d floats
114+
[/[;,.]/, 'delimiter'],
115+
116+
// strings
117+
[/"([^"\\]|\\.)*$/, 'string.invalid'], // non-teminated string
118+
[/"/, { token: 'string.quote', bracket: '@open', next: '@string' }],
119+
120+
// characters
121+
[/'[^\\']'/, 'string'],
122+
[/(')(@escapes)(')/, ['string', 'string.escape', 'string']],
123+
[/'/, 'string.invalid'],
124+
],
125+
126+
comment: [
127+
[/[^\/*]+/, 'comment'],
128+
[/\/\*/, 'comment', '@push'], // nested comment
129+
['\\*/', 'comment', '@pop'],
130+
[/[\/*]/, 'comment'],
131+
],
132+
133+
string: [
134+
[/[^\\"]+/, 'string'],
135+
[/@escapes/, 'string.escape'],
136+
[/\\./, 'string.escape.invalid'],
137+
[/"/, { token: 'string.quote', bracket: '@close', next: '@pop' }],
138+
],
139+
140+
whitespace: [
141+
[/[ \t\r\n]+/, 'white'],
142+
[/\/\*/, 'comment', '@comment'],
143+
[/\/\/.*$/, 'comment'],
144+
],
145+
146+
func_decl: [
147+
[
148+
/[a-z_$][\w$]*/, 'support.function', '@pop',
149+
],
150+
],
151+
},
152+
};
153+
154+
export const themeVsDarkPlus: editor.IStandaloneThemeData = {
155+
base: 'vs-dark',
156+
inherit: true,
157+
colors: {},
158+
rules: [
159+
{ token: 'keyword.control', foreground: 'C586C0' },
160+
{ token: 'variable', foreground: '9CDCFE' },
161+
{ token: 'support.function', foreground: 'DCDCAA' },
162+
],
163+
};

ui/frontend/package.json

+3
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@
1111
"history": "^4.6.0",
1212
"isomorphic-fetch": "^3.0.0",
1313
"lodash": "^4.17.0",
14+
"monaco-editor": "^0.31.1",
1415
"prismjs": "^1.6.0",
1516
"qs": "^6.4.0",
1617
"react": "^17.0.1",
1718
"react-copy-to-clipboard": "^5.0.1",
1819
"react-dom": "^17.0.1",
20+
"react-monaco-editor": "^0.46.0",
1921
"react-popper": "^2.0.0",
2022
"react-portal": "^4.1.4",
2123
"react-prism": "^4.0.0",
@@ -58,6 +60,7 @@
5860
"jest": "^27.0.0",
5961
"json-loader": "^0.5.4",
6062
"mini-css-extract-plugin": "^2.0.0",
63+
"monaco-editor-webpack-plugin": "^7.0.1",
6164
"normalize.css": "^8.0.0",
6265
"postcss": "^8.2.7",
6366
"postcss-loader": "^6.1.0",

0 commit comments

Comments
 (0)