1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-11-29 22:48:10 +02:00

Mobile: Improve inline search performance in large documents (#13259)

This commit is contained in:
Henry Heino
2025-09-30 09:22:55 -07:00
committed by GitHub
parent 91dc23c23f
commit f832eb38ff
10 changed files with 62 additions and 31 deletions

View File

@@ -90,6 +90,8 @@ function editorTheme(themeId: number) {
}; };
} }
const noteEditorSearchChangeSource = 'joplin.noteEditor.setSearchState';
type OnSetVisibleCallback = (visible: boolean)=> void; type OnSetVisibleCallback = (visible: boolean)=> void;
type OnSearchStateChangeCallback = (state: SearchState)=> void; type OnSearchStateChangeCallback = (state: SearchState)=> void;
const useEditorControl = ( const useEditorControl = (
@@ -104,7 +106,7 @@ const useEditorControl = (
}; };
const setSearchStateCallback = (state: SearchState) => { const setSearchStateCallback = (state: SearchState) => {
editorRef.current.setSearchState(state); editorRef.current.setSearchState(state, noteEditorSearchChangeSource);
setSearchState(state); setSearchState(state);
}; };
@@ -310,15 +312,26 @@ function NoteEditor(props: Props) {
case EditorEventType.FollowLink: case EditorEventType.FollowLink:
void CommandService.instance().execute('openItem', event.link); void CommandService.instance().execute('openItem', event.link);
break; break;
case EditorEventType.UpdateSearchDialog: case EditorEventType.UpdateSearchDialog: {
setSearchState(event.searchState); const hasExternalChange = (
event.changeSources.length !== 1
|| event.changeSources[0] !== noteEditorSearchChangeSource
);
if (event.searchState.dialogVisible) { // If the change to the search was done by this editor, it was already applied to the
editorControl.searchControl.showSearch(); // search state. Skipping the update in this case also helps avoid overwriting the
} else { // search state with an older value.
editorControl.searchControl.hideSearch(); if (hasExternalChange) {
setSearchState(event.searchState);
if (event.searchState.dialogVisible) {
editorControl.searchControl.showSearch();
} else {
editorControl.searchControl.hideSearch();
}
} }
break; break;
}
case EditorEventType.Remove: case EditorEventType.Remove:
case EditorEventType.Scroll: case EditorEventType.Scroll:
// Not handled // Not handled

View File

@@ -158,8 +158,7 @@ export const SearchPanel = (props: SearchPanelProps) => {
const state = props.searchState; const state = props.searchState;
const control = props.searchControl; const control = props.searchControl;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied const updateSearchState = (changedData: Partial<SearchState>) => {
const updateSearchState = (changedData: any) => {
const newState = { ...state, ...changedData }; const newState = { ...state, ...changedData };
control.setSearchState(newState); control.setSearchState(newState);
}; };

View File

@@ -56,13 +56,11 @@ const useWebViewSetup = ({
})}) })})
` : ''; ` : '';
const injectedJavaScript = useMemo(() => ` const afterLoadFinishedJs = useRef((): string => '');
if (typeof markdownEditorBundle === 'undefined') { // Store as a callback to avoid rebuilding the string on each content change.
${shim.injectedJs('markdownEditorBundle')}; // Since the editor content is included in editorOptions, for large documents,
window.markdownEditorBundle = markdownEditorBundle; // creating the initial injected JS is potentially expensive.
markdownEditorBundle.setUpLogger(); afterLoadFinishedJs.current = () => `
}
if (!window.cm) { if (!window.cm) {
const parentClassName = ${JSON.stringify(editorOptions?.parentElementOrClassName)}; const parentClassName = ${JSON.stringify(editorOptions?.parentElementOrClassName)};
const foundParent = !!parentClassName && document.getElementsByClassName(parentClassName).length > 0; const foundParent = !!parentClassName && document.getElementsByClassName(parentClassName).length > 0;
@@ -74,6 +72,7 @@ const useWebViewSetup = ({
window.cm = markdownEditorBundle.createMainEditor(${JSON.stringify(editorOptions)}); window.cm = markdownEditorBundle.createMainEditor(${JSON.stringify(editorOptions)});
${jumpToHashJs} ${jumpToHashJs}
// Set the initial selection after jumping to the header -- the initial selection, // Set the initial selection after jumping to the header -- the initial selection,
// if specified, should take precedence. // if specified, should take precedence.
${setInitialSelectionJs} ${setInitialSelectionJs}
@@ -86,7 +85,15 @@ const useWebViewSetup = ({
console.log('No parent element found with class name ', parentClassName); 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. // Scroll to the new hash, if it changes.
const isFirstScrollRef = useRef(true); const isFirstScrollRef = useRef(true);
@@ -158,13 +165,14 @@ const useWebViewSetup = ({
const webViewEventHandlers = useMemo(() => { const webViewEventHandlers = useMemo(() => {
return { return {
onLoadEnd: () => { onLoadEnd: () => {
webviewRef.current?.injectJS(afterLoadFinishedJs.current());
editorMessenger.onWebViewLoaded(); editorMessenger.onWebViewLoaded();
}, },
onMessage: (event: OnMessageEvent) => { onMessage: (event: OnMessageEvent) => {
editorMessenger.onWebViewMessage(event); editorMessenger.onWebViewMessage(event);
}, },
}; };
}, [editorMessenger]); }, [editorMessenger, webviewRef]);
const api = useMemo(() => { const api = useMemo(() => {
return editorMessenger.remoteApi; return editorMessenger.remoteApi;

View File

@@ -98,7 +98,7 @@ export const initialize = async (
}, },
}); });
}); });
editor.setSearchState(initialSearch); editor.setSearchState(initialSearch, 'initialSearch');
messenger.setLocalInterface({ messenger.setLocalInterface({
editor, editor,

View File

@@ -15,6 +15,7 @@ import { noteIdFacet, setNoteIdEffect } from './extensions/selectedNoteIdExtensi
import jumpToHash from './editorCommands/jumpToHash'; import jumpToHash from './editorCommands/jumpToHash';
import { resetImageResourceEffect } from './extensions/rendering/renderBlockImages'; import { resetImageResourceEffect } from './extensions/rendering/renderBlockImages';
import Logger from '@joplin/utils/Logger'; import Logger from '@joplin/utils/Logger';
import { searchChangeSourceEffect } from './extensions/searchExtension';
const logger = Logger.create('CodeMirrorControl'); const logger = Logger.create('CodeMirrorControl');
@@ -181,7 +182,7 @@ export default class CodeMirrorControl extends CodeMirror5Emulation implements E
return getSearchState(this.editor.state); return getSearchState(this.editor.state);
} }
public setSearchState(newState: SearchState) { public setSearchState(newState: SearchState, changeSource = 'setSearchState') {
if (newState.dialogVisible !== searchPanelOpen(this.editor.state)) { if (newState.dialogVisible !== searchPanelOpen(this.editor.state)) {
this.execCommand(newState.dialogVisible ? EditorCommandType.ShowSearch : EditorCommandType.HideSearch); this.execCommand(newState.dialogVisible ? EditorCommandType.ShowSearch : EditorCommandType.HideSearch);
} }
@@ -194,6 +195,7 @@ export default class CodeMirrorControl extends CodeMirror5Emulation implements E
}); });
this.editor.dispatch({ this.editor.dispatch({
effects: [ effects: [
searchChangeSourceEffect.of(changeSource),
setSearchQuery.of(query), setSearchQuery.of(query),
], ],
}); });

View File

@@ -1,17 +1,20 @@
import { EditorState, Extension } from '@codemirror/state'; import { EditorState, Extension, StateEffect } from '@codemirror/state';
import { EditorView } from '@codemirror/view'; import { EditorView } from '@codemirror/view';
import { EditorSettings, OnEventCallback } from '../../types'; import { EditorSettings, OnEventCallback } from '../../types';
import getSearchState from '../utils/getSearchState'; import getSearchState from '../utils/getSearchState';
import { EditorEventType } from '../../events'; import { EditorEventType } from '../../events';
import { search, searchPanelOpen, setSearchQuery } from '@codemirror/search'; import { search, searchPanelOpen, setSearchQuery } from '@codemirror/search';
export const searchChangeSourceEffect = StateEffect.define<string>();
const searchExtension = (onEvent: OnEventCallback, settings: EditorSettings): Extension => { const searchExtension = (onEvent: OnEventCallback, settings: EditorSettings): Extension => {
const onSearchDialogUpdate = (state: EditorState) => { const onSearchDialogUpdate = (state: EditorState, changeSources: string[]) => {
const newSearchState = getSearchState(state); const newSearchState = getSearchState(state);
onEvent({ onEvent({
kind: EditorEventType.UpdateSearchDialog, kind: EditorEventType.UpdateSearchDialog,
searchState: newSearchState, searchState: newSearchState,
changeSources,
}); });
}; };
@@ -30,7 +33,10 @@ const searchExtension = (onEvent: OnEventCallback, settings: EditorSettings): Ex
EditorState.transactionExtender.of((tr) => { EditorState.transactionExtender.of((tr) => {
if (tr.effects.some(e => e.is(setSearchQuery)) || searchPanelOpen(tr.state) !== searchPanelOpen(tr.startState)) { 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; return null;
}), }),

View File

@@ -265,8 +265,8 @@ const createEditor = async (
view.dispatch(transaction); view.dispatch(transaction);
}, },
setSearchState: (newState: SearchState) => { setSearchState: (newState: SearchState, changeSource = 'setSearchState') => {
view.dispatch(updateSearchState(view.state, newState)); view.dispatch(updateSearchState(view.state, newState, changeSource));
}, },
setContentScripts: (_plugins: ContentScriptData[]) => { setContentScripts: (_plugins: ContentScriptData[]) => {
throw new Error('setContentScripts not implemented.'); throw new Error('setContentScripts not implemented.');

View File

@@ -1,6 +1,6 @@
import { getSearchState, search, SearchQuery, setSearchState } from 'prosemirror-search'; import { getSearchState, search, SearchQuery, setSearchState } from 'prosemirror-search';
import { SearchState } from '../../types'; import { SearchState } from '../../types';
import { Plugin, EditorState, Command } from 'prosemirror-state'; import { Plugin, EditorState, Command, Transaction } from 'prosemirror-state';
import { EditorEvent, EditorEventType } from '../../events'; import { EditorEvent, EditorEventType } from '../../events';
const visiblePlugin = new Plugin({ const visiblePlugin = new Plugin({
@@ -32,7 +32,7 @@ export const setSearchVisible = (visible: boolean): Command => (state, dispatch)
const searchExtension = (onEditorEvent: (event: EditorEvent)=> void) => { const searchExtension = (onEditorEvent: (event: EditorEvent)=> void) => {
let lastState: SearchState|null = null; let lastState: SearchState|null = null;
const checkSearchStateChange = (state: EditorState) => { const checkSearchStateChange = (state: EditorState, transaction: Transaction) => {
const currentQuery = getSearchState(state).query; const currentQuery = getSearchState(state).query;
const currentVisible = getSearchVisible(state); const currentVisible = getSearchVisible(state);
@@ -58,6 +58,7 @@ const searchExtension = (onEditorEvent: (event: EditorEvent)=> void) => {
onEditorEvent({ onEditorEvent({
kind: EditorEventType.UpdateSearchDialog, kind: EditorEventType.UpdateSearchDialog,
searchState: currentState, searchState: currentState,
changeSources: [transaction.getMeta(visiblePlugin)?.changeSource ?? 'unknown'],
}); });
} }
}; };
@@ -65,8 +66,8 @@ const searchExtension = (onEditorEvent: (event: EditorEvent)=> void) => {
const checkStateChangePlugin = new Plugin<null>({ const checkStateChangePlugin = new Plugin<null>({
state: { state: {
init: ()=>null, init: ()=>null,
apply: (_transaction, oldValue, _oldState, state) => { apply: (transaction, oldValue, _oldState, state) => {
checkSearchStateChange(state); checkSearchStateChange(state, transaction);
return oldValue; return oldValue;
}, },
}, },
@@ -78,7 +79,7 @@ const searchExtension = (onEditorEvent: (event: EditorEvent)=> void) => {
visiblePlugin, visiblePlugin,
checkStateChangePlugin, checkStateChangePlugin,
], ],
updateState: (editorState: EditorState, searchState: SearchState) => { updateState: (editorState: EditorState, searchState: SearchState, changeSource: string) => {
let transaction = editorState.tr; let transaction = editorState.tr;
setSearchVisible(searchState.dialogVisible)(editorState, (newTransaction) => { setSearchVisible(searchState.dialogVisible)(editorState, (newTransaction) => {
transaction = newTransaction; transaction = newTransaction;
@@ -89,6 +90,7 @@ const searchExtension = (onEditorEvent: (event: EditorEvent)=> void) => {
regexp: searchState.useRegex, regexp: searchState.useRegex,
replace: searchState.replaceText, replace: searchState.replaceText,
})); }));
transaction.setMeta(visiblePlugin, { changeSource });
lastState = { ...searchState }; lastState = { ...searchState };
return transaction; return transaction;

View File

@@ -52,6 +52,7 @@ export interface EditorScrolledEvent {
export interface UpdateSearchDialogEvent { export interface UpdateSearchDialogEvent {
kind: EditorEventType.UpdateSearchDialog; kind: EditorEventType.UpdateSearchDialog;
searchState: SearchState; searchState: SearchState;
changeSources: string[];
} }
export interface RequestEditLinkEvent { export interface RequestEditLinkEvent {

View File

@@ -122,7 +122,7 @@ export interface EditorControl {
// the given [label] and [url]. // the given [label] and [url].
updateLink(label: string, url: string): void; updateLink(label: string, url: string): void;
setSearchState(state: SearchState): void; setSearchState(state: SearchState, changeSource: string): void;
setContentScripts(plugins: ContentScriptData[]): Promise<void>; setContentScripts(plugins: ContentScriptData[]): Promise<void>;