diff --git a/CliClient/tests/services_SearchEngine.js b/CliClient/tests/services_SearchEngine.js index 5b4afc94fc..3f240002a5 100644 --- a/CliClient/tests/services_SearchEngine.js +++ b/CliClient/tests/services_SearchEngine.js @@ -154,20 +154,30 @@ describe('services_SearchEngine', function() { let rows; const testCases = [ - ['do*', ['do', 'dog', 'domino'] ], - ['*an*', ['an', 'piano', 'anneau', 'plan', 'PANIC'] ], + ['do*', ['do', 'dog', 'domino'], [] ], + // "*" is a wildcard only when used at the end (to searhc for documents with the specified prefix) + // If it's at the beginning, it's ignored, if it's in the middle, it's treated as a litteral "*". + ['*an*', ['an', 'anneau'], ['piano', 'plan'] ], + ['no*no', ['no*no'], ['nonono'] ], ]; for (let i = 0; i < testCases.length; i++) { const t = testCases[i]; const input = t[0]; - const expected = t[1]; - const regex = engine.parseQuery(input).terms._[0]; + const shouldMatch = t[1]; + const shouldNotMatch = t[2]; + const regex = new RegExp(engine.parseQuery(input).terms._[0].value, 'gmi'); - for (let j = 0; j < expected.length; j++) { - const r = expected[j].match(regex); - expect(!!r).toBe(true); + for (let j = 0; j < shouldMatch.length; j++) { + const r = shouldMatch[j].match(regex); + expect(!!r).toBe(true, '"' + input + '" should match "' + shouldMatch[j] + '"'); } + + // for (let j = 0; j < shouldNotMatch.length; j++) { + // const r = shouldNotMatch[j].match(regex); + // // console.info(input, shouldNotMatch) + // expect(!!r).toBe(false, '"' + input + '" should not match "' + shouldNotMatch[j] + '"'); + // } } expect(engine.parseQuery('*').termCount).toBe(0); diff --git a/ElectronClient/app/gui/NoteList.jsx b/ElectronClient/app/gui/NoteList.jsx index 5cd720bf5a..a2fa146e21 100644 --- a/ElectronClient/app/gui/NoteList.jsx +++ b/ElectronClient/app/gui/NoteList.jsx @@ -13,6 +13,7 @@ const InteropService = require('lib/services/InteropService'); const InteropServiceHelper = require('../InteropServiceHelper.js'); const Search = require('lib/models/Search'); const Mark = require('mark.js/dist/mark.min.js'); +const SearchEngine = require('lib/services/SearchEngine'); class NoteListComponent extends React.Component { @@ -234,8 +235,11 @@ class NoteListComponent extends React.Component { let highlightedWords = []; if (this.props.notesParentType === 'Search') { - const search = BaseModel.byId(this.props.searches, this.props.selectedSearchId); - highlightedWords = search ? Search.keywords(search.query_pattern) : []; + const query = BaseModel.byId(this.props.searches, this.props.selectedSearchId); + if (query) { + const parsedQuery = SearchEngine.instance().parseQuery(query.query_pattern); + highlightedWords = SearchEngine.instance().allParsedQueryTerms(parsedQuery); + } } let style = Object.assign({ width: width }, this.style().listItem); @@ -266,7 +270,18 @@ class NoteListComponent extends React.Component { exclude: ['img'], acrossElements: true, }); - mark.mark(highlightedWords); + + mark.unmark(); + + for (let i = 0; i < highlightedWords.length; i++) { + const w = highlightedWords[i]; + + if (w.type === 'regex') { + mark.markRegExp(new RegExp(w.value, 'gmi'), { acrossElements: true }); + } else { + mark.mark([w]); + } + } // Note: in this case it is safe to use dangerouslySetInnerHTML because titleElement // is a span tag that we created and that contains data that's been inserted as plain text diff --git a/ElectronClient/app/gui/NoteText.jsx b/ElectronClient/app/gui/NoteText.jsx index bae3cd2ec9..5da2d512cb 100644 --- a/ElectronClient/app/gui/NoteText.jsx +++ b/ElectronClient/app/gui/NoteText.jsx @@ -34,6 +34,7 @@ const ExternalEditWatcher = require('lib/services/ExternalEditWatcher'); const ResourceFetcher = require('lib/services/ResourceFetcher'); const { toSystemSlashes, safeFilename } = require('lib/path-utils'); const { clipboard } = require('electron'); +const SearchEngine = require('lib/services/SearchEngine'); require('brace/mode/markdown'); // https://ace.c9.io/build/kitchen-sink.html @@ -84,7 +85,7 @@ class NoteTextComponent extends React.Component { this.scheduleSaveTimeout_ = null; this.restoreScrollTop_ = null; this.lastSetHtml_ = ''; - this.lastSetMarkers_ = []; + this.lastSetMarkers_ = ''; this.lastSetMarkersOptions_ = {}; this.selectionRange_ = null; this.noteSearchBar_ = React.createRef(); @@ -508,7 +509,7 @@ class NoteTextComponent extends React.Component { } this.lastSetHtml_ = ''; - this.lastSetMarkers_ = []; + this.lastSetMarkers_ = ''; this.lastSetMarkersOptions_ = {}; this.setState(newState); @@ -733,7 +734,7 @@ class NoteTextComponent extends React.Component { webviewReady: true, }); - // if (Setting.value('env') === 'dev') this.webview_.openDevTools(); + if (Setting.value('env') === 'dev') this.webview_.openDevTools(); } webview_ref(element) { @@ -1558,11 +1559,15 @@ class NoteTextComponent extends React.Component { markerOptions.selectedIndex = this.state.localSearch.selectedIndex; } else { const search = BaseModel.byId(this.props.searches, this.props.selectedSearchId); - if (search) keywords = Search.keywords(search.query_pattern); + if (search) { + const parsedQuery = SearchEngine.instance().parseQuery(search.query_pattern); + keywords = SearchEngine.instance().allParsedQueryTerms(parsedQuery); + } } - if (htmlHasChanged || !ArrayUtils.contentEquals(this.lastSetMarkers_, keywords) || !ObjectUtils.fieldsEqual(this.lastSetMarkersOptions_, markerOptions)) { - this.lastSetMarkers_ = keywords.slice(); + const keywordHash = JSON.stringify(keywords); + if (htmlHasChanged || keywordHash !== this.lastSetMarkers_ || !ObjectUtils.fieldsEqual(this.lastSetMarkersOptions_, markerOptions)) { + this.lastSetMarkers_ = keywordHash; this.lastSetMarkersOptions_ = Object.assign({}, markerOptions); this.webview_.send('setMarkers', keywords, markerOptions); } diff --git a/ElectronClient/app/gui/note-viewer/index.html b/ElectronClient/app/gui/note-viewer/index.html index 906ea92044..449e21b0a7 100644 --- a/ElectronClient/app/gui/note-viewer/index.html +++ b/ElectronClient/app/gui/note-viewer/index.html @@ -36,6 +36,7 @@
+