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() { private async fillOptions() {
let tabs: browser.tabs.Tab[] 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") { if (config.get("tabshowhidden") === "true") {
tabs = await browserBg.tabs.query({ tabs = currentWindowTabs
currentWindow: true
})
} else { } else {
tabs = await browserBg.tabs.query({ tabs = await browserBg.tabs.query({
currentWindow: true, currentWindow: true,
hidden: false hidden: false,
}) })
tabs.sort((a, b) => b.lastAccessed - a.lastAccessed)
} }
const options = [] 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" const useMruTabOrder = config.get("tabsort") === "mru"
if (!useMruTabOrder) { if (!useMruTabOrder) {
@ -221,7 +225,7 @@ export class BufferCompletionSource extends Completions.CompletionSourceFuse {
new BufferCompletionOption( new BufferCompletionOption(
(tab.index + 1).toString(), (tab.index + 1).toString(),
tab, tab,
tab === alt, tab.index === altTab.index,
tab_container, tab_container,
), ),
) )

View file

@ -4,6 +4,7 @@ import * as Containers from "@src/lib/containers"
import * as Completions from "@src/completions" import * as Completions from "@src/completions"
import * as Messaging from "@src/lib/messaging" import * as Messaging from "@src/lib/messaging"
import * as config from "@src/lib/config" import * as config from "@src/lib/config"
import { tabTgroup } from "@src/lib/tab_groups"
class TabAllCompletionOption class TabAllCompletionOption
extends Completions.CompletionOptionHTML extends Completions.CompletionOptionHTML
@ -13,15 +14,54 @@ class TabAllCompletionOption
constructor( constructor(
public value: string, public value: string,
tab: browser.tabs.Tab, tab: browser.tabs.Tab,
isAlternative: boolean,
isCurrent: boolean,
winindex: number, winindex: number,
container: browser.contextualIdentities.ContextualIdentity, container: browser.contextualIdentities.ContextualIdentity,
incognito: boolean, incognito: boolean,
tgroupname: string,
) { ) {
super() 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.fuseKeys.push(this.value, tab.title, tab.url)
this.tab = tab 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 // Create HTMLElement
const favIconUrl = tab.favIconUrl const favIconUrl = tab.favIconUrl
? tab.favIconUrl ? tab.favIconUrl
@ -31,14 +71,16 @@ class TabAllCompletionOption
? "incognito" ? "incognito"
: ""}" : ""}"
> >
<td class="prefix"></td> <td class="prefix">${pre}</td>
<td class="prefixplain" hidden>${preplain}</td>
<td class="privatewindow"></td> <td class="privatewindow"></td>
<td class="container"></td> <td class="container"></td>
<td class="icon"><img src="${favIconUrl}" /></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"> <td class="content">
<a class="url" target="_blank" href=${tab.url}>${tab.url}</a> <a class="url" target="_blank" href=${tab.url}>${tab.url}</a>
</td> </td>
<td class="tgroup">${tgroupname}</td>
</tr>` </tr>`
} }
} }
@ -147,6 +189,12 @@ export class TabAllCompletionSource extends Completions.CompletionSourceFuse {
return a.windowId - b.windowId 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 // Check to see if this is a command that needs to exclude the current
// window // window
const excludeCurrentWindow = ["tabgrab"].includes(prefix.trim()) 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 // if we are excluding the current window and this tab is in the current window
// then skip it // then skip it
if (excludeCurrentWindow && tab.windowId === currentWindow.id) continue if (excludeCurrentWindow && tab.windowId === currentWindow.id)
continue
options.push( options.push(
new TabAllCompletionOption( new TabAllCompletionOption(
tab.id.toString(), tab.id.toString(),
tab, tab,
tab.index === altTab.index &&
tab.windowId === altTab.windowId,
tab.active &&
tab.windowId === currentWindowTabs[0].windowId,
winindex, winindex,
await Containers.getFromId(tab.cookieStoreId), await Containers.getFromId(tab.cookieStoreId),
windows[tab.windowId].incognito, windows[tab.windowId].incognito,
await tabTgroup(tab.id),
), ),
) )
} }

