From a76c9d733cf2f1576ec287f5f4c0fead61be6d63 Mon Sep 17 00:00:00 2001 From: hamidreza kalbasi Date: Sun, 22 Aug 2021 02:21:05 +0430 Subject: [PATCH 1/4] Rename advanced editor to Ace --- ui/frontend/ConfigMenu.tsx | 16 ++-- ui/frontend/Editor.module.css | 14 --- ui/frontend/Playground.tsx | 2 +- ui/frontend/actions.ts | 8 +- .../AceEditor.tsx} | 38 ++++---- ui/frontend/editor/Editor.module.css | 14 +++ ui/frontend/{ => editor}/Editor.tsx | 10 +- .../ace-editor.ts} | 0 ui/frontend/local_storage.spec.ts | 15 ++- ui/frontend/local_storage.ts | 94 ++++++++++++++++--- ui/frontend/reducers/configuration.ts | 37 +++++--- ui/frontend/types.ts | 2 +- 12 files changed, 169 insertions(+), 81 deletions(-) delete mode 100644 ui/frontend/Editor.module.css rename ui/frontend/{AdvancedEditor.tsx => editor/AceEditor.tsx} (92%) create mode 100644 ui/frontend/editor/Editor.module.css rename ui/frontend/{ => editor}/Editor.tsx (96%) rename ui/frontend/{advanced-editor.ts => editor/ace-editor.ts} (100%) diff --git a/ui/frontend/ConfigMenu.tsx b/ui/frontend/ConfigMenu.tsx index 58a292439..f5b864645 100644 --- a/ui/frontend/ConfigMenu.tsx +++ b/ui/frontend/ConfigMenu.tsx @@ -22,17 +22,17 @@ interface ConfigMenuProps { } const ConfigMenu: React.SFC = () => { - const keybinding = useSelector((state: State) => state.configuration.keybinding); - const theme = useSelector((state: State) => state.configuration.theme); + const keybinding = useSelector((state: State) => state.configuration.ace.keybinding); + const aceTheme = useSelector((state: State) => state.configuration.ace.theme); const orientation = useSelector((state: State) => state.configuration.orientation); const editorStyle = useSelector((state: State) => state.configuration.editor); - const pairCharacters = useSelector((state: State) => state.configuration.pairCharacters); + const pairCharacters = useSelector((state: State) => state.configuration.ace.pairCharacters); const assemblyFlavor = useSelector((state: State) => state.configuration.assemblyFlavor); const demangleAssembly = useSelector((state: State) => state.configuration.demangleAssembly); const processAssembly = useSelector((state: State) => state.configuration.processAssembly); const dispatch = useDispatch(); - const changeTheme = useCallback((t) => dispatch(actions.changeTheme(t)), [dispatch]); + const changeAceTheme = useCallback((t) => dispatch(actions.changeAceTheme(t)), [dispatch]); const changeKeybinding = useCallback((k) => dispatch(actions.changeKeybinding(k)), [dispatch]); const changeOrientation = useCallback((o) => dispatch(actions.changeOrientation(o)), [dispatch]); const changeEditorStyle = useCallback((e) => dispatch(actions.changeEditor(e)), [dispatch]); @@ -48,11 +48,11 @@ const ConfigMenu: React.SFC = () => { id="editor-style" name="Style" a={Editor.Simple} - b={Editor.Advanced} + b={Editor.Ace} value={editorStyle} onChange={changeEditorStyle} /> - {editorStyle === Editor.Advanced && ( + {editorStyle === Editor.Ace && ( = () => { {ACE_THEMES.map(t => )} diff --git a/ui/frontend/Editor.module.css b/ui/frontend/Editor.module.css deleted file mode 100644 index 365a1bdfa..000000000 --- a/ui/frontend/Editor.module.css +++ /dev/null @@ -1,14 +0,0 @@ -.container { - composes: -autoSize from './shared.module.css'; - position: relative; -} - -.advanced { - composes: -bodyMonospace -autoSize from './shared.module.css'; - position: absolute; -} - -.simple { - composes: advanced; - border: none; -} diff --git a/ui/frontend/Playground.tsx b/ui/frontend/Playground.tsx index d658c3daa..9cb4daa52 100644 --- a/ui/frontend/Playground.tsx +++ b/ui/frontend/Playground.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useRef } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import Split from 'split-grid'; -import Editor from './Editor'; +import Editor from './editor/Editor'; import Header from './Header'; import Notifications from './Notifications'; import Output from './Output'; diff --git a/ui/frontend/actions.ts b/ui/frontend/actions.ts index d51b5149c..01c012efc 100644 --- a/ui/frontend/actions.ts +++ b/ui/frontend/actions.ts @@ -63,7 +63,7 @@ export enum ActionType { SetPage = 'SET_PAGE', ChangeEditor = 'CHANGE_EDITOR', ChangeKeybinding = 'CHANGE_KEYBINDING', - ChangeTheme = 'CHANGE_THEME', + ChangeAceTheme = 'CHANGE_ACE_THEME', ChangePairCharacters = 'CHANGE_PAIR_CHARACTERS', ChangeOrientation = 'CHANGE_ORIENTATION', ChangeAssemblyFlavor = 'CHANGE_ASSEMBLY_FLAVOR', @@ -138,8 +138,8 @@ export const changeEditor = (editor: Editor) => export const changeKeybinding = (keybinding: string) => createAction(ActionType.ChangeKeybinding, { keybinding }); -export const changeTheme = (theme: string) => - createAction(ActionType.ChangeTheme, { theme }); +export const changeAceTheme = (theme: string) => + createAction(ActionType.ChangeAceTheme, { theme }); export const changePairCharacters = (pairCharacters: PairCharacters) => createAction(ActionType.ChangePairCharacters, { pairCharacters }); @@ -816,7 +816,7 @@ export type Action = | ReturnType | ReturnType | ReturnType - | ReturnType + | ReturnType | ReturnType | ReturnType | ReturnType diff --git a/ui/frontend/AdvancedEditor.tsx b/ui/frontend/editor/AceEditor.tsx similarity index 92% rename from ui/frontend/AdvancedEditor.tsx rename to ui/frontend/editor/AceEditor.tsx index 2de10829c..725f3fa95 100644 --- a/ui/frontend/AdvancedEditor.tsx +++ b/ui/frontend/editor/AceEditor.tsx @@ -1,17 +1,17 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { connect } from 'react-redux'; -import { aceResizeKey, offerCrateAutocompleteOnUse } from './selectors'; +import { aceResizeKey, offerCrateAutocompleteOnUse } from '../selectors'; -import State from './state'; -import { AceResizeKey, CommonEditorProps, Crate, PairCharacters, Position, Selection } from './types'; +import State from '../state'; +import { AceResizeKey, CommonEditorProps, Crate, PairCharacters, Position, Selection } from '../types'; import styles from './Editor.module.css'; type Ace = typeof import('ace-builds'); -type AceEditor = import('ace-builds').Ace.Editor; +type AceModule = import('ace-builds').Ace.Editor; type AceCompleter = import('ace-builds').Ace.Completer; -const displayExternCrateAutocomplete = (editor: AceEditor, autocompleteOnUse: boolean) => { +const displayExternCrateAutocomplete = (editor: AceModule, autocompleteOnUse: boolean) => { const { session } = editor; const pos = editor.getCursorPosition(); const line = session.getLine(pos.row); @@ -55,11 +55,11 @@ function useRafDebouncedFunction(fn: (...args: A) => void, onCa }, [fn, onCall, timeout]); } -interface AdvancedEditorProps extends AdvancedEditorAsyncProps { +interface AceEditorProps extends AceEditorAsyncProps { ace: Ace; } -interface AdvancedEditorProps { +interface AceEditorProps { ace: Ace; autocompleteOnUse: boolean; code: string; @@ -75,7 +75,7 @@ interface AdvancedEditorProps { } // Run an effect when the editor or prop changes -function useEditorProp(editor: AceEditor, prop: T, whenPresent: (editor: AceEditor, prop: T) => void) { +function useEditorProp(editor: AceModule, prop: T, whenPresent: (editor: AceModule, prop: T) => void) { useEffect(() => { if (editor) { return whenPresent(editor, prop); @@ -83,8 +83,8 @@ function useEditorProp(editor: AceEditor, prop: T, whenPresent: (editor: AceE }, [editor, prop, whenPresent]); } -const AdvancedEditor: React.SFC = props => { - const [editor, setEditor] = useState(null); +const AceEditor: React.SFC = props => { + const [editor, setEditor] = useState(null); const child = useRef(null); useEffect(() => { @@ -292,7 +292,7 @@ const AdvancedEditor: React.SFC = props => { }, [])); return ( -
+
); }; @@ -315,7 +315,7 @@ enum LoadState { // // Themes and keybindings can be changed at runtime. -interface AdvancedEditorAsyncProps { +interface AceEditorAsyncProps { autocompleteOnUse: boolean; code: string; execute: () => any; @@ -329,7 +329,7 @@ interface AdvancedEditorAsyncProps { pairCharacters: PairCharacters; } -class AdvancedEditorAsync extends React.Component { +class AceEditorAsync extends React.Component { public constructor(props) { super(props); this.state = { @@ -342,7 +342,7 @@ class AdvancedEditorAsync extends React.Component; + return ; } else { return
Loading the ACE editor...
; } @@ -447,13 +447,13 @@ class AdvancedEditorAsync extends React.Component { - const { configuration: { theme, keybinding, pairCharacters } } = state; + const { configuration: { ace: { theme, keybinding, pairCharacters } } } = state; return { theme, @@ -483,4 +483,4 @@ const mapStateToProps = (state: State) => { }; }; -export default connect(mapStateToProps)(AdvancedEditorAsync); +export default connect(mapStateToProps)(AceEditorAsync); diff --git a/ui/frontend/editor/Editor.module.css b/ui/frontend/editor/Editor.module.css new file mode 100644 index 000000000..add373252 --- /dev/null +++ b/ui/frontend/editor/Editor.module.css @@ -0,0 +1,14 @@ +.container { + composes: -autoSize from '../shared.module.css'; + position: relative; +} + +.ace { + composes: -bodyMonospace -autoSize from '../shared.module.css'; + position: absolute; +} + +.simple { + composes: ace; + border: none; +} diff --git a/ui/frontend/Editor.tsx b/ui/frontend/editor/Editor.tsx similarity index 96% rename from ui/frontend/Editor.tsx rename to ui/frontend/editor/Editor.tsx index 12f4003db..031cdb269 100644 --- a/ui/frontend/Editor.tsx +++ b/ui/frontend/editor/Editor.tsx @@ -1,10 +1,10 @@ import React, { useCallback } from 'react'; import { useSelector, useDispatch } from 'react-redux'; -import * as actions from './actions'; -import AdvancedEditor from './AdvancedEditor'; -import { CommonEditorProps, Editor as EditorType, Position, Selection } from './types'; -import { State } from './reducers'; +import * as actions from '../actions'; +import AceEditor from './AceEditor'; +import { CommonEditorProps, Editor as EditorType, Position, Selection } from '../types'; +import { State } from '../reducers'; import styles from './Editor.module.css'; @@ -118,7 +118,7 @@ const Editor: React.SFC = () => { const execute = useCallback(() => dispatch(actions.performPrimaryAction()), [dispatch]); const onEditCode = useCallback((c) => dispatch(actions.editCode(c)), [dispatch]); - const SelectedEditor = editor === EditorType.Simple ? SimpleEditor : AdvancedEditor; + const SelectedEditor = editor === EditorType.Simple ? SimpleEditor : AceEditor; return (
diff --git a/ui/frontend/advanced-editor.ts b/ui/frontend/editor/ace-editor.ts similarity index 100% rename from ui/frontend/advanced-editor.ts rename to ui/frontend/editor/ace-editor.ts diff --git a/ui/frontend/local_storage.spec.ts b/ui/frontend/local_storage.spec.ts index 1d4d05143..4a1fbfe81 100644 --- a/ui/frontend/local_storage.spec.ts +++ b/ui/frontend/local_storage.spec.ts @@ -20,13 +20,24 @@ describe('restoring saved state', () => { test('serialized data is kept', () => { const parsed = easyDeserialize({ - configuration: { theme: 'xcode' }, + configuration: { orientation: 'vertical' }, code: 'not default code', notifications: { seenRustSurvey2018: true }, }); - expect(parsed.configuration.theme).toEqual('xcode'); + expect(parsed.configuration.orientation).toEqual('vertical'); expect(parsed.code).toEqual('not default code'); expect(parsed.notifications.seenRustSurvey2018).toBe(true); }); + + test('data is migrated', () => { + const parsed = easyDeserialize({ + configuration: { editor: 'advanced', theme: 'xcode', keybinding: 'vi', pairCharacters: 'disabled' }, + }); + + expect(parsed.configuration.editor).toEqual('ace'); + expect(parsed.configuration.ace.theme).toEqual('xcode'); + expect(parsed.configuration.ace.keybinding).toEqual('vi'); + expect(parsed.configuration.ace.pairCharacters).toEqual('disabled'); + }); }); diff --git a/ui/frontend/local_storage.ts b/ui/frontend/local_storage.ts index 6f61a2c89..a95390c3e 100644 --- a/ui/frontend/local_storage.ts +++ b/ui/frontend/local_storage.ts @@ -4,17 +4,56 @@ import State from './state'; import storage from './storage'; +import { AssemblyFlavor, DemangleAssembly, Editor, Orientation, PairCharacters, ProcessAssembly } from './types'; -const CURRENT_VERSION = 1; +const CURRENT_VERSION = 2; -export function serialize(state: State) { - return JSON.stringify({ +interface V2Configuration { + version: 2; + configuration: { + editor: Editor; + ace: { + keybinding: string; + theme: string; + pairCharacters: PairCharacters; + } + orientation: Orientation; + assemblyFlavor: AssemblyFlavor; + demangleAssembly: DemangleAssembly; + processAssembly: ProcessAssembly; + }; + code: string; + notifications: any; +} + +interface V1Configuration { + version: 1; + configuration: { + editor: 'simple' | 'advanced'; + keybinding: string; + theme: string; + pairCharacters: PairCharacters; + orientation: Orientation; + assemblyFlavor: AssemblyFlavor; + demangleAssembly: DemangleAssembly; + processAssembly: ProcessAssembly; + }; + code: string; + notifications: any; +} + +type CurrentConfiguration = V2Configuration; + +export function serialize(state: State): string { + const conf: CurrentConfiguration = { version: CURRENT_VERSION, configuration: { editor: state.configuration.editor, - keybinding: state.configuration.keybinding, - theme: state.configuration.theme, - pairCharacters: state.configuration.pairCharacters, + ace: { + keybinding: state.configuration.ace.keybinding, + theme: state.configuration.ace.theme, + pairCharacters: state.configuration.ace.pairCharacters, + }, orientation: state.configuration.orientation, assemblyFlavor: state.configuration.assemblyFlavor, demangleAssembly: state.configuration.demangleAssembly, @@ -22,20 +61,47 @@ export function serialize(state: State) { }, code: state.code, notifications: state.notifications, - }); + }; + return JSON.stringify(conf); } -export function deserialize(savedState) { +function migrateV1(state: V1Configuration): CurrentConfiguration { + const { editor, theme, keybinding, pairCharacters, ...configuration } = state.configuration; + const step: V2Configuration = { + ...state, + configuration: { + ...configuration, + ace: { theme, keybinding, pairCharacters }, + editor: editor === 'advanced' ? Editor.Ace : Editor.Simple, + }, + version: 2, + }; + return migrateV2(step); +} + +function migrateV2(state: V2Configuration): CurrentConfiguration { + return state; +} + +function migrate(state: V1Configuration | V2Configuration): CurrentConfiguration { + switch (state.version) { + case 1: return migrateV1(state); + case 2: return migrateV2(state); + default: return undefined + } +} + +export function deserialize(savedState: string) { if (!savedState) { return undefined; } + const parsedState = JSON.parse(savedState); if (!parsedState) { return undefined; } - if (parsedState.version !== CURRENT_VERSION) { return undefined; } - // This assumes that the keys we serialize with match the keys in the - // live state. If that's no longer true, an additional renaming step - // needs to be added. - delete parsedState.version; - return parsedState; + const result = migrate(parsedState); + if (!result) { return undefined; } + + delete result.version; + return result; } export default storage({ diff --git a/ui/frontend/reducers/configuration.ts b/ui/frontend/reducers/configuration.ts index f7739adb7..0c6bf55c9 100644 --- a/ui/frontend/reducers/configuration.ts +++ b/ui/frontend/reducers/configuration.ts @@ -16,9 +16,11 @@ import { export interface State { editor: Editor; - keybinding: string; - theme: string; - pairCharacters: PairCharacters; + ace: { + keybinding: string; + theme: string; + pairCharacters: PairCharacters; + }; orientation: Orientation; assemblyFlavor: AssemblyFlavor; demangleAssembly: DemangleAssembly; @@ -31,10 +33,12 @@ export interface State { } const DEFAULT: State = { - editor: Editor.Advanced, - keybinding: 'ace', - theme: 'github', - pairCharacters: PairCharacters.Enabled, + editor: Editor.Ace, + ace: { + keybinding: 'ace', + theme: 'github', + pairCharacters: PairCharacters.Enabled, + }, orientation: Orientation.Automatic, assemblyFlavor: AssemblyFlavor.Att, demangleAssembly: DemangleAssembly.Demangle, @@ -50,12 +54,19 @@ export default function configuration(state = DEFAULT, action: Action): State { switch (action.type) { case ActionType.ChangeEditor: return { ...state, editor: action.editor }; - case ActionType.ChangeKeybinding: - return { ...state, keybinding: action.keybinding }; - case ActionType.ChangeTheme: - return { ...state, theme: action.theme }; - case ActionType.ChangePairCharacters: - return { ...state, pairCharacters: action.pairCharacters }; + case ActionType.ChangeKeybinding: { + const { ace } = state; + + return { ...state, ace: { ...ace, keybinding: action.keybinding } }; + } + case ActionType.ChangeAceTheme: { + const { ace } = state; + return { ...state, ace: { ...ace, theme: action.theme } }; + } + case ActionType.ChangePairCharacters: { + const { ace } = state; + return { ...state, ace: { ...ace, pairCharacters: action.pairCharacters } }; + } case ActionType.ChangeOrientation: return { ...state, orientation: action.orientation }; case ActionType.ChangeAssemblyFlavor: diff --git a/ui/frontend/types.ts b/ui/frontend/types.ts index 2ad437c62..07e6fcff2 100644 --- a/ui/frontend/types.ts +++ b/ui/frontend/types.ts @@ -36,7 +36,7 @@ export interface CommonEditorProps { export enum Editor { Simple = 'simple', - Advanced = 'advanced', + Ace = 'ace', } export enum PairCharacters { From 60659f11d5b5c85c7e1defd135a2bc1a1bedb277 Mon Sep 17 00:00:00 2001 From: hamidreza kalbasi Date: Sun, 22 Aug 2021 02:21:05 +0430 Subject: [PATCH 2/4] Add the Monaco editor --- tests/spec/features/editor_types_spec.rb | 2 +- ui/frontend/ConfigMenu.tsx | 31 ++++- ui/frontend/actions.ts | 5 + ui/frontend/editor/Editor.module.css | 12 +- ui/frontend/editor/Editor.tsx | 9 +- ui/frontend/editor/MonacoEditor.tsx | 13 ++ ui/frontend/editor/MonacoEditorCore.tsx | 41 ++++++ ui/frontend/editor/rust_monaco_def.ts | 163 +++++++++++++++++++++++ ui/frontend/package.json | 3 + ui/frontend/reducers/configuration.ts | 10 ++ ui/frontend/types.ts | 1 + ui/frontend/webpack.config.js | 14 ++ ui/frontend/yarn.lock | 28 ++++ 13 files changed, 321 insertions(+), 11 deletions(-) create mode 100644 ui/frontend/editor/MonacoEditor.tsx create mode 100644 ui/frontend/editor/MonacoEditorCore.tsx create mode 100644 ui/frontend/editor/rust_monaco_def.ts diff --git a/tests/spec/features/editor_types_spec.rb b/tests/spec/features/editor_types_spec.rb index 9503dca90..c4fcbbcbc 100644 --- a/tests/spec/features/editor_types_spec.rb +++ b/tests/spec/features/editor_types_spec.rb @@ -7,7 +7,7 @@ before { visit '/' } scenario "using the simple editor" do - in_config_menu { choose("simple") } + in_config_menu { select("simple") } fill_in('editor-simple', with: simple_editor_code) diff --git a/ui/frontend/ConfigMenu.tsx b/ui/frontend/ConfigMenu.tsx index f5b864645..6573ecbe3 100644 --- a/ui/frontend/ConfigMenu.tsx +++ b/ui/frontend/ConfigMenu.tsx @@ -21,9 +21,14 @@ interface ConfigMenuProps { close: () => void; } +const MONACO_THEMES = [ + 'vs', 'vs-dark', 'vscode-dark-plus', +]; + const ConfigMenu: React.SFC = () => { const keybinding = useSelector((state: State) => state.configuration.ace.keybinding); const aceTheme = useSelector((state: State) => state.configuration.ace.theme); + const monacoTheme = useSelector((state: State) => state.configuration.monaco.theme); const orientation = useSelector((state: State) => state.configuration.orientation); const editorStyle = useSelector((state: State) => state.configuration.editor); const pairCharacters = useSelector((state: State) => state.configuration.ace.pairCharacters); @@ -33,6 +38,7 @@ const ConfigMenu: React.SFC = () => { const dispatch = useDispatch(); const changeAceTheme = useCallback((t) => dispatch(actions.changeAceTheme(t)), [dispatch]); + const changeMonacoTheme = useCallback((t) => dispatch(actions.changeMonacoTheme(t)), [dispatch]); const changeKeybinding = useCallback((k) => dispatch(actions.changeKeybinding(k)), [dispatch]); const changeOrientation = useCallback((o) => dispatch(actions.changeOrientation(o)), [dispatch]); const changeEditorStyle = useCallback((e) => dispatch(actions.changeEditor(e)), [dispatch]); @@ -44,14 +50,14 @@ const ConfigMenu: React.SFC = () => { return ( - - + onChange={changeEditorStyle} + > + {[Editor.Simple, Editor.Ace, Editor.Monaco] + .map(k => )} + {editorStyle === Editor.Ace && ( = () => { onChange={changePairCharacters} /> )} + {editorStyle === Editor.Monaco && ( + + + {MONACO_THEMES.map(t => )} + + + )} diff --git a/ui/frontend/actions.ts b/ui/frontend/actions.ts index 01c012efc..1c6c21933 100644 --- a/ui/frontend/actions.ts +++ b/ui/frontend/actions.ts @@ -64,6 +64,7 @@ export enum ActionType { ChangeEditor = 'CHANGE_EDITOR', ChangeKeybinding = 'CHANGE_KEYBINDING', ChangeAceTheme = 'CHANGE_ACE_THEME', + ChangeMonacoTheme = 'CHANGE_MONACO_THEME', ChangePairCharacters = 'CHANGE_PAIR_CHARACTERS', ChangeOrientation = 'CHANGE_ORIENTATION', ChangeAssemblyFlavor = 'CHANGE_ASSEMBLY_FLAVOR', @@ -141,6 +142,9 @@ export const changeKeybinding = (keybinding: string) => export const changeAceTheme = (theme: string) => createAction(ActionType.ChangeAceTheme, { theme }); +export const changeMonacoTheme = (theme: string) => + createAction(ActionType.ChangeMonacoTheme, { theme }); + export const changePairCharacters = (pairCharacters: PairCharacters) => createAction(ActionType.ChangePairCharacters, { pairCharacters }); @@ -817,6 +821,7 @@ export type Action = | ReturnType | ReturnType | ReturnType + | ReturnType | ReturnType | ReturnType | ReturnType diff --git a/ui/frontend/editor/Editor.module.css b/ui/frontend/editor/Editor.module.css index add373252..d80715272 100644 --- a/ui/frontend/editor/Editor.module.css +++ b/ui/frontend/editor/Editor.module.css @@ -3,12 +3,20 @@ position: relative; } -.ace { +.-advanced { composes: -bodyMonospace -autoSize from '../shared.module.css'; position: absolute; } +.ace { + composes: -advanced; +} + +.monaco { + composes: -advanced; +} + .simple { - composes: ace; + composes: -advanced; border: none; } diff --git a/ui/frontend/editor/Editor.tsx b/ui/frontend/editor/Editor.tsx index 031cdb269..ca0ae92b8 100644 --- a/ui/frontend/editor/Editor.tsx +++ b/ui/frontend/editor/Editor.tsx @@ -7,6 +7,7 @@ import { CommonEditorProps, Editor as EditorType, Position, Selection } from '.. import { State } from '../reducers'; import styles from './Editor.module.css'; +import MonacoEditor from './MonacoEditor'; class CodeByteOffsets { readonly code: string; @@ -107,6 +108,12 @@ class SimpleEditor extends React.PureComponent { } } +const editorMap = { + [EditorType.Simple]: SimpleEditor, + [EditorType.Ace]: AceEditor, + [EditorType.Monaco]: MonacoEditor, +}; + const Editor: React.SFC = () => { const code = useSelector((state: State) => state.code); const editor = useSelector((state: State) => state.configuration.editor); @@ -118,7 +125,7 @@ const Editor: React.SFC = () => { const execute = useCallback(() => dispatch(actions.performPrimaryAction()), [dispatch]); const onEditCode = useCallback((c) => dispatch(actions.editCode(c)), [dispatch]); - const SelectedEditor = editor === EditorType.Simple ? SimpleEditor : AceEditor; + const SelectedEditor = editorMap[editor]; return (
diff --git a/ui/frontend/editor/MonacoEditor.tsx b/ui/frontend/editor/MonacoEditor.tsx new file mode 100644 index 000000000..94a281ade --- /dev/null +++ b/ui/frontend/editor/MonacoEditor.tsx @@ -0,0 +1,13 @@ +import React, { Suspense } from 'react'; + +import { CommonEditorProps } from '../types'; + +const MonacoEditorLazy = React.lazy(() => import('./MonacoEditorCore')); + +const MonacoEditor: React.SFC = props => ( + + + +) + +export default MonacoEditor; diff --git a/ui/frontend/editor/MonacoEditorCore.tsx b/ui/frontend/editor/MonacoEditorCore.tsx new file mode 100644 index 000000000..f855b6cb8 --- /dev/null +++ b/ui/frontend/editor/MonacoEditorCore.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { CommonEditorProps } from '../types'; +import MonacoEditor, { EditorWillMount } from 'react-monaco-editor'; +import { useSelector } from 'react-redux'; +import State from '../state'; +import { config, grammar, themeVsDarkPlus } from './rust_monaco_def'; + +import styles from './Editor.module.css'; + +const MODE_ID = 'my-rust'; + +const initMonaco: EditorWillMount = (monaco) => { + monaco.editor.defineTheme('vscode-dark-plus', themeVsDarkPlus); + + monaco.languages.register({ + id: MODE_ID, + }); + + monaco.languages.onLanguage(MODE_ID, async () => { + monaco.languages.setLanguageConfiguration(MODE_ID, config); + monaco.languages.setMonarchTokensProvider(MODE_ID, grammar); + }); +}; + +const MonacoEditorCore: React.SFC = props => { + const theme = useSelector((s: State) => s.configuration.monaco.theme); + + return ( + + ); +} + +export default MonacoEditorCore; diff --git a/ui/frontend/editor/rust_monaco_def.ts b/ui/frontend/editor/rust_monaco_def.ts new file mode 100644 index 000000000..8f7c2e727 --- /dev/null +++ b/ui/frontend/editor/rust_monaco_def.ts @@ -0,0 +1,163 @@ +import { languages, editor } from 'monaco-editor'; + +export const config: languages.LanguageConfiguration = { + comments: { + lineComment: '//', + blockComment: ['/*', '*/'], + }, + brackets: [ + ['{', '}'], + ['[', ']'], + ['(', ')'], + ], + autoClosingPairs: [ + { open: '[', close: ']' }, + { open: '{', close: '}' }, + { open: '(', close: ')' }, + { open: '"', close: '"', notIn: ['string'] }, + ], + surroundingPairs: [ + { open: '{', close: '}' }, + { open: '[', close: ']' }, + { open: '(', close: ')' }, + { open: '"', close: '"' }, + { open: '\'', close: '\'' }, + ], + folding: { + markers: { + start: new RegExp('^\\s*#pragma\\s+region\\b'), + end: new RegExp('^\\s*#pragma\\s+endregion\\b'), + }, + }, +}; + +export const grammar: languages.IMonarchLanguage = { + // Set defaultToken to invalid to see what you do not tokenize yet + // defaultToken: 'invalid', + + keywords: [ + 'as', 'break', 'const', 'crate', 'enum', 'extern', 'false', 'fn', 'impl', 'in', + 'let', 'mod', 'move', 'mut', 'pub', 'ref', 'return', 'self', 'Self', 'static', + 'struct', 'super', 'trait', 'true', 'type', 'unsafe', 'use', 'where', + 'macro_rules', + ], + + controlFlowKeywords: [ + 'continue', 'else', 'for', 'if', 'while', 'loop', 'match', + ], + + typeKeywords: [ + 'Self', 'm32', 'm64', 'm128', 'f80', 'f16', 'f128', 'int', 'uint', 'float', 'char', + 'bool', 'u8', 'u16', 'u32', 'u64', 'f32', 'f64', 'i8', 'i16', 'i32', 'i64', 'str', + 'Option', 'Either', 'c_float', 'c_double', 'c_void', 'FILE', 'fpos_t', 'DIR', 'dirent', + 'c_char', 'c_schar', 'c_uchar', 'c_short', 'c_ushort', 'c_int', 'c_uint', 'c_long', 'c_ulong', + 'size_t', 'ptrdiff_t', 'clock_t', 'time_t', 'c_longlong', 'c_ulonglong', 'intptr_t', + 'uintptr_t', 'off_t', 'dev_t', 'ino_t', 'pid_t', 'mode_t', 'ssize_t', + ], + + operators: [ + '=', '>', '<', '!', '~', '?', ':', '==', '<=', '>=', '!=', + '&&', '||', '++', '--', '+', '-', '*', '/', '&', '|', '^', '%', + '<<', '>>', '>>>', '+=', '-=', '*=', '/=', '&=', '|=', '^=', + '%=', '<<=', '>>=', '>>>=', + ], + + // we include these common regular expressions + symbols: /[=>](?!@symbols)/, '@brackets'], + [/@symbols/, { + cases: { + '@operators': 'operator', + '@default': '', + }, + }], + + // @ annotations. + // As an example, we emit a debugging log message on these tokens. + // Note: message are supressed during the first load -- change some lines to see them. + [/@\s*[a-zA-Z_\$][\w\$]*/, { token: 'annotation', log: 'annotation token: $0' }], + + // numbers + [/\d*\.\d+([eE][\-+]?\d+)?/, 'number.float'], + [/0[xX][0-9a-fA-F]+/, 'number.hex'], + [/\d+/, 'number'], + + // delimiter: after number because of .\d floats + [/[;,.]/, 'delimiter'], + + // strings + [/"([^"\\]|\\.)*$/, 'string.invalid'], // non-teminated string + [/"/, { token: 'string.quote', bracket: '@open', next: '@string' }], + + // characters + [/'[^\\']'/, 'string'], + [/(')(@escapes)(')/, ['string', 'string.escape', 'string']], + [/'/, 'string.invalid'], + ], + + comment: [ + [/[^\/*]+/, 'comment'], + [/\/\*/, 'comment', '@push'], // nested comment + ['\\*/', 'comment', '@pop'], + [/[\/*]/, 'comment'], + ], + + string: [ + [/[^\\"]+/, 'string'], + [/@escapes/, 'string.escape'], + [/\\./, 'string.escape.invalid'], + [/"/, { token: 'string.quote', bracket: '@close', next: '@pop' }], + ], + + whitespace: [ + [/[ \t\r\n]+/, 'white'], + [/\/\*/, 'comment', '@comment'], + [/\/\/.*$/, 'comment'], + ], + + func_decl: [ + [ + /[a-z_$][\w$]*/, 'support.function', '@pop', + ], + ], + }, +}; + +export const themeVsDarkPlus: editor.IStandaloneThemeData = { + base: 'vs-dark', + inherit: true, + colors: {}, + rules: [ + { token: 'keyword.control', foreground: 'C586C0' }, + { token: 'variable', foreground: '9CDCFE' }, + { token: 'support.function', foreground: 'DCDCAA' }, + ], +}; diff --git a/ui/frontend/package.json b/ui/frontend/package.json index 0f37248ea..a410764f9 100644 --- a/ui/frontend/package.json +++ b/ui/frontend/package.json @@ -11,11 +11,13 @@ "history": "^4.6.0", "isomorphic-fetch": "^3.0.0", "lodash": "^4.17.0", + "monaco-editor": "^0.31.1", "prismjs": "^1.6.0", "qs": "^6.4.0", "react": "^17.0.1", "react-copy-to-clipboard": "^5.0.1", "react-dom": "^17.0.1", + "react-monaco-editor": "^0.46.0", "react-popper": "^2.0.0", "react-portal": "^4.1.4", "react-prism": "^4.0.0", @@ -58,6 +60,7 @@ "jest": "^27.0.0", "json-loader": "^0.5.4", "mini-css-extract-plugin": "^2.0.0", + "monaco-editor-webpack-plugin": "^7.0.1", "normalize.css": "^8.0.0", "postcss": "^8.2.7", "postcss-loader": "^6.1.0", diff --git a/ui/frontend/reducers/configuration.ts b/ui/frontend/reducers/configuration.ts index 0c6bf55c9..26fe754ca 100644 --- a/ui/frontend/reducers/configuration.ts +++ b/ui/frontend/reducers/configuration.ts @@ -21,6 +21,9 @@ export interface State { theme: string; pairCharacters: PairCharacters; }; + monaco: { + theme: string; + }; orientation: Orientation; assemblyFlavor: AssemblyFlavor; demangleAssembly: DemangleAssembly; @@ -39,6 +42,9 @@ const DEFAULT: State = { theme: 'github', pairCharacters: PairCharacters.Enabled, }, + monaco: { + theme: 'vscode-dark-plus', + }, orientation: Orientation.Automatic, assemblyFlavor: AssemblyFlavor.Att, demangleAssembly: DemangleAssembly.Demangle, @@ -67,6 +73,10 @@ export default function configuration(state = DEFAULT, action: Action): State { const { ace } = state; return { ...state, ace: { ...ace, pairCharacters: action.pairCharacters } }; } + case ActionType.ChangeMonacoTheme: { + const { monaco } = state; + return { ...state, monaco: { ...monaco, theme: action.theme } }; + } case ActionType.ChangeOrientation: return { ...state, orientation: action.orientation }; case ActionType.ChangeAssemblyFlavor: diff --git a/ui/frontend/types.ts b/ui/frontend/types.ts index 07e6fcff2..ef232f1a8 100644 --- a/ui/frontend/types.ts +++ b/ui/frontend/types.ts @@ -37,6 +37,7 @@ export interface CommonEditorProps { export enum Editor { Simple = 'simple', Ace = 'ace', + Monaco = 'monaco', } export enum PairCharacters { diff --git a/ui/frontend/webpack.config.js b/ui/frontend/webpack.config.js index 2d6318b92..494eff9ac 100644 --- a/ui/frontend/webpack.config.js +++ b/ui/frontend/webpack.config.js @@ -5,6 +5,7 @@ const HtmlPlugin = require('html-webpack-plugin'); const CopyPlugin = require('copy-webpack-plugin'); const MiniCssExtractPlugin = require("mini-css-extract-plugin"); const CompressionPlugin = require("compression-webpack-plugin"); +const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin'); const glob = require('glob'); const basename = require('basename'); @@ -120,6 +121,15 @@ module.exports = function(_, argv) { }, ] }, + // This inlines the codicon.ttf file from Monaco. Using a + // regular file fails because it looks for + // `/assets/assets/...`. Inlining saves a file, and it's + // pretty small compared to the rest of Monaco. + { + test: /\.ttf$/, + include: /node_modules\/monaco-editor/, + type: 'asset/inline', + }, { test: /\.svg$/, type: 'asset/inline', @@ -146,6 +156,10 @@ module.exports = function(_, argv) { filename: `${filenameTemplate}.css`, chunkFilename: `${chunkFilenameTemplate}.css`, }), + new MonacoWebpackPlugin({ + filename: `${filenameTemplate}.worker.js`, + languages: ['rust'], + }), ...(isProduction ? [new CompressionPlugin()] : []), ], diff --git a/ui/frontend/yarn.lock b/ui/frontend/yarn.lock index 3347b2b12..42667ac92 100644 --- a/ui/frontend/yarn.lock +++ b/ui/frontend/yarn.lock @@ -4336,6 +4336,15 @@ loader-utils@^1.4.0: emojis-list "^3.0.0" json5 "^1.0.1" +loader-utils@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.2.tgz#d6e3b4fb81870721ae4e0868ab11dd638368c129" + integrity sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A== + dependencies: + big.js "^5.2.2" + emojis-list "^3.0.0" + json5 "^2.1.2" + locate-path@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" @@ -4537,6 +4546,18 @@ mkdirp@~1.0.4: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== +monaco-editor-webpack-plugin@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/monaco-editor-webpack-plugin/-/monaco-editor-webpack-plugin-7.0.1.tgz#ba19c60aba990184e36ad8722b1ed6a564527c7c" + integrity sha512-M8qIqizltrPlIbrb73cZdTWfU9sIsUVFvAZkL3KGjAHmVWEJ0hZKa/uad14JuOckc0GwnCaoGHvMoYtJjVyCzw== + dependencies: + loader-utils "^2.0.2" + +monaco-editor@^0.31.1: + version "0.31.1" + resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.31.1.tgz#67f597b3e45679d1f551237e12a3a42c4438b97b" + integrity sha512-FYPwxGZAeP6mRRyrr5XTGHD9gRXVjy7GUzF4IPChnyt3fS5WrNxIkS8DNujWf6EQy0Zlzpxw8oTVE+mWI2/D1Q== + ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -5182,6 +5203,13 @@ react-is@^17.0.1, react-is@^17.0.2: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== +react-monaco-editor@^0.46.0: + version "0.46.0" + resolved "https://registry.yarnpkg.com/react-monaco-editor/-/react-monaco-editor-0.46.0.tgz#ac97d5429cd8821d466f0e8e0536ea2a90bbc6d0" + integrity sha512-/GyQ0tQLbjHAuMUNRfKecBYN68o8TwA4fnwH9P+lHbF80ayMAo0PQ60joTQH6R6j839kMn6o9Kk/cbzOxK5DzA== + dependencies: + prop-types "^15.7.2" + react-popper@^2.0.0: version "2.2.5" resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-2.2.5.tgz#1214ef3cec86330a171671a4fbcbeeb65ee58e96" From 6a20d89ac1caf4c0a0fb924095d92321dd1c4e94 Mon Sep 17 00:00:00 2001 From: Jake Goulding Date: Sat, 20 Nov 2021 21:41:57 -0500 Subject: [PATCH 3/4] Add basic test that Monaco is hooked up --- tests/spec/features/editor_types_spec.rb | 25 ++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/spec/features/editor_types_spec.rb b/tests/spec/features/editor_types_spec.rb index c4fcbbcbc..2a8a5e9c9 100644 --- a/tests/spec/features/editor_types_spec.rb +++ b/tests/spec/features/editor_types_spec.rb @@ -25,4 +25,29 @@ def simple_editor_code } EOF end + + scenario "using the Monaco editor" do + in_config_menu { select("monaco") } + + editor = page.find('.monaco-editor') + + # Click on the last line as that will replace the entire content + editor.find('.view-line:last-child').click + t = editor.find('textarea', visible: false) + t.set(monaco_editor_code, clear: :backspace) + + click_on("Run") + + within(:output, :stdout) do + expect(page).to have_content 'Monaco editor' + end + end + + # Missing indentation and closing curly braces as those are auto-inserted + def monaco_editor_code + <<~EOF + fn main() { + println!("Using the Monaco editor"); + EOF + end end From 1b8120b2146f3cd18dedcbd5196a94a911821dbb Mon Sep 17 00:00:00 2001 From: Jake Goulding Date: Sat, 8 Jan 2022 15:03:37 -0500 Subject: [PATCH 4/4] Add notification about Monaco --- ui/frontend/Notifications.tsx | 40 +++++++-------------------- ui/frontend/actions.ts | 3 +- ui/frontend/reducers/notifications.ts | 17 ++++++------ ui/frontend/selectors/index.ts | 18 ++++-------- ui/frontend/types.ts | 3 +- 5 files changed, 25 insertions(+), 56 deletions(-) diff --git a/ui/frontend/Notifications.tsx b/ui/frontend/Notifications.tsx index bcb8540ec..89e8f16b8 100644 --- a/ui/frontend/Notifications.tsx +++ b/ui/frontend/Notifications.tsx @@ -9,49 +9,29 @@ import * as selectors from './selectors'; import styles from './Notifications.module.css'; -const EDITION_URL = 'https://doc.rust-lang.org/edition-guide/'; -const SURVEY_URL = 'https://blog.rust-lang.org/2021/12/08/survey-launch.html'; +const MONACO_EDITOR_URL = 'https://microsoft.github.io/monaco-editor/'; const Notifications: React.SFC = () => { return (
- - +
); }; -const Rust2021IsDefaultNotification: React.SFC = () => { - const showRust2021IsDefault = useSelector(selectors.showRust2021IsDefaultSelector); +const MonacoEditorAvailableNotification: React.SFC = () => { + const monicoEditorAvailable = useSelector(selectors.showMonicoEditorAvailableSelector); const dispatch = useDispatch(); - const seenRust2021IsDefault = useCallback(() => dispatch(actions.seenRust2021IsDefault()), [dispatch]); + const seenMonicoEditorAvailable = useCallback(() => dispatch(actions.seenMonicoEditorAvailable()), [dispatch]); - return showRust2021IsDefault && ( - - As of Rust 1.56, the default edition of Rust is now Rust - 2021. Learn more about editions in the
Edition Guide. - To specify which edition to use, use the advanced compilation options menu. - - ); -}; - - -const RustSurvey2021Notification: React.SFC = () => { - const showRustSurvey2021 = useSelector(selectors.showRustSurvey2021Selector); - - const dispatch = useDispatch(); - const seenRustSurvey2021 = useCallback(() => dispatch(actions.seenRustSurvey2021()), [dispatch]); - - return showRustSurvey2021 && ( - - Please help us take a look at who the Rust community is - composed of, how the Rust project is doing, and how we can - improve the Rust programming experience by completing the 2021 State of Rust Survey. Whether or - not you use Rust today, we want to know your opinions. + return monicoEditorAvailable && ( + + The Monaco Editor, the code editor + that powers VS Code, is now available in the playground. Choose + your preferred editor from the Config menu. ); }; diff --git a/ui/frontend/actions.ts b/ui/frontend/actions.ts index 1c6c21933..0cc7b3346 100644 --- a/ui/frontend/actions.ts +++ b/ui/frontend/actions.ts @@ -705,8 +705,7 @@ export function performVersionsLoad(): ThunkAction { const notificationSeen = (notification: Notification) => createAction(ActionType.NotificationSeen, { notification }); -export const seenRust2021IsDefault = () => notificationSeen(Notification.Rust2021IsDefault); -export const seenRustSurvey2021 = () => notificationSeen(Notification.RustSurvey2021); +export const seenMonicoEditorAvailable = () => notificationSeen(Notification.MonacoEditorAvailable); export const browserWidthChanged = (isSmall: boolean) => createAction(ActionType.BrowserWidthChanged, { isSmall }); diff --git a/ui/frontend/reducers/notifications.ts b/ui/frontend/reducers/notifications.ts index a1e2afd74..7261dd2a4 100644 --- a/ui/frontend/reducers/notifications.ts +++ b/ui/frontend/reducers/notifications.ts @@ -5,27 +5,26 @@ interface State { seenRustSurvey2018: boolean; // expired seenRust2018IsDefault: boolean; // expired seenRustSurvey2020: boolean; // expired - seenRust2021IsDefault: boolean; - seenRustSurvey2021: boolean; + seenRust2021IsDefault: boolean; // expired + seenRustSurvey2021: boolean; // expired + seenMonacoEditorAvailable: boolean; } const DEFAULT: State = { seenRustSurvey2018: true, seenRust2018IsDefault: true, seenRustSurvey2020: true, - seenRust2021IsDefault: false, - seenRustSurvey2021: false, + seenRust2021IsDefault: true, + seenRustSurvey2021: true, + seenMonacoEditorAvailable: false, }; export default function notifications(state = DEFAULT, action: Action): State { switch (action.type) { case ActionType.NotificationSeen: { switch (action.notification) { - case Notification.Rust2021IsDefault: { - return { ...state, seenRust2021IsDefault: true }; - } - case Notification.RustSurvey2021: { - return { ...state, seenRustSurvey2021: true }; + case Notification.MonacoEditorAvailable: { + return { ...state, seenMonacoEditorAvailable: true }; } } } diff --git a/ui/frontend/selectors/index.ts b/ui/frontend/selectors/index.ts index da9b1e0e2..ab0b1d046 100644 --- a/ui/frontend/selectors/index.ts +++ b/ui/frontend/selectors/index.ts @@ -250,23 +250,15 @@ const notificationsSelector = (state: State) => state.notifications; const NOW = new Date(); -const RUST_2021_DEFAULT_END = new Date('2022-01-01T00:00:00Z'); -const RUST_2021_DEFAULT_OPEN = NOW <= RUST_2021_DEFAULT_END; -export const showRust2021IsDefaultSelector = createSelector( +const MONACO_EDITOR_AVAILABLE_END = new Date('2022-02-15T00:00:00Z'); +const MONACO_EDITOR_AVAILABLE_OPEN = NOW <= MONACO_EDITOR_AVAILABLE_END; +export const showMonicoEditorAvailableSelector = createSelector( notificationsSelector, - notifications => RUST_2021_DEFAULT_OPEN && !notifications.seenRust2021IsDefault, -); - -const RUST_SURVEY_2021_END = new Date('2021-12-22T00:00:00Z'); -const RUST_SURVEY_2021_OPEN = NOW <= RUST_SURVEY_2021_END; -export const showRustSurvey2021Selector = createSelector( - notificationsSelector, - notifications => RUST_SURVEY_2021_OPEN && !notifications.seenRustSurvey2021, + notifications => MONACO_EDITOR_AVAILABLE_OPEN && !notifications.seenMonacoEditorAvailable, ); export const anyNotificationsToShowSelector = createSelector( - showRust2021IsDefaultSelector, - showRustSurvey2021Selector, + showMonicoEditorAvailableSelector, (...allNotifications) => allNotifications.some(n => n), ); diff --git a/ui/frontend/types.ts b/ui/frontend/types.ts index ef232f1a8..64d8515a8 100644 --- a/ui/frontend/types.ts +++ b/ui/frontend/types.ts @@ -120,8 +120,7 @@ export enum Focus { } export enum Notification { - Rust2021IsDefault = 'rust-2021-is-default', - RustSurvey2021 = 'rust-survey-2021', + MonacoEditorAvailable = 'monaco-editor-available', } export type AceResizeKey = [Focus, number];