Файли
openidea/crates/ide-plugin/src/wit_host.rs
T
1c5a92f952 feat(plugins): plugin system with extension points, consent, Wasm runtime and plugin API 1.0 (#971)
A plugin system built on WebAssembly components (wasmtime, WASI p2), with a
typed WIT API (`openidea:[email protected]`) and a Rust SDK, shipped as a preview.

- Extension points declared in `plugin.toml` (commands, languages, themes,
  icon themes, menus, key bindings, status items, settings, views, custom
  editors, file icons and templates, run detectors, editor providers),
  validated before anything takes effect and projected onto the workbench's
  own registries: plugin themes and file icon themes are chosen in
  Settings › Appearance and Quick Switch, plugin shortcuts are edited in
  Settings › Keymap, plugin problems are grouped in Settings › Inspections.
- Capabilities with risk levels and consent at install, pinned `process:`
  programs, secrets in the OS keychain.
- Runtime: one thread per active plugin, fuel/time/memory budgets, soft and
  hard deadlines on the editing path, cancellation, crash backoff, idle
  reclaim, a precompiled-component cache; remote projects run code plugins on
  `openidea-server`.
- Web views and custom editors with a sandboxed page, a document bridge
  joined to the text editor's undo history, and split or preview layouts.
- Distribution: signed `.oiplugin` packages, a static marketplace index with
  revocation, versioned installs with rollback, install from a `.zip` or a
  folder. The release workflow signs and publishes the official plugins and
  their index, which released builds use as the default marketplace.
- Official plugins: String Tools, Key Promoter, Git Toolbox, AI Completion,
  Checkstyle, MyBatis Navigator, CSV Editor, Solidity, Material Icons, Harbor
  Night, Excalidraw, Graphviz, Regex Tester, License Header.

Closes #732

Co-authored-by: Deng Liu <[email protected]>
Co-authored-by: florentvdry <[email protected]>
2026-11-04 02:54:31 +08:00

663 рядки
25 KiB
Rust

//! The host side of the `openidea:[email protected]` world: bindings, resource
//! types and the implementation of every import. Capabilities are checked
//! here; a missing one returns `error::denied` (the plugin can degrade).
use crate::runtime::{StoreData, clip};
use crate::sandbox;
use crate::types::{EditorContext, EventKind, HostError, HttpRequest, NotificationLevel, StatusItem, net_host};
use ide_extension::Capability;
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
use wasmtime::component::Resource;
pub(crate) mod bindings {
wasmtime::component::bindgen!({
world: "plugin",
path: "../../wit",
imports: { default: trappable },
with: {
"openidea:plugin/workspace.project": super::ProjectRes,
"openidea:plugin/editor.text-editor": super::EditorRes,
"openidea:plugin/invocation.invocation": super::InvocationRes,
"openidea:plugin/tasks.task": super::TaskRes,
},
});
}
/// The optional `syntax-provider` export, bound on its own when present.
pub(crate) mod syntax_bindings {
wasmtime::component::bindgen!({
world: "plugin-syntax",
path: "../../wit",
imports: { default: trappable },
with: {
"openidea:plugin/types": super::bindings::openidea::plugin::types,
"openidea:plugin/log": super::bindings::openidea::plugin::log,
"openidea:plugin/ui": super::bindings::openidea::plugin::ui,
"openidea:plugin/storage": super::bindings::openidea::plugin::storage,
"openidea:plugin/workspace": super::bindings::openidea::plugin::workspace,
"openidea:plugin/editor": super::bindings::openidea::plugin::editor,
"openidea:plugin/invocation": super::bindings::openidea::plugin::invocation,
"openidea:plugin/vcs": super::bindings::openidea::plugin::vcs,
"openidea:plugin/events": super::bindings::openidea::plugin::events,
"openidea:plugin/tasks": super::bindings::openidea::plugin::tasks,
"openidea:plugin/diagnostics": super::bindings::openidea::plugin::diagnostics,
"openidea:plugin/net": super::bindings::openidea::plugin::net,
"openidea:plugin/secrets": super::bindings::openidea::plugin::secrets,
"openidea:plugin/process": super::bindings::openidea::plugin::process,
"openidea:plugin/views": super::bindings::openidea::plugin::views,
"openidea:plugin/settings": super::bindings::openidea::plugin::settings,
},
});
}
/// The optional `providers` export.
pub(crate) mod providers_bindings {
wasmtime::component::bindgen!({
world: "plugin-providers",
path: "../../wit",
imports: { default: trappable },
with: {
"openidea:plugin/types": super::bindings::openidea::plugin::types,
"openidea:plugin/log": super::bindings::openidea::plugin::log,
"openidea:plugin/ui": super::bindings::openidea::plugin::ui,
"openidea:plugin/storage": super::bindings::openidea::plugin::storage,
"openidea:plugin/workspace": super::bindings::openidea::plugin::workspace,
"openidea:plugin/editor": super::bindings::openidea::plugin::editor,
"openidea:plugin/invocation": super::bindings::openidea::plugin::invocation,
"openidea:plugin/vcs": super::bindings::openidea::plugin::vcs,
"openidea:plugin/events": super::bindings::openidea::plugin::events,
"openidea:plugin/tasks": super::bindings::openidea::plugin::tasks,
"openidea:plugin/diagnostics": super::bindings::openidea::plugin::diagnostics,
"openidea:plugin/net": super::bindings::openidea::plugin::net,
"openidea:plugin/secrets": super::bindings::openidea::plugin::secrets,
"openidea:plugin/process": super::bindings::openidea::plugin::process,
"openidea:plugin/views": super::bindings::openidea::plugin::views,
"openidea:plugin/settings": super::bindings::openidea::plugin::settings,
},
});
}
/// The optional `view-provider` export.
pub(crate) mod views_bindings {
wasmtime::component::bindgen!({
world: "plugin-views",
path: "../../wit",
imports: { default: trappable },
with: {
"openidea:plugin/types": super::bindings::openidea::plugin::types,
"openidea:plugin/log": super::bindings::openidea::plugin::log,
"openidea:plugin/ui": super::bindings::openidea::plugin::ui,
"openidea:plugin/storage": super::bindings::openidea::plugin::storage,
"openidea:plugin/workspace": super::bindings::openidea::plugin::workspace,
"openidea:plugin/editor": super::bindings::openidea::plugin::editor,
"openidea:plugin/invocation": super::bindings::openidea::plugin::invocation,
"openidea:plugin/vcs": super::bindings::openidea::plugin::vcs,
"openidea:plugin/events": super::bindings::openidea::plugin::events,
"openidea:plugin/tasks": super::bindings::openidea::plugin::tasks,
"openidea:plugin/diagnostics": super::bindings::openidea::plugin::diagnostics,
"openidea:plugin/net": super::bindings::openidea::plugin::net,
"openidea:plugin/secrets": super::bindings::openidea::plugin::secrets,
"openidea:plugin/process": super::bindings::openidea::plugin::process,
"openidea:plugin/views": super::bindings::openidea::plugin::views,
"openidea:plugin/settings": super::bindings::openidea::plugin::settings,
},
});
}
use bindings::openidea::plugin::{
diagnostics, editor, events, invocation, log, net, process, secrets, settings, storage, tasks, types as wit, ui,
vcs, views, workspace,
};
/// A project handle: its root.
pub struct ProjectRes {
root: PathBuf,
}
/// The invoking window's focused editor.
pub struct EditorRes {
ctx: EditorContext,
}
/// The current call's invocation (read from [`StoreData`]).
pub struct InvocationRes;
pub struct TaskRes {
id: u64,
}
const MAX_EDITS: usize = 10_000;
const MAX_DIAGNOSTICS: usize = 5_000;
const MAX_REQUEST_BODY: usize = 8 << 20;
const MAX_FIND: u32 = 10_000;
const MAX_STATUS_TEXT: usize = 128;
type Fallible<T> = wasmtime::Result<Result<T, wit::Error>>;
pub(crate) fn to_wit(e: HostError) -> wit::Error {
match e {
HostError::Denied(c) => wit::Error::Denied(c),
HostError::Stale => wit::Error::Stale,
HostError::NotFound(m) => wit::Error::NotFound(m),
HostError::Invalid(m) => wit::Error::Invalid(m),
HostError::Cancelled => wit::Error::Cancelled,
HostError::Failed(m) => wit::Error::Failed(m),
}
}
/// A guest's `error` as text (`denied: net:example.com`, `stale`, …).
impl From<wit::Error> for crate::runtime::GuestError {
fn from(e: wit::Error) -> Self {
let clip = |m: String| clip(m, crate::runtime::MAX_TITLE * 16);
match e {
wit::Error::Denied(c) => Self::Denied(clip(c)),
wit::Error::Stale => Self::Stale,
wit::Error::NotFound(m) => Self::NotFound(clip(m)),
wit::Error::Invalid(m) => Self::Invalid(clip(m)),
wit::Error::Cancelled => Self::Cancelled,
wit::Error::Failed(m) => Self::Failed(clip(m)),
}
}
}
fn pos(p: wit::Position) -> ide_core::Pos {
ide_core::Pos::new(p.line, p.character)
}
fn range_to_wit(r: &ide_core::Range) -> wit::Range {
wit::Range {
start: wit::Position { line: r.start.line, character: r.start.col },
end: wit::Position { line: r.end.line, character: r.end.col },
}
}
impl StoreData {
fn require(&self, cap: Capability) -> Result<(), wit::Error> {
if self.granted.iter().any(|g| g.covers(&cap)) { Ok(()) } else { Err(wit::Error::Denied(cap.to_string())) }
}
fn client(&self) -> Option<&str> {
self.invocation.client.as_deref()
}
fn push<T: Send + 'static>(&mut self, value: T) -> wasmtime::Result<Resource<T>> {
Ok(self.table.push(value)?)
}
pub(crate) fn new_invocation(&mut self) -> wasmtime::Result<Resource<InvocationRes>> {
self.push(InvocationRes)
}
pub(crate) fn drop_invocation(&mut self, res: Resource<InvocationRes>) {
let _ = self.table.delete(res);
}
}
impl wit::Host for StoreData {}
impl log::Host for StoreData {
fn log(&mut self, level: wit::LogLevel, message: String) -> wasmtime::Result<()> {
let id = &self.plugin_id;
let message = clip(message, crate::runtime::MAX_TEXT);
match level {
wit::LogLevel::Trace => tracing::trace!(plugin = %id, "{message}"),
wit::LogLevel::Debug => tracing::debug!(plugin = %id, "{message}"),
wit::LogLevel::Info => tracing::info!(plugin = %id, "{message}"),
wit::LogLevel::Warn => tracing::warn!(plugin = %id, "{message}"),
wit::LogLevel::Error => tracing::error!(plugin = %id, "{message}"),
}
Ok(())
}
}
impl ui::Host for StoreData {
fn show_notification(
&mut self,
level: wit::NotificationLevel,
title: String,
body: String,
) -> wasmtime::Result<()> {
let level = match level {
wit::NotificationLevel::Info => NotificationLevel::Info,
wit::NotificationLevel::Warning => NotificationLevel::Warning,
wit::NotificationLevel::Error => NotificationLevel::Error,
};
let max = crate::runtime::MAX_TITLE;
self.host.notify(self.client(), level, clip(title, max), clip(body, 16 << 10));
Ok(())
}
fn set_status_item(&mut self, id: String, item: ui::StatusItem) -> wasmtime::Result<()> {
let item = StatusItem {
text: clip(item.text, MAX_STATUS_TEXT),
tooltip: item.tooltip.map(|t| clip(t, 1024)),
command: item.command.map(|c| clip(c, 128)),
};
self.host.status_item(&clip(id, 64), Some(item));
Ok(())
}
fn remove_status_item(&mut self, id: String) -> wasmtime::Result<()> {
self.host.status_item(&clip(id, 64), None);
Ok(())
}
}
impl storage::Host for StoreData {
fn get(&mut self, key: String) -> Fallible<Option<String>> {
if let Err(e) = self.require(Capability::Storage) {
return Ok(Err(e));
}
Ok(self.host.storage_get(&key).map_err(to_wit))
}
fn set(&mut self, key: String, value: String) -> Fallible<()> {
if let Err(e) = self.require(Capability::Storage) {
return Ok(Err(e));
}
Ok(self.host.storage_set(&key, Some(value)).map_err(to_wit))
}
fn delete(&mut self, key: String) -> Fallible<()> {
if let Err(e) = self.require(Capability::Storage) {
return Ok(Err(e));
}
Ok(self.host.storage_set(&key, None).map_err(to_wit))
}
fn keys(&mut self, prefix: String) -> Fallible<Vec<String>> {
if let Err(e) = self.require(Capability::Storage) {
return Ok(Err(e));
}
Ok(self.host.storage_keys(&prefix).map_err(to_wit))
}
}
impl workspace::Host for StoreData {
fn projects(&mut self) -> Fallible<Vec<Resource<ProjectRes>>> {
if let Err(e) = self.require(Capability::ProjectRead) {
return Ok(Err(e));
}
let mut out = Vec::new();
for root in self.host.projects() {
out.push(self.push(ProjectRes { root })?);
}
Ok(Ok(out))
}
}
impl workspace::HostProject for StoreData {
fn root(&mut self, this: Resource<ProjectRes>) -> Fallible<String> {
if let Err(e) = self.require(Capability::ProjectRead) {
return Ok(Err(e));
}
Ok(Ok(self.table.get(&this)?.root.to_string_lossy().into_owned()))
}
fn read_file(&mut self, this: Resource<ProjectRes>, path: String) -> Fallible<String> {
if let Err(e) = self.require(Capability::ProjectRead) {
return Ok(Err(e));
}
let root = self.table.get(&this)?.root.clone();
let real = match sandbox::resolve_in_root(&root, &path) {
Ok(real) => real,
Err(e) => return Ok(Err(wit::Error::Invalid(e))),
};
if let Some(text) = self.host.buffer_text(&real) {
return Ok(Ok(text));
}
Ok(sandbox::read_text(&real).map_err(wit::Error::Failed))
}
fn find_files(&mut self, this: Resource<ProjectRes>, glob: String, limit: u32) -> Fallible<Vec<String>> {
if let Err(e) = self.require(Capability::ProjectRead) {
return Ok(Err(e));
}
let root = self.table.get(&this)?.root.clone();
Ok(self.host.find_files(&root, &glob, limit.min(MAX_FIND) as usize).map_err(to_wit))
}
fn drop(&mut self, rep: Resource<ProjectRes>) -> wasmtime::Result<()> {
self.table.delete(rep)?;
Ok(())
}
}
impl editor::Host for StoreData {}
impl editor::HostTextEditor for StoreData {
fn path(&mut self, this: Resource<EditorRes>) -> wasmtime::Result<String> {
Ok(self.table.get(&this)?.ctx.path.to_string_lossy().into_owned())
}
fn language(&mut self, this: Resource<EditorRes>) -> wasmtime::Result<String> {
Ok(self.table.get(&this)?.ctx.language.clone())
}
fn document(&mut self, this: Resource<EditorRes>) -> Fallible<editor::Document> {
if let Err(e) = self.require(Capability::EditorRead) {
return Ok(Err(e));
}
let path = self.table.get(&this)?.ctx.path.clone();
Ok(match self.host.document(&path) {
Some((version, text)) => Ok(editor::Document { version, text }),
None => Err(wit::Error::NotFound(format!("{} is not open", path.display()))),
})
}
fn selections(&mut self, this: Resource<EditorRes>) -> Fallible<Vec<wit::Range>> {
if let Err(e) = self.require(Capability::EditorRead) {
return Ok(Err(e));
}
Ok(Ok(self.table.get(&this)?.ctx.selections.iter().map(range_to_wit).collect()))
}
fn apply_edits(&mut self, this: Resource<EditorRes>, version: u64, edits: Vec<wit::TextEdit>) -> Fallible<u64> {
if let Err(e) = self.require(Capability::EditorWrite) {
return Ok(Err(e));
}
if edits.len() > MAX_EDITS {
return Ok(Err(wit::Error::Invalid(format!("more than {MAX_EDITS} edits"))));
}
let total: usize = edits.iter().map(|e| e.text.len()).sum();
if total > crate::runtime::MAX_TEXT * 8 {
return Ok(Err(wit::Error::Invalid("edits too large".into())));
}
let path = self.table.get(&this)?.ctx.path.clone();
let edits: Vec<ide_core::TextEdit> = edits
.into_iter()
.map(|e| ide_core::TextEdit {
range: ide_core::Range::new(pos(e.range.start), pos(e.range.end)),
new_text: e.text,
})
.collect();
let client = self.invocation.client.clone();
Ok(self.host.apply_edits(client.as_deref(), &path, version, edits).map_err(to_wit))
}
fn drop(&mut self, rep: Resource<EditorRes>) -> wasmtime::Result<()> {
self.table.delete(rep)?;
Ok(())
}
}
impl invocation::Host for StoreData {}
impl invocation::HostInvocation for StoreData {
fn project(&mut self, _this: Resource<InvocationRes>) -> wasmtime::Result<Option<Resource<ProjectRes>>> {
match self.invocation.workspace_root.clone() {
Some(root) => Ok(Some(self.push(ProjectRes { root })?)),
None => Ok(None),
}
}
fn editor(&mut self, _this: Resource<InvocationRes>) -> wasmtime::Result<Option<Resource<EditorRes>>> {
match self.invocation.editor.clone() {
Some(ctx) => Ok(Some(self.push(EditorRes { ctx })?)),
None => Ok(None),
}
}
fn drop(&mut self, rep: Resource<InvocationRes>) -> wasmtime::Result<()> {
// Owned by the host for the duration of the call.
let _ = rep;
Ok(())
}
}
impl vcs::Host for StoreData {
fn status(&mut self, project: Resource<ProjectRes>) -> Fallible<Option<vcs::RepositoryStatus>> {
if let Err(e) = self.require(Capability::VcsRead) {
return Ok(Err(e));
}
let root = self.table.get(&project)?.root.clone();
Ok(self
.host
.vcs_status(&root)
.map(|s| {
s.map(|s| vcs::RepositoryStatus {
root: s.root,
branch: s.branch,
commit: s.commit,
upstream: s.upstream,
ahead: s.ahead,
behind: s.behind,
operation: s.operation,
changed_files: s.changed_files,
})
})
.map_err(to_wit))
}
fn fetch(&mut self, project: Resource<ProjectRes>) -> Fallible<()> {
if let Err(e) = self.require(Capability::VcsWrite) {
return Ok(Err(e));
}
let root = self.table.get(&project)?.root.clone();
let token = self.cancel.token();
Ok(self.host.vcs_fetch(&root, &token).map_err(to_wit))
}
}
fn kind_from_wit(k: events::EventKind) -> EventKind {
match k {
events::EventKind::FileOpened => EventKind::FileOpened,
events::EventKind::FileSaved => EventKind::FileSaved,
events::EventKind::EditorFocused => EventKind::EditorFocused,
events::EventKind::ActionPerformed => EventKind::ActionPerformed,
events::EventKind::VcsChanged => EventKind::VcsChanged,
events::EventKind::Scheduled => EventKind::Scheduled,
}
}
impl events::Host for StoreData {
fn subscribe(&mut self, kind: events::EventKind) -> Fallible<()> {
let kind = kind_from_wit(kind);
if let Some(cap) = kind.capability()
&& let Err(e) = self.require(cap)
{
return Ok(Err(e));
}
self.host.subscribe(kind, true);
Ok(Ok(()))
}
fn unsubscribe(&mut self, kind: events::EventKind) -> wasmtime::Result<()> {
self.host.subscribe(kind_from_wit(kind), false);
Ok(())
}
}
impl tasks::Host for StoreData {
fn begin(&mut self, title: String, cancellable: bool) -> wasmtime::Result<Resource<TaskRes>> {
let token = self.cancel.token();
let id = self.host.task_begin(self.client(), clip(title, crate::runtime::MAX_TITLE), cancellable, token);
self.open_tasks.push(id);
// The call may now run for the background budget.
let extended = self.call_started + self.limits.background;
if extended > self.deadline {
self.deadline = extended;
self.timeout = self.limits.background;
}
self.push(TaskRes { id })
}
}
impl tasks::HostTask for StoreData {
fn report(
&mut self,
this: Resource<TaskRes>,
done: u32,
total: Option<u32>,
message: Option<String>,
) -> wasmtime::Result<()> {
let id = self.table.get(&this)?.id;
let now = Instant::now();
// At most ~10 updates per second reach the UI.
if self.last_report.is_none_or(|t| now.duration_since(t).as_millis() >= 100) || total.is_some_and(|t| done >= t)
{
self.last_report = Some(now);
self.host.task_report(id, done, total, message.map(|m| clip(m, 256)));
}
Ok(())
}
fn is_cancelled(&mut self, this: Resource<TaskRes>) -> wasmtime::Result<bool> {
let id = self.table.get(&this)?.id;
Ok(self.cancel.token().is_cancelled() || self.host.task_cancelled(id))
}
fn drop(&mut self, rep: Resource<TaskRes>) -> wasmtime::Result<()> {
let id = self.table.delete(rep)?.id;
self.open_tasks.retain(|t| *t != id);
self.host.task_end(id);
Ok(())
}
}
impl StoreData {
/// What is left of the running call's wall-clock budget (host operations
/// can't be interrupted by the epoch callback, so they get this as timeout).
fn remaining(&self) -> Duration {
self.deadline.saturating_duration_since(Instant::now())
}
}
impl diagnostics::Host for StoreData {
fn publish(&mut self, path: String, items: Vec<diagnostics::Diagnostic>) -> Fallible<()> {
if let Err(e) = self.require(Capability::ProjectRead) {
return Ok(Err(e));
}
let source = self.plugin_id.clone();
let items = items
.into_iter()
.take(MAX_DIAGNOSTICS)
.map(|d| ide_core::Diagnostic {
range: ide_core::Range::new(pos(d.range.start), pos(d.range.end)),
severity: match d.severity {
diagnostics::Severity::Error => ide_core::Severity::Error,
diagnostics::Severity::Warning => ide_core::Severity::Warning,
diagnostics::Severity::Info => ide_core::Severity::Info,
diagnostics::Severity::Hint => ide_core::Severity::Hint,
},
message: clip(d.message, 4096),
source: Some(source.clone()),
code: d.code.map(|c| clip(c, 128)),
unnecessary: false,
deprecated: false,
server: Some(source.clone()),
})
.collect();
Ok(self.host.publish_diagnostics(Path::new(&path), items).map_err(to_wit))
}
}
impl net::Host for StoreData {
fn fetch(&mut self, req: net::Request) -> Fallible<net::Response> {
let host = match net_host(&req.url) {
Ok(h) => h,
Err(e) => return Ok(Err(wit::Error::Invalid(e))),
};
if let Err(e) = self.require(Capability::Net(host)) {
return Ok(Err(e));
}
if req.headers.iter().any(|h| h.value.contains("{{secret:"))
&& let Err(e) = self.require(Capability::Secrets)
{
return Ok(Err(e));
}
if req.body.as_ref().is_some_and(|b| b.len() > MAX_REQUEST_BODY) {
return Ok(Err(wit::Error::Invalid("request body larger than 8 MiB".into())));
}
let wanted = Duration::from_millis(u64::from(req.timeout_ms.unwrap_or(30_000)));
let request = HttpRequest {
method: req.method,
url: req.url,
headers: req.headers.into_iter().map(|h| (h.name, h.value)).collect(),
body: req.body,
timeout: wanted.min(self.remaining()),
};
let token = self.cancel.token();
Ok(self
.host
.fetch(request, &token)
.map(|r| net::Response {
status: r.status,
headers: r.headers.into_iter().map(|(name, value)| net::Header { name, value }).collect(),
body: r.body,
})
.map_err(to_wit))
}
}
impl secrets::Host for StoreData {
fn is_set(&mut self, name: String) -> wasmtime::Result<bool> {
Ok(self.granted.iter().any(|g| g.covers(&Capability::Secrets)) && self.host.secret_is_set(&name))
}
}
impl process::Host for StoreData {
fn run(
&mut self,
program: String,
args: Vec<String>,
cwd: Option<Resource<ProjectRes>>,
stdin: Option<String>,
) -> Fallible<process::Output> {
if let Err(e) = self.require(Capability::Process(program.clone())) {
return Ok(Err(e));
}
let cwd = match &cwd {
Some(p) => Some(self.table.get(p)?.root.clone()),
None => None,
};
let token = self.cancel.token();
let timeout = self.remaining();
Ok(self
.host
.run_process(&program, args, cwd.as_deref(), stdin, &token, timeout)
.map(|o| process::Output { status: o.status, stdout: o.stdout, stderr: o.stderr })
.map_err(to_wit))
}
}
impl views::Host for StoreData {
fn refresh(&mut self, view: String) -> wasmtime::Result<()> {
self.host.refresh_view(&clip(view, 64));
Ok(())
}
fn post_message(&mut self, view: String, message: String) -> Fallible<()> {
if message.len() > MAX_VIEW_MESSAGE {
return Ok(Err(wit::Error::Invalid(format!("messages are limited to {MAX_VIEW_MESSAGE} bytes"))));
}
Ok(self.host.post_view_message(&clip(view, 64), message).map_err(to_wit))
}
fn open_terminal(&mut self, title: String, program: String, args: Vec<String>) -> Fallible<()> {
if let Err(e) = self.require(Capability::Process(program.clone())) {
return Ok(Err(e));
}
if args.len() > 256 || args.iter().any(|a| a.len() > 4096) {
return Ok(Err(wit::Error::Invalid("too many or too long arguments".into())));
}
let cwd = self.invocation.workspace_root.clone();
let client = self.invocation.client.clone();
let title = clip(title, crate::runtime::MAX_TITLE);
Ok(self.host.open_terminal(client.as_deref(), title, &program, args, cwd.as_deref()).map_err(to_wit))
}
}
impl settings::Host for StoreData {
fn get(&mut self, key: String) -> wasmtime::Result<Option<String>> {
let root = self.invocation.workspace_root.clone();
Ok(self.host.setting(root.as_deref(), &clip(key, 128)))
}
}
impl views_bindings::openidea::plugin::view_types::Host for StoreData {}
/// Largest message between a plugin and its view's frame.
const MAX_VIEW_MESSAGE: usize = 1 << 20;