mirror of
https://github.com/tridactyl/tridactyl.git
synced 2026-09-10 07:16:33 -04:00
Fix selection fighting on startup with many tabs
Also, modestly unify Tab + TabAll completions
This commit is contained in:
parent
af727c7b8d
commit
9422bd8e5b
|
|
@ -4,7 +4,7 @@ import { enumerate } from "@src/lib/itertools"
|
|||
import * as Containers from "@src/lib/containers"
|
||||
import * as Completions from "@src/completions"
|
||||
import * as config from "@src/lib/config"
|
||||
import * as Messaging from "@src/lib/messaging"
|
||||
import { TabCompletionSource } from "@src/completions/TabBase"
|
||||
|
||||
class BufferCompletionOption
|
||||
extends Completions.CompletionOptionHTML
|
||||
|
|
@ -76,10 +76,9 @@ class BufferCompletionOption
|
|||
}
|
||||
}
|
||||
|
||||
export class BufferCompletionSource extends Completions.CompletionSourceFuse {
|
||||
export class BufferCompletionSource extends TabCompletionSource {
|
||||
public options: BufferCompletionOption[]
|
||||
private shouldSetStateFromScore = true
|
||||
private removeTabChangesListener: () => void
|
||||
private navigationAnchorTabId: number
|
||||
|
||||
// TODO:
|
||||
|
|
@ -108,15 +107,7 @@ export class BufferCompletionSource extends Completions.CompletionSourceFuse {
|
|||
this.sortScoredOptions = true
|
||||
this.updateOptions()
|
||||
this._parent.appendChild(this.node)
|
||||
|
||||
this.removeTabChangesListener = Messaging.addListener(
|
||||
"tab_changes",
|
||||
() => this.reactToTabChanges(),
|
||||
)
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this.removeTabChangesListener()
|
||||
this.listenForTabChanges()
|
||||
}
|
||||
|
||||
async onInput(exstr) {
|
||||
|
|
@ -200,21 +191,25 @@ export class BufferCompletionSource extends Completions.CompletionSourceFuse {
|
|||
return res
|
||||
}
|
||||
|
||||
private async fillOptions(prefix: string) {
|
||||
private async fillOptions(prefix: string, generation: number) {
|
||||
// Get alternative tab, defined as last accessed tab in any group in
|
||||
// this window.
|
||||
|
||||
const altTab = await prevActiveTab()
|
||||
// tabmove uses physical order within the pinned or unpinned group.
|
||||
const forceSort = prefix === "tabmove" ? "default" : undefined
|
||||
let tabs = await getSortedTabs(forceSort)
|
||||
const [altTab, sortedTabs, container_all] = await Promise.all([
|
||||
prevActiveTab(),
|
||||
getSortedTabs(forceSort),
|
||||
browserBg.contextualIdentities.query({}).catch(() => []),
|
||||
])
|
||||
if (!this.isCurrentUpdate(generation)) return
|
||||
let tabs = sortedTabs
|
||||
if (prefix === "tabmove") {
|
||||
const activeTab = tabs.find(tab => tab.active)
|
||||
tabs = tabs.filter(tab => tab.pinned === activeTab.pinned)
|
||||
}
|
||||
const options = []
|
||||
|
||||
const container_all = await browserBg.contextualIdentities.query({})
|
||||
const container_map = new Map()
|
||||
container_all.forEach(elem =>
|
||||
container_map.set(elem.cookieStoreId, elem),
|
||||
|
|
@ -230,20 +225,21 @@ export class BufferCompletionSource extends Completions.CompletionSourceFuse {
|
|||
new BufferCompletionOption(
|
||||
(index + 1).toString(),
|
||||
tab,
|
||||
tab.index === altTab.index,
|
||||
tab.id === altTab?.id,
|
||||
tab_container,
|
||||
index,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
this.options = options
|
||||
return options
|
||||
}
|
||||
|
||||
// Eslint doesn't like this decorator but there's nothing we can do about it
|
||||
// eslint-disable-next-line @typescript-eslint/member-ordering
|
||||
@Perf.measuredAsync
|
||||
private async updateOptions(exstr = "", setInitialPosition = true) {
|
||||
const generation = this.beginUpdate()
|
||||
this.lastExstr = exstr
|
||||
if (setInitialPosition) this.navigationAnchorTabId = undefined
|
||||
let [prefix, query] = this.splitOnPrefix(exstr)
|
||||
|
|
@ -271,7 +267,13 @@ export class BufferCompletionSource extends Completions.CompletionSourceFuse {
|
|||
return
|
||||
}
|
||||
|
||||
await this.fillOptions(prefix)
|
||||
const options = await this.fillOptions(prefix, generation)
|
||||
if (!options || !this.isCurrentUpdate(generation)) return
|
||||
const lastFocused =
|
||||
!setInitialPosition && this.lastFocused?.state === "focused"
|
||||
? (this.lastFocused as BufferCompletionOption)
|
||||
: undefined
|
||||
this.options = options
|
||||
this.completion = undefined
|
||||
|
||||
/* console.log('updateOptions', this.optionContainer) */
|
||||
|
|
@ -292,7 +294,22 @@ export class BufferCompletionSource extends Completions.CompletionSourceFuse {
|
|||
this.options.forEach(option => (option.state = "normal"))
|
||||
}
|
||||
this.updateDisplay()
|
||||
if (!setInitialPosition) return
|
||||
if (!setInitialPosition) {
|
||||
const option =
|
||||
this.options.find(
|
||||
option =>
|
||||
option.tabId === lastFocused?.tabId &&
|
||||
option.state !== "hidden",
|
||||
) ||
|
||||
(lastFocused && this.shouldSetStateFromScore
|
||||
? this.getTheNextTabOption(lastFocused)
|
||||
: undefined)
|
||||
if (option) {
|
||||
this.deselect()
|
||||
this.select(option)
|
||||
}
|
||||
return
|
||||
}
|
||||
if (match || query.trim()) return
|
||||
const initialPosition = config.get(
|
||||
"completions",
|
||||
|
|
@ -310,31 +327,8 @@ export class BufferCompletionSource extends Completions.CompletionSourceFuse {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the list of possible tab options and select (focus on)
|
||||
* the appropriate option.
|
||||
*/
|
||||
private async reactToTabChanges(): Promise<void> {
|
||||
const lastFocused = this.lastFocused as BufferCompletionOption
|
||||
const lastFocusedTabId = lastFocused?.tabId
|
||||
const oldIndex = lastFocused?.tabIndex
|
||||
const wasFocused = lastFocused?.state === "focused"
|
||||
await this.updateOptions(this.lastExstr, false)
|
||||
if (!this.options || this.options.length === 0) return
|
||||
const stillExists = this.options.find(
|
||||
o => o.tabId === lastFocusedTabId && o.state !== "hidden",
|
||||
)
|
||||
if (wasFocused) {
|
||||
this.deselect()
|
||||
const option =
|
||||
stillExists ||
|
||||
(this.shouldSetStateFromScore
|
||||
? this.getTheNextTabOption({ tabIndex: oldIndex } as any)
|
||||
: undefined)
|
||||
if (option) this.select(option)
|
||||
}
|
||||
if (!this.node.isConnected || this.state === "hidden") return
|
||||
await Messaging.messageOwnTab("commandline_content", "show")
|
||||
protected refreshForTabChanges() {
|
||||
return this.updateOptions(this.lastExstr, false)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ import * as Perf from "@src/perf"
|
|||
import { browserBg, getSortedTabs, prevActiveTab } from "@src/lib/webext"
|
||||
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"
|
||||
import { TabCompletionSource } from "@src/completions/TabBase"
|
||||
|
||||
class TabAllCompletionOption
|
||||
extends Completions.CompletionOptionHTML
|
||||
|
|
@ -66,6 +66,7 @@ class TabAllCompletionOption
|
|||
const favIconUrl = tab.favIconUrl
|
||||
? tab.favIconUrl
|
||||
: Completions.DEFAULT_FAVICON
|
||||
const faviconLoading = tab.favIconUrl ? "lazy" : "eager"
|
||||
this.html = html`<tr
|
||||
class="BufferAllCompletionOption option container_${container.color} container_${container.icon} container_${container.name} ${incognito
|
||||
? "incognito"
|
||||
|
|
@ -75,7 +76,9 @@ class TabAllCompletionOption
|
|||
<td class="prefixplain" hidden>${preplain}</td>
|
||||
<td class="privatewindow"></td>
|
||||
<td class="container"></td>
|
||||
<td class="icon"><img src="${favIconUrl}" /></td>
|
||||
<td class="icon">
|
||||
<img loading="${faviconLoading}" src="${favIconUrl}" />
|
||||
</td>
|
||||
<td class="title">${valueStr}: ${tab.title}</td>
|
||||
<td class="content">
|
||||
<a class="url" target="_blank" href=${tab.url}>${Completions.decodeUrlForDisplay(tab.url)}</a>
|
||||
|
|
@ -85,10 +88,9 @@ class TabAllCompletionOption
|
|||
}
|
||||
}
|
||||
|
||||
export class TabAllCompletionSource extends Completions.CompletionSourceFuse {
|
||||
export class TabAllCompletionSource extends TabCompletionSource {
|
||||
public options: TabAllCompletionOption[]
|
||||
private shouldSetStateFromScore = true
|
||||
private removeTabChangesListener: () => void
|
||||
|
||||
constructor(private _parent) {
|
||||
super(["taball", "tabgrab"], "TabAllCompletionSource", "All Tabs")
|
||||
|
|
@ -97,15 +99,7 @@ export class TabAllCompletionSource extends Completions.CompletionSourceFuse {
|
|||
this._parent.appendChild(this.node)
|
||||
this.shouldSetStateFromScore =
|
||||
config.get("completions", "TabAll", "autoselect") === "true"
|
||||
|
||||
this.removeTabChangesListener = Messaging.addListener(
|
||||
"tab_changes",
|
||||
() => this.reactToTabChanges(),
|
||||
)
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this.removeTabChangesListener()
|
||||
this.listenForTabChanges()
|
||||
}
|
||||
|
||||
async onInput(exstr) {
|
||||
|
|
@ -116,43 +110,6 @@ export class TabAllCompletionSource extends Completions.CompletionSourceFuse {
|
|||
super.setStateFromScore(scoredOpts, this.shouldSetStateFromScore)
|
||||
}
|
||||
|
||||
/**
|
||||
* Map all windows into a {[windowId]: window} object
|
||||
*/
|
||||
private async getWindows() {
|
||||
const windows = await browserBg.windows.getAll()
|
||||
const response: { [windowId: number]: browser.windows.Window } = {}
|
||||
windows.forEach(win => (response[win.id] = win))
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the list of possible tab options and select (focus on)
|
||||
* the appropriate option.
|
||||
*/
|
||||
private async reactToTabChanges(): Promise<void> {
|
||||
if (this.state === "hidden") return
|
||||
const lastFocused = this.lastFocused as TabAllCompletionOption
|
||||
const lastFocusedTabId =
|
||||
lastFocused?.state === "focused" ? lastFocused.tabId : undefined
|
||||
const oldIndex = (this.options || [])
|
||||
.filter(o => o.state !== "hidden")
|
||||
.indexOf(lastFocused)
|
||||
await this.updateOptions(this.lastExstr)
|
||||
if (lastFocusedTabId !== undefined) {
|
||||
const visibleOptions = this.options.filter(o => o.state !== "hidden")
|
||||
const option =
|
||||
visibleOptions.find(o => o.tabId === lastFocusedTabId) ||
|
||||
visibleOptions[Math.min(oldIndex, visibleOptions.length - 1)]
|
||||
if (option) {
|
||||
this.deselect()
|
||||
this.select(option)
|
||||
}
|
||||
}
|
||||
if (!this.node.isConnected) return
|
||||
await Messaging.messageOwnTab("commandline_content", "show")
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the next option in this BufferCompletionSource assuming
|
||||
* that this BufferCompletionSource length has been reduced by 1
|
||||
|
|
@ -169,7 +126,8 @@ export class TabAllCompletionSource extends Completions.CompletionSourceFuse {
|
|||
// Eslint doesn't like this decorator but there's nothing we can do about it
|
||||
// eslint-disable-next-line @typescript-eslint/member-ordering
|
||||
@Perf.measuredAsync
|
||||
private async updateOptions(exstr = "") {
|
||||
private async updateOptions(exstr = "", preserveSelection = false) {
|
||||
const generation = this.beginUpdate()
|
||||
this.lastExstr = exstr
|
||||
const [prefix] = this.splitOnPrefix(exstr)
|
||||
|
||||
|
|
@ -185,11 +143,14 @@ export class TabAllCompletionSource extends Completions.CompletionSourceFuse {
|
|||
}
|
||||
|
||||
const mru = config.get("tabsort") == "mru"
|
||||
const tabsPromise = getSortedTabs(mru ? "mru" : "default", true)
|
||||
const windowsPromise = this.getWindows()
|
||||
const [tabs, windows] = await Promise.all([tabsPromise, windowsPromise])
|
||||
|
||||
const options = []
|
||||
const [tabs, altTab, currentWindow, containerList] =
|
||||
await Promise.all([
|
||||
getSortedTabs(mru ? "mru" : "default", true),
|
||||
prevActiveTab(),
|
||||
browserBg.windows.getCurrent(),
|
||||
browserBg.contextualIdentities.query({}).catch(() => []),
|
||||
])
|
||||
if (!this.isCurrentUpdate(generation)) return
|
||||
|
||||
if (!mru) {
|
||||
tabs.sort((a, b) => {
|
||||
|
|
@ -198,40 +159,66 @@ export class TabAllCompletionSource extends Completions.CompletionSourceFuse {
|
|||
})
|
||||
}
|
||||
|
||||
const altTab = await prevActiveTab()
|
||||
|
||||
// Check to see if this is a command that needs to exclude the current
|
||||
// window
|
||||
const excludeCurrentWindow = this.canonicalisePrefix(prefix) === "tabgrab"
|
||||
const currentWindow = await browserBg.windows.getCurrent()
|
||||
const windowIndices = new Map(
|
||||
[...new Set(tabs.map(tab => tab.windowId))]
|
||||
.sort((a, b) => a - b)
|
||||
.map((windowId, index) => [windowId, index + 1]),
|
||||
)
|
||||
for (const tab of tabs) {
|
||||
// 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
|
||||
options.push(
|
||||
const includedTabs = tabs.filter(
|
||||
tab => !excludeCurrentWindow || tab.windowId !== currentWindow.id,
|
||||
)
|
||||
const tabGroups = await Promise.all(
|
||||
includedTabs.map(tab =>
|
||||
tabTgroup(tab.id).catch(() => undefined),
|
||||
),
|
||||
)
|
||||
const containerMap = new Map()
|
||||
containerList.forEach(container =>
|
||||
containerMap.set(container.cookieStoreId, container),
|
||||
)
|
||||
if (!this.isCurrentUpdate(generation)) return
|
||||
const options = includedTabs.map(
|
||||
(tab, index) =>
|
||||
new TabAllCompletionOption(
|
||||
tab.id.toString(),
|
||||
tab,
|
||||
tab.index === altTab.index &&
|
||||
tab.windowId === altTab.windowId,
|
||||
tab.id === altTab?.id,
|
||||
tab.active &&
|
||||
tab.windowId === currentWindow.id,
|
||||
windowIndices.get(tab.windowId),
|
||||
await Containers.getFromId(tab.cookieStoreId),
|
||||
windows[tab.windowId].incognito,
|
||||
await tabTgroup(tab.id),
|
||||
containerMap.get(tab.cookieStoreId) ||
|
||||
Containers.DefaultContainer,
|
||||
tab.incognito,
|
||||
tabGroups[index],
|
||||
),
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
const lastFocused = this.lastFocused as TabAllCompletionOption
|
||||
const wasFocused = preserveSelection && lastFocused?.state === "focused"
|
||||
const oldIndex = wasFocused
|
||||
? (this.options || [])
|
||||
.filter(o => o.state !== "hidden")
|
||||
.indexOf(lastFocused)
|
||||
: -1
|
||||
this.completion = undefined
|
||||
this.options = options
|
||||
return this.updateChain()
|
||||
this.updateChain()
|
||||
if (wasFocused) {
|
||||
const visibleOptions = this.options.filter(o => o.state !== "hidden")
|
||||
const option =
|
||||
visibleOptions.find(o => o.tabId === lastFocused.tabId) ||
|
||||
visibleOptions[Math.min(oldIndex, visibleOptions.length - 1)]
|
||||
if (option) {
|
||||
this.deselect()
|
||||
this.select(option)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected refreshForTabChanges() {
|
||||
return this.updateOptions(this.lastExstr, true)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
51
src/completions/TabBase.ts
Normal file
51
src/completions/TabBase.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import * as Completions from "@src/completions"
|
||||
import * as Messaging from "@src/lib/messaging"
|
||||
|
||||
export abstract class TabCompletionSource extends Completions.CompletionSourceFuse {
|
||||
private removeTabChangesListener: () => void
|
||||
private handlingTabChanges = false
|
||||
private tabChangesQueued = false
|
||||
private updateGeneration = 0
|
||||
|
||||
public destroy() {
|
||||
this.updateGeneration++
|
||||
this.tabChangesQueued = false
|
||||
this.removeTabChangesListener()
|
||||
}
|
||||
|
||||
protected listenForTabChanges() {
|
||||
this.removeTabChangesListener = Messaging.addListener(
|
||||
"tab_changes",
|
||||
() => this.reactToTabChanges(),
|
||||
)
|
||||
}
|
||||
|
||||
protected beginUpdate() {
|
||||
return ++this.updateGeneration
|
||||
}
|
||||
|
||||
protected isCurrentUpdate(generation: number) {
|
||||
return generation === this.updateGeneration
|
||||
}
|
||||
|
||||
protected abstract refreshForTabChanges(): Promise<void>
|
||||
|
||||
private async reactToTabChanges(): Promise<void> {
|
||||
if (this.state === "hidden") return
|
||||
this.tabChangesQueued = true
|
||||
if (this.handlingTabChanges) return
|
||||
this.handlingTabChanges = true
|
||||
try {
|
||||
do {
|
||||
this.tabChangesQueued = false
|
||||
await this.refreshForTabChanges()
|
||||
} while (this.tabChangesQueued)
|
||||
} finally {
|
||||
this.handlingTabChanges = false
|
||||
if (this.tabChangesQueued) void this.reactToTabChanges()
|
||||
}
|
||||
if (!this.node.isConnected || this.node.classList.contains("hidden"))
|
||||
return
|
||||
await Messaging.messageOwnTab("commandline_content", "show")
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue