Skip to content
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
dist
dist
.serena
54 changes: 54 additions & 0 deletions background_scripts/all_commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,22 @@ const allCommands = [
noRepeat: true,
},

{
name: "Vomnibar.activateTabGroupSelection",
desc: "Search through your tab groups",
group: "vomnibar",
topFrame: true,
noRepeat: true,
},

{
name: "Vomnibar.activateGroupAssign",
desc: "Assign tab(s) to a group",
group: "vomnibar",
topFrame: true,
noRepeat: true,
},

{
name: "Vomnibar.activateEditUrl",
desc: "Edit the current URL",
Expand Down Expand Up @@ -519,6 +535,44 @@ const allCommands = [
noRepeat: true,
},

{
name: "collapseTabGroup",
desc: "Collapse current tab's group",
group: "tabs",
background: true,
noRepeat: true,
},

{
name: "previousTabGroup",
desc: "Switch to previous tab group",
group: "tabs",
background: true,
noRepeat: true,
},

{
name: "nextTabGroup",
desc: "Switch to next tab group",
group: "tabs",
background: true,
noRepeat: true,
},

{
name: "selectNextTabForGroup",
desc: "Select next tab",
group: "tabs",
background: true,
},

{
name: "selectPreviousTabForGroup",
desc: "Select previous tab",
group: "tabs",
background: true,
},

{
name: "removeTab",
desc: "Close current tab",
Expand Down
7 changes: 7 additions & 0 deletions background_scripts/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,8 @@ const defaultKeyMappings = {
"o": "Vomnibar.activate",
"O": "Vomnibar.activateInNewTab",
"T": "Vomnibar.activateTabSelection",
"ZG": "Vomnibar.activateTabGroupSelection",
"zg": "Vomnibar.activateGroupAssign",
"b": "Vomnibar.activateBookmarks",
"B": "Vomnibar.activateBookmarksInNewTab",
":": "Vomnibar.activateCommandSelection",
Expand Down Expand Up @@ -483,6 +485,11 @@ const defaultKeyMappings = {
"zi": "zoomIn",
"zo": "zoomOut",
"z0": "zoomReset",
"za": "collapseTabGroup",
"zN": "previousTabGroup",
"zn": "nextTabGroup",
"zz": "selectNextTabForGroup",
"ZZ": "selectPreviousTabForGroup",

// Marks
"m": "Marks.activateCreateMode",
Expand Down
11 changes: 7 additions & 4 deletions background_scripts/completion/completers.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export class Suggestion {
// The generated HTML string for showing this suggestion in the Vomnibar.
html;
searchUrl;
groupData;

constructor(options) {
Object.seal(this);
Expand Down Expand Up @@ -741,10 +742,12 @@ export class MultiCompleter {
const queryTerms = request.queryTerms;

// 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) {
// Vomnibar.activateTabSelection, and completers that explicitly opt in via showResultsWithNoQuery.
const showsEmptyResults = this.completers.length == 1 && (
this.completers[0] instanceof TabCompleter ||
this.completers[0].showResultsWithNoQuery
);
if (queryTerms.length == 0 && !showsEmptyResults) {
return [];
}

Expand Down
141 changes: 141 additions & 0 deletions background_scripts/completion/group_completer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import * as ranking from "./ranking.js";
import { Suggestion } from "./completers.js";

const GROUP_COLORS = ["grey", "blue", "red", "yellow", "green", "pink", "purple", "cyan", "orange"];

const COLOR_CSS = {
grey: "#5F6368", blue: "#1A73E8", red: "#D93025", yellow: "#F9AB00",
green: "#1E8E3E", pink: "#D01884", purple: "#8430CE", cyan: "#007B83", orange: "#E37400",
};

async function queryCurrentWindow() {
const win = await chrome.windows.getLastFocused({ populate: false });
const [groups, tabs] = await Promise.all([
chrome.tabGroups.query({ windowId: win.id }),
chrome.tabs.query({ windowId: win.id }),
]);
return { groups, tabs };
}

function buildFirstTabMap(tabs) {
const map = new Map();
for (const tab of tabs.sort((a, b) => a.index - b.index)) {
if (tab.groupId !== -1 && !map.has(tab.groupId)) {
map.set(tab.groupId, tab);
}
}
return map;
}

function esc(s) {
return String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}

function colorSwatch(color) {
const bg = COLOR_CSS[color] ?? "#888";
return `<span class="group-color-swatch" style="background:${bg}"></span>`;
}

function groupHtml(group, url) {
const source = `${colorSwatch(group.color)}${esc(group.color)}`;
const name = esc(group.title || `(${group.color})`);
return `<div class="top-half"><span class="source">${source}</span><span class="title">${name}</span></div>` +
`<div class="bottom-half"><span class="url">${esc(url)}</span></div>`;
}

// ZG: navigate to a tab group by name.
export class TabGroupCompleter {
showResultsWithNoQuery = true;

async filter({ queryTerms }) {
if (!chrome.tabGroups) return [];
const { groups, tabs } = await queryCurrentWindow();
const firstTabByGroup = buildFirstTabMap(tabs);
return groups
.filter((g) => queryTerms.length === 0 || ranking.matches(queryTerms, g.title ?? "", g.color))
.map((group) => {
const firstTab = firstTabByGroup.get(group.id);
const url = firstTab?.url ?? "";
const s = new Suggestion({
queryTerms,
description: group.color || "tab group",
url,
title: group.title || `(${group.color})`,
tabId: firstTab?.id,
deDuplicate: false,
});
s.html = groupHtml(group, url);
s.relevancy = 1;
return s;
});
}
}

// zg step 1: assign highlighted tabs to an existing group, or start creating a new one.
export class TabGroupAssignCompleter {
showResultsWithNoQuery = true;

async filter({ queryTerms, query }) {
if (!chrome.tabGroups) return [];
const { groups, tabs } = await queryCurrentWindow();
const firstTabByGroup = buildFirstTabMap(tabs);
const existingMatches = groups
.filter((g) => queryTerms.length === 0 || ranking.matches(queryTerms, g.title ?? "", g.color))
.map((group) => {
const firstTab = firstTabByGroup.get(group.id);
const url = firstTab?.url ?? "";
const s = new Suggestion({
queryTerms,
description: group.color || "tab group",
url,
title: group.title || `(${group.color})`,
deDuplicate: false,
groupData: { action: "addToGroup", groupId: group.id },
});
s.html = groupHtml(group, url);
s.relevancy = 1;
return s;
});

const name = query.trim();
if (name) {
const create = new Suggestion({
queryTerms: [],
description: "new group",
url: "",
title: `Create "${name}"`,
deDuplicate: false,
groupData: { action: "createGroup", name },
});
create.html = `<div class="top-half"><span class="source">new group</span>` +
`<span class="title">${esc(`Create "${name}"`)}</span></div>`;
create.relevancy = 0;
return [...existingMatches, create];
}
return existingMatches;
}
}

// zg step 2: pick a color for a new group.
export class TabGroupColorCompleter {
showResultsWithNoQuery = true;

async filter({ queryTerms }) {
return GROUP_COLORS
.filter((c) => queryTerms.length === 0 || c.includes(queryTerms[0]?.toLowerCase() ?? ""))
.map((color, i) => {
const s = new Suggestion({
queryTerms,
description: "color",
url: "",
title: color,
deDuplicate: false,
groupData: { action: "setColor", color },
});
s.html = `<div class="top-half"><span class="source">${colorSwatch(color)}</span>` +
`<span class="title">${esc(color)}</span></div>`;
s.relevancy = GROUP_COLORS.length - i;
return s;
});
}
}
67 changes: 48 additions & 19 deletions background_scripts/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import { Commands } from "../background_scripts/commands.js";
import * as exclusions from "../background_scripts/exclusions.js";
import "../background_scripts/completion/search_engines.js";
import "../background_scripts/completion/search_wrapper.js";
import "../background_scripts/completion/completers.js";
import "../background_scripts/tab_operations.js";
import * as marks from "../background_scripts/marks.js";
import * as tabGroups from "./tab_groups.js";

import {
BookmarkCompleter,
Expand All @@ -21,6 +21,11 @@ import {
SearchEngineCompleter,
TabCompleter,
} from "./completion/completers.js";
import {
TabGroupCompleter,
TabGroupAssignCompleter,
TabGroupColorCompleter,
} from "./completion/group_completer.js";

// NOTE(philc): This file has many superfluous return statements in its functions, as a result of
// converting from coffeescript to es6. Many can be removed, but I didn't take the time to
Expand Down Expand Up @@ -49,6 +54,9 @@ const completionSources = {
domains: new DomainCompleter(),
tabs: new TabCompleter(),
searchEngines: new SearchEngineCompleter(),
tabGroups: new TabGroupCompleter(),
tabGroupAssign: new TabGroupAssignCompleter(),
groupColors: new TabGroupColorCompleter(),
};

const completers = {
Expand All @@ -62,6 +70,9 @@ const completers = {
bookmarks: new MultiCompleter([completionSources.bookmarks]),
commands: new MultiCompleter([completionSources.commands]),
tabs: new MultiCompleter([completionSources.tabs]),
tabGroups: new MultiCompleter([completionSources.tabGroups]),
tabGroupAssign: new MultiCompleter([completionSources.tabGroupAssign]),
groupColors: new MultiCompleter([completionSources.groupColors]),
};

// A query dictionary for `chrome.tabs.query` that will return only the visible tabs.
Expand Down Expand Up @@ -175,28 +186,16 @@ function getTabIndex(tab, tabs) {
//
async function selectSpecificTab(request) {
const tab = await chrome.tabs.get(request.id);
if (tab.groupId !== -1 && chrome.tabGroups) {
await chrome.tabGroups.update(tab.groupId, { collapsed: false });
}
// Focus the tab's window. TODO(philc): Why are we null-checking chrome.windows here?
if (chrome.windows != null) {
await chrome.windows.update(tab.windowId, { focused: true });
}
await chrome.tabs.update(request.id, { active: true });
}

function moveTab({ count, tab, registryEntry }) {
if (registryEntry.command === "moveTabLeft") {
count = -count;
}
return chrome.tabs.query(visibleTabsQueryArgs, function (tabs) {
const pinnedCount = (tabs.filter((tab) => tab.pinned)).length;
const minIndex = tab.pinned ? 0 : pinnedCount;
const maxIndex = (tab.pinned ? pinnedCount : tabs.length) - 1;
// The tabs array index of the new position.
const moveIndex = Math.max(minIndex, Math.min(maxIndex, getTabIndex(tab, tabs) + count));
return chrome.tabs.move(tab.id, {
index: tabs[moveIndex].index,
});
});
}

function createRepeatCommand(command) {
return async function (request) {
Expand Down Expand Up @@ -343,8 +342,13 @@ const BackgroundCommands = {
});
},
toggleMuteTab,
moveTabLeft: moveTab,
moveTabRight: moveTab,
collapseTabGroup: tabGroups.collapseTabGroup,
previousTabGroup: tabGroups.previousTabGroup,
nextTabGroup: tabGroups.nextTabGroup,
selectNextTabForGroup: tabGroups.selectNextTabForGroup,
selectPreviousTabForGroup: tabGroups.selectPreviousTabForGroup,
moveTabLeft: tabGroups.moveTab,
moveTabRight: tabGroups.moveTab,

async setZoom({ tabId, registryEntry }) {
const level = registryEntry.options?.["level"] ?? "1";
Expand Down Expand Up @@ -470,8 +474,16 @@ async function removeTabsRelative(direction, { count, tab }) {
// Selects a tab before or after the currently selected tab.
// - direction: "next", "previous", "first" or "last".
function selectTab(direction, { count, tab }) {
chrome.tabs.query(visibleTabsQueryArgs, function (tabs) {
chrome.tabs.query(visibleTabsQueryArgs, async function (tabs) {
if (tabs.length > 1) {
// Skip tabs in collapsed tab groups.
if (chrome.tabGroups) {
const groups = await chrome.tabGroups.query({ windowId: tab.windowId, collapsed: true });
const collapsedGroupIds = new Set(groups.map((g) => g.id));
tabs = tabs.filter((t) => t.id === tab.id || !collapsedGroupIds.has(t.groupId));
if (tabs.length <= 1) return;
}

const toSelect = (() => {
switch (direction) {
case "next":
Expand Down Expand Up @@ -651,6 +663,23 @@ const sendRequestHandlers = {

nextFrame: BackgroundCommands.nextFrame,
selectSpecificTab,

async addTabsToGroup(request) {
const win = await chrome.windows.getLastFocused();
const tabs = await chrome.tabs.query({ windowId: win.id });
const tabIds = tabs.filter((t) => t.highlighted).map((t) => t.id);
if (tabIds.length === 0) return;
await chrome.tabs.group({ tabIds, groupId: request.groupId });
},

async createTabGroupWithColor(request) {
const win = await chrome.windows.getLastFocused();
const tabs = await chrome.tabs.query({ windowId: win.id });
const tabIds = tabs.filter((t) => t.highlighted).map((t) => t.id);
if (tabIds.length === 0) return;
const groupId = await chrome.tabs.group({ tabIds });
await chrome.tabGroups.update(groupId, { title: request.name, color: request.color });
},
createMark: marks.create,
gotoMark: marks.goto,
// Send a message to all frames in the current tab. If request.frameId is provided, then send
Expand Down
Loading