Skip to content
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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
113 changes: 113 additions & 0 deletions background_scripts/bg_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand All @@ -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
Expand All @@ -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;
}
Expand All @@ -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).
Expand All @@ -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;
Copy link
Contributor

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?

// 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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you mean isCurrent here?

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(),

Expand Down
6 changes: 6 additions & 0 deletions background_scripts/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,8 @@ const Commands = {
"previousTab",
"nextTab",
"visitPreviousTab",
"jumpBackTabList",
"jumpForwardTabList",
"firstTab",
"lastTab",
"duplicateTab",
Expand Down Expand Up @@ -340,6 +342,8 @@ const defaultKeyMappings = {
"gt": "nextTab",
"gT": "previousTab",
"^": "visitPreviousTab",
"<c-o>": "jumpBackTabList",
"<c-i>": "jumpForwardTabList",
"<<": "moveTabLeft",
">>": "moveTabRight",
"g0": "firstTab",
Expand Down Expand Up @@ -423,6 +427,8 @@ const commandDescriptions = {
nextTab: ["Go one tab right", { background: true }],
previousTab: ["Go one tab left", { background: true }],
visitPreviousTab: ["Go to previously-visited tab", { background: true }],
jumpBackTabList: ["Jump back in visited tab list", { background: true }],
jumpForwardTabList: ["Jump forward in visited tab list", { background: true }],
firstTab: ["Go to the first tab", { background: true }],
lastTab: ["Go to the last tab", { background: true }],

Expand Down
66 changes: 65 additions & 1 deletion background_scripts/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -350,6 +360,58 @@ const BackgroundCommands = {
}
};

chrome.commands.onCommand.addListener(function(command) {

const sendCommandToCurrentTab = function(requestName) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A better name for this variable would be command.

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);
Copy link
Contributor

Choose a reason for hiding this comment

The 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));
Expand Down Expand Up @@ -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; },
Expand Down
26 changes: 26 additions & 0 deletions manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Copy link
Contributor

Choose a reason for hiding this comment

The 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."
}
},
Copy link
Contributor

Choose a reason for hiding this comment

The 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",
Expand Down
Loading