import { h, Component } from 'preact'; import CodeMirror from '../CodeMirror'; import 'codemirror/addon/edit/matchbrackets.js'; import 'codemirror/addon/edit/matchtags.js'; import 'codemirror/addon/edit/closebrackets.js'; import 'codemirror/addon/edit/closetag.js'; import 'codemirror/addon/comment/comment.js'; import 'codemirror/addon/fold/foldcode.js'; import 'codemirror/addon/fold/foldgutter.js'; import 'codemirror/addon/fold/xml-fold.js'; import 'codemirror/addon/fold/indent-fold.js'; import 'codemirror/addon/fold/comment-fold.js'; import 'codemirror/addon/fold/brace-fold.js'; import 'codemirror/addon/hint/show-hint.js'; import 'codemirror/addon/hint/javascript-hint.js'; import 'codemirror/addon/hint/xml-hint.js'; import 'codemirror/addon/hint/html-hint.js'; import 'codemirror/addon/hint/css-hint.js'; import 'codemirror/addon/selection/active-line.js'; import 'codemirror/addon/search/searchcursor.js'; import 'codemirror/addon/search/search.js'; import 'codemirror/addon/dialog/dialog.js'; import 'codemirror/addon/search/jump-to-line.js'; import 'codemirror/mode/xml/xml.js'; import 'codemirror/mode/css/css.js'; import 'codemirror/mode/javascript/javascript.js'; import 'codemirror/mode/htmlmixed/htmlmixed.js'; import 'codemirror/keymap/sublime.js'; import 'codemirror/keymap/vim.js'; import 'code-blast-codemirror/code-blast.js'; import emmet from '@emmetio/codemirror-plugin'; import { prettify, loadCss } from '../utils'; import { modes } from '../codeModes'; import { deferred } from '../deferred'; import { trackEvent } from '../analytics'; emmet(CodeMirror); let monacoDepsDeferred; window.MonacoEnvironment = { getWorkerUrl(moduleId, label) { switch (label) { case 'html': return 'lib/monaco/workers/html.worker.bundle.js'; case 'json': return 'lib/monaco/workers/json.worker.bundle.js'; case 'css': case 'scss': case 'less': return 'lib/monaco/workers/css.worker.bundle.js'; case 'typescript': case 'javascript': return 'lib/monaco/workers/ts.worker.bundle.js'; default: return 'lib/monaco/workers/editor.worker.bundle.js'; } } }; export default class CodeEditor extends Component { componentDidMount() { this.initEditor(); } shouldComponentUpdate(nextProps) { if (nextProps.prefs !== this.props.prefs) { const { prefs } = nextProps; if (this.props.type === 'monaco') { this.editorReadyDeferred.promise.then(() => { this.instance.updateOptions({ fontSize: prefs.fontSize }); }); } else { this.instance.setOption( 'indentWithTabs', prefs.indentWith !== 'spaces' ); this.instance.setOption( 'blastCode', prefs.isCodeBlastOn ? { effect: 2, shake: false } : false ); this.instance.setOption('theme', prefs.editorTheme); this.instance.setOption('indentUnit', +prefs.indentSize); this.instance.setOption('tabSize', +prefs.indentSize); this.instance.setOption('keyMap', prefs.keymap); this.instance.setOption('lineWrapping', prefs.lineWrap); this.instance.setOption('lineWrapping', prefs.autoCloseTags); this.instance.refresh(); } } // Only condition when this component updates is when prop.type changes if (nextProps.type !== this.props.type) { if ( this.node.parentElement.querySelector('.monaco-editor, .CodeMirror') ) { this.node.parentElement .querySelector('.monaco-editor, .CodeMirror') .remove(); } return true; } return false; } componentDidUpdate(prevProps) { // prop.type changed, reinit the editor. On any other prop, component never updates. this.initEditor(); } setModel(model) { this.editorReadyDeferred.promise.then(() => { this.instance.swapDoc ? this.instance.swapDoc(model) : this.instance.setModel(model); }); } setValue(value) { // HACK: We set a flag on window for an ultra-short duration, which 'change' // listener uses to set the change.origin to 'setValue', otherwise it's // '+input' if (this.props.type === 'monaco') { window.monacoSetValTriggered = true; setTimeout(() => { window.monacoSetValTriggered = false; }, 1); } this.editorReadyDeferred.promise.then(() => { this.instance.setValue(value); }); // We save last set value so that when editor type changes, we can // populate that last value this.lastSetValue = value; this.refresh(); } getValue() { return this.instance.getValue(); } saveViewState() { if (this.props.type === 'monaco') { return this.instance.saveViewState(); } } restoreViewState(state) { if (this.props.type === 'monaco') { this.instance.restoreViewState(state); } } setOption(option, value) { this.editorReadyDeferred.promise.then(() => { if (this.props.type === 'monaco') { this.instance.updateOptions({ [option]: value }); } else { this.instance.setOption(option, value); } }); } setLanguage(value) { this.editorReadyDeferred.promise.then(() => { if (this.props.type === 'monaco') { monaco.editor.setModelLanguage( this.instance.getModel(), this.getMonacoLanguageFromMode(modes[value].cmMode) ); } else { this.instance.setOption('mode', modes[value].cmMode); CodeMirror.autoLoadMode( this.instance, modes[value].cmPath || modes[value].cmMode ); } }); } clearGutter(gutterName) { this.editorReadyDeferred.promise.then(() => { if (this.instance.clearGutter) { this.instance.clearGutter(gutterName); } }); } showErrors(errors) { if (this.props.type === 'codemirror') { const editor = this.instance; errors.forEach(function (error) { editor.operation(function () { var n = document.createElement('div'); n.setAttribute('data-title', error.message); n.classList.add('gutter-error-marker'); editor.setGutterMarker(error.lineNumber, 'error-gutter', n); }); }); } } refresh() { this.editorReadyDeferred.promise.then(() => { this.instance.refresh ? this.instance.refresh() : this.instance.layout(); }); } focus() { this.editorReadyDeferred.promise.then(() => { this.instance.focus(); }); } /** * Converts codemirror mode value to monaco language values. * TODO: Refactor to not be codemirror related. * @param {string} mode Codemirror mode value */ getMonacoLanguageFromMode(mode) { if (['htmlmixed'].includes(mode)) { return 'html'; } if (['css', 'sass', 'scss', 'less', 'stylus'].includes(mode)) { return 'css'; } if (['javascript', 'text/typescript-jsx', 'jsx'].includes(mode)) { return 'javascript'; } return mode; } /** * Loads the asynchronous deps according to the editor type. */ async loadDeps() { if (this.props.type === 'monaco') { if (!monacoDepsDeferred) { monacoDepsDeferred = deferred(); loadCss({ url: 'lib/monaco/monaco.css', id: 'monaco-css' }); import( /* webpackChunkName: "monaco" */ '/lib/monaco/monaco.bundle.js' ).then(() => { monacoDepsDeferred.resolve(); }); } return monacoDepsDeferred.promise; } return Promise.resolve(); } async initEditor() { this.editorReadyDeferred = deferred(); await this.loadDeps(); const { options, prefs } = this.props; if (this.props.type === 'monaco') { this.instance = monaco.editor.create(this.node, { language: this.getMonacoLanguageFromMode(options.mode), value: this.lastSetValue || '', roundedSelection: false, scrollBeyondLastLine: false, theme: 'vs-dark', fontSize: prefs.fontSize, minimap: { enabled: false }, wordWrap: 'on', renderWhitespace: 'all', fontLigatures: true, automaticLayout: true }); window.monacoInstance = this.instance; this.instance.onDidChangeModelContent(change => { this.props.onChange(this.instance, { ...change, origin: window.monacoSetValTriggered ? 'setValue' : '+input' }); }); this.instance.addCommand( monaco.KeyMod.WinCtrl | monaco.KeyMod.Shift | monaco.KeyCode.KEY_F, () => { if (options.prettier) { prettify({ content: this.instance.getValue(), type: options.prettierParser }).then(formattedCode => this.instance.setValue(formattedCode)); trackEvent('ui', 'prettifyKeyboardShortcut'); } } ); this.editorReadyDeferred.resolve(); } else { this.instance = CodeMirror.fromTextArea(this.node, { mode: options.mode, value: this.lastSetValue || '', lineNumbers: true, lineWrapping: !!prefs.lineWrap, autofocus: options.autofocus || false, autoCloseBrackets: true, autoCloseTags: !!prefs.autoCloseTags, matchBrackets: true, matchTags: options.matchTags || false, tabMode: 'indent', keyMap: prefs.keyMap || 'sublime', theme: prefs.editorTheme || 'monokai', lint: !!options.lint, tabSize: +prefs.indentSize || 2, indentWithTabs: prefs.indentWith !== 'spaces', indentUnit: +prefs.indentSize, foldGutter: true, styleActiveLine: true, gutters: options.gutters || [], // cursorScrollMargin: '20', has issue with scrolling profile: options.profile || '', extraKeys: { Up: function (editor) { // Stop up/down keys default behavior when saveditempane is open // if (isSavedItemsPaneOpen) { // return; // } CodeMirror.commands.goLineUp(editor); }, Down: function (editor) { // if (isSavedItemsPaneOpen) { // return; // } CodeMirror.commands.goLineDown(editor); }, 'Shift-Tab': function (editor) { CodeMirror.commands.indentAuto(editor); }, 'Shift-Ctrl-F': function (editor) { if (options.prettier) { prettify({ content: editor.getValue(), type: options.prettierParser }).then(formattedCode => editor.setValue(formattedCode)); } trackEvent('ui', 'prettifyKeyboardShortcut'); }, Tab: function (editor) { if (options.emmet) { const didEmmetWork = editor.execCommand( 'emmetExpandAbbreviation' ); if (didEmmetWork === true) { return; } } const input = $('[data-setting=indentWith]:checked'); if ( !editor.somethingSelected() && (!prefs.indentWith || prefs.indentWith === 'spaces') ) { // softtabs adds spaces. This is required because by default tab key will put tab, but we want // to indent with spaces if `spaces` is preferred mode of indentation. // `somethingSelected` needs to be checked otherwise, all selected code is replaced with softtab. CodeMirror.commands.insertSoftTab(editor); } else { CodeMirror.commands.defaultTab(editor); } }, Enter: 'emmetInsertLineBreak' } }); this.editorReadyDeferred.resolve(); this.instance.on('focus', editor => { if (typeof this.props.onFocus === 'function') this.props.onFocus(editor); }); this.instance.on('change', this.props.onChange); this.instance.addKeyMap({ 'Ctrl-Space': 'autocomplete' }); this.instance.on('inputRead', (editor, input) => { // Process further If this has autocompletition on and also the global // autocomplete setting is on. if ( !this.props.options.noAutocomplete && this.props.prefs.autoComplete ) { if ( input.origin !== '+input' || input.text[0] === ';' || input.text[0] === ',' || input.text[0] === ' ' ) { return; } CodeMirror.commands.autocomplete(editor, null, { completeSingle: false }); } }); } // this.props.onCreation(this.instance); } render() { const node = this.props.type === 'monaco' ? (