You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-11-26 22:41:17 +02:00
This commit is contained in:
@@ -258,7 +258,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/useContextMenu.js
|
||||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useCursorUtils.test.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/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/useExternalPlugins.js
|
||||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useJoplinCommands.js
|
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useJoplinCommands.js
|
||||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useJoplinMode.js
|
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useJoplinMode.js
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -238,7 +238,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/useContextMenu.js
|
||||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useCursorUtils.test.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/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/useExternalPlugins.js
|
||||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useJoplinCommands.js
|
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useJoplinCommands.js
|
||||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useJoplinMode.js
|
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useJoplinMode.js
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import shim from '@joplin/lib/shim';
|
import shim from '@joplin/lib/shim';
|
||||||
import Logger from '@joplin/utils/Logger';
|
import Logger from '@joplin/utils/Logger';
|
||||||
|
import CodeMirror5Emulation from '@joplin/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation';
|
||||||
|
|
||||||
const logger = Logger.create('useEditorSearch');
|
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 [markers, setMarkers] = useState([]);
|
||||||
const [overlay, setOverlay] = useState(null);
|
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
|
// If we run out of matches then just highlight the final match
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
match = cursor.pos;
|
match = { from: cursor.from(), to: cursor.to() };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (match) {
|
if (match) {
|
||||||
@@ -81,7 +85,7 @@ export default function useEditorSearch(CodeMirror: any) {
|
|||||||
if (withSelection) {
|
if (withSelection) {
|
||||||
cm.setSelection(match.from, match.to);
|
cm.setSelection(match.from, match.to);
|
||||||
} else {
|
} else {
|
||||||
cm.scrollTo(match);
|
cm.scrollIntoView(match);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return cm.markText(match.from, match.to, { className: 'cm-search-marker-selected' });
|
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) {
|
if (!options) {
|
||||||
options = { selectedIndex: 0, searchTimestamp: 0 };
|
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
|
// These operations are pretty slow, so we won't add use them until the user
|
||||||
// has finished typing, 500ms is probably enough time
|
// has finished typing, 500ms is probably enough time
|
||||||
const timeout = shim.setTimeout(() => {
|
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);
|
const overlay = searchOverlay(searchTerm);
|
||||||
this.addOverlay(overlay);
|
this.addOverlay(overlay);
|
||||||
setOverlay(overlay);
|
setOverlay(overlay);
|
||||||
@@ -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<any>;
|
||||||
|
editorRef: RefObject<any>;
|
||||||
|
|
||||||
|
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;
|
||||||
@@ -6,7 +6,7 @@ import { EditorCommand, MarkupToHtmlOptions, NoteBodyEditorProps, NoteBodyEditor
|
|||||||
import { commandAttachFileToBody, getResourcesFromPasteEvent } from '../../../utils/resourceHandling';
|
import { commandAttachFileToBody, getResourcesFromPasteEvent } from '../../../utils/resourceHandling';
|
||||||
import { ScrollOptions, ScrollOptionTypes } from '../../../utils/types';
|
import { ScrollOptions, ScrollOptionTypes } from '../../../utils/types';
|
||||||
import { CommandValue } from '../../../utils/types';
|
import { CommandValue } from '../../../utils/types';
|
||||||
import { usePrevious, cursorPositionToTextOffset } from '../utils';
|
import { cursorPositionToTextOffset } from '../utils';
|
||||||
import useScrollHandler from '../utils/useScrollHandler';
|
import useScrollHandler from '../utils/useScrollHandler';
|
||||||
import useElementSize from '@joplin/lib/hooks/useElementSize';
|
import useElementSize from '@joplin/lib/hooks/useElementSize';
|
||||||
import Toolbar from '../Toolbar';
|
import Toolbar from '../Toolbar';
|
||||||
@@ -25,13 +25,13 @@ import { ThemeAppearance } from '@joplin/lib/themes/type';
|
|||||||
import dialogs from '../../../../dialogs';
|
import dialogs from '../../../../dialogs';
|
||||||
import { MarkupToHtml } from '@joplin/renderer';
|
import { MarkupToHtml } from '@joplin/renderer';
|
||||||
const { clipboard } = require('electron');
|
const { clipboard } = require('electron');
|
||||||
const debounce = require('debounce');
|
|
||||||
|
|
||||||
import { reg } from '@joplin/lib/registry';
|
import { reg } from '@joplin/lib/registry';
|
||||||
import ErrorBoundary from '../../../../ErrorBoundary';
|
import ErrorBoundary from '../../../../ErrorBoundary';
|
||||||
import useStyles from '../utils/useStyles';
|
import useStyles from '../utils/useStyles';
|
||||||
import useContextMenu from '../utils/useContextMenu';
|
import useContextMenu from '../utils/useContextMenu';
|
||||||
import useWebviewIpcMessage from '../utils/useWebviewIpcMessage';
|
import useWebviewIpcMessage from '../utils/useWebviewIpcMessage';
|
||||||
|
import useEditorSearchHandler from '../utils/useEditorSearchHandler';
|
||||||
|
|
||||||
function markupRenderOptions(override: MarkupToHtmlOptions = null): MarkupToHtmlOptions {
|
function markupRenderOptions(override: MarkupToHtmlOptions = null): MarkupToHtmlOptions {
|
||||||
return { ...override };
|
return { ...override };
|
||||||
@@ -45,10 +45,6 @@ function CodeMirror(props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
|||||||
|
|
||||||
const [webviewReady, setWebviewReady] = useState(false);
|
const [webviewReady, setWebviewReady] = useState(false);
|
||||||
|
|
||||||
const previousContent = usePrevious(props.content);
|
|
||||||
const previousRenderedBody = usePrevious(renderedBody);
|
|
||||||
const previousSearchMarkers = usePrevious(props.searchMarkers);
|
|
||||||
|
|
||||||
const editorRef = useRef(null);
|
const editorRef = useRef(null);
|
||||||
const rootRef = useRef(null);
|
const rootRef = useRef(null);
|
||||||
const webviewRef = useRef(null);
|
const webviewRef = useRef(null);
|
||||||
@@ -675,37 +671,14 @@ function CodeMirror(props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
|||||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||||
}, [renderedBody, webviewReady]);
|
}, [renderedBody, webviewReady]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEditorSearchHandler({
|
||||||
if (!props.searchMarkers) return () => {};
|
setLocalSearchResultCount: props.setLocalSearchResultCount,
|
||||||
|
searchMarkers: props.searchMarkers,
|
||||||
// If there is a currently active search, it's important to re-search the text as the user
|
webviewRef,
|
||||||
// types. However this is slow for performance so we ONLY want it to happen when there is
|
editorRef,
|
||||||
// a search
|
noteContent: props.content,
|
||||||
|
renderedBody,
|
||||||
// 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]);
|
|
||||||
|
|
||||||
const cellEditorStyle = useMemo(() => {
|
const cellEditorStyle = useMemo(() => {
|
||||||
const output = { ...styles.cellEditor };
|
const output = { ...styles.cellEditor };
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import useListIdent from '../utils/useListIdent';
|
|||||||
import useScrollUtils from '../utils/useScrollUtils';
|
import useScrollUtils from '../utils/useScrollUtils';
|
||||||
import useCursorUtils from '../utils/useCursorUtils';
|
import useCursorUtils from '../utils/useCursorUtils';
|
||||||
import useLineSorting from '../utils/useLineSorting';
|
import useLineSorting from '../utils/useLineSorting';
|
||||||
import useEditorSearch from '../utils/useEditorSearch';
|
import useEditorSearch from '../utils/useEditorSearchExtension';
|
||||||
import useJoplinMode from '../utils/useJoplinMode';
|
import useJoplinMode from '../utils/useJoplinMode';
|
||||||
import useKeymap from '../utils/useKeymap';
|
import useKeymap from '../utils/useKeymap';
|
||||||
import useExternalPlugins from '../utils/useExternalPlugins';
|
import useExternalPlugins from '../utils/useExternalPlugins';
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import CodeMirrorControl from '@joplin/editor/CodeMirror/CodeMirrorControl';
|
|||||||
import useContextMenu from '../utils/useContextMenu';
|
import useContextMenu from '../utils/useContextMenu';
|
||||||
import useWebviewIpcMessage from '../utils/useWebviewIpcMessage';
|
import useWebviewIpcMessage from '../utils/useWebviewIpcMessage';
|
||||||
import Toolbar from '../Toolbar';
|
import Toolbar from '../Toolbar';
|
||||||
|
import useEditorSearchHandler from '../utils/useEditorSearchHandler';
|
||||||
|
|
||||||
const logger = Logger.create('CodeMirror6');
|
const logger = Logger.create('CodeMirror6');
|
||||||
const logDebug = (message: string) => logger.debug(message);
|
const logDebug = (message: string) => logger.debug(message);
|
||||||
@@ -334,6 +335,15 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
|||||||
// }
|
// }
|
||||||
// }, [editorPaneVisible]);
|
// }, [editorPaneVisible]);
|
||||||
|
|
||||||
|
useEditorSearchHandler({
|
||||||
|
setLocalSearchResultCount: props.setLocalSearchResultCount,
|
||||||
|
searchMarkers: props.searchMarkers,
|
||||||
|
webviewRef,
|
||||||
|
editorRef,
|
||||||
|
noteContent: props.content,
|
||||||
|
renderedBody,
|
||||||
|
});
|
||||||
|
|
||||||
useContextMenu({
|
useContextMenu({
|
||||||
plugins: props.plugins,
|
plugins: props.plugins,
|
||||||
editorCutText, editorCopyText, editorPaste,
|
editorCutText, editorCopyText, editorPaste,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import shim from '@joplin/lib/shim';
|
|||||||
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||||
import setupVim from '@joplin/editor/CodeMirror/util/setupVim';
|
import setupVim from '@joplin/editor/CodeMirror/util/setupVim';
|
||||||
import { dirname } from 'path';
|
import { dirname } from 'path';
|
||||||
|
import useEditorSearch from '../utils/useEditorSearchExtension';
|
||||||
|
|
||||||
interface Props extends EditorProps {
|
interface Props extends EditorProps {
|
||||||
style: React.CSSProperties;
|
style: React.CSSProperties;
|
||||||
@@ -32,6 +33,8 @@ const Editor = (props: Props, ref: ForwardedRef<CodeMirrorControl>) => {
|
|||||||
onLogMessageRef.current = props.onLogMessage;
|
onLogMessageRef.current = props.onLogMessage;
|
||||||
}, [props.onEvent, props.onLogMessage]);
|
}, [props.onEvent, props.onLogMessage]);
|
||||||
|
|
||||||
|
useEditorSearch(editor);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!editor) {
|
if (!editor) {
|
||||||
return () => {};
|
return () => {};
|
||||||
@@ -104,6 +107,26 @@ const Editor = (props: Props, ref: ForwardedRef<CodeMirrorControl>) => {
|
|||||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Should run just once
|
// 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(() => {
|
useEffect(() => {
|
||||||
editor?.updateSettings(props.settings);
|
editor?.updateSettings(props.settings);
|
||||||
}, [props.settings, editor]);
|
}, [props.settings, editor]);
|
||||||
|
|||||||
@@ -109,6 +109,41 @@ describe('CodeMirror5Emulation', () => {
|
|||||||
expect(onOtherOptionUpdate).toHaveBeenCalledTimes(1);
|
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('Te!Test!st 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);
|
||||||
|
});
|
||||||
|
|
||||||
it('defineExtension should override previous extensions with the same name', () => {
|
it('defineExtension should override previous extensions with the same name', () => {
|
||||||
const codeMirror = makeCodeMirrorEmulation('Test...');
|
const codeMirror = makeCodeMirrorEmulation('Test...');
|
||||||
const testExtensionFn1 = jest.fn();
|
const testExtensionFn1 = jest.fn();
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { LogMessageCallback } from '../../types';
|
|||||||
import editorCommands from '../editorCommands/editorCommands';
|
import editorCommands from '../editorCommands/editorCommands';
|
||||||
import { StateEffect } from '@codemirror/state';
|
import { StateEffect } from '@codemirror/state';
|
||||||
import { StreamParser } from '@codemirror/language';
|
import { StreamParser } from '@codemirror/language';
|
||||||
import Decorator, { LineWidgetOptions } from './Decorator';
|
import Decorator, { LineWidgetOptions, MarkTextOptions } from './Decorator';
|
||||||
import insertLineAfter from '../editorCommands/insertLineAfter';
|
import insertLineAfter from '../editorCommands/insertLineAfter';
|
||||||
const { pregQuote } = require('@joplin/lib/string-utils-common');
|
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 EditorEventCallback = (editor: CodeMirror5Emulation, ...args: any[])=> void;
|
||||||
type OptionUpdateCallback = (editor: CodeMirror5Emulation, newVal: any, oldVal: any)=> void;
|
type OptionUpdateCallback = (editor: CodeMirror5Emulation, newVal: any, oldVal: any)=> void;
|
||||||
|
|
||||||
|
type OverlayType<State> = StreamParser<State>|{ query: RegExp };
|
||||||
|
|
||||||
interface CodeMirror5OptionRecord {
|
interface CodeMirror5OptionRecord {
|
||||||
onUpdate: OptionUpdateCallback;
|
onUpdate: OptionUpdateCallback;
|
||||||
value: any;
|
value: any;
|
||||||
@@ -26,6 +28,11 @@ interface DocumentPosition {
|
|||||||
ch: number;
|
ch: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DocumentPositionRange {
|
||||||
|
from: DocumentPosition;
|
||||||
|
to: DocumentPosition;
|
||||||
|
}
|
||||||
|
|
||||||
const documentPositionFromPos = (doc: Text, pos: number): DocumentPosition => {
|
const documentPositionFromPos = (doc: Text, pos: number): DocumentPosition => {
|
||||||
const line = doc.lineAt(pos);
|
const line = doc.lineAt(pos);
|
||||||
return {
|
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 {
|
export default class CodeMirror5Emulation extends BaseCodeMirror5Emulation {
|
||||||
private _events: Record<string, EditorEventCallback[]> = {};
|
private _events: Record<string, EditorEventCallback[]> = {};
|
||||||
private _options: Record<string, CodeMirror5OptionRecord> = Object.create(null);
|
private _options: Record<string, CodeMirror5OptionRecord> = Object.create(null);
|
||||||
@@ -271,6 +283,21 @@ export default class CodeMirror5Emulation extends BaseCodeMirror5Emulation {
|
|||||||
return getScrollFraction(this.editor);
|
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) {
|
public defineExtension(name: string, value: any) {
|
||||||
(CodeMirror5Emulation.prototype as any)[name] = value;
|
(CodeMirror5Emulation.prototype as any)[name] = value;
|
||||||
}
|
}
|
||||||
@@ -304,12 +331,17 @@ export default class CodeMirror5Emulation extends BaseCodeMirror5Emulation {
|
|||||||
|
|
||||||
// codemirror-vim's API doesn't match the API docs here -- it expects addOverlay
|
// codemirror-vim's API doesn't match the API docs here -- it expects addOverlay
|
||||||
// to return a SearchQuery. As such, this override returns "any".
|
// to return a SearchQuery. As such, this override returns "any".
|
||||||
public override addOverlay<State>(modeObject: StreamParser<State>|{ query: RegExp }): any {
|
public override addOverlay<State>(modeObject: OverlayType<State>): any {
|
||||||
if ('query' in modeObject) {
|
if ('query' in modeObject) {
|
||||||
return super.addOverlay(modeObject);
|
return super.addOverlay(modeObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._decorator.addOverlay(modeObject);
|
return this._decorator.addOverlay(modeObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override removeOverlay(overlay?: OverlayType<any>): void {
|
||||||
|
super.removeOverlay(overlay);
|
||||||
|
this._decorator.removeOverlay(overlay);
|
||||||
}
|
}
|
||||||
|
|
||||||
public addLineClass(lineNumber: number, where: string, className: string) {
|
public addLineClass(lineNumber: number, where: string, className: string) {
|
||||||
@@ -324,6 +356,16 @@ export default class CodeMirror5Emulation extends BaseCodeMirror5Emulation {
|
|||||||
this._decorator.addLineWidget(lineNumber, node, options);
|
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: Currently copied from useCursorUtils.ts.
|
||||||
// TODO: Remove the duplicate code when CodeMirror 5 is eventually removed.
|
// TODO: Remove the duplicate code when CodeMirror 5 is eventually removed.
|
||||||
public wrapSelections(string1: string, string2: string) {
|
public wrapSelections(string1: string, string2: string) {
|
||||||
|
|||||||
@@ -24,15 +24,16 @@ const mapRangeConfig = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
interface LineCssDecorationSpec extends DecorationRange {
|
interface CssDecorationSpec extends DecorationRange {
|
||||||
cssClass: string;
|
cssClass: string;
|
||||||
|
id?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const addLineDecorationEffect = StateEffect.define<LineCssDecorationSpec>(mapRangeConfig);
|
const addLineDecorationEffect = StateEffect.define<CssDecorationSpec>(mapRangeConfig);
|
||||||
const removeLineDecorationEffect = StateEffect.define<LineCssDecorationSpec>(mapRangeConfig);
|
const removeLineDecorationEffect = StateEffect.define<CssDecorationSpec>(mapRangeConfig);
|
||||||
const addMarkDecorationEffect = StateEffect.define<LineCssDecorationSpec>(mapRangeConfig);
|
const addMarkDecorationEffect = StateEffect.define<CssDecorationSpec>(mapRangeConfig);
|
||||||
// TODO: Support removing mark decorations
|
const removeMarkDecorationEffect = StateEffect.define<CssDecorationSpec>(mapRangeConfig);
|
||||||
// const removeMarkDecorationEffect = StateEffect.define<LineDecorationSpec>(mapRangeConfig);
|
const refreshOverlaysEffect = StateEffect.define();
|
||||||
|
|
||||||
export interface LineWidgetOptions {
|
export interface LineWidgetOptions {
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -46,6 +47,9 @@ interface LineWidgetDecorationSpec extends DecorationRange {
|
|||||||
const addLineWidgetEffect = StateEffect.define<LineWidgetDecorationSpec>(mapRangeConfig);
|
const addLineWidgetEffect = StateEffect.define<LineWidgetDecorationSpec>(mapRangeConfig);
|
||||||
const removeLineWidgetEffect = StateEffect.define<{ element: HTMLElement }>();
|
const removeLineWidgetEffect = StateEffect.define<{ element: HTMLElement }>();
|
||||||
|
|
||||||
|
export interface MarkTextOptions {
|
||||||
|
className: string;
|
||||||
|
}
|
||||||
|
|
||||||
class WidgetDecorationWrapper extends WidgetType {
|
class WidgetDecorationWrapper extends WidgetType {
|
||||||
public constructor(
|
public constructor(
|
||||||
@@ -78,6 +82,7 @@ interface LineWidgetControl {
|
|||||||
export default class Decorator {
|
export default class Decorator {
|
||||||
private _extension: Extension;
|
private _extension: Extension;
|
||||||
private _effectDecorations: DecorationSet = Decoration.none;
|
private _effectDecorations: DecorationSet = Decoration.none;
|
||||||
|
private _nextLineWidgetId = 0;
|
||||||
|
|
||||||
private constructor(private editor: EditorView) {
|
private constructor(private editor: EditorView) {
|
||||||
const decorator = this;
|
const decorator = this;
|
||||||
@@ -93,8 +98,24 @@ export default class Decorator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public update(update: ViewUpdate) {
|
public update(update: ViewUpdate) {
|
||||||
if (update.viewportChanged || update.docChanged) {
|
const updated = false;
|
||||||
|
const doUpdate = () => {
|
||||||
|
if (updated) return;
|
||||||
|
|
||||||
this.decorations = decorator.createOverlayDecorations(update.view);
|
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 +141,18 @@ export default class Decorator {
|
|||||||
private _decorationCache: Record<string, Decoration> = Object.create(null);
|
private _decorationCache: Record<string, Decoration> = Object.create(null);
|
||||||
private _overlays: (StreamParser<any>)[] = [];
|
private _overlays: (StreamParser<any>)[] = [];
|
||||||
|
|
||||||
private classNameToCssDecoration(className: string, isLineDecoration: boolean) {
|
private classNameToCssDecoration(className: string, isLineDecoration: boolean, id?: number) {
|
||||||
let decoration;
|
let decoration;
|
||||||
|
|
||||||
if (className in this._decorationCache) {
|
if (className in this._decorationCache && id === undefined) {
|
||||||
decoration = this._decorationCache[className];
|
decoration = this._decorationCache[className];
|
||||||
} else {
|
} else {
|
||||||
const attributes = { class: className };
|
const attributes = { class: className };
|
||||||
|
|
||||||
if (isLineDecoration) {
|
if (isLineDecoration) {
|
||||||
decoration = Decoration.line({ attributes });
|
decoration = Decoration.line({ attributes, id });
|
||||||
} else {
|
} else {
|
||||||
decoration = Decoration.mark({ attributes });
|
decoration = Decoration.mark({ attributes, id });
|
||||||
}
|
}
|
||||||
|
|
||||||
this._decorationCache[className] = decoration;
|
this._decorationCache[className] = decoration;
|
||||||
@@ -153,7 +174,7 @@ export default class Decorator {
|
|||||||
const isLineDecoration = effect.is(addLineDecorationEffect);
|
const isLineDecoration = effect.is(addLineDecorationEffect);
|
||||||
if (isMarkDecoration || isLineDecoration) {
|
if (isMarkDecoration || isLineDecoration) {
|
||||||
const decoration = this.classNameToCssDecoration(
|
const decoration = this.classNameToCssDecoration(
|
||||||
effect.value.cssClass, isLineDecoration,
|
effect.value.cssClass, isLineDecoration, effect.value.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
const value = effect.value;
|
const value = effect.value;
|
||||||
@@ -165,21 +186,25 @@ export default class Decorator {
|
|||||||
decorations = decorations.update({
|
decorations = decorations.update({
|
||||||
add: [decoration.range(from, to)],
|
add: [decoration.range(from, to)],
|
||||||
});
|
});
|
||||||
} else if (effect.is(removeLineDecorationEffect)) {
|
} else if (effect.is(removeLineDecorationEffect) || effect.is(removeMarkDecorationEffect)) {
|
||||||
const doc = transaction.state.doc;
|
const doc = transaction.state.doc;
|
||||||
const targetFrom = doc.lineAt(effect.value.from).from;
|
const targetFrom = doc.lineAt(effect.value.from).from;
|
||||||
const targetTo = doc.lineAt(effect.value.to).to;
|
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({
|
decorations = decorations.update({
|
||||||
// Returns true only for decorations that should be kept.
|
// Returns true only for decorations that should be kept.
|
||||||
filter: (from, to, value) => {
|
filter: (from, to, value) => {
|
||||||
if (from >= targetFrom && to <= targetTo && value.eq(targetDecoration)) {
|
if (targetId !== undefined) {
|
||||||
return false;
|
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)) {
|
} else if (effect.is(addLineWidgetEffect)) {
|
||||||
@@ -296,6 +321,22 @@ export default class Decorator {
|
|||||||
|
|
||||||
public addOverlay<State>(modeObject: StreamParser<State>) {
|
public addOverlay<State>(modeObject: StreamParser<State>) {
|
||||||
this._overlays.push(modeObject);
|
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) {
|
private addRemoveLineClass(lineNumber: number, className: string, add: boolean) {
|
||||||
@@ -336,6 +377,27 @@ export default class Decorator {
|
|||||||
return lineClasses;
|
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 {
|
private createLineWidgetControl(node: HTMLElement, options: LineWidgetOptions): LineWidgetControl {
|
||||||
return {
|
return {
|
||||||
node,
|
node,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { EditorView } from '@codemirror/view';
|
|||||||
import { EditorCommandType, EditorControl, EditorSettings, LogMessageCallback, ContentScriptData, SearchState } from '../types';
|
import { EditorCommandType, EditorControl, EditorSettings, LogMessageCallback, ContentScriptData, SearchState } from '../types';
|
||||||
import CodeMirror5Emulation from './CodeMirror5Emulation/CodeMirror5Emulation';
|
import CodeMirror5Emulation from './CodeMirror5Emulation/CodeMirror5Emulation';
|
||||||
import editorCommands from './editorCommands/editorCommands';
|
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 { updateLink } from './markdown/markdownCommands';
|
||||||
import { SearchQuery, setSearchQuery } from '@codemirror/search';
|
import { SearchQuery, setSearchQuery } from '@codemirror/search';
|
||||||
import PluginLoader from './pluginApi/PluginLoader';
|
import PluginLoader from './pluginApi/PluginLoader';
|
||||||
@@ -137,9 +137,20 @@ export default class CodeMirrorControl extends CodeMirror5Emulation implements E
|
|||||||
}
|
}
|
||||||
|
|
||||||
public addStyles(...styles: Parameters<typeof EditorView.theme>) {
|
public addStyles(...styles: Parameters<typeof EditorView.theme>) {
|
||||||
|
const compartment = new Compartment();
|
||||||
this.editor.dispatch({
|
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 setContentScripts(plugins: ContentScriptData[]) {
|
public setContentScripts(plugins: ContentScriptData[]) {
|
||||||
|
|||||||
Reference in New Issue
Block a user