You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-11-26 22:41:17 +02:00
Mobile: Improve inline search performance in large documents (#13259)
This commit is contained in:
@@ -90,6 +90,8 @@ function editorTheme(themeId: number) {
|
||||
};
|
||||
}
|
||||
|
||||
const noteEditorSearchChangeSource = 'joplin.noteEditor.setSearchState';
|
||||
|
||||
type OnSetVisibleCallback = (visible: boolean)=> void;
|
||||
type OnSearchStateChangeCallback = (state: SearchState)=> void;
|
||||
const useEditorControl = (
|
||||
@@ -104,7 +106,7 @@ const useEditorControl = (
|
||||
};
|
||||
|
||||
const setSearchStateCallback = (state: SearchState) => {
|
||||
editorRef.current.setSearchState(state);
|
||||
editorRef.current.setSearchState(state, noteEditorSearchChangeSource);
|
||||
setSearchState(state);
|
||||
};
|
||||
|
||||
@@ -310,15 +312,26 @@ function NoteEditor(props: Props) {
|
||||
case EditorEventType.FollowLink:
|
||||
void CommandService.instance().execute('openItem', event.link);
|
||||
break;
|
||||
case EditorEventType.UpdateSearchDialog:
|
||||
setSearchState(event.searchState);
|
||||
case EditorEventType.UpdateSearchDialog: {
|
||||
const hasExternalChange = (
|
||||
event.changeSources.length !== 1
|
||||
|| event.changeSources[0] !== noteEditorSearchChangeSource
|
||||
);
|
||||
|
||||
if (event.searchState.dialogVisible) {
|
||||
editorControl.searchControl.showSearch();
|
||||
} else {
|
||||
editorControl.searchControl.hideSearch();
|
||||
// If the change to the search was done by this editor, it was already applied to the
|
||||
// search state. Skipping the update in this case also helps avoid overwriting the
|
||||
// search state with an older value.
|
||||
if (hasExternalChange) {
|
||||
setSearchState(event.searchState);
|
||||
|
||||
if (event.searchState.dialogVisible) {
|
||||
editorControl.searchControl.showSearch();
|
||||
} else {
|
||||
editorControl.searchControl.hideSearch();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case EditorEventType.Remove:
|
||||
case EditorEventType.Scroll:
|
||||
// Not handled
|
||||
|
||||
@@ -158,8 +158,7 @@ export const SearchPanel = (props: SearchPanelProps) => {
|
||||
const state = props.searchState;
|
||||
const control = props.searchControl;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const updateSearchState = (changedData: any) => {
|
||||
const updateSearchState = (changedData: Partial<SearchState>) => {
|
||||
const newState = { ...state, ...changedData };
|
||||
control.setSearchState(newState);
|
||||
};
|
||||
|
||||
@@ -56,13 +56,11 @@ const useWebViewSetup = ({
|
||||
})})
|
||||
` : '';
|
||||
|
||||
const injectedJavaScript = useMemo(() => `
|
||||
if (typeof markdownEditorBundle === 'undefined') {
|
||||
${shim.injectedJs('markdownEditorBundle')};
|
||||
window.markdownEditorBundle = markdownEditorBundle;
|
||||
markdownEditorBundle.setUpLogger();
|
||||
}
|
||||
|
||||
const afterLoadFinishedJs = useRef((): string => '');
|
||||
// Store as a callback to avoid rebuilding the string on each content change.
|
||||
// Since the editor content is included in editorOptions, for large documents,
|
||||
// creating the initial injected JS is potentially expensive.
|
||||
afterLoadFinishedJs.current = () => `
|
||||
if (!window.cm) {
|
||||
const parentClassName = ${JSON.stringify(editorOptions?.parentElementOrClassName)};
|
||||
const foundParent = !!parentClassName && document.getElementsByClassName(parentClassName).length > 0;
|
||||
@@ -74,6 +72,7 @@ const useWebViewSetup = ({
|
||||
window.cm = markdownEditorBundle.createMainEditor(${JSON.stringify(editorOptions)});
|
||||
|
||||
${jumpToHashJs}
|
||||
|
||||
// Set the initial selection after jumping to the header -- the initial selection,
|
||||
// if specified, should take precedence.
|
||||
${setInitialSelectionJs}
|
||||
@@ -86,7 +85,15 @@ const useWebViewSetup = ({
|
||||
console.log('No parent element found with class name ', parentClassName);
|
||||
}
|
||||
}
|
||||
`, [jumpToHashJs, setInitialSearchJs, setInitialSelectionJs, editorOptions]);
|
||||
`;
|
||||
|
||||
const injectedJavaScript = useMemo(() => `
|
||||
if (typeof markdownEditorBundle === 'undefined') {
|
||||
${shim.injectedJs('markdownEditorBundle')};
|
||||
window.markdownEditorBundle = markdownEditorBundle;
|
||||
markdownEditorBundle.setUpLogger();
|
||||
}
|
||||
`, []);
|
||||
|
||||
// Scroll to the new hash, if it changes.
|
||||
const isFirstScrollRef = useRef(true);
|
||||
@@ -158,13 +165,14 @@ const useWebViewSetup = ({
|
||||
const webViewEventHandlers = useMemo(() => {
|
||||
return {
|
||||
onLoadEnd: () => {
|
||||
webviewRef.current?.injectJS(afterLoadFinishedJs.current());
|
||||
editorMessenger.onWebViewLoaded();
|
||||
},
|
||||
onMessage: (event: OnMessageEvent) => {
|
||||
editorMessenger.onWebViewMessage(event);
|
||||
},
|
||||
};
|
||||
}, [editorMessenger]);
|
||||
}, [editorMessenger, webviewRef]);
|
||||
|
||||
const api = useMemo(() => {
|
||||
return editorMessenger.remoteApi;
|
||||
|
||||
@@ -98,7 +98,7 @@ export const initialize = async (
|
||||
},
|
||||
});
|
||||
});
|
||||
editor.setSearchState(initialSearch);
|
||||
editor.setSearchState(initialSearch, 'initialSearch');
|
||||
|
||||
messenger.setLocalInterface({
|
||||
editor,
|
||||
|
||||
@@ -15,6 +15,7 @@ import { noteIdFacet, setNoteIdEffect } from './extensions/selectedNoteIdExtensi
|
||||
import jumpToHash from './editorCommands/jumpToHash';
|
||||
import { resetImageResourceEffect } from './extensions/rendering/renderBlockImages';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import { searchChangeSourceEffect } from './extensions/searchExtension';
|
||||
|
||||
const logger = Logger.create('CodeMirrorControl');
|
||||
|
||||
@@ -181,7 +182,7 @@ export default class CodeMirrorControl extends CodeMirror5Emulation implements E
|
||||
return getSearchState(this.editor.state);
|
||||
}
|
||||
|
||||
public setSearchState(newState: SearchState) {
|
||||
public setSearchState(newState: SearchState, changeSource = 'setSearchState') {
|
||||
if (newState.dialogVisible !== searchPanelOpen(this.editor.state)) {
|
||||
this.execCommand(newState.dialogVisible ? EditorCommandType.ShowSearch : EditorCommandType.HideSearch);
|
||||
}
|
||||
@@ -194,6 +195,7 @@ export default class CodeMirrorControl extends CodeMirror5Emulation implements E
|
||||
});
|
||||
this.editor.dispatch({
|
||||
effects: [
|
||||
searchChangeSourceEffect.of(changeSource),
|
||||
setSearchQuery.of(query),
|
||||
],
|
||||
});
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import { EditorState, Extension } from '@codemirror/state';
|
||||
import { EditorState, Extension, StateEffect } from '@codemirror/state';
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import { EditorSettings, OnEventCallback } from '../../types';
|
||||
import getSearchState from '../utils/getSearchState';
|
||||
import { EditorEventType } from '../../events';
|
||||
import { search, searchPanelOpen, setSearchQuery } from '@codemirror/search';
|
||||
|
||||
export const searchChangeSourceEffect = StateEffect.define<string>();
|
||||
|
||||
const searchExtension = (onEvent: OnEventCallback, settings: EditorSettings): Extension => {
|
||||
const onSearchDialogUpdate = (state: EditorState) => {
|
||||
const onSearchDialogUpdate = (state: EditorState, changeSources: string[]) => {
|
||||
const newSearchState = getSearchState(state);
|
||||
|
||||
onEvent({
|
||||
kind: EditorEventType.UpdateSearchDialog,
|
||||
searchState: newSearchState,
|
||||
changeSources,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -30,7 +33,10 @@ const searchExtension = (onEvent: OnEventCallback, settings: EditorSettings): Ex
|
||||
|
||||
EditorState.transactionExtender.of((tr) => {
|
||||
if (tr.effects.some(e => e.is(setSearchQuery)) || searchPanelOpen(tr.state) !== searchPanelOpen(tr.startState)) {
|
||||
onSearchDialogUpdate(tr.state);
|
||||
const changeSourceEffects = tr.effects.filter(effect => effect.is(searchChangeSourceEffect));
|
||||
const changeSources = changeSourceEffects.map(effect => effect.value);
|
||||
|
||||
onSearchDialogUpdate(tr.state, changeSources);
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
|
||||
@@ -265,8 +265,8 @@ const createEditor = async (
|
||||
|
||||
view.dispatch(transaction);
|
||||
},
|
||||
setSearchState: (newState: SearchState) => {
|
||||
view.dispatch(updateSearchState(view.state, newState));
|
||||
setSearchState: (newState: SearchState, changeSource = 'setSearchState') => {
|
||||
view.dispatch(updateSearchState(view.state, newState, changeSource));
|
||||
},
|
||||
setContentScripts: (_plugins: ContentScriptData[]) => {
|
||||
throw new Error('setContentScripts not implemented.');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getSearchState, search, SearchQuery, setSearchState } from 'prosemirror-search';
|
||||
import { SearchState } from '../../types';
|
||||
import { Plugin, EditorState, Command } from 'prosemirror-state';
|
||||
import { Plugin, EditorState, Command, Transaction } from 'prosemirror-state';
|
||||
import { EditorEvent, EditorEventType } from '../../events';
|
||||
|
||||
const visiblePlugin = new Plugin({
|
||||
@@ -32,7 +32,7 @@ export const setSearchVisible = (visible: boolean): Command => (state, dispatch)
|
||||
const searchExtension = (onEditorEvent: (event: EditorEvent)=> void) => {
|
||||
|
||||
let lastState: SearchState|null = null;
|
||||
const checkSearchStateChange = (state: EditorState) => {
|
||||
const checkSearchStateChange = (state: EditorState, transaction: Transaction) => {
|
||||
const currentQuery = getSearchState(state).query;
|
||||
const currentVisible = getSearchVisible(state);
|
||||
|
||||
@@ -58,6 +58,7 @@ const searchExtension = (onEditorEvent: (event: EditorEvent)=> void) => {
|
||||
onEditorEvent({
|
||||
kind: EditorEventType.UpdateSearchDialog,
|
||||
searchState: currentState,
|
||||
changeSources: [transaction.getMeta(visiblePlugin)?.changeSource ?? 'unknown'],
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -65,8 +66,8 @@ const searchExtension = (onEditorEvent: (event: EditorEvent)=> void) => {
|
||||
const checkStateChangePlugin = new Plugin<null>({
|
||||
state: {
|
||||
init: ()=>null,
|
||||
apply: (_transaction, oldValue, _oldState, state) => {
|
||||
checkSearchStateChange(state);
|
||||
apply: (transaction, oldValue, _oldState, state) => {
|
||||
checkSearchStateChange(state, transaction);
|
||||
return oldValue;
|
||||
},
|
||||
},
|
||||
@@ -78,7 +79,7 @@ const searchExtension = (onEditorEvent: (event: EditorEvent)=> void) => {
|
||||
visiblePlugin,
|
||||
checkStateChangePlugin,
|
||||
],
|
||||
updateState: (editorState: EditorState, searchState: SearchState) => {
|
||||
updateState: (editorState: EditorState, searchState: SearchState, changeSource: string) => {
|
||||
let transaction = editorState.tr;
|
||||
setSearchVisible(searchState.dialogVisible)(editorState, (newTransaction) => {
|
||||
transaction = newTransaction;
|
||||
@@ -89,6 +90,7 @@ const searchExtension = (onEditorEvent: (event: EditorEvent)=> void) => {
|
||||
regexp: searchState.useRegex,
|
||||
replace: searchState.replaceText,
|
||||
}));
|
||||
transaction.setMeta(visiblePlugin, { changeSource });
|
||||
lastState = { ...searchState };
|
||||
|
||||
return transaction;
|
||||
|
||||
@@ -52,6 +52,7 @@ export interface EditorScrolledEvent {
|
||||
export interface UpdateSearchDialogEvent {
|
||||
kind: EditorEventType.UpdateSearchDialog;
|
||||
searchState: SearchState;
|
||||
changeSources: string[];
|
||||
}
|
||||
|
||||
export interface RequestEditLinkEvent {
|
||||
|
||||
@@ -122,7 +122,7 @@ export interface EditorControl {
|
||||
// the given [label] and [url].
|
||||
updateLink(label: string, url: string): void;
|
||||
|
||||
setSearchState(state: SearchState): void;
|
||||
setSearchState(state: SearchState, changeSource: string): void;
|
||||
|
||||
setContentScripts(plugins: ContentScriptData[]): Promise<void>;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user