From 8e2e5a8aa50f915a7f71f834e75458effc4d7107 Mon Sep 17 00:00:00 2001 From: Suvesh Moza Date: Sun, 21 Dec 2025 14:49:57 +0530 Subject: [PATCH 1/4] feat(autocmd): add TabEnter autocmd --- src/glide/browser/base/content/browser.mts | 78 ++++++++- src/glide/browser/base/content/glide.d.ts | 15 ++ .../content/test/autocmds/browser_autocmds.ts | 163 ++++++++++++++++++ 3 files changed, 255 insertions(+), 1 deletion(-) diff --git a/src/glide/browser/base/content/browser.mts b/src/glide/browser/base/content/browser.mts index 0b012550..0377bf3b 100644 --- a/src/glide/browser/base/content/browser.mts +++ b/src/glide/browser/base/content/browser.mts @@ -869,6 +869,7 @@ class GlideBrowserClass { "nsISupportsWeakReference", ]), + $last_selected_tab: null as BrowserTab | null, $last_location: null as string | null, /** @@ -914,6 +915,25 @@ class GlideBrowserClass { GlideBrowser._log.debug("onLocationChange - clearing buffer"); await GlideBrowser.clear_buffer(); + const current_tab = gBrowser.selectedTab; + + if (current_tab) { + const is_tab_switch = this.$last_selected_tab !== null && this.$last_selected_tab !== current_tab; + + GlideBrowser._log.debug("TabEnter check:", { + last_tab: this.$last_selected_tab, + current_tab, + is_tab_switch, + }); + + if (is_tab_switch) { + GlideBrowser._log.debug("TabEnter: firing for", location.spec); + await GlideBrowser.#invoke_tabenter_autocmd(location); + } + + this.$last_selected_tab = current_tab; + } + await GlideBrowser.#invoke_urlenter_autocmd(location); }, }); @@ -1005,8 +1025,64 @@ class GlideBrowserClass { } } + async #invoke_tabenter_autocmd(location: nsIURI) { + const cmds = GlideBrowser.autocmds.TabEnter ?? []; + GlideBrowser._log.debug("TabEnter autocmds registered:", cmds.length); + + if (!cmds.length) { + GlideBrowser._log.debug("TabEnter: no autocmds registered, skipping"); + return; + } + + if (!GlideBrowser.extension?.tabManager) { + GlideBrowser._log.debug("TabEnter autocmd skipped: extension not ready yet"); + return; + } + + const args: glide.AutocmdArgs["TabEnter"] = { + url: location.spec, + get tab_id() { + return assert_present( + GlideBrowser.extension.tabManager.getWrapper(gBrowser.selectedTab), + "could not resolve tab wrapper", + ).id; + }, + }; + + const results = await Promise.allSettled(cmds.map((cmd) => + (async () => { + if (!GlideBrowser.#test_url_autocmd_pattern(cmd.pattern, location)) { + return; + } + + const cleanup = await cmd.callback(args); + if (typeof cleanup === "function") { + GlideBrowser.buffer_cleanups.push({ callback: cleanup, source: "TabEnter cleanup" }); + } + })() + )); + + for (const result of results) { + if (result.status === "fulfilled") { + continue; + } + + GlideBrowser._log.error(result.reason); + + // TODO: if there are many errors this would be overwhelming... + // maybe limit the number of errors we display at once? + + const loc = GlideBrowser.#clean_stack(result.reason, "#invoke_tabenter_autocmd") ?? ""; + GlideBrowser.add_notification("glide-autocmd-error", { + label: `Error occurred in TabEnter autocmd \`${loc}\` - ${result.reason}`, + priority: MozElements.NotificationBox.prototype.PRIORITY_CRITICAL_HIGH, + buttons: [GlideBrowser.remove_all_notifications_button], + }); + } + } + #test_url_autocmd_pattern( - pattern: glide.AutocmdPatterns["UrlEnter"], + pattern: glide.AutocmdPatterns["UrlEnter"] | glide.AutocmdPatterns["TabEnter"], location: nsIURI, ): boolean { if ("test" in pattern) { diff --git a/src/glide/browser/base/content/glide.d.ts b/src/glide/browser/base/content/glide.d.ts index 5106deb5..0f949e4a 100644 --- a/src/glide/browser/base/content/glide.d.ts +++ b/src/glide/browser/base/content/glide.d.ts @@ -139,6 +139,18 @@ declare global { callback: (args: glide.AutocmdArgs[Event]) => void, ): void; + /** + * Create an autocmd that will be invoked whenever the focused tab changes. + * + * This includes: + * 1. Switching tabs + */ + create( + event: Event, + pattern: glide.AutocmdPatterns[Event], + callback: (args: glide.AutocmdArgs[Event]) => void, + ): void; + /** * Create an autocmd that will be invoked whenever the mode changes. * @@ -2209,6 +2221,7 @@ declare global { type AutocmdEvent = | "UrlEnter" + | "TabEnter" | "ModeChanged" | "ConfigLoaded" | "WindowLoaded" @@ -2216,6 +2229,7 @@ declare global { | "KeyStateChanged"; type AutocmdPatterns = { UrlEnter: RegExp | { hostname?: string }; + TabEnter: RegExp | { hostname?: string }; ModeChanged: "*" | `${GlideMode | "*"}:${GlideMode | "*"}`; ConfigLoaded: null; WindowLoaded: null; @@ -2224,6 +2238,7 @@ declare global { }; type AutocmdArgs = { UrlEnter: { readonly url: string; readonly tab_id: number }; + TabEnter: { readonly url: string; readonly tab_id: number }; ModeChanged: { /** * This may be `null` when first loading Glide or when reloading the config. diff --git a/src/glide/browser/base/content/test/autocmds/browser_autocmds.ts b/src/glide/browser/base/content/test/autocmds/browser_autocmds.ts index 272a0d73..35b2a966 100644 --- a/src/glide/browser/base/content/test/autocmds/browser_autocmds.ts +++ b/src/glide/browser/base/content/test/autocmds/browser_autocmds.ts @@ -524,6 +524,169 @@ add_task(async function test_autocmd_remove() { ); }); +add_task(async function test_tabenter_only_fires_on_tab_switch() { + await GlideTestUtils.reload_config(function _() { + glide.g.calls = []; + + glide.autocmds.create("TabEnter", /input_test/, () => { + glide.g.calls!.push("tabenter"); + }); + + glide.autocmds.create("UrlEnter", /input_test/, () => { + glide.g.calls!.push("urlenter"); + }); + }); + + await BrowserTestUtils.withNewTab(INPUT_TEST_URI, async _ => { + await waiter(() => glide.g.calls).isjson(["urlenter"]); + is(num_calls(), 1, "Initial navigation should only trigger UrlEnter, not TabEnter"); + + const tab1 = gBrowser.selectedTab; + + BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, INPUT_TEST_URI + "?navigate"); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + await waiter(() => glide.g.calls).isjson(["urlenter", "urlenter"]); + is(num_calls(), 2, "Same-tab navigation should only trigger UrlEnter"); + + await BrowserTestUtils.withNewTab("about:mozilla", async _ => { + await sleep_frames(5); + is(num_calls(), 2, "Opening non-matching page should not trigger any autocmd"); + + await BrowserTestUtils.switchTab(gBrowser, tab1); + await waiter(() => glide.g.calls).isjson(["urlenter", "urlenter", "tabenter", "urlenter"]); + is(num_calls(), 4, "Switching tabs should trigger both TabEnter and UrlEnter"); + }); + }); +}); + +add_task(async function test_tabenter_regexp_filter() { + await GlideTestUtils.reload_config(function _() { + glide.g.calls = []; + + glide.autocmds.create("TabEnter", /input_test\.html/, () => { + glide.g.calls!.push("expected-call"); + }); + + glide.autocmds.create("TabEnter", /definitely-wont-match/, () => { + glide.g.calls!.push("bad-call"); + }); + }); + + await BrowserTestUtils.withNewTab(INPUT_TEST_URI, async _ => { + const tab1 = gBrowser.selectedTab; + + await BrowserTestUtils.withNewTab("about:blank", async _ => { + await BrowserTestUtils.switchTab(gBrowser, tab1); + await waiter(() => glide.g.calls).isjson( + ["expected-call"], + "TabEnter autocmd should be triggered only for matching URL pattern", + ); + }); + }); +}); + +add_task(async function test_tabenter_host_filter() { + await GlideTestUtils.reload_config(function _() { + glide.g.calls = []; + + glide.autocmds.create("TabEnter", { hostname: "mochi.test" }, () => { + glide.g.calls!.push("expected-call"); + }); + + glide.autocmds.create("TabEnter", { hostname: "definitely-wont-match" }, () => { + glide.g.calls!.push("bad-call"); + }); + }); + + await BrowserTestUtils.withNewTab(INPUT_TEST_URI, async _ => { + const tab1 = gBrowser.selectedTab; + + await BrowserTestUtils.withNewTab("about:blank", async _ => { + await BrowserTestUtils.switchTab(gBrowser, tab1); + await waiter(() => glide.g.calls).isjson( + ["expected-call"], + "TabEnter autocmd should be triggered only for matching hostname", + ); + }); + }); +}); + +add_task(async function test_tabenter_tab_id() { + await BrowserTestUtils.withNewTab(INPUT_TEST_URI, async _ => { + const tab1 = gBrowser.selectedTab; + + await GlideTestUtils.reload_config(function _() { + glide.autocmds.create("TabEnter", /input_test/, ({ tab_id }) => { + glide.g.value = tab_id; + }); + }); + + await BrowserTestUtils.withNewTab("about:blank", async _ => { + await BrowserTestUtils.switchTab(gBrowser, tab1); + await until(() => glide.g.value !== undefined); + + is( + glide.g.value, + (await glide.tabs.active()).id, + "TabEnter autocmd should be passed a tab ID that matches the active tab ID", + ); + }); + }); +}); + +add_task(async function test_tabenter_error_handling() { + await GlideTestUtils.reload_config(function _() { + glide.autocmds.create("TabEnter", /input_test/, () => { + throw new Error("tabenter failed"); + }); + }); + + await BrowserTestUtils.withNewTab(INPUT_TEST_URI, async _ => { + const tab1 = gBrowser.selectedTab; + + await BrowserTestUtils.withNewTab("about:blank", async _ => { + await BrowserTestUtils.switchTab(gBrowser, tab1); + + let notification = await until(() => gNotificationBox.getNotificationWithValue("glide-autocmd-error")); + + ok(notification, "Error notification should be shown"); + ok( + notification.shadowRoot + .querySelector(".message") + ?.textContent?.includes("Error occurred in TabEnter autocmd"), + "Notification should indicate TabEnter autocmd error", + ); + gNotificationBox.removeNotification(notification); + }); + }); +}); + +add_task(async function test_tabenter_multiple_callbacks() { + await GlideTestUtils.reload_config(function _() { + glide.g.calls = []; + + glide.autocmds.create("TabEnter", /input_test/, () => { + glide.g.calls!.push("first"); + }); + + glide.autocmds.create("TabEnter", /input_test/, () => { + glide.g.calls!.push("second"); + }); + }); + + await BrowserTestUtils.withNewTab(INPUT_TEST_URI, async _ => { + const tab1 = gBrowser.selectedTab; + + await BrowserTestUtils.withNewTab("about:blank", async _ => { + await BrowserTestUtils.switchTab(gBrowser, tab1); + await waiter(() => glide.g.calls).isjson( + ["first", "second"], + "Multiple TabEnter autocmds should fire in registration order", + ); + }); + }); +}); + function num_calls() { return (glide.g.calls ?? []).length; } From 6a589f96612e37e41f08d56b4f907f5f6da9e18a Mon Sep 17 00:00:00 2001 From: Suvesh Moza Date: Wed, 24 Dec 2025 01:05:12 +0530 Subject: [PATCH 2/4] add docs for TabEnter autocmd --- src/glide/docs/autocmds.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/glide/docs/autocmds.md b/src/glide/docs/autocmds.md index b998f1bd..6d58f899 100644 --- a/src/glide/docs/autocmds.md +++ b/src/glide/docs/autocmds.md @@ -119,6 +119,33 @@ Callback arguments: } ``` +## TabEnter + +Fired when the focused tab changes, which can happen under the following circumstances: + +- switching tabs + +The `pattern` is either a `RegExp` that matches against the entire URL, or an object with `{ hostname: string }`. + +The callback can also `return` a function that will be called when _another_ `TabEnter` event would be fired. + +```typescript +glide.autocmds.create("TabEnter", { + hostname: "example.com", +}, () => { + // +}); +``` + +Callback arguments: + +```typescript {% highlight_prefix="interface x " %} +{ + tab_id: number; + url: string; +} +``` + ## ModeChanged Fired when the mode changes. From b25cbbd74f8957d69598c51618349fd8ff49ed46 Mon Sep 17 00:00:00 2001 From: Robert Craigie Date: Fri, 13 Feb 2026 21:18:30 +0000 Subject: [PATCH 3/4] docs cleanup --- src/glide/browser/base/content/glide.d.ts | 3 --- src/glide/docs/autocmds.md | 8 +++----- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/glide/browser/base/content/glide.d.ts b/src/glide/browser/base/content/glide.d.ts index 0f949e4a..4a88b125 100644 --- a/src/glide/browser/base/content/glide.d.ts +++ b/src/glide/browser/base/content/glide.d.ts @@ -141,9 +141,6 @@ declare global { /** * Create an autocmd that will be invoked whenever the focused tab changes. - * - * This includes: - * 1. Switching tabs */ create( event: Event, diff --git a/src/glide/docs/autocmds.md b/src/glide/docs/autocmds.md index 6d58f899..77db727e 100644 --- a/src/glide/docs/autocmds.md +++ b/src/glide/docs/autocmds.md @@ -100,7 +100,7 @@ Fired when the focused URL changes, which can happen under the following circums The `pattern` is either a `RegExp` that matches against the entire URL, or an object with `{ hostname: string }`. -The callback can also `return` a function that will be called when _another_ `UrlEnter` event would be fired. +The callback can also `return` a function that will be called when _another_ `UrlEnter` event would be fired so that you can run any necessary cleanup. ```typescript glide.autocmds.create("UrlEnter", { @@ -121,13 +121,11 @@ Callback arguments: ## TabEnter -Fired when the focused tab changes, which can happen under the following circumstances: - -- switching tabs +Fired when the focused tab changes. The `pattern` is either a `RegExp` that matches against the entire URL, or an object with `{ hostname: string }`. -The callback can also `return` a function that will be called when _another_ `TabEnter` event would be fired. +The callback can also `return` a function that will be called when _another_ `TabEnter` event would be fired so that you can run any necessary cleanup. ```typescript glide.autocmds.create("TabEnter", { From 6c6435c3953efc10f5e7fbb8ac59d0bad187bdbf Mon Sep 17 00:00:00 2001 From: Suvesh Moza Date: Fri, 27 Feb 2026 17:15:32 +0530 Subject: [PATCH 4/4] apply suggestions from review --- src/glide/browser/base/content/browser.mts | 6 +++++- .../base/content/test/autocmds/browser_autocmds.ts | 14 +++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/glide/browser/base/content/browser.mts b/src/glide/browser/base/content/browser.mts index 0377bf3b..8f4deea0 100644 --- a/src/glide/browser/base/content/browser.mts +++ b/src/glide/browser/base/content/browser.mts @@ -912,13 +912,17 @@ class GlideBrowserClass { return; // ignore iframes etc. } + if (location.schemeIs("about") && location.spec === "about:blank") { + return; + } + GlideBrowser._log.debug("onLocationChange - clearing buffer"); await GlideBrowser.clear_buffer(); const current_tab = gBrowser.selectedTab; if (current_tab) { - const is_tab_switch = this.$last_selected_tab !== null && this.$last_selected_tab !== current_tab; + const is_tab_switch = this.$last_selected_tab == null || this.$last_selected_tab !== current_tab; GlideBrowser._log.debug("TabEnter check:", { last_tab: this.$last_selected_tab, diff --git a/src/glide/browser/base/content/test/autocmds/browser_autocmds.ts b/src/glide/browser/base/content/test/autocmds/browser_autocmds.ts index 35b2a966..e6f3d546 100644 --- a/src/glide/browser/base/content/test/autocmds/browser_autocmds.ts +++ b/src/glide/browser/base/content/test/autocmds/browser_autocmds.ts @@ -538,23 +538,23 @@ add_task(async function test_tabenter_only_fires_on_tab_switch() { }); await BrowserTestUtils.withNewTab(INPUT_TEST_URI, async _ => { - await waiter(() => glide.g.calls).isjson(["urlenter"]); - is(num_calls(), 1, "Initial navigation should only trigger UrlEnter, not TabEnter"); + await waiter(() => glide.g.calls).isjson(["tabenter", "urlenter"]); + is(num_calls(), 2, "Initial navigation should trigger both TabEnter and UrlEnter"); const tab1 = gBrowser.selectedTab; BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, INPUT_TEST_URI + "?navigate"); await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); - await waiter(() => glide.g.calls).isjson(["urlenter", "urlenter"]); - is(num_calls(), 2, "Same-tab navigation should only trigger UrlEnter"); + await waiter(() => glide.g.calls).isjson(["tabenter", "urlenter", "urlenter"]); + is(num_calls(), 3, "Same-tab navigation should only trigger UrlEnter"); await BrowserTestUtils.withNewTab("about:mozilla", async _ => { await sleep_frames(5); - is(num_calls(), 2, "Opening non-matching page should not trigger any autocmd"); + is(num_calls(), 3, "Opening non-matching page should not trigger any autocmd"); await BrowserTestUtils.switchTab(gBrowser, tab1); - await waiter(() => glide.g.calls).isjson(["urlenter", "urlenter", "tabenter", "urlenter"]); - is(num_calls(), 4, "Switching tabs should trigger both TabEnter and UrlEnter"); + await waiter(() => glide.g.calls).isjson(["tabenter", "urlenter", "urlenter", "tabenter", "urlenter"]); + is(num_calls(), 5, "Switching tabs should trigger both TabEnter and UrlEnter"); }); }); });