Source code
Revision control
Copy as Markdown
Other Tools
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
UrlbarUtils: "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs",
UrlbarTokenizer:
"moz-src:///browser/components/urlbar/UrlbarTokenizer.sys.mjs",
});
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"maxResults",
"browser.urlbar.mentions.maxResults"
);
export const MENTION_TYPE = /** @type {const} */ ({
TAB_OPEN: "TAB_OPEN",
TAB_RECENTLY_CLOSED: "TAB_RECENTLY_CLOSED",
});
/**
* @typedef {object} TabResult
* @property {string} url - Tab URL
* @property {string} title - Tab title
* @property {string} icon - Tab icon
* @property {(typeof MENTION_TYPE)[keyof typeof MENTION_TYPE]} type - Tab type
* @property {number} timestamp - Tab timestamp
*/
/**
* Mentions suggestions search for the Smartbar mentions panel.
*
* Fetches and filters open and recently closed tabs for display in the
* mentions suggestions panel in the Smartbar.
*
* NOTE: This provider is not compatible with UrlbarProvidersManager and only
* intended for standalone use within the Smartbar mentions feature.
*/
export class SmartbarMentionsPanelSearch {
#tabs = null;
constructor(browserWindow) {
this.#tabs = this.#getOpenAndClosedTabs(browserWindow);
}
/**
* Return filtered tabs sorted by recency.
*
* @param {string} searchString
* @returns {TabResult[]}
*/
startQuery(searchString) {
return this.#filterTabs(searchString)
.sort((a, b) => b.timestamp - a.timestamp)
.slice(0, lazy.maxResults);
}
#filterTabs(searchString) {
if (!this.#tabs) {
return [];
}
if (!searchString) {
return this.#tabs;
}
// Tokenize the search string
const tokens = lazy.UrlbarTokenizer.tokenize({
searchString: searchString.substring(0, lazy.UrlbarUtils.MAX_TEXT_LENGTH),
trimmedSearchString: searchString.trim(),
});
if (!tokens.length) {
return this.#tabs;
}
return this.#tabs.filter(tab => {
const normalizedUrl = this.#normalizeUrl(tab.url);
const matches = lazy.UrlbarUtils.getTokenMatches(
tokens,
`${tab.title} ${normalizedUrl}`.substring(
0,
lazy.UrlbarUtils.MAX_TEXT_LENGTH
),
lazy.UrlbarUtils.HIGHLIGHT.TYPED
);
return !!matches.length;
});
}
#getOpenAndClosedTabs(browserWindow) {
const tabsMap = new Map();
// Open tabs
for (const tab of browserWindow.gBrowser.tabs) {
const url = tab.linkedBrowser?.currentURI?.spec;
if (!url) {
continue;
}
// Use the first occurrence of the URL
if (tabsMap.has(url)) {
continue;
}
tabsMap.set(url, {
url,
title: tab.label || url,
icon: tab.getAttribute("image") || "",
type: MENTION_TYPE.TAB_OPEN,
timestamp: tab.lastAccessed,
});
}
// Recently closed tabs
try {
const closedTabData =
lazy.SessionStore.getClosedTabDataForWindow(browserWindow);
for (const closedTab of closedTabData) {
const state = closedTab.state;
// Get the active history entry using the same pattern as in
// RecentlyClosedTabsAndWindowsMenuUtils.
const activeIndex = (state.index || state.entries.length) - 1;
if (activeIndex < 0 || !state.entries[activeIndex]) {
continue;
}
const entry = state.entries[activeIndex];
const url = entry.url;
if (!url) {
continue;
}
// Use the first occurrence of the URL
if (tabsMap.has(url)) {
continue;
}
tabsMap.set(url, {
url,
title: entry.title || url,
icon: closedTab.image || "",
type: MENTION_TYPE.TAB_RECENTLY_CLOSED,
timestamp: closedTab.closedAt,
});
}
} catch (e) {
console.error("Error getting recently closed tabs:", e);
}
return Array.from(tabsMap.values());
}
#normalizeUrl(url) {
try {
const [stripped] = lazy.UrlbarUtils.stripPrefixAndTrim(url, {
stripHttp: true,
stripHttps: true,
trimSlash: true,
trimEmptyQuery: true,
trimEmptyHash: true,
});
return stripped;
} catch {
return url;
}
}
}