View file

@ -1,20 +1,64 @@
import * as Completions from "@src/completions" 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 { implements Completions.CompletionOptionFuse {
public fuseKeys = [] public fuseKeys = []
constructor(group: string, tabCount: number) { constructor(
group: string,
tabCount: number,
current: boolean,
alternate: boolean,
audible: boolean,
urls: string[],
) {
super() super()
this.value = group 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(group)
this.fuseKeys.push(pre)
this.fuseKeys.push(preplain)
this.fuseKeys.push(urls)
this.html = html`<tr class="TabGroupCompletionOption option"> 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="title">${group}</td>
<td class="tabcount">${tabCount} tab${ <td class="tabcount">
tabCount !== 1 ? "s" : "" ${tabCount} tab${tabCount !== 1 ? "s" : ""}
}</td> </td>
<td class="content"></td>
</tr>` </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) { constructor(private _parent: any) {
super( super(
["tgroupswitch", "tgroupmove"], ["tgroupswitch", "tgroupmove", "tgroupclose"],
"TabGroupCompletionSource", "TabGroupCompletionSource",
"Tab Groups", "Tab Groups",
) )
@ -32,7 +76,11 @@ export class TabGroupCompletionSource extends Completions.CompletionSourceFuse {
this._parent.appendChild(this.node) this._parent.appendChild(this.node)
} }
async filter(exstr: string) { async onInput(exstr) {
return this.updateOptions(exstr)
}
private async updateOptions(exstr = "") {
this.lastExstr = exstr this.lastExstr = exstr
const [prefix] = this.splitOnPrefix(exstr) const [prefix] = this.splitOnPrefix(exstr)
@ -47,22 +95,28 @@ export class TabGroupCompletionSource extends Completions.CompletionSourceFuse {
return return
} }
return this.updateDisplay()
}
private async updateOptions() {
const currentGroup = await windowTgroup() const currentGroup = await windowTgroup()
const otherGroups = [...(await tgroups())].filter( const alternateGroup = await windowLastTgroup()
group => group !== currentGroup, const groups = [...(await tgroups())]
)
this.options = await Promise.all( this.options = await Promise.all(
otherGroups.map(async group => { groups.map(async group => {
const tabCount = (await tgroupTabs(group)).length const tabs = await tgroupTabs(group)
const o = new TabGroupCompletionOption(group, tabCount) 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" o.state = "normal"
return o 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 Extensions from "@src/lib/extension_info"
import * as webrequests from "@src/background/webrequests" import * as webrequests from "@src/background/webrequests"
import * as commandsHelper from "@src/background/commands" 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 = { ALL_EXCMDS = {
"": BGSELF, "": BGSELF,
@ -3457,7 +3457,7 @@ export async function tgroupcreate(name: string) {
const promises = [] const promises = []
const groups = await tgroups() const groups = await tgroups()
if (groups.has(name)) { if (groups.has(name) || name === "#") {
throw new Error(`Tab group "${name}" already exists`) 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)))) promises.push(tgroupTabs(name, true).then(tabs => browserBg.tabs.hide(tabs.map(tab => tab.id))))
} else { } else {
promises.push( promises.push(
browser.tabs.query({ currentWindow: true }).then(tabs => { browser.tabs.query({ currentWindow: true, pinned: false }).then(tabs => {
setTabTgroup( setTabTgroup(
name, name,
tabs.map(({ id }) => id), tabs.map(({ id }) => id),
@ -3488,15 +3488,23 @@ export async function tgroupcreate(name: string) {
/** /**
* Switch to a different tab group, hiding all other tabs. * 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. * @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 //#background
export async function tgroupswitch(name: string) { export async function tgroupswitch(name: string) {
if (name === "#") {
return tgrouplast().then(() => name)
}
if (name == (await windowTgroup())) { if (name == (await windowTgroup())) {
throw new Error(`Already on tab group "${name}"`) return
} }
const groups = await tgroups() 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 * @param name The name of the tab group to close. If not specified, close the
* one tab group. * 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 //#background
export async function tgroupclose() { export async function tgroupclose(name?: string) {
const groups = await tgroups() const groups = await tgroups()
if (groups.size == 0) { if (groups.size == 0) {
throw new Error("No tab groups exist") throw new Error("No tab groups exist")
} else if (groups.size == 1) { } else if (groups.size == 1) {
throw new Error("This is the only tab group") 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) { } else if (groups.size > 1) {
const closeGroup = await windowTgroup() const currentGroup = await windowTgroup()
const newTabGroup = await tgroupActivateLast() 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 => { await tgroupTabs(closeGroup).then(tabs => {
browser.tabs.remove(tabs.map(tab => tab.id)) 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. * @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) { if (groups.size == 0) {
throw new Error("No tab groups exist") throw new Error("No tab groups exist")
} }
if (!groups.has(name)) {
throw new Error(`Tab group "${name}" does not exist`)
}
if (name == currentGroup) { if (name == currentGroup) {
throw new Error(`Tab is already on group "${name}"`) 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) const tabCount = await tgroupTabs(currentGroup).then(tabs => tabs.length)
await setTabTgroup(name) await setTabTgroup(name)
setContentStateGroup(name)
const currentTabId = await activeTabId() const currentTabId = await activeTabId()
// switch to other group if this is the last tab in the current group // 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. 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. 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) { if (id === undefined) {
id = await activeWindowId() id = await activeWindowId()
} }
return (browserBg.sessions.getWindowValue( return browserBg.sessions.getWindowValue(
id, id,
"tridactyl-active-tgroup", "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. * Clear the active tab group for the current window.
* *
@ -91,10 +107,10 @@ export async function tabTgroup(id?: number) {
if (id === undefined) { if (id === undefined) {
id = await activeTabId() id = await activeTabId()
} }
return (browserBg.sessions.getTabValue( return browserBg.sessions.getTabValue(
id, id,
"tridactyl-tgroup", "tridactyl-tgroup",
) as unknown) as string ) as unknown as string
} }
/** /**
@ -252,12 +268,8 @@ export async function tgroupActivate(name: string) {
* *
*/ */
export async function tgroupActivateLast() { export async function tgroupActivateLast() {
const otherGroupsTabs = await tgroupTabs(await windowTgroup(), true) const lastTabGroup = await windowLastTgroup()
otherGroupsTabs.sort((a, b) => b.lastAccessed - a.lastAccessed) return tgroupActivate(lastTabGroup).then(() => lastTabGroup)
const lastTabId = otherGroupsTabs[0].id
return browserBg.tabs
.update(lastTabId, { active: true })
.then(() => tabTgroup(lastTabId))
} }
/** /**
@ -325,10 +337,14 @@ export async function tgroupHandleTabActivated(activeInfo) {
await setWindowTgroup(tabGroup, activeInfo.windowId) await setWindowTgroup(tabGroup, activeInfo.windowId)
promises.push( 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( 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) return Promise.all(promises)

View file

@ -79,6 +79,17 @@ input:focus {
#completions table tr td.privatewindow { #completions table tr td.privatewindow {
width: 1.5em; 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:nth-of-type(3) { width: 5em; } */
#completions table tr td.content { #completions table tr td.content {
width: 50%; width: 50%;
@ -259,6 +270,14 @@ a.url:hover {
text-overflow: ellipsis; text-overflow: ellipsis;
} }
#completions .TabGroupCompletionOption td.title {
width: 15%;
}
#completions .TabGroupCompletionOption td.content {
width: auto;
}
.HelpCompletionOption td.name { .HelpCompletionOption td.name {
width: 25%; width: 25%;
} }