/* eslint-disable import/prefer-default-export */ // This contains the CodeMirror instance, which needs to be built into a bundle // using `npm run buildInjectedJs`. This bundle is then loaded from // NoteEditor.tsx into the webview. // // In general, since this file is harder to debug due to the intermediate built // step, it's better to keep it as light as possible - it shoud just be a light // wrapper to access CodeMirror functionalities. Anything else should be done // from NoteEditor.tsx. import { EditorState, Extension } from '@codemirror/state'; import { markdown } from '@codemirror/lang-markdown'; import { highlightSelectionMatches, search } from '@codemirror/search'; import { defaultHighlightStyle, syntaxHighlighting, HighlightStyle } from '@codemirror/language'; import { tags } from '@lezer/highlight'; import { EditorView, drawSelection, highlightSpecialChars, ViewUpdate } from '@codemirror/view'; import { undo, redo, history, undoDepth, redoDepth } from '@codemirror/commands'; import { keymap } from '@codemirror/view'; import { indentOnInput } from '@codemirror/language'; import { searchKeymap } from '@codemirror/search'; import { historyKeymap, defaultKeymap } from '@codemirror/commands'; interface CodeMirrorResult { editor: EditorView; undo: Function; redo: Function; select(anchor: number, head: number): void; scrollSelectionIntoView(): void; insertText(text: string): void; } function postMessage(name: string, data: any) { (window as any).ReactNativeWebView.postMessage(JSON.stringify({ data, name, })); } function logMessage(...msg: any[]) { postMessage('onLog', { value: msg }); } // For an example on how to customize the theme, see: // // https://github.com/codemirror/theme-one-dark/blob/main/src/one-dark.ts // // For a tutorial, see: // // https://codemirror.net/6/examples/styling/#themes // // Use Safari developer tools to view the content of the CodeMirror iframe while // the app is running. It seems that what appears as ".ͼ1" in the CSS is the // equivalent of "&" in the theme object. So to target ".ͼ1.cm-focused", you'd // use '&.cm-focused' in the theme. const createTheme = (theme: any): Extension[] => { const isDarkTheme = theme.appearance === 'dark'; const baseGlobalStyle: Record = { color: theme.color, backgroundColor: theme.backgroundColor, fontFamily: theme.fontFamily, fontSize: `${theme.fontSize}px`, }; const baseCursorStyle: Record = { }; const baseContentStyle: Record = { }; const baseSelectionStyle: Record = { }; // If we're in dark mode, the caret and selection are difficult to see. // Adjust them appropriately if (isDarkTheme) { // Styling the caret requires styling both the caret itself // and the CodeMirror caret. // See https://codemirror.net/6/examples/styling/#themes baseContentStyle.caretColor = 'white'; baseCursorStyle.borderLeftColor = 'white'; baseSelectionStyle.backgroundColor = '#6b6b6b'; } const baseTheme = EditorView.baseTheme({ '&': baseGlobalStyle, // These must be !important or more specific than CodeMirror's built-ins '.cm-content': baseContentStyle, '&.cm-focused .cm-cursor': baseCursorStyle, '&.cm-focused .cm-selectionBackground, ::selection': baseSelectionStyle, '&.cm-focused': { outline: 'none', }, }); const appearanceTheme = EditorView.theme({}, { dark: isDarkTheme }); const baseHeadingStyle = { fontWeight: 'bold', fontFamily: theme.fontFamily, }; const highlightingStyle = HighlightStyle.define([ { tag: tags.strong, fontWeight: 'bold', }, { tag: tags.emphasis, fontStyle: 'italic', }, { ...baseHeadingStyle, tag: tags.heading1, fontSize: '1.6em', borderBottom: `1px solid ${theme.dividerColor}`, }, { ...baseHeadingStyle, tag: tags.heading2, fontSize: '1.4em', }, { ...baseHeadingStyle, tag: tags.heading3, fontSize: '1.3em', }, { ...baseHeadingStyle, tag: tags.heading4, fontSize: '1.2em', }, { ...baseHeadingStyle, tag: tags.heading5, fontSize: '1.1em', }, { ...baseHeadingStyle, tag: tags.heading6, fontSize: '1.0em', }, { tag: tags.list, fontFamily: theme.fontFamily, }, ]); return [ baseTheme, appearanceTheme, syntaxHighlighting(highlightingStyle), // If we haven't defined highlighting for tags, fall back // to the default. syntaxHighlighting(defaultHighlightStyle, { fallback: true }), ]; }; export function initCodeMirror(parentElement: any, initialText: string, theme: any): CodeMirrorResult { logMessage('Initializing CodeMirror...'); let schedulePostUndoRedoDepthChangeId_: any = 0; function schedulePostUndoRedoDepthChange(editor: EditorView, doItNow: boolean = false) { if (schedulePostUndoRedoDepthChangeId_) { if (doItNow) { clearTimeout(schedulePostUndoRedoDepthChangeId_); } else { return; } } schedulePostUndoRedoDepthChangeId_ = setTimeout(() => { schedulePostUndoRedoDepthChangeId_ = null; postMessage('onUndoRedoDepthChange', { undoDepth: undoDepth(editor.state), redoDepth: redoDepth(editor.state), }); }, doItNow ? 0 : 1000); } const editor = new EditorView({ state: EditorState.create({ // See https://github.com/codemirror/basic-setup/blob/main/src/codemirror.ts // for a sample configuration. extensions: [ markdown(), ...createTheme(theme), history(), search(), drawSelection(), highlightSpecialChars(), highlightSelectionMatches(), indentOnInput(), EditorView.lineWrapping, EditorView.contentAttributes.of({ autocapitalize: 'sentence' }), EditorView.updateListener.of((viewUpdate: ViewUpdate) => { if (viewUpdate.docChanged) { postMessage('onChange', { value: editor.state.doc.toString() }); schedulePostUndoRedoDepthChange(editor); } if (!viewUpdate.state.selection.eq(viewUpdate.startState.selection)) { const mainRange = viewUpdate.state.selection.main; const selStart = mainRange.from; const selEnd = mainRange.to; postMessage('onSelectionChange', { selection: { start: selStart, end: selEnd } }); } }), keymap.of([ ...defaultKeymap, ...historyKeymap, ...searchKeymap, ]), ], doc: initialText, }), parent: parentElement, }); return { editor, undo: () => { undo(editor); schedulePostUndoRedoDepthChange(editor, true); }, redo: () => { redo(editor); schedulePostUndoRedoDepthChange(editor, true); }, select: (anchor: number, head: number) => { editor.dispatch(editor.state.update({ selection: { anchor, head }, scrollIntoView: true, })); }, scrollSelectionIntoView: () => { editor.dispatch(editor.state.update({ scrollIntoView: true, })); }, insertText: (text: string) => { editor.dispatch(editor.state.replaceSelection(text)); }, }; }