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;