diff --git a/BUILD.md b/BUILD.md index e7ed1cb6a6..f745398672 100644 --- a/BUILD.md +++ b/BUILD.md @@ -34,8 +34,8 @@ First you need to setup React Native to build projects with native code. For thi Then: cd ReactNativeClient - npm start-android - # Or: npm start-ios + npm run start-android + # Or: npm run start-ios To run the iOS application, it might be easier to open the file `ios/Joplin.xcworkspace` on XCode and run the app from there. diff --git a/CliClient/tests/models_Note.js b/CliClient/tests/models_Note.js index d779617dd1..d2076e4c19 100644 --- a/CliClient/tests/models_Note.js +++ b/CliClient/tests/models_Note.js @@ -6,6 +6,7 @@ const { time } = require('lib/time-utils.js'); const { sortedIds, createNTestNotes, asyncTest, fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('test-utils.js'); const Folder = require('lib/models/Folder.js'); const Note = require('lib/models/Note.js'); +const Setting = require('lib/models/Setting.js'); const BaseModel = require('lib/BaseModel.js'); const ArrayUtils = require('lib/ArrayUtils.js'); const { shim } = require('lib/shim'); @@ -216,4 +217,54 @@ describe('models_Note', function() { const hasThrown = await checkThrowAsync(async () => await Folder.copyToFolder(note1.id, folder2.id)); expect(hasThrown).toBe(true); })); + + it('should convert resource paths from internal to external paths', asyncTest(async () => { + const resourceDirName = Setting.value('resourceDirName'); + const resourceDir = Setting.value('resourceDir'); + const r1 = await shim.createResourceFromPath(`${__dirname}/../tests/support/photo.jpg`); + const r2 = await shim.createResourceFromPath(`${__dirname}/../tests/support/photo.jpg`); + + const testCases = [ + [ + false, + '', + '', + ], + [ + true, + '', + '', + ], + [ + false, + `![](:/${r1.id})`, + `![](${resourceDirName}/${r1.id}.jpg)`, + ], + [ + false, + `![](:/${r1.id}) ![](:/${r1.id}) ![](:/${r2.id})`, + `![](${resourceDirName}/${r1.id}.jpg) ![](${resourceDirName}/${r1.id}.jpg) ![](${resourceDirName}/${r2.id}.jpg)`, + ], + [ + true, + `![](:/${r1.id})`, + `![](file://${resourceDir}/${r1.id}.jpg)`, + ], + [ + true, + `![](:/${r1.id}) ![](:/${r1.id}) ![](:/${r2.id})`, + `![](file://${resourceDir}/${r1.id}.jpg) ![](file://${resourceDir}/${r1.id}.jpg) ![](file://${resourceDir}/${r2.id}.jpg)`, + ], + ]; + + for (const testCase of testCases) { + const [useAbsolutePaths, input, expected] = testCase; + const internalToExternal = await Note.replaceResourceInternalToExternalLinks(input, { useAbsolutePaths }); + expect(expected).toBe(internalToExternal); + + const externalToInternal = await Note.replaceResourceExternalToInternalLinks(internalToExternal, { useAbsolutePaths }); + expect(externalToInternal).toBe(input); + } + })); + }); diff --git a/CliClient/tests/services_SearchEngine.js b/CliClient/tests/services_SearchEngine.js index 5d2fa62564..7499a6ee21 100644 --- a/CliClient/tests/services_SearchEngine.js +++ b/CliClient/tests/services_SearchEngine.js @@ -92,6 +92,32 @@ describe('services_SearchEngine', function() { expect(rows[2].id).toBe(n1.id); })); + it('should tell where the results are found', asyncTest(async () => { + const notes = [ + await Note.save({ title: 'abcd efgh', body: 'abcd' }), + await Note.save({ title: 'abcd' }), + await Note.save({ title: 'efgh', body: 'abcd' }), + ]; + + await engine.syncTables(); + + const testCases = [ + ['abcd', ['title', 'body'], ['title'], ['body']], + ['efgh', ['title'], [], ['title']], + ]; + + for (const testCase of testCases) { + const rows = await engine.search(testCase[0]); + + for (let i = 0; i < notes.length; i++) { + const row = rows.find(row => row.id === notes[i].id); + const actual = row ? row.fields.sort().join(',') : ''; + const expected = testCase[i + 1].sort().join(','); + expect(expected).toBe(actual); + } + } + })); + it('should order search results by relevance (2)', asyncTest(async () => { // 1 const n1 = await Note.save({ title: 'abcd efgh', body: 'XX abcd XX efgh' }); diff --git a/CliClient/tests/test-utils.js b/CliClient/tests/test-utils.js index 7be60201cb..de7f2d1292 100644 --- a/CliClient/tests/test-utils.js +++ b/CliClient/tests/test-utils.js @@ -143,6 +143,7 @@ async function switchClient(id) { Resource.encryptionService_ = encryptionServices_[id]; BaseItem.revisionService_ = revisionServices_[id]; + Setting.setConstant('resourceDirName', resourceDirName(id)); Setting.setConstant('resourceDir', resourceDir(id)); await Setting.load(); @@ -213,9 +214,14 @@ async function setupDatabase(id = null) { if (!Setting.value('clientId')) Setting.setValue('clientId', uuid.create()); } +function resourceDirName(id = null) { + if (id === null) id = currentClient_; + return `resources-${id}`; +} + function resourceDir(id = null) { if (id === null) id = currentClient_; - return `${__dirname}/data/resources-${id}`; + return `${__dirname}/data/${resourceDirName(id)}`; } async function setupDatabaseAndSynchronizer(id = null) { diff --git a/ElectronClient/InteropServiceHelper.js b/ElectronClient/InteropServiceHelper.js index 64d836e5be..340db816d8 100644 --- a/ElectronClient/InteropServiceHelper.js +++ b/ElectronClient/InteropServiceHelper.js @@ -52,27 +52,35 @@ class InteropServiceHelper { win = bridge().newBrowserWindow(windowOptions); return new Promise((resolve, reject) => { - win.webContents.on('did-finish-load', async () => { + win.webContents.on('did-finish-load', () => { - if (target === 'pdf') { - try { - const data = await win.webContents.printToPDF(options); - resolve(data); - } catch (error) { - reject(error); - } finally { - cleanup(); + // did-finish-load will trigger when most assets are done loading, probably + // images, JavaScript and CSS. However it seems it might trigger *before* + // all fonts are loaded, which will break for example Katex rendering. + // So we need to add an additional timer to make sure fonts are loaded + // as it doesn't seem there's any easy way to figure that out. + setTimeout(async () => { + if (target === 'pdf') { + try { + const data = await win.webContents.printToPDF(options); + resolve(data); + } catch (error) { + reject(error); + } finally { + cleanup(); + } + } else { + win.webContents.print(options, (success, reason) => { + // TODO: This is correct but broken in Electron 4. Need to upgrade to 5+ + // It calls the callback right away with "false" even if the document hasn't be print yet. + + cleanup(); + if (!success && reason !== 'cancelled') reject(new Error(`Could not print: ${reason}`)); + resolve(); + }); } - } else { - win.webContents.print(options, (success, reason) => { - // TODO: This is correct but broken in Electron 4. Need to upgrade to 5+ - // It calls the callback right away with "false" even if the document hasn't be print yet. + }, 2000); - cleanup(); - if (!success && reason !== 'cancelled') reject(new Error(`Could not print: ${reason}`)); - resolve(); - }); - } }); win.loadURL(url.format({ diff --git a/ElectronClient/gui/PromptDialog.jsx b/ElectronClient/gui/PromptDialog.jsx index c49a13b8f2..cc13a0df14 100644 --- a/ElectronClient/gui/PromptDialog.jsx +++ b/ElectronClient/gui/PromptDialog.jsx @@ -233,7 +233,7 @@ class PromptDialog extends React.Component { const buttonComps = []; if (buttonTypes.indexOf('ok') >= 0) { buttonComps.push( - ); diff --git a/ElectronClient/gui/editors/TinyMCE.tsx b/ElectronClient/gui/editors/TinyMCE.tsx index b30eac8497..c65350d729 100644 --- a/ElectronClient/gui/editors/TinyMCE.tsx +++ b/ElectronClient/gui/editors/TinyMCE.tsx @@ -55,9 +55,10 @@ function findBlockSource(node:any) { function newBlockSource(language:string = '', content:string = ''):any { const fence = language === 'katex' ? '$$' : '```'; + const fenceLanguage = language === 'katex' ? '' : language; return { - openCharacters: `\n${fence}${language}\n`, + openCharacters: `\n${fence}${fenceLanguage}\n`, closeCharacters: `\n${fence}\n`, content: content, node: null, @@ -444,6 +445,7 @@ const TinyMCE = (props:TinyMCEProps, ref:any) => { noneditable_noneditable_class: 'joplin-editable', // Can be a regex too valid_elements: '*[*]', // We already filter in sanitize_html menubar: false, + relative_urls: false, branding: false, target_list: false, table_resize_bars: false, diff --git a/ElectronClient/plugins/GotoAnything.jsx b/ElectronClient/plugins/GotoAnything.jsx index f692ef886d..9ad3bdccb8 100644 --- a/ElectronClient/plugins/GotoAnything.jsx +++ b/ElectronClient/plugins/GotoAnything.jsx @@ -12,7 +12,6 @@ const HelpButton = require('../gui/HelpButton.min'); const { surroundKeywords, nextWhitespaceIndex } = require('lib/string-utils.js'); const { mergeOverlappingIntervals } = require('lib/ArrayUtils.js'); const PLUGIN_NAME = 'gotoAnything'; -const itemHeight = 60; class GotoAnything { @@ -38,6 +37,7 @@ class Dialog extends React.PureComponent { keywords: [], listType: BaseModel.TYPE_NOTE, showHelp: false, + resultsInBody: false, }; this.styles_ = {}; @@ -55,14 +55,30 @@ class Dialog extends React.PureComponent { } style() { - if (this.styles_[this.props.theme]) return this.styles_[this.props.theme]; + const styleKey = [this.props.theme, this.state.resultsInBody ? '1' : '0'].join('-'); + + if (this.styles_[styleKey]) return this.styles_[styleKey]; const theme = themeStyle(this.props.theme); - this.styles_[this.props.theme] = { + const itemHeight = this.state.resultsInBody ? 84 : 64; + + this.styles_[styleKey] = { dialogBox: Object.assign({}, theme.dialogBox, { minWidth: '50%', maxWidth: '50%' }), input: Object.assign({}, theme.inputStyle, { flex: 1 }), - row: { overflow: 'hidden', height: itemHeight, display: 'flex', justifyContent: 'center', flexDirection: 'column', paddingLeft: 10, paddingRight: 10 }, + row: { + overflow: 'hidden', + height: itemHeight, + display: 'flex', + justifyContent: 'center', + flexDirection: 'column', + paddingLeft: 10, + paddingRight: 10, + borderBottomWidth: 1, + borderBottomStyle: 'solid', + borderBottomColor: theme.dividerColor, + boxSizing: 'border-box', + }, help: Object.assign({}, theme.textStyle, { marginBottom: 10 }), inputHelpWrapper: { display: 'flex', flexDirection: 'row', alignItems: 'center' }, }; @@ -78,22 +94,23 @@ class Dialog extends React.PureComponent { const rowTitleStyle = Object.assign({}, rowTextStyle, { fontSize: rowTextStyle.fontSize * 1.4, - marginBottom: 4, + marginBottom: this.state.resultsInBody ? 6 : 4, color: theme.colorFaded, }); const rowFragmentsStyle = Object.assign({}, rowTextStyle, { fontSize: rowTextStyle.fontSize * 1.2, - marginBottom: 4, + marginBottom: this.state.resultsInBody ? 8 : 6, color: theme.colorFaded, }); - this.styles_[this.props.theme].rowSelected = Object.assign({}, this.styles_[this.props.theme].row, { backgroundColor: theme.selectedColor }); - this.styles_[this.props.theme].rowPath = rowTextStyle; - this.styles_[this.props.theme].rowTitle = rowTitleStyle; - this.styles_[this.props.theme].rowFragments = rowFragmentsStyle; + this.styles_[styleKey].rowSelected = Object.assign({}, this.styles_[styleKey].row, { backgroundColor: theme.selectedColor }); + this.styles_[styleKey].rowPath = rowTextStyle; + this.styles_[styleKey].rowTitle = rowTitleStyle; + this.styles_[styleKey].rowFragments = rowFragmentsStyle; + this.styles_[styleKey].itemHeight = itemHeight; - return this.styles_[this.props.theme]; + return this.styles_[styleKey]; } componentDidMount() { @@ -144,17 +161,14 @@ class Dialog extends React.PureComponent { }, 10); } - makeSearchQuery(query, field) { + makeSearchQuery(query) { const output = []; - const splitted = (field === 'title') - ? query.split(' ') - : query.substr(1).trim().split(' '); // body + const splitted = query.split(' '); for (let i = 0; i < splitted.length; i++) { const s = splitted[i].trim(); if (!s) continue; - - output.push(field === 'title' ? `title:${s}*` : `body:${s}*`); + output.push(`${s}*`); } return output.join(' '); @@ -166,6 +180,8 @@ class Dialog extends React.PureComponent { } async updateList() { + let resultsInBody = false; + if (!this.state.query) { this.setState({ results: [], keywords: [] }); } else { @@ -187,55 +203,60 @@ class Dialog extends React.PureComponent { const path = Folder.folderPathString(this.props.folders, row.parent_id); results[i] = Object.assign({}, row, { path: path ? path : '/' }); } - } else if (this.state.query.indexOf('/') === 0) { // BODY + } else { // Note TITLE or BODY listType = BaseModel.TYPE_NOTE; - searchQuery = this.makeSearchQuery(this.state.query, 'body'); + searchQuery = this.makeSearchQuery(this.state.query); results = await SearchEngine.instance().search(searchQuery); - const limit = 20; - const searchKeywords = this.keywords(searchQuery); - const notes = await Note.byIds(results.map(result => result.id).slice(0, limit), { fields: ['id', 'body'] }); - const notesById = notes.reduce((obj, { id, body }) => ((obj[[id]] = body), obj), {}); + resultsInBody = !!results.find(row => row.fields.includes('body')); - for (let i = 0; i < results.length; i++) { - const row = results[i]; - let fragments = '...'; - - if (i < limit) { // Display note fragments of search keyword matches - const indices = []; - const body = notesById[row.id]; - - // Iterate over all matches in the body for each search keyword - for (const { valueRegex } of searchKeywords) { - for (const match of body.matchAll(new RegExp(valueRegex, 'ig'))) { - // 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 - indices.push([match.index, nextWhitespaceIndex(body, match.index + match[0].length + 15)]); - if (indices.length > 20) break; - } - } - - // Merge multiple overlapping fragments into a single fragment to prevent repeated content - // e.g. 'Joplin is a free, open source' and 'open source note taking application' - // will result in 'Joplin is a free, open source note taking application' - const mergedIndices = mergeOverlappingIntervals(indices, 3); - fragments = mergedIndices.map(f => body.slice(f[0], f[1])).join(' ... '); - // Add trailing ellipsis if the final fragment doesn't end where the note is ending - if (mergedIndices[mergedIndices.length - 1][1] !== body.length) fragments += ' ...'; + if (!resultsInBody) { + for (let i = 0; i < results.length; i++) { + const row = results[i]; + const path = Folder.folderPathString(this.props.folders, row.parent_id); + results[i] = Object.assign({}, row, { path: path }); } + } else { + const limit = 20; + const searchKeywords = this.keywords(searchQuery); + const notes = await Note.byIds(results.map(result => result.id).slice(0, limit), { fields: ['id', 'body'] }); + const notesById = notes.reduce((obj, { id, body }) => ((obj[[id]] = body), obj), {}); - const path = Folder.folderPathString(this.props.folders, row.parent_id); - results[i] = Object.assign({}, row, { path, fragments }); - } - } else { // TITLE - listType = BaseModel.TYPE_NOTE; - searchQuery = this.makeSearchQuery(this.state.query, 'title'); - results = await SearchEngine.instance().search(searchQuery); + for (let i = 0; i < results.length; i++) { + const row = results[i]; + const path = Folder.folderPathString(this.props.folders, row.parent_id); - for (let i = 0; i < results.length; i++) { - const row = results[i]; - const path = Folder.folderPathString(this.props.folders, row.parent_id); - results[i] = Object.assign({}, row, { path: path }); + if (row.fields.includes('body')) { + let fragments = '...'; + + if (i < limit) { // Display note fragments of search keyword matches + const indices = []; + const body = notesById[row.id]; + + // Iterate over all matches in the body for each search keyword + for (const { valueRegex } of searchKeywords) { + for (const match of body.matchAll(new RegExp(valueRegex, 'ig'))) { + // 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 + indices.push([match.index, nextWhitespaceIndex(body, match.index + match[0].length + 15)]); + if (indices.length > 20) break; + } + } + + // Merge multiple overlapping fragments into a single fragment to prevent repeated content + // e.g. 'Joplin is a free, open source' and 'open source note taking application' + // will result in 'Joplin is a free, open source note taking application' + const mergedIndices = mergeOverlappingIntervals(indices, 3); + fragments = mergedIndices.map(f => body.slice(f[0], f[1])).join(' ... '); + // Add trailing ellipsis if the final fragment doesn't end where the note is ending + if (mergedIndices[mergedIndices.length - 1][1] !== body.length) fragments += ' ...'; + } + + results[i] = Object.assign({}, row, { path, fragments }); + } else { + results[i] = Object.assign({}, row, { path: path, fragments: '' }); + } + } } } @@ -252,6 +273,7 @@ class Dialog extends React.PureComponent { results: results, keywords: this.keywords(searchQuery), selectedItemId: selectedItemId, + resultsInBody: resultsInBody, }); } } @@ -315,12 +337,15 @@ class Dialog extends React.PureComponent { : surroundKeywords(this.state.keywords, item.title, ``, ''); const fragmentsHtml = !item.fragments ? null : surroundKeywords(this.state.keywords, item.fragments, ``, ''); - const pathComp = !item.path ? null :
{item.path}
; + + const folderIcon = ; + const pathComp = !item.path ? null :
{folderIcon} {item.path}
; + const fragmentComp = !fragmentsHtml ? null :
; return (
-
+ {fragmentComp} {pathComp}
); @@ -374,17 +399,19 @@ class Dialog extends React.PureComponent { } renderList() { - const style = { + const style = this.style(); + + const itemListStyle = { marginTop: 5, - height: Math.min(itemHeight * this.state.results.length, 7 * itemHeight), + height: Math.min(style.itemHeight * this.state.results.length, 7 * style.itemHeight), }; return ( ); @@ -393,7 +420,7 @@ class Dialog extends React.PureComponent { render() { const theme = themeStyle(this.props.theme); const style = this.style(); - const helpComp = !this.state.showHelp ? null :
{_('Type a note title to jump to it. Or type # followed by a tag name, or @ followed by a notebook name, or / followed by note content.')}
; + const helpComp = !this.state.showHelp ? null :
{_('Type a note title or part of its content to jump to it. Or type # followed by a tag name, or @ followed by a notebook name.')}
; return (
diff --git a/README.md b/README.md index 50f21cc984..0dd41007f7 100644 --- a/README.md +++ b/README.md @@ -301,7 +301,7 @@ Notes are sorted by "relevance". Currently it means the notes that contain the r # Goto Anything -In the desktop application, press Ctrl+G or Cmd+G and type the title of a note to jump directly to it. You can also type `#` followed by a tag or `@` followed by a notebook title. +In the desktop application, press Ctrl+G or Cmd+G and type a note title or part of its content to jump to it. Or type # followed by a tag name, or @ followed by a notebook name. # Global shortcut diff --git a/ReactNativeClient/lib/joplin-database.js b/ReactNativeClient/lib/joplin-database.js index 4674ab931c..a92ba489ab 100644 --- a/ReactNativeClient/lib/joplin-database.js +++ b/ReactNativeClient/lib/joplin-database.js @@ -124,6 +124,7 @@ class JoplinDatabase extends Database { this.initialized_ = false; this.tableFields_ = null; this.version_ = null; + this.tableFieldNames_ = {}; } initialized() { @@ -136,11 +137,14 @@ class JoplinDatabase extends Database { } tableFieldNames(tableName) { + if (this.tableFieldNames_[tableName]) return this.tableFieldNames_[tableName]; + const tf = this.tableFields(tableName); const output = []; for (let i = 0; i < tf.length; i++) { output.push(tf[i].name); } + this.tableFieldNames_[tableName] = output; return output; } diff --git a/ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/katex.js b/ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/katex.js index c2252d2997..ba9164f073 100644 --- a/ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/katex.js +++ b/ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/katex.js @@ -230,7 +230,7 @@ module.exports = { const katexInline = function(latex) { options.displayMode = false; try { - return `${latex}${renderToStringWithCache(latex, options)}`; + return `${md.utils.escapeHtml(latex)}${renderToStringWithCache(latex, options)}`; } catch (error) { console.error('Katex error for:', latex, error); return latex; @@ -245,7 +245,7 @@ module.exports = { const katexBlock = function(latex) { options.displayMode = true; try { - return `
${latex}
${renderToStringWithCache(latex, options)}
`; + return `
${md.utils.escapeHtml(latex)}
${renderToStringWithCache(latex, options)}
`; } catch (error) { console.error('Katex error for:', latex, error); return latex; diff --git a/ReactNativeClient/lib/models/Note.js b/ReactNativeClient/lib/models/Note.js index cbe186d215..f36f24b3a9 100644 --- a/ReactNativeClient/lib/models/Note.js +++ b/ReactNativeClient/lib/models/Note.js @@ -162,10 +162,12 @@ class Note extends BaseItem { const id = resourceIds[i]; const resource = await Resource.load(id); if (!resource) continue; - const resourcePath = options.useAbsolutePaths ? Resource.fullPath(resource) : Resource.relativePath(resource); + const resourcePath = options.useAbsolutePaths ? `file://${Resource.fullPath(resource)}` : Resource.relativePath(resource); body = body.replace(new RegExp(`:/${id}`, 'gi'), resourcePath); } + this.logger().info('replaceResourceInternalToExternalLinks result', body); + return body; } @@ -176,8 +178,8 @@ class Note extends BaseItem { const pathsToTry = []; if (options.useAbsolutePaths) { - pathsToTry.push(Setting.value('resourceDir')); - pathsToTry.push(shim.pathRelativeToCwd(Setting.value('resourceDir'))); + pathsToTry.push(`file://${Setting.value('resourceDir')}`); + pathsToTry.push(`file://${shim.pathRelativeToCwd(Setting.value('resourceDir'))}`); } else { pathsToTry.push(Resource.baseRelativeDirectoryPath()); } @@ -193,6 +195,8 @@ class Note extends BaseItem { }); } + this.logger().info('replaceResourceExternalToInternalLinks result', body); + return body; } diff --git a/ReactNativeClient/lib/services/SearchEngine.js b/ReactNativeClient/lib/services/SearchEngine.js index c477b94f37..5ea3c999ed 100644 --- a/ReactNativeClient/lib/services/SearchEngine.js +++ b/ReactNativeClient/lib/services/SearchEngine.js @@ -192,16 +192,17 @@ class SearchEngine { return row && row['total'] ? row['total'] : 0; } - columnIndexesFromOffsets_(offsets) { + fieldNamesFromOffsets_(offsets) { + const notesNormalizedFieldNames = this.db().tableFieldNames('notes_normalized'); const occurenceCount = Math.floor(offsets.length / 4); - const indexes = []; - + const output = []; for (let i = 0; i < occurenceCount; i++) { - const colIndex = offsets[i * 4] - 1; - if (indexes.indexOf(colIndex) < 0) indexes.push(colIndex); + const colIndex = offsets[i * 4]; + const fieldName = notesNormalizedFieldNames[colIndex]; + if (!output.includes(fieldName)) output.push(fieldName); } - return indexes; + return output; } calculateWeight_(offsets, termCount) { @@ -234,16 +235,17 @@ class SearchEngine { return occurenceCount / spread; } - orderResults_(rows, parsedQuery) { + processResults_(rows, parsedQuery) { for (let i = 0; i < rows.length; i++) { const row = rows[i]; const offsets = row.offsets.split(' ').map(o => Number(o)); row.weight = this.calculateWeight_(offsets, parsedQuery.termCount); - // row.colIndexes = this.columnIndexesFromOffsets_(offsets); - // row.offsets = offsets; + row.fields = this.fieldNamesFromOffsets_(offsets); } rows.sort((a, b) => { + if (a.fields.includes('title') && !b.fields.includes('title')) return -1; + if (!a.fields.includes('title') && b.fields.includes('title')) return +1; if (a.weight < b.weight) return +1; if (a.weight > b.weight) return -1; if (a.is_todo && a.todo_completed) return +1; @@ -404,7 +406,7 @@ class SearchEngine { const sql = 'SELECT notes_fts.id, notes_fts.title AS normalized_title, offsets(notes_fts) AS offsets, notes.title, notes.user_updated_time, notes.is_todo, notes.todo_completed, notes.parent_id FROM notes_fts LEFT JOIN notes ON notes_fts.id = notes.id WHERE notes_fts MATCH ?'; try { const rows = await this.db().selectAll(sql, [query]); - this.orderResults_(rows, parsedQuery); + this.processResults_(rows, parsedQuery); return rows; } catch (error) { this.logger().warn(`Cannot execute MATCH query: ${query}: ${error.message}`); diff --git a/ReactNativeClient/root.js b/ReactNativeClient/root.js index 4c366a51b0..62ceb66db8 100644 --- a/ReactNativeClient/root.js +++ b/ReactNativeClient/root.js @@ -752,7 +752,7 @@ class AppComponent extends React.Component { }} > - +