diff --git a/background_scripts/commands.js b/background_scripts/commands.js index ba8787e62..e6577dd31 100644 --- a/background_scripts/commands.js +++ b/background_scripts/commands.js @@ -325,6 +325,7 @@ const Commands = { "Vomnibar.activateBookmarks", "Vomnibar.activateBookmarksInNewTab", "Vomnibar.activateTabSelection", + "Vomnibar.restoreSession", "Vomnibar.activateEditUrl", "Vomnibar.activateEditUrlInNewTab", ], @@ -556,6 +557,7 @@ const commandDescriptions = { "Vomnibar.activateEditUrlInNewTab": ["Edit the current URL and open in a new tab", { topFrame: true, }], + "Vomnibar.restoreSession": ["Restore a recently closed session", { topFrame: true }], nextFrame: ["Select the next frame on the page", { background: true }], mainFrame: ["Select the page's main/top frame", { topFrame: true, noRepeat: true }], diff --git a/background_scripts/completion.js b/background_scripts/completion.js index 1dce50820..a4e37f283 100644 --- a/background_scripts/completion.js +++ b/background_scripts/completion.js @@ -39,6 +39,7 @@ class Suggestion { deDuplicate = true; // The tab represented by this suggestion. Populated by TabCompleter. tabId; + sessionId; // Whether this is a suggestion provided by a user's custom search engine. isCustomSearch; // Whether this is meant to be the first suggestion from the user's custom search engine which @@ -517,6 +518,49 @@ class TabCompleter { } } +class RecentlyClosedTabCompleter { + async filter({ queryTerms, completerName }) { + if (completerName != "recentlyClosed" && queryTerms.length == 0) return []; + + const tabUrls = (await chrome.tabs.query({})).map((tab) => tab.url); + let sessions = (await chrome.sessions.getRecentlyClosed({})).filter( + (session) => (session.tab && !tabUrls.includes(session.tab.url) || session.window) + ); + const results = sessions.filter(session => session.tab ? RankingUtils.matches(queryTerms, session.tab.url, session.tab.title) : RankingUtils.matches(queryTerms, ...session.window.tabs.map(t => t.url), ...session.window.tabs.map(t => t.title))); + const suggestions = results + .map((session) => { + const suggestion = new Suggestion({ + queryTerms, + description: "session", + url: session.tab ? session.tab.url : session.window.tabs.map(t => t.title).join(", "), + title: session.tab ? session.tab.title : `${session.window.tabs.length} tabs`, + sessionId: (session.tab || session.window).sessionId, + deDuplicate: false, + }); + suggestion.relevancy = this.computeRelevancy(suggestion); + return suggestion; + }) + .sort((a, b) => b.relevancy - a.relevancy); + suggestions.forEach(function (suggestion, i) { + suggestion.relevancy *= 8; + suggestion.relevancy /= i / 4 + 1; + }); + return suggestions; + } + + computeRelevancy(suggestion) { + if (suggestion.queryTerms.length) { + return RankingUtils.wordRelevancy( + suggestion.queryTerms, + suggestion.url, + suggestion.title + ); + } else { + return BgUtils.tabRecency.recencyScore(suggestion.sessionId); + } + } +} + class SearchEngineCompleter { cancel() { CompletionSearch.cancel(); @@ -624,9 +668,9 @@ class MultiCompleter { // The only UX where we support showing results when there are no query terms is via // Vomnibar.activateTabSelection, where we show the list of open tabs by recency. - const isTabCompleter = this.completers.length == 1 && - this.completers[0] instanceof TabCompleter; - if (queryTerms.length == 0 && !isTabCompleter) { + const showResults = this.completers.length == 1 && + (this.completers[0] instanceof TabCompleter || this.completers[0] instanceof RecentlyClosedTabCompleter); + if (queryTerms.length == 0 && !showResults) { return []; } @@ -963,6 +1007,7 @@ Object.assign(globalThis, { HistoryCompleter, DomainCompleter, TabCompleter, + RecentlyClosedTabCompleter, SearchEngineCompleter, HistoryCache, RankingUtils, diff --git a/background_scripts/main.js b/background_scripts/main.js index 880066113..3178d6367 100644 --- a/background_scripts/main.js +++ b/background_scripts/main.js @@ -23,6 +23,7 @@ const completionSources = { history: new HistoryCompleter(), domains: new DomainCompleter(), tabs: new TabCompleter(), + recentlyClosed: new RecentlyClosedTabCompleter(), searchEngines: new SearchEngineCompleter(), }; @@ -36,6 +37,7 @@ const completers = { ]), bookmarks: new MultiCompleter([completionSources.bookmarks]), tabs: new MultiCompleter([completionSources.tabs]), + recentlyClosed: new MultiCompleter([completionSources.recentlyClosed]), }; const onURLChange = (details) => { @@ -124,6 +126,11 @@ const toggleMuteTab = (request, sender) => { } }; +// +// Restore the session with the ID specified in request.id +// +const restoreSession = (request) => chrome.sessions.restore(request.id); + // // Selects the tab with the ID specified in request.id // @@ -537,6 +544,7 @@ const sendRequestHandlers = { nextFrame: BackgroundCommands.nextFrame, selectSpecificTab, + restoreSession, createMark: Marks.create.bind(Marks), gotoMark: Marks.goto.bind(Marks), // Send a message to all frames in the current tab. If request.frameId is provided, then send diff --git a/content_scripts/mode_normal.js b/content_scripts/mode_normal.js index 7ae99f50b..470b36fbf 100644 --- a/content_scripts/mode_normal.js +++ b/content_scripts/mode_normal.js @@ -341,6 +341,7 @@ if (typeof Vomnibar !== "undefined") { "Vomnibar.activateBookmarksInNewTab": Vomnibar.activateBookmarksInNewTab.bind(Vomnibar), "Vomnibar.activateEditUrl": Vomnibar.activateEditUrl.bind(Vomnibar), "Vomnibar.activateEditUrlInNewTab": Vomnibar.activateEditUrlInNewTab.bind(Vomnibar), + "Vomnibar.restoreSession": Vomnibar.restoreSession.bind(Vomnibar), }); } diff --git a/content_scripts/vomnibar.js b/content_scripts/vomnibar.js index 75f2800dc..1d56db11b 100644 --- a/content_scripts/vomnibar.js +++ b/content_scripts/vomnibar.js @@ -24,6 +24,13 @@ const Vomnibar = { }); }, + restoreSession(sourceFrameId) { + this.open(sourceFrameId, { + completer: "recentlyClosed", + selectFirst: true, + }); + }, + activateBookmarks(sourceFrameId) { this.open(sourceFrameId, { completer: "bookmarks", diff --git a/pages/vomnibar.js b/pages/vomnibar.js index 04cb35731..25531135b 100644 --- a/pages/vomnibar.js +++ b/pages/vomnibar.js @@ -372,6 +372,11 @@ class VomnibarUI { openCompletion(completion, openInNewTab) { if (completion.description == "tab") { chrome.runtime.sendMessage({ handler: "selectSpecificTab", id: completion.tabId }); + } else if (completion.description == "session") { + chrome.runtime.sendMessage({ + handler: "restoreSession", + id: completion.sessionId, + }); } else { this.launchUrl(completion.url, openInNewTab); }