diff --git a/.eslintrc b/.eslintrc index 23f3809..8566445 100644 --- a/.eslintrc +++ b/.eslintrc @@ -22,6 +22,7 @@ "comma-dangle": "off", "consistent-return": "off", "default-case": "off", + "dot-location": ["error", "property"], "function-paren-newline": "off", "id-length": "off", "indent": ["error", 2], diff --git a/README.md b/README.md index c5e2ffe..2fa4cdf 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Firefox by WebExtensions API. - [ ] open command - [x] open a link - [ ] search by keywords with engined - - [ ] complete URLs from history + - [x] complete URLs from history - [ ] complete keywords for search - [x] tabs navigation - [x] select a tabs by keyboard diff --git a/manifest.json b/manifest.json index 4e32964..dfb4ae9 100644 --- a/manifest.json +++ b/manifest.json @@ -16,7 +16,8 @@ }, "permissions": [ "sessions", - "tabs" + "tabs", + "history" ], "web_accessible_resources": [ "build/console.html" diff --git a/src/actions/command.js b/src/actions/command.js index 3e6eadb..3e879a6 100644 --- a/src/actions/command.js +++ b/src/actions/command.js @@ -1,4 +1,5 @@ import * as tabs from '../background/tabs'; +import * as histories from '../background/histories'; import * as consoleActions from './console'; const normalizeUrl = (string) => { @@ -39,9 +40,11 @@ const bufferCommand = (keywords) => { const doCommand = (name, remaining) => { switch (name) { + case 'o': case 'open': // TODO use search engined and pass keywords to them return openCommand(normalizeUrl(remaining)); + case 't': case 'tabopen': return tabopenCommand(normalizeUrl(remaining)); case 'b': @@ -53,6 +56,26 @@ const doCommand = (name, remaining) => { const getCompletions = (command, keywords) => { switch (command) { + case 'o': + case 'open': + case 't': + case 'tabopen': + return histories.getCompletions(keywords).then((pages) => { + let items = pages.map((page) => { + return { + caption: page.title, + content: page.url, + url: page.url + }; + }); + return [ + { + name: 'History', + items + } + ]; + }); + case 'b': case 'buffer': return tabs.getCompletions(keywords).then((gotTabs) => { let items = gotTabs.map((tab) => { diff --git a/src/background/histories.js b/src/background/histories.js new file mode 100644 index 0000000..6b6e4c6 --- /dev/null +++ b/src/background/histories.js @@ -0,0 +1,84 @@ +const filterHttp = (items) => { + const httpsHosts = items + .filter(item => item[1].protocol === 'https:') + .map(item => item[1].host); + const httpsHostSet = new Set(httpsHosts); + return items.filter( + item => !(item[1].protocol === 'http:' && httpsHostSet.has(item[1].host)) + ); +}; + +const filterEmptyTitle = (items) => { + return items.filter(item => item[0].title && item[0].title !== ''); +}; + +const filterClosedPath = (items) => { + const allSimplePaths = items + .filter(item => item[1].hash === '' && item[1].search === '') + .map(item => item[1].origin + item[1].pathname); + const allSimplePathSet = new Set(allSimplePaths); + return items.filter( + item => !(item[1].hash === '' && item[1].search === '' && + (/\/$/).test(item[1].pathname) && + allSimplePathSet.has( + (item[1].origin + item[1].pathname).replace(/\/$/, '') + ) + ) + ); +}; + +const reduceByPathname = (items, min) => { + let hash = {}; + for (let item of items) { + let pathname = item[1].origin + item[1].pathname; + if (!hash[pathname]) { + hash[pathname] = item; + } else if (hash[pathname][1].href.length > item[1].href.length) { + hash[pathname] = item; + } + } + let filtered = Object.values(hash); + if (filtered.length < min) { + return items; + } + return filtered; +}; + +const reduceByOrigin = (items, min) => { + let hash = {}; + for (let item of items) { + let origin = item[1].origin; + if (!hash[origin]) { + hash[origin] = item; + } else if (hash[origin][1].href.length > item[1].href.length) { + hash[origin] = item; + } + } + let filtered = Object.values(hash); + if (filtered.length < min) { + return items; + } + return filtered; +}; + +const getCompletions = (keyword) => { + return browser.history.search({ + text: keyword, + startTime: 0, + }).then((historyItems) => { + return [historyItems.map(item => [item, new URL(item.url)])] + .map(filterEmptyTitle) + .map(filterHttp) + .map(filterClosedPath) + .map(items => reduceByPathname(items, 10)) + .map(items => reduceByOrigin(items, 10)) + .map(items => items + .sort((x, y) => x[0].visitCount < y[0].visitCount) + .slice(0, 10) + .map(item => item[0]) + .sort((x, y) => x.url > y.url) + )[0]; + }); +}; + +export { getCompletions }; diff --git a/src/console/console.scss b/src/console/console.scss index 7bb46dd..5823dce 100644 --- a/src/console/console.scss +++ b/src/console/console.scss @@ -50,11 +50,16 @@ body { &-caption { display: inline-block; width: 40%; + text-overflow: ellipsis; + overflow: hidden; } &-url { display: inline-block; color: green; + width: 60%; + text-overflow: ellipsis; + overflow: hidden; } } }