From 509da44c9577672501cfe5cdc34b2db393a534d2 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Mon, 12 Feb 2024 11:56:37 -0800 Subject: [PATCH 01/10] Desktop: Beta editor: Fix search results not highlighted --- .eslintignore | 3 +- .gitignore | 3 +- ...rSearch.ts => useEditorSearchExtension.ts} | 14 ++- .../utils/useEditorSearchHandler.ts | 66 +++++++++++++ .../NoteBody/CodeMirror/v5/CodeMirror.tsx | 47 ++------- .../NoteBody/CodeMirror/v5/Editor.tsx | 2 +- .../NoteBody/CodeMirror/v6/CodeMirror.tsx | 10 ++ .../NoteBody/CodeMirror/v6/Editor.tsx | 23 +++++ .../CodeMirror5Emulation.ts | 65 ++++++++++++- .../CodeMirror5Emulation/Decorator.ts | 97 +++++++++++++++---- .../editor/CodeMirror/CodeMirrorControl.ts | 15 ++- 11 files changed, 276 insertions(+), 69 deletions(-) rename packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/{useEditorSearch.ts => useEditorSearchExtension.ts} (91%) create mode 100644 packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useEditorSearchHandler.ts diff --git a/.eslintignore b/.eslintignore index 6448ee750db..07b913d1621 100644 --- a/.eslintignore +++ b/.eslintignore @@ -251,7 +251,8 @@ packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/types.js packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useContextMenu.js packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useCursorUtils.test.js packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useCursorUtils.js -packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useEditorSearch.js +packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useEditorSearchExtension.js +packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useEditorSearchHandler.js packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useExternalPlugins.js packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useJoplinCommands.js packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useJoplinMode.js diff --git a/.gitignore b/.gitignore index a44e69d2517..878e25b6eb3 100644 --- a/.gitignore +++ b/.gitignore @@ -231,7 +231,8 @@ packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/types.js packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useContextMenu.js packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useCursorUtils.test.js packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useCursorUtils.js -packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useEditorSearch.js +packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useEditorSearchExtension.js +packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useEditorSearchHandler.js packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useExternalPlugins.js packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useJoplinCommands.js packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useJoplinMode.js diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useEditorSearch.ts b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useEditorSearchExtension.ts similarity index 91% rename from packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useEditorSearch.ts rename to packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useEditorSearchExtension.ts index 5ea54beb6a6..8e0ebd8879c 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useEditorSearch.ts +++ b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useEditorSearchExtension.ts @@ -1,10 +1,14 @@ import { useEffect, useRef, useState } from 'react'; import shim from '@joplin/lib/shim'; import Logger from '@joplin/utils/Logger'; +import CodeMirror5Emulation from '@joplin/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation'; const logger = Logger.create('useEditorSearch'); -export default function useEditorSearch(CodeMirror: any) { +// Registers a helper CodeMirror extension to be used with +// useEditorSearchHandler. + +export default function useEditorSearchExtension(CodeMirror: CodeMirror5Emulation) { const [markers, setMarkers] = useState([]); const [overlay, setOverlay] = useState(null); @@ -73,7 +77,7 @@ export default function useEditorSearch(CodeMirror: any) { // If we run out of matches then just highlight the final match break; } - match = cursor.pos; + match = { from: cursor.from(), to: cursor.to() }; } if (match) { @@ -81,7 +85,7 @@ export default function useEditorSearch(CodeMirror: any) { if (withSelection) { cm.setSelection(match.from, match.to); } else { - cm.scrollTo(match); + cm.scrollIntoView(match); } } return cm.markText(match.from, match.to, { className: 'cm-search-marker-selected' }); @@ -107,7 +111,7 @@ export default function useEditorSearch(CodeMirror: any) { }; }, []); - CodeMirror.defineExtension('setMarkers', function(keywords: any, options: any) { + CodeMirror?.defineExtension('setMarkers', function(keywords: any, options: any) { if (!options) { options = { selectedIndex: 0, searchTimestamp: 0 }; } @@ -172,7 +176,7 @@ export default function useEditorSearch(CodeMirror: any) { // These operations are pretty slow, so we won't add use them until the user // has finished typing, 500ms is probably enough time const timeout = shim.setTimeout(() => { - const scrollMarks = this.showMatchesOnScrollbar(searchTerm, true, 'cm-search-marker-scrollbar'); + const scrollMarks = this.showMatchesOnScrollbar?.(searchTerm, true, 'cm-search-marker-scrollbar'); const overlay = searchOverlay(searchTerm); this.addOverlay(overlay); setOverlay(overlay); diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useEditorSearchHandler.ts b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useEditorSearchHandler.ts new file mode 100644 index 00000000000..d88aa0c49ff --- /dev/null +++ b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useEditorSearchHandler.ts @@ -0,0 +1,66 @@ +import { RefObject, useEffect } from 'react'; +import usePrevious from '../../../../hooks/usePrevious'; +import { RenderedBody } from './types'; +const debounce = require('debounce'); + +interface Props { + setLocalSearchResultCount(count: number): void; + searchMarkers: any; + webviewRef: RefObject; + editorRef: RefObject; + + noteContent: string; + renderedBody: RenderedBody; +} + +const useEditorSearchHandler = (props: Props) => { + const { webviewRef, editorRef, renderedBody, noteContent, searchMarkers } = props; + + const previousContent = usePrevious(noteContent); + const previousRenderedBody = usePrevious(renderedBody); + const previousSearchMarkers = usePrevious(searchMarkers); + + useEffect(() => { + if (!searchMarkers) return () => {}; + + // If there is a currently active search, it's important to re-search the text as the user + // types. However this is slow for performance so we ONLY want it to happen when there is + // a search + + // Note that since the CodeMirror component also needs to handle the viewer pane, we need + // to check if the rendered body has changed too (it will be changed with a delay after + // props.content has been updated). + const textChanged = searchMarkers.keywords.length > 0 && (noteContent !== previousContent || renderedBody !== previousRenderedBody); + + if (webviewRef.current && (searchMarkers !== previousSearchMarkers || textChanged)) { + webviewRef.current.send('setMarkers', searchMarkers.keywords, searchMarkers.options); + + if (editorRef.current) { + // Fixes https://github.com/laurent22/joplin/issues/7565 + const debouncedMarkers = debounce(() => { + const matches = editorRef.current.setMarkers(searchMarkers.keywords, searchMarkers.options); + + props.setLocalSearchResultCount(matches); + }, 50); + debouncedMarkers(); + return () => { + debouncedMarkers.clear(); + }; + } + } + return () => {}; + }, [ + editorRef, + webviewRef, + searchMarkers, + previousSearchMarkers, + props.setLocalSearchResultCount, + noteContent, + previousContent, + previousRenderedBody, + renderedBody, + ]); + +}; + +export default useEditorSearchHandler; diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/CodeMirror.tsx b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/CodeMirror.tsx index 9174523c06a..68b044ba5dc 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/CodeMirror.tsx +++ b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/CodeMirror.tsx @@ -6,7 +6,7 @@ import { EditorCommand, MarkupToHtmlOptions, NoteBodyEditorProps, NoteBodyEditor import { commandAttachFileToBody, getResourcesFromPasteEvent } from '../../../utils/resourceHandling'; import { ScrollOptions, ScrollOptionTypes } from '../../../utils/types'; import { CommandValue } from '../../../utils/types'; -import { usePrevious, cursorPositionToTextOffset } from '../utils'; +import { cursorPositionToTextOffset } from '../utils'; import useScrollHandler from '../utils/useScrollHandler'; import useElementSize from '@joplin/lib/hooks/useElementSize'; import Toolbar from '../Toolbar'; @@ -25,13 +25,13 @@ import { ThemeAppearance } from '@joplin/lib/themes/type'; import dialogs from '../../../../dialogs'; import { MarkupToHtml } from '@joplin/renderer'; const { clipboard } = require('electron'); -const debounce = require('debounce'); import { reg } from '@joplin/lib/registry'; import ErrorBoundary from '../../../../ErrorBoundary'; import useStyles from '../utils/useStyles'; import useContextMenu from '../utils/useContextMenu'; import useWebviewIpcMessage from '../utils/useWebviewIpcMessage'; +import useEditorSearchHandler from '../utils/useEditorSearchHandler'; function markupRenderOptions(override: MarkupToHtmlOptions = null): MarkupToHtmlOptions { return { ...override }; @@ -45,10 +45,6 @@ function CodeMirror(props: NoteBodyEditorProps, ref: ForwardedRef { - if (!props.searchMarkers) return () => {}; - - // If there is a currently active search, it's important to re-search the text as the user - // types. However this is slow for performance so we ONLY want it to happen when there is - // a search - - // Note that since the CodeMirror component also needs to handle the viewer pane, we need - // to check if the rendered body has changed too (it will be changed with a delay after - // props.content has been updated). - const textChanged = props.searchMarkers.keywords.length > 0 && (props.content !== previousContent || renderedBody !== previousRenderedBody); - - if (webviewRef.current && (props.searchMarkers !== previousSearchMarkers || textChanged)) { - webviewRef.current.send('setMarkers', props.searchMarkers.keywords, props.searchMarkers.options); - - if (editorRef.current) { - // Fixes https://github.com/laurent22/joplin/issues/7565 - const debouncedMarkers = debounce(() => { - const matches = editorRef.current.setMarkers(props.searchMarkers.keywords, props.searchMarkers.options); - - props.setLocalSearchResultCount(matches); - }, 50); - debouncedMarkers(); - return () => { - debouncedMarkers.clear(); - }; - } - } - return () => {}; - // eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied - }, [props.searchMarkers, previousSearchMarkers, props.setLocalSearchResultCount, props.content, previousContent, renderedBody, previousRenderedBody, renderedBody]); + useEditorSearchHandler({ + setLocalSearchResultCount: props.setLocalSearchResultCount, + searchMarkers: props.searchMarkers, + webviewRef, + editorRef, + noteContent: props.content, + renderedBody, + }); const cellEditorStyle = useMemo(() => { const output = { ...styles.cellEditor }; 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 49ca42e9fdb..9f3bcd3602b 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/Editor.tsx +++ b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/Editor.tsx @@ -16,7 +16,7 @@ import useListIdent from '../utils/useListIdent'; import useScrollUtils from '../utils/useScrollUtils'; import useCursorUtils from '../utils/useCursorUtils'; import useLineSorting from '../utils/useLineSorting'; -import useEditorSearch from '../utils/useEditorSearch'; +import useEditorSearch from '../utils/useEditorSearchExtension'; import useJoplinMode from '../utils/useJoplinMode'; import useKeymap from '../utils/useKeymap'; import useExternalPlugins from '../utils/useExternalPlugins'; diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/CodeMirror.tsx b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/CodeMirror.tsx index 05625153e79..fe94491d944 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/CodeMirror.tsx +++ b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/CodeMirror.tsx @@ -26,6 +26,7 @@ import CodeMirrorControl from '@joplin/editor/CodeMirror/CodeMirrorControl'; import useContextMenu from '../utils/useContextMenu'; import useWebviewIpcMessage from '../utils/useWebviewIpcMessage'; import Toolbar from '../Toolbar'; +import useEditorSearchHandler from '../utils/useEditorSearchHandler'; const logger = Logger.create('CodeMirror6'); const logDebug = (message: string) => logger.debug(message); @@ -338,6 +339,15 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef) => { onLogMessageRef.current = props.onLogMessage; }, [props.onEvent, props.onLogMessage]); + useEditorSearch(editor); + useEffect(() => { if (!editor) { return () => {}; @@ -104,6 +107,26 @@ const Editor = (props: Props, ref: ForwardedRef) => { // eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Should run just once }, []); + const theme = props.settings.themeData; + useEffect(() => { + if (!editor) return () => {}; + + const styles = editor.addStyles({ + '& .cm-search-marker *, & .cm-search-marker': { + color: theme.searchMarkerColor, + backgroundColor: theme.searchMarkerBackgroundColor, + }, + '& .cm-search-marker-selected *, & .cm-search-marker-selected': { + background: `${theme.selectedColor2} !important`, + color: `${theme.color2} !important`, + }, + }); + + return () => { + styles.remove(); + }; + }, [editor, theme]); + useEffect(() => { editor?.updateSettings(props.settings); }, [props.settings, editor]); diff --git a/packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.ts b/packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.ts index af97a5ad0fc..0d28d9a9fee 100644 --- a/packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.ts +++ b/packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.ts @@ -6,7 +6,7 @@ import { LogMessageCallback } from '../../types'; import editorCommands from '../editorCommands/editorCommands'; import { StateEffect } from '@codemirror/state'; import { StreamParser } from '@codemirror/language'; -import Decorator, { LineWidgetOptions } from './Decorator'; +import Decorator, { LineWidgetOptions, MarkTextOptions } from './Decorator'; import insertLineAfter from '../editorCommands/insertLineAfter'; const { pregQuote } = require('@joplin/lib/string-utils-common'); @@ -16,6 +16,8 @@ type CodeMirror5Command = (codeMirror: CodeMirror5Emulation)=> void; type EditorEventCallback = (editor: CodeMirror5Emulation, ...args: any[])=> void; type OptionUpdateCallback = (editor: CodeMirror5Emulation, newVal: any, oldVal: any)=> void; +type OverlayType = StreamParser|{ query: RegExp }; + interface CodeMirror5OptionRecord { onUpdate: OptionUpdateCallback; value: any; @@ -26,6 +28,11 @@ interface DocumentPosition { ch: number; } +interface DocumentPositionRange { + from: DocumentPosition; + to: DocumentPosition; +} + const documentPositionFromPos = (doc: Text, pos: number): DocumentPosition => { const line = doc.lineAt(pos); return { @@ -35,6 +42,11 @@ const documentPositionFromPos = (doc: Text, pos: number): DocumentPosition => { }; }; +const posFromDocumentPosition = (doc: Text, pos: DocumentPosition) => { + const line = doc.line(pos.line + 1); + return line.from + pos.ch; +}; + export default class CodeMirror5Emulation extends BaseCodeMirror5Emulation { private _events: Record = {}; private _options: Record = Object.create(null); @@ -214,7 +226,20 @@ export default class CodeMirror5Emulation extends BaseCodeMirror5Emulation { if (typeof query === 'string') { query = new RegExp(pregQuote(query)); } - return super.getSearchCursor(query, pos || { line: 0, ch: 0 }); + const result = super.getSearchCursor(query, pos || { line: 0, ch: 0 }); + + return { + // .pos isn't implemented by CM6 Vim, but is still used by + // some Joplin code. + get pos() { + return { + from: result.from(), + to: result.to(), + }; + }, + + ...result, + }; } public lineAtHeight(height: number, _mode?: 'local') { @@ -271,8 +296,23 @@ export default class CodeMirror5Emulation extends BaseCodeMirror5Emulation { return getScrollFraction(this.editor); } + // CodeMirror-Vim's scrollIntoView only supports pos as a DocumentPosition. + public override scrollIntoView( + pos: DocumentPosition|DocumentPositionRange, margin?: number, + ): void { + const isPosition = (arg: unknown): arg is DocumentPosition => { + return (arg as any).line !== undefined && (arg as any).ch !== undefined; + }; + + if (isPosition(pos)) { + return super.scrollIntoView(pos, margin); + } else { + return super.scrollIntoView(pos.from, margin); + } + } + public defineExtension(name: string, value: any) { - (CodeMirror5Emulation.prototype as any)[name] ??= value; + (CodeMirror5Emulation.prototype as any)[name] = value; } public defineOption(name: string, defaultValue: any, onUpdate: OptionUpdateCallback) { @@ -304,12 +344,17 @@ export default class CodeMirror5Emulation extends BaseCodeMirror5Emulation { // 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: StreamParser|{ query: RegExp }): any { + public override addOverlay(modeObject: OverlayType): any { if ('query' in modeObject) { return super.addOverlay(modeObject); } - this._decorator.addOverlay(modeObject); + return this._decorator.addOverlay(modeObject); + } + + public override removeOverlay(overlay?: OverlayType): void { + super.removeOverlay(overlay); + this._decorator.removeOverlay(overlay); } public addLineClass(lineNumber: number, where: string, className: string) { @@ -324,6 +369,16 @@ export default class CodeMirror5Emulation extends BaseCodeMirror5Emulation { this._decorator.addLineWidget(lineNumber, node, options); } + public markText(from: DocumentPosition, to: DocumentPosition, options?: MarkTextOptions) { + const doc = this.editor.state.doc; + + return this._decorator.markText( + posFromDocumentPosition(doc, from), + posFromDocumentPosition(doc, to), + options, + ); + } + // TODO: Currently copied from useCursorUtils.ts. // TODO: Remove the duplicate code when CodeMirror 5 is eventually removed. public wrapSelections(string1: string, string2: string) { diff --git a/packages/editor/CodeMirror/CodeMirror5Emulation/Decorator.ts b/packages/editor/CodeMirror/CodeMirror5Emulation/Decorator.ts index 6b9f93ca6d9..66d6cacb3b7 100644 --- a/packages/editor/CodeMirror/CodeMirror5Emulation/Decorator.ts +++ b/packages/editor/CodeMirror/CodeMirror5Emulation/Decorator.ts @@ -24,15 +24,16 @@ const mapRangeConfig = { }, }; -interface LineCssDecorationSpec extends DecorationRange { +interface CssDecorationSpec extends DecorationRange { cssClass: string; + id?: number; } -const addLineDecorationEffect = StateEffect.define(mapRangeConfig); -const removeLineDecorationEffect = StateEffect.define(mapRangeConfig); -const addMarkDecorationEffect = StateEffect.define(mapRangeConfig); -// TODO: Support removing mark decorations -// const removeMarkDecorationEffect = StateEffect.define(mapRangeConfig); +const addLineDecorationEffect = StateEffect.define(mapRangeConfig); +const removeLineDecorationEffect = StateEffect.define(mapRangeConfig); +const addMarkDecorationEffect = StateEffect.define(mapRangeConfig); +const removeMarkDecorationEffect = StateEffect.define(mapRangeConfig); +const refreshOverlaysEffect = StateEffect.define(); export interface LineWidgetOptions { className?: string; @@ -46,6 +47,10 @@ interface LineWidgetDecorationSpec extends DecorationRange { const addLineWidgetEffect = StateEffect.define(mapRangeConfig); const removeLineWidgetEffect = StateEffect.define<{ element: HTMLElement }>(); +export interface MarkTextOptions { + className: string; + id: number; +} class WidgetDecorationWrapper extends WidgetType { public constructor( @@ -78,6 +83,7 @@ interface LineWidgetControl { export default class Decorator { private _extension: Extension; private _effectDecorations: DecorationSet = Decoration.none; + private _nextLineWidgetId = 0; private constructor(private editor: EditorView) { const decorator = this; @@ -93,8 +99,24 @@ export default class Decorator { } public update(update: ViewUpdate) { - if (update.viewportChanged || update.docChanged) { + const updated = false; + const doUpdate = () => { + if (updated) return; + this.decorations = decorator.createOverlayDecorations(update.view); + }; + + if (update.viewportChanged || update.docChanged) { + doUpdate(); + } else { + for (const transaction of update.transactions) { + for (const effect of transaction.effects) { + if (effect.is(refreshOverlaysEffect)) { + doUpdate(); + break; + } + } + } } } }, { @@ -120,18 +142,18 @@ export default class Decorator { private _decorationCache: Record = Object.create(null); private _overlays: (StreamParser)[] = []; - private classNameToCssDecoration(className: string, isLineDecoration: boolean) { + private classNameToCssDecoration(className: string, isLineDecoration: boolean, id?: number) { let decoration; - if (className in this._decorationCache) { + if (className in this._decorationCache && id === undefined) { decoration = this._decorationCache[className]; } else { const attributes = { class: className }; if (isLineDecoration) { - decoration = Decoration.line({ attributes }); + decoration = Decoration.line({ attributes, id }); } else { - decoration = Decoration.mark({ attributes }); + decoration = Decoration.mark({ attributes, id }); } this._decorationCache[className] = decoration; @@ -153,7 +175,7 @@ export default class Decorator { const isLineDecoration = effect.is(addLineDecorationEffect); if (isMarkDecoration || isLineDecoration) { const decoration = this.classNameToCssDecoration( - effect.value.cssClass, isLineDecoration, + effect.value.cssClass, isLineDecoration, effect.value.id, ); const value = effect.value; @@ -165,21 +187,25 @@ export default class Decorator { decorations = decorations.update({ add: [decoration.range(from, to)], }); - } else if (effect.is(removeLineDecorationEffect)) { + } else if (effect.is(removeLineDecorationEffect) || effect.is(removeMarkDecorationEffect)) { const doc = transaction.state.doc; const targetFrom = doc.lineAt(effect.value.from).from; const targetTo = doc.lineAt(effect.value.to).to; - const targetDecoration = this.classNameToCssDecoration(effect.value.cssClass, true); + const targetId = effect.value.id; + const targetDecoration = this.classNameToCssDecoration( + effect.value.cssClass, effect.is(removeLineDecorationEffect), + ); decorations = decorations.update({ // Returns true only for decorations that should be kept. filter: (from, to, value) => { - if (from >= targetFrom && to <= targetTo && value.eq(targetDecoration)) { - return false; + if (targetId !== undefined) { + return value.spec.id === effect.value.id; } - return true; + const isInRange = from >= targetFrom && to <= targetTo; + return isInRange && value.eq(targetDecoration); }, }); } else if (effect.is(addLineWidgetEffect)) { @@ -296,6 +322,22 @@ export default class Decorator { public addOverlay(modeObject: StreamParser) { this._overlays.push(modeObject); + + this.editor.dispatch({ + effects: [refreshOverlaysEffect.of(null)], + }); + + return { + clear: () => this.removeOverlay(modeObject), + }; + } + + public removeOverlay(overlay: any) { + this._overlays = this._overlays.filter(other => other !== overlay); + + this.editor.dispatch({ + effects: [refreshOverlaysEffect.of(null)], + }); } private addRemoveLineClass(lineNumber: number, className: string, add: boolean) { @@ -336,6 +378,27 @@ export default class Decorator { return lineClasses; } + public markText(from: number, to: number, options?: MarkTextOptions) { + const effectOptions: CssDecorationSpec = { + cssClass: options.className ?? '', + id: this._nextLineWidgetId++, + from, + to, + }; + + this.editor.dispatch({ + effects: addMarkDecorationEffect.of(effectOptions), + }); + + return { + clear: () => { + this.editor.dispatch({ + effects: removeMarkDecorationEffect.of(effectOptions), + }); + }, + }; + } + private createLineWidgetControl(node: HTMLElement, options: LineWidgetOptions): LineWidgetControl { return { node, diff --git a/packages/editor/CodeMirror/CodeMirrorControl.ts b/packages/editor/CodeMirror/CodeMirrorControl.ts index 6e39383a2e5..2807c8148cc 100644 --- a/packages/editor/CodeMirror/CodeMirrorControl.ts +++ b/packages/editor/CodeMirror/CodeMirrorControl.ts @@ -2,7 +2,7 @@ import { EditorView } from '@codemirror/view'; import { EditorCommandType, EditorControl, EditorSettings, LogMessageCallback, PluginData, SearchState } from '../types'; import CodeMirror5Emulation from './CodeMirror5Emulation/CodeMirror5Emulation'; import editorCommands from './editorCommands/editorCommands'; -import { EditorSelection, Extension, StateEffect } from '@codemirror/state'; +import { Compartment, EditorSelection, Extension, StateEffect } from '@codemirror/state'; import { updateLink } from './markdown/markdownCommands'; import { SearchQuery, setSearchQuery } from '@codemirror/search'; import PluginLoader from './pluginApi/PluginLoader'; @@ -121,9 +121,20 @@ export default class CodeMirrorControl extends CodeMirror5Emulation implements E } public addStyles(...styles: Parameters) { + const compartment = new Compartment(); this.editor.dispatch({ - effects: StateEffect.appendConfig.of(EditorView.theme(...styles)), + effects: StateEffect.appendConfig.of( + compartment.of(EditorView.theme(...styles)), + ), }); + + return { + remove: () => { + this.editor.dispatch({ + effects: compartment.reconfigure([]), + }); + }, + }; } public setPlugins(plugins: PluginData[]) { From b36ff9e9f4d12852ae8f0f9fd6f06e06db448130 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Mon, 12 Feb 2024 12:51:36 -0800 Subject: [PATCH 02/10] Add test --- .../CodeMirror5Emulation.test.ts | 35 +++++++++++++++++++ .../CodeMirror5Emulation/Decorator.ts | 3 +- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.test.ts b/packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.test.ts index c5ab4609a16..5b4d0ae0235 100644 --- a/packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.test.ts +++ b/packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.test.ts @@ -108,4 +108,39 @@ describe('CodeMirror5Emulation', () => { // additional times if its option hasn't updated. expect(onOtherOptionUpdate).toHaveBeenCalledTimes(1); }); + + it('markText decorations should be removable', () => { + const codeMirror = makeCodeMirrorEmulation('Test 1\nTest 2'); + + const markDecoration = codeMirror.markText( + { line: 0, ch: 0 }, + { line: 0, ch: 6 }, + { className: 'test-mark-decoration' }, + ); + + const markDecoration2 = codeMirror.markText( + { line: 1, ch: 0 }, + { line: 1, ch: 1 }, + { className: 'test-decoration-2' }, + ); + + const editorDom = codeMirror.cm6.dom; + expect(editorDom.querySelectorAll('.test-mark-decoration')).toHaveLength(1); + expect(editorDom.querySelectorAll('.test-decoration-2')).toHaveLength(1); + + codeMirror.setCursor(0, 2); + codeMirror.replaceSelection('Test'); + + // Editing the document shouldn't remove the mark + expect(codeMirror.editor.state.doc.toString()).toBe('TeTestst 1\nTest 2'); + expect(editorDom.querySelectorAll('.test-mark-decoration')).toHaveLength(1); + + // Clearing should remove only the decoration that was cleared. + markDecoration.clear(); + expect(editorDom.querySelectorAll('.test-mark-decoration')).toHaveLength(0); + expect(editorDom.querySelectorAll('.test-decoration-2')).toHaveLength(1); + + markDecoration2.clear(); + expect(editorDom.querySelectorAll('.test-decoration-2')).toHaveLength(0); + }); }); diff --git a/packages/editor/CodeMirror/CodeMirror5Emulation/Decorator.ts b/packages/editor/CodeMirror/CodeMirror5Emulation/Decorator.ts index 66d6cacb3b7..c5185b2fb65 100644 --- a/packages/editor/CodeMirror/CodeMirror5Emulation/Decorator.ts +++ b/packages/editor/CodeMirror/CodeMirror5Emulation/Decorator.ts @@ -49,7 +49,6 @@ const removeLineWidgetEffect = StateEffect.define<{ element: HTMLElement }>(); export interface MarkTextOptions { className: string; - id: number; } class WidgetDecorationWrapper extends WidgetType { @@ -201,7 +200,7 @@ export default class Decorator { // Returns true only for decorations that should be kept. filter: (from, to, value) => { if (targetId !== undefined) { - return value.spec.id === effect.value.id; + return value.spec.id !== effect.value.id; } const isInRange = from >= targetFrom && to <= targetTo; From e162f03045d6e24b4da67de24bb18e99bc651c35 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Mon, 12 Feb 2024 13:31:21 -0800 Subject: [PATCH 03/10] Revert unnecessary change --- .../CodeMirror5Emulation/CodeMirror5Emulation.ts | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.ts b/packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.ts index 0d28d9a9fee..93ce545dcb9 100644 --- a/packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.ts +++ b/packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.ts @@ -226,20 +226,7 @@ export default class CodeMirror5Emulation extends BaseCodeMirror5Emulation { if (typeof query === 'string') { query = new RegExp(pregQuote(query)); } - const result = super.getSearchCursor(query, pos || { line: 0, ch: 0 }); - - return { - // .pos isn't implemented by CM6 Vim, but is still used by - // some Joplin code. - get pos() { - return { - from: result.from(), - to: result.to(), - }; - }, - - ...result, - }; + return super.getSearchCursor(query, pos || { line: 0, ch: 0 }); } public lineAtHeight(height: number, _mode?: 'local') { From af9ebbc154a926fe9d39104276989e1fd5ac02a0 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Tue, 13 Feb 2024 19:37:40 -0800 Subject: [PATCH 04/10] Desktop: Support rich markdown plugin in the beta editor with minimal changes --- .eslintignore | 1 + .gitignore | 1 + packages/app-desktop/app.ts | 2 +- .../NoteBody/CodeMirror/v6/Editor.tsx | 6 ++ .../CodeMirror5BuiltInOptions.ts | 83 +++++++++++++++++++ .../CodeMirror5Emulation.ts | 44 +++++++++- .../CodeMirror/markdown/decoratorExtension.ts | 18 ++-- .../CodeMirror/pluginApi/PluginLoader.ts | 4 +- packages/editor/CodeMirror/theme.ts | 6 +- 9 files changed, 150 insertions(+), 15 deletions(-) create mode 100644 packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5BuiltInOptions.ts diff --git a/.eslintignore b/.eslintignore index 07b913d1621..bbe6c48be5b 100644 --- a/.eslintignore +++ b/.eslintignore @@ -575,6 +575,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 878e25b6eb3..acf4cb0a853 100644 --- a/.gitignore +++ b/.gitignore @@ -555,6 +555,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 2c2c621b286..9eea48e3165 100644 --- a/packages/app-desktop/app.ts +++ b/packages/app-desktop/app.ts @@ -202,7 +202,7 @@ 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 - const css = `.CodeMirror *, .cm-editor .cm-content { font-family: ${fontFamilies.join(', ')} !important; }`; + const css = `.CodeMirror:not(.cm-editor) *, .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/v6/Editor.tsx b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/Editor.tsx index fe653279de0..e7df2f10550 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 00000000000..15d32e2315f --- /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 93ce545dcb9..a88821ce2fd 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 { @@ -366,6 +383,25 @@ export default class CodeMirror5Emulation extends BaseCodeMirror5Emulation { ); } + 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); + } + // TODO: Currently copied from useCursorUtils.ts. // TODO: Remove the duplicate code when CodeMirror 5 is eventually removed. public wrapSelections(string1: string, string2: string) { diff --git a/packages/editor/CodeMirror/markdown/decoratorExtension.ts b/packages/editor/CodeMirror/markdown/decoratorExtension.ts index ef97fc4c645..5dccc551161 100644 --- a/packages/editor/CodeMirror/markdown/decoratorExtension.ts +++ b/packages/editor/CodeMirror/markdown/decoratorExtension.ts @@ -10,6 +10,8 @@ import { ViewPlugin, DecorationSet, ViewUpdate } from '@codemirror/view'; import { ensureSyntaxTree } from '@codemirror/language'; import { RangeSetBuilder } from '@codemirror/state'; +const baseLineDecoration = Decoration.line({ class: 'CodeMirror-line' }); + const regionStartDecoration = Decoration.line({ attributes: { class: 'cm-regionFirstLine' }, }); @@ -49,27 +51,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({ @@ -192,6 +194,10 @@ const computeDecorations = (view: EditorView) => { } }, }); + + // For CodeMirror 5 compatibility, we add a CodeMirror-line decoration to all + // visible lines. + addDecorationToLines(from, to, baseLineDecoration); } // Decorations need to be sorted in ascending order first by start position, diff --git a/packages/editor/CodeMirror/pluginApi/PluginLoader.ts b/packages/editor/CodeMirror/pluginApi/PluginLoader.ts index 0daa67fdfb3..394589ce330 100644 --- a/packages/editor/CodeMirror/pluginApi/PluginLoader.ts +++ b/packages/editor/CodeMirror/pluginApi/PluginLoader.ts @@ -72,7 +72,7 @@ export default class PluginLoader { return; } - scriptElement.innerText = ` + scriptElement.appendChild(document.createTextNode(` (async () => { const exports = {}; const require = window.__pluginLoaderRequireFunctions[${JSON.stringify(this.pluginLoaderId)}]; @@ -84,7 +84,7 @@ export default class PluginLoader { window.__pluginLoaderScriptLoadCallbacks[${JSON.stringify(scriptId)}](exports); })(); - `; + `)); (window as any).__pluginLoaderScriptLoadCallbacks[scriptId] = onLoad; diff --git a/packages/editor/CodeMirror/theme.ts b/packages/editor/CodeMirror/theme.ts index c80851c8024..fed60baea9c 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, From 7a70dcf250171a0bba9f0b4ac5b4b1756ff3cbae Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Tue, 13 Feb 2024 20:26:59 -0800 Subject: [PATCH 05/10] Line widget fixes --- .../CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.ts | 2 +- packages/editor/CodeMirror/CodeMirror5Emulation/Decorator.ts | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.ts b/packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.ts index a88821ce2fd..88f87b5c49c 100644 --- a/packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.ts +++ b/packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.ts @@ -370,7 +370,7 @@ 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 markText(from: DocumentPosition, to: DocumentPosition, options?: MarkTextOptions) { diff --git a/packages/editor/CodeMirror/CodeMirror5Emulation/Decorator.ts b/packages/editor/CodeMirror/CodeMirror5Emulation/Decorator.ts index c5185b2fb65..cd5d111e389 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; } } From 19ade0bea6a326c7106adfe973156eb2a61b8fe4 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Tue, 13 Feb 2024 21:34:13 -0800 Subject: [PATCH 06/10] Fix table monospacing --- packages/app-desktop/app.ts | 4 +++- .../gui/NoteEditor/NoteBody/CodeMirror/v5/Editor.tsx | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/app-desktop/app.ts b/packages/app-desktop/app.ts index 9eea48e3165..7597d4a65f0 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 specifity here. Incorrect specificity can break monospaced fonts in tables. - const css = `.CodeMirror:not(.cm-editor) *, .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 9f3bcd3602b..063f16c604c 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); From 5ff7fff9c8698f3dc1ed33a0186bb386772ca29b Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Tue, 13 Feb 2024 21:51:33 -0800 Subject: [PATCH 07/10] Fix PluginLoader tests --- packages/editor/CodeMirror/pluginApi/PluginLoader.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor/CodeMirror/pluginApi/PluginLoader.ts b/packages/editor/CodeMirror/pluginApi/PluginLoader.ts index 394589ce330..dcd6b46176f 100644 --- a/packages/editor/CodeMirror/pluginApi/PluginLoader.ts +++ b/packages/editor/CodeMirror/pluginApi/PluginLoader.ts @@ -129,7 +129,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); From 262953ab6d8ac195eafa888872889c3d18c783a7 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Tue, 20 Feb 2024 12:01:32 -0800 Subject: [PATCH 08/10] Remove additional each-line class --- packages/editor/CodeMirror/markdown/decoratorExtension.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/editor/CodeMirror/markdown/decoratorExtension.ts b/packages/editor/CodeMirror/markdown/decoratorExtension.ts index 5dccc551161..8c29e91dd49 100644 --- a/packages/editor/CodeMirror/markdown/decoratorExtension.ts +++ b/packages/editor/CodeMirror/markdown/decoratorExtension.ts @@ -10,8 +10,6 @@ import { ViewPlugin, DecorationSet, ViewUpdate } from '@codemirror/view'; import { ensureSyntaxTree } from '@codemirror/language'; import { RangeSetBuilder } from '@codemirror/state'; -const baseLineDecoration = Decoration.line({ class: 'CodeMirror-line' }); - const regionStartDecoration = Decoration.line({ attributes: { class: 'cm-regionFirstLine' }, }); @@ -194,10 +192,6 @@ const computeDecorations = (view: EditorView) => { } }, }); - - // For CodeMirror 5 compatibility, we add a CodeMirror-line decoration to all - // visible lines. - addDecorationToLines(from, to, baseLineDecoration); } // Decorations need to be sorted in ascending order first by start position, From cac9983f3a59b026185d8ac3be421d6c8fe33099 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Tue, 5 Mar 2024 10:24:16 -0800 Subject: [PATCH 09/10] Spelling fixes --- packages/app-desktop/app.ts | 2 +- packages/tools/cspell/dictionary4.txt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/app-desktop/app.ts b/packages/app-desktop/app.ts index ca0cf952b4f..a75a9531591 100644 --- a/packages/app-desktop/app.ts +++ b/packages/app-desktop/app.ts @@ -202,7 +202,7 @@ 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 specifity here. Incorrect specificity can break monospaced fonts in tables. + // Note: Be careful about the specificity here. Incorrect specificity can break monospaced fonts in tables. const css = `.CodeMirror5 *, .cm-editor .cm-content { font-family: ${fontFamilies.join(', ')} !important; }`; const styleTag = document.createElement('style'); diff --git a/packages/tools/cspell/dictionary4.txt b/packages/tools/cspell/dictionary4.txt index b9e473a8720..c9729b2c035 100644 --- a/packages/tools/cspell/dictionary4.txt +++ b/packages/tools/cspell/dictionary4.txt @@ -91,3 +91,4 @@ activatable titlewrapper notyf Notyf +activeline From 8bd1bf934a7b98a34de6d9c7408d51013b18468c Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Fri, 8 Mar 2024 10:47:20 -0800 Subject: [PATCH 10/10] Fix function duplicated by merge --- .../CodeMirror5Emulation/CodeMirror5Emulation.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.ts b/packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.ts index e9096e003a3..9b1af2bf445 100644 --- a/packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.ts +++ b/packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.ts @@ -373,16 +373,6 @@ export default class CodeMirror5Emulation extends BaseCodeMirror5Emulation { return this._decorator.addLineWidget(lineNumber, node, options); } - public markText(from: DocumentPosition, to: DocumentPosition, options?: MarkTextOptions) { - const doc = this.editor.state.doc; - - return this._decorator.markText( - posFromDocumentPosition(doc, from), - posFromDocumentPosition(doc, to), - options, - ); - } - public addWidget(pos: DocumentPosition, node: HTMLElement) { if (node.parentElement) { node.remove();