Skip to content
This repository has been archived by the owner on Dec 11, 2019. It is now read-only.

Bloodhound for URL bar suggestions & move suggestions to browser process #8824

Merged
merged 22 commits into from
May 18, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
258b937
Tokenize urlbar suggestions and use bloodhound
bbondy May 10, 2017
5b59bfe
Change URL input entered into an app action
bbondy May 10, 2017
cfeb861
Don't store suggestion click handlers in state
bbondy May 10, 2017
dd1bb22
Change URL suggestion generation to async
bbondy May 11, 2017
be7ad9f
Split out suggestion generation into parts
bbondy May 12, 2017
c215ab3
Make suggestions independent of windowState
bbondy May 12, 2017
9cf76c0
Default search engine windowState -> appState
bbondy May 12, 2017
9d49e4b
Move suggestion files to common app dir
bbondy May 12, 2017
2d719a7
Move suggestion creation into the browser process
bbondy May 13, 2017
4d89a15
Use bloodhound lib for suggestions
bbondy May 14, 2017
6fe984d
Respect HISTORY and BOOKMARK suggestion settings
bbondy May 14, 2017
b02eeb3
Support history after bloodhound init
bbondy May 14, 2017
7ff4a15
Add tests for urlBarSuggestionsReducer
bbondy May 14, 2017
08fd817
Fix search suggestions in browser process
bbondy May 16, 2017
eeda733
Debounce on suggestion generation
bbondy May 16, 2017
6d3cade
Remove some imperative actions for clearing suggestion selectedIndex
bbondy May 16, 2017
46c7bf0
Tokenize with urlParse and for non urls
bbondy May 16, 2017
1dab48d
Fix some failing tests post suggestion changes
bbondy May 17, 2017
b37785e
Improve sorting of results
bbondy May 18, 2017
afd891f
Tab should cycle between results
bbondy May 18, 2017
ff41f60
Don't do suggestion actions on non-char key presses
bbondy May 18, 2017
f94239a
Match entered paths better
bbondy May 18, 2017
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions app/browser/reducers/urlBarSuggestionsReducer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */

'use strict'

const appConstants = require('../../../js/constants/appConstants')
const {generateNewSuggestionsList, generateNewSearchXHRResults} = require('../../common/lib/suggestion')
const {init, add} = require('../../common/lib/siteSuggestions')
const Immutable = require('immutable')
const {makeImmutable} = require('../../common/state/immutableUtil')
const tabState = require('../../common/state/tabState')

const urlBarSuggestionsReducer = (state, action) => {
switch (action.actionType) {
case appConstants.APP_ADD_SITE:
if (Immutable.List.isList(action.siteDetail)) {
action.siteDetail.forEach((s) => {
add(s)
})
} else {
add(action.siteDetail)
}
break
case appConstants.APP_SET_STATE:
init(Object.values(action.appState.get('sites').toJS()))
break
case appConstants.APP_URL_BAR_TEXT_CHANGED:
generateNewSuggestionsList(state, action.windowId, action.tabId, action.input)
generateNewSearchXHRResults(state, action.windowId, action.tabId, action.input)
break
case appConstants.APP_SEARCH_SUGGESTION_RESULTS_AVAILABLE:
state = state.set('searchResults', makeImmutable(action.searchResults))
if (action.query) {
const windowId = tabState.windowId(state, action.tabId)
generateNewSuggestionsList(state, windowId, action.tabId, action.query)
}
break
}
return state
}

module.exports = urlBarSuggestionsReducer
32 changes: 32 additions & 0 deletions app/common/lib/fetchSearchSuggestions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */

const appActions = require('../../../js/actions/appActions')
const {request} = require('../../../js/lib/request')
const debounce = require('../../../js/lib/debounce')

const fetchSearchSuggestions = debounce((windowId, tabId, autocompleteURL, searchTerms) => {
autocompleteURL.replace('{searchTerms}', encodeURIComponent(searchTerms))
request(autocompleteURL.replace('{searchTerms}', encodeURIComponent(searchTerms)), (err, response, body) => {
if (err) {
return
}

let searchResults
let query
try {
const parsed = JSON.parse(body)
query = parsed[0]
searchResults = parsed[1]
} catch (e) {
console.warn(e)
return
}

// Once we have the online suggestions, append them to the others
appActions.searchSuggestionResultsAvailable(tabId, query, searchResults)
})
}, 10)

