дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 11:15:40 +02:00
feat(plugins): file marketplace plugins under categories
`plugin.toml` takes `categories` (up to 3 of languages, themes, icon-themes, linters, vcs, ai, frameworks, tools, other). A plugin that declares none is filed under what its contributions imply (themes, icon themes, languages), else under other; categories an IDE doesn't know are ignored, and `check` warns about them. Index entries carry the categories, and Settings › Plugins › Available filters by them and shows them in a plugin's details. The official plugins declare theirs.
Цей коміт міститься в:
1 джерело
af0200e2e7
коміт
53aa2d5704
46 files changed
+245
-18
No files matched your search
@@ -4145,6 +4145,8 @@ export type IndexEntry = {
|
||||
* Empty: any.
|
||||
*/
|
||||
engine?: string,
|
||||
/** Marketplace categories (`CATEGORIES`); an IDE ignores those it doesn't know. */
|
||||
categories?: string[],
|
||||
};
|
||||
|
||||
export type IndexStatus = {
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
IconButton,
|
||||
Input,
|
||||
SearchField,
|
||||
Select,
|
||||
Spinner,
|
||||
Tabs,
|
||||
showContextMenu,
|
||||
@@ -572,6 +573,71 @@ function gearMenu(plugins: readonly PluginInfo[], actions: GearActions): MenuIte
|
||||
];
|
||||
}
|
||||
|
||||
const CATEGORIES = [
|
||||
"languages",
|
||||
"themes",
|
||||
"icon-themes",
|
||||
"linters",
|
||||
"vcs",
|
||||
"ai",
|
||||
"frameworks",
|
||||
"tools",
|
||||
"other",
|
||||
] as const;
|
||||
type Category = (typeof CATEGORIES)[number];
|
||||
|
||||
function categoryLabel(c: Category): string {
|
||||
switch (c) {
|
||||
case "languages":
|
||||
return tx("Languages");
|
||||
case "themes":
|
||||
return tx("Themes");
|
||||
case "icon-themes":
|
||||
return tx("Icon Themes");
|
||||
case "linters":
|
||||
return tx("Linters & Formatters");
|
||||
case "vcs":
|
||||
return tx("Version Control");
|
||||
case "ai":
|
||||
return tx("AI");
|
||||
case "frameworks":
|
||||
return tx("Frameworks");
|
||||
case "tools":
|
||||
return tx("Tools");
|
||||
case "other":
|
||||
return tx("Other");
|
||||
}
|
||||
}
|
||||
|
||||
/** A marketplace plugin's categories this IDE knows; `other` without any. */
|
||||
function categoriesOf(m: MarketPlugin): Category[] {
|
||||
const known = CATEGORIES.filter((c) => m.categories?.includes(c));
|
||||
return known.length > 0 ? known : ["other"];
|
||||
}
|
||||
|
||||
/** Filters the marketplace by category; lists the categories that have plugins. */
|
||||
function CategoryFilter({
|
||||
market,
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
market: readonly MarketPlugin[];
|
||||
value: Category | "all";
|
||||
onChange: (value: Category | "all") => void;
|
||||
}) {
|
||||
const counts = new Map<Category, number>();
|
||||
for (const m of market) for (const c of categoriesOf(m)) counts.set(c, (counts.get(c) ?? 0) + 1);
|
||||
const options = [
|
||||
{ value: "all" as const, label: tx("All Categories"), detail: String(market.length) },
|
||||
...CATEGORIES.filter((c) => counts.has(c)).map((c) => ({
|
||||
value: c,
|
||||
label: categoryLabel(c),
|
||||
detail: String(counts.get(c)),
|
||||
})),
|
||||
];
|
||||
return <Select aria-label={tx("Category")} value={value} options={options} onChange={onChange} />;
|
||||
}
|
||||
|
||||
/** The marketplace index URL, editable in place. */
|
||||
function MarketUrlBar() {
|
||||
const current = usePlugins((s) => s.marketUrl);
|
||||
@@ -667,6 +733,10 @@ function MarketDetails({ entry, onInstalled }: { entry: MarketPlugin; onInstalle
|
||||
<T>Id</T>
|
||||
</dt>
|
||||
<dd className={styles.mono}>{entry.id}</dd>
|
||||
<dt>
|
||||
<T>Category</T>
|
||||
</dt>
|
||||
<dd>{categoriesOf(entry).map(categoryLabel).join(", ")}</dd>
|
||||
{entry.installed && (
|
||||
<>
|
||||
<dt>
|
||||
@@ -706,13 +776,15 @@ export function PluginsPage() {
|
||||
void refreshMarket(false);
|
||||
}, []);
|
||||
const [selectedMarketId, setSelectedMarketId] = useState<string | null>(null);
|
||||
const [category, setCategory] = useState<Category | "all">("all");
|
||||
const marketShown = useMemo(
|
||||
() =>
|
||||
(market ?? []).filter((m) => {
|
||||
const q = query.trim().toLowerCase();
|
||||
if (category !== "all" && !categoriesOf(m).includes(category)) return false;
|
||||
return !q || matches([m.name, m.id, m.description, m.publisher, ...(m.authors ?? [])], q);
|
||||
}),
|
||||
[market, query],
|
||||
[market, query, category],
|
||||
);
|
||||
const selectedMarket = marketShown.find((m) => m.id === selectedMarketId) ?? null;
|
||||
|
||||
@@ -722,7 +794,11 @@ export function PluginsPage() {
|
||||
const linked = shown.filter((p) => p.source === "linked");
|
||||
const selected = shown.find((p) => p.id === selectedId) ?? shown[0] ?? null;
|
||||
|
||||
const catalogShown = useMemo(() => filterCatalog(catalog ?? [], query), [catalog, query]);
|
||||
// Local packages have no categories: shown only without a category filter.
|
||||
const catalogShown = useMemo(
|
||||
() => (category === "all" ? filterCatalog(catalog ?? [], query) : []),
|
||||
[catalog, query, category],
|
||||
);
|
||||
const selectedEntry = catalogShown.find((c) => c.id === selectedCatalogId) ?? catalogShown[0] ?? null;
|
||||
|
||||
const showInstalled = (id: string | null) => {
|
||||
@@ -827,6 +903,11 @@ export function PluginsPage() {
|
||||
<li className={styles.marketItem}>
|
||||
<MarketUrlBar />
|
||||
</li>
|
||||
{(market?.length ?? 0) > 0 && (
|
||||
<li className={styles.marketItem}>
|
||||
<CategoryFilter market={market ?? []} value={category} onChange={setCategory} />
|
||||
</li>
|
||||
)}
|
||||
{!marketUrl && (
|
||||
<li className={styles.notice} role="note">
|
||||
<Icon name="info" size={14} />
|
||||
@@ -855,7 +936,7 @@ export function PluginsPage() {
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{catalog && catalogShown.length === 0 && (
|
||||
{catalog && category === "all" && catalogShown.length === 0 && (
|
||||
<li className={styles.listEmpty}>
|
||||
{catalog.length === 0 ? tx("No local plugin packages.") : tx("No plugins match.")}
|
||||
</li>
|
||||
|
||||
@@ -646,6 +646,48 @@ describe("marketplace", () => {
|
||||
expect(asked[0]).toMatchObject({ pluginId: "hello", kind: "install" });
|
||||
});
|
||||
|
||||
it("filters the marketplace by category", async () => {
|
||||
const entry = (id: string, name: string, categories: string[]) => ({
|
||||
id,
|
||||
name,
|
||||
version: "1.0.0",
|
||||
description: "",
|
||||
authors: [],
|
||||
publisher: "acme",
|
||||
download: `${id}.oiplugin`,
|
||||
digest: "x",
|
||||
capabilities: [],
|
||||
engine: "^1.0",
|
||||
categories,
|
||||
installed: null,
|
||||
compatible: true,
|
||||
});
|
||||
setPluginsBackend({
|
||||
...createMemoryPluginsBackend([], sampleCatalog()),
|
||||
marketUrl: async () => "https://plugins.example/index.json",
|
||||
market: async () => [
|
||||
entry("acme.dusk", "Dusk", ["themes"]),
|
||||
entry("acme.lint", "Lint", ["linters", "tools"]),
|
||||
entry("acme.misc", "Misc", ["robots"]),
|
||||
],
|
||||
});
|
||||
const user = userEvent.setup();
|
||||
render(<PluginsPage />);
|
||||
await user.click(screen.getByRole("tab", { name: /Available/ }));
|
||||
const list = await screen.findByRole("list", { name: "Available plugins" });
|
||||
await waitFor(() => expect(within(list).getByText("Dusk")).toBeInTheDocument());
|
||||
await user.click(within(list).getByRole("button", { name: "Category" }));
|
||||
const options = within(screen.getByRole("listbox"))
|
||||
.getAllByRole("option")
|
||||
.map((o) => o.textContent);
|
||||
expect(options).toEqual(["All Categories3", "Themes1", "Linters & Formatters1", "Tools1", "Other1"]);
|
||||
await user.click(within(screen.getByRole("listbox")).getByText("Tools"));
|
||||
expect(within(list).queryByText("Dusk")).not.toBeInTheDocument();
|
||||
expect(within(list).queryByText("Local")).not.toBeInTheDocument();
|
||||
await user.click(within(list).getByText("Lint"));
|
||||
expect(screen.getByText("Linters & Formatters, Tools")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows a plugin this IDE can't load without installing it", async () => {
|
||||
setPluginsBackend({
|
||||
...createMemoryPluginsBackend([], sampleCatalog()),
|
||||
|
||||
@@ -6063,5 +6063,11 @@
|
||||
"Methods pulled up to an interface are declared abstract there.": "上拉到接口的方法将在接口中声明为抽象方法。",
|
||||
"The file changed; open the dialog again": "文件已更改,请重新打开对话框",
|
||||
"Incompatible": "不兼容",
|
||||
"Needs plugin API {engine}. Update OpenIDEA to install it.": "需要插件 API {engine}。请更新 OpenIDEA 后再安装。"
|
||||
"Needs plugin API {engine}. Update OpenIDEA to install it.": "需要插件 API {engine}。请更新 OpenIDEA 后再安装。",
|
||||
"Icon Themes": "图标主题",
|
||||
"Linters & Formatters": "检查与格式化",
|
||||
"AI": "AI",
|
||||
"Frameworks": "框架",
|
||||
"All Categories": "全部分类",
|
||||
"Category": "分类"
|
||||
}
|
||||
Reference in new issue
Заблокувати користувача