Merge pull request #4539 from rddunphy/feature/tab-groups

Tab group fixes
This commit is contained in:
Oliver Blanthorn 2023-01-21 22:32:53 +01:00 committed by GitHub
commit 640dfb34bf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 240 additions and 59 deletions

View file

@ -181,21 +181,25 @@ export class BufferCompletionSource extends Completions.CompletionSourceFuse {
private async fillOptions() {
let tabs: browser.tabs.Tab[]
// Get alternative tab, defined as last accessed tab in any group in
// this window.
const currentWindowTabs = await browserBg.tabs.query({
currentWindow: true,
})
currentWindowTabs.sort((a, b) => b.lastAccessed - a.lastAccessed)
const altTab = currentWindowTabs[1]
if (config.get("tabshowhidden") === "true") {
tabs = await browserBg.tabs.query({
currentWindow: true
})
tabs = currentWindowTabs
} else {
tabs = await browserBg.tabs.query({
currentWindow: true,
hidden: false
hidden: false,
})
tabs.sort((a, b) => b.lastAccessed - a.lastAccessed)
}
const options = []
// Get alternative tab, defined as last accessed tab.
tabs.sort((a, b) => b.lastAccessed - a.lastAccessed)
const alt = tabs[1]
const useMruTabOrder = config.get("tabsort") === "mru"
if (!useMruTabOrder) {
@ -221,7 +225,7 @@ export class BufferCompletionSource extends Completions.CompletionSourceFuse {
new BufferCompletionOption(
(tab.index + 1).toString(),
tab,
tab === alt,
tab.index === altTab.index,
tab_container,
),
)

View file

@ -4,6 +4,7 @@ import * as Containers from "@src/lib/containers"
import * as Completions from "@src/completions"
import * as Messaging from "@src/lib/messaging"
import * as config from "@src/lib/config"
import { tabTgroup } from "@src/lib/tab_groups"
class TabAllCompletionOption
extends Completions.CompletionOptionHTML
@ -13,15 +14,54 @@ class TabAllCompletionOption
constructor(
public value: string,
tab: browser.tabs.Tab,
isAlternative: boolean,
isCurrent: boolean,
winindex: number,
container: browser.contextualIdentities.ContextualIdentity,
incognito: boolean,
tgroupname: string,
) {
super()
this.value = `${winindex}.${tab.index + 1}`
const valueStr = `${winindex}.${tab.index + 1}`
this.value = valueStr
this.fuseKeys.push(this.value, tab.title, tab.url)
this.tab = tab
// pre contains max four uppercase characters for tab status.
// If statusstylepretty is set to true replace use unicode characters,
// but keep plain letters in hidden column for completion.
let preplain = ""
if (isCurrent) {
preplain += "%"
} else if (isAlternative) {
preplain += "#"
this.value = "#"
}
let pre = preplain
if (tab.pinned) preplain += "P"
if (tab.audible) preplain += "A"
if (tab.mutedInfo.muted) preplain += "M"
if (tab.discarded) preplain += "D"
if (config.get("completions", "Tab", "statusstylepretty") === "true") {
if (tab.pinned) pre += "\uD83D\uDCCC"
if (tab.audible) pre += "\uD83D\uDD0A"
if (tab.mutedInfo.muted) pre += "\uD83D\uDD07"
if (tab.discarded) pre += "\u2296"
} else {
pre = preplain
}
tgroupname = tgroupname === undefined ? "" : tgroupname
// Push prefix before padding so we don't match on whitespace
this.fuseKeys.push(pre)
this.fuseKeys.push(preplain)
this.fuseKeys.push(tgroupname)
// Push properties we want to fuzmatch on
this.fuseKeys.push(String(tab.index + 1), tab.title, tab.url)
// Create HTMLElement
const favIconUrl = tab.favIconUrl
? tab.favIconUrl
@ -31,14 +71,16 @@ class TabAllCompletionOption
? "incognito"
: ""}"
>
<td class="prefix"></td>
<td class="prefix">${pre}</td>
<td class="prefixplain" hidden>${preplain}</td>
<td class="privatewindow"></td>
<td class="container"></td>
<td class="icon"><img src="${favIconUrl}" /></td>
<td class="title">${this.value}: ${tab.title}</td>
<td class="title">${valueStr}: ${tab.title}</td>
<td class="content">
<a class="url" target="_blank" href=${tab.url}>${tab.url}</a>
</td>
<td class="tgroup">${tgroupname}</td>
</tr>`
}
}
@ -147,6 +189,12 @@ export class TabAllCompletionSource extends Completions.CompletionSourceFuse {
return a.windowId - b.windowId
})
const currentWindowTabs = await browserBg.tabs.query({
currentWindow: true,
})
currentWindowTabs.sort((a, b) => b.lastAccessed - a.lastAccessed)
const altTab = currentWindowTabs[1]
// Check to see if this is a command that needs to exclude the current
// window
const excludeCurrentWindow = ["tabgrab"].includes(prefix.trim())
@ -162,14 +210,20 @@ export class TabAllCompletionSource extends Completions.CompletionSourceFuse {
}
// if we are excluding the current window and this tab is in the current window
// then skip it
if (excludeCurrentWindow && tab.windowId === currentWindow.id) continue
if (excludeCurrentWindow && tab.windowId === currentWindow.id)
continue
options.push(
new TabAllCompletionOption(
tab.id.toString(),
tab,
tab.index === altTab.index &&
tab.windowId === altTab.windowId,
tab.active &&
tab.windowId === currentWindowTabs[0].windowId,
winindex,
await Containers.getFromId(tab.cookieStoreId),
windows[tab.windowId].incognito,
await tabTgroup(tab.id),
),
)
}

View file

@ -1,20 +1,64 @@
import * as Completions from "@src/completions"
import { tgroups, windowTgroup, tgroupTabs } from "@src/lib/tab_groups"
import * as config from "@src/lib/config"
import {
tgroups,
windowTgroup,
windowLastTgroup,
tgroupTabs,
} from "@src/lib/tab_groups"
class TabGroupCompletionOption extends Completions.CompletionOptionHTML
class TabGroupCompletionOption
extends Completions.CompletionOptionHTML
implements Completions.CompletionOptionFuse {
public fuseKeys = []
constructor(group: string, tabCount: number) {
constructor(
group: string,
tabCount: number,
current: boolean,
alternate: boolean,
audible: boolean,
urls: string[],
) {
super()
this.value = group
let preplain = ""
if (current) {
preplain += "%"
}
if (alternate) {
preplain += "#"
}
let pre = preplain
if (audible) {
preplain += "A"
}
if (config.get("completions", "Tab", "statusstylepretty") === "true") {
if (audible) {
pre += "\uD83D\uDD0A"
}
} else {
pre = preplain
}
this.fuseKeys.push(group)
this.fuseKeys.push(pre)
this.fuseKeys.push(preplain)
this.fuseKeys.push(urls)
this.html = html`<tr class="TabGroupCompletionOption option">
<td class="prefix">${pre}</td>
<td class="prefixplain" hidden>${preplain}</td>
<td class="title">${group}</td>
<td class="tabcount">${tabCount} tab${
tabCount !== 1 ? "s" : ""
}</td>
<td class="tabcount">
${tabCount} tab${tabCount !== 1 ? "s" : ""}
</td>
<td class="content"></td>
</tr>`
const urlMarkup = urls.map(
u => `<a class="url" target="_blank" href="${u}">${u}</a>`,
)
this.html.lastElementChild.innerHTML = urlMarkup.join(",")
}
}
@ -23,7 +67,7 @@ export class TabGroupCompletionSource extends Completions.CompletionSourceFuse {
constructor(private _parent: any) {
super(
["tgroupswitch", "tgroupmove"],
["tgroupswitch", "tgroupmove", "tgroupclose"],
"TabGroupCompletionSource",
"Tab Groups",
)
@ -32,7 +76,11 @@ export class TabGroupCompletionSource extends Completions.CompletionSourceFuse {
this._parent.appendChild(this.node)
}
async filter(exstr: string) {
async onInput(exstr) {
return this.updateOptions(exstr)
}
private async updateOptions(exstr = "") {
this.lastExstr = exstr
const [prefix] = this.splitOnPrefix(exstr)
@ -47,22 +95,28 @@ export class TabGroupCompletionSource extends Completions.CompletionSourceFuse {
return
}
return this.updateDisplay()
}
private async updateOptions() {
const currentGroup = await windowTgroup()
const otherGroups = [...(await tgroups())].filter(
group => group !== currentGroup,
)
const alternateGroup = await windowLastTgroup()
const groups = [...(await tgroups())]
this.options = await Promise.all(
otherGroups.map(async group => {
const tabCount = (await tgroupTabs(group)).length
const o = new TabGroupCompletionOption(group, tabCount)
groups.map(async group => {
const tabs = await tgroupTabs(group)
const audible = tabs.some(t => t.audible)
tabs.sort((a, b) => b.lastAccessed - a.lastAccessed)
const urls = tabs.map(t => t.url)
const o = new TabGroupCompletionOption(
group,
tabs.length,
group === currentGroup,
group === alternateGroup,
audible,
urls,
)
o.state = "normal"
return o
}),
)
return this.updateDisplay()
this.completion = undefined
return this.updateChain()
}
}

View file

@ -170,7 +170,7 @@ import * as Updates from "@src/lib/updates"
import * as Extensions from "@src/lib/extension_info"
import * as webrequests from "@src/background/webrequests"
import * as commandsHelper from "@src/background/commands"
import { tgroups, tgroupActivate, setTabTgroup, setWindowTgroup, setTgroups, windowTgroup, tgroupClearOldInfo, tgroupLastTabId, tgroupTabs, clearAllTgroupInfo, tgroupActivateLast, tgroupHandleTabActivated, tgroupHandleTabCreated, tgroupHandleTabAttached, tgroupHandleTabUpdated, tgroupHandleTabRemoved, tgroupHandleTabDetached } from "./lib/tab_groups"
import { tgroups, tgroupActivate, setTabTgroup, setWindowTgroup, setTgroups, windowTgroup, windowLastTgroup, tgroupClearOldInfo, tgroupLastTabId, tgroupTabs, clearAllTgroupInfo, tgroupActivateLast, tgroupHandleTabActivated, tgroupHandleTabCreated, tgroupHandleTabAttached, tgroupHandleTabUpdated, tgroupHandleTabRemoved, tgroupHandleTabDetached } from "./lib/tab_groups"
ALL_EXCMDS = {
"": BGSELF,
@ -3457,7 +3457,7 @@ export async function tgroupcreate(name: string) {
const promises = []
const groups = await tgroups()
if (groups.has(name)) {
if (groups.has(name) || name === "#") {
throw new Error(`Tab group "${name}" already exists`)
}
@ -3468,7 +3468,7 @@ export async function tgroupcreate(name: string) {
promises.push(tgroupTabs(name, true).then(tabs => browserBg.tabs.hide(tabs.map(tab => tab.id))))
} else {
promises.push(
browser.tabs.query({ currentWindow: true }).then(tabs => {
browser.tabs.query({ currentWindow: true, pinned: false }).then(tabs => {
setTabTgroup(
name,
tabs.map(({ id }) => id),
@ -3488,15 +3488,23 @@ export async function tgroupcreate(name: string) {
/**
* Switch to a different tab group, hiding all other tabs.
*
* "%" denotes the current tab group and "#" denotes the tab group that was
* last active. "A" indates a tab group that contains an audible tab. Use
* `:set completions.Tab.statusstylepretty true` to display a unicode character
* instead.
*
* @param name The name of the tab group to switch to.
*
* If the tab group does not exist, act like tgroupcreate.
* If the tab group does not exist, act like [[tgroupcreate]].
*
*/
//#background
export async function tgroupswitch(name: string) {
if (name === "#") {
return tgrouplast().then(() => name)
}
if (name == (await windowTgroup())) {
throw new Error(`Already on tab group "${name}"`)
return
}
const groups = await tgroups()
@ -3543,22 +3551,39 @@ export async function tgrouprename(name: string) {
}
/**
* Close the current tab group.
* Close all tabs in a tab group and delete the group.
*
* First switch to the previously active tab group. Do nothing if there is only
* one tab group.
* @param name The name of the tab group to close. If not specified, close the
* current tab group and switch to the previously active tab group.
*
* Do nothing if there is only one tab group - to discard all tab group
* information, use [[tgroupabort]].
*
*/
//#background
export async function tgroupclose() {
export async function tgroupclose(name?: string) {
const groups = await tgroups()
if (groups.size == 0) {
throw new Error("No tab groups exist")
} else if (groups.size == 1) {
throw new Error("This is the only tab group")
} else if (name !== undefined && name !== "#" && !groups.has(name)) {
throw new Error(`No tab group named "${name}"`)
} else if (groups.size > 1) {
const closeGroup = await windowTgroup()
const newTabGroup = await tgroupActivateLast()
const currentGroup = await windowTgroup()
let closeGroup = currentGroup
if (name === "#") {
closeGroup = await windowLastTgroup()
if (name === undefined) {
throw new Error("No alternate tab group")
}
} else if (name !== undefined) {
closeGroup = name
}
let newTabGroup = currentGroup
if (closeGroup === currentGroup) {
newTabGroup = await tgroupActivateLast()
}
await tgroupTabs(closeGroup).then(tabs => {
browser.tabs.remove(tabs.map(tab => tab.id))
})
@ -3567,7 +3592,7 @@ export async function tgroupclose() {
}
/**
* Move the current tab to another tab group.
* Move the current tab to another tab group, creating it if it does not exist.
*
* @param name The name of the tab group to move the tab to.
*
@ -3583,16 +3608,25 @@ export async function tgroupmove(name: string) {
if (groups.size == 0) {
throw new Error("No tab groups exist")
}
if (!groups.has(name)) {
throw new Error(`Tab group "${name}" does not exist`)
}
if (name == currentGroup) {
throw new Error(`Tab is already on group "${name}"`)
}
if (name === "#") {
name = await windowLastTgroup()
if (name === undefined) {
throw new Error("No alternate tab group")
}
}
if (!groups.has(name)) {
// Create new tab group if there isn't one with this name
groups.add(name)
await setTgroups(groups)
}
const tabCount = await tgroupTabs(currentGroup).then(tabs => tabs.length)
await setTabTgroup(name)
setContentStateGroup(name)
const currentTabId = await activeTabId()
// switch to other group if this is the last tab in the current group
@ -4054,7 +4088,7 @@ export async function yankimage(url: string): Promise<void> {
A string following the following format: "[0-9]+.[0-9]+" means the first number being the index of the window that should be selected and the second one being the index of the tab within that window. [[taball]] has completions for this format.
"%" denotes the current tab and "#" denotes the tab that was last accessed in this window. "P", "A", "M" and "D" indicate tab status (i.e. a pinned, audible, muted or discarded tab. Use `:set completions.Tab.statusstylepretty true` to display unicode characters instead. "P","A","M","D" can be used to filter by tab status in either setting.
"%" denotes the current tab and "#" denotes the tab that was last accessed in this window. "P", "A", "M" and "D" indicate tab status (i.e. a pinned, audible, muted or discarded tab). Use `:set completions.Tab.statusstylepretty true` to display unicode characters instead. "P","A","M","D" can be used to filter by tab status in either setting.
A non integer string means to search the URL and title for matches, in this window if called from tab, all windows if called from taball. Title matches can contain '*' as a wildcard.
*/

View file

@ -49,10 +49,10 @@ export async function windowTgroup(id?: number) {
if (id === undefined) {
id = await activeWindowId()
}
return (browserBg.sessions.getWindowValue(
return browserBg.sessions.getWindowValue(
id,
"tridactyl-active-tgroup",
) as unknown) as string
) as unknown as string
}
/**
@ -73,6 +73,22 @@ export async function setWindowTgroup(name: string, id?: number) {
)
}
/*
* Return the last active tab group for a window or undefined.
*
* @param id The id of the window. Use the current window if not specified.
*
*/
export async function windowLastTgroup(id?: number) {
const otherGroupsTabs = await tgroupTabs(await windowTgroup(id), true)
if (otherGroupsTabs.length === 0) {
return undefined
}
otherGroupsTabs.sort((a, b) => b.lastAccessed - a.lastAccessed)
const lastTabId = otherGroupsTabs[0].id
return tabTgroup(lastTabId)
}
/**
* Clear the active tab group for the current window.
*
@ -91,10 +107,10 @@ export async function tabTgroup(id?: number) {
if (id === undefined) {
id = await activeTabId()
}
return (browserBg.sessions.getTabValue(
return browserBg.sessions.getTabValue(
id,
"tridactyl-tgroup",
) as unknown) as string
) as unknown as string
}
/**
@ -252,12 +268,8 @@ export async function tgroupActivate(name: string) {
*
*/
export async function tgroupActivateLast() {
const otherGroupsTabs = await tgroupTabs(await windowTgroup(), true)
otherGroupsTabs.sort((a, b) => b.lastAccessed - a.lastAccessed)
const lastTabId = otherGroupsTabs[0].id
return browserBg.tabs
.update(lastTabId, { active: true })
.then(() => tabTgroup(lastTabId))
const lastTabGroup = await windowLastTgroup()
return tgroupActivate(lastTabGroup).then(() => lastTabGroup)
}
/**
@ -325,10 +337,14 @@ export async function tgroupHandleTabActivated(activeInfo) {
await setWindowTgroup(tabGroup, activeInfo.windowId)
promises.push(
tgroupTabs(tabGroup, false, activeInfo.windowId).then(tabs => browserBg.tabs.show(tabs.map(tab => tab.id))),
tgroupTabs(tabGroup, false, activeInfo.windowId).then(tabs =>
browserBg.tabs.show(tabs.map(tab => tab.id)),
),
)
promises.push(
tgroupTabs(tabGroup, true, activeInfo.windowId).then(tabs => browserBg.tabs.hide(tabs.map(tab => tab.id))),
tgroupTabs(tabGroup, true, activeInfo.windowId).then(tabs =>
browserBg.tabs.hide(tabs.map(tab => tab.id)),
),
)
}
return Promise.all(promises)

View file

@ -79,6 +79,17 @@ input:focus {
#completions table tr td.privatewindow {
width: 1.5em;
}
#completions table tr td.tabcount {
width: 6em;
}
#completions table tr td.tgroup {
width: 10em;
padding-left: 0.5em;
text-align: right;
}
#completions table tr td.tgroup:empty {
display: none;
}
/* #completions table tr td:nth-of-type(3) { width: 5em; } */
#completions table tr td.content {
width: 50%;
@ -259,6 +270,14 @@ a.url:hover {
text-overflow: ellipsis;
}
#completions .TabGroupCompletionOption td.title {
width: 15%;
}
#completions .TabGroupCompletionOption td.content {
width: auto;
}
.HelpCompletionOption td.name {
width: 25%;
}