diff --git a/crates/ide-host/src/ops/lsp.rs b/crates/ide-host/src/ops/lsp.rs index 67f31cb4..a0105b87 100644 --- a/crates/ide-host/src/ops/lsp.rs +++ b/crates/ide-host/src/ops/lsp.rs @@ -76,9 +76,17 @@ async fn plugin_call(f: impl FnOnce() -> T + Send + 'static) tokio::task::spawn_blocking(f).await.ok() } -/// Items from plugins carry no server (resolve and commands don't apply). +/// Items from plugins carry no server: commands don't apply, and a code +/// action is resolved by the plugin named in its `data` ([`PluginActionRef`]). const PLUGIN_SERVER: ServerId = ServerId(0); +/// `data` of a plugin's code action whose edits it computes when picked. +#[derive(serde::Serialize, serde::Deserialize)] +struct PluginActionRef { + plugin: String, + data: String, +} + /// A plugin's `completion-item.kind` (wit/providers.wit lists them). fn plugin_completion_kind(kind: &str) -> Option { Some(match kind { @@ -120,14 +128,14 @@ fn plugin_completion(item: ide_plugin::CompletionItem) -> CompletionItem { kind, documentation: item.documentation.map(|value| Markup { kind: "markdown".into(), value }), sort_text: item.sort_text, - filter_text: None, + filter_text: item.filter_text, preselect: false, deprecated: false, insert_text: item.insert_text, - is_snippet: false, + is_snippet: item.snippet, replace_range: item.replace, replace_range_tab: None, - additional_edits: Vec::new(), + additional_edits: item.additional_edits, commit_characters: Vec::new(), command: None, data: None, @@ -314,7 +322,7 @@ pub async fn lsp_code_actions( }; let plugin_actions: Vec = plugin_actions .into_iter() - .map(|(_, a)| plugin_code_action(a)) + .map(|(plugin, a)| plugin_code_action(&plugin, a)) .filter(|a| { only.is_empty() || a.kind.as_ref().is_some_and(|k| only.iter().any(|o| k == o || k.starts_with(&format!("{o}.")))) @@ -330,24 +338,53 @@ pub async fn lsp_code_actions( prepare_actions(project, doc, version, actions).await } -fn plugin_code_action(a: ide_plugin::CodeAction) -> CodeAction { - let files = a +fn plugin_code_action(plugin: &str, a: ide_plugin::CodeAction) -> CodeAction { + // With `data` and no edits, the plugin computes them when the action is picked. + let lazy = a.edits.is_empty() && a.data.is_some(); + let files: Vec<_> = a .edits .into_iter() .map(|(path, edits)| ide_core::FileEdit::Edit { path: path.into(), edits, version: None }) .collect(); + let data = a + .data + .filter(|_| lazy) + .and_then(|data| serde_json::to_value(PluginActionRef { plugin: plugin.to_owned(), data }).ok()); CodeAction { title: a.title, kind: a.kind, is_preferred: a.preferred, disabled_reason: None, - edit: Some(WorkspaceEdit { files, preconditions: Vec::new() }), + edit: (!lazy).then(|| WorkspaceEdit { files, preconditions: Vec::new() }), command: None, - data: None, + data, server: PLUGIN_SERVER, } } +/// A code action resolved by whoever offered it: its language server, or +/// the plugin its `data` names. +async fn resolve_action(host: &Host, window: &str, doc: DocId, action: CodeAction) -> IdeResult { + if action.server != PLUGIN_SERVER { + return host.project(window)?.lsp.resolve_code_action(doc, action).await; + } + let Some(r) = action.data.clone().and_then(|d| serde_json::from_value::(d).ok()) else { + return Ok(action); + }; + let (plugins, d) = plugin_providers(host, window, doc, ProviderKind::CodeActions) + .ok_or_else(|| ide_core::IdeError::unavailable("the plugin that offered this action is gone"))?; + let pending = ide_plugin::CodeAction { + title: action.title.clone(), + kind: action.kind.clone(), + preferred: action.is_preferred, + edits: Vec::new(), + data: Some(r.data), + }; + let plugin = r.plugin.clone(); + let resolved = blocking(move || plugins.resolve_code_action(&plugin, &d, pending)).await?; + Ok(plugin_code_action(&r.plugin, ide_plugin::CodeAction { data: None, ..resolved })) +} + /// Cancellable code actions with the kinds the servers advertise (Refactor This, refactoring availability). pub async fn lsp_code_action_query( host: &Host, @@ -381,7 +418,7 @@ pub async fn lsp_resolve_code_action( let project = host.project(window)?; let documents = project.documents.clone(); let version = blocking(move || documents.version(doc)).await?; - let action = project.lsp.resolve_code_action(doc, action).await?; + let action = resolve_action(host, window, doc, action).await?; Ok(prepare_actions(project, doc, version, vec![action]).await?.remove(0)) } @@ -391,7 +428,7 @@ pub async fn lsp_apply_code_action(host: &Host, window: &str, doc: DocId, action let action = if action.edit.is_none() && action.command.is_none() { let documents = project.documents.clone(); let version = blocking(move || documents.version(doc)).await?; - let action = project.lsp.resolve_code_action(doc, action).await?; + let action = resolve_action(host, window, doc, action).await?; prepare_actions(project.clone(), doc, version, vec![action]).await?.remove(0) } else { action @@ -801,6 +838,25 @@ pub(crate) fn register(d: &mut Dispatcher) { mod tests { use super::*; + #[test] + fn a_plugin_action_with_data_and_no_edits_is_resolved_by_its_plugin() { + let action = |edits: Vec<(std::path::PathBuf, Vec)>, data: Option<&str>| ide_plugin::CodeAction { + title: "Fix".into(), + kind: None, + preferred: false, + edits, + data: data.map(str::to_owned), + }; + let lazy = plugin_code_action("acme.x", action(Vec::new(), Some("ref"))); + assert!(lazy.edit.is_none()); + let r: PluginActionRef = serde_json::from_value(lazy.data.unwrap()).unwrap(); + assert_eq!((r.plugin.as_str(), r.data.as_str()), ("acme.x", "ref")); + // With edits, `data` is the plugin's business: applied as is. + let edit = TextEdit { range: Range::default(), new_text: "x".into() }; + let eager = plugin_code_action("acme.x", action(vec![("/a".into(), vec![edit])], Some("ref"))); + assert!(eager.edit.is_some() && eager.data.is_none()); + } + #[test] fn every_completion_kind_the_plugin_api_documents_is_understood() { let wit = include_str!("../../../../wit/providers.wit"); diff --git a/crates/ide-host/src/plugins/providers.rs b/crates/ide-host/src/plugins/providers.rs index d60b806a..b37c12e1 100644 --- a/crates/ide-host/src/plugins/providers.rs +++ b/crates/ide-host/src/plugins/providers.rs @@ -292,6 +292,21 @@ impl PluginHost { .collect() } + /// The edits of an action `plugin_id` returned with `data`, now that the + /// user picked it: a call with the command budget, not the edit path's. + pub fn resolve_code_action( + &self, + plugin_id: &str, + doc: &ProviderDocument, + action: CodeAction, + ) -> ide_core::IdeResult { + let d = Arc::new(doc.clone()); + let timeout = self.limits.timeout; + self.code_call(plugin_id, ide_plugin::Invocation::default(), "resolving the code action", move |rt, _| { + rt.resolve_code_action(&d, &action, timeout) + }) + } + pub fn provide_definition(&self, doc: &ProviderDocument, at: ide_core::Pos) -> Vec { let d = Arc::new(doc.clone()); self.edit_calls(ProviderKind::Definition, &doc.language, move |rt, timeout| { diff --git a/crates/ide-host/tests/plugins.rs b/crates/ide-host/tests/plugins.rs index 1b8eb466..fb592967 100644 --- a/crates/ide-host/tests/plugins.rs +++ b/crates/ide-host/tests/plugins.rs @@ -635,7 +635,7 @@ fn idle_instances_are_reclaimed_and_schedules_reactivate() { assert_eq!(f.host.instances(), 0); let info = f.host.info_of("misbehave").unwrap(); assert_eq!((info.status, info.stats.reclaims), (PluginStatus::Inactive, 1)); - assert_eq!(f.command_titles("misbehave").len(), 8, "contributions stay"); + assert_eq!(f.command_titles("misbehave").len(), 10, "contributions stay"); assert_eq!(f.run("misbehave", "echo", Some("y")).unwrap().as_deref(), Some("y")); assert_eq!(f.host.info_of("misbehave").unwrap().stats.activations, 2); @@ -2410,3 +2410,56 @@ fn uninstalling_deletes_the_plugins_secrets() { f.host.uninstall("openidea.ai-completion").unwrap(); assert!(keychain.0.lock().is_empty(), "{:?}", keychain.0.lock().keys().collect::>()); } + +/// Plugin API 1.0 leaves room to grow: event kinds are names, and the editor +/// providers carry snippets, imports, lazily computed quick fixes and tagged +/// problems. +#[test] +fn completion_quick_fixes_problems_and_event_kinds_of_api_1() { + let f = fixture(Limits::default()); + let pkg = f._tmp.path().join("misbehave-providers"); + copy_dir(&misbehave_pkg(), &pkg); + let manifest = std::fs::read_to_string(pkg.join("plugin.toml")).unwrap(); + std::fs::write( + pkg.join("plugin.toml"), + format!("{manifest}\n[[contributes.providers]]\nkinds = [\"completion\", \"code-actions\"]\nlanguages = [\"text\"]\n"), + ) + .unwrap(); + f.install(&pkg); + let doc = |text: &str| ide_plugin::ProviderDocument { + path: f.project.join("a.txt"), + language: "text".into(), + version: 1, + text: text.into(), + }; + let at = ide_core::Pos::new(0, 0); + + let items = f.host.provide_completion(&doc("rich"), at, None); + let item = &items[0].1; + assert_eq!( + (item.insert_text.as_str(), item.snippet, item.filter_text.as_deref()), + ("f(${1:x})$0", true, Some("fun")) + ); + assert_eq!(item.additional_edits[0].new_text, "use f;\n"); + + // Offered without edits; computed when picked. + let actions = f.host.provide_code_actions(&doc("lazy"), ide_core::Range::default()); + let (plugin, action) = actions.into_iter().next().expect("offered"); + assert!(action.edits.is_empty() && action.data.as_deref() == Some("fix")); + let resolved = f.host.resolve_code_action(&plugin, &doc("lazy"), action.clone()).unwrap(); + assert_eq!(resolved.edits[0].1[0].new_text, "fixed "); + let foreign = ide_plugin::CodeAction { data: Some("other".into()), ..action }; + assert!(f.host.resolve_code_action(&plugin, &doc("lazy"), foreign).is_err()); + + // A kind this IDE doesn't know is refused, not a trap. + let err = f.host.run_command("misbehave", "subscribe", Some("file-deleted"), f.inv()).unwrap_err(); + assert!(err.message.contains("no event kind"), "{}", err.message); + let err = f.host.run_command("misbehave", "subscribe", Some("file-saved"), f.inv()).unwrap_err(); + assert!(err.message.contains("events"), "known, but not granted: {}", err.message); + + // Tags the IDE knows, the tool that found it. + let path = f.project.join("a.txt"); + f.host.run_command("misbehave", "diagnose", Some(&path.to_string_lossy()), f.inv()).unwrap(); + let d = f.workspace.diagnostics.lock().get(&("misbehave".into(), path)).cloned().unwrap(); + assert_eq!((d[0].unnecessary, d[0].deprecated, d[0].source.as_deref()), (true, false, Some("lint"))); +} diff --git a/crates/ide-plugin/src/runtime.rs b/crates/ide-plugin/src/runtime.rs index fdb48019..bd5a9975 100644 --- a/crates/ide-plugin/src/runtime.rs +++ b/crates/ide-plugin/src/runtime.rs @@ -467,7 +467,6 @@ const MAX_SYNTAX_ITEMS: usize = 200_000; const MAX_BATCH: usize = 1000; fn event_to_wit(e: WorkbenchEvent) -> wit_events::Event { - use crate::types::ActionSource as S; match e { WorkbenchEvent::FileOpened(p) => wit_events::Event::FileOpened(p), WorkbenchEvent::FileSaved(p) => wit_events::Event::FileSaved(p), @@ -477,14 +476,7 @@ fn event_to_wit(e: WorkbenchEvent) -> wit_events::Event { WorkbenchEvent::ActionPerformed(a) => wit_events::Event::ActionPerformed(wit_events::ActionPerformed { action: a.action, title: a.title, - source: match a.source { - S::Keyboard => wit_events::ActionSource::Keyboard, - S::Menu => wit_events::ActionSource::Menu, - S::Toolbar => wit_events::ActionSource::Toolbar, - S::ContextMenu => wit_events::ActionSource::ContextMenu, - S::Search => wit_events::ActionSource::Search, - S::Other => wit_events::ActionSource::Other, - }, + source: a.source.name().to_owned(), shortcut: a.shortcut, }), } @@ -670,9 +662,17 @@ impl Runtime { detail: i.detail.map(|d| clip(d, MAX_TITLE)), kind: i.kind.map(|k| clip(k, 32)), insert_text: clip(i.insert_text, 64 << 10), + snippet: i.snippet, replace: i.replace.map(range_from_wit), sort_text: i.sort_text.map(|t| clip(t, 64)), + filter_text: i.filter_text.map(|t| clip(t, MAX_TITLE)), documentation: i.documentation.map(|d| clip(d, 16 << 10)), + additional_edits: i + .additional_edits + .into_iter() + .take(MAX_ADDITIONAL_EDITS) + .map(edit_from_wit) + .collect(), }) .collect()) } @@ -699,27 +699,20 @@ impl Runtime { ) -> Result, CallError> { let d = doc_to_wit(doc); let actions = self.provider_call(timeout, |p, s| p.call_code_actions(s, &d, range_to_wit(selection)))?; - Ok(actions - .into_iter() - .take(MAX_PROVIDER_ITEMS) - .map(|a| CodeAction { - title: clip(a.title, MAX_TITLE), - kind: a.kind.map(|k| clip(k, 64)), - preferred: a.preferred, - edits: a - .edits - .into_iter() - .map(|f| { - let edits = f - .edits - .into_iter() - .map(|e| ide_core::TextEdit { range: range_from_wit(e.range), new_text: e.text }) - .collect(); - (std::path::PathBuf::from(f.path), edits) - }) - .collect(), - }) - .collect()) + Ok(actions.into_iter().take(MAX_PROVIDER_ITEMS).map(code_action_from_wit).collect()) + } + + /// The edits of an action `provide_code_actions` returned with `data`. + pub fn resolve_code_action( + &mut self, + doc: &ProviderDocument, + action: &CodeAction, + timeout: Duration, + ) -> Result { + let d = doc_to_wit(doc); + let a = code_action_to_wit(action); + let resolved = self.provider_call(timeout, |p, s| p.call_resolve_code_action(s, &d, &a))?; + resolved.map(code_action_from_wit).map_err(|e| CallError::Guest(e.into())) } pub fn provide_definition( @@ -968,6 +961,47 @@ const MAX_VIEW_ITEMS: usize = 20_000; const MAX_CELL: usize = 64 << 10; const MAX_HTML: usize = 4 << 20; +/// Most additional edits of a completion item. +const MAX_ADDITIONAL_EDITS: usize = 100; + +fn edit_from_wit(e: wit_types::TextEdit) -> ide_core::TextEdit { + ide_core::TextEdit { range: range_from_wit(e.range), new_text: e.text } +} + +fn code_action_from_wit(a: wit_prov::CodeAction) -> CodeAction { + CodeAction { + title: clip(a.title, MAX_TITLE), + kind: a.kind.map(|k| clip(k, 64)), + preferred: a.preferred, + edits: a + .edits + .into_iter() + .map(|f| (std::path::PathBuf::from(f.path), f.edits.into_iter().map(edit_from_wit).collect())) + .collect(), + data: a.data.map(|d| clip(d, 64 << 10)), + } +} + +fn code_action_to_wit(a: &CodeAction) -> wit_prov::CodeAction { + wit_prov::CodeAction { + title: a.title.clone(), + kind: a.kind.clone(), + preferred: a.preferred, + edits: a + .edits + .iter() + .map(|(path, edits)| wit_prov::FileEdit { + path: path.to_string_lossy().into_owned(), + edits: edits + .iter() + .map(|e| wit_types::TextEdit { range: range_to_wit(e.range), text: e.new_text.clone() }) + .collect(), + }) + .collect(), + data: a.data.clone(), + } +} + fn opt_clip(s: Option, max: usize) -> Option { s.map(|s| clip(s, max)) } @@ -975,6 +1009,9 @@ fn opt_clip(s: Option, max: usize) -> Option { fn view_from_wit(v: wit_vt::View) -> View { let body = match v.body { wit_vt::Body::Message(m) => ViewBody::Message(clip(m, 16 << 10)), + wit_vt::Body::Other(e) => { + ViewBody::Message(format!("This view needs a newer version of OpenIDEA ({}).", clip(e.kind, 64))) + } wit_vt::Body::Tree(items) => ViewBody::Tree( items .into_iter() @@ -1006,10 +1043,8 @@ fn view_from_wit(v: wit_vt::View) -> View { fields .into_iter() .take(MAX_VIEW_ITEMS) - .map(|f| ViewField { - key: clip(f.key, 128), - label: clip(f.label, MAX_TITLE), - value: match f.value { + .filter_map(|f| { + let value = match f.value { wit_vt::FieldValue::Text(t) => FieldValue::Text(clip(t, MAX_CELL)), wit_vt::FieldValue::Multiline(t) => FieldValue::Multiline(clip(t, 1 << 20)), wit_vt::FieldValue::Checkbox(b) => FieldValue::Checkbox(b), @@ -1017,7 +1052,10 @@ fn view_from_wit(v: wit_vt::View) -> View { options: options.into_iter().take(1000).map(|o| clip(o, MAX_TITLE)).collect(), selected: clip(selected, MAX_TITLE), }, - }, + // A field type of a newer API: left out. + wit_vt::FieldValue::Other(_) => return None, + }; + Some(ViewField { key: clip(f.key, 128), label: clip(f.label, MAX_TITLE), value }) }) .collect(), ), @@ -1067,3 +1105,26 @@ fn event_to_wit_view(e: ViewEvent) -> wit_vt::ViewEvent { ViewEvent::Message { text } => W::Message(text), } } + +#[cfg(test)] +mod tests { + use super::*; + + fn ext(kind: &str) -> wit_types::Extension { + wit_types::Extension { kind: kind.into(), data: "{}".into() } + } + + #[test] + fn bodies_and_fields_of_a_newer_api_degrade() { + let view = |body| wit_vt::View { toolbar: Vec::new(), body, status: None }; + let shown = view_from_wit(view(wit_vt::Body::Other(ext("markdown")))); + assert_eq!(shown.body, ViewBody::Message("This view needs a newer version of OpenIDEA (markdown).".into())); + let field = |key: &str, value| wit_vt::Field { key: key.into(), label: key.into(), value }; + let form = view_from_wit(view(wit_vt::Body::Form(vec![ + field("color", wit_vt::FieldValue::Other(ext("color"))), + field("name", wit_vt::FieldValue::Text("n".into())), + ]))); + let ViewBody::Form(fields) = form.body else { panic!("a form") }; + assert_eq!(fields.iter().map(|f| f.key.as_str()).collect::>(), ["name"], "unknown fields are left out"); + } +} diff --git a/crates/ide-plugin/src/types.rs b/crates/ide-plugin/src/types.rs index dbd80edb..b1d7b215 100644 --- a/crates/ide-plugin/src/types.rs +++ b/crates/ide-plugin/src/types.rs @@ -126,6 +126,20 @@ pub enum ActionSource { Other, } +impl ActionSource { + /// As plugins see it. + pub fn name(self) -> &'static str { + match self { + Self::Keyboard => "keyboard", + Self::Menu => "menu", + Self::Toolbar => "toolbar", + Self::ContextMenu => "context-menu", + Self::Search => "search", + Self::Other => "other", + } + } +} + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] pub struct ActionPerformed { @@ -348,9 +362,14 @@ pub struct CompletionItem { /// WIT `completion-kind` name (`method`, `keyword`, …). pub kind: Option, pub insert_text: String, + /// `insert_text` has tab stops and placeholders. + pub snippet: bool, pub replace: Option, pub sort_text: Option, + pub filter_text: Option, pub documentation: Option, + /// Applied with the item elsewhere in the document (an import). + pub additional_edits: Vec, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -365,6 +384,8 @@ pub struct CodeAction { pub kind: Option, pub preferred: bool, pub edits: Vec<(PathBuf, Vec)>, + /// The plugin's reference: the edits come from `resolve_code_action`. + pub data: Option, } #[derive(Clone, Debug, PartialEq, Eq)] diff --git a/crates/ide-plugin/src/wit_host.rs b/crates/ide-plugin/src/wit_host.rs index 265f992a..0a2bbe36 100644 --- a/crates/ide-plugin/src/wit_host.rs +++ b/crates/ide-plugin/src/wit_host.rs @@ -440,20 +440,11 @@ impl vcs::Host for StoreData { } } -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); + fn subscribe(&mut self, kind: String) -> Fallible<()> { + let Some(kind) = EventKind::parse(&kind) else { + return Ok(Err(wit::Error::Invalid(format!("no event kind {kind:?} in this OpenIDEA")))); + }; if let Some(cap) = kind.capability() && let Err(e) = self.require(cap) { @@ -463,8 +454,10 @@ impl events::Host for StoreData { Ok(Ok(())) } - fn unsubscribe(&mut self, kind: events::EventKind) -> wasmtime::Result<()> { - self.host.subscribe(kind_from_wit(kind), false); + fn unsubscribe(&mut self, kind: String) -> wasmtime::Result<()> { + if let Some(kind) = EventKind::parse(&kind) { + self.host.subscribe(kind, false); + } Ok(()) } } @@ -542,10 +535,10 @@ impl diagnostics::Host for StoreData { diagnostics::Severity::Hint => ide_core::Severity::Hint, }, message: clip(d.message, 4096), - source: Some(source.clone()), + source: Some(d.source.map_or_else(|| source.clone(), |s| clip(s, 128))), code: d.code.map(|c| clip(c, 128)), - unnecessary: false, - deprecated: false, + unnecessary: d.tags.iter().any(|t| t == "unnecessary"), + deprecated: d.tags.iter().any(|t| t == "deprecated"), server: Some(source.clone()), }) .collect(); diff --git a/crates/ide-plugin/tests/fixtures/hello/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/hello/dist/plugin.wasm index b0c8caee..b55671ab 100644 Binary files a/crates/ide-plugin/tests/fixtures/hello/dist/plugin.wasm and b/crates/ide-plugin/tests/fixtures/hello/dist/plugin.wasm differ 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 2edfa822..707df000 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 @@ -86dfe70a30ab1f7955a16e96c1b32c8be663456b871cc4d3bed7dc6052c062da +c9acbc021f3840649823cb7680e13907d429c6fda8271fdb0ae6e5ea89ed15ee diff --git a/crates/ide-plugin/tests/fixtures/jobs/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/jobs/dist/plugin.wasm index aaa324d9..d15e5082 100644 Binary files a/crates/ide-plugin/tests/fixtures/jobs/dist/plugin.wasm and b/crates/ide-plugin/tests/fixtures/jobs/dist/plugin.wasm differ 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 61e1857b..695b65da 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 @@ -0dacb82ac35c1f132dc66e85dbf0663b729f818279d3b32cd06403e502fbf880 +2fb6aa879562733bf5b7f11dabe8fc6a415c9ef695868fe7845417a4245d2dc8 diff --git a/crates/ide-plugin/tests/fixtures/misbehave/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/misbehave/dist/plugin.wasm index d6018357..37db117e 100644 Binary files a/crates/ide-plugin/tests/fixtures/misbehave/dist/plugin.wasm and b/crates/ide-plugin/tests/fixtures/misbehave/dist/plugin.wasm differ 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 1c5ed64f..59546337 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 @@ -0ad2e4c6977a576b7eb27fc1e535fd056dd9da64bdfe6cfda9ca9f57537d0384 +25b0f419a68a3b1651d3c2774d834b05e40ede52bc5ed8c0eba6d27a204efe82 diff --git a/crates/ide-plugin/tests/fixtures/misbehave/plugin.toml b/crates/ide-plugin/tests/fixtures/misbehave/plugin.toml index 29dba7d6..1eae84d2 100644 --- a/crates/ide-plugin/tests/fixtures/misbehave/plugin.toml +++ b/crates/ide-plugin/tests/fixtures/misbehave/plugin.toml @@ -40,3 +40,11 @@ title = "insert" [[contributes.commands]] id = "cancel" title = "cancel" + +[[contributes.commands]] +id = "subscribe" +title = "subscribe" + +[[contributes.commands]] +id = "diagnose" +title = "diagnose" diff --git a/crates/ide-plugin/tests/fixtures/misbehave/src/lib.rs b/crates/ide-plugin/tests/fixtures/misbehave/src/lib.rs index cd6d6511..890bf845 100644 --- a/crates/ide-plugin/tests/fixtures/misbehave/src/lib.rs +++ b/crates/ide-plugin/tests/fixtures/misbehave/src/lib.rs @@ -14,7 +14,9 @@ wit_bindgen::generate!({ generate_all, }); -use exports::openidea::plugin::providers::{self, CodeAction, CompletionItem, Document, InlineSuggestion, LineMarker}; +use exports::openidea::plugin::providers::{ + self, CodeAction, CompletionItem, Document, FileEdit, InlineSuggestion, LineMarker, +}; use exports::openidea::plugin::syntax_provider::{self, Fold, Symbol, Token}; use exports::openidea::plugin::{commands, event_handler, lifecycle}; use openidea::plugin::{invocation::Invocation, types}; @@ -67,6 +69,23 @@ impl commands::Guest for Misbehave { } // The user gave up: not the plugin failing. "cancel" => Err(types::Error::Cancelled), + // Event kinds are names: one this IDE doesn't know is refused. + "subscribe" => openidea::plugin::events::subscribe(&arg.unwrap_or_default()).map(|()| None), + // Tagged problems from another tool on the line 0 of `arg`. + "diagnose" => { + use openidea::plugin::diagnostics::{self, Diagnostic, Severity}; + let start = types::Position { line: 0, character: 0 }; + let d = Diagnostic { + range: types::Range { start, end: start }, + severity: Severity::Warning, + message: "unused".into(), + code: None, + source: Some("lint".into()), + tags: vec!["unnecessary".into(), "glowing".into()], + related: Vec::new(), + }; + diagnostics::publish(&arg.unwrap_or_default(), &[d]).map(|()| None) + } other => Err(types::Error::NotFound(format!("command {other}"))), } } @@ -113,14 +132,24 @@ impl syntax_provider::Guest for Misbehave { impl providers::Guest for Misbehave { fn completion(doc: Document, _at: types::Position, _trigger: Option) -> Vec { behave(&doc.text); + // `rich`: a snippet that also adds an import. + let rich = doc.text == "rich"; + let start = types::Position { line: 0, character: 0 }; vec![CompletionItem { label: doc.path.clone(), detail: None, kind: Some("keyword".into()), - insert_text: doc.path, + insert_text: if rich { "f(${1:x})$0".into() } else { doc.path }, + snippet: rich, replace: None, sort_text: None, + filter_text: rich.then(|| "fun".into()), documentation: None, + additional_edits: if rich { + vec![types::TextEdit { range: types::Range { start, end: start }, text: "use f;\n".into() }] + } else { + Vec::new() + }, }] } @@ -128,8 +157,27 @@ impl providers::Guest for Misbehave { None } - fn code_actions(_doc: Document, _selection: types::Range) -> Vec { - Vec::new() + /// `lazy`: one action whose edits come from `resolve-code-action`. + fn code_actions(doc: Document, _selection: types::Range) -> Vec { + if doc.text != "lazy" { + return Vec::new(); + } + vec![CodeAction { + title: "Fix later".into(), + kind: Some("quickfix".into()), + preferred: false, + edits: Vec::new(), + data: Some("fix".into()), + }] + } + + fn resolve_code_action(doc: Document, action: CodeAction) -> Result { + if action.data.as_deref() != Some("fix") { + return Err(types::Error::Invalid("not one of mine".into())); + } + let start = types::Position { line: 0, character: 0 }; + let edit = types::TextEdit { range: types::Range { start, end: start }, text: "fixed ".into() }; + Ok(CodeAction { edits: vec![FileEdit { path: doc.path, edits: vec![edit] }], ..action }) } fn definition(_doc: Document, _at: types::Position) -> Vec { diff --git a/plugins/README.md b/plugins/README.md index ae53436b..a579afb6 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -321,6 +321,11 @@ Events are batched: a running plugin gets them within a fraction of a second, an idle one collects them for 2 s before it is woken, so frequent events (Key Promoter's actions) don't keep it alive. +Kinds are names (`subscribe` takes a string), so later IDEs add kinds without +a new API: an IDE refuses a kind it doesn't know with `invalid`, and a kind +newer than the plugin's SDK arrives as `Event::Other { kind, data }` (JSON). +The SDK's event enums are `#[non_exhaustive]`; match them with a `_` arm. + ### Providers Declare what the plugin provides in the editor; only plugins whose @@ -338,7 +343,14 @@ Then `register_plugin!(MyPlugin, providers)` and implement the document (path, language, version, text). Results join the language servers' (completion lists, ⌥⏎ quick fixes, ⌘B, gutter icons; a code action carries edits for any files, applied as one change). A completion item's -`kind` (`CompletionKind` in the SDK, a string in the WIT) picks its icon. +`kind` (`CompletionKind` in the SDK, a string in the WIT) picks its icon; it +can be a `snippet` (`$1`, `${1:name}`, `$0`), match on `filter_text` instead +of its label, and bring `additional_edits` elsewhere in the document (an +import). A code action can leave `edits` empty and set `data`: its edits are +then computed by `resolve_code_action` when the user picks it, under the +command budget instead of the 50 ms. Problems (`publish_diagnostics`) can name +the tool that found them (`source`) and be `unnecessary` (faded) or +`deprecated` (struck through). Editor providers run under the edit-path budget: 50 ms, after which the result is dropped, and an interruption at 500 ms. Every plugin providing a kind is asked at the same time, so each has the whole budget. @@ -393,6 +405,9 @@ collapse of items, form fields, table cells, inserted and deleted rows, and `html` messages. A tree is a flat list whose items name their `parent`; an `expandable` item gets its children when it is expanded. A table gets only the rows it shows (`viewport`): report `total_rows` and return those. +Bodies, field types and view events added by later APIs travel as `Other { +kind, data }`: an IDE that doesn't know a body says it needs updating, and +leaves out a field type it doesn't know. Showing a view activates the plugin (`on-view:`). `views` become tool windows; a `customEditors` file (`priority = "default"`) opens in the @@ -890,7 +905,10 @@ only grows: Extend with new types and new functions (or interfaces). `types.error` is used everywhere and is no exception; new situations use `failed(string)` or `invalid(string)`. Sets that keep growing (completion, token, symbol and - fold kinds, icons) are strings for this reason. + fold kinds, icons, event kinds, action sources, diagnostic tags) are + strings for this reason, and variants that grow (`event`, view `body`, + `view-event`, `field-value`) have an `other(extension)` case, a kind and + JSON data, that carries later cases to older plugins and IDEs. 2. **Host interfaces (imports) may gain functions and interfaces**: older components don't import them. 3. **Plugin interfaces (exports) never gain functions** once released: the @@ -902,6 +920,11 @@ only grows: minor with a message that says to update the IDE or rebuild with the SDK. 5. **One version number**: the `wit/` package version, `ENGINE_VERSION` and `API_VERSION` agree, and the SDK's `wit/` copy is identical to `wit/`. +6. **1.x calls are synchronous and pass documents whole.** Host calls block + the plugin's instance, and providers get the document's text with each + call. WASI 0.3 async components come with 2.0, behind an adapter in the + host that keeps 1.x plugins running; incremental document sync, if + needed, is a new interface. Checked automatically: diff --git a/plugins/official/ai-completion/dist/plugin.wasm b/plugins/official/ai-completion/dist/plugin.wasm index c51a0c67..b163d582 100644 Binary files a/plugins/official/ai-completion/dist/plugin.wasm and b/plugins/official/ai-completion/dist/plugin.wasm differ diff --git a/plugins/official/ai-completion/dist/plugin.wasm.sources b/plugins/official/ai-completion/dist/plugin.wasm.sources index 8baa3d1f..e5251dd3 100644 --- a/plugins/official/ai-completion/dist/plugin.wasm.sources +++ b/plugins/official/ai-completion/dist/plugin.wasm.sources @@ -1 +1 @@ -aa09a92c0ba9d85a01c56bbf4455e40ae44f005b443ce1002bbe8011408ff975 +28e4bf10a41bdda011111b0e150ebe96205c57f410bad98c19eea3f1286349d6 diff --git a/plugins/official/checkstyle/dist/plugin.wasm b/plugins/official/checkstyle/dist/plugin.wasm index 98fccd27..e4ffd3c2 100644 Binary files a/plugins/official/checkstyle/dist/plugin.wasm and b/plugins/official/checkstyle/dist/plugin.wasm differ diff --git a/plugins/official/checkstyle/dist/plugin.wasm.sources b/plugins/official/checkstyle/dist/plugin.wasm.sources index 1c46bf44..66dca89d 100644 --- a/plugins/official/checkstyle/dist/plugin.wasm.sources +++ b/plugins/official/checkstyle/dist/plugin.wasm.sources @@ -1 +1 @@ -908c2fd177ad2bb9a8a8492505151c3b81579191156c5ec53f705127f50a8ee9 +7e015486cc916f5edbac542863373fd7204f5bcd534f6c408d5f9d2b72913862 diff --git a/plugins/official/checkstyle/src/lib.rs b/plugins/official/checkstyle/src/lib.rs index 74f17cc4..aebf998e 100644 --- a/plugins/official/checkstyle/src/lib.rs +++ b/plugins/official/checkstyle/src/lib.rs @@ -97,12 +97,8 @@ fn diagnostic(p: &Problem, text: &str) -> Diagnostic { let line_len = text.split('\n').nth(p.line as usize).map_or(0, |l| l.encode_utf16().count() as u32); let start = p.column.unwrap_or(0).min(line_len); let end = if p.column.is_some() && start < line_len { start + 1 } else { line_len }; - Diagnostic { - range: Range::new(Position::new(p.line, start), Position::new(p.line, end.max(start))), - severity: p.severity, - message: p.message.clone(), - code: Some(p.check.clone()), - } + let range = Range::new(Position::new(p.line, start), Position::new(p.line, end.max(start))); + Diagnostic::new(range, p.severity, p.message.clone()).code(p.check.clone()) } /// The project containing `path`. @@ -188,6 +184,7 @@ impl Providers for Checkstyle { kind: Some("quickfix".into()), preferred: true, edits: vec![(doc.path.clone(), vec![edit])], + data: None, }); } } diff --git a/plugins/official/csv-editor/dist/plugin.wasm b/plugins/official/csv-editor/dist/plugin.wasm index f3529166..5e29e582 100644 Binary files a/plugins/official/csv-editor/dist/plugin.wasm and b/plugins/official/csv-editor/dist/plugin.wasm differ diff --git a/plugins/official/csv-editor/dist/plugin.wasm.sources b/plugins/official/csv-editor/dist/plugin.wasm.sources index fcd50761..3a0d19f0 100644 --- a/plugins/official/csv-editor/dist/plugin.wasm.sources +++ b/plugins/official/csv-editor/dist/plugin.wasm.sources @@ -1 +1 @@ -64270fa0f5b3084c78764f09b8ce161742fdbff9c28fb90590da39594e558db1 +a6cf59e74595e13908e2cbf1752c58665c6963de258413c9d7332d2a7dd9cbd0 diff --git a/plugins/official/git-toolbox/dist/plugin.wasm b/plugins/official/git-toolbox/dist/plugin.wasm index c7d8ad40..936c901a 100644 Binary files a/plugins/official/git-toolbox/dist/plugin.wasm and b/plugins/official/git-toolbox/dist/plugin.wasm differ diff --git a/plugins/official/git-toolbox/dist/plugin.wasm.sources b/plugins/official/git-toolbox/dist/plugin.wasm.sources index 0bcb7925..13c47727 100644 --- a/plugins/official/git-toolbox/dist/plugin.wasm.sources +++ b/plugins/official/git-toolbox/dist/plugin.wasm.sources @@ -1 +1 @@ -22876f842f727a2b9aa292986df3393f5b5f30c5c34319f5665f7d2701c8accf +b8dde206a74b222b93e61056d4cbe403617ae240d142e01e91420a2aacdb2da4 diff --git a/plugins/official/graphviz/dist/plugin.wasm b/plugins/official/graphviz/dist/plugin.wasm index 337a9781..172e8708 100644 Binary files a/plugins/official/graphviz/dist/plugin.wasm and b/plugins/official/graphviz/dist/plugin.wasm differ diff --git a/plugins/official/graphviz/dist/plugin.wasm.sources b/plugins/official/graphviz/dist/plugin.wasm.sources index f750b77c..8bdfeeb8 100644 --- a/plugins/official/graphviz/dist/plugin.wasm.sources +++ b/plugins/official/graphviz/dist/plugin.wasm.sources @@ -1 +1 @@ -14c60782eca79a25cf2c38d58c05be0eb97c4afc607d4cc83302d1445d094aab +a681c2abd4a65d6aeb62efd514e9b170f4fca52b0c14d2d380a3b04be0a3fbc6 diff --git a/plugins/official/graphviz/src/lib.rs b/plugins/official/graphviz/src/lib.rs index cd76c651..c8ab1210 100644 --- a/plugins/official/graphviz/src/lib.rs +++ b/plugins/official/graphviz/src/lib.rs @@ -52,12 +52,9 @@ pub fn diagnostics(text: &str, reports: &[Report]) -> Vec { .map(|r| { let line = r.line.map_or(0, |l| l.saturating_sub(1)).min(lines.len().saturating_sub(1) as u32); let len = lines.get(line as usize).map_or(0, |l| l.encode_utf16().count() as u32); - Diagnostic { - range: Range::new(Position::new(line, 0), Position::new(line, len)), - severity: if r.error { Severity::Error } else { Severity::Warning }, - message: r.message.clone(), - code: Some("graphviz".into()), - } + let range = Range::new(Position::new(line, 0), Position::new(line, len)); + let severity = if r.error { Severity::Error } else { Severity::Warning }; + Diagnostic::new(range, severity, r.message.clone()).code("graphviz") }) .collect() } diff --git a/plugins/official/key-promoter/dist/plugin.wasm b/plugins/official/key-promoter/dist/plugin.wasm index 9ef9e3d0..4a092d36 100644 Binary files a/plugins/official/key-promoter/dist/plugin.wasm and b/plugins/official/key-promoter/dist/plugin.wasm differ diff --git a/plugins/official/key-promoter/dist/plugin.wasm.sources b/plugins/official/key-promoter/dist/plugin.wasm.sources index 069068aa..23ac06dc 100644 --- a/plugins/official/key-promoter/dist/plugin.wasm.sources +++ b/plugins/official/key-promoter/dist/plugin.wasm.sources @@ -1 +1 @@ -7f5aacde2b8a8bcee5365c8cbda91045195688899c6537dc2159037cca74a672 +3c83bc9559df54457e19e650cdd21bab9d609e22daaa1fc4f596910b70834a34 diff --git a/plugins/official/license-header/dist/plugin.wasm b/plugins/official/license-header/dist/plugin.wasm index 1c82e278..6672d2bd 100644 Binary files a/plugins/official/license-header/dist/plugin.wasm and b/plugins/official/license-header/dist/plugin.wasm differ diff --git a/plugins/official/license-header/dist/plugin.wasm.sources b/plugins/official/license-header/dist/plugin.wasm.sources index 7dc3c5ab..a70c551b 100644 --- a/plugins/official/license-header/dist/plugin.wasm.sources +++ b/plugins/official/license-header/dist/plugin.wasm.sources @@ -1 +1 @@ -45a7ee993b5c234b0d25e5b05ef60e714fa3edf63d6bd0d6a29b13587c8d09ce +63f1bbdf6eca5a4642cd3453ae384ff3f876de0373c12008b757a63852164b71 diff --git a/plugins/official/license-header/src/lib.rs b/plugins/official/license-header/src/lib.rs index 7fd5d789..1da3c9e1 100644 --- a/plugins/official/license-header/src/lib.rs +++ b/plugins/official/license-header/src/lib.rs @@ -129,12 +129,8 @@ pub fn header_edit(path: &str, text: &str, year: i64, owner: &str, license: &str fn missing(text: &str) -> Diagnostic { let line = insert_line(text); let len = text.lines().nth(line as usize).map_or(0, |l| l.encode_utf16().count() as u32); - Diagnostic { - range: Range::new(Position::new(line, 0), Position::new(line, len)), - severity: Severity::Info, - message: "No license header".into(), - code: Some(CODE.into()), - } + Diagnostic::new(Range::new(Position::new(line, 0), Position::new(line, len)), Severity::Info, "No license header") + .code(CODE) } /// Reports `path` when it takes a header and has none, clears it otherwise. @@ -274,6 +270,7 @@ impl Providers for LicenseHeader { kind: Some("quickfix".into()), preferred: true, edits: vec![(doc.path.clone(), vec![edit])], + data: None, }] } } diff --git a/plugins/official/mybatis/dist/plugin.wasm b/plugins/official/mybatis/dist/plugin.wasm index ca729516..1fe1055f 100644 Binary files a/plugins/official/mybatis/dist/plugin.wasm and b/plugins/official/mybatis/dist/plugin.wasm differ diff --git a/plugins/official/mybatis/dist/plugin.wasm.sources b/plugins/official/mybatis/dist/plugin.wasm.sources index 758553fc..532aada7 100644 --- a/plugins/official/mybatis/dist/plugin.wasm.sources +++ b/plugins/official/mybatis/dist/plugin.wasm.sources @@ -1 +1 @@ -d5cf81455ae8b2b0fd6d1402d8775913bd7999db62b9bdf87675890ce52b1e80 +0db5cb24c358651d0e728ab3efb52286290c1a38ec379435225c935d4165ad6a diff --git a/plugins/official/regex-tester/dist/plugin.wasm b/plugins/official/regex-tester/dist/plugin.wasm index 1d39d764..d2740781 100644 Binary files a/plugins/official/regex-tester/dist/plugin.wasm and b/plugins/official/regex-tester/dist/plugin.wasm differ diff --git a/plugins/official/regex-tester/dist/plugin.wasm.sources b/plugins/official/regex-tester/dist/plugin.wasm.sources index 013089c4..845c9957 100644 --- a/plugins/official/regex-tester/dist/plugin.wasm.sources +++ b/plugins/official/regex-tester/dist/plugin.wasm.sources @@ -1 +1 @@ -227c8150c6ad7a5a20236764f149c876cbbc3b082763bf2e7fc05de99726b740 +257478b050ea360c55e904f2a89c6069293139ff0d97ba860de3f29dac927787 diff --git a/plugins/official/solidity/dist/plugin.wasm b/plugins/official/solidity/dist/plugin.wasm index 4fd185f6..e1a8d32d 100644 Binary files a/plugins/official/solidity/dist/plugin.wasm and b/plugins/official/solidity/dist/plugin.wasm differ diff --git a/plugins/official/solidity/dist/plugin.wasm.sources b/plugins/official/solidity/dist/plugin.wasm.sources index 20e8de03..681447c6 100644 --- a/plugins/official/solidity/dist/plugin.wasm.sources +++ b/plugins/official/solidity/dist/plugin.wasm.sources @@ -1 +1 @@ -bbadcaa7a86a3e0a3f8be6d86aed2229d5cdfd128fda3435b4d64c7e19ad1df2 +21e7510d6a3712bf0f21d53a5c8d652e3c2787d89a1fc25998cde96efe124207 diff --git a/plugins/official/string-tools/dist/plugin.wasm b/plugins/official/string-tools/dist/plugin.wasm index b38c2f9c..b38b7db6 100644 Binary files a/plugins/official/string-tools/dist/plugin.wasm and b/plugins/official/string-tools/dist/plugin.wasm differ diff --git a/plugins/official/string-tools/dist/plugin.wasm.sources b/plugins/official/string-tools/dist/plugin.wasm.sources index 7c78b969..56d78c0a 100644 --- a/plugins/official/string-tools/dist/plugin.wasm.sources +++ b/plugins/official/string-tools/dist/plugin.wasm.sources @@ -1 +1 @@ -09cc92e8e731f8f59f3857a71f9a30cc3d9e04a1c32092920f573b0f26592855 +04844bfbfbcb1770f4f4742658944c592922b037b2387e9340de58ef0d9baacc diff --git a/sdk/rust/openidea-plugin/src/host.rs b/sdk/rust/openidea-plugin/src/host.rs index 187c5f8b..760c6d30 100644 --- a/sdk/rust/openidea-plugin/src/host.rs +++ b/sdk/rust/openidea-plugin/src/host.rs @@ -152,24 +152,12 @@ mod imp { wit::vcs::fetch(p).map_err(error) } - fn kind(k: EventKind) -> wit::events::EventKind { - use wit::events::EventKind as K; - match k { - EventKind::FileOpened => K::FileOpened, - EventKind::FileSaved => K::FileSaved, - EventKind::EditorFocused => K::EditorFocused, - EventKind::ActionPerformed => K::ActionPerformed, - EventKind::VcsChanged => K::VcsChanged, - EventKind::Scheduled => K::Scheduled, - } - } - pub fn subscribe(k: EventKind) -> Result<(), Error> { - wit::events::subscribe(kind(k)).map_err(error) + wit::events::subscribe(k.name()).map_err(error) } pub fn unsubscribe(k: EventKind) { - wit::events::unsubscribe(kind(k)) + wit::events::unsubscribe(k.name()) } pub fn begin_task(title: &str, cancellable: bool) -> TaskRes { @@ -189,7 +177,7 @@ mod imp { } pub fn publish_diagnostics(path: &str, items: &[Diagnostic]) -> Result<(), Error> { - use wit::diagnostics::{Diagnostic as D, Severity as S}; + use wit::diagnostics::{Diagnostic as D, RelatedInformation as R, Severity as S}; let items: Vec = items .iter() .map(|d| D { @@ -202,6 +190,23 @@ mod imp { }, message: d.message.clone(), code: d.code.clone(), + source: d.source.clone(), + tags: [(d.unnecessary, "unnecessary"), (d.deprecated, "deprecated")] + .into_iter() + .filter(|(on, _)| *on) + .map(|(_, tag)| tag.to_owned()) + .collect(), + related: d + .related + .iter() + .map(|r| R { + location: wit::types::Location { + path: r.location.path.clone(), + range: range_out(r.location.range), + }, + message: r.message.clone(), + }) + .collect(), }) .collect(); wit::diagnostics::publish(path, &items).map_err(error) @@ -264,7 +269,7 @@ mod imp { } pub fn event(e: wit::events::Event) -> Event { - use wit::events::{ActionSource as S, Event as E}; + use wit::events::Event as E; match e { E::FileOpened(p) => Event::FileOpened(p), E::FileSaved(p) => Event::FileSaved(p), @@ -274,16 +279,10 @@ mod imp { E::ActionPerformed(a) => Event::ActionPerformed(ActionPerformed { action: a.action, title: a.title, - source: match a.source { - S::Keyboard => ActionSource::Keyboard, - S::Menu => ActionSource::Menu, - S::Toolbar => ActionSource::Toolbar, - S::ContextMenu => ActionSource::ContextMenu, - S::Search => ActionSource::Search, - S::Other => ActionSource::Other, - }, + source: ActionSource::parse(&a.source), shortcut: a.shortcut, }), + E::Other(x) => Event::Other { kind: x.kind, data: x.data }, } } } diff --git a/sdk/rust/openidea-plugin/src/lib.rs b/sdk/rust/openidea-plugin/src/lib.rs index f59d728c..57aebe86 100644 --- a/sdk/rust/openidea-plugin/src/lib.rs +++ b/sdk/rust/openidea-plugin/src/lib.rs @@ -255,6 +255,16 @@ macro_rules! __providers_exports { $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, @@ -420,9 +430,12 @@ pub mod __rt { 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() } @@ -443,25 +456,54 @@ pub mod __rt { sel: b::types::Range, ) -> Vec { let d = doc(d); - with::(|x| x.code_actions(&d, crate::host::__range_from(sel))) - .into_iter() - .map(|a| 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(|e| b::types::TextEdit { range: crate::host::__range_to(e.range), text: e.text }) - .collect(), - }) - .collect(), - }) - .collect() + 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( @@ -531,6 +573,9 @@ pub mod __rt { 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(), @@ -543,6 +588,7 @@ pub mod __rt { 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 @@ -582,6 +628,7 @@ pub mod __rt { 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) } diff --git a/sdk/rust/openidea-plugin/src/mock.rs b/sdk/rust/openidea-plugin/src/mock.rs index 33ce121d..66f38eb7 100644 --- a/sdk/rust/openidea-plugin/src/mock.rs +++ b/sdk/rust/openidea-plugin/src/mock.rs @@ -722,11 +722,8 @@ mod tests { let out = mock.invocation().project().unwrap().run_process("java", &["-version".into()], None).unwrap(); assert_eq!(out.stdout, "-version"); assert_eq!(mock.process_runs()[0].cwd.as_deref(), Some("/project")); - host::publish_diagnostics( - "/project/a.java", - &[Diagnostic { range: Range::default(), severity: Severity::Warning, message: "m".into(), code: None }], - ) - .unwrap(); + host::publish_diagnostics("/project/a.java", &[Diagnostic::new(Range::default(), Severity::Warning, "m")]) + .unwrap(); assert_eq!(mock.diagnostics()["/project/a.java"].len(), 1); } } diff --git a/sdk/rust/openidea-plugin/src/providers.rs b/sdk/rust/openidea-plugin/src/providers.rs index c10f75cf..2cf80850 100644 --- a/sdk/rust/openidea-plugin/src/providers.rs +++ b/sdk/rust/openidea-plugin/src/providers.rs @@ -92,12 +92,18 @@ pub struct CompletionItem { pub label: String, pub detail: Option, pub kind: Option, + /// With `snippet`, tab stops and placeholders (`$1`, `${1:name}`, `$0`). pub insert_text: String, + pub snippet: bool, /// Replaced on accept; `None`: the word at the caret. pub replace: Option, pub sort_text: Option, + /// Matched against what was typed instead of `label`. + pub filter_text: Option, /// Markdown. pub documentation: Option, + /// Applied with the item elsewhere in the document (an import). + pub additional_edits: Vec, } impl CompletionItem { @@ -107,9 +113,12 @@ impl CompletionItem { detail: None, kind: Some(kind), insert_text: label.into(), + snippet: false, replace: None, sort_text: None, + filter_text: None, documentation: None, + additional_edits: Vec::new(), } } } @@ -129,6 +138,9 @@ pub struct CodeAction { pub preferred: bool, /// (absolute path, edits) per file. pub edits: Vec<(String, Vec)>, + /// The plugin's own reference: with it and no `edits`, the edits come + /// from [`Providers::resolve_code_action`] once the user picks the action. + pub data: Option, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -157,6 +169,12 @@ pub trait Providers { Vec::new() } + /// The edits of an action [`Providers::code_actions`] returned with + /// `data`, when the user picks it (the command budget applies, not 50 ms). + fn resolve_code_action(&mut self, _doc: &Document, action: CodeAction) -> Result { + Ok(action) + } + fn definition(&mut self, _doc: &Document, _at: Position) -> Vec { Vec::new() } diff --git a/sdk/rust/openidea-plugin/src/types.rs b/sdk/rust/openidea-plugin/src/types.rs index 5a74f67d..6a6fbd7a 100644 --- a/sdk/rust/openidea-plugin/src/types.rs +++ b/sdk/rust/openidea-plugin/src/types.rs @@ -143,7 +143,9 @@ pub struct StatusItem { pub command: Option, } +/// Kinds of workbench events; later SDKs add more. #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[non_exhaustive] pub enum EventKind { FileOpened, FileSaved, @@ -154,17 +156,46 @@ pub enum EventKind { Scheduled, } -/// How the user started an action. +impl EventKind { + /// The name the IDE knows it by. + pub fn name(self) -> &'static str { + match self { + Self::FileOpened => "file-opened", + Self::FileSaved => "file-saved", + Self::EditorFocused => "editor-focused", + Self::ActionPerformed => "action-performed", + Self::VcsChanged => "vcs-changed", + Self::Scheduled => "scheduled", + } + } +} + +/// How the user started an action; later IDEs may report more. #[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] pub enum ActionSource { Keyboard, Menu, Toolbar, ContextMenu, Search, + /// Anything else, including sources this SDK doesn't know. Other, } +impl ActionSource { + pub fn parse(name: &str) -> Self { + match name { + "keyboard" => Self::Keyboard, + "menu" => Self::Menu, + "toolbar" => Self::Toolbar, + "context-menu" => Self::ContextMenu, + "search" => Self::Search, + _ => Self::Other, + } + } +} + #[derive(Clone, Debug, PartialEq, Eq)] pub struct ActionPerformed { /// Action id (`editor.duplicate-line`). @@ -177,6 +208,7 @@ pub struct ActionPerformed { /// A workbench event (see [`crate::host::subscribe`]). #[derive(Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] pub enum Event { FileOpened(String), FileSaved(String), @@ -186,6 +218,11 @@ pub enum Event { VcsChanged(String), /// The `on-schedule` period elapsed. Scheduled, + /// A kind this SDK doesn't know: its name and JSON data. + Other { + kind: String, + data: String, + }, } /// The repository containing a project. @@ -227,6 +264,41 @@ pub struct Diagnostic { pub message: String, /// Rule or check id. pub code: Option, + /// The tool that found it, when not the plugin itself. + pub source: Option, + /// Code that does nothing (shown faded). + pub unnecessary: bool, + /// Use of something deprecated (struck through). + pub deprecated: bool, + /// Other places that explain it. + pub related: Vec, +} + +impl Diagnostic { + pub fn new(range: Range, severity: Severity, message: impl Into) -> Self { + Self { + range, + severity, + message: message.into(), + code: None, + source: None, + unnecessary: false, + deprecated: false, + related: Vec::new(), + } + } + + pub fn code(mut self, code: impl Into) -> Self { + self.code = Some(code.into()); + self + } +} + +/// Another place that explains a diagnostic. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RelatedInformation { + pub location: Location, + pub message: String, } /// An HTTP request (see [`crate::host::fetch`]). @@ -276,3 +348,17 @@ pub struct ProcessOutput { pub stdout: String, pub stderr: String, } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn names_the_ide_uses() { + assert_eq!(EventKind::ActionPerformed.name(), "action-performed"); + assert_eq!(ActionSource::parse("context-menu"), ActionSource::ContextMenu); + assert_eq!(ActionSource::parse("pen"), ActionSource::Other, "a source of a newer IDE"); + let d = Diagnostic::new(Range::default(), Severity::Hint, "m").code("c"); + assert_eq!((d.code.as_deref(), d.unnecessary, d.related.len()), (Some("c"), false, 0)); + } +} diff --git a/sdk/rust/openidea-plugin/src/views.rs b/sdk/rust/openidea-plugin/src/views.rs index 3da8e506..b8a693d5 100644 --- a/sdk/rust/openidea-plugin/src/views.rs +++ b/sdk/rust/openidea-plugin/src/views.rs @@ -110,12 +110,19 @@ impl ListItem { } #[derive(Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] pub enum FieldValue { Text(String), Multiline(String), Checkbox(bool), /// Options and the chosen one. Choice(Vec, String), + /// A field type of a newer plugin API: its kind and JSON data (IDEs that + /// don't know it leave the field out). + Other { + kind: String, + data: String, + }, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -142,6 +149,7 @@ pub struct Table { } #[derive(Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] pub enum Body { Message(String), Tree(Vec), @@ -151,6 +159,12 @@ pub enum Body { /// Shown in a sandboxed frame; its script talks to the plugin with /// `openidea.postMessage(text)` and `openidea.onMessage(fn)`. Html(String), + /// A body of a newer plugin API: its kind and JSON data (IDEs that don't + /// know it say they need updating). + Other { + kind: String, + data: String, + }, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -197,6 +211,7 @@ impl Viewport { } #[derive(Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] pub enum ViewEvent { Button(String), Activate(String), @@ -213,6 +228,11 @@ pub enum ViewEvent { InsertRow(u32), DeleteRows(Vec), Message(String), + /// An event this SDK doesn't know: its kind and JSON data. + Other { + kind: String, + data: String, + }, } /// Content of the plugin's declared views and custom editors. diff --git a/sdk/rust/openidea-plugin/wit/events.wit b/sdk/rust/openidea-plugin/wit/events.wit index 8470c298..2d118d81 100644 --- a/sdk/rust/openidea-plugin/wit/events.wit +++ b/sdk/rust/openidea-plugin/wit/events.wit @@ -2,36 +2,15 @@ package openidea:plugin@1.0.0; /// Workbench events, delivered in batches to `event-handler.handle`. interface events { - use types.{error}; - - enum event-kind { - file-opened, - file-saved, - /// The focused editor changed. - editor-focused, - /// An IDE action ran. Requires `events.input`. - action-performed, - /// A repository's state changed (commit, checkout, fetch, …). - vcs-changed, - /// The plugin's `on-schedule` period elapsed. - scheduled, - } - - /// How the user started an action. - enum action-source { - keyboard, - menu, - toolbar, - context-menu, - search, - other, - } + use types.{error, extension}; record action-performed { /// Action id (`editor.duplicate-line`). action: string, title: string, - source: action-source, + /// How the user started it: `keyboard`, `menu`, `toolbar`, + /// `context-menu`, `search` or `other` (more may be added). + source: string, /// Its keyboard shortcut, when it has one (`Ctrl+D`). shortcut: option, } @@ -43,14 +22,21 @@ interface events { action-performed(action-performed), vcs-changed(string), scheduled, + /// A kind added after 1.0 (subscribed to by its name). + other(extension), } /// Delivers `kind` events from now on, for the rest of the session (also /// across idle reclaim: an event activates the plugin again). Declare /// `activation = ["on-event:"]` to get them before first use. + /// Kinds: `file-opened`, `file-saved`, `editor-focused` (the focused editor + /// changed), `vcs-changed` (a repository's state changed: commit, + /// checkout, fetch…), `action-performed` (an IDE action ran) and + /// `scheduled` (the `on-schedule` period elapsed); later IDEs add more, + /// and an IDE that doesn't know one fails with `invalid`. /// Requires `events` (`events.input` for `action-performed`). - subscribe: func(kind: event-kind) -> result<_, error>; - unsubscribe: func(kind: event-kind); + subscribe: func(kind: string) -> result<_, error>; + unsubscribe: func(kind: string); } /// Long-running work with a progress indicator. While a task is open, the diff --git a/sdk/rust/openidea-plugin/wit/io.wit b/sdk/rust/openidea-plugin/wit/io.wit index a1aeb849..f1fe634a 100644 --- a/sdk/rust/openidea-plugin/wit/io.wit +++ b/sdk/rust/openidea-plugin/wit/io.wit @@ -2,7 +2,7 @@ package openidea:plugin@1.0.0; /// Problems in files, shown in the editor and the Problems view. interface diagnostics { - use types.{error, range}; + use types.{error, range, location}; enum severity { error, @@ -11,12 +11,23 @@ interface diagnostics { hint, } + /// Another place that explains a diagnostic (the earlier declaration, say). + record related-information { + location: location, + message: string, + } + record diagnostic { range: range, severity: severity, message: string, /// Rule or check id (`LineLength`). code: option, + /// The tool that found it, when not the plugin itself (`checkstyle`). + source: option, + /// `unnecessary` (shown faded) and `deprecated` (struck through); unknown ones are ignored. + tags: list, + related: list, } /// Replaces the plugin's diagnostics for the file at `path` (absolute, in diff --git a/sdk/rust/openidea-plugin/wit/providers.wit b/sdk/rust/openidea-plugin/wit/providers.wit index cfca6560..97dd4c7a 100644 --- a/sdk/rust/openidea-plugin/wit/providers.wit +++ b/sdk/rust/openidea-plugin/wit/providers.wit @@ -5,7 +5,7 @@ package openidea:plugin@1.0.0; /// on the editing path: a result later than ~50 ms is dropped, a call still /// running at 500 ms is interrupted. interface providers { - use types.{position, range, text-edit, location}; + use types.{error, position, range, text-edit, location}; /// The document a request is about. record document { @@ -24,11 +24,17 @@ interface providers { /// constant, struct, event, operator or type-parameter. Unknown kinds /// show no icon. kind: option, + /// With `snippet`, tab stops and placeholders (`$1`, `${1:name}`, `$0`). insert-text: string, + snippet: bool, /// Replaced on accept; `none`: the word at the caret. replace: option, sort-text: option, + /// Matched against what was typed instead of `label`. + filter-text: option, documentation: option, + /// Applied with the item elsewhere in the document (an import). + additional-edits: list, } record inline-suggestion { @@ -48,6 +54,9 @@ interface providers { kind: option, preferred: bool, edits: list, + /// The plugin's own reference: with it and no `edits`, the edits are + /// computed by `resolve-code-action` once the user picks the action. + data: option, } record line-marker { @@ -64,6 +73,9 @@ interface providers { inline-completion: func(doc: document, at: position) -> option; /// Actions for `selection` (quick fixes for the plugin's own diagnostics). code-actions: func(doc: document, selection: range) -> list; + /// The edits of an action `code-actions` returned with `data`, when the + /// user picks it (the command budget applies, not the editor's 50 ms). + resolve-code-action: func(doc: document, action: code-action) -> result; definition: func(doc: document, at: position) -> list; line-markers: func(doc: document) -> list; } diff --git a/sdk/rust/openidea-plugin/wit/types.wit b/sdk/rust/openidea-plugin/wit/types.wit index 5c4f237e..2e2b48f7 100644 --- a/sdk/rust/openidea-plugin/wit/types.wit +++ b/sdk/rust/openidea-plugin/wit/types.wit @@ -52,6 +52,15 @@ interface types { range: range, } + /// A case added after 1.0, carried by the `other` case of a variant that + /// grows (events, view bodies, view events, form fields): its kind and its + /// data as JSON. Whoever doesn't know the kind ignores it or shows a + /// fallback; the SDKs decode the kinds they know. + record extension { + kind: string, + data: string, + } + /// Passed to `lifecycle.activate`. record context { plugin-id: string, diff --git a/sdk/rust/openidea-plugin/wit/views.wit b/sdk/rust/openidea-plugin/wit/views.wit index de1ae780..dcc806dc 100644 --- a/sdk/rust/openidea-plugin/wit/views.wit +++ b/sdk/rust/openidea-plugin/wit/views.wit @@ -8,6 +8,8 @@ package openidea:plugin@1.0.0; /// runs in the WebView, except a `html` body, which is shown in a sandboxed /// frame that can only exchange messages with the plugin. interface view-types { + use types.{extension}; + record button { id: string, label: string, @@ -43,6 +45,8 @@ interface view-types { checkbox(bool), /// The options and the chosen one. choice(tuple, string>), + /// A field type added after 1.0 (an IDE that doesn't know it leaves it out). + other(extension), } record field { @@ -72,6 +76,8 @@ interface view-types { /// network, no IDE API. Its script talks to the plugin with /// `openidea.postMessage(text)` / `openidea.onMessage(fn)`. html(string), + /// A body added after 1.0; an IDE that doesn't know it says it needs updating. + other(extension), } record view { @@ -109,6 +115,8 @@ interface view-types { delete-rows(list), /// From the `html` body's script. message(string), + /// An event added after 1.0, from a body or field added after 1.0. + other(extension), } } diff --git a/wit/events.wit b/wit/events.wit index 8470c298..2d118d81 100644 --- a/wit/events.wit +++ b/wit/events.wit @@ -2,36 +2,15 @@ package openidea:plugin@1.0.0; /// Workbench events, delivered in batches to `event-handler.handle`. interface events { - use types.{error}; - - enum event-kind { - file-opened, - file-saved, - /// The focused editor changed. - editor-focused, - /// An IDE action ran. Requires `events.input`. - action-performed, - /// A repository's state changed (commit, checkout, fetch, …). - vcs-changed, - /// The plugin's `on-schedule` period elapsed. - scheduled, - } - - /// How the user started an action. - enum action-source { - keyboard, - menu, - toolbar, - context-menu, - search, - other, - } + use types.{error, extension}; record action-performed { /// Action id (`editor.duplicate-line`). action: string, title: string, - source: action-source, + /// How the user started it: `keyboard`, `menu`, `toolbar`, + /// `context-menu`, `search` or `other` (more may be added). + source: string, /// Its keyboard shortcut, when it has one (`Ctrl+D`). shortcut: option, } @@ -43,14 +22,21 @@ interface events { action-performed(action-performed), vcs-changed(string), scheduled, + /// A kind added after 1.0 (subscribed to by its name). + other(extension), } /// Delivers `kind` events from now on, for the rest of the session (also /// across idle reclaim: an event activates the plugin again). Declare /// `activation = ["on-event:"]` to get them before first use. + /// Kinds: `file-opened`, `file-saved`, `editor-focused` (the focused editor + /// changed), `vcs-changed` (a repository's state changed: commit, + /// checkout, fetch…), `action-performed` (an IDE action ran) and + /// `scheduled` (the `on-schedule` period elapsed); later IDEs add more, + /// and an IDE that doesn't know one fails with `invalid`. /// Requires `events` (`events.input` for `action-performed`). - subscribe: func(kind: event-kind) -> result<_, error>; - unsubscribe: func(kind: event-kind); + subscribe: func(kind: string) -> result<_, error>; + unsubscribe: func(kind: string); } /// Long-running work with a progress indicator. While a task is open, the diff --git a/wit/io.wit b/wit/io.wit index a1aeb849..f1fe634a 100644 --- a/wit/io.wit +++ b/wit/io.wit @@ -2,7 +2,7 @@ package openidea:plugin@1.0.0; /// Problems in files, shown in the editor and the Problems view. interface diagnostics { - use types.{error, range}; + use types.{error, range, location}; enum severity { error, @@ -11,12 +11,23 @@ interface diagnostics { hint, } + /// Another place that explains a diagnostic (the earlier declaration, say). + record related-information { + location: location, + message: string, + } + record diagnostic { range: range, severity: severity, message: string, /// Rule or check id (`LineLength`). code: option, + /// The tool that found it, when not the plugin itself (`checkstyle`). + source: option, + /// `unnecessary` (shown faded) and `deprecated` (struck through); unknown ones are ignored. + tags: list, + related: list, } /// Replaces the plugin's diagnostics for the file at `path` (absolute, in diff --git a/wit/providers.wit b/wit/providers.wit index cfca6560..97dd4c7a 100644 --- a/wit/providers.wit +++ b/wit/providers.wit @@ -5,7 +5,7 @@ package openidea:plugin@1.0.0; /// on the editing path: a result later than ~50 ms is dropped, a call still /// running at 500 ms is interrupted. interface providers { - use types.{position, range, text-edit, location}; + use types.{error, position, range, text-edit, location}; /// The document a request is about. record document { @@ -24,11 +24,17 @@ interface providers { /// constant, struct, event, operator or type-parameter. Unknown kinds /// show no icon. kind: option, + /// With `snippet`, tab stops and placeholders (`$1`, `${1:name}`, `$0`). insert-text: string, + snippet: bool, /// Replaced on accept; `none`: the word at the caret. replace: option, sort-text: option, + /// Matched against what was typed instead of `label`. + filter-text: option, documentation: option, + /// Applied with the item elsewhere in the document (an import). + additional-edits: list, } record inline-suggestion { @@ -48,6 +54,9 @@ interface providers { kind: option, preferred: bool, edits: list, + /// The plugin's own reference: with it and no `edits`, the edits are + /// computed by `resolve-code-action` once the user picks the action. + data: option, } record line-marker { @@ -64,6 +73,9 @@ interface providers { inline-completion: func(doc: document, at: position) -> option; /// Actions for `selection` (quick fixes for the plugin's own diagnostics). code-actions: func(doc: document, selection: range) -> list; + /// The edits of an action `code-actions` returned with `data`, when the + /// user picks it (the command budget applies, not the editor's 50 ms). + resolve-code-action: func(doc: document, action: code-action) -> result; definition: func(doc: document, at: position) -> list; line-markers: func(doc: document) -> list; } diff --git a/wit/types.wit b/wit/types.wit index 5c4f237e..2e2b48f7 100644 --- a/wit/types.wit +++ b/wit/types.wit @@ -52,6 +52,15 @@ interface types { range: range, } + /// A case added after 1.0, carried by the `other` case of a variant that + /// grows (events, view bodies, view events, form fields): its kind and its + /// data as JSON. Whoever doesn't know the kind ignores it or shows a + /// fallback; the SDKs decode the kinds they know. + record extension { + kind: string, + data: string, + } + /// Passed to `lifecycle.activate`. record context { plugin-id: string, diff --git a/wit/views.wit b/wit/views.wit index de1ae780..dcc806dc 100644 --- a/wit/views.wit +++ b/wit/views.wit @@ -8,6 +8,8 @@ package openidea:plugin@1.0.0; /// runs in the WebView, except a `html` body, which is shown in a sandboxed /// frame that can only exchange messages with the plugin. interface view-types { + use types.{extension}; + record button { id: string, label: string, @@ -43,6 +45,8 @@ interface view-types { checkbox(bool), /// The options and the chosen one. choice(tuple, string>), + /// A field type added after 1.0 (an IDE that doesn't know it leaves it out). + other(extension), } record field { @@ -72,6 +76,8 @@ interface view-types { /// network, no IDE API. Its script talks to the plugin with /// `openidea.postMessage(text)` / `openidea.onMessage(fn)`. html(string), + /// A body added after 1.0; an IDE that doesn't know it says it needs updating. + other(extension), } record view { @@ -109,6 +115,8 @@ interface view-types { delete-rows(list), /// From the `html` body's script. message(string), + /// An event added after 1.0, from a body or field added after 1.0. + other(extension), } }