You've already forked joplin
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:
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ export const initialize = async (
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
editor.setSearchState(initialSearch);
|
editor.setSearchState(initialSearch, 'initialSearch');
|
||||||
|
|
||||||
messenger.setLocalInterface({
|
messenger.setLocalInterface({
|
||||||
editor,
|
editor,
|
||||||
|
|||||||
@@ -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),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -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.');
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user