module.exports = fetchSearchSuggestions
138 changes: 138 additions & 0 deletions app/common/lib/siteSuggestions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */

const Bloodhound = require('bloodhound-js')
const {isUrl} = require('../../../js/lib/appUrlUtil')
const siteTags = require('../../../js/constants/siteTags')
const urlParse = require('../urlParse')

let initialized = false
let engine
let lastQueryOptions

// Same as sortByAccessCountWithAgeDecay but if one is a prefix of the
// other then it is considered always sorted first.
const sortForSuggestions = (s1, s2) => {
return lastQueryOptions.internalSort(s1, s2)
}

const getSiteIdentity = (data) => {
if (typeof data === 'string') {
return data
}
return (data.location || '') + (data.partitionNumber ? '|' + data.partitionNumber : '')
}

const init = (sites) => {
engine = new Bloodhound({
local: sites.toJS ? sites.toJS() : sites,
sorter: sortForSuggestions,
queryTokenizer: tokenizeInput,
datumTokenizer: tokenizeInput,
identify: getSiteIdentity
})
const promise = engine.initialize()
promise.then(() => {
initialized = true
})
return promise
}

const getPartsFromNonUrlInput = (input) =>
input.toLowerCase().split(/[,-.\s\\/?&]/)

const getTagToken = (tag) => '|' + tag + '|'

const tokenizeInput = (data) => {
let url = data || ''
let parts = []

const isSiteObject = typeof data === 'object' && data !== null
if (isSiteObject) {
// When lastAccessTime is 1 it is a default built-in entry which we don't want
// to appear in suggestions.
if (data.lastAccessedTime === 1) {
return []
}
url = data.location
if (data.title) {
parts = getPartsFromNonUrlInput(data.title)
}
if (data.tags) {
parts = parts.concat(data.tags.map(getTagToken))
}
} else {
if (lastQueryOptions && !lastQueryOptions.historySuggestionsOn && lastQueryOptions.bookmarkSuggestionsOn) {
parts.push(getTagToken(siteTags.BOOKMARK))
}
}

if (url && isUrl(url)) {
const parsedUrl = urlParse(url.toLowerCase())
// Cache parsed value for latter use when sorting
if (isSiteObject) {
data.parsedUrl = parsedUrl
}
if (parsedUrl.hash) {
parts.push(parsedUrl.hash.slice(1))
}
if (parsedUrl.host) {
parts = parts.concat(parsedUrl.host.split('.'))
}
if (parsedUrl.pathname) {
parts = parts.concat(parsedUrl.pathname.split(/[.\s\\/]/))
}
if (parsedUrl.query) {
parts = parts.concat(parsedUrl.query.split(/[&=]/))
}
if (parsedUrl.protocol) {
parts = parts.concat(parsedUrl.protocol)
}
} else if (url) {
parts = parts.concat(getPartsFromNonUrlInput(url))
}
return parts.filter(x => !!x)
}

const add = (data) => {
if (!initialized) {
return
}
if (typeof data === 'string') {
engine.add(data)
} else {
engine.add(data.toJS ? data.toJS() : data)
}
}

const query = (input, options = {}) => {
if (!initialized) {
return Promise.resolve([])
}

return new Promise((resolve, reject) => {
const {getSortForSuggestions} = require('./suggestion')
input = (input || '').toLowerCase()
lastQueryOptions = Object.assign({}, options, {
input,
internalSort: getSortForSuggestions(input)
})
if (lastQueryOptions.historySuggestionsOn !== false || lastQueryOptions.bookmarkSuggestionsOn !== false) {
engine.search(input, function (results) {
resolve(results)
}, function (err) {
reject(err)
})
} else {
resolve([])
}
})
}

module.exports = {
init,
add,
tokenizeInput,
query
}
Loading