import * as React from 'react'; import { useEffect, useImperativeHandle, useState, useRef, useCallback, forwardRef } from 'react'; import * as CodeMirror from 'codemirror'; import 'codemirror/addon/comment/comment'; import 'codemirror/addon/dialog/dialog'; import 'codemirror/addon/edit/closebrackets'; import 'codemirror/addon/edit/continuelist'; import 'codemirror/addon/scroll/scrollpastend'; import 'codemirror/addon/scroll/annotatescrollbar'; import 'codemirror/addon/search/matchesonscrollbar'; import 'codemirror/addon/search/searchcursor'; import useListIdent from './utils/useListIdent'; import useScrollUtils from './utils/useScrollUtils'; import useCursorUtils from './utils/useCursorUtils'; import useLineSorting from './utils/useLineSorting'; import useEditorSearch from './utils/useEditorSearch'; import useJoplinMode from './utils/useJoplinMode'; import 'codemirror/keymap/emacs'; import 'codemirror/keymap/vim'; import 'codemirror/keymap/sublime'; // Used for swapLineUp and swapLineDown import 'codemirror/mode/meta'; const { reg } = require('lib/registry.js'); // Based on http://pypl.github.io/PYPL.html // +XML (HTML) +CSS and Markdown added const topLanguages = [ 'python', 'clike', 'javascript', 'jsx', 'php', 'r', 'swift', 'go', 'vb', 'vbscript', 'ruby', 'rust', 'dart', 'lua', 'groovy', 'perl', 'cobol', 'julia', 'haskell', 'pascal', 'css', 'xml', 'markdown', ]; // Load Top Modes for (let i = 0; i < topLanguages.length; i++) { const mode = topLanguages[i]; if (CodeMirror.modeInfo.find((m: any) => m.mode === mode)) { require(`codemirror/mode/${mode}/${mode}`); } else { reg.logger().error('Cannot find CodeMirror mode: ', mode); } } export interface EditorProps { value: string, mode: string, style: any, theme: any, readOnly: boolean, autoMatchBraces: boolean, keyMap: string, onChange: any, onScroll: any, onEditorContextMenu: any, onEditorPaste: any, } function Editor(props: EditorProps, ref: any) { const [editor, setEditor] = useState(null); const editorParent = useRef(null); // Codemirror plugins add new commands to codemirror (or change it's behavior) // This command adds the smartListIndent function which will be bound to tab useListIdent(CodeMirror); useScrollUtils(CodeMirror); useCursorUtils(CodeMirror); useLineSorting(CodeMirror); useEditorSearch(CodeMirror); useJoplinMode(CodeMirror); CodeMirror.keyMap.basic = { 'Left': 'goCharLeft', 'Right': 'goCharRight', 'Up': 'goLineUp', 'Down': 'goLineDown', 'End': 'goLineEnd', 'Home': 'goLineStartSmart', 'PageUp': 'goPageUp', 'PageDown': 'goPageDown', 'Delete': 'delCharAfter', 'Backspace': 'delCharBefore', 'Shift-Backspace': 'delCharBefore', 'Tab': 'smartListIndent', 'Shift-Tab': 'smartListUnindent', 'Enter': 'insertListElement', 'Insert': 'toggleOverwrite', 'Esc': 'singleSelection', }; CodeMirror.keyMap.default = { 'Ctrl-A': 'selectAll', 'Ctrl-D': 'deleteLine', 'Ctrl-Z': 'undo', 'Shift-Ctrl-Z': 'redo', 'Ctrl-Y': 'redo', 'Ctrl-Home': 'goDocStart', 'Ctrl-End': 'goDocEnd', 'Ctrl-Up': 'goLineUp', 'Ctrl-Down': 'goLineDown', 'Ctrl-Left': 'goGroupLeft', 'Ctrl-Right': 'goGroupRight', 'Alt-Left': 'goLineStart', 'Alt-Right': 'goLineEnd', 'Ctrl-Backspace': 'delGroupBefore', 'Ctrl-Delete': 'delGroupAfter', 'Ctrl-[': 'indentLess', 'Ctrl-]': 'indentMore', 'Ctrl-/': 'toggleComment', 'Ctrl-Alt-S': 'sortSelectedLines', 'Alt-Up': 'swapLineUp', 'Alt-Down': 'swapLineDown', 'fallthrough': 'basic', }; CodeMirror.keyMap.pcDefault = CodeMirror.keyMap.default; CodeMirror.keyMap.macDefault = { 'Cmd-A': 'selectAll', 'Cmd-D': 'deleteLine', 'Cmd-Z': 'undo', 'Shift-Cmd-Z': 'redo', 'Cmd-Y': 'redo', 'Cmd-Home': 'goDocStart', 'Cmd-Up': 'goDocStart', 'Cmd-End': 'goDocEnd', 'Cmd-Down': 'goDocEnd', 'Alt-Left': 'goGroupLeft', 'Alt-Right': 'goGroupRight', 'Cmd-Left': 'goLineLeft', 'Cmd-Right': 'goLineRight', 'Alt-Backspace': 'delGroupBefore', 'Alt-Delete': 'delGroupAfter', 'Cmd-[': 'indentLess', 'Cmd-]': 'indentMore', 'Cmd-/': 'toggleComment', 'Cmd-Opt-S': 'sortSelectedLines', 'Opt-Up': 'swapLineUp', 'Opt-Down': 'swapLineDown', 'fallthrough': 'basic', }; useImperativeHandle(ref, () => { return editor; }); const editor_change = useCallback((cm: any, change: any) => { if (props.onChange && change.origin !== 'setValue') { props.onChange(cm.getValue()); } }, [props.onChange]); // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars const editor_scroll = useCallback((_cm: any) => { props.onScroll(); }, [props.onScroll]); // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars const editor_mousedown = useCallback((_cm: any, event: any) => { if (event && event.button === 2) { props.onEditorContextMenu(); } }, [props.onEditorContextMenu]); // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars const editor_paste = useCallback((_cm: any, _event: any) => { props.onEditorPaste(); }, [props.onEditorPaste]); // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars const editor_drop = useCallback((cm: any, _event: any) => { cm.focus(); }, []); const editor_drag = useCallback((cm: any, event: any) => { // This is the type for all drag and drops that are external to codemirror // setting the cursor allows us to drop them in the right place if (event.dataTransfer.effectAllowed === 'all') { const coords = cm.coordsChar({ left: event.x, top: event.y }); cm.setCursor(coords); } }, []); useEffect(() => { if (!editorParent.current) return () => {}; const cmOptions = { value: props.value, screenReaderLabel: props.value, theme: props.theme, mode: props.mode, readOnly: props.readOnly, autoCloseBrackets: props.autoMatchBraces, inputStyle: 'textarea', // contenteditable loses cursor position on focus change, use textarea instead lineWrapping: true, lineNumbers: false, scrollPastEnd: true, indentWithTabs: true, indentUnit: 4, spellcheck: true, allowDropFileTypes: [''], // disable codemirror drop handling keyMap: props.keyMap ? props.keyMap : 'default', }; const cm = CodeMirror(editorParent.current, cmOptions); setEditor(cm); cm.on('change', editor_change); cm.on('scroll', editor_scroll); cm.on('mousedown', editor_mousedown); cm.on('paste', editor_paste); cm.on('drop', editor_drop); cm.on('dragover', editor_drag); return () => { // Clean up codemirror cm.off('change', editor_change); cm.off('scroll', editor_scroll); cm.off('mousedown', editor_mousedown); cm.off('paste', editor_paste); cm.off('drop', editor_drop); cm.off('dragover', editor_drag); editorParent.current.removeChild(cm.getWrapperElement()); setEditor(null); }; }, []); useEffect(() => { if (editor) { // Value can also be changed by the editor itself so we need this guard // to prevent loops if (props.value !== editor.getValue()) { editor.setValue(props.value); editor.clearHistory(); } editor.setOption('screenReaderLabel', props.value); editor.setOption('theme', props.theme); editor.setOption('mode', props.mode); editor.setOption('readOnly', props.readOnly); editor.setOption('autoCloseBrackets', props.autoMatchBraces); editor.setOption('keyMap', props.keyMap ? props.keyMap : 'default'); } }, [props.value, props.theme, props.mode, props.readOnly, props.autoMatchBraces, props.keyMap]); useEffect(() => { if (editor) { // Need to let codemirror know that it's container's size has changed so that it can // re-compute anything it needs to. This ensures the cursor (and anything that is // based on window size will be correct // Manually calling refresh here will cause a double refresh in some instances (when the // windows size is changed for example) but this is a fairly quick operation so it's worth // it. editor.refresh(); } }, [props.style.width, props.style.height]); return
; } export default forwardRef(Editor);