diff --git a/crates/ide-host/src/bin/openidea-plugin/main.rs b/crates/ide-host/src/bin/openidea-plugin/main.rs index c0bee922b..7d07cbef3 100644 --- a/crates/ide-host/src/bin/openidea-plugin/main.rs +++ b/crates/ide-host/src/bin/openidea-plugin/main.rs @@ -33,7 +33,9 @@ usage: openidea-plugin [options] commands: new [--template T] [--id ID] [--name NAME] [--sdk PATH] - create a plugin; T = command (default), theme, icon-theme, language + create a plugin; T = command (default), theme, icon-theme, language. A Rust + plugin depends on the SDK of this tool's release (git); --sdk PATH uses a + local sdk/rust/openidea-plugin instead build [dir] [--debug] compile a Rust plugin to dist/plugin.wasm and check it check [dir|package] validate the package (folder or .oiplugin) as the IDE would install it link [dir] load the folder in the IDE without installing it @@ -177,7 +179,7 @@ fn cmd_new(args: &[String]) -> Result<(), String> { std::fs::create_dir_all(&dir).map_err(|e| format!("{}: {e}", dir.display()))?; let dir = canonical(&dir)?; let sdk = match template { - templates::Template::Command => Some(sdk_path(a.value("sdk"), &dir)?), + templates::Template::Command => Some(sdk_dependency(a.value("sdk"), &dir)?), _ => None, }; let vars = templates::Vars { id: &id, name: &name, sdk: sdk.as_deref() }; @@ -232,12 +234,16 @@ fn title_case(s: &str) -> String { .join(" ") } -/// The SDK dependency path written into a new plugin's Cargo.toml: `--sdk`, -/// or the SDK of the source tree this tool was built from. -fn sdk_path(explicit: Option<&str>, plugin_dir: &Path) -> Result { +/// The SDK dependency written into a new plugin's Cargo.toml: by path with `--sdk`, or +/// when the plugin is inside the source tree this tool was built from; otherwise the +/// repository at this tool's release tag. +fn sdk_dependency(explicit: Option<&str>, plugin_dir: &Path) -> Result { let sdk = match explicit { Some(p) => PathBuf::from(p), - None => PathBuf::from(concat!(env!("CARGO_MANIFEST_DIR"), "/../../sdk/rust/openidea-plugin")), + None => match dunce::canonicalize(concat!(env!("CARGO_MANIFEST_DIR"), "/../..")) { + Ok(tree) if plugin_dir.starts_with(&tree) => tree.join("sdk/rust/openidea-plugin"), + _ => return Ok(format!("{{ git = \"{REPOSITORY}\", tag = \"v{}\" }}", env!("CARGO_PKG_VERSION"))), + }, }; let sdk = dunce::canonicalize(&sdk).map_err(|_| { format!("the plugin SDK was not found at {}; pass --sdk ", sdk.display()) @@ -246,9 +252,11 @@ fn sdk_path(explicit: Option<&str>, plugin_dir: &Path) -> Result return Err(format!("{} is not the openidea-plugin SDK (no Cargo.toml)", sdk.display())); } let path = relative(plugin_dir, &sdk).unwrap_or(sdk); - Ok(path.to_string_lossy().replace('\\', "/")) + Ok(format!("{{ path = \"{}\" }}", path.to_string_lossy().replace('\\', "/"))) } +const REPOSITORY: &str = "https://github.com/l0ng-ai/openidea"; + /// `to` relative to `from` when they share a root (keeps packages in a repo movable). fn relative(from: &Path, to: &Path) -> Option { let (f, t): (Vec<_>, Vec<_>) = (from.components().collect(), to.components().collect()); @@ -781,6 +789,27 @@ mod tests { assert_eq!(relative(Path::new("/x/y"), Path::new("/z")), None); } + #[test] + fn new_plugins_depend_on_the_sdk_by_path_only_in_its_source_tree() { + let tree = dunce::canonicalize(concat!(env!("CARGO_MANIFEST_DIR"), "/../..")).unwrap(); + assert_eq!( + sdk_dependency(None, &tree.join("plugins/official/new")).unwrap(), + "{ path = \"../../../sdk/rust/openidea-plugin\" }" + ); + let outside = dunce::canonicalize(std::env::temp_dir()).unwrap().join("my-plugin"); + assert_eq!( + sdk_dependency(None, &outside).unwrap(), + format!("{{ git = \"https://github.com/l0ng-ai/openidea\", tag = \"v{}\" }}", env!("CARGO_PKG_VERSION")) + ); + let sdk = tree.join("sdk/rust/openidea-plugin"); + assert!(sdk_dependency(Some(sdk.to_str().unwrap()), &outside).unwrap().starts_with("{ path = ")); + assert!( + sdk_dependency(Some(tree.join("wit").to_str().unwrap()), &outside) + .unwrap_err() + .contains("not the openidea-plugin SDK") + ); + } + /// A theme package `id` signed by `publisher` with `key`, packed into `out`. fn signed_theme(tmp: &Path, id: &str, publisher: &str, key: &str, out: &Path) { signed_theme_version(tmp, id, "0.1.0", "^1.0", publisher, key, out); @@ -901,13 +930,44 @@ mod tests { assert!(err.contains("unknown publisher"), "{err}"); } + /// `cargo test` of a new Rust plugin (its own tests against the mock host) with this SDK. + #[test] + fn the_command_template_builds_against_the_sdk() { + let dir = std::env::temp_dir().join(format!("openidea-plugin-template-build-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let dir = dunce::canonicalize(&dir).unwrap(); + let plugin = dir.join("plugin"); + let sdk = concat!(env!("CARGO_MANIFEST_DIR"), "/../../sdk/rust/openidea-plugin"); + let sdk = sdk_dependency(Some(sdk), &plugin).unwrap(); + let vars = templates::Vars { id: "acme.sample", name: "Sample", sdk: Some(&sdk) }; + for (rel, content) in templates::render(templates::Template::Command, &vars) { + let path = plugin.join(rel); + std::fs::create_dir_all(path.parent().unwrap()).unwrap(); + std::fs::write(path, content).unwrap(); + } + let out = Command::new(std::env::var("CARGO").unwrap_or_else(|_| "cargo".into())) + .args(["test", "--offline", "--manifest-path"]) + .arg(plugin.join("Cargo.toml")) + .env("CARGO_TARGET_DIR", dir.join("target")) + .output() + .unwrap(); + assert!( + out.status.success(), + "{}{}", + String::from_utf8_lossy(&out.stdout), + String::from_utf8_lossy(&out.stderr) + ); + std::fs::remove_dir_all(&dir).unwrap(); + } + #[test] fn every_template_is_a_valid_package() { for t in templates::Template::ALL { let dir = std::env::temp_dir().join(format!("openidea-plugin-template-{}-{}", t.label(), std::process::id())); let _ = std::fs::remove_dir_all(&dir); - let vars = templates::Vars { id: "acme.sample", name: "Sample", sdk: Some("../sdk") }; + let vars = templates::Vars { id: "acme.sample", name: "Sample", sdk: Some("{ path = \"../sdk\" }") }; for (rel, content) in templates::render(t, &vars) { let path = dir.join(rel); std::fs::create_dir_all(path.parent().unwrap()).unwrap(); diff --git a/crates/ide-host/src/bin/openidea-plugin/templates.rs b/crates/ide-host/src/bin/openidea-plugin/templates.rs index 6c1c04e0c..44613eeaa 100644 --- a/crates/ide-host/src/bin/openidea-plugin/templates.rs +++ b/crates/ide-host/src/bin/openidea-plugin/templates.rs @@ -32,6 +32,7 @@ impl Template { pub struct Vars<'a> { pub id: &'a str, pub name: &'a str, + /// The SDK dependency, as a TOML value (`{ path = "…" }`). pub sdk: Option<&'a str>, } @@ -118,7 +119,7 @@ publish = false crate-type = ["cdylib", "rlib"] [dependencies] -openidea-plugin = { path = "__SDK__" } +openidea-plugin = __SDK__ [profile.release] opt-level = "s" @@ -130,7 +131,7 @@ panic = "abort" const COMMAND_LIB: &str = r#"//! __NAME__: an OpenIDEA plugin. -use openidea_plugin::{Invocation, NotificationLevel, Plugin, host, register_plugin}; +use openidea_plugin::{Error, Invocation, NotificationLevel, Plugin, host, register_plugin}; #[derive(Default)] pub struct Main { @@ -138,7 +139,7 @@ pub struct Main { } impl Plugin for Main { - fn run_command(&mut self, id: &str, ctx: &Invocation, _arg: Option<&str>) -> Result, String> { + fn run_command(&mut self, id: &str, ctx: &Invocation, _arg: Option<&str>) -> Result, Error> { match id { "say-hello" => { // Instance state lasts until the plugin is idle for a while; @@ -153,7 +154,7 @@ impl Plugin for Main { let text = editor.document()?.text; Ok(Some(format!("{} lines in {}", text.lines().count(), editor.path()))) } - _ => Err(format!("unknown command {id}")), + _ => Err(format!("unknown command {id}").into()), } } } diff --git a/plugins/README.md b/plugins/README.md index a579afb6d..6743ab671 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -44,7 +44,8 @@ The plugin API is defined in [`wit/`](../wit/) (`openidea:plugin@1.0.0`, world The `openidea-plugin` tool scaffolds, builds, checks and links plugins: ```sh -cargo install --path crates/ide-host --bin openidea-plugin # or: cargo run -p ide-host --bin openidea-plugin -- … +cargo install --locked --git https://github.com/l0ng-ai/openidea ide-host --bin openidea-plugin +# in a checkout of this repository: cargo install --path crates/ide-host --bin openidea-plugin openidea-plugin new my-plugin # Rust command plugin (SDK) openidea-plugin new my-theme --template theme # also: icon-theme, language @@ -54,6 +55,17 @@ openidea-plugin build # → dist/plugin.wasm, t openidea-plugin link # load this folder in the IDE ``` +Each release also attaches the tool for Linux x64 +(`openidea-plugin-linux-x86_64.gz`). A plugin is a project of its own: keep it +in its own repository, and list it in the community marketplace when it is +ready (§8.1). + +A Rust plugin made by `new` depends on the SDK of the tool's release, from +this repository's tag (`openidea-plugin = { git = "…", tag = "vX.Y.Z" }`); +change the tag to build against a later API. Inside a checkout of this +repository, or with `--sdk `, it depends on +that SDK by path instead. + `build` runs `cargo build --release --target wasm32-wasip2` (installing the target through rustup if needed), copies the component to the manifest's `wasm` path and runs `check`. `check` validates the package exactly as @@ -212,7 +224,7 @@ edition = "2024" crate-type = ["cdylib", "rlib"] [dependencies] -openidea-plugin = { path = "…/sdk/rust/openidea-plugin" } +openidea-plugin = { git = "https://github.com/l0ng-ai/openidea", tag = "vX.Y.Z" } # or { path = "…/sdk/rust/openidea-plugin" } [profile.release] opt-level = "s" @@ -773,8 +785,19 @@ granted, and starts the approved program. - **Project plugins:** put (or symlink) the package in `/.openidea/plugins//`. They load only in trusted projects and only after you approve their capabilities. +- **Where:** installing, removing, enabling, granting and reloading plugins + work on the welcome screen and in trusted projects. A project opened in + restricted mode can browse the marketplace and show a package's install + page, but not change which plugin code runs. - Commands appear in Search Everywhere › Actions under their category, and can be bound in Settings › Keymap like any other action. +- **Troubleshooting:** `openidea-plugin check` says why a package would be + refused. Settings › Plugins shows each plugin's state, why it failed if it + did, and its call stats (§3, _Sandbox and limits_). `host::log` lines go to + the IDE log (Help › _Show Log in Finder_ / _Explorer_ / _Files_) tagged with + the plugin id; debug and trace lines are written when the IDE is started + with `RUST_LOG=info,ide_plugin=trace`. `eprintln!` output is logged as + well, and a panic's message is shown with the crash. ## 8. Distribution