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(() => { browser.runtime.onStartup.addListener(() => {
config.getAsync("autocmds", "TriStart").then(aucmds => { config.getAsync("autocmds", "TriStart").then(aucmds => {
const hosts = Object.keys(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 there's only one rule and it's "all", no need to check the hostname
if (hosts.length === 1 && hosts[0] === ".*") { if (hosts.length === 1 && hosts[0] === ".*") {
autocmd_logger.debug( run(hosts[0])
`TriStart matched ${hosts[0]}: ${aucmds[hosts[0]]}`,
)
controller.acceptExCmd(aucmds[hosts[0]])
} else { } else {
native.run("hostname").then(hostname => { native.run("hostname").then(hostname => {
for (const host of hosts) { for (const host of hosts) {
if (new RegExp(host).exec(hostname.content)) { if (new RegExp(host).exec(hostname.content)) {
autocmd_logger.debug( run(host)
`TriStart matched ${host}: ${aucmds[host]}`,
)
controller.acceptExCmd(aucmds[host])
} }
} }
}) })
@ -189,7 +187,9 @@ for (const requestEvent of webrequests.requestEvents) {
config.getAsync("autocmds", requestEvent).then(aucmds => { config.getAsync("autocmds", requestEvent).then(aucmds => {
if (!aucmds) return if (!aucmds) return
const patterns = Object.keys(aucmds) 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( webrequests.registerWebRequestAutocmd(
requestEvent, requestEvent,
pattern, pattern,

View file

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

View file

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

View file

@ -2471,14 +2471,16 @@ export async function loadaucmds(cmdType: "DocStart" | "DocLoad" | "DocEnd" | "T
TRI_FIRED_URL: owntab.url, TRI_FIRED_URL: owntab.url,
} }
for (const aukey of aukeyarr) { for (const aukey of aukeyarr) {
for (const [k, v] of Object.entries(replacements)) { for (let aucmd of [aucmds[aukey]].flat()) {
aucmds[aukey] = aucmds[aukey].replace(k, v) for (const [k, v] of Object.entries(replacements)) {
} aucmd = aucmd.replace(k, v)
try { }
autocmd_logger.debug(`${cmdType} matched ${aukey}: ${aucmds[aukey]}`) try {
await controller.acceptExCmd(aucmds[aukey]) autocmd_logger.debug(`${cmdType} matched ${aukey}: ${aucmd}`)
} catch (e) { await controller.acceptExCmd(aucmd)
autocmd_logger.error((e as Error).toString()) } 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. * - `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. * 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 //#background
@ -4864,10 +4867,10 @@ export async function autocmd(event: string, url: string, ...excmd: string[]) {
if (!getAutocmdEvents().includes(event)) { if (!getAutocmdEvents().includes(event)) {
throw new Error(event + " is not a supported event.") throw new Error(event + " is not a supported event.")
} }
if (webrequests.requestEvents.includes(event)) { const commands = [...new Set([config.USERCONFIG.autocmds?.[event]?.[url] ?? [], excmd.join(" ")].flat())]
await webrequests.registerWebRequestAutocmd(event, url, excmd.join(" ")) if (webrequests.requestEvents.includes(event))
} webrequests.registerWebRequestAutocmd(event, url, commands)
return config.set("autocmds", event, url, excmd.join(" ")) return config.set("autocmds", event, url, commands)
} }
/** /**
@ -4956,14 +4959,25 @@ export function proxyremove(name: string) {
@param event An event from [[autocmd]] @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 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 //#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 (!getAutocmdEvents().includes(event)) throw new Error(`${event} is not a supported event.`)
if (webrequests.requestEvents.includes(event)) { const command = excmd.join(" ")
webrequests.unregisterWebRequestAutocmd(event, url) 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") { } else if (i === "autocmds") {
for (const a of Object.keys(pconf[i][e])) { for (const a of Object.keys(pconf[i][e])) {
const value = pconf[i][e][a] for (const command of [pconf[i][e][a]].flat())
parseobj.aucmds.push( parseobj.aucmds.push(command === null ? `autocmddelete ${e} ${a}` : `autocmd ${e} ${a} ${command}`)
value === null
? `autocmddelete ${e} ${a}`
: `autocmd ${e} ${a} ${value}`,
)
} }
} else if (i === "autocontain") { } else if (i === "autocontain") {
parseobj.aucons.push(`autocontain ${e} ${pconf[i][e]}`) parseobj.aucons.push(`autocontain ${e} ${pconf[i][e]}`)