From 7ffaa0b5da41e27d59e17166c458623fc604fae5 Mon Sep 17 00:00:00 2001
From: Pedro Pessoa <54516747+yzPeedro@users.noreply.github.com>
Date: Wed, 4 Nov 2026 20:48:39 -0300
Subject: [PATCH] feat(plugins): let plugin tool windows use an SVG icon from
the package (#1363)
A view's `icon` could only be a built-in icon name, so a plugin's tool
window button couldn't show the plugin's own mark. Like `fileIcons`, an
`icon` ending in `.svg` is now a path in the package: the host reads and
checks it at install time (a missing file or an SVG with `.toml` format, see §6) | no |
-| `runDetectors` | `id`, `name`, `files`, `command`, `args?`, `test?` (§6.2) | no |
-| `providers` | `kinds`, `languages` (editor providers, see _Providers_) | yes |
-| `secrets` | `name`, `label`, `description?` (see _Network_) | to use them |
-| `menus` | `menu` (`editor.context`, `projectView.context`, `editorTab.context`, `tools`), `command`, `when?` | no |
-| `keymaps` | `command`, `key`, `mac?` (`Ctrl+Alt+C`; a chord: `Meta+K Meta+C`) | no |
-| `statusBar` | `id`, `text`, `tooltip?`, `command?`, `align?`, `priority?` | no |
-| `settings` | `schema` (JSON file), `title?` — one table, see _Settings_ | no |
-| `views` | `id`, `title`, `icon?`, `location` (`left`, `right`, `bottom`), `web?` (see _Web views_) | without `web` |
-| `customEditors` | `id`, `title`, `patterns` (file names), `priority` (`default`, `option`), `layout?`, `web?` | without `web` |
-| `fileIcons` | `id`, `patterns` (file names), `icon` (SVG), `iconDark?` — whatever icon theme is in use | no |
-| `fileTemplates` | `id`, `name`, `extension`, `path` (the new file's content; template variables work) — New menu | no |
+| Point | Declaration | Code needed |
+| --------------- | --------------------------------------------------------------------------------------------------------------------------------- | ------------- |
+| `commands` | `id`, `title`, `category?`, `when?` | to run them |
+| `themes` | `path` (theme JSON), `editor-scheme?` | no |
+| `iconThemes` | `id`, `label`, `path` (icon theme JSON, see §5) | no |
+| `languages` | `path` (`languages/.toml` format, see §6) | no |
+| `runDetectors` | `id`, `name`, `files`, `command`, `args?`, `test?` (§6.2) | no |
+| `providers` | `kinds`, `languages` (editor providers, see _Providers_) | yes |
+| `secrets` | `name`, `label`, `description?` (see _Network_) | to use them |
+| `menus` | `menu` (`editor.context`, `projectView.context`, `editorTab.context`, `tools`), `command`, `when?` | no |
+| `keymaps` | `command`, `key`, `mac?` (`Ctrl+Alt+C`; a chord: `Meta+K Meta+C`) | no |
+| `statusBar` | `id`, `text`, `tooltip?`, `command?`, `align?`, `priority?` | no |
+| `settings` | `schema` (JSON file), `title?` — one table, see _Settings_ | no |
+| `views` | `id`, `title`, `icon?` (an icon name, or an SVG of the package), `location` (`left`, `right`, `bottom`), `web?` (see _Web views_) | without `web` |
+| `customEditors` | `id`, `title`, `patterns` (file names), `priority` (`default`, `option`), `layout?`, `web?` | without `web` |
+| `fileIcons` | `id`, `patterns` (file names), `icon` (SVG), `iconDark?` — whatever icon theme is in use | no |
+| `fileTemplates` | `id`, `name`, `extension`, `path` (the new file's content; template variables work) — New menu | no |
Declared contributions are live as soon as the plugin is enabled — commands
show in Find Action, menus and the status bar, shortcuts work, themes and icon
diff --git a/src/features/plugins/uiPoints.tsx b/src/features/plugins/uiPoints.tsx
index 4fd675cf..d00ed2d2 100644
--- a/src/features/plugins/uiPoints.tsx
+++ b/src/features/plugins/uiPoints.tsx
@@ -290,6 +290,49 @@ function projectFileIcon(pluginId: string, decl: FileIconDecl): () => void {
export const pluginToolWindowId = (pluginId: string, view: string) => `plugin.${pluginId}.${view}`;
+function registerView(pluginId: string, decl: ViewDecl, icon: string): () => void {
+ return registerToolWindow({
+ id: pluginToolWindowId(pluginId, decl.id),
+ title: decl.title,
+ icon,
+ anchor: decl.location,
+ order: 900,
+ component: () =>
+ decl.web ? (
+
+ ) : (
+
+ ),
+ });
+}
+
+function projectView(pluginId: string, decl: ViewDecl): () => void {
+ if (!decl.icon?.endsWith(".svg")) return registerView(pluginId, decl, decl.icon ?? "plugin");
+ let disposed = false;
+ let undo = () => {};
+ void pluginsBackend()
+ .assets(pluginId, `viewIcon/${decl.id}`)
+ .then((assets) => {
+ if (disposed) return;
+ const svg = assets.find(([id]) => id === `viewIcon/${decl.id}`)?.[1];
+ const name = `ext:${pluginId}/view-${decl.id}`;
+ const unregister = svg ? registerExternalIcons([[name, svg]]) : () => {};
+ const remove = registerView(pluginId, decl, svg ? name : "plugin");
+ undo = () => {
+ remove();
+ unregister();
+ };
+ })
+ .catch((e: unknown) => {
+ console.warn(`plugin ${pluginId}: view icon ${decl.id}`, e);
+ if (!disposed) undo = registerView(pluginId, decl, "plugin");
+ });
+ return () => {
+ disposed = true;
+ undo();
+ };
+}
+
/** Registers the client side of the P7 points; returns their disposer. */
export function defineViewUiPoints(): () => void {
const disposers = [
@@ -326,20 +369,7 @@ export function defineViewUiPoints(): () => void {
}),
defineUiPoint("views", {
title: "Tool Windows",
- project: ({ pluginId, decl }) =>
- registerToolWindow({
- id: pluginToolWindowId(pluginId, decl.id),
- title: decl.title,
- icon: decl.icon ?? "plugin",
- anchor: decl.location,
- order: 900,
- component: () =>
- decl.web ? (
-
- ) : (
-
- ),
- }),
+ project: ({ pluginId, decl }) => projectView(pluginId, decl),
}),
defineUiPoint("customEditors", {
title: "Editors",