1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-18 09:35:20 +02:00

Desktop: Fixes #11409: Goto Anything fails for long strings (#11436)

This commit is contained in:
Laurent Cozic 2024-11-23 16:47:46 +00:00 committed by GitHub
parent 48fd5d30f7
commit e652db05e1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 36 additions and 19 deletions

View File

@ -13,7 +13,7 @@ import Folder from '@joplin/lib/models/Folder';
import Note from '@joplin/lib/models/Note'; import Note from '@joplin/lib/models/Note';
import ItemList from '../gui/ItemList'; import ItemList from '../gui/ItemList';
import HelpButton from '../gui/HelpButton'; import HelpButton from '../gui/HelpButton';
import { surroundKeywords, nextWhitespaceIndex, removeDiacritics } from '@joplin/lib/string-utils'; import { surroundKeywords, nextWhitespaceIndex, removeDiacritics, escapeRegExp, KeywordType } from '@joplin/lib/string-utils';
import { mergeOverlappingIntervals } from '@joplin/lib/ArrayUtils'; import { mergeOverlappingIntervals } from '@joplin/lib/ArrayUtils';
import markupLanguageUtils from '@joplin/lib/utils/markupLanguageUtils'; import markupLanguageUtils from '@joplin/lib/utils/markupLanguageUtils';
import focusEditorIfEditorCommand from '@joplin/lib/services/commands/focusEditorIfEditorCommand'; import focusEditorIfEditorCommand from '@joplin/lib/services/commands/focusEditorIfEditorCommand';
@ -56,7 +56,7 @@ interface State {
query: string; query: string;
results: GotoAnythingSearchResult[]; results: GotoAnythingSearchResult[];
selectedItemId: string; selectedItemId: string;
keywords: string[]; keywords: (string | ComplexTerm)[];
listType: number; listType: number;
showHelp: boolean; showHelp: boolean;
resultsInBody: boolean; resultsInBody: boolean;
@ -376,9 +376,13 @@ class DialogComponent extends React.PureComponent<Props, State> {
results = results.filter(r => !!notesById[r.id]) results = results.filter(r => !!notesById[r.id])
.map(r => ({ ...r, title: notesById[r.id].title })); .map(r => ({ ...r, title: notesById[r.id].title }));
const normalizedKeywords = (await this.keywords(searchQuery)).map( const keywordRegexes = (await this.keywords(searchQuery)).map(term => {
({ valueRegex }: ComplexTerm) => new RegExp(removeDiacritics(valueRegex), 'ig'), if (typeof term === 'string') {
); return new RegExp(escapeRegExp(term), 'ig');
} else {
return new RegExp(removeDiacritics(term.valueRegex), 'ig');
}
});
for (let i = 0; i < results.length; i++) { for (let i = 0; i < results.length; i++) {
const row = results[i]; const row = results[i];
@ -393,7 +397,7 @@ class DialogComponent extends React.PureComponent<Props, State> {
const normalizedBody = removeDiacritics(body); const normalizedBody = removeDiacritics(body);
// Iterate over all matches in the body for each search keyword // Iterate over all matches in the body for each search keyword
for (const keywordRegex of normalizedKeywords) { for (const keywordRegex of keywordRegexes) {
for (const match of normalizedBody.matchAll(keywordRegex)) { for (const match of normalizedBody.matchAll(keywordRegex)) {
// Populate 'indices' with [begin index, end index] of each note fragment // Populate 'indices' with [begin index, end index] of each note fragment
// Begins at the regex matching index, ends at the next whitespace after seeking 15 characters to the right // Begins at the regex matching index, ends at the next whitespace after seeking 15 characters to the right
@ -547,7 +551,7 @@ class DialogComponent extends React.PureComponent<Props, State> {
const wrapKeywordMatches = (unescapedContent: string) => { const wrapKeywordMatches = (unescapedContent: string) => {
return surroundKeywords( return surroundKeywords(
this.state.keywords, this.state.keywords as KeywordType,
unescapedContent, unescapedContent,
`<span class="match-highlight" style="font-weight: bold; color: ${theme.searchMarkerColor}; background-color: ${theme.searchMarkerBackgroundColor}">`, `<span class="match-highlight" style="font-weight: bold; color: ${theme.searchMarkerColor}; background-color: ${theme.searchMarkerBackgroundColor}">`,
'</span>', '</span>',

View File

@ -7,13 +7,13 @@ import useQueuedAsyncEffect from '@joplin/lib/hooks/useQueuedAsyncEffect';
import { NoteEntity } from '@joplin/lib/services/database/types'; import { NoteEntity } from '@joplin/lib/services/database/types';
import SearchEngineUtils from '@joplin/lib/services/search/SearchEngineUtils'; import SearchEngineUtils from '@joplin/lib/services/search/SearchEngineUtils';
import Note from '@joplin/lib/models/Note'; import Note from '@joplin/lib/models/Note';
import SearchEngine from '@joplin/lib/services/search/SearchEngine'; import SearchEngine, { ComplexTerm } from '@joplin/lib/services/search/SearchEngine';
import { ProgressBar } from 'react-native-paper'; import { ProgressBar } from 'react-native-paper';
import shim from '@joplin/lib/shim'; import shim from '@joplin/lib/shim';
interface Props { interface Props {
query: string; query: string;
onHighlightedWordsChange: (highlightedWords: string[])=> void; onHighlightedWordsChange: (highlightedWords: (ComplexTerm | string)[])=> void;
ftsEnabled: number; ftsEnabled: number;
} }

View File

@ -11,6 +11,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import IconButton from '../../IconButton'; import IconButton from '../../IconButton';
import SearchResults from './SearchResults'; import SearchResults from './SearchResults';
import AccessibleView from '../../accessibility/AccessibleView'; import AccessibleView from '../../accessibility/AccessibleView';
import { ComplexTerm } from '@joplin/lib/services/search/SearchEngine';
interface Props { interface Props {
themeId: number; themeId: number;
@ -73,7 +74,7 @@ const SearchScreenComponent: React.FC<Props> = props => {
setQuery(''); setQuery('');
}, []); }, []);
const onHighlightedWordsChange = useCallback((words: string[]) => { const onHighlightedWordsChange = useCallback((words: (ComplexTerm | string)[]) => {
props.dispatch({ props.dispatch({
type: 'SET_HIGHLIGHTED', type: 'SET_HIGHLIGHTED',
words, words,

View File

@ -39,7 +39,7 @@ const SyncTargetAmazonS3 = require('./SyncTargetAmazonS3.js');
import EncryptionService from './services/e2ee/EncryptionService'; import EncryptionService from './services/e2ee/EncryptionService';
import ResourceFetcher from './services/ResourceFetcher'; import ResourceFetcher from './services/ResourceFetcher';
import SearchEngineUtils from './services/search/SearchEngineUtils'; import SearchEngineUtils from './services/search/SearchEngineUtils';
import SearchEngine, { ProcessResultsRow } from './services/search/SearchEngine'; import SearchEngine, { ComplexTerm, ProcessResultsRow } from './services/search/SearchEngine';
import RevisionService from './services/RevisionService'; import RevisionService from './services/RevisionService';
import ResourceService from './services/ResourceService'; import ResourceService from './services/ResourceService';
import DecryptionWorker from './services/DecryptionWorker'; import DecryptionWorker from './services/DecryptionWorker';
@ -240,7 +240,7 @@ export default class BaseApplication {
}); });
let notes: NoteEntity[] = []; let notes: NoteEntity[] = [];
let highlightedWords: string[] = []; let highlightedWords: (ComplexTerm | string)[] = [];
let searchResults: ProcessResultsRow[] = []; let searchResults: ProcessResultsRow[] = [];
if (parentId) { if (parentId) {

View File

@ -63,11 +63,22 @@ export interface ComplexTerm {
} }
export interface Terms { export interface Terms {
// This `string | ComplexTerm` type that ends up propagating throughout the app is a bit of a
// mess, but it reflects how it was originally done in plain JS. Ideally it should be refactor
// to use a simple type.
_: (string | ComplexTerm)[]; _: (string | ComplexTerm)[];
title: (string | ComplexTerm)[]; title: (string | ComplexTerm)[];
body: (string | ComplexTerm)[]; body: (string | ComplexTerm)[];
} }
export interface ParsedQuery {
termCount: number;
keys: string[];
terms: Terms; // text terms
allTerms: Term[];
any: boolean;
}
export default class SearchEngine { export default class SearchEngine {
public static instance_: SearchEngine = null; public static instance_: SearchEngine = null;
@ -521,7 +532,7 @@ export default class SearchEngine {
return regexString; return regexString;
} }
public async parseQuery(query: string) { public async parseQuery(query: string): Promise<ParsedQuery> {
const trimQuotes = (str: string) => str.startsWith('"') ? str.substr(1, str.length - 2) : str; const trimQuotes = (str: string) => str.startsWith('"') ? str.substr(1, str.length - 2) : str;
@ -606,14 +617,11 @@ export default class SearchEngine {
}; };
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied public allParsedQueryTerms(parsedQuery: ParsedQuery) {
public allParsedQueryTerms(parsedQuery: any) {
if (!parsedQuery || !parsedQuery.termCount) return []; if (!parsedQuery || !parsedQuery.termCount) return [];
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied let output: typeof parsedQuery.terms._ = [];
let output: any[] = []; for (const col of Object.keys(parsedQuery.terms) as (keyof Terms)[]) {
for (const col in parsedQuery.terms) {
if (!parsedQuery.terms.hasOwnProperty(col)) continue;
output = output.concat(parsedQuery.terms[col]); output = output.concat(parsedQuery.terms[col]);
} }
return output; return output;

View File

@ -100,6 +100,10 @@ export function removeDiacritics(str: string) {
return str; return str;
} }
export function escapeRegExp(keyword: string) {
return keyword.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}
export function wrap(text: string, indent: string, width: number) { export function wrap(text: string, indent: string, width: number) {
const wrap_ = require('word-wrap'); const wrap_ = require('word-wrap');