/ embark-ui / src / components / TextEditor.js
TextEditor.js
  1  import React from 'react';
  2  import * as monaco from 'monaco-editor';
  3  import PropTypes from 'prop-types';
  4  import FontAwesomeIcon from 'react-fontawesome';
  5  import classNames from 'classnames';
  6  
  7  import {DARK_THEME, LIGHT_THEME} from '../constants';
  8  
  9  const SUPPORTED_LANGUAGES = ['css', 'sol', 'html', 'json'];
 10  const DEFAULT_LANGUAGE = 'javascript';
 11  const EDITOR_ID = 'react-monaco-editor-container';
 12  const GUTTER_GLYPH_MARGIN = 2;
 13  
 14  let editor;
 15  
 16  const initMonaco = (value, theme) => {
 17    let model;
 18    if (editor) {
 19      model = editor.getModel();
 20    }
 21    editor = monaco.editor.create(document.getElementById(EDITOR_ID), {
 22      glyphMargin: true,
 23      value,
 24      model
 25    });
 26  };
 27  
 28  class TextEditor extends React.Component {
 29    constructor(props) {
 30      super(props);
 31      this.state = {decorations: []};
 32    }
 33    componentDidMount() {
 34      initMonaco();
 35      editor.onDidChangeModelContent((_event) => {
 36        const value = editor.getValue();
 37        this.props.onFileContentChange(value);
 38      });
 39      editor.layout();
 40      window.addEventListener('resize', this.handleResize);
 41  
 42      editor.onMouseDown((e) => {
 43        if (e.target.type === GUTTER_GLYPH_MARGIN){
 44          this.props.toggleBreakpoint(this.props.currentFile.name, e.target.position.lineNumber);
 45        }
 46      });
 47    }
 48  
 49    handleResize = () => editor.layout();
 50  
 51  
 52    getLanguage() {
 53      if (!this.props.currentFile.name) {
 54        return DEFAULT_LANGUAGE;
 55      }
 56      const extension = this.props.currentFile.name.split('.').pop();
 57      return SUPPORTED_LANGUAGES[SUPPORTED_LANGUAGES.indexOf(extension)] || DEFAULT_LANGUAGE;
 58    }
 59  
 60    extractRowCol(errorMessage) {
 61      const errorSplit = errorMessage.split(':');
 62      if (errorSplit.length >= 3) {
 63        return {row: parseInt(errorSplit[1], 10), col: parseInt(errorSplit[2], 10)};
 64      }
 65      return {row: 0, col: 0};
 66    }
 67  
 68    updateMarkers() {
 69      // TODO: Fetch Result of compilation in embark, via ws
 70      // const {errors, warnings} = this.props.contractCompile;
 71      // const markers = [].concat(errors).concat(warnings).filter((e) => e).map((e) => {
 72      //   const {row, col} = this.extractRowCol(e.formattedMessage);
 73      //   return {
 74      //     startLineNumber: row,
 75      //     startColumn: col,
 76      //     endLineNumber: row,
 77      //     endColumn: col + 1,
 78      //     message: e.formattedMessage,
 79      //     severity: e.severity
 80      //   };
 81      // });
 82      // monaco.editor.setModelMarkers(editor.getModel(), 'test', markers);
 83    }
 84  
 85    updateLanguage() {
 86      const newLanguage = this.getLanguage();
 87      const currentLanguage = editor.getModel().getModeId();
 88      if (newLanguage !== currentLanguage) {
 89        monaco.editor.setModelLanguage(editor.getModel(), newLanguage);
 90      }
 91    }
 92  
 93    updateDecorations() {
 94      const newDecorations = this.props.breakpoints.map(breakpoint => (
 95        {
 96          range: new monaco.Range(breakpoint,1,breakpoint,1),
 97          options: {
 98            isWholeLine: true,
 99            glyphMarginClassName: 'bg-primary rounded-circle'
100          }
101        }
102      ));
103  
104      let debuggerLine = this.props.debuggerLine;
105      newDecorations.push({
106        range: new monaco.Range(debuggerLine,1,debuggerLine,1),
107          options: {
108            isWholeLine: true,
109            className: 'text-editor__debuggerLine'
110          }
111      });
112      const decorations = editor.deltaDecorations(this.state.decorations, newDecorations);
113      this.setState({decorations: decorations});
114    }
115  
116    setTheme() {
117      const vsTheme = this.props.theme === DARK_THEME ? 'vs-dark' : 'vs';
118      monaco.editor.setTheme(vsTheme);
119    }
120  
121    componentDidUpdate(prevProps) {
122      if (this.props.currentFile.content !== prevProps.currentFile.content) {
123        editor.setValue(this.props.currentFile.content || '');
124      }
125  
126      this.updateMarkers();
127      const expectedDecorationsLength = this.props.debuggerLine ? this.props.breakpoints.length + 1 : this.props.breakpoints.length;
128      if (expectedDecorationsLength !== this.state.decorations.length || this.props.debuggerLine !== prevProps.debuggerLine) {
129        this.updateDecorations();
130      }
131  
132      this.setTheme();
133      this.updateLanguage();
134      this.handleResize();
135    }
136  
137    renderTabs() {
138      return (
139        <ul className="list-inline m-0 p-0">
140          {this.props.editorTabs.map(file => (
141            <li key={file.name} className={classNames("p-2", "list-inline-item", "mr-0", "border-right", {
142              'bg-white': file.active && LIGHT_THEME === this.props.theme,
143              'bg-black': file.active && DARK_THEME === this.props.theme
144            })}>
145              <a
146                href="#switch-tab"
147                onClick={() => this.props.addEditorTabs(file)}
148                className="p-2 text-muted"
149              >
150                {file.name}
151              </a>
152              <FontAwesomeIcon style={{cursor: 'pointer'}} onClick={() => this.props.removeEditorTabs(file)} className="px-1" name="close"/>
153            </li>
154          ))}
155        </ul>
156      );
157    }
158  
159    render() {
160      return (
161        <div className="h-100 d-flex flex-column">
162          {this.renderTabs()}
163          <div style={{height: '100%'}} id={EDITOR_ID}/>
164        </div>
165      );
166    }
167  }
168  
169  TextEditor.propTypes = {
170    onFileContentChange: PropTypes.func,
171    currentFile: PropTypes.object,
172    toggleBreakpoint: PropTypes.func,
173    breakpoints: PropTypes.array,
174    debuggerLine: PropTypes.number,
175    editorTabs: PropTypes.array,
176    removeEditorTabs: PropTypes.func,
177    addEditorTabs: PropTypes.func,
178    theme: PropTypes.string
179  };
180  
181  export default TextEditor;