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