//! SDK for OpenIDEA plugins written in Rust (plugin API 1.0). //! //! ```ignore //! use openidea_plugin::{Error, Invocation, Plugin, register_plugin}; //! //! #[derive(Default)] //! struct Shout; //! //! impl Plugin for Shout { //! fn run_command(&mut self, id: &str, ctx: &Invocation, _arg: Option<&str>) -> Result, Error> { //! match id { //! "shout" => { //! let editor = ctx.editor().ok_or("no editor is focused")?; //! editor.transform_selections(|s| s.to_uppercase())?; //! Ok(None) //! } //! _ => Err(Error::NotFound(format!("command {id}"))), //! } //! } //! } //! //! register_plugin!(Shout); //! ``` //! //! On `wasm32-wasip2` the [`host`] functions and handles call the IDE through //! the `openidea:plugin@1.0.0` WIT world. On any other target they call the //! [`mock`] host, so plugin logic is tested with plain `cargo test`: //! //! ```ignore //! let host = openidea_plugin::mock::MockHost::install(); //! host.open_editor("/project/a.txt", "rust", "hi there", &[(0, 0, 0, 2)]); //! Shout.run_command("shout", &host.invocation(), None).unwrap(); //! assert_eq!(host.editor_text(), "HI there"); //! ``` //! //! Instance memory is a cache: the instance is dropped after a crash or when //! idle. Keep what must survive in [`host::storage`]. //! //! Build with `openidea-plugin build` (or `cargo build --release --target //! wasm32-wasip2`); see `plugins/README.md`. pub mod host; #[cfg(not(target_arch = "wasm32"))] pub mod mock; pub mod providers; pub mod syntax; pub mod text; mod types; pub mod views; pub use host::{Invocation, Project, Task, TextEditor}; pub use types::*; #[cfg(target_arch = "wasm32")] #[doc(hidden)] pub mod bindings { wit_bindgen::generate!({ world: "plugin", path: "wit", pub_export_macro: true, export_macro_name: "__export_plugin_world", default_bindings_module: "openidea_plugin::bindings", }); } /// The `syntax` world: only the `syntax-provider` export, merged with /// `plugin` into one component (world `plugin-syntax`). #[cfg(target_arch = "wasm32")] #[doc(hidden)] pub mod syntax_bindings { wit_bindgen::generate!({ world: "syntax", path: "wit", pub_export_macro: true, export_macro_name: "__export_syntax_world", default_bindings_module: "openidea_plugin::syntax_bindings", }); } /// The `editor-providers` world: only the `providers` export, merged with /// `plugin` (world `plugin-providers`). #[cfg(target_arch = "wasm32")] #[doc(hidden)] pub mod providers_bindings { wit_bindgen::generate!({ world: "editor-providers", path: "wit", pub_export_macro: true, export_macro_name: "__export_editor_providers_world", default_bindings_module: "openidea_plugin::providers_bindings", with: { "openidea:plugin/types@1.0.0": crate::bindings::openidea::plugin::types, }, }); } /// The `views-provider` world: only the `view-provider` export, merged with /// `plugin` (world `plugin-views`). #[cfg(target_arch = "wasm32")] #[doc(hidden)] pub mod views_bindings { wit_bindgen::generate!({ world: "views-provider", path: "wit", pub_export_macro: true, export_macro_name: "__export_views_world", default_bindings_module: "openidea_plugin::views_bindings", with: { "openidea:plugin/types@1.0.0": crate::bindings::openidea::plugin::types, "openidea:plugin/workspace@1.0.0": crate::bindings::openidea::plugin::workspace, "openidea:plugin/editor@1.0.0": crate::bindings::openidea::plugin::editor, "openidea:plugin/invocation@1.0.0": crate::bindings::openidea::plugin::invocation, }, }); } /// The plugin API version this SDK targets. pub const API_VERSION: &str = "1.0"; /// A plugin. One instance lives as long as the plugin's Wasm instance (until /// it is disabled, crashes or is reclaimed when idle). pub trait Plugin: 'static { /// Called once when the instance is created: subscribe to events and /// restore state from [`host::storage`]. An error fails the activation. fn activate(&mut self, _ctx: &Context) -> Result<(), Error> { Ok(()) } /// Called before the instance is dropped. fn deactivate(&mut self) {} /// Runs one of the plugin's commands; `Ok(Some(text))` is shown to the user. /// A plain message (`Err("…".into())`, or `?` on a `String` error) is /// [`Error::Failed`]; return [`Error::Cancelled`] when the user gave up. fn run_command(&mut self, id: &str, ctx: &Invocation, arg: Option<&str>) -> Result, Error>; /// A batch of subscribed events (see [`host::subscribe`]), oldest first. fn handle_events(&mut self, _events: &[Event], _ctx: &Invocation) {} } /// Exports `T` (a [`Plugin`] + `Default`) as the plugin's component. With /// `syntax`, `T` is also a [`syntax::SyntaxProvider`] for its languages. #[macro_export] macro_rules! register_plugin { ($plugin:ty) => { // Natively (tests) nothing is exported; keep the type "used". #[cfg(not(target_arch = "wasm32"))] const _: fn() -> $plugin = <$plugin as ::core::default::Default>::default; #[cfg(target_arch = "wasm32")] const _: () = { struct __OpenideaComponent; $crate::__plugin_exports!($plugin, __OpenideaComponent, $crate::bindings); $crate::bindings::__export_plugin_world!(__OpenideaComponent with_types_in $crate::bindings); }; }; ($plugin:ty, providers) => { #[cfg(not(target_arch = "wasm32"))] const _: fn() -> $plugin = <$plugin as ::core::default::Default>::default; #[cfg(target_arch = "wasm32")] const _: () = { struct __OpenideaComponent; $crate::__plugin_exports!($plugin, __OpenideaComponent, $crate::bindings); $crate::__providers_exports!($plugin, __OpenideaComponent); $crate::bindings::__export_plugin_world!(__OpenideaComponent with_types_in $crate::bindings); }; }; ($plugin:ty, views) => { #[cfg(not(target_arch = "wasm32"))] const _: fn() -> $plugin = <$plugin as ::core::default::Default>::default; #[cfg(target_arch = "wasm32")] const _: () = { struct __OpenideaComponent; $crate::__plugin_exports!($plugin, __OpenideaComponent, $crate::bindings); $crate::__views_exports!($plugin, __OpenideaComponent); $crate::bindings::__export_plugin_world!(__OpenideaComponent with_types_in $crate::bindings); }; }; ($plugin:ty, providers, views) => { #[cfg(not(target_arch = "wasm32"))] const _: fn() -> $plugin = <$plugin as ::core::default::Default>::default; #[cfg(target_arch = "wasm32")] const _: () = { struct __OpenideaComponent; $crate::__plugin_exports!($plugin, __OpenideaComponent, $crate::bindings); $crate::__providers_exports!($plugin, __OpenideaComponent); $crate::__views_exports!($plugin, __OpenideaComponent); $crate::bindings::__export_plugin_world!(__OpenideaComponent with_types_in $crate::bindings); }; }; ($plugin:ty, syntax) => { #[cfg(not(target_arch = "wasm32"))] const _: fn() -> $plugin = <$plugin as ::core::default::Default>::default; #[cfg(target_arch = "wasm32")] const _: () = { struct __OpenideaComponent; $crate::__plugin_exports!($plugin, __OpenideaComponent, $crate::bindings); impl $crate::syntax_bindings::exports::openidea::plugin::syntax_provider::Guest for __OpenideaComponent { fn highlights( language: ::std::string::String, text: ::std::string::String, ) -> ::std::vec::Vec<$crate::syntax_bindings::exports::openidea::plugin::syntax_provider::Token> { $crate::__rt::highlights::<$plugin>(&language, &text) } fn outline( language: ::std::string::String, text: ::std::string::String, ) -> ::std::vec::Vec<$crate::syntax_bindings::exports::openidea::plugin::syntax_provider::Symbol> { $crate::__rt::outline::<$plugin>(&language, &text) } fn folding( language: ::std::string::String, text: ::std::string::String, ) -> ::std::vec::Vec<$crate::syntax_bindings::exports::openidea::plugin::syntax_provider::Fold> { $crate::__rt::folding::<$plugin>(&language, &text) } } $crate::bindings::__export_plugin_world!(__OpenideaComponent with_types_in $crate::bindings); $crate::syntax_bindings::__export_syntax_world!(__OpenideaComponent with_types_in $crate::syntax_bindings); }; }; } /// `register_plugin!(T, providers)`: `T` is also a [`providers::Providers`]. #[doc(hidden)] #[macro_export] macro_rules! __providers_exports { ($plugin:ty, $component:ident) => { impl $crate::providers_bindings::exports::openidea::plugin::providers::Guest for $component { fn completion( doc: $crate::providers_bindings::exports::openidea::plugin::providers::Document, at: $crate::bindings::openidea::plugin::types::Position, trigger: ::core::option::Option<::std::string::String>, ) -> ::std::vec::Vec<$crate::providers_bindings::exports::openidea::plugin::providers::CompletionItem> { $crate::__rt::completion::<$plugin>(doc, at, trigger) } fn inline_completion( doc: $crate::providers_bindings::exports::openidea::plugin::providers::Document, at: $crate::bindings::openidea::plugin::types::Position, ) -> ::core::option::Option<$crate::providers_bindings::exports::openidea::plugin::providers::InlineSuggestion> { $crate::__rt::inline_completion::<$plugin>(doc, at) } fn code_actions( doc: $crate::providers_bindings::exports::openidea::plugin::providers::Document, selection: $crate::bindings::openidea::plugin::types::Range, ) -> ::std::vec::Vec<$crate::providers_bindings::exports::openidea::plugin::providers::CodeAction> { $crate::__rt::code_actions::<$plugin>(doc, selection) } fn resolve_code_action( doc: $crate::providers_bindings::exports::openidea::plugin::providers::Document, action: $crate::providers_bindings::exports::openidea::plugin::providers::CodeAction, ) -> ::core::result::Result< $crate::providers_bindings::exports::openidea::plugin::providers::CodeAction, $crate::bindings::openidea::plugin::types::Error, > { $crate::__rt::resolve_code_action::<$plugin>(doc, action) } fn definition( doc: $crate::providers_bindings::exports::openidea::plugin::providers::Document, at: $crate::bindings::openidea::plugin::types::Position, ) -> ::std::vec::Vec<$crate::bindings::openidea::plugin::types::Location> { $crate::__rt::definition::<$plugin>(doc, at) } fn line_markers( doc: $crate::providers_bindings::exports::openidea::plugin::providers::Document, ) -> ::std::vec::Vec<$crate::providers_bindings::exports::openidea::plugin::providers::LineMarker> { $crate::__rt::line_markers::<$plugin>(doc) } } $crate::providers_bindings::__export_editor_providers_world!($component with_types_in $crate::providers_bindings); }; } /// `register_plugin!(T, views)`: `T` is also a [`views::Views`]. #[doc(hidden)] #[macro_export] macro_rules! __views_exports { ($plugin:ty, $component:ident) => { impl $crate::views_bindings::exports::openidea::plugin::view_provider::Guest for $component { fn render( view: ::std::string::String, viewport: ::core::option::Option<$crate::views_bindings::openidea::plugin::view_types::Viewport>, ctx: &$crate::bindings::openidea::plugin::invocation::Invocation, ) -> ::core::result::Result< $crate::views_bindings::openidea::plugin::view_types::View, $crate::bindings::openidea::plugin::types::Error, > { $crate::__rt::render_view::<$plugin>(view, viewport, ctx) } fn handle( view: ::std::string::String, event: $crate::views_bindings::openidea::plugin::view_types::ViewEvent, ctx: &$crate::bindings::openidea::plugin::invocation::Invocation, ) -> ::core::result::Result<(), $crate::bindings::openidea::plugin::types::Error> { $crate::__rt::view_event::<$plugin>(view, event, ctx) } } $crate::views_bindings::__export_views_world!($component with_types_in $crate::views_bindings); }; } #[doc(hidden)] #[macro_export] macro_rules! __plugin_exports { ($plugin:ty, $component:ident, $b:path) => { impl $crate::bindings::exports::openidea::plugin::lifecycle::Guest for $component { fn activate( ctx: $crate::bindings::openidea::plugin::types::Context, ) -> ::core::result::Result<(), $crate::bindings::openidea::plugin::types::Error> { $crate::__rt::activate::<$plugin>(ctx) } fn deactivate() { $crate::__rt::deactivate::<$plugin>() } } impl $crate::bindings::exports::openidea::plugin::commands::Guest for $component { fn run( id: ::std::string::String, ctx: &$crate::bindings::openidea::plugin::invocation::Invocation, arg: ::core::option::Option<::std::string::String>, ) -> ::core::result::Result< ::core::option::Option<::std::string::String>, $crate::bindings::openidea::plugin::types::Error, > { $crate::__rt::run_command::<$plugin>(id, ctx, arg) } } impl $crate::bindings::exports::openidea::plugin::event_handler::Guest for $component { fn handle( events: ::std::vec::Vec<$crate::bindings::openidea::plugin::events::Event>, ctx: &$crate::bindings::openidea::plugin::invocation::Invocation, ) { $crate::__rt::handle_events::<$plugin>(events, ctx) } } }; } #[cfg(target_arch = "wasm32")] #[doc(hidden)] pub mod __rt { //! The single plugin instance behind the exports. use super::syntax::SyntaxProvider; use super::{Context, Invocation, Plugin}; use crate::bindings::openidea::plugin as b; use crate::syntax_bindings::exports::openidea::plugin::syntax_provider as wit; use std::any::Any; use std::cell::RefCell; thread_local! { static INSTANCE: RefCell>> = const { RefCell::new(None) }; } fn with(f: impl FnOnce(&mut T) -> R) -> R { INSTANCE.with(|cell| { let mut slot = cell.borrow_mut(); let plugin = slot.get_or_insert_with(|| Box::new(T::default())); f(plugin.downcast_mut::().expect("one plugin type per component")) }) } pub fn activate(ctx: b::types::Context) -> Result<(), b::types::Error> { let ctx = Context { plugin_id: ctx.plugin_id, plugin_version: ctx.plugin_version, api_version: ctx.api_version, capabilities: ctx.capabilities, }; with::(|p| p.activate(&ctx)).map_err(error_out) } pub fn deactivate() { with::(|p| p.deactivate()); INSTANCE.with(|cell| cell.borrow_mut().take()); } pub fn run_command( id: String, ctx: &b::invocation::Invocation, arg: Option, ) -> Result, b::types::Error> { let ctx = Invocation::__new(ctx); with::(|p| p.run_command(&id, &ctx, arg.as_deref())).map_err(error_out) } // ---- providers ---- use crate::providers as p; use crate::providers_bindings::exports::openidea::plugin::providers as w; fn doc(d: w::Document) -> p::Document { p::Document { path: d.path, language: d.language, version: d.version, text: d.text } } fn at(p: b::types::Position) -> crate::Position { crate::Position::new(p.line, p.character) } fn loc(l: crate::Location) -> b::types::Location { b::types::Location { path: l.path, range: crate::host::__range_to(l.range) } } pub fn completion( d: w::Document, pos: b::types::Position, trigger: Option, ) -> Vec { let d = doc(d); with::(|x| x.completion(&d, at(pos), trigger.as_deref())) .into_iter() .map(|i| w::CompletionItem { label: i.label, detail: i.detail, kind: i.kind.map(|k| k.as_str().to_owned()), insert_text: i.insert_text, snippet: i.snippet, replace: i.replace.map(crate::host::__range_to), sort_text: i.sort_text, filter_text: i.filter_text, documentation: i.documentation, additional_edits: i.additional_edits.into_iter().map(edit_out).collect(), }) .collect() } pub fn inline_completion( d: w::Document, pos: b::types::Position, ) -> Option { let d = doc(d); with::(|x| x.inline_completion(&d, at(pos))).map(|s| w::InlineSuggestion { insert_text: s.insert_text, replace: s.replace.map(crate::host::__range_to), }) } pub fn code_actions( d: w::Document, sel: b::types::Range, ) -> Vec { let d = doc(d); with::(|x| x.code_actions(&d, crate::host::__range_from(sel))).into_iter().map(action_out).collect() } pub fn resolve_code_action( d: w::Document, action: w::CodeAction, ) -> Result { let d = doc(d); with::(|x| x.resolve_code_action(&d, action_in(action))).map(action_out).map_err(error_out) } fn edit_out(e: crate::TextEdit) -> b::types::TextEdit { b::types::TextEdit { range: crate::host::__range_to(e.range), text: e.text } } fn action_out(a: p::CodeAction) -> w::CodeAction { w::CodeAction { title: a.title, kind: a.kind, preferred: a.preferred, edits: a .edits .into_iter() .map(|(path, edits)| w::FileEdit { path, edits: edits.into_iter().map(edit_out).collect() }) .collect(), data: a.data, } } fn action_in(a: w::CodeAction) -> p::CodeAction { p::CodeAction { title: a.title, kind: a.kind, preferred: a.preferred, edits: a .edits .into_iter() .map(|f| { let edits = f .edits .into_iter() .map(|e| crate::TextEdit { range: crate::host::__range_from(e.range), text: e.text }) .collect(); (f.path, edits) }) .collect(), data: a.data, } } pub fn definition( d: w::Document, pos: b::types::Position, ) -> Vec { let d = doc(d); with::(|x| x.definition(&d, at(pos))).into_iter().map(loc).collect() } pub fn line_markers(d: w::Document) -> Vec { let d = doc(d); with::(|x| x.line_markers(&d)) .into_iter() .map(|m| w::LineMarker { line: m.line, icon: m.icon, tooltip: m.tooltip, target: m.target.map(loc) }) .collect() } // ---- views ---- use crate::views as v; use crate::views_bindings::openidea::plugin::view_types as vt; fn error_out(e: crate::Error) -> b::types::Error { use b::types::Error as E; match e { crate::Error::Denied(c) => E::Denied(c), crate::Error::Stale => E::Stale, crate::Error::NotFound(m) => E::NotFound(m), crate::Error::Invalid(m) => E::Invalid(m), crate::Error::Cancelled => E::Cancelled, crate::Error::Failed(m) => E::Failed(m), } } fn view_out(view: v::View) -> vt::View { let body = match view.body { v::Body::Message(m) => vt::Body::Message(m), v::Body::Tree(items) => vt::Body::Tree( items .into_iter() .map(|i| vt::TreeItem { key: i.key, parent: i.parent, label: i.label, icon: i.icon, detail: i.detail, expandable: i.expandable, expanded: i.expanded, }) .collect(), ), v::Body::List(items) => vt::Body::List( items .into_iter() .map(|i| vt::ListItem { key: i.key, label: i.label, icon: i.icon, detail: i.detail }) .collect(), ), v::Body::Form(fields) => vt::Body::Form( fields .into_iter() .map(|f| vt::Field { key: f.key, label: f.label, value: match f.value { v::FieldValue::Text(t) => vt::FieldValue::Text(t), v::FieldValue::Multiline(t) => vt::FieldValue::Multiline(t), v::FieldValue::Checkbox(c) => vt::FieldValue::Checkbox(c), v::FieldValue::Choice(o, s) => vt::FieldValue::Choice((o, s)), v::FieldValue::Other { kind, data } => { vt::FieldValue::Other(b::types::Extension { kind, data }) } }, }) .collect(), ), v::Body::Table(t) => vt::Body::Table(vt::Table { columns: t.columns, total_rows: t.total_rows, first_row: t.first_row, rows: t.rows, editable: t.editable, }), v::Body::Html(h) => vt::Body::Html(h), v::Body::Other { kind, data } => vt::Body::Other(b::types::Extension { kind, data }), }; vt::View { toolbar: view .toolbar .into_iter() .map(|b| vt::Button { id: b.id, label: b.label, icon: b.icon, enabled: b.enabled }) .collect(), body, status: view.status, } } pub fn render_view( view: String, viewport: Option, ctx: &b::invocation::Invocation, ) -> Result { let ctx = Invocation::__new(ctx); let viewport = viewport.map(|v| v::Viewport { first_row: v.first_row, rows: v.rows }); with::(|p| p.render(&view, viewport, &ctx)).map(view_out).map_err(error_out) } pub fn view_event( view: String, event: vt::ViewEvent, ctx: &b::invocation::Invocation, ) -> Result<(), b::types::Error> { let ctx = Invocation::__new(ctx); let event = match event { vt::ViewEvent::Button(id) => v::ViewEvent::Button(id), vt::ViewEvent::Activate(k) => v::ViewEvent::Activate(k), vt::ViewEvent::Select(k) => v::ViewEvent::Select(k), vt::ViewEvent::Expand(k) => v::ViewEvent::Expand(k), vt::ViewEvent::Collapse(k) => v::ViewEvent::Collapse(k), vt::ViewEvent::Field((k, value)) => v::ViewEvent::Field(k, value), vt::ViewEvent::Cell(c) => v::ViewEvent::Cell { row: c.row, column: c.column, value: c.value }, vt::ViewEvent::InsertRow(r) => v::ViewEvent::InsertRow(r), vt::ViewEvent::DeleteRows(r) => v::ViewEvent::DeleteRows(r), vt::ViewEvent::Message(m) => v::ViewEvent::Message(m), vt::ViewEvent::Other(x) => v::ViewEvent::Other { kind: x.kind, data: x.data }, }; with::(|p| p.handle(&view, event, &ctx)).map_err(error_out) } pub fn handle_events(events: Vec, ctx: &b::invocation::Invocation) { let events: Vec = events.into_iter().map(crate::host::__event).collect(); let ctx = Invocation::__new(ctx); with::(|p| p.handle_events(&events, &ctx)) } fn offset(n: usize) -> u32 { u32::try_from(n).unwrap_or(u32::MAX) } pub fn highlights(language: &str, text: &str) -> Vec { with::(|p| p.highlights(language, text)) .into_iter() .map(|t| wit::Token { start: offset(t.range.start), end: offset(t.range.end), kind: t.kind.name().into() }) .collect() } pub fn outline(language: &str, text: &str) -> Vec { with::(|p| p.outline(language, text)) .into_iter() .map(|s| wit::Symbol { name: s.name, detail: s.detail, kind: s.kind.name().into(), start: offset(s.range.start), end: offset(s.range.end), name_start: offset(s.name_range.start), name_end: offset(s.name_range.end), parent: s.parent.map(offset), }) .collect() } pub fn folding(language: &str, text: &str) -> Vec { with::(|p| p.folding(language, text)) .into_iter() .map(|f| wit::Fold { start_line: f.start_line, end_line: f.end_line, kind: match f.kind { super::syntax::FoldKind::Region => "region", super::syntax::FoldKind::Comment => "comment", super::syntax::FoldKind::Imports => "imports", } .into(), }) .collect() } }