You've already forked joplin
							
							
				mirror of
				https://github.com/laurent22/joplin.git
				synced 2025-10-31 00:07:48 +02:00 
			
		
		
		
	* Go to anything by body * Made limit parameter required * Made parameter required Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
This commit is contained in:
		
				
					committed by
					
						 GitHub
						GitHub
					
				
			
			
				
	
			
			
			
						parent
						
							d54e52b1a8
						
					
				
				
					commit
					a45128807e
				
			| @@ -49,4 +49,41 @@ describe('ArrayUtils', function() { | ||||
| 		expect(ArrayUtils.contentEquals(['b'], ['a', 'b'])).toBe(false); | ||||
| 	})); | ||||
|  | ||||
| 	it('should merge overlapping intervals', asyncTest(async () => { | ||||
| 		const testCases = [ | ||||
| 			[ | ||||
| 				[], | ||||
| 				[], | ||||
| 			], | ||||
| 			[ | ||||
| 				[[0, 50]], | ||||
| 				[[0, 50]], | ||||
| 			], | ||||
| 			[ | ||||
| 				[[0, 20], [20, 30]], | ||||
| 				[[0, 30]], | ||||
| 			], | ||||
| 			[ | ||||
| 				[[0, 10], [10, 50], [15, 30], [20, 80], [80, 95]], | ||||
| 				[[0, 95]], | ||||
| 			], | ||||
| 			[ | ||||
| 				[[0, 5], [0, 10], [25, 35], [30, 60], [50, 60], [85, 100]], | ||||
| 				[[0, 10], [25, 60], [85, 100]], | ||||
| 			], | ||||
| 			[ | ||||
| 				[[0, 5], [10, 40], [35, 50], [35, 75], [50, 60], [80, 85], [80, 90]], | ||||
| 				[[0, 5], [10, 75], [80, 90]], | ||||
| 			], | ||||
| 		]; | ||||
|  | ||||
| 		testCases.forEach((t, i) => { | ||||
| 			const intervals = t[0]; | ||||
| 			const expected = t[1]; | ||||
|  | ||||
| 			const actual = ArrayUtils.mergeOverlappingIntervals(intervals, intervals.length); | ||||
| 			expect(actual).toEqual(expected, `Test case ${i}`); | ||||
| 		}); | ||||
| 	})); | ||||
|  | ||||
| }); | ||||
|   | ||||
| @@ -41,4 +41,23 @@ describe('StringUtils', function() { | ||||
| 		} | ||||
| 	})); | ||||
|  | ||||
| 	it('should find the next whitespace character', asyncTest(async () => { | ||||
| 		const testCases = [ | ||||
| 			['', [[0, 0]]], | ||||
| 			['Joplin', [[0, 6], [3, 6], [6, 6]]], | ||||
| 			['Joplin is a free, open source\n note taking and *to-do* application', [[0, 6], [12, 17], [23, 29], [48, 54]]], | ||||
| 		]; | ||||
|  | ||||
| 		testCases.forEach((t, i) => { | ||||
| 			const str = t[0]; | ||||
| 			t[1].forEach((pair, j) => { | ||||
| 				const begin = pair[0]; | ||||
| 				const expected = pair[1]; | ||||
|  | ||||
| 				const actual = StringUtils.nextWhitespaceIndex(str, begin); | ||||
| 				expect(actual).toBe(expected, `Test string ${i} - case ${j}`); | ||||
| 			}); | ||||
| 		}); | ||||
| 	})); | ||||
|  | ||||
| }); | ||||
|   | ||||
| @@ -6,10 +6,11 @@ const SearchEngine = require('lib/services/SearchEngine'); | ||||
| const BaseModel = require('lib/BaseModel'); | ||||
| const Tag = require('lib/models/Tag'); | ||||
| const Folder = require('lib/models/Folder'); | ||||
| const Note = require('lib/models/Note'); | ||||
| const { ItemList } = require('../gui/ItemList.min'); | ||||
| const HelpButton = require('../gui/HelpButton.min'); | ||||
| const { surroundKeywords } = require('lib/string-utils.js'); | ||||
|  | ||||
| const { surroundKeywords, nextWhitespaceIndex } = require('lib/string-utils.js'); | ||||
| const { mergeOverlappingIntervals } = require('lib/ArrayUtils.js'); | ||||
| const PLUGIN_NAME = 'gotoAnything'; | ||||
| const itemHeight = 60; | ||||
|  | ||||
| @@ -76,13 +77,20 @@ class Dialog extends React.PureComponent { | ||||
|  | ||||
| 		const rowTitleStyle = Object.assign({}, rowTextStyle, { | ||||
| 			fontSize: rowTextStyle.fontSize * 1.4, | ||||
| 			marginBottom: 5, | ||||
| 			marginBottom: 4, | ||||
| 			color: theme.colorFaded, | ||||
| 		}); | ||||
|  | ||||
| 		const rowFragmentsStyle = Object.assign({}, rowTextStyle, { | ||||
| 			fontSize: rowTextStyle.fontSize * 1.2, | ||||
| 			marginBottom: 4, | ||||
| 			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; | ||||
|  | ||||
| 		return this.styles_[this.props.theme]; | ||||
| 	} | ||||
| @@ -125,14 +133,17 @@ class Dialog extends React.PureComponent { | ||||
| 		}, 10); | ||||
| 	} | ||||
|  | ||||
| 	makeSearchQuery(query) { | ||||
| 		const splitted = query.split(' '); | ||||
| 	makeSearchQuery(query, field) { | ||||
| 		const output = []; | ||||
| 		const splitted = (field === 'title') | ||||
| 			? query.split(' ') | ||||
| 			: query.substr(1).trim().split(' '); // body | ||||
|  | ||||
| 		for (let i = 0; i < splitted.length; i++) { | ||||
| 			const s = splitted[i].trim(); | ||||
| 			if (!s) continue; | ||||
|  | ||||
| 			output.push(`title:${s}*`); | ||||
| 			output.push(field === 'title' ? `title:${s}*` : `body:${s}*`); | ||||
| 		} | ||||
|  | ||||
| 		return output.join(' '); | ||||
| @@ -165,9 +176,49 @@ class Dialog extends React.PureComponent { | ||||
| 					const path = Folder.folderPathString(this.props.folders, row.parent_id); | ||||
| 					results[i] = Object.assign({}, row, { path: path ? path : '/' }); | ||||
| 				} | ||||
| 			} else { // NOTES | ||||
| 			} else if (this.state.query.indexOf('/') === 0) { // BODY | ||||
| 				listType = BaseModel.TYPE_NOTE; | ||||
| 				searchQuery = this.makeSearchQuery(this.state.query); | ||||
| 				searchQuery = this.makeSearchQuery(this.state.query, 'body'); | ||||
| 				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), {}); | ||||
|  | ||||
| 				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 += ' ...'; | ||||
| 					} | ||||
|  | ||||
| 					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++) { | ||||
| @@ -248,13 +299,17 @@ class Dialog extends React.PureComponent { | ||||
| 		const theme = themeStyle(this.props.theme); | ||||
| 		const style = this.style(); | ||||
| 		const rowStyle = item.id === this.state.selectedItemId ? style.rowSelected : style.row; | ||||
| 		const titleHtml = surroundKeywords(this.state.keywords, item.title, `<span style="font-weight: bold; color: ${theme.colorBright};">`, '</span>'); | ||||
| 		const titleHtml = item.fragments | ||||
| 			? `<span style="font-weight: bold; color: ${theme.colorBright};">${item.title}</span>` | ||||
| 			: surroundKeywords(this.state.keywords, item.title, `<span style="font-weight: bold; color: ${theme.colorBright};">`, '</span>'); | ||||
|  | ||||
| 		const fragmentsHtml = !item.fragments ? null : surroundKeywords(this.state.keywords, item.fragments, `<span style="font-weight: bold; color: ${theme.colorBright};">`, '</span>'); | ||||
| 		const pathComp = !item.path ? null : <div style={style.rowPath}>{item.path}</div>; | ||||
|  | ||||
| 		return ( | ||||
| 			<div key={item.id} style={rowStyle} onClick={this.listItem_onClick} data-id={item.id} data-parent-id={item.parent_id}> | ||||
| 				<div style={style.rowTitle} dangerouslySetInnerHTML={{ __html: titleHtml }}></div> | ||||
| 				<div style={style.rowFragments} dangerouslySetInnerHTML={{ __html: fragmentsHtml }}></div> | ||||
| 				{pathComp} | ||||
| 			</div> | ||||
| 		); | ||||
| @@ -327,7 +382,7 @@ class Dialog extends React.PureComponent { | ||||
| 	render() { | ||||
| 		const theme = themeStyle(this.props.theme); | ||||
| 		const style = this.style(); | ||||
| 		const helpComp = !this.state.showHelp ? null : <div style={style.help}>{_('Type a note title to jump to it. Or type # followed by a tag name, or @ followed by a notebook name.')}</div>; | ||||
| 		const helpComp = !this.state.showHelp ? null : <div style={style.help}>{_('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.')}</div>; | ||||
|  | ||||
| 		return ( | ||||
| 			<div style={theme.dialogModalLayer}> | ||||
|   | ||||
| @@ -58,4 +58,26 @@ ArrayUtils.contentEquals = function(array1, array2) { | ||||
| 	return true; | ||||
| }; | ||||
|  | ||||
| // Merges multiple overlapping intervals into a single interval | ||||
| // e.g. [0, 25], [20, 50], [75, 100] --> [0, 50], [75, 100] | ||||
| ArrayUtils.mergeOverlappingIntervals = function(intervals, limit) { | ||||
| 	intervals.sort((a, b) => a[0] - b[0]); | ||||
|  | ||||
| 	const stack = []; | ||||
| 	if (intervals.length) { | ||||
| 		stack.push(intervals[0]); | ||||
| 		for (let i = 1; i < intervals.length && stack.length < limit; i++) { | ||||
| 			const top = stack[stack.length - 1]; | ||||
| 			if (top[1] < intervals[i][0]) { | ||||
| 				stack.push(intervals[i]); | ||||
| 			} else if (top[1] < intervals[i][1]) { | ||||
| 				top[1] = intervals[i][1]; | ||||
| 				stack.pop(); | ||||
| 				stack.push(top); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	return stack; | ||||
| }; | ||||
|  | ||||
| module.exports = ArrayUtils; | ||||
|   | ||||
| @@ -264,6 +264,12 @@ function substrWithEllipsis(s, start, length) { | ||||
| 	return `${s.substr(start, length - 3)}...`; | ||||
| } | ||||
|  | ||||
| function nextWhitespaceIndex(s, begin) { | ||||
| 	// returns index of the next whitespace character | ||||
| 	const i = s.slice(begin).search(/\s/); | ||||
| 	return i < 0 ? s.length : begin + i; | ||||
| } | ||||
|  | ||||
| const REGEX_JAPANESE = /[\u3000-\u303f]|[\u3040-\u309f]|[\u30a0-\u30ff]|[\uff00-\uff9f]|[\u4e00-\u9faf]|[\u3400-\u4dbf]/; | ||||
| const REGEX_CHINESE = /[\u4e00-\u9fff]|[\u3400-\u4dbf]|[\u{20000}-\u{2a6df}]|[\u{2a700}-\u{2b73f}]|[\u{2b740}-\u{2b81f}]|[\u{2b820}-\u{2ceaf}]|[\uf900-\ufaff]|[\u3300-\u33ff]|[\ufe30-\ufe4f]|[\uf900-\ufaff]|[\u{2f800}-\u{2fa1f}]/u; | ||||
| const REGEX_KOREAN = /[\uac00-\ud7af]|[\u1100-\u11ff]|[\u3130-\u318f]|[\ua960-\ua97f]|[\ud7b0-\ud7ff]/; | ||||
| @@ -279,4 +285,4 @@ function scriptType(s) { | ||||
| 	return 'en'; | ||||
| } | ||||
|  | ||||
| module.exports = Object.assign({ removeDiacritics, substrWithEllipsis, escapeFilename, wrap, splitCommandString, padLeft, toTitleCase, urlDecode, escapeHtml, surroundKeywords, scriptType, commandArgumentsToString }, stringUtilsCommon); | ||||
| module.exports = Object.assign({ removeDiacritics, substrWithEllipsis, nextWhitespaceIndex, escapeFilename, wrap, splitCommandString, padLeft, toTitleCase, urlDecode, escapeHtml, surroundKeywords, scriptType, commandArgumentsToString }, stringUtilsCommon); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user