diff --git a/background_scripts/commands.js b/background_scripts/commands.js old mode 100644 new mode 100755 index a6715f5f8..4dac28d1c --- a/background_scripts/commands.js +++ b/background_scripts/commands.js @@ -225,6 +225,7 @@ const Commands = { "Vomnibar.activateBookmarks", "Vomnibar.activateBookmarksInNewTab", "Vomnibar.activateTabSelection", + "Vomnibar.moveTabToWindow", "Vomnibar.activateEditUrl", "Vomnibar.activateEditUrlInNewTab"], findCommands: ["enterFindMode", "performFind", "performBackwardsFind"], @@ -243,7 +244,12 @@ const Commands = { "removeTab", "restoreTab", "moveTabToNewWindow", - "closeTabsOnLeft","closeTabsOnRight", + "closeTabsOnLeft", + "closeOneTabOnLeft", + "closeOneTabOnLeftAndSelf", + "closeTabsOnRight", + "closeOneTabOnRight", + "closeOneTabOnRightAndSelf", "closeOtherTabs", "moveTabLeft", "moveTabRight"], @@ -274,7 +280,11 @@ const Commands = { "moveTabLeft", "moveTabRight", "closeTabsOnLeft", + "closeOneTabOnLeft", + "closeOneTabOnLeftAndSelf", "closeTabsOnRight", + "closeOneTabOnRight", + "closeOneTabOnRightAndSelf", "closeOtherTabs", "enterVisualLineMode", "toggleViewSource", @@ -325,6 +335,7 @@ const defaultKeyMappings = { "o": "Vomnibar.activate", "O": "Vomnibar.activateInNewTab", "T": "Vomnibar.activateTabSelection", + "w": "Vomnibar.moveTabToWindow", "b": "Vomnibar.activateBookmarks", "B": "Vomnibar.activateBookmarksInNewTab", "ge": "Vomnibar.activateEditUrl", @@ -437,7 +448,11 @@ const commandDescriptions = { toggleMuteTab: ["Mute or unmute current tab", { background: true, noRepeat: true }], closeTabsOnLeft: ["Close tabs on the left", {background: true, noRepeat: true}], + closeOneTabOnLeft: ["Close one tab on the left", {background: true}], + closeOneTabOnLeftAndSelf: ["Close one tab on the left and itself", {background: true}], closeTabsOnRight: ["Close tabs on the right", {background: true, noRepeat: true}], + closeOneTabOnRight: ["Close one tab on the right", {background: true}], + closeOneTabOnRightAndSelf: ["Close one tab on the right and itself", {background: true}], closeOtherTabs: ["Close all other tabs", {background: true, noRepeat: true}], moveTabLeft: ["Move tab to the left", { background: true }], @@ -446,6 +461,7 @@ const commandDescriptions = { "Vomnibar.activate": ["Open URL, bookmark or history entry", { topFrame: true }], "Vomnibar.activateInNewTab": ["Open URL, bookmark or history entry in a new tab", { topFrame: true }], "Vomnibar.activateTabSelection": ["Search through your open tabs", { topFrame: true }], + "Vomnibar.moveTabToWindow": ["Move current tab to an existing window", { topFrame: true }], "Vomnibar.activateBookmarks": ["Open a bookmark", { topFrame: true }], "Vomnibar.activateBookmarksInNewTab": ["Open a bookmark in a new tab", { topFrame: true }], "Vomnibar.activateEditUrl": ["Edit the current URL", { topFrame: true }], diff --git a/background_scripts/completion.js b/background_scripts/completion.js index 1f9685cd8..bcc0dfbea 100644 --- a/background_scripts/completion.js +++ b/background_scripts/completion.js @@ -490,6 +490,57 @@ class TabCompleter { } } +// Searches through all windows, matching on title and URL. +class WindowCompleter { + filter({ name, queryTerms }, onComplete) { + if ((name !== "windows") && (queryTerms.length === 0)) + return onComplete([]); + + chrome.windows.getAll({populate: true}, windows => { + chrome.tabs.query({currentWindow: true, active: true}, tabs => { + const curTab = tabs[0]; + // an array of active tabs for each window + const activeTabs = windows.filter(window => (curTab.windowId !== window.id) && (curTab.incognito === window.incognito)) + .map(window => window.tabs.find(tab => tab.active)); + const results = activeTabs.filter(activeTab => RankingUtils.matches(queryTerms, activeTab.url, activeTab.title)); + const suggestions = results.map(activeTab => { + const suggestion = new Suggestion({ + queryTerms, + type: "window", + url: activeTab.url, + title: activeTab.title, + tabId: curTab.id, + windowId: activeTab.windowId, + deDuplicate: false, + }); + suggestion.relevancy = this.computeRelevancy(suggestion); + return suggestion; + }).sort((a, b) => b.relevancy - a.relevancy); + // Boost relevancy with a multiplier so a relevant tab doesn't + // get crowded out by results from competing completers. To + // prevent tabs from crowding out everything else in turn, + // penalize them for being further down the results list by + // scaling on a hyperbola starting at 1 and approaching 0 + // asymptotically for higher indexes. The multiplier and the + // curve fall-off were objectively chosen on the grounds that + // they seem to work pretty well. + suggestions.forEach(function(suggestion,i) { + suggestion.relevancy *= 8; + return suggestion.relevancy /= ( (i / 4) + 1 ); + }); + onComplete(suggestions); + }); + }); + } + + computeRelevancy(suggestion) { + if (suggestion.queryTerms.length) + return RankingUtils.wordRelevancy(suggestion.queryTerms, suggestion.url, suggestion.title); + else + return BgUtils.tabRecency.recencyScore(suggestion.windowId); + } +} + class SearchEngineCompleter { constructor() { this.previousSuggestions = null; @@ -1091,6 +1142,7 @@ Object.assign(global, { HistoryCompleter, DomainCompleter, TabCompleter, + WindowCompleter, SearchEngineCompleter, HistoryCache, RankingUtils, diff --git a/background_scripts/main.js b/background_scripts/main.js old mode 100644 new mode 100755 index dd569d129..bc8f76fb2 --- a/background_scripts/main.js +++ b/background_scripts/main.js @@ -43,6 +43,7 @@ const completionSources = { history: new HistoryCompleter, domains: new DomainCompleter, tabs: new TabCompleter, + windows: new WindowCompleter, searchEngines: new SearchEngineCompleter }; @@ -52,10 +53,12 @@ const completers = { completionSources.history, completionSources.domains, completionSources.tabs, - completionSources.searchEngines - ]), + completionSources.windows, + completionSources.searchEngines, + ]), bookmarks: new MultiCompleter([completionSources.bookmarks]), - tabs: new MultiCompleter([completionSources.tabs]) + tabs: new MultiCompleter([completionSources.tabs]), + windows: new MultiCompleter([completionSources.windows]), }; const completionHandlers = { @@ -225,6 +228,16 @@ const selectSpecificTab = request => chrome.tabs.get(request.id, function(tab) { return chrome.tabs.update(request.id, { active: true }); }); +// +// Move the tab with request.tabId to the window with request.windowId +// +const moveTabToSpecificWindow = request => { + // must focus window first, otherwise focus will shift to address bar + chrome.windows.update(request.windowId, { focused: true }); + chrome.tabs.move(request.tabId, {windowId: request.windowId, index: -1}); + chrome.tabs.update(request.tabId, { active: true }); +}; + const moveTab = function({count, tab, registryEntry}) { if (registryEntry.command === "moveTabLeft") count = -count; @@ -325,7 +338,11 @@ const BackgroundCommands = { }, closeTabsOnLeft(request) { return removeTabsRelative("before", request); }, + closeOneTabOnLeft(request) { return removeTabsRelative("beforeOne", request); }, + closeOneTabOnLeftAndSelf(request) { return removeTabsRelative("beforeOneAndSelf", request); }, closeTabsOnRight(request) { return removeTabsRelative("after", request); }, + closeOneTabOnRight(request) { return removeTabsRelative("afterOne", request); }, + closeOneTabOnRightAndSelf(request) { return removeTabsRelative("afterOneAndSelf", request); }, closeOtherTabs(request) { return removeTabsRelative("both", request); }, visitPreviousTab({count, tab}) { @@ -360,13 +377,21 @@ var forCountTabs = (count, currentTab, callback) => chrome.tabs.query({currentWi }); // Remove tabs before, after, or either side of the currently active tab -var removeTabsRelative = (direction, {tab: activeTab}) => chrome.tabs.query({currentWindow: true}, function(tabs) { +var removeTabsRelative = (direction, {count, tab: activeTab}) => chrome.tabs.query({currentWindow: true}, function(tabs) { const shouldDelete = (() => { switch (direction) { case "before": return index => index < activeTab.index; + case "beforeOne": + return index => index < activeTab.index && index >= activeTab.index - count; + case "beforeOneAndSelf": + return index => index <= activeTab.index && index >= activeTab.index - count; case "after": return index => index > activeTab.index; + case "afterOne": + return index => index > activeTab.index && index <= activeTab.index + count; + case "afterOneAndSelf": + return index => index >= activeTab.index && index <= activeTab.index + count; case "both": return index => index !== activeTab.index; } })(); @@ -629,6 +654,7 @@ var sendRequestHandlers = { frameFocused: handleFrameFocused, nextFrame: BackgroundCommands.nextFrame, selectSpecificTab, + moveTabToSpecificWindow, createMark: Marks.create.bind(Marks), gotoMark: Marks.goto.bind(Marks), // Send a message to all frames in the current tab. diff --git a/content_scripts/mode_normal.js b/content_scripts/mode_normal.js index ad6abd5dd..083bf0ec9 100644 --- a/content_scripts/mode_normal.js +++ b/content_scripts/mode_normal.js @@ -70,8 +70,8 @@ var NormalModeCommands = { scrollToRight() { Scroller.scrollTo("x", "max"); }, scrollUp(count) { Scroller.scrollBy("y", -1 * Settings.get("scrollStepSize") * count); }, scrollDown(count) { Scroller.scrollBy("y", Settings.get("scrollStepSize") * count); }, - scrollPageUp(count) { Scroller.scrollBy("y", "viewSize", (-1/2) * count); }, - scrollPageDown(count) { Scroller.scrollBy("y", "viewSize", (1/2) * count); }, + scrollPageUp(count) { Scroller.scrollBy("y", "viewSize", (-1.2/2) * count); }, + scrollPageDown(count) { Scroller.scrollBy("y", "viewSize", (1.2/2) * count); }, scrollFullPageUp(count) { Scroller.scrollBy("y", "viewSize", -1 * count); }, scrollFullPageDown(count) { Scroller.scrollBy("y", "viewSize", 1 * count); }, scrollLeft(count) { Scroller.scrollBy("x", -1 * Settings.get("scrollStepSize") * count); }, @@ -274,6 +274,7 @@ if (typeof Vomnibar !== 'undefined') { "Vomnibar.activate": Vomnibar.activate.bind(Vomnibar), "Vomnibar.activateInNewTab": Vomnibar.activateInNewTab.bind(Vomnibar), "Vomnibar.activateTabSelection": Vomnibar.activateTabSelection.bind(Vomnibar), + "Vomnibar.moveTabToWindow": Vomnibar.moveTabToWindow.bind(Vomnibar), "Vomnibar.activateBookmarks": Vomnibar.activateBookmarks.bind(Vomnibar), "Vomnibar.activateBookmarksInNewTab": Vomnibar.activateBookmarksInNewTab.bind(Vomnibar), "Vomnibar.activateEditUrl": Vomnibar.activateEditUrl.bind(Vomnibar), diff --git a/content_scripts/vomnibar.js b/content_scripts/vomnibar.js index 883ab4847..e5bac9308 100644 --- a/content_scripts/vomnibar.js +++ b/content_scripts/vomnibar.js @@ -31,6 +31,13 @@ const Vomnibar = { }); }, + moveTabToWindow(sourceFrameId) { + return this.open(sourceFrameId, { + completer: "windows", + selectFirst: true, + }); + }, + activateBookmarks(sourceFrameId) { return this.open(sourceFrameId, { completer: "bookmarks", diff --git a/pages/vomnibar.js b/pages/vomnibar.js index f0caa482e..268035a04 100644 --- a/pages/vomnibar.js +++ b/pages/vomnibar.js @@ -365,7 +365,8 @@ class BackgroundCompleter { this.name = name; this.completionActions = { navigateToUrl(url) { return openInNewTab => Vomnibar.getCompleter().launchUrl(url, openInNewTab); }, - switchToTab(tabId) { return () => chrome.runtime.sendMessage({handler: "selectSpecificTab", id: tabId}); } + switchToTab(tabId) { return () => chrome.runtime.sendMessage({handler: "selectSpecificTab", id: tabId}); }, + moveToWindow(tabId, windowId) { return () => chrome.runtime.sendMessage({handler: "moveTabToSpecificWindow", tabId: tabId, windowId: windowId}); } }; this.port = chrome.runtime.connect({name: "completions"}); @@ -381,14 +382,17 @@ class BackgroundCompleter { if (msg.id === this.messageId) { // The result objects coming from the background page will be of the form: // { html: "", type: "", url: "", ... } - // Type will be one of [tab, bookmark, history, domain, search], or a custom search engine description. + // Type will be one of [tab, bookmark, history, domain, search, window], or a custom search engine description. for (let result of msg.results) { - Object.assign(result, { - performAction: - result.type === "tab" ? - this.completionActions.switchToTab(result.tabId) : - this.completionActions.navigateToUrl(result.url) - }); + let completionAction; + if (result.type === "tab") { + completionAction = this.completionActions.switchToTab(result.tabId); + } else if (result.type === "window") { + completionAction = this.completionActions.moveToWindow(result.tabId, result.windowId); + } else { + completionAction = this.completionActions.navigateToUrl(result.url); + } + Object.assign(result, { performAction: completionAction }); } // Handle the message, but only if it hasn't arrived too late.