Skip to content

Commit cf11b0e

Browse files
committed
Merge remote-tracking branch 'origin/master' into feat/move-tab-to-existing-window
2 parents a8d985d + 665d267 commit cf11b0e

File tree

14 files changed

+299
-157
lines changed

14 files changed

+299
-157
lines changed

CHANGELOG.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,33 @@
1+
2.1.2 (2024-04-03)
2+
3+
- Better fix for Vomnibar doesn't always list tabs by recency.
4+
([#4368](https://github.com/philc/vimium/issues/4368))
5+
- Add a workaround to make link hints work on Github Enterprise.
6+
([#4446](https://github.com/philc/vimium/issues/4446))
7+
- Fix position=end is ignored in createTab command
8+
([#4450](https://github.com/philc/vimium/issues/4450))
9+
10+
2.1.1 (2024-03-29)
11+
12+
- Fix exclusion rule popup not working. ([#4447](https://github.com/philc/vimium/issues/4447))
13+
14+
2.1.0 (2024-03-27)
15+
16+
- Fix Vomnibar doesn't always list tabs by recency.
17+
([#4368](https://github.com/philc/vimium/issues/4368))
18+
- Better domain detection in the Vomnibar ([#3268](https://github.com/philc/vimium/issues/3268))
19+
- Exclude keys based on the top frame URL, not a subframe's URL. This fixes many cases where the
20+
excluded keys feature didn't seem to work. ([#4402](https://github.com/philc/vimium/issues/4402))
21+
- After selecting a link, if ESC is pressed, mouse out of the link. With this, Wikipedia's and
22+
Github's link preview popups can be dismissed after following a link.
23+
([#3073](https://github.com/philc/vimium/issues/3073))
24+
- Fix link hints do not appear for links inside of github's popups. This fix is available on Chrome
25+
114+, and soon Firefox. ([#4408](https://github.com/philc/vimium/issues/4408))
26+
127
2.0.5, 2.0.6 (2023-11-06)
228

329
- Fix bug where "esc" wouldn't unfocus a textarea like it should.
4-
([#4336](https://github.com/philc/vimium/issues/4336)),
30+
([#4336](https://github.com/philc/vimium/issues/4336))
531
- Fix passNextKey command.
632

733
2.0.4 (2023-10-19)

background_scripts/background.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import "../lib/utils.js";
22
import "../lib/settings.js";
33
import "../lib/url_utils.js";
4+
import "../background_scripts/tab_recency.js";
45
import "../background_scripts/bg_utils.js";
56
import "../background_scripts/commands.js";
67
import "../background_scripts/exclusions.js";

background_scripts/bg_utils.js

Lines changed: 1 addition & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,3 @@
1-
const TIME_DELTA = 500; // Milliseconds.
2-
3-
// TabRecency associates a logical timestamp with each tab id. These are used to provide an initial
4-
// recency-based ordering in the tabs vomnibar (which allows jumping quickly between
5-
// recently-visited tabs).
6-
class TabRecency {
7-
constructor() {
8-
this.timestamp = 1;
9-
this.current = -1;
10-
this.cache = {};
11-
this.lastVisited = null;
12-
this.lastVisitedTime = null;
13-
14-
chrome.tabs.onActivated.addListener((activeInfo) => this.register(activeInfo.tabId));
15-
chrome.tabs.onRemoved.addListener((tabId) => this.deregister(tabId));
16-
17-
chrome.tabs.onReplaced.addListener((addedTabId, removedTabId) => {
18-
this.deregister(removedTabId);
19-
this.register(addedTabId);
20-
});
21-
22-
if (chrome.windows != null) {
23-
chrome.windows.onFocusChanged.addListener((wnd) => {
24-
if (wnd !== chrome.windows.WINDOW_ID_NONE) {
25-
chrome.tabs.query({ windowId: wnd, active: true }, (tabs) => {
26-
if (tabs[0]) {
27-
this.register(tabs[0].id);
28-
}
29-
});
30-
}
31-
});
32-
}
33-
}
34-
35-
register(tabId) {
36-
const currentTime = new Date();
37-
// Register tabId if it's been visited for at least @timeDelta ms. Tabs which are visited only
38-
// for a very-short time (e.g. those passed through with `5J`) aren't registered as visited.
39-
if ((this.lastVisitedTime != null) && (TIME_DELTA <= (currentTime - this.lastVisitedTime))) {
40-
this.cache[this.lastVisited] = ++this.timestamp;
41-
}
42-
43-
this.current = this.lastVisited = tabId;
44-
this.lastVisitedTime = currentTime;
45-
}
46-
47-
deregister(tabId) {
48-
if (tabId === this.lastVisited) {
49-
// Ensure we don't register this tab, since it's going away.
50-
this.lastVisited = this.lastVisitedTime = null;
51-
}
52-
delete this.cache[tabId];
53-
}
54-
55-
// Recently-visited tabs get a higher score (except the current tab, which gets a low score).
56-
recencyScore(tabId) {
57-
if (!this.cache[tabId]) {
58-
this.cache[tabId] = 1;
59-
}
60-
if (tabId === this.current) {
61-
return 0.0;
62-
} else {
63-
return this.cache[tabId] / this.timestamp;
64-
}
65-
}
66-
67-
// Returns a list of tab Ids sorted by recency, most recent tab first.
68-
getTabsByRecency() {
69-
const tabIds = Object.keys(this.cache || {});
70-
tabIds.sort((a, b) => this.cache[b] - this.cache[a]);
71-
return tabIds.map((tId) => parseInt(tId));
72-
}
73-
}
74-
751
const BgUtils = {
762
tabRecency: new TabRecency(),
773

@@ -91,21 +17,9 @@ const BgUtils = {
9117
async getFirefoxVersion() {
9218
return globalThis.browser ? (await browser.runtime.getBrowserInfo()).version : null;
9319
},
94-
95-
escapedEntities: {
96-
'"': "&quots;",
97-
"&": "&amp;",
98-
"'": "&apos;",
99-
"<": "&lt;",
100-
">": "&gt;",
101-
},
102-
103-
escapeAttribute(string) {
104-
return string.replace(/["&'<>]/g, (char) => BgUtils.escapedEntities[char]);
105-
},
10620
};
10721

108-
BgUtils.TIME_DELTA = TIME_DELTA; // Referenced by our tests.
22+
BgUtils.tabRecency.init();
10923

11024
Object.assign(globalThis, {
11125
BgUtils,

background_scripts/completion.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,7 @@ class DomainCompleter {
479479
// If the query is empty, then return a list of open tabs, sorted by recency.
480480
class TabCompleter {
481481
async filter({ queryTerms }) {
482+
await BgUtils.tabRecency.init();
482483
// We search all tabs, not just those in the current window.
483484
const tabs = await chrome.tabs.query({});
484485
const results = tabs.filter((tab) => RankingUtils.matches(queryTerms, tab.url, tab.title));

background_scripts/main.js

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -202,8 +202,11 @@ const BackgroundCommands = {
202202
request.urls = [request.url];
203203
} else {
204204
// Otherwise, if we have a registryEntry containing URLs, then use them.
205-
const urlList = request.registryEntry.optionList
206-
.filter(async (opt) => await UrlUtils.isUrl(opt));
205+
// TODO(philc): This would be clearer if we try to detect options (a=b) rather than URLs,
206+
// because the syntax for options is well defined ([a-zA-Z]+=\S+).
207+
const promises = request.registryEntry.optionList.map((opt) => UrlUtils.isUrl(opt));
208+
const isUrl = await Promise.all(promises);
209+
const urlList = request.registryEntry.optionList.filter((_, i) => isUrl[i]);
207210
if (urlList.length > 0) {
208211
request.urls = urlList;
209212
} else {
@@ -355,10 +358,13 @@ const BackgroundCommands = {
355358
await removeTabsRelative("both", request);
356359
},
357360

358-
visitPreviousTab({ count, tab }) {
359-
const tabIds = BgUtils.tabRecency.getTabsByRecency().filter((tabId) => tabId !== tab.id);
361+
async visitPreviousTab({ count, tab }) {
362+
await BgUtils.tabRecency.init();
363+
let tabIds = BgUtils.tabRecency.getTabsByRecency();
364+
tabIds = tabIds.filter((tabId) => tabId !== tab.id);
360365
if (tabIds.length > 0) {
361-
selectSpecificTab({ id: tabIds[(count - 1) % tabIds.length] });
366+
const id = tabIds[(count - 1) % tabIds.length];
367+
selectSpecificTab({ id });
362368
}
363369
},
364370

background_scripts/tab_operations.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ async function openUrlInCurrentTab(request) {
1212
// (like github.com, developer.mozilla.org) will raise an error when we try to run this code. See
1313
// https://github.com/philc/vimium/issues/4331.
1414
if (UrlUtils.hasJavascriptPrefix(request.url)) {
15-
const tabId = request.tabId;
1615
const scriptingArgs = {
1716
target: { tabId: request.tabId },
1817
func: (text) => {
@@ -87,7 +86,8 @@ async function openUrlInNewTab(request, callback) {
8786

8887
tabConfig.openerTabId = request.tab.id;
8988

90-
// clean position and active, so following `openUrlInNewTab(request)` will create a tab just next to this new tab
89+
// clean position and active, so following `openUrlInNewTab(request)` will create a tab just next
90+
// to this new tab
9191
return chrome.tabs.create(
9292
tabConfig,
9393
(tab) => callback(Object.assign(request, { tab, tabId: tab.id, position: "", active: false })),

background_scripts/tab_recency.js

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
// TabRecency associates an integer with each tab id representing how recently it has been accessed.
2+
// The order of tabs as tracked by TabRecency is used to provide a recency-based ordering in the
3+
// tabs vomnibar.
4+
//
5+
// The values are persisted to chrome.storage.session so that they're not lost when the extension's
6+
// background page is unloaded.
7+
//
8+
// Callers must await TabRecency.init before calling recencyScore or getTabsByRecency.
9+
//
10+
// In theory, the browser's tab.lastAccessed timestamp field should allow us to sort tabs by
11+
// recency, but in practice it does not work across several edge cases. See the comments on #4368.
12+
class TabRecency {
13+
constructor() {
14+
this.counter = 1;
15+
this.tabIdToCounter = {};
16+
this.loaded = false;
17+
this.queuedActions = [];
18+
}
19+
20+
// Add listeners to chrome.tabs, and load the index from session storage.
21+
async init() {
22+
if (this.initPromise) {
23+
await this.initPromise;
24+
return;
25+
}
26+
let resolveFn;
27+
this.initPromise = new Promise((resolve, _reject) => {
28+
resolveFn = resolve;
29+
});
30+
31+
chrome.tabs.onActivated.addListener((activeInfo) => {
32+
this.queueAction("register", activeInfo.tabId);
33+
});
34+
chrome.tabs.onRemoved.addListener((tabId) => {
35+
this.queueAction("deregister", tabId);
36+
});
37+
38+
chrome.tabs.onReplaced.addListener((addedTabId, removedTabId) => {
39+
this.queueAction("deregister", removedTabId);
40+
this.queueAction("register", addedTabId);
41+
});
42+
43+
chrome.windows.onFocusChanged.addListener(async (windowId) => {
44+
if (windowId == chrome.windows.WINDOW_ID_NONE) return;
45+
const tabs = await chrome.tabs.query({ windowId, active: true });
46+
if (tabs[0]) {
47+
this.queueAction("register", tabs[0].id);
48+
}
49+
});
50+
51+
await this.loadFromStorage();
52+
while (this.queuedActions.length > 0) {
53+
const [action, tabId] = this.queuedActions.shift();
54+
this.handleAction(action, tabId);
55+
}
56+
this.loaded = true;
57+
resolveFn();
58+
}
59+
60+
// Loads the index from session storage.
61+
async loadFromStorage() {
62+
const tabsPromise = chrome.tabs.query({});
63+
const storagePromise = chrome.storage.session.get("tabRecency");
64+
const [tabs, storage] = await Promise.all([tabsPromise, storagePromise]);
65+
if (storage.tabRecency == null) return;
66+
67+
let maxCounter = 0;
68+
for (const counter of Object.values(storage.tabRecency)) {
69+
if (maxCounter < counter) maxCounter = counter;
70+
}
71+
if (this.counter < maxCounter) {
72+
this.counter = maxCounter;
73+
}
74+
75+
this.tabIdToCounter = Object.assign({}, storage.tabRecency);
76+
77+
// Remove any tab IDs which aren't currently loaded.
78+
const tabIds = new Set(tabs.map((t) => t.id));
79+
for (const id in this.tabIdToCounter) {
80+
if (!tabIds.has(parseInt(id))) {
81+
delete this.tabIdToCounter[id];
82+
}
83+
}
84+
}
85+
86+
async saveToStorage() {
87+
await chrome.storage.session.set({ tabRecency: this.tabIdToCounter });
88+
}
89+
90+
// - action: "register" or "unregister".
91+
queueAction(action, tabId) {
92+
if (!this.loaded) {
93+
this.queuedActions.push([action, tabId]);
94+
} else {
95+
this.handleAction(action, tabId);
96+
}
97+
}
98+
99+
// - action: "register" or "unregister".
100+
handleAction(action, tabId) {
101+
if (action == "register") {
102+
this.register(tabId);
103+
} else if (action == "deregister") {
104+
this.deregister(tabId);
105+
} else {
106+
throw new Error(`Unexpected action type: ${action}`);
107+
}
108+
}
109+
110+
register(tabId) {
111+
this.counter++;
112+
this.tabIdToCounter[tabId] = this.counter;
113+
this.saveToStorage();
114+
}
115+
116+
deregister(tabId) {
117+
delete this.tabIdToCounter[tabId];
118+
this.saveToStorage();
119+
}
120+
121+
// Recently-visited tabs get a higher score (except the current tab, which gets a low score).
122+
recencyScore(tabId) {
123+
if (!this.loaded) throw new Error("TabRecency hasn't yet been loaded.");
124+
const tabCounter = this.tabIdToCounter[tabId];
125+
const isCurrentTab = tabCounter == this.counter;
126+
if (isCurrentTab) return 0;
127+
return (tabCounter ?? 1) / this.counter; // tabCounter may be null.
128+
}
129+
130+
// Returns a list of tab Ids sorted by recency, most recent tab first.
131+
getTabsByRecency() {
132+
if (!this.loaded) throw new Error("TabRecency hasn't yet been loaded.");
133+
const ids = Object.keys(this.tabIdToCounter);
134+
ids.sort((a, b) => this.tabIdToCounter[b] - this.tabIdToCounter[a]);
135+
return ids.map((id) => parseInt(id));
136+
}
137+
}
138+
139+
Object.assign(globalThis, { TabRecency });

content_scripts/link_hints.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,25 @@ class LinkHintsMode {
409409
{ id: "vimiumHintMarkerContainer", className: "vimiumReset" },
410410
);
411411

412+
// TODO(philc): 2024-03-27 Remove this hasPopoverSupport check once Firefox has popover support.
413+
// Also move this CSS into vimium.css.
414+
const hasPopoverSupport = this.hintMarkerContainingDiv.showPopover != null;
415+
if (hasPopoverSupport) {
416+
this.hintMarkerContainingDiv.popover = "manual";
417+
this.hintMarkerContainingDiv.showPopover();
418+
Object.assign(this.hintMarkerContainingDiv.style, {
419+
top: 0,
420+
left: 0,
421+
position: "absolute",
422+
// This display: block is required to override Github Enterprise's CSS circa 2024-04-01. See
423+
// #4446.
424+
display: "block",
425+
width: "100%",
426+
height: "100%",
427+
overflow: "visible",
428+
});
429+
}
430+
412431
this.setIndicator();
413432
}
414433

content_scripts/vimium.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@ tbody.vimiumReset {
7373

7474
/* Linkhints CSS */
7575

76+
div#vimiumHintMarkerContainer {
77+
pointer-events: none;
78+
}
79+
7680
div.internalVimiumHintMarker {
7781
position: absolute;
7882
display: block;

manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"manifest_version": 3,
33
"name": "Vimium",
4-
"version": "2.0.6",
4+
"version": "2.1.2",
55
"description": "The Hacker's Browser. Vimium provides keyboard shortcuts for navigation and control in the spirit of Vim.",
66
"icons": { "16": "icons/icon16.png", "48": "icons/icon48.png", "128": "icons/icon128.png" },
77
"minimum_chrome_version": "105.0",

0 commit comments

Comments
 (0)