Fix #650: allow multiple autocmds per event/match

This commit is contained in:
Oliver Blanthorn 2026-07-22 18:13:37 +02:00
parent 0e640b05aa
commit d3629bc8c8
No known key found for this signature in database
GPG key ID: 2BB8C36BB504BFF3
5 changed files with 50 additions and 39 deletions

View file

@ -132,20 +132,18 @@ const autocmd_logger = new Logging.Logger("autocmds")
browser.runtime.onStartup.addListener(() => {
config.getAsync("autocmds", "TriStart").then(aucmds => {
const hosts = Object.keys(aucmds)
const run = async host => {
autocmd_logger.debug(`TriStart matched ${host}: ${aucmds[host]}`)
for (const command of [aucmds[host]].flat()) await controller.acceptExCmd(command)
}
// If there's only one rule and it's "all", no need to check the hostname
if (hosts.length === 1 && hosts[0] === ".*") {
autocmd_logger.debug(
`TriStart matched ${hosts[0]}: ${aucmds[hosts[0]]}`,
)
controller.acceptExCmd(aucmds[hosts[0]])
run(hosts[0])
} else {
native.run("hostname").then(hostname => {
for (const host of hosts) {
if (new RegExp(host).exec(hostname.content)) {
autocmd_logger.debug(
`TriStart matched ${host}: ${aucmds[host]}`,
)
controller.acceptExCmd(aucmds[host])
run(host)
}
}
})
@ -189,7 +187,9 @@ for (const requestEvent of webrequests.requestEvents) {
config.getAsync("autocmds", requestEvent).then(aucmds => {
if (!aucmds) return
const patterns = Object.keys(aucmds)
patterns.forEach(pattern =>
// Async isolates invalid persisted patterns from the rest.
// eslint-disable-next-line @typescript-eslint/require-await
patterns.forEach(async pattern =>
webrequests.registerWebRequestAutocmd(
requestEvent,
pattern,

View file

@ -15,18 +15,19 @@ export const requestEvents = Object.keys(requestEventExpraInfoSpecMap)
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
export const LISTENERS: Record<string, Record<string, Function>> = {}
export const registerWebRequestAutocmd = async (
export const registerWebRequestAutocmd = (
requestEvent: string,
pattern: string,
func: string,
func: string | string[],
) => {
// I'm being lazy - strictly the functions map strings to void | blocking responses
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
const listener = eval(func) as Function
const listeners = [func].flat().map(source => (eval(source) as Function).bind(undefined))
const listener = details => listeners.reduce((response, listener) => listener(details) ?? response, undefined)
if (!LISTENERS[requestEvent]) LISTENERS[requestEvent] = {}
await browser.webRequest["on" + requestEvent].addListener(
browser.webRequest["on" + requestEvent].addListener(
listener,
{ urls: [pattern] },
requestEventExpraInfoSpecMap[requestEvent],
@ -37,15 +38,15 @@ export const registerWebRequestAutocmd = async (
// Remove any previously registered autocmd for the same pattern
if (oldListener) {
await browser.webRequest["on" + requestEvent].removeListener(
browser.webRequest["on" + requestEvent].removeListener(
oldListener
)
}
}
export const unregisterWebRequestAutocmd = async (requestEvent, pattern) => {
export const unregisterWebRequestAutocmd = (requestEvent, pattern) => {
if (LISTENERS[requestEvent] && LISTENERS[requestEvent][pattern]) {
await browser.webRequest["on" + requestEvent].removeListener(
browser.webRequest["on" + requestEvent].removeListener(
LISTENERS[requestEvent][pattern],
)
}

View file

@ -128,7 +128,7 @@ export class AutocmdCompletionSource extends Completions.CompletionSourceFuse {
// for `autocmd` itself.
let description = ""
if (is_autocmddelete) {
description = command
description = [command].flat().join(" | ")
}
const opt = new AutocmdCompletionOption(
pattern,

View file

@ -2471,14 +2471,16 @@ export async function loadaucmds(cmdType: "DocStart" | "DocLoad" | "DocEnd" | "T
TRI_FIRED_URL: owntab.url,
}
for (const aukey of aukeyarr) {
for (const [k, v] of Object.entries(replacements)) {
aucmds[aukey] = aucmds[aukey].replace(k, v)
}
try {
autocmd_logger.debug(`${cmdType} matched ${aukey}: ${aucmds[aukey]}`)
await controller.acceptExCmd(aucmds[aukey])
} catch (e) {
autocmd_logger.error((e as Error).toString())
for (let aucmd of [aucmds[aukey]].flat()) {
for (const [k, v] of Object.entries(replacements)) {
aucmd = aucmd.replace(k, v)
}
try {
autocmd_logger.debug(`${cmdType} matched ${aukey}: ${aucmd}`)
await controller.acceptExCmd(aucmd)
} catch (e) {
autocmd_logger.error((e as Error).toString())
}
}
}
}
@ -4856,6 +4858,7 @@ export function getAutocmdEvents() {
* - `TRI_FIRED_URL`: The URL of the document that the tab is displaying.
* For debugging, use `:set logging.autocmds debug` and check the Firefox web console. `WebRequest` events have no logging.
* Running `:autocmd [event] [match] [commmand]` repeatedly will add multiple listeners for the same event, provided `command` is distinct. To remove listeners, see [[autocmddelete]].
*
*/
//#background
@ -4864,10 +4867,10 @@ export async function autocmd(event: string, url: string, ...excmd: string[]) {
if (!getAutocmdEvents().includes(event)) {
throw new Error(event + " is not a supported event.")
}
if (webrequests.requestEvents.includes(event)) {
await webrequests.registerWebRequestAutocmd(event, url, excmd.join(" "))
}
return config.set("autocmds", event, url, excmd.join(" "))
const commands = [...new Set([config.USERCONFIG.autocmds?.[event]?.[url] ?? [], excmd.join(" ")].flat())]
if (webrequests.requestEvents.includes(event))
webrequests.registerWebRequestAutocmd(event, url, commands)
return config.set("autocmds", event, url, commands)
}
/**
@ -4956,14 +4959,25 @@ export function proxyremove(name: string) {
@param event An event from [[autocmd]]
@param url Exactly the "url" you entered when you made the [[autocmd]] you wish to delete. See `:viewconfig autocmds` if you have forgotten.
@param excmd The exact command to delete. Omit it to delete every command for the event and URL.
*/
//#background
export function autocmddelete(event: string, url: string) {
export async function autocmddelete(event: string, url: string, ...excmd: string[]) {
if (!getAutocmdEvents().includes(event)) throw new Error(`${event} is not a supported event.`)
if (webrequests.requestEvents.includes(event)) {
webrequests.unregisterWebRequestAutocmd(event, url)
const command = excmd.join(" ")
let commands
if (command) {
const stored = config.USERCONFIG.autocmds?.[event]?.[url]
commands = [stored === undefined ? config.DEFAULTS.autocmds?.[event]?.[url] ?? [] : stored ?? []].flat()
if (!commands.includes(command)) return
commands = commands.filter(candidate => candidate !== command)
}
return config.set("autocmds", event, url, null)
if (webrequests.requestEvents.includes(event)) {
if (commands?.length)
webrequests.registerWebRequestAutocmd(event, url, commands)
else webrequests.unregisterWebRequestAutocmd(event, url)
}
return config.set("autocmds", event, url, commands?.length ? commands : null)
}
/**

View file

@ -2659,12 +2659,8 @@ const parseConfigHelper = (pconf, parseobj, prefix = []) => {
}
} else if (i === "autocmds") {
for (const a of Object.keys(pconf[i][e])) {
const value = pconf[i][e][a]
parseobj.aucmds.push(
value === null
? `autocmddelete ${e} ${a}`
: `autocmd ${e} ${a} ${value}`,
)
for (const command of [pconf[i][e][a]].flat())
parseobj.aucmds.push(command === null ? `autocmddelete ${e} ${a}` : `autocmd ${e} ${a} ${command}`)
}
} else if (i === "autocontain") {
parseobj.aucons.push(`autocontain ${e} ${pconf[i][e]}`)