diff --git a/src/plugins/navigation/__tests__/testAutoCompleteSearch.node.js b/src/plugins/navigation/__tests__/testAutoCompleteSearch.node.js new file mode 100644 index 000000000..06a977c96 --- /dev/null +++ b/src/plugins/navigation/__tests__/testAutoCompleteSearch.node.js @@ -0,0 +1,112 @@ +/** + * Copyright 2018-present Facebook. + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * @format + * @flow strict-local + */ + +import {filterMatchPatterns} from '../util/autoCompleteProvider'; + +import type {URI} from '../flow-types'; + +// choose all k length combinations from array +const stringCombination: (Array, number) => Array = ( + patterns, + k, +) => { + const n = patterns.length; + const returnArr = new Array(0); + const args = new Array(k).fill(0).map((_, idx) => idx); + (function build(args) { + const pattern = args.map(i => patterns[i]).join(''); + returnArr.push(pattern); + if (args[args.length - 1] < n - 1) { + for (let i = args.length - 1; i >= 0; i--) { + const newArgs = args.map((value, idx) => + idx >= i ? value + 1 : value, + ); + build(newArgs); + } + } + })(args); + return returnArr; +}; + +// Create a map of 364 pairs +const constructMatchPatterns: () => Map = () => { + const matchPatterns = new Map(); + + const NUM_PATERNS_PER_ENTRY = 3; + + const patterns = [ + 'abcdefghijklmnopqrstuvwxy', + 'ababababababababababababa', + 'cdcdcdcdcdcdcdcdcdcdcdcdc', + 'efefefefefefefefefefefefe', + 'ghghghghghghghghghghghghg', + 'ijijijijijijijijijijijiji', + 'klklklklklklklklklklklklk', + 'mnmnmnmnmnmnmnmnmnmnmnmnm', + 'opopopopopopopopopopopopo', + 'qrqrqrqrqrqrqrqrqrqrqrqrq', + 'ststststststststststststs', + 'uvuvuvuvuvuvuvuvuvuvuvuvu', + 'wxwxwxwxwxwxwxwxwxwxwxwxw', + 'yzyzyzyzyzyzyzyzyzyzyzyzy', + ]; + + stringCombination(patterns, NUM_PATERNS_PER_ENTRY).forEach(pattern => + matchPatterns.set(pattern, pattern), + ); + + return matchPatterns; +}; + +test('construct match patterns', () => { + const matchPatterns = constructMatchPatterns(); + expect(matchPatterns.size).toBe(364); +}); + +test('search for abcdefghijklmnopqrstuvwxy in matchPatterns', () => { + const matchPatterns = constructMatchPatterns(); + const filteredMatchPatterns = filterMatchPatterns( + matchPatterns, + 'abcdefghijklmnopqrstuvwxy', + Infinity, + ); + // Fixing abcdefghijklmnopqrstuvwxy, we have 13C2 = 78 patterns that will match + expect(filteredMatchPatterns.size).toBe(78); +}); + +test('search for ????? in matchPatterns', () => { + const matchPatterns = constructMatchPatterns(); + const filteredMatchPatterns = filterMatchPatterns( + matchPatterns, + '?????', + Infinity, + ); + // ????? Does not exist in our seach so should return 0 + expect(filteredMatchPatterns.size).toBe(0); +}); + +test('search for abcdefghijklmnopqrstuvwxyababababababababababababacdcdcdcdcdcdcdcdcdcdcdcdc in matchPatterns', () => { + const matchPatterns = constructMatchPatterns(); + const filteredMatchPatterns = filterMatchPatterns( + matchPatterns, + 'abcdefghijklmnopqrstuvwxyababababababababababababacdcdcdcdcdcdcdcdcdcdcdcdc', + Infinity, + ); + // Should only appear once in our patterns + expect(filteredMatchPatterns.size).toBe(1); +}); + +test('find first five occurences of abcdefghijklmnopqrstuvwxy', () => { + const matchPatterns = constructMatchPatterns(); + const filteredMatchPatterns = filterMatchPatterns( + matchPatterns, + 'abcdefghijklmnopqrstuvwxy', + 5, + ); + expect(filteredMatchPatterns.size).toBe(5); +}); diff --git a/src/plugins/navigation/flow-types.js b/src/plugins/navigation/flow-types.js index 38c3ade0d..f926eb65a 100644 --- a/src/plugins/navigation/flow-types.js +++ b/src/plugins/navigation/flow-types.js @@ -6,10 +6,12 @@ * @flow strict-local */ +export type URI = string; + export type State = {| - bookmarks: Map, + bookmarks: Map, shouldShowSaveBookmarkDialog: boolean, - saveBookmarkURI: ?string, + saveBookmarkURI: ?URI, |}; export type PersistedState = {| @@ -18,10 +20,21 @@ export type PersistedState = {| export type NavigationEvent = {| date: ?Date, - uri: ?string, + uri: ?URI, |}; export type Bookmark = {| - uri: string, + uri: URI, commonName: string, |}; + +export type AutoCompleteProvider = {| + icon: string, + matchPatterns: Map, +|}; + +export type AutoCompleteLineItem = {| + icon: string, + matchPattern: string, + uri: URI, +|}; diff --git a/src/plugins/navigation/util/autoCompleteProvider.js b/src/plugins/navigation/util/autoCompleteProvider.js new file mode 100644 index 000000000..41abbfb52 --- /dev/null +++ b/src/plugins/navigation/util/autoCompleteProvider.js @@ -0,0 +1,76 @@ +/** + * Copyright 2018-present Facebook. + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * @format + * @flow strict-local + */ + +import type { + URI, + Bookmark, + AutoCompleteProvider, + AutoCompleteLineItem, +} from '../flow-types'; + +export const bookmarksToAutoCompleteProvider: ( + Map, +) => AutoCompleteProvider = bookmarks => { + const autoCompleteProvider = { + icon: 'bookmark', + matchPatterns: new Map(), + }; + bookmarks.forEach((bookmark, uri) => { + const matchPattern = bookmark.commonName + ' - ' + uri; + autoCompleteProvider.matchPatterns.set(matchPattern, uri); + }); + return autoCompleteProvider; +}; + +export const filterMatchPatterns: ( + Map, + string, + number, +) => Map = (matchPatterns, query, maxItems) => { + const filteredPatterns = new Map(); + for (const [pattern, uri] of matchPatterns) { + if (filteredPatterns.size >= maxItems) { + break; + } else if (pattern.toLowerCase().includes(query.toLowerCase())) { + filteredPatterns.set(pattern, uri); + } + } + return filteredPatterns; +}; + +const filterProvider: ( + AutoCompleteProvider, + string, + number, +) => AutoCompleteProvider = (provider, query, maxItems) => { + return { + ...provider, + matchPatterns: filterMatchPatterns(provider.matchPatterns, query, maxItems), + }; +}; + +export const filterProvidersToLineItems: ( + Array, + string, + number, +) => Array = (providers, query, maxItems) => { + let itemsLeft = maxItems; + const lineItems = new Array(0); + for (const provider of providers) { + const filteredProvider = filterProvider(provider, query, itemsLeft); + filteredProvider.matchPatterns.forEach((uri, matchPattern) => { + lineItems.unshift({ + icon: provider.icon, + matchPattern, + uri, + }); + }); + itemsLeft -= filteredProvider.matchPatterns.size; + } + return lineItems; +};