From 708f1b051b105204a9922f7eabbbf24ad3a5cffb Mon Sep 17 00:00:00 2001 From: Arnaud Ritti <77437157+arnaud-ritti@users.noreply.github.com> Date: Fri, 6 Nov 2026 15:54:44 +0100 Subject: [PATCH] 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 <77437157+arnaud-ritti@users.noreply.github.com> --- crates/ide-host/src/plugins/index_ops.rs | 7 +++- crates/ide-host/src/plugins/mod.rs | 1 + crates/ide-host/src/plugins/remote_host.rs | 2 + crates/ide-host/src/plugins/views.rs | 7 +++- crates/ide-host/tests/distribution.rs | 2 +- crates/ide-host/tests/plugin_indexing.rs | 7 +++- crates/ide-host/tests/plugins.rs | 20 ++++++++- crates/ide-host/tests/remote_plugins.rs | 9 +++++ crates/ide-plugin/src/types.rs | 2 + crates/ide-plugin/src/wit_host.rs | 4 ++ .../fixtures/hello/dist/plugin.wasm.sources | 2 +- .../fixtures/indexer/dist/plugin.wasm.sources | 2 +- .../tests/fixtures/jobs/dist/plugin.wasm | Bin 133023 -> 134456 bytes .../fixtures/jobs/dist/plugin.wasm.sources | 2 +- .../tests/fixtures/jobs/plugin.toml | 5 +++ .../ide-plugin/tests/fixtures/jobs/src/lib.rs | 7 +++- .../misbehave/dist/plugin.wasm.sources | 2 +- plugins/README.md | 2 +- .../ai-completion/dist/plugin.wasm.sources | 2 +- .../checkstyle/dist/plugin.wasm.sources | 2 +- .../csv-editor/dist/plugin.wasm.sources | 2 +- .../git-toolbox/dist/plugin.wasm.sources | 2 +- .../graphviz/dist/plugin.wasm.sources | 2 +- .../key-promoter/dist/plugin.wasm.sources | 2 +- .../license-header/dist/plugin.wasm.sources | 2 +- plugins/official/mybatis/dist/plugin.wasm | Bin 168245 -> 168367 bytes .../official/mybatis/dist/plugin.wasm.sources | 2 +- .../regex-tester/dist/plugin.wasm.sources | 2 +- .../solidity/dist/plugin.wasm.sources | 2 +- .../string-tools/dist/plugin.wasm.sources | 2 +- sdk/rust/openidea-plugin/src/host.rs | 10 +++++ sdk/rust/openidea-plugin/src/mock.rs | 18 +++++++++ sdk/rust/openidea-plugin/wit/workspace.wit | 5 +++ src-tauri/src/commands/plugin.rs | 2 + src/bindings.ts | 2 +- src/features/macros/model.test.ts | 26 +++++++++++- src/features/macros/model.ts | 38 +++++++++++++----- .../plugins/__tests__/plugins.test.tsx | 22 ++++++++++ src/features/plugins/__tests__/views.test.tsx | 28 ++++++++++++- src/features/plugins/backend.ts | 4 +- src/features/plugins/points.tsx | 11 ++++- src/features/plugins/store.ts | 9 ++++- src/features/plugins/uiPoints.tsx | 6 ++- wit/workspace.wit | 5 +++ 44 files changed, 249 insertions(+), 42 deletions(-) diff --git a/crates/ide-host/src/plugins/index_ops.rs b/crates/ide-host/src/plugins/index_ops.rs index ebc1b5d9b..9047a9ff9 100644 --- a/crates/ide-host/src/plugins/index_ops.rs +++ b/crates/ide-host/src/plugins/index_ops.rs @@ -121,7 +121,12 @@ pub async fn plugin_navigation_index( plugins.index_navigation( &project.root, crate::plugins::navigation::Snapshot { path: &path, version, text: &text, symbols: &symbols, scope }, - Invocation { client: Some(client.clone()), workspace_root: Some(project.root.clone()), editor: None }, + Invocation { + client: Some(client.clone()), + workspace_root: Some(project.root.clone()), + editor: None, + selected_paths: Vec::new(), + }, )?; // A document edit while WASM was executing must not publish stale ranges. if cancel.is_cancelled() || document.is_some_and(|id| project.documents.version(id).ok() != version) { diff --git a/crates/ide-host/src/plugins/mod.rs b/crates/ide-host/src/plugins/mod.rs index 78abae291..cfb7de9e3 100644 --- a/crates/ide-host/src/plugins/mod.rs +++ b/crates/ide-host/src/plugins/mod.rs @@ -1393,6 +1393,7 @@ impl PluginHost { workspace_root: client.as_deref().and_then(|c| self.shared.project_of(c)), client: client.clone(), editor: None, + selected_paths: Vec::new(), }; let kinds: BTreeSet = events.iter().map(WorkbenchEvent::kind).collect(); { diff --git a/crates/ide-host/src/plugins/remote_host.rs b/crates/ide-host/src/plugins/remote_host.rs index ce18c9b0a..166021f74 100644 --- a/crates/ide-host/src/plugins/remote_host.rs +++ b/crates/ide-host/src/plugins/remote_host.rs @@ -367,10 +367,12 @@ impl RemotePlugins { let command_id: String = c.arg("commandId")?; let arg: Option = c.arg("arg")?; let editor: Option = c.arg("editor")?; + let paths: Option> = c.arg("paths")?; let invocation = Invocation { client: Some(c.window.clone()), workspace_root: c.host.project(&c.window).ok().map(|p| p.root.clone()), editor: editor.map(PluginEditor::context), + selected_paths: paths.unwrap_or_default(), }; blocking(move || host.run_command(&plugin_id, &command_id, arg.as_deref(), invocation)).await } diff --git a/crates/ide-host/src/plugins/views.rs b/crates/ide-host/src/plugins/views.rs index a62484ea0..f12e31572 100644 --- a/crates/ide-host/src/plugins/views.rs +++ b/crates/ide-host/src/plugins/views.rs @@ -25,7 +25,12 @@ pub fn view_invocation( Some(EditorContext { path: info.path.0, language: info.language.0, selections: Vec::new() }) } }; - Ok(Invocation { client: Some(window.to_owned()), workspace_root: project.as_ref().map(|p| p.root.clone()), editor }) + Ok(Invocation { + client: Some(window.to_owned()), + workspace_root: project.as_ref().map(|p| p.root.clone()), + editor, + ..Invocation::default() + }) } async fn blocking(f: impl FnOnce() -> IdeResult + Send + 'static) -> IdeResult { diff --git a/crates/ide-host/tests/distribution.rs b/crates/ide-host/tests/distribution.rs index 74ae75e5d..ff23881f5 100644 --- a/crates/ide-host/tests/distribution.rs +++ b/crates/ide-host/tests/distribution.rs @@ -58,7 +58,7 @@ fn fixture() -> Fixture { } fn inv() -> Invocation { - Invocation { client: Some("main".into()), workspace_root: None, editor: None } + Invocation { client: Some("main".into()), workspace_root: None, editor: None, selected_paths: Vec::new() } } fn misbehave() -> PathBuf { diff --git a/crates/ide-host/tests/plugin_indexing.rs b/crates/ide-host/tests/plugin_indexing.rs index 015136f91..045531a54 100644 --- a/crates/ide-host/tests/plugin_indexing.rs +++ b/crates/ide-host/tests/plugin_indexing.rs @@ -171,7 +171,12 @@ fn two_windows_of_same_root_keep_unsaved_snapshots_isolated() { .index_navigation( &f.root, Snapshot { path, version: Some(1), text, symbols: &symbols, scope: "" }, - Invocation { client: Some(client.into()), workspace_root: Some(f.root.clone()), editor: None }, + Invocation { + client: Some(client.into()), + workspace_root: Some(f.root.clone()), + editor: None, + selected_paths: Vec::new(), + }, ) .unwrap(); } diff --git a/crates/ide-host/tests/plugins.rs b/crates/ide-host/tests/plugins.rs index 9f13fb9bb..ff160d73a 100644 --- a/crates/ide-host/tests/plugins.rs +++ b/crates/ide-host/tests/plugins.rs @@ -231,7 +231,12 @@ fn fixture(limits: Limits) -> Fixture { impl Fixture { fn inv(&self) -> Invocation { - Invocation { client: Some("main".into()), workspace_root: Some(self.project.clone()), editor: None } + Invocation { + client: Some("main".into()), + workspace_root: Some(self.project.clone()), + editor: None, + selected_paths: Vec::new(), + } } fn install(&self, dir: &Path) -> PluginInfo { @@ -357,7 +362,7 @@ fn read_file_is_confined_to_the_project() { assert!(err.contains("outside the project root"), "{err}"); assert!(f.run("misbehave", "read", Some("/etc/hosts")).is_err()); // Without a project there is nothing to read. - let inv = Invocation { client: None, workspace_root: None, editor: None }; + let inv = Invocation::default(); let err = f.host.run_command("misbehave", "read", Some("src/notes.txt"), inv).unwrap_err().message; assert!(err.contains("no project is open"), "{err}"); assert_eq!(f.run("misbehave", "root", None).unwrap().unwrap(), to_ipc(&f.project)); @@ -2142,6 +2147,17 @@ fn views_open_project_files_at_a_line() { assert!(opened.is_none()); } +#[test] +fn commands_see_the_paths_they_were_run_on() { + let f = fixture(Limits::default()); + f.install(&sample("jobs")); + let (a, b) = (f.project.join("src/a.txt"), f.project.join("src/b.txt")); + let inv = Invocation { selected_paths: vec![a.clone(), b.clone()], ..f.inv() }; + let out = f.host.run_command("jobs", "selection", Some(&to_ipc(&a)), inv).unwrap(); + assert_eq!(out.as_deref(), Some(format!("{}\n{}", to_ipc(&a), to_ipc(&b)).as_str())); + assert_eq!(f.run("jobs", "selection", None).unwrap().as_deref(), Some("")); +} + #[test] fn vcs_remotes_clipboard_and_open_url() { let f = fixture(Limits::default()); diff --git a/crates/ide-host/tests/remote_plugins.rs b/crates/ide-host/tests/remote_plugins.rs index dd3bd868e..cf0959b50 100644 --- a/crates/ide-host/tests/remote_plugins.rs +++ b/crates/ide-host/tests/remote_plugins.rs @@ -300,5 +300,14 @@ async fn remote_projects_get_plugin_views_and_custom_editors() { }) }) .await; + + let selected = invoke( + &peer, + "plugin_run_command", + json!({ "pluginId": "jobs", "commandId": "selection", "arg": null, "editor": null, "paths": ["/srv/src/a.txt"] }), + ) + .await + .unwrap(); + assert_eq!(selected, "/srv/src/a.txt"); plugins.host().shutdown(); } diff --git a/crates/ide-plugin/src/types.rs b/crates/ide-plugin/src/types.rs index b1d7b215d..bd43473c8 100644 --- a/crates/ide-plugin/src/types.rs +++ b/crates/ide-plugin/src/types.rs @@ -28,6 +28,8 @@ pub struct Invocation { pub client: Option, pub workspace_root: Option, pub editor: Option, + /// Files and folders the command was run on. + pub selected_paths: Vec, } /// A failed host call, as the plugin sees it (WIT `types.error`). diff --git a/crates/ide-plugin/src/wit_host.rs b/crates/ide-plugin/src/wit_host.rs index c2154f365..ed4fc02d3 100644 --- a/crates/ide-plugin/src/wit_host.rs +++ b/crates/ide-plugin/src/wit_host.rs @@ -419,6 +419,10 @@ impl invocation::HostInvocation for StoreData { } } + fn selected_paths(&mut self, _this: Resource) -> wasmtime::Result> { + Ok(self.invocation.selected_paths.iter().take(MAX_FIND as usize).map(|p| to_ipc(p)).collect()) + } + fn drop(&mut self, rep: Resource) -> wasmtime::Result<()> { // Owned by the host for the duration of the call. let _ = rep; diff --git a/crates/ide-plugin/tests/fixtures/hello/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/hello/dist/plugin.wasm.sources index 961a27e8e..de090043a 100644 --- a/crates/ide-plugin/tests/fixtures/hello/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/hello/dist/plugin.wasm.sources @@ -1 +1 @@ -42044c4b2e3d768b922d6e5026ff4e725d37b265896ea7f383f1d737e2feffb7 +7a84aaf2ad8fcb0bf2f9fc0eeff6ad8664512a3fa6d9c4c205c19eff9a3e1315 diff --git a/crates/ide-plugin/tests/fixtures/indexer/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/indexer/dist/plugin.wasm.sources index 0c492615c..489e534c1 100644 --- a/crates/ide-plugin/tests/fixtures/indexer/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/indexer/dist/plugin.wasm.sources @@ -1 +1 @@ -87eb2a28875abbc649b98d2a7f87263da217f9ad28f1395d96a61db5f781d9f0 +92aece4d9f24c4429f2dcfc64e65a53219103267130731af49b65c7c581048d4 diff --git a/crates/ide-plugin/tests/fixtures/jobs/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/jobs/dist/plugin.wasm index 8b2e04da24f592d60606e1bcfe1edc5c5048c13d..3121b3a5ac6fdd64bcdcaa28ba82e4108952d2e3 100644 GIT binary patch delta 21085 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 WAu5W}!lHi4^)9u4-LtYxrvCv`Jxs9x delta 20262 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 diff --git a/crates/ide-plugin/tests/fixtures/jobs/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/jobs/dist/plugin.wasm.sources index deff9f831..706af4df6 100644 --- a/crates/ide-plugin/tests/fixtures/jobs/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/jobs/dist/plugin.wasm.sources @@ -1 +1 @@ -3d3e43d4e49efa7a97984335f7f2df2cb286321bd0368251f37818aa6c1b70ad +17136d5359a8f5153ca2650e4c4b2c8f86eb31d0b99ef17974e0217c04f9a6e5 diff --git a/crates/ide-plugin/tests/fixtures/jobs/plugin.toml b/crates/ide-plugin/tests/fixtures/jobs/plugin.toml index 7453c9ec5..3bd3396e8 100644 --- a/crates/ide-plugin/tests/fixtures/jobs/plugin.toml +++ b/crates/ide-plugin/tests/fixtures/jobs/plugin.toml @@ -25,6 +25,11 @@ id = "follow" title = "Jobs: Follow Selected Job" category = "Jobs" +[[contributes.commands]] +id = "selection" +title = "Jobs: Show Selected Paths" +category = "Jobs" + [contributes.settings] title = "Jobs" schema = "settings.schema.json" diff --git a/crates/ide-plugin/tests/fixtures/jobs/src/lib.rs b/crates/ide-plugin/tests/fixtures/jobs/src/lib.rs index 131ef2cce..7e4e0070f 100644 --- a/crates/ide-plugin/tests/fixtures/jobs/src/lib.rs +++ b/crates/ide-plugin/tests/fixtures/jobs/src/lib.rs @@ -4,7 +4,7 @@ use openidea_plugin::host; use openidea_plugin::views::{Body, Button, TreeItem, View, ViewEvent, Viewport, Views}; -use openidea_plugin::{Error, Invocation, Plugin, register_plugin}; +use openidea_plugin::{Error, Invocation, NotificationLevel, Plugin, register_plugin}; pub const VIEW: &str = "jobs"; @@ -48,6 +48,11 @@ impl Plugin for Jobs { host::open_url(arg.unwrap_or_default())?; Ok(None) } + "selection" => { + let paths = ctx.selected_paths().join("\n"); + host::notify(NotificationLevel::Info, "Selected paths", &paths); + Ok(Some(paths)) + } _ => Err(Error::NotFound(format!("command {id}"))), } } diff --git a/crates/ide-plugin/tests/fixtures/misbehave/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/misbehave/dist/plugin.wasm.sources index b6b627bd4..4310ad831 100644 --- a/crates/ide-plugin/tests/fixtures/misbehave/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/misbehave/dist/plugin.wasm.sources @@ -1 +1 @@ -1d8561e04cb777efbafc6f64963473b9b69654a72ebb2be34f49c6c53c6f76e2 +b697a2aa34b0de05db3e89bb8449ab6619119fa003b19fabe36b0932246a26fe diff --git a/plugins/README.md b/plugins/README.md index fdce7ff71..43131745d 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -364,7 +364,7 @@ then run `openidea-plugin check`. | `host::setting(key)`, `setting_bool`, `setting_string`, `setting_number` | `settings.get` | The user's value (project over global) or the schema's default. | `run-command` receives an optional string argument: `executeAction("plugin..", "arg")` -from another feature, or nothing when run from Search Everywhere / a shortcut. +from another feature, or nothing when run from Search Everywhere / a shortcut. `ctx.selected_paths()` lists the files and folders a Project view or editor tab menu ran the command on (the argument is the first of them); it is empty otherwise. No capability is needed. ### Events diff --git a/plugins/official/ai-completion/dist/plugin.wasm.sources b/plugins/official/ai-completion/dist/plugin.wasm.sources index ae7c9f9e4..50d9693cc 100644 --- a/plugins/official/ai-completion/dist/plugin.wasm.sources +++ b/plugins/official/ai-completion/dist/plugin.wasm.sources @@ -1 +1 @@ -5a707c0a9efb6f98f43d93f1b4207cc0ab21ac84d693e0fd9981d143e7552a1c +f63e6547849ce8f5a0de4384eb44a903583135dea7e5a82829daf3a395e98465 diff --git a/plugins/official/checkstyle/dist/plugin.wasm.sources b/plugins/official/checkstyle/dist/plugin.wasm.sources index 9b9e9e3da..5d43c4829 100644 --- a/plugins/official/checkstyle/dist/plugin.wasm.sources +++ b/plugins/official/checkstyle/dist/plugin.wasm.sources @@ -1 +1 @@ -34728307b2054d6a494040f0523caf6f62b96c0f67f5b5e9a7e4508370e79419 +af6dbfa8c34e5cb62851f02479f86350ee915c18028993331b4372a739074b80 diff --git a/plugins/official/csv-editor/dist/plugin.wasm.sources b/plugins/official/csv-editor/dist/plugin.wasm.sources index e0795c324..f0f217a64 100644 --- a/plugins/official/csv-editor/dist/plugin.wasm.sources +++ b/plugins/official/csv-editor/dist/plugin.wasm.sources @@ -1 +1 @@ -c8de2df9607fef2a7e75023e78db06ac83de8598a43632b684756d4e0df6b51b +920da064d50f59aa34825f5dc8c59442b7185891bbded8f7f1c87c8192c093ce diff --git a/plugins/official/git-toolbox/dist/plugin.wasm.sources b/plugins/official/git-toolbox/dist/plugin.wasm.sources index 4ed042a00..61ca0e559 100644 --- a/plugins/official/git-toolbox/dist/plugin.wasm.sources +++ b/plugins/official/git-toolbox/dist/plugin.wasm.sources @@ -1 +1 @@ -2523894a6fb5a75d29429ab096fdd9acf150c05eb4ea13c9e92920c8d8ca4459 +7c9279246766fd92f854fe8dc33f0165236f616a448782dfa59ea9c05463cf8a diff --git a/plugins/official/graphviz/dist/plugin.wasm.sources b/plugins/official/graphviz/dist/plugin.wasm.sources index 07811c63d..dc541c60e 100644 --- a/plugins/official/graphviz/dist/plugin.wasm.sources +++ b/plugins/official/graphviz/dist/plugin.wasm.sources @@ -1 +1 @@ -b534a088809e38ff210ed169b6968f3e793fa0fdcb12d022d2bcee5a58834514 +55cc148dc0eddf924e97074a46707736e563b0fe6d348a09610b1822ed085d6b diff --git a/plugins/official/key-promoter/dist/plugin.wasm.sources b/plugins/official/key-promoter/dist/plugin.wasm.sources index e0b0643a4..a47653ca4 100644 --- a/plugins/official/key-promoter/dist/plugin.wasm.sources +++ b/plugins/official/key-promoter/dist/plugin.wasm.sources @@ -1 +1 @@ -145bad8868c73368d4e83087861293fad309aaa87263bb12508a6acf693c25da +eda75943217c5273773ef0f2934c20d6bda50411670d99145b50e45d104509cc diff --git a/plugins/official/license-header/dist/plugin.wasm.sources b/plugins/official/license-header/dist/plugin.wasm.sources index a280e4a5c..a0c7e5687 100644 --- a/plugins/official/license-header/dist/plugin.wasm.sources +++ b/plugins/official/license-header/dist/plugin.wasm.sources @@ -1 +1 @@ -f2f4a7d3fbe4b86369e35969782dd66150df6ae4451e375122d3c1b232caeac1 +5a83f6c626f01c80910e1e74e6452bc2bbef8de3fe402d5540f506dc6dd02ed0 diff --git a/plugins/official/mybatis/dist/plugin.wasm b/plugins/official/mybatis/dist/plugin.wasm index 5c7d59bc8ff04641b4529ed06619272452241a62..4ebdb24f6648a848e83c5a19eeacadd01ba05c65 100644 GIT binary patch delta 9282 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 delta 9416 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 diff --git a/plugins/official/mybatis/dist/plugin.wasm.sources b/plugins/official/mybatis/dist/plugin.wasm.sources index 26af2f4b7..8ba5cafe9 100644 --- a/plugins/official/mybatis/dist/plugin.wasm.sources +++ b/plugins/official/mybatis/dist/plugin.wasm.sources @@ -1 +1 @@ -5e7f7adc2c49503d2278ed2a446f18f69561ce51df264100f77f50d69804c277 +3984d6b51bea7212bbabadc2a5a632c4cba7a2a988033fa7918e33c203fcfa23 diff --git a/plugins/official/regex-tester/dist/plugin.wasm.sources b/plugins/official/regex-tester/dist/plugin.wasm.sources index 46f075233..6b356d89a 100644 --- a/plugins/official/regex-tester/dist/plugin.wasm.sources +++ b/plugins/official/regex-tester/dist/plugin.wasm.sources @@ -1 +1 @@ -01c6f9e9b1e76b3328294f6a31ba4d8f3545d04a4fa56cc8069c59aa37a13f27 +2a70078e8edf29c9a4b590d661a1a67cb605dca3781a89ffcf6dc8acb0d183b0 diff --git a/plugins/official/solidity/dist/plugin.wasm.sources b/plugins/official/solidity/dist/plugin.wasm.sources index 6c4e41b3c..0cf74655f 100644 --- a/plugins/official/solidity/dist/plugin.wasm.sources +++ b/plugins/official/solidity/dist/plugin.wasm.sources @@ -1 +1 @@ -745e791fe2fde2b4d7b58b40a33a8037999aac947865a8f4a5127021b7f16156 +4449ee6a1ccee1a71ebed1073bb4e30df1686ebd0a2812bdd579ad876d0a43cf diff --git a/plugins/official/string-tools/dist/plugin.wasm.sources b/plugins/official/string-tools/dist/plugin.wasm.sources index 5e33d0308..eb256aec5 100644 --- a/plugins/official/string-tools/dist/plugin.wasm.sources +++ b/plugins/official/string-tools/dist/plugin.wasm.sources @@ -1 +1 @@ -4fff5b261e8255aa2dac8342694dee129ea4a8c9520105c719d0c16a16846517 +adf2ab3e93e0147350b6db899ec2e44e07862e99f49609aecbc2b866e5282c3d diff --git a/sdk/rust/openidea-plugin/src/host.rs b/sdk/rust/openidea-plugin/src/host.rs index 6b93a1e7e..8c796e51a 100644 --- a/sdk/rust/openidea-plugin/src/host.rs +++ b/sdk/rust/openidea-plugin/src/host.rs @@ -117,6 +117,10 @@ mod imp { i.editor() } + pub fn invocation_selected_paths(i: InvocationRef<'_>) -> Vec { + i.selected_paths() + } + pub fn editor_path(e: &EditorRes) -> String { e.path() } @@ -327,6 +331,12 @@ impl<'a> Invocation<'a> { pub fn editor(&self) -> Option { imp::invocation_editor(self.inner).map(|inner| TextEditor { inner }) } + + /// The files and folders the command was run on, as absolute paths. Empty when it was not + /// run on any, or when the menu was for a tab without a file (a virtual document). + pub fn selected_paths(&self) -> Vec { + imp::invocation_selected_paths(self.inner) + } } /// A project open in the IDE. diff --git a/sdk/rust/openidea-plugin/src/mock.rs b/sdk/rust/openidea-plugin/src/mock.rs index a011569eb..28332df01 100644 --- a/sdk/rust/openidea-plugin/src/mock.rs +++ b/sdk/rust/openidea-plugin/src/mock.rs @@ -56,6 +56,7 @@ struct State { other_projects: Vec, files: BTreeMap, editor: Option, + selected_paths: Vec, notifications: Vec, logs: Vec<(LogLevel, String)>, status: BTreeMap, @@ -144,6 +145,11 @@ impl MockHost { self.state.borrow_mut().files.insert(path.to_owned(), text.to_owned()); } + /// Sets the paths invocations report as selected. + pub fn select_paths(&self, paths: &[&str]) { + self.state.borrow_mut().selected_paths = paths.iter().map(|p| (*p).to_owned()).collect(); + } + /// Focuses an editor on `text` with selections `(line, col, line, col)`. pub fn open_editor(&self, path: &str, language: &str, text: &str, selections: &[(u32, u32, u32, u32)]) { let selections = @@ -473,6 +479,10 @@ pub mod current { }) } + pub fn invocation_selected_paths(_: InvocationRef<'_>) -> Vec { + with(|s| s.selected_paths.clone()) + } + pub fn editor_path(_: &EditorRes) -> String { editor(|_, e| e.path.clone()) } @@ -717,6 +727,14 @@ mod tests { use super::*; use crate::host; + #[test] + fn invocations_carry_selected_paths() { + let host = MockHost::install(); + assert!(host.invocation().selected_paths().is_empty()); + host.select_paths(&["/p/a", "/p/b"]); + assert_eq!(host.invocation().selected_paths(), ["/p/a", "/p/b"]); + } + #[test] fn records_and_enforces() { let mock = MockHost::install(); diff --git a/sdk/rust/openidea-plugin/wit/workspace.wit b/sdk/rust/openidea-plugin/wit/workspace.wit index 4563344d9..aa7214fb1 100644 --- a/sdk/rust/openidea-plugin/wit/workspace.wit +++ b/sdk/rust/openidea-plugin/wit/workspace.wit @@ -59,6 +59,11 @@ interface invocation { project: func() -> option; /// The window's focused editor. editor: func() -> option; + /// The files and folders the command was run on (a multi-selection in + /// a file tree, for instance), as absolute paths. + /// Empty when it was not run on any, or when the menu was for a tab + /// without a file (a virtual document). + selected-paths: func() -> list; } } diff --git a/src-tauri/src/commands/plugin.rs b/src-tauri/src/commands/plugin.rs index 989c906e1..5c38e419a 100644 --- a/src-tauri/src/commands/plugin.rs +++ b/src-tauri/src/commands/plugin.rs @@ -325,6 +325,7 @@ pub async fn plugin_run_command( command_id: String, arg: Option, editor: Option, + paths: Option>, ) -> IdeResult> { let host = state.plugins.clone(); let project = state.project(window.label()).ok(); @@ -332,6 +333,7 @@ pub async fn plugin_run_command( client: Some(window.label().to_owned()), workspace_root: project.as_ref().map(|p| p.root.clone()), editor: editor.map(PluginEditor::context), + selected_paths: paths.unwrap_or_default(), }; blocking(move || host.run_command(&plugin_id, &command_id, arg.as_deref(), invocation)).await } diff --git a/src/bindings.ts b/src/bindings.ts index 7696f16ed..17e583d0c 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -1023,7 +1023,7 @@ export const commands = { path: string, language: string, selections: Range[], -} | null) => typedError(__TAURI_INVOKE("plugin_run_command", { pluginId, commandId, arg, editor })), +} | null, paths: string[] | null) => typedError(__TAURI_INVOKE("plugin_run_command", { pluginId, commandId, arg, editor, paths })), /** Workbench events from this window (only the kinds of `plugin_event_kinds`). */ pluginPostEvents: (events: WorkbenchEvent[]) => __TAURI_INVOKE("plugin_post_events", { events }), /** Kinds of workbench events plugins want (re-fetch on `subscriptionsChanged`). */ diff --git a/src/features/macros/model.test.ts b/src/features/macros/model.test.ts index 37fdf6466..02c948813 100644 --- a/src/features/macros/model.test.ts +++ b/src/features/macros/model.test.ts @@ -57,7 +57,7 @@ describe("macros", () => { localStorage.setItem( STORAGE_KEY, JSON.stringify({ - last: [{ kind: "text", text: "a" }, { kind: "action" }, { kind: "action", id: "x", arg: {} }], + last: [{ kind: "text", text: "a" }, { kind: "action" }, { kind: "action", id: "x", arg: { a: { b: 1 } } }], saved: [{ name: "" }], }), ); @@ -96,7 +96,6 @@ describe("macros", () => { expect(storableArg("x")).toBe("x"); expect(storableArg(2)).toBe(2); expect(storableArg(undefined)).toBeUndefined(); - expect(storableArg({ path: "/a" })).toBeNull(); expect(storableArg(Number.NaN)).toBeNull(); const title = (id: string) => (id === "a" ? "Action A" : id); expect(describeStep({ kind: "action", id: "a", arg: "x" }, title)).toBe("Action A (x)"); @@ -104,6 +103,29 @@ describe("macros", () => { expect(describeStep({ kind: "key", key: "Enter" }, title)).toBe("Press Enter"); }); + it("stores a selection payload and rejects anything that is not plain data", () => { + const selection = { path: "/a", paths: ["/a", "/b"] }; + expect(storableArg(selection)).toEqual(selection); + expect(storableArg(["/a"])).toEqual(["/a"]); + expect(storableArg(true)).toBe(true); + expect(storableArg({ a: { b: 1 } })).toBeNull(); + expect(storableArg([1])).toBeNull(); + expect(storableArg(new Date())).toBeNull(); + expect(storableArg(() => 1)).toBeNull(); + expect(storableArg({ n: Number.NaN })).toBeNull(); + expect(storableArg(Object.create(null))).toEqual({}); + }); + + it("round-trips an object argument through storage and describes it", () => { + const arg = { path: "/a", paths: ["/a", "/b"] }; + saveMacro("Sel", [{ kind: "action", id: "p.run", arg }]); + _reloadMacros(); + expect(useMacros.getState().saved[0]?.steps).toEqual([{ kind: "action", id: "p.run", arg }]); + expect(describeStep({ kind: "action", id: "p.run", arg }, (id) => id)).toBe("p.run (/a)"); + expect(describeStep({ kind: "action", id: "p.run", arg: ["/a", "/b"] }, (id) => id)).toBe("p.run (/a, /b)"); + expect(describeStep({ kind: "action", id: "p.run", arg: { n: 1 } }, (id) => id)).toBe('p.run ({"n":1})'); + }); + it("names the editor keys a macro records", () => { const k = (key: string, mods: Partial> = {}) => editorKeyName({ key, altKey: false, ctrlKey: false, metaKey: false, shiftKey: false, ...mods }); diff --git a/src/features/macros/model.ts b/src/features/macros/model.ts index 061ab2dce..2df71dc4e 100644 --- a/src/features/macros/model.ts +++ b/src/features/macros/model.ts @@ -6,9 +6,26 @@ */ import { create } from "zustand"; +/** Plain data an action can be run with: primitives, string lists, or a flat object of those. */ +export type MacroArg = MacroValue | { [key: string]: MacroValue }; +type MacroValue = string | number | boolean | string[]; + +const isMacroValue = (v: unknown): v is MacroValue => + typeof v === "string" || + typeof v === "boolean" || + (typeof v === "number" && Number.isFinite(v)) || + (Array.isArray(v) && v.every((x) => typeof x === "string")); + +function isMacroArg(v: unknown): v is MacroArg { + if (isMacroValue(v)) return true; + if (!v || typeof v !== "object") return false; + const proto = Object.getPrototypeOf(v); + return (proto === Object.prototype || proto === null) && Object.values(v).every(isMacroValue); +} + /** An editor key replayed as a CodeMirror command (see `editorKeys.ts`). */ export type MacroStep = - | { kind: "action"; id: string; arg?: string | number | boolean } + | { kind: "action"; id: string; arg?: MacroArg } | { kind: "text"; text: string } | { kind: "key"; key: string }; @@ -32,8 +49,7 @@ export const STORAGE_KEY = "openidea.macros.v1"; function isStep(v: unknown): v is MacroStep { if (!v || typeof v !== "object") return false; const s = v as Record; - if (s.kind === "action") - return typeof s.id === "string" && ["undefined", "string", "number", "boolean"].includes(typeof s.arg); + if (s.kind === "action") return typeof s.id === "string" && (s.arg === undefined || isMacroArg(s.arg)); if (s.kind === "text") return typeof s.text === "string"; return s.kind === "key" && typeof s.key === "string"; } @@ -82,12 +98,10 @@ export function recordStep(step: MacroStep) { useMacros.setState({ recording: next }); } -/** An action's argument when it can be stored (plain values only). */ -export function storableArg(arg: unknown): string | number | boolean | undefined | null { +/** An action's argument when it can be stored (plain data only). */ +export function storableArg(arg: unknown): MacroArg | undefined | null { if (arg === undefined) return undefined; - if (typeof arg === "string" || typeof arg === "boolean" || (typeof arg === "number" && Number.isFinite(arg))) - return arg; - return null; + return isMacroArg(arg) ? arg : null; } export function startRecording() { @@ -127,11 +141,17 @@ export function deleteMacro(name: string) { persist(); } +function describeArg(arg: MacroArg): string { + if (Array.isArray(arg)) return arg.join(", "); + if (typeof arg === "object") return typeof arg.path === "string" ? arg.path : JSON.stringify(arg); + return String(arg); +} + /** One line per step, for Edit Macros. */ export function describeStep(step: MacroStep, title: (id: string) => string): string { switch (step.kind) { case "action": - return step.arg === undefined ? title(step.id) : `${title(step.id)} (${String(step.arg)})`; + return step.arg === undefined ? title(step.id) : `${title(step.id)} (${describeArg(step.arg)})`; case "text": return `Type "${step.text.replace(/\n/g, "↵")}"`; case "key": diff --git a/src/features/plugins/__tests__/plugins.test.tsx b/src/features/plugins/__tests__/plugins.test.tsx index 5e0ab8a88..5606169dc 100644 --- a/src/features/plugins/__tests__/plugins.test.tsx +++ b/src/features/plugins/__tests__/plugins.test.tsx @@ -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 }); diff --git a/src/features/plugins/__tests__/views.test.tsx b/src/features/plugins/__tests__/views.test.tsx index 1bff35352..f4a30924d 100644 --- a/src/features/plugins/__tests__/views.test.tsx +++ b/src/features/plugins/__tests__/views.test.tsx @@ -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(); }); diff --git a/src/features/plugins/backend.ts b/src/features/plugins/backend.ts index 753bf93c4..84476bdee 100644 --- a/src/features/plugins/backend.ts +++ b/src/features/plugins/backend.ts @@ -53,6 +53,7 @@ export interface PluginsBackend { commandId: string, arg: string | null, editor: PluginEditor | null, + paths: string[], ): Promise; /** Workbench events plugins subscribed to (see `eventKinds`). */ postEvents(events: WorkbenchEvent[]): Promise; @@ -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); }, diff --git a/src/features/plugins/points.tsx b/src/features/plugins/points.tsx index 0e033f714..a2820ff90 100644 --- a/src/features/plugins/points.tsx +++ b/src/features/plugins/points.tsx @@ -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, diff --git a/src/features/plugins/store.ts b/src/features/plugins/store.ts index 047a4ce16..da26d38b6 100644 --- a/src/features/plugins/store.ts +++ b/src/features/plugins/store.ts @@ -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; diff --git a/src/features/plugins/uiPoints.tsx b/src/features/plugins/uiPoints.tsx index eb0f05292..d4139cf62 100644 --- a/src/features/plugins/uiPoints.tsx +++ b/src/features/plugins/uiPoints.tsx @@ -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; } diff --git a/wit/workspace.wit b/wit/workspace.wit index 4563344d9..aa7214fb1 100644 --- a/wit/workspace.wit +++ b/wit/workspace.wit @@ -59,6 +59,11 @@ interface invocation { project: func() -> option; /// The window's focused editor. editor: func() -> option; + /// The files and folders the command was run on (a multi-selection in + /// a file tree, for instance), as absolute paths. + /// Empty when it was not run on any, or when the menu was for a tab + /// without a file (a virtual document). + selected-paths: func() -> list; } }