diff --git a/src/browser/app/profile/features.inc b/src/browser/app/profile/features.inc index 83993709f1..4db348e569 100644 --- a/src/browser/app/profile/features.inc +++ b/src/browser/app/profile/features.inc @@ -78,6 +78,9 @@ pref('zen.glance.hold-duration', 300); // in ms pref('zen.glance.open-essential-external-links', true); pref('zen.glance.activation-method', 'alt'); // ctrl, alt, shift, none, hold +pref('zen.tabsearch.enabled', true); +pref('zen.tabsearch.auto-select-result', false); + pref('zen.view.sidebar-height-throttle', 200); // in ms pref('zen.view.sidebar-expanded.max-width', 500); diff --git a/src/browser/base/content/zen-assets.inc.xhtml b/src/browser/base/content/zen-assets.inc.xhtml index a22c866350..acb2a69612 100644 --- a/src/browser/base/content/zen-assets.inc.xhtml +++ b/src/browser/base/content/zen-assets.inc.xhtml @@ -44,4 +44,5 @@ Services.scriptloader.loadSubScript("chrome://browser/content/zen-components/Zen Services.scriptloader.loadSubScript("chrome://browser/content/zen-components/ZenGlanceManager.mjs", this); Services.scriptloader.loadSubScript("chrome://browser/content/zen-components/ZenMediaController.mjs", this); Services.scriptloader.loadSubScript("chrome://browser/content/zen-components/ZenDownloadAnimation.mjs", this); +Services.scriptloader.loadSubScript("chrome://browser/content/zen-components/ZenTabSearch.mjs", this); diff --git a/src/browser/base/content/zen-assets.jar.inc.mn b/src/browser/base/content/zen-assets.jar.inc.mn index b083e20ef2..90ee0206a4 100644 --- a/src/browser/base/content/zen-assets.jar.inc.mn +++ b/src/browser/base/content/zen-assets.jar.inc.mn @@ -61,6 +61,8 @@ content/browser/zen-components/actors/ZenGlanceChild.sys.mjs (../../zen/glance/actors/ZenGlanceChild.sys.mjs) content/browser/zen-components/actors/ZenGlanceParent.sys.mjs (../../zen/glance/actors/ZenGlanceParent.sys.mjs) + content/browser/zen-components/ZenTabSearch.mjs (../../zen/tabsearch/ZenTabSearch.mjs) + content/browser/zen-components/ZenFolders.mjs (../../zen/folders/ZenFolders.mjs) content/browser/zen-styles/zen-folders.css (../../zen/folders/zen-folders.css) diff --git a/src/browser/base/content/zen-keysets.inc.xhtml b/src/browser/base/content/zen-keysets.inc.xhtml index 1b3c25f231..51f8147e77 100644 --- a/src/browser/base/content/zen-keysets.inc.xhtml +++ b/src/browser/base/content/zen-keysets.inc.xhtml @@ -47,6 +47,10 @@ + + + + diff --git a/src/browser/components/preferences/zen-settings.js b/src/browser/components/preferences/zen-settings.js index 512d2e91d0..31ec22939b 100644 --- a/src/browser/components/preferences/zen-settings.js +++ b/src/browser/components/preferences/zen-settings.js @@ -777,6 +777,7 @@ var gZenWorkspacesSettings = { }; Services.prefs.addObserver('zen.tab-unloader.enabled', tabsUnloaderPrefListener); Services.prefs.addObserver('zen.glance.enabled', tabsUnloaderPrefListener); // We can use the same listener for both prefs + Services.prefs.addObserver('zen.tabsearch.enabled', tabsUnloaderPrefListener); // We can use the same listener for both prefs Services.prefs.addObserver( 'zen.workspaces.container-specific-essentials-enabled', tabsUnloaderPrefListener @@ -1169,6 +1170,16 @@ Preferences.addAll([ type: 'bool', default: true, }, + { + id: 'zen.tabsearch.enabled', + type: 'bool', + default: true, + }, + { + id: 'zen.tabsearch.auto-select-result', + type: 'bool', + default: false, + }, { id: 'zen.theme.color-prefs.use-workspace-colors', type: 'bool', diff --git a/src/browser/components/tabbrowser/content/tabbrowser-js.patch b/src/browser/components/tabbrowser/content/tabbrowser-js.patch index dfaaacb142..2f7dd39338 100644 --- a/src/browser/components/tabbrowser/content/tabbrowser-js.patch +++ b/src/browser/components/tabbrowser/content/tabbrowser-js.patch @@ -2,7 +2,32 @@ diff --git a/browser/components/tabbrowser/content/tabbrowser.js b/browser/compo index 6dece2b9d0462d90a28e75350ce983d87816ef73..5c49c43714b3914130f8d821d902f9f255c4ebc9 100644 --- a/browser/components/tabbrowser/content/tabbrowser.js +++ b/browser/components/tabbrowser/content/tabbrowser.js -@@ -415,11 +415,45 @@ +@@ -117,6 +117,24 @@ + UrlbarProviderOpenTabs: + "resource:///modules/UrlbarProviderOpenTabs.sys.mjs", + }); ++ // Add TabSelect event listener to update open tab information ++ this.tabContainer.addEventListener("TabSelect", async event => { ++ let tab = event.target; ++ let browser = tab.linkedBrowser; ++ let url = browser.currentURI.spec; ++ let userContextId = tab.getAttribute("usercontextid") || 0; ++ let isInPrivateWindow = PrivateBrowsingUtils.isWindowPrivate(window); ++ ++ try { ++ await this.UrlbarProviderOpenTabs.updateOpenTab( ++ url, ++ parseInt(userContextId), ++ isInPrivateWindow ++ ); ++ } catch (error) { ++ console.error("Failed to update open tab:", error); ++ } ++ }); + ChromeUtils.defineLazyGetter(this, "tabLocalization", () => { + return new Localization( + ["browser/tabbrowser.ftl", "branding/brand.ftl"], +@@ -415,11 +433,45 @@ return this.tabContainer.visibleTabs; } @@ -50,7 +75,7 @@ index 6dece2b9d0462d90a28e75350ce983d87816ef73..5c49c43714b3914130f8d821d902f9f2 } return i; } -@@ -571,6 +605,7 @@ +@@ -571,6 +623,7 @@ this.tabpanels.appendChild(panel); let tab = this.tabs[0]; @@ -58,7 +83,7 @@ index 6dece2b9d0462d90a28e75350ce983d87816ef73..5c49c43714b3914130f8d821d902f9f2 tab.linkedPanel = uniqueId; this._selectedTab = tab; this._selectedBrowser = browser; -@@ -836,11 +871,13 @@ +@@ -836,11 +889,13 @@ } this.showTab(aTab); @@ -75,7 +100,7 @@ index 6dece2b9d0462d90a28e75350ce983d87816ef73..5c49c43714b3914130f8d821d902f9f2 this.moveTabTo(aTab, { tabIndex: this.pinnedTabCount, forceUngrouped: true, -@@ -857,12 +894,15 @@ +@@ -857,12 +912,15 @@ } if (this.tabContainer.verticalMode) { @@ -92,7 +117,7 @@ index 6dece2b9d0462d90a28e75350ce983d87816ef73..5c49c43714b3914130f8d821d902f9f2 }); } else { this.moveTabTo(aTab, { -@@ -1046,6 +1086,8 @@ +@@ -1046,6 +1104,8 @@ let LOCAL_PROTOCOLS = ["chrome:", "about:", "resource:", "data:"]; @@ -101,7 +126,7 @@ index 6dece2b9d0462d90a28e75350ce983d87816ef73..5c49c43714b3914130f8d821d902f9f2 if ( aIconURL && !aLoadingPrincipal && -@@ -1056,6 +1098,9 @@ +@@ -1056,6 +1116,9 @@ ); return; } @@ -111,7 +136,7 @@ index 6dece2b9d0462d90a28e75350ce983d87816ef73..5c49c43714b3914130f8d821d902f9f2 let browser = this.getBrowserForTab(aTab); browser.mIconURL = aIconURL; -@@ -1305,6 +1350,7 @@ +@@ -1305,6 +1368,7 @@ if (!this._previewMode) { newTab.recordTimeFromUnloadToReload(); newTab.updateLastAccessed(); @@ -119,7 +144,7 @@ index 6dece2b9d0462d90a28e75350ce983d87816ef73..5c49c43714b3914130f8d821d902f9f2 oldTab.updateLastAccessed(); // if this is the foreground window, update the last-seen timestamps. if (this.ownerGlobal == BrowserWindowTracker.getTopWindow()) { -@@ -1457,6 +1503,9 @@ +@@ -1457,6 +1521,9 @@ } let activeEl = document.activeElement; @@ -129,7 +154,7 @@ index 6dece2b9d0462d90a28e75350ce983d87816ef73..5c49c43714b3914130f8d821d902f9f2 // If focus is on the old tab, move it to the new tab. if (activeEl == oldTab) { newTab.focus(); -@@ -1780,7 +1829,8 @@ +@@ -1780,7 +1847,8 @@ } _setTabLabel(aTab, aLabel, { beforeTabOpen, isContentTitle, isURL } = {}) { @@ -139,7 +164,7 @@ index 6dece2b9d0462d90a28e75350ce983d87816ef73..5c49c43714b3914130f8d821d902f9f2 return false; } -@@ -1888,7 +1938,7 @@ +@@ -1888,7 +1956,7 @@ newIndex = this.selectedTab._tPos + 1; } @@ -148,7 +173,7 @@ index 6dece2b9d0462d90a28e75350ce983d87816ef73..5c49c43714b3914130f8d821d902f9f2 if (this.isTabGroupLabel(targetTab)) { throw new Error( "Replacing a tab group label with a tab is not supported" -@@ -2152,6 +2202,7 @@ +@@ -2152,6 +2220,7 @@ uriIsAboutBlank, userContextId, skipLoad, @@ -156,7 +181,7 @@ index 6dece2b9d0462d90a28e75350ce983d87816ef73..5c49c43714b3914130f8d821d902f9f2 } = {}) { let b = document.createXULElement("browser"); // Use the JSM global to create the permanentKey, so that if the -@@ -2225,8 +2276,7 @@ +@@ -2225,8 +2294,7 @@ // we use a different attribute name for this? b.setAttribute("name", name); } @@ -166,7 +191,7 @@ index 6dece2b9d0462d90a28e75350ce983d87816ef73..5c49c43714b3914130f8d821d902f9f2 b.setAttribute("transparent", "true"); } -@@ -2391,7 +2441,7 @@ +@@ -2391,7 +2459,7 @@ let panel = this.getPanel(browser); let uniqueId = this._generateUniquePanelID(); @@ -175,7 +200,7 @@ index 6dece2b9d0462d90a28e75350ce983d87816ef73..5c49c43714b3914130f8d821d902f9f2 aTab.linkedPanel = uniqueId; // Inject the into the DOM if necessary. -@@ -2450,8 +2500,8 @@ +@@ -2450,8 +2518,8 @@ // If we transitioned from one browser to two browsers, we need to set // hasSiblings=false on both the existing browser and the new browser. if (this.tabs.length == 2) { @@ -186,7 +211,7 @@ index 6dece2b9d0462d90a28e75350ce983d87816ef73..5c49c43714b3914130f8d821d902f9f2 } else { aTab.linkedBrowser.browsingContext.hasSiblings = this.tabs.length > 1; } -@@ -2679,6 +2729,7 @@ +@@ -2679,6 +2747,7 @@ schemelessInput, hasValidUserGestureActivation = false, textDirectiveUserActivation = false, @@ -194,7 +219,7 @@ index 6dece2b9d0462d90a28e75350ce983d87816ef73..5c49c43714b3914130f8d821d902f9f2 } = {} ) { // all callers of addTab that pass a params object need to pass -@@ -2689,6 +2740,12 @@ +@@ -2689,6 +2758,12 @@ ); } @@ -207,7 +232,7 @@ index 6dece2b9d0462d90a28e75350ce983d87816ef73..5c49c43714b3914130f8d821d902f9f2 if (!UserInteraction.running("browser.tabs.opening", window)) { UserInteraction.start("browser.tabs.opening", "initting", window); } -@@ -2752,6 +2809,16 @@ +@@ -2752,6 +2827,16 @@ noInitialLabel, skipBackgroundNotify, }); @@ -224,7 +249,7 @@ index 6dece2b9d0462d90a28e75350ce983d87816ef73..5c49c43714b3914130f8d821d902f9f2 if (insertTab) { if (typeof index == "number") { elementIndex = this.#tabIndexToElementIndex(index); -@@ -2779,6 +2846,7 @@ +@@ -2779,6 +2864,7 @@ openWindowInfo, skipLoad, triggeringRemoteType, @@ -232,7 +257,7 @@ index 6dece2b9d0462d90a28e75350ce983d87816ef73..5c49c43714b3914130f8d821d902f9f2 })); if (focusUrlBar) { -@@ -2898,6 +2966,12 @@ +@@ -2898,6 +2984,12 @@ } } @@ -245,7 +270,7 @@ index 6dece2b9d0462d90a28e75350ce983d87816ef73..5c49c43714b3914130f8d821d902f9f2 // Additionally send pinned tab events if (pinned) { this._notifyPinnedStatus(t); -@@ -2945,12 +3019,15 @@ +@@ -2945,12 +3037,15 @@ * @param {string} [label=] * @returns {MozTabbrowserTabGroup} */ diff --git a/src/browser/components/urlbar/UrlbarMuxerUnifiedComplete-sys-mjs.patch b/src/browser/components/urlbar/UrlbarMuxerUnifiedComplete-sys-mjs.patch index 2d6f456e28..bcd54408c6 100644 --- a/src/browser/components/urlbar/UrlbarMuxerUnifiedComplete-sys-mjs.patch +++ b/src/browser/components/urlbar/UrlbarMuxerUnifiedComplete-sys-mjs.patch @@ -1,8 +1,28 @@ diff --git a/browser/components/urlbar/UrlbarMuxerUnifiedComplete.sys.mjs b/browser/components/urlbar/UrlbarMuxerUnifiedComplete.sys.mjs -index dcf1af43d62979d3226d7f704c51a2f0bb935cc0..8879d657b99cb20cd657c2e4841738ffaa09c658 100644 +index dcf1af43d62979d3226d7f704c51a2f0bb935cc0..a8271ac083c54947756e6ef35a3b4eac407b03f9 100644 --- a/browser/components/urlbar/UrlbarMuxerUnifiedComplete.sys.mjs +++ b/browser/components/urlbar/UrlbarMuxerUnifiedComplete.sys.mjs -@@ -794,6 +794,7 @@ class MuxerUnifiedComplete extends UrlbarMuxer { +@@ -117,6 +117,19 @@ class MuxerUnifiedComplete extends UrlbarMuxer { + // When you add state, update _copyState() as necessary. + }; + ++ let window = Services.wm.getMostRecentWindow("navigator:browser"); ++ if (window && typeof window.gZenTabSearch !== "undefined" && window.gURLBar && window.gURLBar.searchMode && window.gURLBar.searchMode.source === 4) { ++ unsortedResults.sort((a, b) => { ++ const lastOpenedA = a.payload?.lastOpened || 0; ++ const lastOpenedB = b.payload?.lastOpened|| 0; ++ return lastOpenedB - lastOpenedA; // Descending order ++ }); ++ if (window.gZenTabSearch.shouldInjectHeuristic()) { ++ unsortedResults[0].heuristic = true ++ unsortedResults[0].providerName = "HeuristicFallback" ++ } ++ } ++ + // Do the first pass over all results to build some state. + for (let result of unsortedResults) { + // Add each result to the appropriate `resultsByGroup` map. +@@ -794,6 +805,7 @@ class MuxerUnifiedComplete extends UrlbarMuxer { } if (result.providerName == lazy.UrlbarProviderTabToSearch.name) { diff --git a/src/browser/components/urlbar/UrlbarProviderOpenTabs-sys-mjs.patch b/src/browser/components/urlbar/UrlbarProviderOpenTabs-sys-mjs.patch new file mode 100644 index 0000000000..dbc5e21693 --- /dev/null +++ b/src/browser/components/urlbar/UrlbarProviderOpenTabs-sys-mjs.patch @@ -0,0 +1,120 @@ +diff --git a/browser/components/urlbar/UrlbarProviderOpenTabs.sys.mjs b/browser/components/urlbar/UrlbarProviderOpenTabs.sys.mjs +index 8e76ad53dbc9af7a748b6eecf6ccf7984c61b0b6..a4a2663793a4dcd38fa88fa88c2f30993ba4e49f 100644 +--- a/browser/components/urlbar/UrlbarProviderOpenTabs.sys.mjs ++++ b/browser/components/urlbar/UrlbarProviderOpenTabs.sys.mjs +@@ -241,6 +241,53 @@ export class UrlbarProviderOpenTabs extends UrlbarProvider { + await addToMemoryTable(url, userContextId).catch(console.error); + } + ++ /** ++ * Updates the last_opened timestamp for an open tab and ensures it is registered. ++ * ++ * @param {string} url Address of the tab ++ * @param {integer|string} userContextId Containers user context id ++ * @param {boolean} isInPrivateWindow In private browsing window or not ++ * @returns {Promise} resolved after the update. ++ */ ++ static async updateOpenTab(url, userContextId, isInPrivateWindow) { ++ // Ensure userContextId is consistently treated as an integer. ++ userContextId = parseInt(userContextId); ++ if (!Number.isInteger(userContextId)) { ++ lazy.logger.error("Invalid userContextId while updating openTab: ", { ++ url, ++ userContextId, ++ isInPrivateWindow, ++ }); ++ return; ++ } ++ ++ lazy.logger.info("Updating openTab: ", { ++ url, ++ userContextId, ++ isInPrivateWindow, ++ }); ++ ++ // Adjust userContextId for private windows. ++ userContextId = UrlbarProviderOpenTabs.getUserContextIdForOpenPagesTable( ++ userContextId, ++ isInPrivateWindow ++ ); ++ ++ // Ensure the tab is registered in the memory table. ++ let entries = gOpenTabUrls.get(userContextId); ++ if (!entries) { ++ entries = new Map(); ++ gOpenTabUrls.set(userContextId, entries); ++ } ++ if (!entries.has(url)) { ++ entries.set(url, 1); // Register the tab if not already present. ++ await addToMemoryTable(url, userContextId).catch(console.error); ++ } ++ ++ // Update the last_opened timestamp for the tab. ++ await updateLastOpened(url, userContextId).catch(console.error); ++ } ++ + /** + * Unregisters a previously registered open tab. + * +@@ -300,8 +347,9 @@ export class UrlbarProviderOpenTabs extends UrlbarProvider { + await UrlbarProviderOpenTabs.promiseDBPopulated; + await conn.executeCached( + ` +- SELECT url, userContextId ++ SELECT url, userContextId, last_opened + FROM moz_openpages_temp ++ ORDER BY last_opened DESC + `, + {}, + (row, cancel) => { +@@ -341,7 +389,7 @@ async function addToMemoryTable(url, userContextId, count = 1) { + let conn = await lazy.PlacesUtils.promiseLargeCacheDBConnection(); + await conn.executeCached( + ` +- INSERT OR REPLACE INTO moz_openpages_temp (url, userContextId, open_count) ++ INSERT OR REPLACE INTO moz_openpages_temp (url, userContextId, open_count, last_opened) + VALUES ( :url, + :userContextId, + IFNULL( ( SELECT open_count + 1 +@@ -349,7 +397,8 @@ async function addToMemoryTable(url, userContextId, count = 1) { + WHERE url = :url + AND userContextId = :userContextId ), + :count +- ) ++ ), ++ strftime('%s', 'now') -- Update the last_opened timestamp + ) + `, + { url, userContextId, count } +@@ -357,6 +406,31 @@ async function addToMemoryTable(url, userContextId, count = 1) { + }); + } + ++/** ++ * Updates the last_opened timestamp for a tab in the memory table. ++ * ++ * @param {string} url Address of the tab ++ * @param {number} userContextId Containers user context id ++ * @returns {Promise} resolved after the update. ++ */ ++async function updateLastOpened(url, userContextId) { ++ if (!UrlbarProviderOpenTabs.memoryTableInitialized) { ++ return; ++ } ++ await lazy.UrlbarProvidersManager.runInCriticalSection(async () => { ++ let conn = await lazy.PlacesUtils.promiseLargeCacheDBConnection(); ++ await conn.executeCached( ++ ` ++ UPDATE moz_openpages_temp ++ SET last_opened = strftime('%s', 'now') -- Update to current timestamp ++ WHERE url = :url ++ AND userContextId = :userContextId ++ `, ++ { url, userContextId } ++ ); ++ }); ++} ++ + /** + * Removes an open page from the memory table. + * diff --git a/src/browser/components/urlbar/UrlbarProviderPlaces-sys-mjs.patch b/src/browser/components/urlbar/UrlbarProviderPlaces-sys-mjs.patch index 37ca86d1ee..c52cc2ad4c 100644 --- a/src/browser/components/urlbar/UrlbarProviderPlaces-sys-mjs.patch +++ b/src/browser/components/urlbar/UrlbarProviderPlaces-sys-mjs.patch @@ -2,22 +2,23 @@ diff --git a/browser/components/urlbar/UrlbarProviderPlaces.sys.mjs b/browser/co index b1481a11ef38037bec13939928f72f9772e335a9..925f0dc34bf84bb9e0f143f5c1973a87e7b4f8ac 100644 --- a/browser/components/urlbar/UrlbarProviderPlaces.sys.mjs +++ b/browser/components/urlbar/UrlbarProviderPlaces.sys.mjs -@@ -35,6 +35,8 @@ const QUERYINDEX_SWITCHTAB = 9; +@@ -35,6 +35,9 @@ const QUERYINDEX_SWITCHTAB = 9; const QUERYINDEX_FRECENCY = 10; const QUERYINDEX_USERCONTEXTID = 11; const QUERYINDEX_LASTVIST = 12; +const QUERYINDEX_PINNEDTITLE = 13; +const QUERYINDEX_PINNEDURL = 14; ++const QUERYINDEX_LASTOPENED = 15; // Constants to support an alternative frecency algorithm. const PAGES_USE_ALT_FRECENCY = Services.prefs.getBoolPref( -@@ -65,11 +67,14 @@ const SQL_BOOKMARK_TAGS_FRAGMENT = `EXISTS(SELECT 1 FROM moz_bookmarks WHERE fk +@@ -65,11 +68,14 @@ const SQL_BOOKMARK_TAGS_FRAGMENT = `EXISTS(SELECT 1 FROM moz_bookmarks WHERE fk // condition once, and avoid evaluating "btitle" and "tags" when it is false. function defaultQuery(conditions = "") { let query = `SELECT :query_type, h.url, h.title, ${SQL_BOOKMARK_TAGS_FRAGMENT}, - h.visit_count, h.typed, h.id, t.open_count, ${PAGES_FRECENCY_FIELD}, t.userContextId, h.last_visit_date + h.visit_count, h.typed, h.id, t.open_count, ${PAGES_FRECENCY_FIELD}, t.userContextId, h.last_visit_date, -+ zp.title AS pinned_title, zp.url AS pinned_url ++ zp.title AS pinned_title, zp.url AS pinned_url, t.last_opened FROM moz_places h LEFT JOIN moz_openpages_temp t ON t.url = h.url @@ -27,7 +28,7 @@ index b1481a11ef38037bec13939928f72f9772e335a9..925f0dc34bf84bb9e0f143f5c1973a87 WHERE ( (:switchTabsEnabled AND t.open_count > 0) OR ${PAGES_FRECENCY_FIELD} <> 0 -@@ -83,7 +88,7 @@ function defaultQuery(conditions = "") { +@@ -83,7 +89,7 @@ function defaultQuery(conditions = "") { :matchBehavior, :searchBehavior, NULL) ELSE AUTOCOMPLETE_MATCH(:searchString, h.url, @@ -36,14 +37,38 @@ index b1481a11ef38037bec13939928f72f9772e335a9..925f0dc34bf84bb9e0f143f5c1973a87 h.visit_count, h.typed, 0, t.open_count, :matchBehavior, :searchBehavior, NULL) -@@ -1132,11 +1137,14 @@ Search.prototype = { +@@ -293,6 +299,7 @@ function convertLegacyMatches(context, matches, urls) { + firstToken: context.tokens[0], + userContextId: match.userContextId, + lastVisit: match.lastVisit, ++ lastOpened: match.lastOpened, + }); + // Should not happen, but better safe than sorry. + if (!result) { +@@ -342,6 +349,7 @@ function makeUrlbarResult(tokens, info) { + icon: info.icon, + userContextId: info.userContextId, + lastVisit: info.lastVisit, ++ lastOpened: info.lastOpened + }); + if (lazy.UrlbarPrefs.get("secondaryActions.switchToTab")) { + payload[0].action = UrlbarUtils.createTabSwitchSecondaryAction( +@@ -414,6 +422,7 @@ function makeUrlbarResult(tokens, info) { + blockL10n, + helpUrl, + lastVisit: info.lastVisit, ++ lastOpened: info.lastOpened, + }) + ); + } +@@ -1132,15 +1141,19 @@ Search.prototype = { let lastVisit = lastVisitPRTime ? lazy.PlacesUtils.toDate(lastVisitPRTime).getTime() : undefined; - + let pinnedTitle = row.getResultByIndex(QUERYINDEX_PINNEDTITLE); + let pinnedUrl = row.getResultByIndex(QUERYINDEX_PINNEDURL); -+ ++ let lastOpened = row.getResultByIndex(QUERYINDEX_LASTOPENED); + let match = { placeId, @@ -54,3 +79,8 @@ index b1481a11ef38037bec13939928f72f9772e335a9..925f0dc34bf84bb9e0f143f5c1973a87 icon: UrlbarUtils.getIconForUrl(url), frecency: frecency || FRECENCY_DEFAULT, userContextId, + lastVisit, ++ lastOpened: lastOpened || 0 + }; + if (openPageCount > 0 && this.hasBehavior("openpage")) { + if ( diff --git a/src/browser/components/urlbar/UrlbarUtils-sys-mjs.patch b/src/browser/components/urlbar/UrlbarUtils-sys-mjs.patch new file mode 100644 index 0000000000..655523a708 --- /dev/null +++ b/src/browser/components/urlbar/UrlbarUtils-sys-mjs.patch @@ -0,0 +1,24 @@ +diff --git a/browser/components/urlbar/UrlbarUtils.sys.mjs b/browser/components/urlbar/UrlbarUtils.sys.mjs +index 0968dfa885512096f1e58ec898eda139d9aea217..87974b95053bbb2b359bdcf6cf645104314abdca 100644 +--- a/browser/components/urlbar/UrlbarUtils.sys.mjs ++++ b/browser/components/urlbar/UrlbarUtils.sys.mjs +@@ -1775,6 +1775,9 @@ UrlbarUtils.RESULT_PAYLOAD_SCHEMA = { + lastVisit: { + type: "number", + }, ++ lastOpened: { ++ type: "number", ++ }, + title: { + type: "string", + }, +@@ -1908,6 +1911,9 @@ UrlbarUtils.RESULT_PAYLOAD_SCHEMA = { + lastVisit: { + type: "number", + }, ++ lastOpened: { ++ type: "number", ++ }, + originalUrl: { + type: "string", + }, diff --git a/src/toolkit/components/places/PlacesUtils-sys-mjs.patch b/src/toolkit/components/places/PlacesUtils-sys-mjs.patch new file mode 100644 index 0000000000..8e4e808cc6 --- /dev/null +++ b/src/toolkit/components/places/PlacesUtils-sys-mjs.patch @@ -0,0 +1,12 @@ +diff --git a/toolkit/components/places/PlacesUtils.sys.mjs b/toolkit/components/places/PlacesUtils.sys.mjs +index 572d8678994b338e62f46869a2840948a74a6b1c..618878321bc605fb01d8caba466ff81ccc39fcb8 100644 +--- a/toolkit/components/places/PlacesUtils.sys.mjs ++++ b/toolkit/components/places/PlacesUtils.sys.mjs +@@ -2248,6 +2248,7 @@ ChromeUtils.defineLazyGetter(lazy, "gAsyncDBLargeCacheConnPromised", () => + url TEXT, + userContextId INTEGER, + open_count INTEGER, ++ last_opened INTEGER, + PRIMARY KEY (url, userContextId) + )`); + await conn.execute(` diff --git a/src/toolkit/components/places/nsPlacesTables-h.patch b/src/toolkit/components/places/nsPlacesTables-h.patch new file mode 100644 index 0000000000..34a01ed6fb --- /dev/null +++ b/src/toolkit/components/places/nsPlacesTables-h.patch @@ -0,0 +1,12 @@ +diff --git a/toolkit/components/places/nsPlacesTables.h b/toolkit/components/places/nsPlacesTables.h +index bfcc0c24aca4fcd951d66bd96c13ebe1a7df32eb..0ad78a3e1cd68b6efc3ea0adbb2553085fec1ef1 100644 +--- a/toolkit/components/places/nsPlacesTables.h ++++ b/toolkit/components/places/nsPlacesTables.h +@@ -196,6 +196,7 @@ + " url TEXT" \ + ", userContextId INTEGER" \ + ", open_count INTEGER" \ ++ ", last_opened INTEGER" \ + ", PRIMARY KEY (url, userContextId)" \ + ")") + diff --git a/src/zen/common/zen-sets.js b/src/zen/common/zen-sets.js index 1f73018f32..49670293c1 100644 --- a/src/zen/common/zen-sets.js +++ b/src/zen/common/zen-sets.js @@ -97,6 +97,11 @@ document.addEventListener( case 'cmd_zenIgnoreUnloadTab': gZenTabUnloader.ignoreUnloadTab(); break; + case 'cmd_zenSearchTabs': + gZenTabSearch.searchTabsShortcut(); + break; + case 'cmd_zenBackwardSearchTabs': + gZenTabSearch.searchTabsShortcut(-1); default: if (event.target.id.startsWith('cmd_zenWorkspaceSwitch')) { const index = parseInt(event.target.id.replace('cmd_zenWorkspaceSwitch', ''), 10) - 1; diff --git a/src/zen/kbs/ZenKeyboardShortcuts.mjs b/src/zen/kbs/ZenKeyboardShortcuts.mjs index 9feec1f7e2..ca088946f9 100644 --- a/src/zen/kbs/ZenKeyboardShortcuts.mjs +++ b/src/zen/kbs/ZenKeyboardShortcuts.mjs @@ -77,6 +77,7 @@ const defaultKeyboardGroups = { ], searchAndFind: [ 'zen-search-focus-shortcut', + 'zen-search-tabs-shortcut', 'zen-search-focus-shortcut-alt', 'zen-find-shortcut', 'zen-search-find-again-shortcut-2', @@ -734,6 +735,31 @@ class ZenKeyboardShortcutsLoader { ) ); + //tabs search + newShortcutList.push( + new KeyShortcut( + 'zen-backward-search-tabs', + 'Q', + '', + 'searchAndFind', + KeyShortcutModifiers.fromObject({ accel: false, alt: true, shift: true }), + 'cmd_zenBackwardSearchTabs', + 'zen-backward-search-shortcut-tabs' + ) + ); + + newShortcutList.push( + new KeyShortcut( + 'zen-search-tabs', + 'Q', + '', + 'searchAndFind', + KeyShortcutModifiers.fromObject({ accel: false, alt: true }), + 'cmd_zenSearchTabs', + 'zen-search-shortcut-tabs' + ) + ); + return newShortcutList; } diff --git a/src/zen/tabsearch/ZenTabSearch.mjs b/src/zen/tabsearch/ZenTabSearch.mjs new file mode 100644 index 0000000000..d57fe56be8 --- /dev/null +++ b/src/zen/tabsearch/ZenTabSearch.mjs @@ -0,0 +1,117 @@ +{ + class ZenTabSearch extends ZenDOMOperatedFeature { + init() { + this._heuristic = false; + this._updateAutoSelectResult = this._updateAutoSelectResult.bind(this); + this._handleKeyUp = this._handleKeyUp.bind(this); + Services.prefs.addObserver( + 'zen.tabsearch.auto-select-result', + this._updateAutoSelectResult, + false + ); + this._updateAutoSelectResult(); + window.setTimeout(() => { + this._setupListener(); + }, 500); + } + + _updateAutoSelectResult() { + this.autoSelectResult = Services.prefs.getBoolPref('zen.tabsearch.auto-select-result', false); + } + + _setupListener() { + // Ensure gURLBar, its panel, and inputField are available + if (typeof gURLBar !== 'undefined') { + console.log('gURLBar or its components are not yet initialized'); + // Bind the handlers to this instance for correct 'this' and removal + const self = this; // Capture the correct 'this' context + gURLBar.controller.addQueryListener({ + onViewOpen() { + window.addEventListener('keyup', self._handleKeyUp, true); + self._heuristic = false; + }, + onViewClose() { + window.removeEventListener('keyup', self._handleKeyUp, true); + self._heuristic = false; + }, + onQueryStarted() { + window.removeEventListener('keyup', self._handleKeyUp, true); + }, + onQueryFinished() { + self._heuristic = true; + }, + + }); + } else { + // Retry if gURLBar or its components are not yet initialized + window.setTimeout(() => { + // 'this' here will refer to the ZenTabSearch instance because + // setTimeout is called on 'window', and the callback is an arrow function, + // which lexically captures 'this' from the surrounding scope (the catch block). + // In the catch block, 'this' is the ZenTabSearch instance. + this._setupListener(); + }, 500); + } + } + + _handleKeyUp(event) { + if (event.key === 'Alt' || event.key === 'Control') { + if ( + this._heuristic && + this.autoSelectResult && + gURLBar.view.isOpen && + gURLBar.searchMode?.source === 4 /* URLBarUtils.RESULT_SOURCE.TABS */ + ) { + // simulate enter key press + if (this.autoSelectResult) { + const enterEvent = new KeyboardEvent('keydown', { + key: 'Enter', + code: 'Enter', + keyCode: 13, + which: 13, + bubbles: true, + cancelable: true, + }); + gURLBar.handleCommand(enterEvent); + event.preventDefault(); + event.stopImmediatePropagation(); + } + } else { + // window.removeEventListener('keyup', this._handleKeyUp, true); + } + } + } + + shouldInjectHeuristic() { + if (!this.autoSelectResult) return true; + if (this._heuristic) return true; + return false; + } + + async searchTabsShortcut(offset = 1) { + // if the search is already open, we just need to select the next result + if ( + gURLBar.view.isOpen && + gURLBar.searchMode && + gURLBar.searchMode.source === 4 /* URLBarUtils.RESULT_SOURCE.TABS */ + ) { + if (gURLBar.view.visibleRowCount > 0) { + if (offset > 0) { + gURLBar.view.selectBy(offset); + } else if (offset < 0) { + gURLBar.view.selectBy(-offset, { reverse: true }); + } + window.addEventListener('keyup', this._handleKeyUp, true); + this._heuristic = true; + } + } else { + gURLBar.search('% '); + this._heuristic = false; + } + } + } + + if (Services.prefs.getBoolPref('zen.tabsearch.enabled', true)) { + window.gZenTabSearch = new ZenTabSearch(); + } +} diff --git a/src/zen/workspaces/ZenWorkspaces.mjs b/src/zen/workspaces/ZenWorkspaces.mjs index af67474e31..89f6bbb7c1 100644 --- a/src/zen/workspaces/ZenWorkspaces.mjs +++ b/src/zen/workspaces/ZenWorkspaces.mjs @@ -3137,7 +3137,7 @@ var gZenWorkspaces = new (class extends ZenMultiWindowFeature { if (!this._hasInitializedTabsStrip) { return gBrowser.browsers; } - const browsers = Array.from(gBrowser.tabpanels.querySelectorAll('browser')); + const browsers = Array.from(this.allStoredTabs).map(tab => tab.linkedBrowser); // Sort browsers by making the current workspace first const currentWorkspace = this.activeWorkspace; const sortedBrowsers = browsers.sort((a, b) => {