Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 81 additions & 1 deletion src/glide/browser/base/content/browser.mts
Original file line number Diff line number Diff line change
Expand Up @@ -869,6 +869,7 @@ class GlideBrowserClass {
"nsISupportsWeakReference",
]),

$last_selected_tab: null as BrowserTab | null,
$last_location: null as string | null,

/**
Expand Down Expand Up @@ -911,9 +912,32 @@ 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;

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);
},
});
Expand Down Expand Up @@ -1005,8 +1029,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") ?? "<unknown>";
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) {
Expand Down
12 changes: 12 additions & 0 deletions src/glide/browser/base/content/glide.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,15 @@ declare global {
callback: (args: glide.AutocmdArgs[Event]) => void,
): void;

/**
* Create an autocmd that will be invoked whenever the focused tab changes.
*/
create<const Event extends "TabEnter">(
event: Event,
pattern: glide.AutocmdPatterns[Event],
callback: (args: glide.AutocmdArgs[Event]) => void,
): void;

/**
* Create an autocmd that will be invoked whenever the mode changes.
*
Expand Down Expand Up @@ -2209,13 +2218,15 @@ declare global {

type AutocmdEvent =
| "UrlEnter"
| "TabEnter"
| "ModeChanged"
| "ConfigLoaded"
| "WindowLoaded"
| "CommandLineExit"
| "KeyStateChanged";
type AutocmdPatterns = {
UrlEnter: RegExp | { hostname?: string };
TabEnter: RegExp | { hostname?: string };
ModeChanged: "*" | `${GlideMode | "*"}:${GlideMode | "*"}`;
ConfigLoaded: null;
WindowLoaded: null;
Expand All @@ -2224,6 +2235,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.
Expand Down
163 changes: 163 additions & 0 deletions src/glide/browser/base/content/test/autocmds/browser_autocmds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(["tabenter", "urlenter"]);
is(num_calls(), 2, "Initial navigation should trigger both TabEnter and UrlEnter");

Comment thread
suveshmoza marked this conversation as resolved.
const tab1 = gBrowser.selectedTab;

BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, INPUT_TEST_URI + "?navigate");
await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
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(), 3, "Opening non-matching page should not trigger any autocmd");

await BrowserTestUtils.switchTab(gBrowser, tab1);
await waiter(() => glide.g.calls).isjson(["tabenter", "urlenter", "urlenter", "tabenter", "urlenter"]);
is(num_calls(), 5, "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;
}
27 changes: 26 additions & 1 deletion src/glide/docs/autocmds.md
Original file line number Diff line number Diff line change
Expand Up @@ -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", {
Expand All @@ -119,6 +119,31 @@ Callback arguments:
}
```

## TabEnter

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 so that you can run any necessary cleanup.

```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.
Expand Down