text: import React, { Component, Suspense } from 'react';
  import PropTypes from 'prop-types';
  import { connect } from 'react-redux';
  import { createSelector } from 'reselect';
  
  import {
    canFocusEditorSelector,
    executeChallenge,
    inAccessibilityModeSelector,
    saveEditorContent,
    setEditorFocusability,
    setAccessibilityMode,
    updateFile
  } from '../redux';
  import { userSelector, isDonationModalOpenSelector } from '../../../redux';
  import { Loader } from '../../../components/helpers';
  
  const MonacoEditor = React.lazy(() => import('react-monaco-editor'));
  
  const propTypes = {
    canFocus: PropTypes.bool,
    containerRef: PropTypes.any.isRequired,
    contents: PropTypes.string,
    dimensions: PropTypes.object,
    executeChallenge: PropTypes.func.isRequired,
    ext: PropTypes.string,
    fileKey: PropTypes.string,
    inAccessibilityMode: PropTypes.bool.isRequired,
    saveEditorContent: PropTypes.func.isRequired,
    setAccessibilityMode: PropTypes.func.isRequired,
    setEditorFocusability: PropTypes.func,
    theme: PropTypes.string,
    updateFile: PropTypes.func.isRequired
  };
  
  const mapStateToProps = createSelector(
    canFocusEditorSelector,
    inAccessibilityModeSelector,
    isDonationModalOpenSelector,
    userSelector,
    (canFocus, accessibilityMode, open, { theme = 'default' }) => ({
      canFocus: open ? false : canFocus,
      inAccessibilityMode: accessibilityMode,
      theme
    })
  );
  
  const mapDispatchToProps = {
    executeChallenge,
    saveEditorContent,
    setAccessibilityMode,
    setEditorFocusability,
    updateFile
  };
  
  const modeMap = {
    css: 'css',
    html: 'html',
    js: 'javascript',
    jsx: 'javascript'
  };
  
  var monacoThemesDefined = false;
  const defineMonacoThemes = monaco => {
    if (monacoThemesDefined) {
      return;
    }
    monacoThemesDefined = true;
    const yellowColor = 'FFFF00';
    const lightBlueColor = '9CDCFE';
    const darkBlueColor = '00107E';
    monaco.editor.defineTheme('vs-dark-custom', {
      base: 'vs-dark',
      inherit: true,
      colors: {
        'editor.background': '#2a2a40'
      },
      rules: [
        { token: 'delimiter.js', foreground: lightBlueColor },
        { token: 'delimiter.parenthesis.js', foreground: yellowColor },
        { token: 'delimiter.array.js', foreground: yellowColor },
        { token: 'delimiter.bracket.js', foreground: yellowColor }
      ]
    });
    monaco.editor.defineTheme('vs-custom', {
      base: 'vs',
      inherit: true,
      rules: [{ token: 'identifier.js', foreground: darkBlueColor }]
    });
  };
  
  class Editor extends Component {
    constructor(...props) {
      super(...props);
  
      this.options = {
        fontSize: '18px',
        scrollBeyondLastLine: false,
        selectionHighlight: false,
        overviewRulerBorder: false,
        hideCursorInOverviewRuler: true,
        renderIndentGuides: false,
        minimap: {
          enabled: false
        },
        selectOnLineNumbers: true,
        wordWrap: 'on',
        scrollbar: {
          horizontal: 'hidden',
          vertical: 'visible',
          verticalHasArrows: false,
          useShadows: false,
          verticalScrollbarSize: 5
        },
        parameterHints: {
          enabled: false
        }
      };
  
      this._editor = null;
      this.focusOnEditor = this.focusOnEditor.bind(this);
    }
  
    editorWillMount = monaco => {
      defineMonacoThemes(monaco);
    };
  
    editorDidMount = (editor, monaco) => {
      this._editor = editor;
      this._editor.updateOptions({
        accessibilitySupport: this.props.inAccessibilityMode ? 'on' : 'auto'
      });
      // Users who are using screen readers should not have to move focus from
      // the editor to the description every time they open a challenge.
      if (this.props.canFocus && !this.props.inAccessibilityMode) {
        this._editor.focus();
      } else this.focusOnHotkeys();
      this._editor.addAction({
        id: 'execute-challenge',
        label: 'Run tests',
        keybindings: [
          /* eslint-disable no-bitwise */
          monaco.KeyMod.chord(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter)
        ],
        run: this.props.executeChallenge
      });
      this._editor.addAction({
        id: 'leave-editor',
        label: 'Leave editor',
        keybindings: [monaco.KeyCode.Escape],
        run: () => {
          this.focusOnHotkeys();
          this.props.setEditorFocusability(false);
        }
      });
      this._editor.addAction({
        id: 'save-editor-content',
        label: 'Save editor content to localStorage',
        keybindings: [
          monaco.KeyMod.chord(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KEY_S)
        ],
        run: this.props.saveEditorContent
      });
      this._editor.addAction({
        id: 'toggle-accessibility',
        label: 'Toggle Accessibility Mode',
        keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.F1],
        run: () => {
          const currentAccessibility = this.props.inAccessibilityMode;
          // The store needs to be updated first, as onDidChangeConfiguration is
          // called before updateOptions returns
          this.props.setAccessibilityMode(!currentAccessibility);
          this._editor.updateOptions({
            accessibilitySupport: currentAccessibility ? 'auto' : 'on'
          });
        }
      });
      this._editor.onDidFocusEditorWidget(() =>
        this.props.setEditorFocusability(true)
      );
      // This is to persist changes caused by the accessibility tooltip.
      // Unfortunately it relies on Monaco's implementation details
      this._editor.onDidChangeConfiguration(() => {
        if (
          this._editor.getConfiguration().accessibilitySupport === 2 &&
          !this.props.inAccessibilityMode
        ) {
          this.props.setAccessibilityMode(true);
        }
      });
    };
  
    focusOnHotkeys() {
      if (this.props.containerRef.current) {
        this.props.containerRef.current.focus();
      }
    }
  
    focusOnEditor() {
      this._editor.focus();
    }
  
    onChange = editorValue => {
      const { updateFile, fileKey } = this.props;
      updateFile({ key: fileKey, editorValue });
    };
  
    componentDidUpdate(prevProps) {
      if (this.props.dimensions !== prevProps.dimensions && this._editor) {
        this._editor.layout();
      }
    }
  
    render() {
      const { contents, ext, theme, fileKey } = this.props;
      const editorTheme = theme === 'night' ? 'vs-dark-custom' : 'vs-custom';
      return (
        <Suspense fallback={<Loader timeout={600} />}>
          <MonacoEditor
            editorDidMount={this.editorDidMount}
            editorWillMount={this.editorWillMount}
            key={`${editorTheme}-${fileKey}`}
            language={modeMap[ext]}
            onChange={this.onChange}
            options={this.options}
            theme={editorTheme}
            value={contents}
          />
        </Suspense>
      );
    }
  }
  
  Editor.displayName = 'Editor';
  Editor.propTypes = propTypes;
  
  // NOTE: withRef gets replaced by forwardRef in react-redux 6,
  // https://github.com/reduxjs/react-redux/releases/tag/v6.0.0
  export default connect(
    mapStateToProps,
    mapDispatchToProps,
    null,
    { withRef: true }
  )(Editor);
  