mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-24 10:27:10 +02:00
Desktop: Enable searching in editor rather than the viewer for CodeMirror (#3360)
This commit is contained in:
parent
e68eb196b7
commit
44d3a4213f
@ -107,6 +107,7 @@ ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/Toolbar.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/index.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/types.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useCursorUtils.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useEditorSearch.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useJoplinMode.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useLineSorting.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useListIdent.js
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -98,6 +98,7 @@ ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/Toolbar.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/index.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/types.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useCursorUtils.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useEditorSearch.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useJoplinMode.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useLineSorting.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useListIdent.js
|
||||
|
@ -25,6 +25,7 @@ const markdownUtils = require('lib/markdownUtils');
|
||||
const { _ } = require('lib/locale');
|
||||
const { reg } = require('lib/registry.js');
|
||||
const dialogs = require('../../../dialogs');
|
||||
const { themeStyle } = require('lib/theme');
|
||||
|
||||
function markupRenderOptions(override: any = null) {
|
||||
return { ...override };
|
||||
@ -47,6 +48,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
props_onChangeRef.current = props.onChange;
|
||||
const contentKeyHasChangedRef = useRef(false);
|
||||
contentKeyHasChangedRef.current = previousContentKey !== props.contentKey;
|
||||
const theme = themeStyle(props.theme);
|
||||
|
||||
const rootSize = useRootSize({ rootRef });
|
||||
|
||||
@ -274,6 +276,65 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
menu.popup(bridge().window());
|
||||
}, [props.content, editorCutText, editorPasteText, editorCopyText, onEditorPaste]);
|
||||
|
||||
useEffect(() => {
|
||||
const element = document.createElement('style');
|
||||
element.setAttribute('id', 'codemirrorStyle');
|
||||
document.head.appendChild(element);
|
||||
element.appendChild(document.createTextNode(`
|
||||
/* These must be important to prevent the codemirror defaults from taking over*/
|
||||
.CodeMirror {
|
||||
font-family: monospace;
|
||||
height: 100% !important;
|
||||
width: 100% !important;
|
||||
color: inherit !important;
|
||||
background-color: inherit !important;
|
||||
position: absolute !important;
|
||||
-webkit-box-shadow: none !important; // Some themes add a box shadow for some reason
|
||||
}
|
||||
|
||||
.cm-header-1 {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.cm-header-2 {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
.cm-header-3 {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.cm-header-4, .cm-header-5, .cm-header-6 {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.cm-header-1, .cm-header-2, .cm-header-3, .cm-header-4, .cm-header-5, .cm-header-6 {
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
.cm-search-marker {
|
||||
background: ${theme.searchMarkerBackgroundColor};
|
||||
color: ${theme.searchMarkerColor} !important;
|
||||
}
|
||||
|
||||
.cm-search-marker-selected {
|
||||
background: ${theme.selectedColor2};
|
||||
color: ${theme.color2} !important;
|
||||
}
|
||||
|
||||
.cm-search-marker-scrollbar {
|
||||
background: ${theme.searchMarkerBackgroundColor};
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
opacity: .5;
|
||||
}
|
||||
`));
|
||||
|
||||
return () => {
|
||||
document.head.removeChild(element);
|
||||
};
|
||||
}, [props.theme]);
|
||||
|
||||
const webview_domReady = useCallback(() => {
|
||||
setWebviewReady(true);
|
||||
}, []);
|
||||
@ -331,9 +392,41 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
|
||||
useEffect(() => {
|
||||
if (props.searchMarkers !== previousSearchMarkers || renderedBody !== previousRenderedBody) {
|
||||
webviewRef.current.wrappedInstance.send('setMarkers', props.searchMarkers.keywords, props.searchMarkers.options);
|
||||
// Force both viewers to be visible during search
|
||||
// This view should only change when the search terms change, this means the user
|
||||
// is always presented with the currently highlighted text, but can revert
|
||||
// to the viewer if they only want to scroll through matches
|
||||
if (!props.visiblePanes.includes('editor') && props.searchMarkers !== previousSearchMarkers) {
|
||||
props.dispatch({
|
||||
type: 'NOTE_VISIBLE_PANES_SET',
|
||||
panes: ['editor', 'viewer'],
|
||||
});
|
||||
}
|
||||
// SEARCHHACK
|
||||
// TODO: remove this options hack when aceeditor is removed
|
||||
// Currently the webviewRef will send out an ipcMessage to set the results count
|
||||
// Also setting it here will start an infinite loop of repeating the search
|
||||
// Unfortunately we can't remove the function in the webview setMarkers
|
||||
// until the aceeditor is remove.
|
||||
// The below search is more accurate than the webview based one as it searches
|
||||
// the text and not rendered html (rendered html fails if there is a match
|
||||
// in a katex block)
|
||||
// Once AceEditor is removed the options definition below can be removed and
|
||||
// props.searchMarkers.options can be directly passed to as the 3rd argument below
|
||||
// (replacing options)
|
||||
let options = { notFromAce: true };
|
||||
if (props.searchMarkers.options) {
|
||||
options = Object.assign({}, props.searchMarkers.options, options);
|
||||
}
|
||||
webviewRef.current.wrappedInstance.send('setMarkers', props.searchMarkers.keywords, options);
|
||||
// SEARCHHACK
|
||||
if (editorRef.current) {
|
||||
|
||||
const matches = editorRef.current.setMarkers(props.searchMarkers.keywords, props.searchMarkers.options);
|
||||
props.setLocalSearchResultCount(matches);
|
||||
}
|
||||
}
|
||||
}, [props.searchMarkers, renderedBody]);
|
||||
}, [props.searchMarkers, props.setLocalSearchResultCount, renderedBody]);
|
||||
|
||||
const cellEditorStyle = useMemo(() => {
|
||||
const output = { ...styles.cellEditor };
|
||||
|
@ -8,11 +8,15 @@ import 'codemirror/addon/dialog/dialog';
|
||||
import 'codemirror/addon/edit/closebrackets';
|
||||
import 'codemirror/addon/edit/continuelist';
|
||||
import 'codemirror/addon/scroll/scrollpastend';
|
||||
import 'codemirror/addon/scroll/annotatescrollbar';
|
||||
import 'codemirror/addon/search/matchesonscrollbar';
|
||||
import 'codemirror/addon/search/searchcursor';
|
||||
|
||||
import useListIdent from './utils/useListIdent';
|
||||
import useScrollUtils from './utils/useScrollUtils';
|
||||
import useCursorUtils from './utils/useCursorUtils';
|
||||
import useLineSorting from './utils/useLineSorting';
|
||||
import useEditorSearch from './utils/useEditorSearch';
|
||||
import useJoplinMode from './utils/useJoplinMode';
|
||||
|
||||
import 'codemirror/keymap/emacs';
|
||||
@ -85,6 +89,7 @@ function Editor(props: EditorProps, ref: any) {
|
||||
useScrollUtils(CodeMirror);
|
||||
useCursorUtils(CodeMirror);
|
||||
useLineSorting(CodeMirror);
|
||||
useEditorSearch(CodeMirror);
|
||||
useJoplinMode(CodeMirror);
|
||||
|
||||
CodeMirror.keyMap.basic = {
|
||||
|
@ -11,7 +11,7 @@ interface ToolbarProps {
|
||||
}
|
||||
|
||||
function styles_(props:ToolbarProps) {
|
||||
return buildStyle('AceEditorToolbar', props.theme, (/* theme:any*/) => {
|
||||
return buildStyle('CodeMirrorToolbar', props.theme, (/* theme:any*/) => {
|
||||
const theme = themeStyle(props.theme);
|
||||
return {
|
||||
root: {
|
||||
|
File diff suppressed because one or more lines are too long
@ -0,0 +1,151 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
export default function useEditorSearch(CodeMirror: any) {
|
||||
|
||||
const [markers, setMarkers] = useState([]);
|
||||
const [overlay, setOverlay] = useState(null);
|
||||
const [scrollbarMarks, setScrollbarMarks] = useState(null);
|
||||
const [previousKeywordValue, setPreviousKeywordValue] = useState(null);
|
||||
const [overlayTimeout, setOverlayTimeout] = useState(null);
|
||||
const overlayTimeoutRef = useRef(null);
|
||||
overlayTimeoutRef.current = overlayTimeout;
|
||||
|
||||
function clearMarkers() {
|
||||
for (let i = 0; i < markers.length; i++) {
|
||||
markers[i].clear();
|
||||
}
|
||||
|
||||
setMarkers([]);
|
||||
}
|
||||
|
||||
function clearOverlay(cm: any) {
|
||||
if (overlay) cm.removeOverlay(overlay);
|
||||
if (scrollbarMarks) scrollbarMarks.clear();
|
||||
|
||||
if (overlayTimeout) clearTimeout(overlayTimeout);
|
||||
|
||||
setOverlay(null);
|
||||
setScrollbarMarks(null);
|
||||
setOverlayTimeout(null);
|
||||
}
|
||||
|
||||
// Modified from codemirror/addons/search/search.js
|
||||
function searchOverlay(query: RegExp) {
|
||||
return { token: function(stream: any) {
|
||||
query.lastIndex = stream.pos;
|
||||
const match = query.exec(stream.string);
|
||||
if (match && match.index == stream.pos) {
|
||||
stream.pos += match[0].length || 1;
|
||||
return 'search-marker';
|
||||
} else if (match) {
|
||||
stream.pos = match.index;
|
||||
} else {
|
||||
stream.skipToEnd();
|
||||
}
|
||||
return null;
|
||||
} };
|
||||
}
|
||||
|
||||
// Highlights the currently active found work
|
||||
// It's possible to get tricky with this fucntions and just use findNext/findPrev
|
||||
// but this is fast enough and works more naturally with the current search logic
|
||||
function highlightSearch(cm: any, searchTerm: RegExp, index: number) {
|
||||
const marks: any = [];
|
||||
|
||||
const cursor = cm.getSearchCursor(searchTerm);
|
||||
|
||||
let match = null;
|
||||
for (let j = 0; j < index + 1; j++) {
|
||||
if (!cursor.findNext()) {
|
||||
// If we run out of matches then just highlight the final match
|
||||
break;
|
||||
}
|
||||
match = cursor.pos;
|
||||
}
|
||||
|
||||
if (match) {
|
||||
marks.push(cm.markText(match.from, match.to, { className: 'cm-search-marker-selected' }));
|
||||
cm.scrollIntoView(match);
|
||||
}
|
||||
|
||||
return marks;
|
||||
}
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Escaping
|
||||
function escapeRegExp(keyword: string) {
|
||||
return keyword.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
|
||||
}
|
||||
|
||||
function getSearchTerm(keyword: any) {
|
||||
const value = escapeRegExp(keyword.value);
|
||||
return new RegExp(value, 'gi');
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (overlayTimeoutRef.current) clearTimeout(overlayTimeoutRef.current);
|
||||
overlayTimeoutRef.current = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
CodeMirror.defineExtension('setMarkers', function(keywords: any, options: any) {
|
||||
if (!options) {
|
||||
options = { selectedIndex: 0 };
|
||||
}
|
||||
|
||||
clearMarkers();
|
||||
|
||||
// HIGHLIGHT KEYWORDS
|
||||
// When doing a global search it's possible to have multiple keywords
|
||||
// This means we need to highlight each one
|
||||
let marks: any = [];
|
||||
for (let i = 0; i < keywords.length; i++) {
|
||||
const keyword = keywords[i];
|
||||
|
||||
if (keyword.value === '') continue;
|
||||
|
||||
const searchTerm = getSearchTerm(keyword);
|
||||
|
||||
marks = marks.concat(highlightSearch(this, searchTerm, options.selectedIndex));
|
||||
}
|
||||
|
||||
setMarkers(marks);
|
||||
|
||||
// SEARCHOVERLAY
|
||||
// We only want to highlight all matches when there is only 1 search term
|
||||
if (keywords.length !== 1 || keywords[0].value == '') {
|
||||
clearOverlay(this);
|
||||
setPreviousKeywordValue('');
|
||||
return 0;
|
||||
}
|
||||
|
||||
const searchTerm = getSearchTerm(keywords[0]);
|
||||
|
||||
// Determine the number of matches in the source, this is passed on
|
||||
// to the NoteEditor component
|
||||
const regexMatches = this.getValue().match(searchTerm);
|
||||
const nMatches = regexMatches ? regexMatches.length : 0;
|
||||
|
||||
// Don't bother clearing and re-calculating the overlay if the search term
|
||||
// hasn't changed
|
||||
if (keywords[0].value === previousKeywordValue) return nMatches;
|
||||
|
||||
clearOverlay(this);
|
||||
setPreviousKeywordValue(keywords[0].value);
|
||||
|
||||
// These operations are pretty slow, so we won't add use them until the user
|
||||
// has finished typing, 500ms is probably enough time
|
||||
const timeout = setTimeout(() => {
|
||||
const scrollMarks = this.showMatchesOnScrollbar(searchTerm, true, 'cm-search-marker-scrollbar');
|
||||
const overlay = searchOverlay(searchTerm);
|
||||
this.addOverlay(overlay);
|
||||
setOverlay(overlay);
|
||||
setScrollbarMarks(scrollMarks);
|
||||
}, 500);
|
||||
|
||||
setOverlayTimeout(timeout);
|
||||
overlayTimeoutRef.current = timeout;
|
||||
|
||||
return nMatches;
|
||||
});
|
||||
}
|
@ -397,6 +397,7 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
dispatch: props.dispatch,
|
||||
noteToolbar: null,// renderNoteToolbar(),
|
||||
onScroll: onScroll,
|
||||
setLocalSearchResultCount: setLocalSearchResultCount,
|
||||
searchMarkers: searchMarkers,
|
||||
visiblePanes: props.noteVisiblePanes || ['editor', 'viewer'],
|
||||
keyboardMode: Setting.value('editor.keyboardMode'),
|
||||
|
@ -43,6 +43,7 @@ export interface NoteBodyEditorProps {
|
||||
disabled: boolean;
|
||||
dispatch: Function;
|
||||
noteToolbar: any;
|
||||
setLocalSearchResultCount(count: number): void,
|
||||
searchMarkers: any,
|
||||
visiblePanes: string[],
|
||||
keyboardMode: string,
|
||||
|
@ -21,7 +21,7 @@
|
||||
}
|
||||
|
||||
mark {
|
||||
background: #F3B717;
|
||||
background: #F7D26E;
|
||||
color: black;
|
||||
}
|
||||
|
||||
@ -272,7 +272,11 @@
|
||||
let elementIndex = 0;
|
||||
|
||||
const onEachElement = (element) => {
|
||||
if (!('selectedIndex' in options)) return;
|
||||
// SEARCHHACK
|
||||
// TODO: remove notFromAce hack when removing aceeditor
|
||||
// when removing just remove the 'notFromAce' part and leave the rest alone
|
||||
if (!('selectedIndex' in options) || 'notFromAce' in options) return;
|
||||
// SEARCHHACK
|
||||
|
||||
if (('selectedIndex' in options) && elementIndex === options.selectedIndex) {
|
||||
markSelectedElement_ = element;
|
||||
@ -298,13 +302,21 @@
|
||||
}, markKeywordOptions);
|
||||
}
|
||||
|
||||
ipcProxySendToHost('setMarkerCount', elementIndex);
|
||||
// SEARCHHACK
|
||||
// TODO: Remove this block (until the other SEARCHHACK marker) when removing Ace
|
||||
// HACK: Aceeditor uses this view to handle all the searching
|
||||
// The newer editor wont and this needs to be disabled in order to
|
||||
// prevent an infinite loop
|
||||
if (!('notFromAce' in options)) {
|
||||
ipcProxySendToHost('setMarkerCount', elementIndex);
|
||||
|
||||
// We only scroll the element into view if the search just happened. So when the user type the search
|
||||
// or select the next/previous result, we scroll into view. However for other actions that trigger a
|
||||
// re-render, we don't scroll as this is normally not wanted.
|
||||
// This is to go around this issue: https://github.com/laurent22/joplin/issues/1833
|
||||
if (selectedElement && Date.now() - options.searchTimestamp <= 1000) selectedElement.scrollIntoView();
|
||||
// We only scroll the element into view if the search just happened. So when the user type the search
|
||||
// or select the next/previous result, we scroll into view. However for other actions that trigger a
|
||||
// re-render, we don't scroll as this is normally not wanted.
|
||||
// This is to go around this issue: https://github.com/laurent22/joplin/issues/1833
|
||||
if (selectedElement && Date.now() - options.searchTimestamp <= 1000) selectedElement.scrollIntoView();
|
||||
}
|
||||
// SEARCHHACK
|
||||
}
|
||||
|
||||
let markLoader_ = { state: 'idle', whenDone: null };
|
||||
|
@ -170,33 +170,3 @@ a {
|
||||
from {transform: rotate(0deg);}
|
||||
to {transform: rotate(360deg);}
|
||||
}
|
||||
|
||||
/* These must be important to prevent the codemirror defaults from taking over*/
|
||||
.CodeMirror {
|
||||
font-family: monospace;
|
||||
height: 100% !important;
|
||||
width: 100% !important;
|
||||
color: inherit !important;
|
||||
background-color: inherit !important;
|
||||
position: absolute !important;
|
||||
}
|
||||
|
||||
.cm-header-1 {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.cm-header-2 {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
.cm-header-3 {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.cm-header-4, .cm-header-5, .cm-header-6 {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.cm-header-1, .cm-header-2, .cm-header-3, .cm-header-4, .cm-header-5, .cm-header-6 {
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
@ -21,6 +21,8 @@ const lightStyle = {
|
||||
|
||||
raisedBackgroundColor: '#e5e5e5',
|
||||
raisedColor: '#222222',
|
||||
searchMarkerBackgroundColor: '#F7D26E',
|
||||
searchMarkerColor: 'black',
|
||||
|
||||
warningBackgroundColor: '#FFD08D',
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user