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",