2020-06-06 17:00:20 +02:00
|
|
|
import * as React from 'react';
|
|
|
|
import { useEffect, useImperativeHandle, useState, useRef, useCallback, forwardRef } from 'react';
|
2020-06-18 19:58:11 +02:00
|
|
|
|
|
|
|
import * as CodeMirror from 'codemirror';
|
|
|
|
|
2020-06-06 17:00:20 +02:00
|
|
|
import 'codemirror/addon/comment/comment';
|
|
|
|
import 'codemirror/addon/dialog/dialog';
|
|
|
|
import 'codemirror/addon/edit/closebrackets';
|
|
|
|
import 'codemirror/addon/edit/continuelist';
|
2020-07-23 00:13:23 +02:00
|
|
|
import 'codemirror/addon/scroll/annotatescrollbar';
|
|
|
|
import 'codemirror/addon/search/matchesonscrollbar';
|
|
|
|
import 'codemirror/addon/search/searchcursor';
|
2020-06-06 17:00:20 +02:00
|
|
|
|
|
|
|
import useListIdent from './utils/useListIdent';
|
|
|
|
import useScrollUtils from './utils/useScrollUtils';
|
|
|
|
import useCursorUtils from './utils/useCursorUtils';
|
|
|
|
import useLineSorting from './utils/useLineSorting';
|
2020-07-23 00:13:23 +02:00
|
|
|
import useEditorSearch from './utils/useEditorSearch';
|
2020-07-12 20:42:19 +02:00
|
|
|
import useJoplinMode from './utils/useJoplinMode';
|
2020-06-06 17:00:20 +02:00
|
|
|
|
|
|
|
import 'codemirror/keymap/emacs';
|
|
|
|
import 'codemirror/keymap/vim';
|
2020-06-16 13:59:33 +02:00
|
|
|
import 'codemirror/keymap/sublime'; // Used for swapLineUp and swapLineDown
|
2020-06-06 17:00:20 +02:00
|
|
|
|
2020-07-15 12:46:14 +02:00
|
|
|
import 'codemirror/mode/meta';
|
2020-08-02 17:26:26 +02:00
|
|
|
const { shim } = require('lib/shim.js');
|
2020-07-15 12:46:14 +02:00
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
2020-06-06 17:00:20 +02:00
|
|
|
|
|
|
|
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);
|
2020-07-23 00:13:23 +02:00
|
|
|
useEditorSearch(CodeMirror);
|
2020-07-12 20:42:19 +02:00
|
|
|
useJoplinMode(CodeMirror);
|
2020-06-06 17:00:20 +02:00
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
}, []);
|
|
|
|
|
2020-08-02 17:26:26 +02:00
|
|
|
useEffect(() => {
|
|
|
|
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',
|
|
|
|
};
|
|
|
|
|
|
|
|
if (shim.isMac()) {
|
|
|
|
CodeMirror.keyMap.default = {
|
|
|
|
// MacOS
|
|
|
|
'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',
|
|
|
|
'Cmd-Left': 'goLineLeft',
|
|
|
|
'Cmd-Right': 'goLineRight',
|
|
|
|
'Alt-Left': 'goGroupLeft',
|
|
|
|
'Alt-Right': 'goGroupRight',
|
|
|
|
'Alt-Backspace': 'delGroupBefore',
|
|
|
|
'Alt-Delete': 'delGroupAfter',
|
|
|
|
'Cmd-[': 'indentLess',
|
|
|
|
'Cmd-]': 'indentMore',
|
|
|
|
'Cmd-/': 'toggleComment',
|
|
|
|
'Cmd-Opt-S': 'sortSelectedLines',
|
|
|
|
'Opt-Up': 'swapLineUp',
|
|
|
|
'Opt-Down': 'swapLineDown',
|
|
|
|
|
|
|
|
'fallthrough': 'basic',
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
CodeMirror.keyMap.default = {
|
|
|
|
// Windows/linux
|
|
|
|
'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',
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}, []);
|
|
|
|
|
2020-06-06 17:00:20 +02:00
|
|
|
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,
|
|
|
|
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);
|
2020-07-23 00:18:52 +02:00
|
|
|
}
|
|
|
|
}, [props.value]);
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (editor) {
|
2020-06-06 17:00:20 +02:00
|
|
|
editor.setOption('theme', props.theme);
|
2020-07-23 00:18:52 +02:00
|
|
|
}
|
|
|
|
}, [props.theme]);
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (editor) {
|
2020-06-06 17:00:20 +02:00
|
|
|
editor.setOption('mode', props.mode);
|
2020-07-23 00:18:52 +02:00
|
|
|
}
|
|
|
|
}, [props.mode]);
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (editor) {
|
2020-06-06 17:00:20 +02:00
|
|
|
editor.setOption('readOnly', props.readOnly);
|
2020-07-23 00:18:52 +02:00
|
|
|
}
|
|
|
|
}, [props.readOnly]);
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (editor) {
|
2020-06-06 17:00:20 +02:00
|
|
|
editor.setOption('autoCloseBrackets', props.autoMatchBraces);
|
2020-07-23 00:18:52 +02:00
|
|
|
}
|
|
|
|
}, [props.autoMatchBraces]);
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (editor) {
|
2020-06-06 17:00:20 +02:00
|
|
|
editor.setOption('keyMap', props.keyMap ? props.keyMap : 'default');
|
|
|
|
}
|
2020-07-23 00:18:52 +02:00
|
|
|
}, [props.keyMap]);
|
2020-06-06 17:00:20 +02:00
|
|
|
|
|
|
|
return <div style={props.style} ref={editorParent} />;
|
|
|
|
}
|
|
|
|
|
|
|
|
export default forwardRef(Editor);
|