mirror of
https://github.com/laurent22/joplin.git
synced 2025-01-02 12:47:41 +02:00
Desktop: Allow searching when only the note viewer is visible and sync search with editor (#10866)
This commit is contained in:
parent
1edef99811
commit
b5313067cd
@ -841,9 +841,11 @@ packages/editor/CodeMirror/utils/formatting/toggleRegionFormatGlobally.test.js
|
||||
packages/editor/CodeMirror/utils/formatting/toggleRegionFormatGlobally.js
|
||||
packages/editor/CodeMirror/utils/formatting/toggleSelectedLinesStartWith.js
|
||||
packages/editor/CodeMirror/utils/formatting/types.js
|
||||
packages/editor/CodeMirror/utils/getSearchState.js
|
||||
packages/editor/CodeMirror/utils/growSelectionToNode.js
|
||||
packages/editor/CodeMirror/utils/handlePasteEvent.js
|
||||
packages/editor/CodeMirror/utils/isInSyntaxNode.js
|
||||
packages/editor/CodeMirror/utils/searchExtension.js
|
||||
packages/editor/CodeMirror/utils/setupVim.js
|
||||
packages/editor/SelectionFormatting.js
|
||||
packages/editor/events.js
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -818,9 +818,11 @@ packages/editor/CodeMirror/utils/formatting/toggleRegionFormatGlobally.test.js
|
||||
packages/editor/CodeMirror/utils/formatting/toggleRegionFormatGlobally.js
|
||||
packages/editor/CodeMirror/utils/formatting/toggleSelectedLinesStartWith.js
|
||||
packages/editor/CodeMirror/utils/formatting/types.js
|
||||
packages/editor/CodeMirror/utils/getSearchState.js
|
||||
packages/editor/CodeMirror/utils/growSelectionToNode.js
|
||||
packages/editor/CodeMirror/utils/handlePasteEvent.js
|
||||
packages/editor/CodeMirror/utils/isInSyntaxNode.js
|
||||
packages/editor/CodeMirror/utils/searchExtension.js
|
||||
packages/editor/CodeMirror/utils/setupVim.js
|
||||
packages/editor/SelectionFormatting.js
|
||||
packages/editor/events.js
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useCallback, 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';
|
||||
@ -20,16 +20,15 @@ export default function useEditorSearchExtension(CodeMirror: CodeMirror5Emulatio
|
||||
const overlayTimeoutRef = useRef(null);
|
||||
overlayTimeoutRef.current = overlayTimeout;
|
||||
|
||||
function clearMarkers() {
|
||||
const clearMarkers = useCallback(() => {
|
||||
for (let i = 0; i < markers.length; i++) {
|
||||
markers[i].clear();
|
||||
}
|
||||
|
||||
setMarkers([]);
|
||||
}
|
||||
}, [markers]);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
function clearOverlay(cm: any) {
|
||||
const clearOverlay = useCallback((cm: CodeMirror5Emulation) => {
|
||||
if (overlay) cm.removeOverlay(overlay);
|
||||
if (scrollbarMarks) {
|
||||
try {
|
||||
@ -47,10 +46,10 @@ export default function useEditorSearchExtension(CodeMirror: CodeMirror5Emulatio
|
||||
setOverlay(null);
|
||||
setScrollbarMarks(null);
|
||||
setOverlayTimeout(null);
|
||||
}
|
||||
}, [scrollbarMarks, overlay, overlayTimeout]);
|
||||
|
||||
// Modified from codemirror/addons/search/search.js
|
||||
function searchOverlay(query: RegExp) {
|
||||
const searchOverlay = useCallback((query: RegExp) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
return { token: function(stream: any) {
|
||||
query.lastIndex = stream.pos;
|
||||
@ -65,13 +64,13 @@ export default function useEditorSearchExtension(CodeMirror: CodeMirror5Emulatio
|
||||
}
|
||||
return null;
|
||||
} };
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Highlights the currently active found work
|
||||
// It's possible to get tricky with this functions and just use findNext/findPrev
|
||||
// but this is fast enough and works more naturally with the current search logic
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
function highlightSearch(cm: any, searchTerm: RegExp, index: number, scrollTo: boolean, withSelection: boolean) {
|
||||
function highlightSearch(cm: CodeMirror5Emulation, searchTerm: RegExp, index: number, scrollTo: boolean, withSelection: boolean) {
|
||||
const cursor = cm.getSearchCursor(searchTerm);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
@ -122,6 +121,12 @@ export default function useEditorSearchExtension(CodeMirror: CodeMirror5Emulatio
|
||||
options = { selectedIndex: 0, searchTimestamp: 0 };
|
||||
}
|
||||
|
||||
if (options.showEditorMarkers === false) {
|
||||
clearMarkers();
|
||||
clearOverlay(this);
|
||||
return;
|
||||
}
|
||||
|
||||
clearMarkers();
|
||||
|
||||
// HIGHLIGHT KEYWORDS
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { RefObject, useEffect } from 'react';
|
||||
import { RefObject, useEffect, useMemo, useRef } from 'react';
|
||||
import usePrevious from '../../../../hooks/usePrevious';
|
||||
import { RenderedBody } from './types';
|
||||
import { SearchMarkers } from '../../../utils/useSearchMarkers';
|
||||
const debounce = require('debounce');
|
||||
|
||||
interface Props {
|
||||
@ -14,14 +15,31 @@ interface Props {
|
||||
|
||||
noteContent: string;
|
||||
renderedBody: RenderedBody;
|
||||
showEditorMarkers: boolean;
|
||||
}
|
||||
|
||||
const useEditorSearchHandler = (props: Props) => {
|
||||
const { webviewRef, editorRef, renderedBody, noteContent, searchMarkers } = props;
|
||||
const {
|
||||
webviewRef, editorRef, renderedBody, noteContent, searchMarkers, showEditorMarkers,
|
||||
} = props;
|
||||
|
||||
const previousContent = usePrevious(noteContent);
|
||||
const previousRenderedBody = usePrevious(renderedBody);
|
||||
const previousSearchMarkers = usePrevious(searchMarkers);
|
||||
const showEditorMarkersRef = useRef(showEditorMarkers);
|
||||
showEditorMarkersRef.current = showEditorMarkers;
|
||||
|
||||
// Fixes https://github.com/laurent22/joplin/issues/7565
|
||||
const debouncedMarkers = useMemo(() => debounce((searchMarkers: SearchMarkers) => {
|
||||
if (!editorRef.current) return;
|
||||
|
||||
if (showEditorMarkersRef.current) {
|
||||
const matches = editorRef.current.setMarkers(searchMarkers.keywords, searchMarkers.options);
|
||||
props.setLocalSearchResultCount(matches);
|
||||
} else {
|
||||
editorRef.current.setMarkers(searchMarkers.keywords, { ...searchMarkers.options, showEditorMarkers: false });
|
||||
}
|
||||
}, 50), [editorRef, props.setLocalSearchResultCount]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!searchMarkers) return () => {};
|
||||
@ -37,19 +55,7 @@ const useEditorSearchHandler = (props: Props) => {
|
||||
|
||||
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();
|
||||
};
|
||||
}
|
||||
debouncedMarkers(searchMarkers);
|
||||
}
|
||||
return () => {};
|
||||
}, [
|
||||
@ -62,6 +68,7 @@ const useEditorSearchHandler = (props: Props) => {
|
||||
previousContent,
|
||||
previousRenderedBody,
|
||||
renderedBody,
|
||||
debouncedMarkers,
|
||||
]);
|
||||
|
||||
};
|
||||
|
@ -686,6 +686,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
editorRef,
|
||||
noteContent: props.content,
|
||||
renderedBody,
|
||||
showEditorMarkers: true,
|
||||
});
|
||||
|
||||
const cellEditorStyle = useMemo(() => {
|
||||
|
@ -16,7 +16,7 @@ import { MarkupToHtml } from '@joplin/renderer';
|
||||
const { clipboard } = require('electron');
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
import ErrorBoundary from '../../../../ErrorBoundary';
|
||||
import { EditorKeymap, EditorLanguageType, EditorSettings, UserEventSource } from '@joplin/editor/types';
|
||||
import { EditorKeymap, EditorLanguageType, EditorSettings, SearchState, UserEventSource } from '@joplin/editor/types';
|
||||
import useStyles from '../utils/useStyles';
|
||||
import { EditorEvent, EditorEventType } from '@joplin/editor/events';
|
||||
import useScrollHandler from '../utils/useScrollHandler';
|
||||
@ -175,6 +175,9 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
}
|
||||
},
|
||||
supportsCommand: (name: string) => {
|
||||
if (name === 'search' && !props.visiblePanes.includes('editor')) {
|
||||
return false;
|
||||
}
|
||||
return name in commands || editorRef.current.supportsCommand(name);
|
||||
},
|
||||
execCommand: async (cmd: EditorCommand) => {
|
||||
@ -197,7 +200,7 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
return commandOutput;
|
||||
},
|
||||
};
|
||||
}, [props.content, commands, resetScroll, setEditorPercentScroll, setViewerPercentScroll]);
|
||||
}, [props.content, props.visiblePanes, commands, resetScroll, setEditorPercentScroll, setViewerPercentScroll]);
|
||||
|
||||
const webview_domReady = useCallback(() => {
|
||||
setWebviewReady(true);
|
||||
@ -321,6 +324,7 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
editorRef,
|
||||
noteContent: props.content,
|
||||
renderedBody,
|
||||
showEditorMarkers: !props.useLocalSearch,
|
||||
});
|
||||
|
||||
useContextMenu({
|
||||
@ -330,6 +334,7 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
editorClassName: 'cm-editor',
|
||||
});
|
||||
|
||||
const lastSearchState = useRef<SearchState|null>(null);
|
||||
const onEditorEvent = useCallback((event: EditorEvent) => {
|
||||
if (event.kind === EditorEventType.Scroll) {
|
||||
editor_scroll();
|
||||
@ -337,8 +342,17 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
codeMirror_change(event.value);
|
||||
} else if (event.kind === EditorEventType.SelectionRangeChange) {
|
||||
setSelectionRange({ from: event.from, to: event.to });
|
||||
} else if (event.kind === EditorEventType.UpdateSearchDialog) {
|
||||
if (lastSearchState.current?.searchText !== event.searchState.searchText) {
|
||||
props.setLocalSearch(event.searchState.searchText);
|
||||
}
|
||||
}, [editor_scroll, codeMirror_change]);
|
||||
|
||||
if (lastSearchState.current?.dialogVisible !== event.searchState.dialogVisible) {
|
||||
props.setShowLocalSearch(event.searchState.dialogVisible);
|
||||
}
|
||||
lastSearchState.current = event.searchState;
|
||||
}
|
||||
}, [editor_scroll, codeMirror_change, props.setLocalSearch, props.setShowLocalSearch]);
|
||||
|
||||
const editorSettings = useMemo((): EditorSettings => {
|
||||
const isHTMLNote = props.contentMarkupLanguage === MarkupToHtml.MARKUP_LANGUAGE_HTML;
|
||||
@ -389,6 +403,8 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
onEvent={onEditorEvent}
|
||||
onLogMessage={logDebug}
|
||||
onEditorPaste={onEditorPaste}
|
||||
externalSearch={props.searchMarkers}
|
||||
useLocalSearch={props.useLocalSearch}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -13,12 +13,15 @@ import { dirname } from 'path';
|
||||
import useKeymap from './utils/useKeymap';
|
||||
import useEditorSearch from '../utils/useEditorSearchExtension';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import { SearchMarkers } from '../../../utils/useSearchMarkers';
|
||||
|
||||
interface Props extends EditorProps {
|
||||
style: React.CSSProperties;
|
||||
pluginStates: PluginStates;
|
||||
|
||||
onEditorPaste: (event: Event)=> void;
|
||||
externalSearch: SearchMarkers;
|
||||
useLocalSearch: boolean;
|
||||
}
|
||||
|
||||
const Editor = (props: Props, ref: ForwardedRef<CodeMirrorControl>) => {
|
||||
@ -117,6 +120,25 @@ const Editor = (props: Props, ref: ForwardedRef<CodeMirrorControl>) => {
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Should run just once
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const searchState = editor.getSearchState();
|
||||
const externalSearchText = props.externalSearch.keywords.map(k => k.value).join(' ') || searchState.searchText;
|
||||
|
||||
if (externalSearchText === searchState.searchText && searchState.dialogVisible === props.useLocalSearch) {
|
||||
return;
|
||||
}
|
||||
|
||||
editor.setSearchState({
|
||||
...searchState,
|
||||
dialogVisible: props.useLocalSearch,
|
||||
searchText: externalSearchText,
|
||||
});
|
||||
}, [editor, props.externalSearch, props.useLocalSearch]);
|
||||
|
||||
const theme = props.settings.themeData;
|
||||
useEffect(() => {
|
||||
if (!editor) return () => {};
|
||||
|
@ -411,6 +411,9 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
noteToolbar: null,
|
||||
onScroll: onScroll,
|
||||
setLocalSearchResultCount: setLocalSearchResultCount,
|
||||
setLocalSearch: localSearch_change,
|
||||
setShowLocalSearch,
|
||||
useLocalSearch: showLocalSearch,
|
||||
searchMarkers: searchMarkers,
|
||||
visiblePanes: props.noteVisiblePanes || ['editor', 'viewer'],
|
||||
keyboardMode: Setting.value('editor.keyboardMode'),
|
||||
@ -506,6 +509,7 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
onPrevious={localSearch_previous}
|
||||
onClose={localSearch_close}
|
||||
visiblePanes={props.noteVisiblePanes}
|
||||
editorType={props.bodyEditor}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import { RenderResult, RenderResultPluginAsset } from '@joplin/renderer/types';
|
||||
import { Dispatch } from 'redux';
|
||||
import { ProcessResultsRow } from '@joplin/lib/services/search/SearchEngine';
|
||||
import { DropHandler } from './useDropHandler';
|
||||
import { SearchMarkers } from './useSearchMarkers';
|
||||
|
||||
export interface AllAssetsOptions {
|
||||
contentMaxWidthTarget?: string;
|
||||
@ -119,8 +120,11 @@ export interface NoteBodyEditorProps {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
noteToolbar: any;
|
||||
setLocalSearchResultCount(count: number): void;
|
||||
setLocalSearch(search: string): void;
|
||||
setShowLocalSearch(show: boolean): void;
|
||||
useLocalSearch: boolean;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
searchMarkers: any;
|
||||
searchMarkers: SearchMarkers;
|
||||
visiblePanes: string[];
|
||||
keyboardMode: string;
|
||||
resourceInfos: ResourceInfos;
|
||||
|
@ -18,6 +18,7 @@ interface Props {
|
||||
resultCount: number;
|
||||
selectedIndex: number;
|
||||
visiblePanes: string[];
|
||||
editorType: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
style: any;
|
||||
}
|
||||
@ -26,6 +27,7 @@ class NoteSearchBar extends React.Component<Props> {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
private backgroundColor: any;
|
||||
private searchInputRef: React.RefObject<HTMLInputElement>;
|
||||
|
||||
public constructor(props: Props) {
|
||||
super(props);
|
||||
@ -40,6 +42,7 @@ class NoteSearchBar extends React.Component<Props> {
|
||||
this.focus = this.focus.bind(this);
|
||||
|
||||
this.backgroundColor = undefined;
|
||||
this.searchInputRef = React.createRef();
|
||||
}
|
||||
|
||||
public style() {
|
||||
@ -133,10 +136,8 @@ class NoteSearchBar extends React.Component<Props> {
|
||||
}
|
||||
|
||||
public focus() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
focus('NoteSearchBar::focus', this.refs.searchInput as any);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
(this.refs.searchInput as any).select();
|
||||
focus('NoteSearchBar::focus', this.searchInputRef.current);
|
||||
this.searchInputRef.current?.select();
|
||||
}
|
||||
|
||||
public render() {
|
||||
@ -173,7 +174,9 @@ class NoteSearchBar extends React.Component<Props> {
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
const allowScrolling = this.props.visiblePanes.indexOf('editor') >= 0;
|
||||
const editorVisible = this.props.visiblePanes.includes('editor');
|
||||
const usesEditorSearch = this.props.editorType === 'CodeMirror6' && editorVisible;
|
||||
const allowScrolling = editorVisible;
|
||||
|
||||
const viewerWarning = (
|
||||
<div style={textStyle}>
|
||||
@ -181,6 +184,8 @@ class NoteSearchBar extends React.Component<Props> {
|
||||
</div>
|
||||
);
|
||||
|
||||
if (usesEditorSearch) return null;
|
||||
|
||||
return (
|
||||
<div className="note-search-bar" style={this.props.style}>
|
||||
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
||||
@ -190,7 +195,7 @@ class NoteSearchBar extends React.Component<Props> {
|
||||
value={query}
|
||||
onChange={this.searchInput_change}
|
||||
onKeyDown={this.searchInput_keyDown}
|
||||
ref="searchInput"
|
||||
ref={this.searchInputRef}
|
||||
type="text"
|
||||
style={{ width: 200, marginRight: 5, backgroundColor: this.backgroundColor, color: theme.color }}
|
||||
/>
|
||||
|
@ -63,5 +63,61 @@ test.describe('markdownEditor', () => {
|
||||
await mainWindow.keyboard.press('Home');
|
||||
await expect(firstItemLocator).toBeFocused();
|
||||
});
|
||||
|
||||
test('should sync local search between the viewer and editor', async ({ mainWindow }) => {
|
||||
const mainScreen = new MainScreen(mainWindow);
|
||||
await mainScreen.waitFor();
|
||||
const noteEditor = mainScreen.noteEditor;
|
||||
|
||||
await mainScreen.createNewNote('Note');
|
||||
|
||||
await noteEditor.focusCodeMirrorEditor();
|
||||
|
||||
await mainWindow.keyboard.type('# Testing');
|
||||
await mainWindow.keyboard.press('Enter');
|
||||
await mainWindow.keyboard.press('Enter');
|
||||
await mainWindow.keyboard.type('This is a test of search. `Test inline code`');
|
||||
|
||||
const viewer = noteEditor.getNoteViewerIframe();
|
||||
await expect(viewer.locator('h1')).toHaveText('Testing');
|
||||
|
||||
const matches = viewer.locator('mark');
|
||||
await expect(matches).toHaveCount(0);
|
||||
|
||||
await mainWindow.keyboard.press(process.platform === 'darwin' ? 'Meta+f' : 'Control+f');
|
||||
await expect(noteEditor.editorSearchInput).toBeVisible();
|
||||
|
||||
await noteEditor.editorSearchInput.click();
|
||||
await noteEditor.editorSearchInput.fill('test');
|
||||
await mainWindow.keyboard.press('Enter');
|
||||
|
||||
// Should show at least one match in the viewer
|
||||
await expect(matches).toHaveCount(3);
|
||||
await expect(matches.first()).toBeAttached();
|
||||
|
||||
// Should show matches in code regions
|
||||
await noteEditor.editorSearchInput.fill('inline code');
|
||||
await mainWindow.keyboard.press('Enter');
|
||||
await expect(matches).toHaveCount(1);
|
||||
|
||||
// Should continue searching after switching to view-only mode
|
||||
await noteEditor.toggleEditorLayoutButton.click();
|
||||
await noteEditor.toggleEditorLayoutButton.click();
|
||||
await expect(noteEditor.codeMirrorEditor).not.toBeVisible();
|
||||
await expect(noteEditor.editorSearchInput).not.toBeVisible();
|
||||
await expect(noteEditor.viewerSearchInput).toBeVisible();
|
||||
|
||||
// Should stop searching after closing the search input
|
||||
await noteEditor.viewerSearchInput.click();
|
||||
await expect(matches).toHaveCount(1);
|
||||
await mainWindow.keyboard.press('Escape');
|
||||
await expect(noteEditor.viewerSearchInput).not.toBeVisible();
|
||||
await expect(matches).toHaveCount(0);
|
||||
|
||||
// After showing the viewer again, search should still be hidden
|
||||
await noteEditor.toggleEditorLayoutButton.click();
|
||||
await expect(noteEditor.codeMirrorEditor).toBeVisible();
|
||||
await expect(noteEditor.editorSearchInput).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -7,6 +7,9 @@ export default class NoteEditorPage {
|
||||
public readonly noteTitleInput: Locator;
|
||||
public readonly attachFileButton: Locator;
|
||||
public readonly toggleEditorsButton: Locator;
|
||||
public readonly toggleEditorLayoutButton: Locator;
|
||||
public readonly editorSearchInput: Locator;
|
||||
public readonly viewerSearchInput: Locator;
|
||||
private readonly containerLocator: Locator;
|
||||
|
||||
public constructor(private readonly page: Page) {
|
||||
@ -16,6 +19,10 @@ export default class NoteEditorPage {
|
||||
this.noteTitleInput = this.containerLocator.locator('.title-input');
|
||||
this.attachFileButton = this.containerLocator.getByRole('button', { name: 'Attach file' });
|
||||
this.toggleEditorsButton = this.containerLocator.getByRole('button', { name: 'Toggle editors' });
|
||||
this.toggleEditorLayoutButton = this.containerLocator.getByRole('button', { name: 'Toggle editor layout' });
|
||||
// The editor and viewer have slightly different search UI
|
||||
this.editorSearchInput = this.containerLocator.getByPlaceholder('Find');
|
||||
this.viewerSearchInput = this.containerLocator.getByPlaceholder('Search...');
|
||||
}
|
||||
|
||||
public toolbarButtonLocator(title: string) {
|
||||
@ -26,7 +33,7 @@ export default class NoteEditorPage {
|
||||
// The note viewer can change content when the note re-renders. As such,
|
||||
// a new locator needs to be created after re-renders (and this can't be a
|
||||
// static property).
|
||||
return this.page.frame({ url: /.*note-viewer[/\\]index.html.*/ });
|
||||
return this.page.frameLocator('[src$="note-viewer/index.html"]');
|
||||
}
|
||||
|
||||
public getTinyMCEFrameLocator() {
|
||||
|
@ -4,12 +4,13 @@ import CodeMirror5Emulation from './CodeMirror5Emulation/CodeMirror5Emulation';
|
||||
import editorCommands from './editorCommands/editorCommands';
|
||||
import { Compartment, EditorSelection, Extension, StateEffect } from '@codemirror/state';
|
||||
import { updateLink } from './markdown/markdownCommands';
|
||||
import { SearchQuery, setSearchQuery } from '@codemirror/search';
|
||||
import { searchPanelOpen, SearchQuery, setSearchQuery } from '@codemirror/search';
|
||||
import PluginLoader from './pluginApi/PluginLoader';
|
||||
import customEditorCompletion, { editorCompletionSource, enableLanguageDataAutocomplete } from './pluginApi/customEditorCompletion';
|
||||
import { CompletionSource } from '@codemirror/autocomplete';
|
||||
import { RegionSpec } from './utils/formatting/RegionSpec';
|
||||
import toggleInlineSelectionFormat from './utils/formatting/toggleInlineSelectionFormat';
|
||||
import getSearchState from './utils/getSearchState';
|
||||
|
||||
interface Callbacks {
|
||||
onUndoRedo(): void;
|
||||
@ -153,7 +154,15 @@ export default class CodeMirrorControl extends CodeMirror5Emulation implements E
|
||||
this._callbacks.onSettingsChange(newSettings);
|
||||
}
|
||||
|
||||
public getSearchState(): SearchState {
|
||||
return getSearchState(this.editor.state);
|
||||
}
|
||||
|
||||
public setSearchState(newState: SearchState) {
|
||||
if (newState.dialogVisible !== searchPanelOpen(this.editor.state)) {
|
||||
this.execCommand(newState.dialogVisible ? EditorCommandType.ShowSearch : EditorCommandType.HideSearch);
|
||||
}
|
||||
|
||||
const query = new SearchQuery({
|
||||
search: newState.searchText,
|
||||
caseSensitive: newState.caseSensitive,
|
||||
@ -161,8 +170,11 @@ export default class CodeMirrorControl extends CodeMirror5Emulation implements E
|
||||
replace: newState.replaceText,
|
||||
});
|
||||
this.editor.dispatch({
|
||||
effects: setSearchQuery.of(query),
|
||||
effects: [
|
||||
setSearchQuery.of(query),
|
||||
],
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
public addStyles(...styles: Parameters<typeof EditorView.theme>) {
|
||||
|
@ -1,8 +1,6 @@
|
||||
import { Compartment, EditorState, Prec } from '@codemirror/state';
|
||||
import { indentOnInput, syntaxHighlighting } from '@codemirror/language';
|
||||
import {
|
||||
openSearchPanel, closeSearchPanel, getSearchQuery, search,
|
||||
} from '@codemirror/search';
|
||||
import { openSearchPanel, closeSearchPanel, searchPanelOpen } from '@codemirror/search';
|
||||
|
||||
import { classHighlighter } from '@lezer/highlight';
|
||||
|
||||
@ -15,7 +13,7 @@ import { keymap, KeyBinding } from '@codemirror/view';
|
||||
import { searchKeymap } from '@codemirror/search';
|
||||
import { historyKeymap } from '@codemirror/commands';
|
||||
|
||||
import { SearchState, EditorProps, EditorSettings } from '../types';
|
||||
import { EditorProps, EditorSettings } from '../types';
|
||||
import { EditorEventType, SelectionRangeChangeEvent } from '../events';
|
||||
import {
|
||||
decreaseIndent, increaseIndent,
|
||||
@ -32,6 +30,7 @@ import CodeMirrorControl from './CodeMirrorControl';
|
||||
import insertLineAfter from './editorCommands/insertLineAfter';
|
||||
import handlePasteEvent from './utils/handlePasteEvent';
|
||||
import biDirectionalTextExtension from './utils/biDirectionalTextExtension';
|
||||
import searchExtension from './utils/searchExtension';
|
||||
|
||||
const createEditor = (
|
||||
parentElement: HTMLElement, props: EditorProps,
|
||||
@ -41,7 +40,6 @@ const createEditor = (
|
||||
|
||||
props.onLogMessage('Initializing CodeMirror...');
|
||||
|
||||
let searchVisible = false;
|
||||
|
||||
// Handles firing an event when the undo/redo stack changes
|
||||
let schedulePostUndoRedoDepthChangeId_: ReturnType<typeof setTimeout>|null = null;
|
||||
@ -92,36 +90,6 @@ const createEditor = (
|
||||
});
|
||||
};
|
||||
|
||||
const onSearchDialogUpdate = () => {
|
||||
const query = getSearchQuery(editor.state);
|
||||
const searchState: SearchState = {
|
||||
searchText: query.search,
|
||||
replaceText: query.replace,
|
||||
useRegex: query.regexp,
|
||||
caseSensitive: query.caseSensitive,
|
||||
dialogVisible: searchVisible,
|
||||
};
|
||||
props.onEvent({
|
||||
kind: EditorEventType.UpdateSearchDialog,
|
||||
searchState,
|
||||
});
|
||||
};
|
||||
|
||||
const showSearchDialog = () => {
|
||||
if (!searchVisible) {
|
||||
openSearchPanel(editor);
|
||||
}
|
||||
searchVisible = true;
|
||||
onSearchDialogUpdate();
|
||||
};
|
||||
|
||||
const hideSearchDialog = () => {
|
||||
if (searchVisible) {
|
||||
closeSearchPanel(editor);
|
||||
}
|
||||
searchVisible = false;
|
||||
onSearchDialogUpdate();
|
||||
};
|
||||
|
||||
const globalSpellcheckEnabled = () => {
|
||||
return editor.contentDOM.spellcheck;
|
||||
@ -188,11 +156,11 @@ const createEditor = (
|
||||
const keymapConfig = Prec.low(keymap.of([
|
||||
// Custom mod-f binding: Toggle the external dialog implementation
|
||||
// (don't show/hide the Panel dialog).
|
||||
keyCommand('Mod-f', (_: EditorView) => {
|
||||
if (searchVisible) {
|
||||
hideSearchDialog();
|
||||
keyCommand('Mod-f', (editor: EditorView) => {
|
||||
if (searchPanelOpen(editor.state)) {
|
||||
closeSearchPanel(editor);
|
||||
} else {
|
||||
showSearchDialog();
|
||||
openSearchPanel(editor);
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
@ -226,22 +194,7 @@ const createEditor = (
|
||||
|
||||
dynamicConfig.of(configFromSettings(props.settings)),
|
||||
historyCompartment.of(history()),
|
||||
|
||||
search(settings.useExternalSearch ? {
|
||||
createPanel(_: EditorView) {
|
||||
return {
|
||||
// The actual search dialog is implemented with react native,
|
||||
// use a dummy element.
|
||||
dom: document.createElement('div'),
|
||||
mount() {
|
||||
showSearchDialog();
|
||||
},
|
||||
destroy() {
|
||||
hideSearchDialog();
|
||||
},
|
||||
};
|
||||
},
|
||||
} : undefined),
|
||||
searchExtension(props.onEvent, props.settings),
|
||||
|
||||
// Allows multiple selections and allows selecting a rectangle
|
||||
// with ctrl (as in CodeMirror 5)
|
||||
@ -295,6 +248,7 @@ const createEditor = (
|
||||
notifySelectionChange(viewUpdate);
|
||||
notifySelectionFormattingChange(viewUpdate);
|
||||
}),
|
||||
|
||||
],
|
||||
doc: initialText,
|
||||
}),
|
||||
|
17
packages/editor/CodeMirror/utils/getSearchState.ts
Normal file
17
packages/editor/CodeMirror/utils/getSearchState.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { EditorState } from '@codemirror/state';
|
||||
import { SearchState } from '../../types';
|
||||
import { getSearchQuery, searchPanelOpen } from '@codemirror/search';
|
||||
|
||||
const getSearchState = (state: EditorState) => {
|
||||
const query = getSearchQuery(state);
|
||||
const searchState: SearchState = {
|
||||
searchText: query.search,
|
||||
replaceText: query.replace,
|
||||
useRegex: query.regexp,
|
||||
caseSensitive: query.caseSensitive,
|
||||
dialogVisible: searchPanelOpen(state),
|
||||
};
|
||||
return searchState;
|
||||
};
|
||||
|
||||
export default getSearchState;
|
40
packages/editor/CodeMirror/utils/searchExtension.ts
Normal file
40
packages/editor/CodeMirror/utils/searchExtension.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { EditorState, Extension } from '@codemirror/state';
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import { EditorSettings, OnEventCallback } from '../../types';
|
||||
import getSearchState from './getSearchState';
|
||||
import { EditorEventType } from '../../events';
|
||||
import { search, searchPanelOpen, setSearchQuery } from '@codemirror/search';
|
||||
|
||||
const searchExtension = (onEvent: OnEventCallback, settings: EditorSettings): Extension => {
|
||||
const onSearchDialogUpdate = (state: EditorState) => {
|
||||
const newSearchState = getSearchState(state);
|
||||
|
||||
onEvent({
|
||||
kind: EditorEventType.UpdateSearchDialog,
|
||||
searchState: newSearchState,
|
||||
});
|
||||
};
|
||||
|
||||
return [
|
||||
search(settings.useExternalSearch ? {
|
||||
createPanel(_editor: EditorView) {
|
||||
return {
|
||||
// The actual search dialog is implemented with react native,
|
||||
// use a dummy element.
|
||||
dom: document.createElement('div'),
|
||||
mount() { },
|
||||
destroy() { },
|
||||
};
|
||||
},
|
||||
} : undefined),
|
||||
|
||||
EditorState.transactionExtender.of((tr) => {
|
||||
if (tr.effects.some(e => e.is(setSearchQuery)) || searchPanelOpen(tr.state) !== searchPanelOpen(tr.startState)) {
|
||||
onSearchDialogUpdate(tr.state);
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
];
|
||||
};
|
||||
|
||||
export default searchExtension;
|
Loading…
Reference in New Issue
Block a user