дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 11:15:40 +02:00
feat(plugins): let a command read every file selected in the Project view (#1744)
Plugin API 1.2 adds `invocation.selected-paths`: the files and folders a Project view or editor tab menu ran the command on, as absolute paths (the command's argument stays the first of them, so existing plugins see no change). The frontend passes the menu's selection to `plugin_run_command` on the desktop and through the remote dispatcher; the Rust SDK and its mock gain `Invocation::selected_paths`. All components are rebuilt for the WIT change. Closes #1621 Co-authored-by: arnaud-ritti <[email protected]>
Цей коміт міститься в:
1 джерело
068dcfdb0a
коміт
708f1b051b
44 files changed
+249
-42
No files matched your search
@@ -446,10 +446,32 @@ describe("bridge events", () => {
|
||||
language: "plaintext",
|
||||
selections: [{ start: { line: 1, col: 0 }, end: { line: 1, col: 4 } }],
|
||||
},
|
||||
[],
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
it("forwards a menu's path and selection to the command", async () => {
|
||||
const calls: unknown[][] = [];
|
||||
setPluginsBackend({
|
||||
...backend,
|
||||
runCommand: async (...args) => {
|
||||
calls.push(args);
|
||||
return null;
|
||||
},
|
||||
});
|
||||
undefinePoints();
|
||||
undefinePoints = () => {};
|
||||
const stop = startPluginBridge();
|
||||
await refreshPlugins();
|
||||
await act(() =>
|
||||
executeAction("plugin.hello.insert-date", { path: "/p/a.java", paths: ["/p/a.java", "/p/b.java"] }),
|
||||
);
|
||||
expect(calls[0]?.[2]).toBe("/p/a.java");
|
||||
expect(calls[0]?.[4]).toEqual(["/p/a.java", "/p/b.java"]);
|
||||
stop();
|
||||
});
|
||||
|
||||
it("shows status items and tasks of running plugin code", () => {
|
||||
const item = { text: "main ↑1", tooltip: "Branch", command: "refresh" };
|
||||
handlePluginEvent({ type: "statusItemChanged", pluginId: "git", id: "branch", item });
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { View, ViewEvent } from "@/bindings";
|
||||
import { registerAction } from "@/platform/actions";
|
||||
import { bindingsFor, effectiveBindings, registerExtensionBindings, useKeymap } from "@/platform/keymap";
|
||||
import { createMemoryPluginsBackend, setPluginsBackend, type PluginsBackend } from "../backend";
|
||||
import { defineBuiltinUiPoints } from "../points";
|
||||
import { commandArgs, defineBuiltinUiPoints } from "../points";
|
||||
import { clearUiContributions, syncUiContributions } from "../projection";
|
||||
import { pluginMenuItems, pluginViewItemMenuItems, withPluginSetting } from "../uiPoints";
|
||||
import { matchesPattern } from "../views/customEditor";
|
||||
@@ -162,6 +162,13 @@ describe("plugin keymaps", () => {
|
||||
});
|
||||
|
||||
describe("plugin menus", () => {
|
||||
it("unpacks command arguments", () => {
|
||||
expect(commandArgs("x")).toEqual(["x", []]);
|
||||
expect(commandArgs({ path: "/a", paths: ["/a", "/b"] })).toEqual(["/a", ["/a", "/b"]]);
|
||||
expect(commandArgs(undefined)).toEqual([null, []]);
|
||||
expect(commandArgs({ path: 3 })).toEqual([null, []]);
|
||||
});
|
||||
|
||||
it("projects context menu entries for their command's action", () => {
|
||||
const off = registerAction({ id: "plugin.p.check", title: "Check", run: () => {} });
|
||||
const plugin = {
|
||||
@@ -188,9 +195,26 @@ describe("plugin menus", () => {
|
||||
[plugin],
|
||||
);
|
||||
expect(pluginMenuItems("projectView", ["/p/a.java"])).toEqual([
|
||||
{ kind: "action", action: "plugin.p.check", arg: "/p/a.java" },
|
||||
{ kind: "action", action: "plugin.p.check", arg: { path: "/p/a.java", paths: ["/p/a.java"] } },
|
||||
]);
|
||||
expect(pluginMenuItems("projectView", ["/p/a.java", "/p/b.java"])[0]).toMatchObject({
|
||||
arg: { path: "/p/a.java", paths: ["/p/a.java", "/p/b.java"] },
|
||||
});
|
||||
expect(pluginMenuItems("editor", ["/p/a.java"])).toEqual([]);
|
||||
syncUiContributions(
|
||||
[
|
||||
{
|
||||
pluginId: "p",
|
||||
point: "menus",
|
||||
key: "editor.context/always",
|
||||
decl: { menu: "editor.context", command: "check" },
|
||||
},
|
||||
],
|
||||
[plugin],
|
||||
);
|
||||
expect(pluginMenuItems("editor", ["/p/a.java"])).toEqual([
|
||||
{ kind: "action", action: "plugin.p.check", arg: undefined },
|
||||
]);
|
||||
off();
|
||||
});
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ export interface PluginsBackend {
|
||||
commandId: string,
|
||||
arg: string | null,
|
||||
editor: PluginEditor | null,
|
||||
paths: string[],
|
||||
): Promise<string | null>;
|
||||
/** Workbench events plugins subscribed to (see `eventKinds`). */
|
||||
postEvents(events: WorkbenchEvent[]): Promise<void>;
|
||||
@@ -150,7 +151,8 @@ export const tauriPluginsBackend: PluginsBackend = {
|
||||
await call(commands.pluginUninstall(id));
|
||||
},
|
||||
reload: (id) => call(commands.pluginReload(id)),
|
||||
runCommand: (pluginId, commandId, arg, editor) => call(commands.pluginRunCommand(pluginId, commandId, arg, editor)),
|
||||
runCommand: (pluginId, commandId, arg, editor, paths) =>
|
||||
call(commands.pluginRunCommand(pluginId, commandId, arg, editor, paths)),
|
||||
postEvents: async (events) => {
|
||||
await commands.pluginPostEvents(events);
|
||||
},
|
||||
|
||||
@@ -17,6 +17,14 @@ import { defineViewUiPoints, pluginSettingsPageId } from "./uiPoints";
|
||||
import { isLive, runPluginCommand } from "./store";
|
||||
import styles from "./PluginsPage.module.css";
|
||||
|
||||
/** Unpacks an action argument: a bare path, or a menu's `{ path, paths }`. */
|
||||
export function commandArgs(arg: unknown): [string | null, string[]] {
|
||||
if (typeof arg === "string") return [arg, []];
|
||||
const { path, paths } = (arg ?? {}) as { path?: unknown; paths?: unknown };
|
||||
if (typeof path !== "string" || !Array.isArray(paths)) return [null, []];
|
||||
return [path, paths.filter((p): p is string => typeof p === "string")];
|
||||
}
|
||||
|
||||
export const pluginActionId = (pluginId: string, commandId: string) => `plugin.${pluginId}.${commandId}`;
|
||||
|
||||
interface CommandDecl {
|
||||
@@ -148,7 +156,8 @@ export function defineBuiltinUiPoints(): () => void {
|
||||
keywords: ["plugin", plugin.name, pluginId],
|
||||
description: tx`From plugin ${plugin.name}`,
|
||||
run: async ({ arg }) => {
|
||||
await runPluginCommand(pluginId, decl.id, typeof arg === "string" ? arg : null);
|
||||
const [path, paths] = commandArgs(arg);
|
||||
await runPluginCommand(pluginId, decl.id, path, paths);
|
||||
},
|
||||
}),
|
||||
Details: CommandsDetails,
|
||||
|
||||
@@ -392,10 +392,15 @@ export function commandTitle(pluginId: string, commandId: string): string {
|
||||
}
|
||||
|
||||
/** Runs a plugin command from an action; failures become notifications. */
|
||||
export async function runPluginCommand(pluginId: string, commandId: string, arg: string | null = null) {
|
||||
export async function runPluginCommand(
|
||||
pluginId: string,
|
||||
commandId: string,
|
||||
arg: string | null = null,
|
||||
paths: string[] = [],
|
||||
) {
|
||||
const title = commandTitle(pluginId, commandId);
|
||||
try {
|
||||
return await pluginsBackend().runCommand(pluginId, commandId, arg, editorContext());
|
||||
return await pluginsBackend().runCommand(pluginId, commandId, arg, editorContext(), paths);
|
||||
} catch (e) {
|
||||
await refreshPlugins();
|
||||
const status = usePlugins.getState().plugins.find((p) => p.id === pluginId)?.status;
|
||||
|
||||
@@ -86,7 +86,11 @@ export function pluginMenuItems(menu: ContextMenuId, paths: readonly string[]):
|
||||
const action = pluginActionId(pluginId, decl.command);
|
||||
if (!getAction(action)) continue;
|
||||
// The editor menu runs with the focused editor; the others name their file.
|
||||
out.push({ kind: "action", action, arg: menu === "editor" ? undefined : paths[0] });
|
||||
out.push({
|
||||
kind: "action",
|
||||
action,
|
||||
arg: menu === "editor" ? undefined : { path: paths[0], paths: [...paths] },
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
Reference in new issue
Заблокувати користувача