diff --git a/.eslintignore b/.eslintignore
index facf53b2b..618ef565f 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -604,6 +604,7 @@ packages/default-plugins/utils/getCurrentCommitHash.js
packages/default-plugins/utils/getPathToPatchFileFor.js
packages/default-plugins/utils/readRepositoryJson.js
packages/default-plugins/utils/waitForCliInput.js
+packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5BuiltInOptions.js
packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.test.js
packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.js
packages/editor/CodeMirror/CodeMirror5Emulation/Decorator.js
diff --git a/.gitignore b/.gitignore
index 32ddcc56e..665c9ffed 100644
--- a/.gitignore
+++ b/.gitignore
@@ -584,6 +584,7 @@ packages/default-plugins/utils/getCurrentCommitHash.js
packages/default-plugins/utils/getPathToPatchFileFor.js
packages/default-plugins/utils/readRepositoryJson.js
packages/default-plugins/utils/waitForCliInput.js
+packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5BuiltInOptions.js
packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.test.js
packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.js
packages/editor/CodeMirror/CodeMirror5Emulation/Decorator.js
diff --git a/packages/app-desktop/app.ts b/packages/app-desktop/app.ts
index 23d7635b4..a75a95315 100644
--- a/packages/app-desktop/app.ts
+++ b/packages/app-desktop/app.ts
@@ -201,8 +201,10 @@ class Application extends BaseApplication {
// The '*' and '!important' parts are necessary to make sure Russian text is displayed properly
// https://github.com/laurent22/joplin/issues/155
+ //
+ // Note: Be careful about the specificity here. Incorrect specificity can break monospaced fonts in tables.
- const css = `.CodeMirror *, .cm-editor .cm-content { font-family: ${fontFamilies.join(', ')} !important; }`;
+ const css = `.CodeMirror5 *, .cm-editor .cm-content { font-family: ${fontFamilies.join(', ')} !important; }`;
const styleTag = document.createElement('style');
styleTag.type = 'text/css';
styleTag.appendChild(document.createTextNode(css));
diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/Editor.tsx b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/Editor.tsx
index 9f3bcd360..063f16c60 100644
--- a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/Editor.tsx
+++ b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/Editor.tsx
@@ -283,7 +283,7 @@ function Editor(props: EditorProps, ref: any) {
}
}, [pluginOptions, editor]);
- return
;
+ return ;
}
export default forwardRef(Editor);
diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/Editor.tsx b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/Editor.tsx
index ee063c856..19fe5bb12 100644
--- a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/Editor.tsx
+++ b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/Editor.tsx
@@ -98,6 +98,12 @@ const Editor = (props: Props, ref: ForwardedRef) => {
const editor = createEditor(editorContainerRef.current, editorProps);
editor.addStyles({
'.cm-scroller': { overflow: 'auto' },
+ '&.CodeMirror': {
+ height: 'unset',
+ background: 'unset',
+ overflow: 'unset',
+ direction: 'unset',
+ },
});
setEditor(editor);
diff --git a/packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5BuiltInOptions.ts b/packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5BuiltInOptions.ts
new file mode 100644
index 000000000..15d32e231
--- /dev/null
+++ b/packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5BuiltInOptions.ts
@@ -0,0 +1,83 @@
+import { Compartment, Extension, RangeSetBuilder, StateEffect } from '@codemirror/state';
+import { Decoration, DecorationSet, EditorView, ViewPlugin, ViewUpdate } from '@codemirror/view';
+
+const activeLineDecoration = Decoration.line({ class: 'CodeMirror-activeline CodeMirror-activeline-background' });
+
+const optionToExtension: Record = {
+ 'styleActiveLine': [
+ ViewPlugin.fromClass(class {
+ public decorations: DecorationSet;
+
+ public constructor(view: EditorView) {
+ this.updateDecorations(view);
+ }
+
+ public update(update: ViewUpdate) {
+ this.updateDecorations(update.view);
+ }
+
+ private updateDecorations(view: EditorView) {
+ const builder = new RangeSetBuilder();
+ let lastLine = -1;
+
+ for (const selection of view.state.selection.ranges) {
+ const startLine = selection.from;
+ const line = view.state.doc.lineAt(startLine);
+
+ if (line.number !== lastLine) {
+ builder.add(line.from, line.from, activeLineDecoration);
+ }
+
+ lastLine = line.number;
+ }
+
+ this.decorations = builder.finish();
+ }
+ }, {
+ decorations: plugin => plugin.decorations,
+ }),
+ EditorView.baseTheme({
+ '&dark .CodeMirror-activeline-background': {
+ background: '#3304',
+ color: 'white',
+ },
+ '&light .CodeMirror-activeline-background': {
+ background: '#7ff4',
+ color: 'black',
+ },
+ }),
+ ],
+};
+
+// Maps several CM5 options to CM6 extensions
+export default class CodeMirror5BuiltInOptions {
+ private activeOptions: string[] = [];
+ private extensionCompartment: Compartment = new Compartment();
+
+ public constructor(private editor: EditorView) {
+ editor.dispatch({
+ effects: StateEffect.appendConfig.of(this.extensionCompartment.of([])),
+ });
+ }
+
+ private updateExtensions() {
+ const extensions = this.activeOptions.map(option => optionToExtension[option]);
+ this.editor.dispatch({
+ effects: this.extensionCompartment.reconfigure(extensions),
+ });
+ }
+
+ public supportsOption(option: string) {
+ return optionToExtension.hasOwnProperty(option);
+ }
+
+ public setOption(optionName: string, value: boolean) {
+ this.activeOptions = this.activeOptions.filter(other => other !== optionName);
+
+ if (value) {
+ this.activeOptions.push(optionName);
+ }
+
+ this.updateExtensions();
+ }
+}
diff --git a/packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.ts b/packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.ts
index f9dd4232c..9b1af2bf4 100644
--- a/packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.ts
+++ b/packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.ts
@@ -8,6 +8,7 @@ import { StateEffect } from '@codemirror/state';
import { StreamParser } from '@codemirror/language';
import Decorator, { LineWidgetOptions, MarkTextOptions } from './Decorator';
import insertLineAfter from '../editorCommands/insertLineAfter';
+import CodeMirror5BuiltInOptions from './CodeMirror5BuiltInOptions';
const { pregQuote } = require('@joplin/lib/string-utils-common');
@@ -52,6 +53,7 @@ export default class CodeMirror5Emulation extends BaseCodeMirror5Emulation {
private _options: Record = Object.create(null);
private _decorator: Decorator;
private _decoratorExtension: Extension;
+ private _builtInOptions: CodeMirror5BuiltInOptions;
// Used by some plugins to store state.
public state: Record = Object.create(null);
@@ -70,6 +72,7 @@ export default class CodeMirror5Emulation extends BaseCodeMirror5Emulation {
const { decorator, extension: decoratorExtension } = Decorator.create(editor);
this._decorator = decorator;
this._decoratorExtension = decoratorExtension;
+ this._builtInOptions = new CodeMirror5BuiltInOptions(editor);
editor.dispatch({
effects: StateEffect.appendConfig.of(this.makeCM6Extensions()),
@@ -129,10 +132,8 @@ export default class CodeMirror5Emulation extends BaseCodeMirror5Emulation {
return { dom };
}),
- // Note: We can allow legacy CM5 CSS to apply to the editor
- // with a line similar to the following:
- // EditorView.editorAttributes.of({ class: 'CodeMirror' }),
- // Many of these styles, however, don't work well with CodeMirror 6.
+ // Allows legacy CM5 CSS to apply to the editor:
+ EditorView.editorAttributes.of({ class: 'CodeMirror' }),
];
}
@@ -316,6 +317,8 @@ export default class CodeMirror5Emulation extends BaseCodeMirror5Emulation {
const oldValue = this._options[name].value;
this._options[name].value = value;
this._options[name].onUpdate(this, value, oldValue);
+ } else if (this._builtInOptions.supportsOption(name)) {
+ this._builtInOptions.setOption(name, value);
} else {
super.setOption(name, value);
}
@@ -329,6 +332,20 @@ export default class CodeMirror5Emulation extends BaseCodeMirror5Emulation {
}
}
+ public override coordsChar(coords: { left: number; top: number }, mode?: 'div' | 'local'): DocumentPosition {
+ // codemirror-vim's API only supports "div" mode. Thus, we convert
+ // local to div:
+ if (mode !== 'div') {
+ const bbox = this.editor.contentDOM.getBoundingClientRect();
+ coords = {
+ left: coords.left - bbox.left,
+ top: coords.top - bbox.top,
+ };
+ }
+
+ return super.coordsChar(coords, 'div');
+ }
+
// codemirror-vim's API doesn't match the API docs here -- it expects addOverlay
// to return a SearchQuery. As such, this override returns "any".
public override addOverlay(modeObject: OverlayType): any {
@@ -353,7 +370,26 @@ export default class CodeMirror5Emulation extends BaseCodeMirror5Emulation {
}
public addLineWidget(lineNumber: number, node: HTMLElement, options: LineWidgetOptions) {
- this._decorator.addLineWidget(lineNumber, node, options);
+ return this._decorator.addLineWidget(lineNumber, node, options);
+ }
+
+ public addWidget(pos: DocumentPosition, node: HTMLElement) {
+ if (node.parentElement) {
+ node.remove();
+ }
+
+ const loc = posFromDocumentPosition(this.editor.state.doc, pos);
+ const screenCoords = this.editor.coordsAtPos(loc);
+ const bbox = this.editor.contentDOM.getBoundingClientRect();
+
+ node.style.position = 'absolute';
+
+ const left = screenCoords.left - bbox.left;
+ node.style.left = `${left}px`;
+ node.style.maxWidth = `${bbox.width - left}px`;
+ node.style.top = `${screenCoords.top + this.editor.scrollDOM.scrollTop}px`;
+
+ this.editor.scrollDOM.appendChild(node);
}
public markText(from: DocumentPosition, to: DocumentPosition, options?: MarkTextOptions) {
diff --git a/packages/editor/CodeMirror/CodeMirror5Emulation/Decorator.ts b/packages/editor/CodeMirror/CodeMirror5Emulation/Decorator.ts
index c5185b2fb..cd5d111e3 100644
--- a/packages/editor/CodeMirror/CodeMirror5Emulation/Decorator.ts
+++ b/packages/editor/CodeMirror/CodeMirror5Emulation/Decorator.ts
@@ -68,6 +68,9 @@ class WidgetDecorationWrapper extends WidgetType {
container.classList.add(this.options.className);
}
+ // Applies margins and related CSS:
+ container.classList.add('cm-line');
+
return container;
}
}
diff --git a/packages/editor/CodeMirror/markdown/decoratorExtension.ts b/packages/editor/CodeMirror/markdown/decoratorExtension.ts
index 9eaee4686..a9627f79a 100644
--- a/packages/editor/CodeMirror/markdown/decoratorExtension.ts
+++ b/packages/editor/CodeMirror/markdown/decoratorExtension.ts
@@ -49,27 +49,27 @@ const blockQuoteDecoration = Decoration.line({
});
const header1LineDecoration = Decoration.line({
- attributes: { class: 'cm-h1 cm-headerLine' },
+ attributes: { class: 'cm-h1 cm-headerLine cm-header' },
});
const header2LineDecoration = Decoration.line({
- attributes: { class: 'cm-h2 cm-headerLine' },
+ attributes: { class: 'cm-h2 cm-headerLine cm-header' },
});
const header3LineDecoration = Decoration.line({
- attributes: { class: 'cm-h3 cm-headerLine' },
+ attributes: { class: 'cm-h3 cm-headerLine cm-header' },
});
const header4LineDecoration = Decoration.line({
- attributes: { class: 'cm-h4 cm-headerLine' },
+ attributes: { class: 'cm-h4 cm-headerLine cm-header' },
});
const header5LineDecoration = Decoration.line({
- attributes: { class: 'cm-h5 cm-headerLine' },
+ attributes: { class: 'cm-h5 cm-headerLine cm-header' },
});
const header6LineDecoration = Decoration.line({
- attributes: { class: 'cm-h6 cm-headerLine' },
+ attributes: { class: 'cm-h6 cm-headerLine cm-header' },
});
const tableHeaderDecoration = Decoration.line({
diff --git a/packages/editor/CodeMirror/pluginApi/PluginLoader.ts b/packages/editor/CodeMirror/pluginApi/PluginLoader.ts
index 97ac246ab..d821997ec 100644
--- a/packages/editor/CodeMirror/pluginApi/PluginLoader.ts
+++ b/packages/editor/CodeMirror/pluginApi/PluginLoader.ts
@@ -132,7 +132,7 @@ export default class PluginLoader {
pluginId: plugin.pluginId,
contentScriptId: plugin.contentScriptId,
};
- const loadedPlugin = exports.default(context);
+ const loadedPlugin = exports.default(context) ?? {};
loadedPlugin.plugin?.(this.editor);
diff --git a/packages/editor/CodeMirror/theme.ts b/packages/editor/CodeMirror/theme.ts
index c80851c80..fed60baea 100644
--- a/packages/editor/CodeMirror/theme.ts
+++ b/packages/editor/CodeMirror/theme.ts
@@ -85,10 +85,12 @@ const createTheme = (theme: EditorTheme): Extension[] => {
};
const codeMirrorTheme = EditorView.theme({
- '&': baseGlobalStyle,
+ // Include &.CodeMirror to handle the case where additional CodeMirror 5 styles
+ // need to be overridden.
+ '&, &.CodeMirror': baseGlobalStyle,
// These must be !important or more specific than CodeMirror's built-ins
- '.cm-content': {
+ '& .cm-content': {
fontFamily: theme.fontFamily,
...baseContentStyle,
paddingBottom: theme.isDesktop ? '400px' : undefined,
diff --git a/packages/tools/cspell/dictionary4.txt b/packages/tools/cspell/dictionary4.txt
index f9f2b1ac0..0b649d1f6 100644
--- a/packages/tools/cspell/dictionary4.txt
+++ b/packages/tools/cspell/dictionary4.txt
@@ -91,4 +91,5 @@ activatable
titlewrapper
notyf
Notyf
-Prec
\ No newline at end of file
+activeline
+Prec