mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-12 08:54:00 +02:00
245 lines
6.8 KiB
TypeScript
245 lines
6.8 KiB
TypeScript
/* 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<string, string> = {
|
|
color: theme.color,
|
|
backgroundColor: theme.backgroundColor,
|
|
fontFamily: theme.fontFamily,
|
|
fontSize: `${theme.fontSize}px`,
|
|
};
|
|
const baseCursorStyle: Record<string, string> = { };
|
|
const baseContentStyle: Record<string, string> = { };
|
|
const baseSelectionStyle: Record<string, string> = { };
|
|
|
|
// 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));
|
|
},
|
|
};
|
|
}
|