дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 11:15: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
@@ -4,42 +4,197 @@
|
||||
* development and tests.
|
||||
*/
|
||||
import { Channel } from "@tauri-apps/api/core";
|
||||
import type { ActiveTheme, CatalogPlugin, PluginEvent, PluginInfo, PluginsSnapshot } from "@/bindings";
|
||||
import type {
|
||||
ActiveTheme,
|
||||
CapabilityInfo,
|
||||
CatalogPlugin,
|
||||
Consent,
|
||||
EventKind,
|
||||
InstallPreview,
|
||||
MarketPlugin,
|
||||
PluginEditor,
|
||||
PluginEvent,
|
||||
PluginInfo,
|
||||
PluginsSnapshot,
|
||||
PluginSecretState,
|
||||
PluginStatusItem,
|
||||
UiContribution,
|
||||
View,
|
||||
ViewEvent,
|
||||
Viewport,
|
||||
WorkbenchEvent,
|
||||
} from "@/bindings";
|
||||
import { tx } from "@/platform/i18n";
|
||||
import { call, commands, IpcError, isTauri } from "@/platform/ipc";
|
||||
import { editorContext } from "./editorContext";
|
||||
|
||||
export interface PluginsBackend {
|
||||
list(): Promise<PluginsSnapshot>;
|
||||
setEnabled(id: string, enabled: boolean): Promise<void>;
|
||||
installDir(path: string): Promise<PluginInfo>;
|
||||
/** Validates a package for the install page; nothing is copied. */
|
||||
previewInstall(path: string): Promise<InstallPreview>;
|
||||
installDir(path: string, consent: Consent): Promise<PluginInfo>;
|
||||
/** Confirms a plugin waiting for consent. */
|
||||
grant(id: string, consent: Consent): Promise<PluginInfo>;
|
||||
setCapability(id: string, capability: string, granted: boolean): Promise<PluginInfo>;
|
||||
/** Validated assets (icon theme mapping and SVGs) whose id starts with `prefix`. */
|
||||
assets(pluginId: string, prefix: string): Promise<[string, string][]>;
|
||||
/** `language` / `file` / `view` activation events. */
|
||||
activationEvent(kind: "language" | "file" | "view", value: string): Promise<void>;
|
||||
/** Plugin packages available locally (no network marketplace). */
|
||||
catalog(): Promise<CatalogPlugin[]>;
|
||||
uninstall(id: string): Promise<void>;
|
||||
reload(id: string): Promise<PluginInfo>;
|
||||
runCommand(pluginId: string, commandId: string, arg: string | null): Promise<string | null>;
|
||||
/** Runs a command with the focused editor (path, language, selections) as its context. */
|
||||
runCommand(
|
||||
pluginId: string,
|
||||
commandId: string,
|
||||
arg: string | null,
|
||||
editor: PluginEditor | null,
|
||||
): Promise<string | null>;
|
||||
/** Workbench events plugins subscribed to (see `eventKinds`). */
|
||||
postEvents(events: WorkbenchEvent[]): Promise<void>;
|
||||
/** Kinds of workbench events plugins want; re-fetch on `subscriptionsChanged`. */
|
||||
eventKinds(): Promise<EventKind[]>;
|
||||
/** Plugin status bar items (later changes arrive as `statusItemChanged`). */
|
||||
statusItems(): Promise<PluginStatusItem[]>;
|
||||
cancelTask(taskId: string): Promise<void>;
|
||||
/** Marketplace plugins: the index read again (`refresh`) or the last one read. */
|
||||
market(refresh: boolean): Promise<MarketPlugin[]>;
|
||||
marketUrl(): Promise<string | null>;
|
||||
setMarketUrl(url: string | null): Promise<void>;
|
||||
/** Downloads and checks a marketplace plugin; the folder to install from. */
|
||||
marketDownload(id: string): Promise<string>;
|
||||
/** Unpacks a package file (`.oiplugin` or `.zip`); the folder to install from. */
|
||||
unpackPackage(path: string): Promise<string>;
|
||||
/** Goes back to the version the last update replaced. */
|
||||
rollBack(id: string): Promise<PluginInfo>;
|
||||
/** File picker for a package: an `.oiplugin`, a `.zip` or a folder's `plugin.toml`; `null` when cancelled. */
|
||||
pickPackage(): Promise<string | null>;
|
||||
/** Content of a plugin view (`doc`: the document of a custom editor). */
|
||||
renderView(pluginId: string, view: string, viewport: Viewport | null, doc: number | null): Promise<View>;
|
||||
viewEvent(pluginId: string, view: string, event: ViewEvent, doc: number | null): Promise<void>;
|
||||
/** The URL of a web view's page (`web = …`), under the `openidea-plugin` scheme. */
|
||||
webPageUrl(pluginId: string, view: string): Promise<string>;
|
||||
/** Save dialog for a web view's `openidea.saveFile`; the path written, or `null` when cancelled. */
|
||||
saveWebFile(name: string, filters: FileFilter[] | undefined, bytes: Uint8Array): Promise<string | null>;
|
||||
/** Open dialog for a web view's `openidea.openFile`; `null` when cancelled. */
|
||||
openWebFile(filters: FileFilter[] | undefined): Promise<{ name: string; data: Uint8Array } | null>;
|
||||
/** Which of a plugin's declared secrets are stored (values never come back). */
|
||||
secrets(pluginId: string): Promise<PluginSecretState[]>;
|
||||
/** Stores a secret (`null` removes it). */
|
||||
setSecret(pluginId: string, name: string, value: string | null): Promise<void>;
|
||||
setActiveFile(path: string | null): Promise<void>;
|
||||
setTheme(pluginId: string | null, themeId: string | null): Promise<ActiveTheme | null>;
|
||||
/** The selected theme's resolved frame colour (`#rrggbb[aa]`), which new windows open in. */
|
||||
setThemeFrame(color: string | null): Promise<void>;
|
||||
/** Folder picker; `null` when cancelled. */
|
||||
/** A package to install: a `.zip`, or the `plugin.toml` of a folder. */
|
||||
pickPackage(): Promise<string | null>;
|
||||
pickFolder(): Promise<string | null>;
|
||||
/** Subscribes to backend plugin events (this window only). */
|
||||
onEvent(handler: (e: PluginEvent) => void): () => void;
|
||||
}
|
||||
|
||||
/** A file type choice of the save / open dialogs. */
|
||||
export interface FileFilter {
|
||||
name: string;
|
||||
extensions: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* `path` under the `openidea-plugin` scheme, as the platform's webview spells
|
||||
* custom schemes (WebView2 maps them onto `http://<scheme>.localhost`).
|
||||
*/
|
||||
export function webSchemeUrl(path: string, windows = /windows/i.test(navigator.userAgent)): string {
|
||||
const encoded = path.split("/").map(encodeURIComponent).join("/");
|
||||
return windows ? `http://openidea-plugin.localhost/${encoded}` : `openidea-plugin://localhost/${encoded}`;
|
||||
}
|
||||
|
||||
function toBase64(bytes: Uint8Array): string {
|
||||
let binary = "";
|
||||
for (let i = 0; i < bytes.length; i += 0x8000) binary += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
|
||||
return btoa(binary);
|
||||
}
|
||||
|
||||
function fromBase64(b64: string): Uint8Array {
|
||||
const binary = atob(b64);
|
||||
const out = new Uint8Array(binary.length);
|
||||
for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
|
||||
return out;
|
||||
}
|
||||
|
||||
export const tauriPluginsBackend: PluginsBackend = {
|
||||
list: () => call(commands.pluginList()),
|
||||
setEnabled: async (id, enabled) => {
|
||||
await call(commands.pluginSetEnabled(id, enabled));
|
||||
},
|
||||
installDir: (path) => call(commands.pluginInstallDir(path)),
|
||||
previewInstall: (path) => call(commands.pluginPreviewInstall(path)),
|
||||
installDir: (path, consent) => call(commands.pluginInstallDir(path, consent)),
|
||||
grant: (id, consent) => call(commands.pluginGrant(id, consent)),
|
||||
setCapability: (id, capability, granted) => call(commands.pluginSetCapability(id, capability, granted)),
|
||||
assets: (pluginId, prefix) => call(commands.pluginAssets(pluginId, prefix)),
|
||||
activationEvent: async (kind, value) => {
|
||||
await call(commands.pluginActivationEvent(kind, value));
|
||||
},
|
||||
catalog: () => call(commands.pluginCatalog()),
|
||||
uninstall: async (id) => {
|
||||
await call(commands.pluginUninstall(id));
|
||||
},
|
||||
reload: (id) => call(commands.pluginReload(id)),
|
||||
runCommand: (pluginId, commandId, arg) => call(commands.pluginRunCommand(pluginId, commandId, arg)),
|
||||
runCommand: (pluginId, commandId, arg, editor) => call(commands.pluginRunCommand(pluginId, commandId, arg, editor)),
|
||||
postEvents: async (events) => {
|
||||
await commands.pluginPostEvents(events);
|
||||
},
|
||||
eventKinds: () => commands.pluginEventKinds(),
|
||||
statusItems: () => commands.pluginStatusItems(),
|
||||
cancelTask: async (taskId) => {
|
||||
await commands.pluginCancelTask(taskId);
|
||||
},
|
||||
market: (refresh) => call(commands.pluginMarket(refresh)),
|
||||
marketUrl: () => commands.pluginMarketUrl(),
|
||||
setMarketUrl: async (url) => {
|
||||
await commands.pluginSetMarketUrl(url);
|
||||
},
|
||||
marketDownload: (id) => call(commands.pluginMarketDownload(id)),
|
||||
unpackPackage: (path) => call(commands.pluginUnpackPackage(path)),
|
||||
rollBack: (id) => call(commands.pluginRollBack(id)),
|
||||
pickPackage: async () => {
|
||||
const { open } = await import("@tauri-apps/plugin-dialog");
|
||||
const picked = await open({
|
||||
multiple: false,
|
||||
title: tx("Install Plugin from Package File"),
|
||||
filters: [{ name: tx("Plugin package"), extensions: ["oiplugin", "zip", "toml"] }],
|
||||
});
|
||||
return (Array.isArray(picked) ? picked[0] : picked) ?? null;
|
||||
},
|
||||
// A tool window's calls (no `doc`) carry the window's active editor.
|
||||
renderView: (pluginId, view, viewport, doc) =>
|
||||
call(commands.pluginViewRender(pluginId, view, viewport, doc, doc == null ? editorContext() : null)),
|
||||
viewEvent: async (pluginId, view, event, doc) => {
|
||||
await call(commands.pluginViewEvent(pluginId, view, event, doc, doc == null ? editorContext() : null));
|
||||
},
|
||||
webPageUrl: async (pluginId, view) => {
|
||||
const page = await call(commands.pluginWebPage(pluginId, view));
|
||||
return webSchemeUrl(page.path);
|
||||
},
|
||||
saveWebFile: async (name, filters, bytes) => {
|
||||
const { save } = await import("@tauri-apps/plugin-dialog");
|
||||
const path = await save({ defaultPath: name, filters });
|
||||
if (!path) return null;
|
||||
await call(commands.pluginWebSaveFile(path, toBase64(bytes)));
|
||||
return path;
|
||||
},
|
||||
openWebFile: async (filters) => {
|
||||
const { open } = await import("@tauri-apps/plugin-dialog");
|
||||
const picked = await open({ multiple: false, filters });
|
||||
const path = (Array.isArray(picked) ? picked[0] : picked) ?? null;
|
||||
if (!path) return null;
|
||||
const data = fromBase64(await call(commands.pluginWebReadFile(path)));
|
||||
return { name: path.split(/[\\/]/).pop() ?? path, data };
|
||||
},
|
||||
secrets: (pluginId) => call(commands.pluginSecrets(pluginId)),
|
||||
setSecret: async (pluginId, name, value) => {
|
||||
await call(commands.pluginSetSecret(pluginId, name, value));
|
||||
},
|
||||
setActiveFile: async (path) => {
|
||||
await commands.pluginSetActiveFile(path);
|
||||
},
|
||||
@@ -47,13 +202,9 @@ export const tauriPluginsBackend: PluginsBackend = {
|
||||
setThemeFrame: async (color) => {
|
||||
await call(commands.pluginSetThemeFrame(color));
|
||||
},
|
||||
pickPackage: async () => {
|
||||
pickFolder: async () => {
|
||||
const { open } = await import("@tauri-apps/plugin-dialog");
|
||||
const picked = await open({
|
||||
multiple: false,
|
||||
title: tx("Install Plugin from Disk"),
|
||||
filters: [{ name: tx("Plugin package"), extensions: ["zip", "toml"] }],
|
||||
});
|
||||
const picked = await open({ directory: true, multiple: false, title: "Install Plugin from Disk" });
|
||||
return (Array.isArray(picked) ? picked[0] : picked) ?? null;
|
||||
},
|
||||
onEvent: (handler) => {
|
||||
@@ -66,7 +217,7 @@ export const tauriPluginsBackend: PluginsBackend = {
|
||||
const tauriListeners = new Set<(e: PluginEvent) => void>();
|
||||
let subscribed = false;
|
||||
|
||||
/** One channel per window; the backend addresses insert-text requests to the invoking window. */
|
||||
/** One channel per window; the backend addresses notifications and consent requests to the invoking window. */
|
||||
function ensureSubscribed() {
|
||||
if (subscribed) return;
|
||||
subscribed = true;
|
||||
@@ -82,7 +233,7 @@ function ensureSubscribed() {
|
||||
// ---------------------------------------------------------------------------
|
||||
// Browser simulation: the bundled sample plugins, with `hello` implemented in JS.
|
||||
|
||||
// templates/theme-plugin, abridged.
|
||||
// plugins/official/harbor-night, abridged.
|
||||
const SAMPLE_THEMES: ActiveTheme["theme"][] = [
|
||||
{
|
||||
id: "harbor-night",
|
||||
@@ -101,6 +252,27 @@ const SAMPLE_THEMES: ActiveTheme["theme"][] = [
|
||||
},
|
||||
];
|
||||
|
||||
const cap = (id: string, risk: CapabilityInfo["risk"], description: string): CapabilityInfo => ({
|
||||
id,
|
||||
risk,
|
||||
description,
|
||||
optional: false,
|
||||
granted: true,
|
||||
});
|
||||
|
||||
const HELLO_COMMANDS = [
|
||||
{ id: "insert-date", title: "Hello: Insert Date", category: "Hello" },
|
||||
{ id: "word-count", title: "Hello: Word Count of Active File", category: "Hello" },
|
||||
];
|
||||
|
||||
/** UI contributions of the sample plugins (what the backend would project). */
|
||||
function sampleUi(id: string): UiContribution[] {
|
||||
if (id === "hello") return HELLO_COMMANDS.map((c) => ({ pluginId: id, point: "commands", key: c.id, decl: c }));
|
||||
if (id === "theme-harbor-night")
|
||||
return SAMPLE_THEMES.map((t) => ({ pluginId: id, point: "themes", key: t.id, decl: t }));
|
||||
return [];
|
||||
}
|
||||
|
||||
function samplePlugins(): PluginInfo[] {
|
||||
const base = {
|
||||
authors: ["OpenIDEA contributors"],
|
||||
@@ -109,10 +281,23 @@ function samplePlugins(): PluginInfo[] {
|
||||
source: "installed" as const,
|
||||
enabled: true,
|
||||
error: null,
|
||||
languages: [],
|
||||
themes: [],
|
||||
commands: [],
|
||||
permissions: [],
|
||||
capabilities: [],
|
||||
contributions: [],
|
||||
activation: [],
|
||||
warnings: [],
|
||||
stats: {
|
||||
activations: 0,
|
||||
activationMs: 0,
|
||||
calls: 0,
|
||||
errors: 0,
|
||||
maxCallMs: 0,
|
||||
p95CallMs: 0,
|
||||
slowCalls: 0,
|
||||
crashes: 0,
|
||||
reclaims: 0,
|
||||
},
|
||||
publisher: null,
|
||||
previousVersion: null,
|
||||
};
|
||||
return [
|
||||
{
|
||||
@@ -122,13 +307,15 @@ function samplePlugins(): PluginInfo[] {
|
||||
version: "0.1.0",
|
||||
description: "Sample plugin: inserts today's date and counts words in the active file.",
|
||||
path: "~/Library/Application Support/ai.l0ng.openidea/plugins/hello",
|
||||
status: "active",
|
||||
status: "inactive",
|
||||
hasCode: true,
|
||||
permissions: ["project-read", "editor-read", "editor-write"],
|
||||
commands: [
|
||||
{ id: "insert-date", title: "Hello: Insert Date", category: "Hello" },
|
||||
{ id: "word-count", title: "Hello: Word Count of Active File", category: "Hello" },
|
||||
activation: ["on-startup-finished"],
|
||||
capabilities: [
|
||||
cap("editor.write", "medium", "Change text in the editor"),
|
||||
cap("project.read", "low", "Read files in the open project"),
|
||||
cap("editor.read", "low", "Read the focused editor's file and selection"),
|
||||
],
|
||||
contributions: HELLO_COMMANDS.map((c) => ({ point: "commands", label: c.title })),
|
||||
},
|
||||
{
|
||||
...base,
|
||||
@@ -139,7 +326,7 @@ function samplePlugins(): PluginInfo[] {
|
||||
path: "~/Library/Application Support/ai.l0ng.openidea/plugins/theme-harbor-night",
|
||||
status: "static",
|
||||
hasCode: false,
|
||||
themes: SAMPLE_THEMES.map(({ id, name, kind }) => ({ id, name, kind })),
|
||||
contributions: SAMPLE_THEMES.map((t) => ({ point: "themes", label: t.name })),
|
||||
},
|
||||
{
|
||||
...base,
|
||||
@@ -150,31 +337,35 @@ function samplePlugins(): PluginInfo[] {
|
||||
path: "~/Library/Application Support/ai.l0ng.openidea/plugins/language-toy",
|
||||
status: "static",
|
||||
hasCode: false,
|
||||
languages: [{ id: "toy", name: "Toy", extensions: ["toy"] }],
|
||||
contributions: [{ point: "languages", label: "Toy (.toy)" }],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** The sample packages as a local catalog (the desktop app scans `plugins/`). */
|
||||
export function sampleCatalog(): CatalogPlugin[] {
|
||||
return samplePlugins().map((p) => ({
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
version: p.version,
|
||||
description: p.description,
|
||||
authors: p.authors,
|
||||
homepage: p.homepage,
|
||||
changeNotes: `${p.version} — first release.`,
|
||||
path: `plugins/${p.id}`,
|
||||
hasCode: p.hasCode,
|
||||
permissions: p.permissions,
|
||||
commands: p.commands,
|
||||
languageCount: p.languages.length,
|
||||
themeCount: p.themes.length,
|
||||
problem: null,
|
||||
}));
|
||||
return samplePlugins().map((p) => {
|
||||
const counts = new Map<string, number>();
|
||||
for (const c of p.contributions) counts.set(c.point, (counts.get(c.point) ?? 0) + 1);
|
||||
return {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
version: p.version,
|
||||
description: p.description,
|
||||
authors: p.authors,
|
||||
homepage: p.homepage,
|
||||
changeNotes: `${p.version} — first release.`,
|
||||
path: `plugins/${p.id}`,
|
||||
hasCode: p.hasCode,
|
||||
capabilities: p.capabilities.map((c) => ({ ...c, granted: false })),
|
||||
contributions: [...counts].map(([point, count]) => ({ point, count })),
|
||||
problem: null,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const live = (p: PluginInfo) => p.status === "active" || p.status === "inactive" || p.status === "static";
|
||||
|
||||
export function createMemoryPluginsBackend(
|
||||
initial: PluginInfo[] = samplePlugins(),
|
||||
catalog: CatalogPlugin[] = sampleCatalog(),
|
||||
@@ -183,6 +374,8 @@ export function createMemoryPluginsBackend(
|
||||
const templates = samplePlugins();
|
||||
let theme: { pluginId: string; themeId: string } | null = null;
|
||||
let activeFile: string | null = null;
|
||||
const secrets = new Map<string, string>();
|
||||
let marketUrl: string | null = null;
|
||||
const listeners = new Set<(e: PluginEvent) => void>();
|
||||
const emit = (e: PluginEvent) => listeners.forEach((l) => l(e));
|
||||
const find = (id: string) => {
|
||||
@@ -197,16 +390,36 @@ export function createMemoryPluginsBackend(
|
||||
if (!p?.enabled || !t) return null;
|
||||
return { pluginId: p.id, theme: t };
|
||||
};
|
||||
const ui = () => plugins.filter(live).flatMap((p) => sampleUi(p.id));
|
||||
const preview = (path: string): InstallPreview => {
|
||||
const entry = catalog.find((c) => c.path === path);
|
||||
if (!entry) throw new IpcError({ kind: "unavailable", message: `installing ${path} requires the desktop app` });
|
||||
return {
|
||||
id: entry.id,
|
||||
name: entry.name,
|
||||
version: entry.version,
|
||||
description: entry.description,
|
||||
authors: entry.authors,
|
||||
hasCode: entry.hasCode,
|
||||
capabilities: entry.capabilities,
|
||||
contributions: entry.contributions,
|
||||
warnings: [],
|
||||
replaces: plugins.find((p) => p.id === entry.id)?.version ?? null,
|
||||
unsigned: true,
|
||||
publisher: null,
|
||||
};
|
||||
};
|
||||
return {
|
||||
list: async () => ({ plugins: plugins.map((p) => ({ ...p })), activeTheme: activeTheme() }),
|
||||
list: async () => ({ plugins: plugins.map((p) => ({ ...p })), activeTheme: activeTheme(), ui: ui() }),
|
||||
setEnabled: async (id, enabled) => {
|
||||
const p = find(id);
|
||||
p.enabled = enabled;
|
||||
p.status = enabled ? (p.hasCode ? "active" : "static") : "disabled";
|
||||
p.status = enabled ? (p.hasCode ? "inactive" : "static") : "disabled";
|
||||
p.error = null;
|
||||
emit({ type: "commandsChanged" });
|
||||
if (theme?.pluginId === id) emit({ type: "themeChanged", theme: activeTheme() });
|
||||
},
|
||||
previewInstall: async (path) => preview(path),
|
||||
installDir: async (path) => {
|
||||
// Only catalog packages can be "installed" here; there are no files in the browser.
|
||||
const entry = catalog.find((c) => c.path === path);
|
||||
@@ -219,6 +432,22 @@ export function createMemoryPluginsBackend(
|
||||
emit({ type: "commandsChanged" });
|
||||
return { ...info };
|
||||
},
|
||||
grant: async (id) => {
|
||||
const p = find(id);
|
||||
p.status = p.hasCode ? "inactive" : "static";
|
||||
p.capabilities = p.capabilities.map((c) => ({ ...c, granted: true }));
|
||||
emit({ type: "commandsChanged" });
|
||||
return { ...p };
|
||||
},
|
||||
setCapability: async (id, capability, granted) => {
|
||||
const p = find(id);
|
||||
p.capabilities = p.capabilities.map((c) => (c.id === capability ? { ...c, granted } : c));
|
||||
if (!granted && p.capabilities.some((c) => c.id === capability && !c.optional)) p.status = "needsConsent";
|
||||
emit({ type: "commandsChanged" });
|
||||
return { ...p };
|
||||
},
|
||||
assets: async () => [],
|
||||
activationEvent: async () => {},
|
||||
catalog: async () => catalog.map((c) => ({ ...c })),
|
||||
uninstall: async (id) => {
|
||||
find(id);
|
||||
@@ -228,13 +457,10 @@ export function createMemoryPluginsBackend(
|
||||
reload: async (id) => find(id),
|
||||
runCommand: async (pluginId, commandId) => {
|
||||
const p = find(pluginId);
|
||||
if (p.status !== "active")
|
||||
if (p.status !== "active" && p.status !== "inactive")
|
||||
throw new IpcError({ kind: "unavailable", message: `plugin "${pluginId}" is not running` });
|
||||
if (commandId === "insert-date") {
|
||||
const date = new Date().toISOString().slice(0, 10);
|
||||
emit({ type: "insertText", pluginId, text: date });
|
||||
return date;
|
||||
}
|
||||
p.status = "active";
|
||||
if (commandId === "insert-date") return new Date().toISOString().slice(0, 10);
|
||||
if (commandId === "word-count") {
|
||||
if (!activeFile) throw new IpcError({ kind: "internal", message: "no file is open in the editor" });
|
||||
const name = activeFile.split("/").pop() ?? activeFile;
|
||||
@@ -243,6 +469,44 @@ export function createMemoryPluginsBackend(
|
||||
}
|
||||
throw new IpcError({ kind: "internal", message: `unknown command: ${commandId}` });
|
||||
},
|
||||
postEvents: async () => {},
|
||||
eventKinds: async () => [],
|
||||
statusItems: async () => [],
|
||||
cancelTask: async () => {},
|
||||
market: async () => [],
|
||||
marketUrl: async () => marketUrl,
|
||||
setMarketUrl: async (url) => {
|
||||
marketUrl = url;
|
||||
},
|
||||
marketDownload: async (id) => {
|
||||
throw new IpcError({ kind: "unavailable", message: `installing ${id} requires the desktop app` });
|
||||
},
|
||||
unpackPackage: async (path) => {
|
||||
throw new IpcError({ kind: "unavailable", message: `unpacking ${path} requires the desktop app` });
|
||||
},
|
||||
rollBack: async (id) => {
|
||||
throw new IpcError({ kind: "notFound", message: `${id} has no earlier version to roll back to` });
|
||||
},
|
||||
pickPackage: async () => null,
|
||||
renderView: async (pluginId, view) => ({
|
||||
toolbar: [],
|
||||
body: { kind: "message", value: `${pluginId} · ${view} (plugin views need the desktop app)` },
|
||||
status: null,
|
||||
}),
|
||||
viewEvent: async () => {},
|
||||
webPageUrl: async (pluginId, view) => {
|
||||
throw new IpcError({ kind: "unavailable", message: `${pluginId} · ${view}: web views need the desktop app` });
|
||||
},
|
||||
saveWebFile: async () => null,
|
||||
openWebFile: async () => null,
|
||||
secrets: async (pluginId) =>
|
||||
[...secrets.entries()]
|
||||
.filter(([k]) => k.startsWith(`${pluginId}/`))
|
||||
.map(([k]) => ({ name: k.slice(pluginId.length + 1), set: true })),
|
||||
setSecret: async (pluginId, name, value) => {
|
||||
if (value === null) secrets.delete(`${pluginId}/${name}`);
|
||||
else secrets.set(`${pluginId}/${name}`, value);
|
||||
},
|
||||
setActiveFile: async (path) => {
|
||||
activeFile = path;
|
||||
},
|
||||
@@ -253,7 +517,7 @@ export function createMemoryPluginsBackend(
|
||||
return t;
|
||||
},
|
||||
setThemeFrame: async () => {},
|
||||
pickPackage: async () => null,
|
||||
pickFolder: async () => null,
|
||||
onEvent: (handler) => {
|
||||
listeners.add(handler);
|
||||
return () => void listeners.delete(handler);
|
||||
|
||||
Reference in new issue
Заблокувати користувача