-
Notifications
You must be signed in to change notification settings - Fork 2.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Expose global commands at chrome://extensions/shortcuts #3785
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,6 +10,7 @@ class TabRecency { | |
this.cache = {}; | ||
this.lastVisited = null; | ||
this.lastVisitedTime = null; | ||
this.jumpList = null; | ||
|
||
chrome.tabs.onActivated.addListener(activeInfo => this.register(activeInfo.tabId)); | ||
chrome.tabs.onRemoved.addListener(tabId => this.deregister(tabId)); | ||
|
@@ -31,6 +32,31 @@ class TabRecency { | |
} | ||
} | ||
|
||
getJumpBackTabId({count}) { | ||
let backTabId = -1; | ||
if (!this.jumpList) { | ||
// getTabsByRecency might not include the current tab, eg if it was just | ||
// opened. Tabs aren't added until they have been seen for some time and | ||
// then an event is fired (like navigating back to the window). Add the | ||
// current tab if it hasn't been added. | ||
const tabs = this.getTabsByRecency().reverse(); | ||
if (tabs.length > 0 && tabs[tabs.length - 1] !== this.current) { | ||
tabs.push(this.current); | ||
} | ||
this.jumpList = new TabJumpList(tabs); | ||
} | ||
backTabId = this.jumpList.getJumpBackTabId({count}); | ||
return backTabId === -1 ? this.current : backTabId; | ||
} | ||
|
||
getJumpForwardTabId({count}) { | ||
let forwardTabId = -1; | ||
if (this.jumpList) { | ||
forwardTabId = this.jumpList.getJumpForwardTabId({count}); | ||
} | ||
return forwardTabId === -1 ? this.current : forwardTabId; | ||
} | ||
|
||
register(tabId) { | ||
const currentTime = new Date(); | ||
// Register tabId if it has been visited for at least @timeDelta ms. Tabs which are visited only for a | ||
|
@@ -39,6 +65,10 @@ class TabRecency { | |
this.cache[this.lastVisited] = ++this.timestamp; | ||
} | ||
|
||
if (this.jumpList && !this.jumpList.isCoherent(tabId)) { | ||
this.jumpList = null; | ||
} | ||
|
||
this.current = (this.lastVisited = tabId); | ||
this.lastVisitedTime = currentTime; | ||
} | ||
|
@@ -49,6 +79,11 @@ class TabRecency { | |
this.lastVisited = (this.lastVisitedTime = null); | ||
} | ||
delete this.cache[tabId]; | ||
|
||
if (this.jumpList) { | ||
const jumpListInvalidated = this.jumpList.deregister(tabId); | ||
if (jumpListInvalidated) this.jumpList = null; | ||
} | ||
} | ||
|
||
// Recently-visited tabs get a higher score (except the current tab, which gets a low score). | ||
|
@@ -69,6 +104,84 @@ class TabRecency { | |
} | ||
} | ||
|
||
// TabJumpList maintains a list of visited tabs. When no jumps have occurred, | ||
// the list is all open tabs--the current (i.e. most recently visited) tab is | ||
// the last element. The tab visited the longest in the past is the 0th tab. | ||
// Jumping backwards moves through the open tabs. The index is maintained, | ||
// allowing jumping forward to move again back to newer tabs. A manual | ||
// navigation through a mechanism other than a jump resets the jump list. | ||
class TabJumpList { | ||
|
||
constructor(tabs) { | ||
this.tabs = tabs; | ||
this.activeIdx = tabs.length - 1; | ||
// Tabs can be deleted after a TabJumpList has flattened the tabs into an | ||
// array. Rather than O(N) look through the tabs, we'll just maintain | ||
// deleted IDs and skip them. | ||
this.deletedTabs = new Set(); | ||
} | ||
|
||
isCoherent(currentTabId) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did you mean |
||
return this.tabs[this.activeIdx] === currentTabId; | ||
} | ||
|
||
// Returns true if deregistering this tab invalidates the jump list. | ||
deregister(tabId) { | ||
if (this.tabs[this.activeIdx] == tabId) return true; | ||
|
||
this.deletedTabs.add(tabId); | ||
return false; | ||
} | ||
|
||
getJumpBackTabId({count = 1}) { | ||
let candidateIdx = -1; | ||
let need = count; | ||
for (let i = this.activeIdx - 1; i >= 0; i--) { | ||
let candidateId = this.tabs[i]; | ||
if (this.deletedTabs.has(candidateId)) { | ||
continue; | ||
} | ||
candidateIdx = i; | ||
need--; | ||
if (need <= 0) { | ||
break; | ||
} | ||
} | ||
|
||
if (candidateIdx === -1) { | ||
// We're at the oldest tab. | ||
return -1; | ||
} | ||
|
||
this.activeIdx = candidateIdx; | ||
return this.tabs[this.activeIdx]; | ||
} | ||
|
||
getJumpForwardTabId({count = 1}) { | ||
let candidateIdx = -1; | ||
let need = count; | ||
for (let i = this.activeIdx + 1; i < this.tabs.length; i++) { | ||
let candidateId = this.tabs[i]; | ||
if (this.deletedTabs.has(candidateId)) { | ||
continue; | ||
} | ||
candidateIdx = i; | ||
need--; | ||
if (need <= 0) { | ||
break; | ||
} | ||
} | ||
|
||
if (candidateIdx === -1) { | ||
// We're at the newest tab. | ||
return -1; | ||
} | ||
|
||
this.activeIdx = candidateIdx; | ||
return this.tabs[this.activeIdx]; | ||
} | ||
} | ||
|
||
var BgUtils = { | ||
tabRecency: new TabRecency(), | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -332,6 +332,16 @@ const BackgroundCommands = { | |
return selectSpecificTab({id: tabIds[(count-1) % tabIds.length]}); | ||
}, | ||
|
||
jumpBackTabList({count}) { | ||
const tabId = BgUtils.tabRecency.getJumpBackTabId({count}); | ||
return selectSpecificTab({id: tabId}); | ||
}, | ||
|
||
jumpForwardTabList({count}) { | ||
const tabId = BgUtils.tabRecency.getJumpForwardTabId({count}); | ||
return selectSpecificTab({id: tabId}); | ||
}, | ||
|
||
reload({count, tabId, registryEntry, tab: {windowId}}){ | ||
const bypassCache = registryEntry.options.hard != null ? registryEntry.options.hard : false; | ||
return chrome.tabs.query({windowId}, function(tabs) { | ||
|
@@ -350,6 +360,58 @@ const BackgroundCommands = { | |
} | ||
}; | ||
|
||
chrome.commands.onCommand.addListener(function(command) { | ||
|
||
const sendCommandToCurrentTab = function(requestName) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A better name for this variable would be |
||
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { | ||
if (tabs[0]) { | ||
const tabId = tabs[0].id; | ||
chrome.tabs.sendMessage(tabId, { | ||
name: 'runInTopFrame', | ||
registryEntry: { | ||
command: requestName, | ||
optionList: [], | ||
}, | ||
}); | ||
} | ||
}); | ||
}; | ||
|
||
switch (command) { | ||
case "jump-back-tab": | ||
BackgroundCommands.jumpBackTabList({count: 1}); | ||
break; | ||
case "jump-forward-tab": | ||
BackgroundCommands.jumpForwardTabList({count: 1}); | ||
break; | ||
case "open-vomnibox": | ||
sendCommandToCurrentTab("Vomnibar.activate"); | ||
break; | ||
case "open-vomnibox-new-tab": | ||
sendCommandToCurrentTab("Vomnibar.activateInNewTab"); | ||
break; | ||
case "open-vomnibox-tab": | ||
sendCommandToCurrentTab("Vomnibar.activateTabSelection"); | ||
break; | ||
case "open-vomnibox-bookmark": | ||
sendCommandToCurrentTab("Vomnibar.activateBookmarks"); | ||
break; | ||
case "open-vomnibox-bookmark-new-tab": | ||
sendCommandToCurrentTab("Vomnibar.activateBookmarksInNewTab"); | ||
break; | ||
case "switch-to-previous-tab": | ||
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { | ||
if (tabs[0]) { | ||
const tabId = tabs[0].id; | ||
BackgroundCommands.visitPreviousTab({count: 1, tab: { id: tabId} }); | ||
} | ||
}); | ||
break; | ||
default: | ||
console.error('unrecognized command: ', command); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You don't need to add a space after the string. The logger joins multiple things together with spaces by default. |
||
} | ||
}); | ||
|
||
var forCountTabs = (count, currentTab, callback) => chrome.tabs.query({currentWindow: true}, function(tabs) { | ||
const activeTabIndex = currentTab.index; | ||
const startTabIndex = Math.max(0, Math.min(activeTabIndex, tabs.length - count)); | ||
|
@@ -613,7 +675,9 @@ var portHandlers = { | |
}; | ||
|
||
var sendRequestHandlers = { | ||
runBackgroundCommand(request) { return BackgroundCommands[request.registryEntry.command](request); }, | ||
runBackgroundCommand(request) { | ||
return BackgroundCommands[request.registryEntry.command](request); | ||
}, | ||
// getCurrentTabUrl is used by the content scripts to get their full URL, because window.location cannot help | ||
// with Chrome-specific URLs like "view-source:http:..". | ||
getCurrentTabUrl({tab}) { return tab.url; }, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,32 @@ | |
"48": "icons/icon48.png", | ||
"128": "icons/icon128.png" }, | ||
"minimum_chrome_version": "69.0", | ||
"commands": { | ||
"jump-back-tab": { | ||
"description": "Jump back in the tab stack." | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Everywhere else you call it a tab list (instead of a stack). These should be consistent. |
||
}, | ||
"jump-forward-tab": { | ||
"description": "Jump forward in the tab stack." | ||
}, | ||
"open-vomnibox": { | ||
"description": "Open the vomnibox for the same tab." | ||
}, | ||
"open-vomnibox-new-tab": { | ||
"description": "Open the vomnibox for a new tab." | ||
}, | ||
"open-vomnibox-tab": { | ||
"description": "Open the vomnibox with tabs." | ||
}, | ||
"open-vomnibox-bookmark": { | ||
"description": "Open the vomnibox with history." | ||
}, | ||
"open-vomnibox-bookmark-new-tab": { | ||
"description": "Open the vomnibox with history." | ||
}, | ||
"switch-to-previous-tab": { | ||
"description": "Switch to the previously used tab." | ||
} | ||
}, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just a personal opinion: Maybe we can use command names of Vimium directly, like what I've done in https://github.com/gdh1995/vimium-c/blob/dc5e026083db67fc6f471c907e5ca37b7b5a5316/manifest.json#L38-L63 . Then we may add new syntaxes to key mappings to assign options to such global "shortcuts" (like https://github.com/gdh1995/vimium-c/wiki/Trigger-commands-in-an-input-box#shortcuts) |
||
"background": { | ||
"scripts": [ | ||
"lib/utils.js", | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This name could be more descriptive/explicit. Is that
activeTabId
?