Файли
openidea/src/features/welcome/CustomizePage.tsx
T
1c5a92f952 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]>
2026-11-04 02:54:31 +08:00

205 рядки
7.8 KiB
TypeScript
Неформатований Анотація Історія

Цей файл містить неоднозначні символи Юнікоду
Цей файл містить символи Юнікоду, які можна сплутати з іншими символами. Якщо ви вважаєте, що це зроблено навмисно, можете сміливо ігнорувати це попередження. Щоб показати їх, скористайтеся кнопкою Escape.
/**
* Welcome › Customize (IntelliJ's quick settings): colour theme, accent, IDE
* font size, keymap and startup behaviour, written straight to the global
* settings keys the Settings dialog edits.
*/
import { useMemo, useState, type CSSProperties, type ReactNode } from "react";
import { executeAction } from "@/platform/actions";
import { call, commands, isTauri } from "@/platform/ipc";
import { defaultPreset, PRESET_TITLES } from "@/platform/keymap";
import { useProject } from "@/platform/project";
import { Checkbox, Select, type SelectOption, T } from "@/ui";
import { notifications } from "@/workbench/contributions";
import { ACCENT_PRESETS, DEFAULT_ACCENT, themeSelectOptions, useThemeChoices } from "@/workbench/theme";
import { reopenEnabled, setReopenPreference, useSession } from "./session";
import styles from "./Welcome.module.css";
import { isThemeChoiceId, type ThemeChoiceId } from "@/platform/themes";
import { tx } from "@/platform/i18n";
type Theme = ThemeChoiceId;
const DEFAULT_UI_FONT_SIZE = 13;
const DEFAULT_UI_FONT = "Inter";
export const UI_FONT_SIZES = [10, 11, 12, 13, 14, 15, 16, 17, 18];
/** Writes one global settings key and publishes the new effective settings. */
export async function setGlobalSetting(key: string, value: unknown): Promise<void> {
if (!isTauri()) return;
try {
useProject.setState({ settings: await call(commands.settingsSet("global", key, JSON.stringify(value))) });
} catch (e) {
notifications().notify({
level: "error",
title: "Cannot save setting",
body: e instanceof Error ? e.message : String(e),
key: `welcome.setting:${key}`,
});
}
}
interface AppearanceView {
theme: Theme;
syncWithOs: boolean;
accent: string;
fontSize: number;
keymap: string;
}
/** The values Customize shows, from (possibly partial / legacy) settings. */
export function customizeView(settings: ReturnType<typeof useProject.getState>["settings"]): AppearanceView {
const a = (settings?.appearance ?? {}) as Record<string, unknown>;
const theme = isThemeChoiceId(a.theme) ? a.theme : "light";
const custom = a.useCustomFont === true;
const size = typeof a.uiFontSize === "number" ? a.uiFontSize : DEFAULT_UI_FONT_SIZE;
return {
theme,
syncWithOs: a.syncWithOs === true || a.theme === "system",
accent: typeof a.accent === "string" ? a.accent.toLowerCase() : DEFAULT_ACCENT,
fontSize: custom ? size : DEFAULT_UI_FONT_SIZE,
keymap: typeof settings?.keymap === "string" && settings.keymap ? settings.keymap : defaultPreset(),
};
}
function Field({ label, children }: { label: string; children: ReactNode }) {
return (
<div className={styles.field}>
<span className={styles.fieldLabel}>{label}</span>
<div className={styles.fieldControl}>{children}</div>
</div>
);
}
function Section({ title, children }: { title: string; children: ReactNode }) {
return (
<section className={styles.section} aria-label={title}>
<h2 className={styles.sectionTitle}>{title}</h2>
{children}
</section>
);
}
export function CustomizePage() {
const settings = useProject((s) => s.settings);
const pref = useSession((s) => s.reopenPref);
const v = customizeView(settings);
// Optimistic value while a write is in flight (and the only value outside Tauri).
const [pending, setPending] = useState<Partial<AppearanceView & { reopen: boolean }>>({});
const shown = { ...v, reopen: reopenEnabled(settings, pref), ...pending };
const update = <K extends keyof typeof shown>(field: K, value: (typeof shown)[K], write: () => Promise<void>) => {
setPending((p) => ({ ...p, [field]: value }));
void write().finally(() =>
setPending((p) => {
const { [field]: _, ...rest } = p;
return rest;
}),
);
};
const themes = useThemeChoices();
const keymaps = useMemo<SelectOption[]>(() => {
const custom = (settings?.customKeymaps ?? {}) as Record<string, { name?: string }>;
const list: SelectOption[] = Object.entries(PRESET_TITLES).map(([value, label]) => ({ value, label }));
for (const [id, k] of Object.entries(custom)) list.push({ value: id, label: k?.name || id });
if (!list.some((o) => o.value === shown.keymap)) list.push({ value: shown.keymap, label: shown.keymap });
return list;
}, [settings?.customKeymaps, shown.keymap]);
return (
<div className={styles.customize}>
<Section title="Color theme">
<Field label="Theme">
<Select<Theme>
value={shown.theme}
options={themeSelectOptions(themes, shown.theme)}
disabled={shown.syncWithOs}
aria-label="Color theme"
onChange={(t) => update("theme", t, () => setGlobalSetting("appearance.theme", t))}
/>
<Checkbox
checked={shown.syncWithOs}
label="Sync with OS"
onChange={(on) =>
update("syncWithOs", on, async () => {
await setGlobalSetting("appearance.syncWithOs", on);
// Legacy "system" theme: make the explicit choice concrete.
if (!on && settings?.appearance?.theme === "system")
await setGlobalSetting("appearance.theme", "light");
})
}
/>
</Field>
<Field label="Accent color">
<div className={styles.swatches} role="radiogroup" aria-label={tx("Accent color")}>
{ACCENT_PRESETS.map((p) => (
<button
key={p.value}
type="button"
role="radio"
aria-checked={shown.accent === p.value}
aria-label={p.label}
title={p.label}
className={styles.swatch}
style={{ "--swatch": p.value } as CSSProperties}
onClick={() => update("accent", p.value, () => setGlobalSetting("appearance.accent", p.value))}
/>
))}
</div>
</Field>
</Section>
<Section title="Accessibility">
<Field label="IDE font">
<Select
value={String(shown.fontSize)}
options={UI_FONT_SIZES.map((n) => ({ value: String(n), label: String(n) }))}
aria-label="IDE font size"
onChange={(raw) => {
const n = Number(raw);
update("fontSize", n, async () => {
// The size only applies with "Use custom font" (Settings › Appearance).
await setGlobalSetting("appearance.uiFontSize", n);
const family = settings?.appearance?.uiFontFamily;
const keepFamily =
settings?.appearance?.useCustomFont === true && !!family && family !== DEFAULT_UI_FONT;
await setGlobalSetting("appearance.useCustomFont", n !== DEFAULT_UI_FONT_SIZE || keepFamily);
});
}}
/>
</Field>
</Section>
<Section title="Keymap">
<Field label="Keymap">
<Select
value={shown.keymap}
options={keymaps}
aria-label="Keymap"
onChange={(k) => update("keymap", k, () => setGlobalSetting("keymap", k))}
/>
<button type="button" className={styles.link} onClick={() => void executeAction("settings.keymap")}>
<T>Configure…</T>
</button>
</Field>
</Section>
<Section title="Startup">
<Checkbox
checked={shown.reopen}
label="Reopen last project on startup"
onChange={(on) =>
update("reopen", on, async () => {
setReopenPreference(on);
await setGlobalSetting("general.reopenLastProject", on);
})
}
/>
</Section>
<button type="button" className={styles.link} onClick={() => void executeAction("file.settings")}>
<T>All settings…</T>
</button>
</div>
);
}