дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 03:05:40 +02:00
feat(plugins): plugin system with extension points, consent, Wasm runtime and plugin API 1.0 (#971)
A plugin system built on WebAssembly components (wasmtime, WASI p2), with a typed WIT API (`openidea:[email protected]`) and a Rust SDK, shipped as a preview. - Extension points declared in `plugin.toml` (commands, languages, themes, icon themes, menus, key bindings, status items, settings, views, custom editors, file icons and templates, run detectors, editor providers), validated before anything takes effect and projected onto the workbench's own registries: plugin themes and file icon themes are chosen in Settings › Appearance and Quick Switch, plugin shortcuts are edited in Settings › Keymap, plugin problems are grouped in Settings › Inspections. - Capabilities with risk levels and consent at install, pinned `process:` programs, secrets in the OS keychain. - Runtime: one thread per active plugin, fuel/time/memory budgets, soft and hard deadlines on the editing path, cancellation, crash backoff, idle reclaim, a precompiled-component cache; remote projects run code plugins on `openidea-server`. - Web views and custom editors with a sandboxed page, a document bridge joined to the text editor's undo history, and split or preview layouts. - Distribution: signed `.oiplugin` packages, a static marketplace index with revocation, versioned installs with rollback, install from a `.zip` or a folder. The release workflow signs and publishes the official plugins and their index, which released builds use as the default marketplace. - Official plugins: String Tools, Key Promoter, Git Toolbox, AI Completion, Checkstyle, MyBatis Navigator, CSV Editor, Solidity, Material Icons, Harbor Night, Excalidraw, Graphviz, Regex Tester, License Header. Closes #732 Co-authored-by: Deng Liu <[email protected]> Co-authored-by: florentvdry <[email protected]>
Цей коміт міститься в:
450 files changed
+47018
-3058
No files matched your search
@@ -13,7 +13,7 @@ import {
|
||||
} from "react";
|
||||
import clsx from "clsx";
|
||||
import { create } from "zustand";
|
||||
import { getAction, isActionEnabled, executeAction, actionToggled } from "@/platform/actions";
|
||||
import { getAction, isActionEnabled, executeActionFrom, actionToggled } from "@/platform/actions";
|
||||
import { useContextKeys } from "@/platform/context";
|
||||
import { shortcutLabel } from "@/platform/keymap";
|
||||
import { Icon, type IconName } from "./icons";
|
||||
@@ -84,7 +84,7 @@ export function resolveMenuItems(items: MenuItem[]): Resolved[] {
|
||||
shortcut: shortcutLabel(item.action),
|
||||
disabled: !!item.disabled || !isActionEnabled(a),
|
||||
checked: actionToggled(a),
|
||||
run: () => void executeAction(item.action, item.arg),
|
||||
run: () => void executeActionFrom("menu", item.action, item.arg),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
*/
|
||||
import { useId, useMemo, type CSSProperties } from "react";
|
||||
import clsx from "clsx";
|
||||
import { create } from "zustand";
|
||||
import styles from "./icons.module.css";
|
||||
|
||||
const RAW = import.meta.glob<string>(["./expui/**/*.svg", "./openidea/**/*.svg"], {
|
||||
@@ -537,10 +538,64 @@ export type IconName = BaseName | `symbol-${SymbolKind}`;
|
||||
export const ICON_NAMES = Object.keys(ICONS) as BaseName[];
|
||||
|
||||
function iconPath(name: string): string | undefined {
|
||||
if (name.startsWith(EXTERNAL_PREFIX)) return EXTERNAL.has(name) ? name : undefined;
|
||||
if (name.startsWith("symbol-")) return SYMBOL_ICONS[name.slice(7) as SymbolKind];
|
||||
return (ICONS as Record<string, string>)[name];
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Icons supplied at runtime (plugin icon themes). The markup was sanitised by
|
||||
// the backend (`ide_host::plugins::sanitize_svg`); names are `ext:<source>/<icon>`.
|
||||
|
||||
const EXTERNAL_PREFIX = "ext:";
|
||||
const EXTERNAL = new Map<string, string>();
|
||||
|
||||
/** Registers icons by name (`ext:…`); the returned function removes them. */
|
||||
export function registerExternalIcons(icons: ReadonlyArray<readonly [name: string, svg: string]>): () => void {
|
||||
const names: string[] = [];
|
||||
for (const [name, svg] of icons) {
|
||||
if (!name.startsWith(EXTERNAL_PREFIX)) continue;
|
||||
EXTERNAL.set(name, svg);
|
||||
prepared.delete(`${name}|16`);
|
||||
prepared.delete(`${name}|20`);
|
||||
names.push(name);
|
||||
}
|
||||
return () => {
|
||||
for (const name of names) {
|
||||
EXTERNAL.delete(name);
|
||||
prepared.delete(`${name}|16`);
|
||||
prepared.delete(`${name}|20`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** A file icon theme (plugin `iconThemes`). Each method returns an icon name, or null for the built-in one. */
|
||||
export interface FileIconTheme {
|
||||
file(fileName: string): string | null;
|
||||
/** Plain folders only: folders with a role (source root, excluded, …) keep their built-in icons. */
|
||||
folder(folderName: string, expanded: boolean): string | null;
|
||||
}
|
||||
|
||||
let fileIconTheme: FileIconTheme | null = null;
|
||||
|
||||
/** Bumped when the file icon theme changes; file lists subscribe to redraw their icons. */
|
||||
export const useFileIconRevision = create<number>(() => 0);
|
||||
|
||||
/**
|
||||
* Sets the file icon theme: consulted after the user's own file type
|
||||
* associations and before the built-in icons.
|
||||
*/
|
||||
export function setFileIconTheme(theme: FileIconTheme | null) {
|
||||
if (fileIconTheme === theme) return;
|
||||
fileIconTheme = theme;
|
||||
useFileIconRevision.setState((n) => n + 1, true);
|
||||
}
|
||||
|
||||
/** The icon theme's icon for a plain folder, or null to keep the built-in one. */
|
||||
export function themedFolderIconName(folderName: string, expanded = false): IconName | null {
|
||||
return (fileIconTheme?.folder(folderName, expanded) ?? null) as IconName | null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SVG processing
|
||||
|
||||
@@ -562,6 +617,7 @@ interface Prepared {
|
||||
}
|
||||
|
||||
function raw(path: string): string | undefined {
|
||||
if (path.startsWith(EXTERNAL_PREFIX)) return EXTERNAL.get(path);
|
||||
return RAW[path.startsWith("openidea/") ? `./${path}.svg` : `./expui/${path}.svg`];
|
||||
}
|
||||
|
||||
@@ -930,10 +986,29 @@ export function setFileIconOverride(resolve: ((fileName: string) => IconName | n
|
||||
iconOverride = resolve;
|
||||
}
|
||||
|
||||
/** Icons for particular files (plugin `fileIcons`), newest first: after the user's, before the icon theme's. */
|
||||
let fileIconProviders: ReadonlyArray<(fileName: string) => string | null> = [];
|
||||
|
||||
/** Adds a file icon provider (an `ext:` icon name or null); the returned function removes it. */
|
||||
export function addFileIconProvider(resolve: (fileName: string) => string | null): () => void {
|
||||
fileIconProviders = [resolve, ...fileIconProviders];
|
||||
useFileIconRevision.setState((n) => n + 1, true);
|
||||
return () => {
|
||||
fileIconProviders = fileIconProviders.filter((p) => p !== resolve);
|
||||
useFileIconRevision.setState((n) => n + 1, true);
|
||||
};
|
||||
}
|
||||
|
||||
/** Picks a file-type icon from a file name. */
|
||||
export function fileIconName(fileName: string): IconName {
|
||||
const custom = iconOverride?.(fileName);
|
||||
if (custom) return custom;
|
||||
for (const provide of fileIconProviders) {
|
||||
const icon = provide(fileName);
|
||||
if (icon && iconPath(icon)) return icon as IconName;
|
||||
}
|
||||
const themed = fileIconTheme?.file(fileName);
|
||||
if (themed) return themed as IconName;
|
||||
const lower = fileName.toLowerCase();
|
||||
const named = FILE_NAMES[lower] ?? NAME_PATTERNS.find(([re]) => re.test(lower))?.[1];
|
||||
if (named) return named;
|
||||
|
||||
Reference in new issue
Заблокувати користувача