diff --git a/crates/ide-host/src/ops/lsp.rs b/crates/ide-host/src/ops/lsp.rs index c2dff41be..535b1acd4 100644 --- a/crates/ide-host/src/ops/lsp.rs +++ b/crates/ide-host/src/ops/lsp.rs @@ -195,9 +195,24 @@ pub async fn lsp_hover( ) -> IdeResult> { let lsp = host.project(window)?.lsp.clone(); let cancel = host.begin_request(request); - let result = lsp.hover(doc, pos, cancel).await; + let Some((plugins, d)) = plugin_providers(host, window, doc, ProviderKind::Hover) else { + let result = lsp.hover(doc, pos, cancel).await; + host.end_request(request); + return result; + }; + let (result, hovers) = + tokio::join!(lsp.hover(doc, pos, cancel), plugin_call(move || plugins.provide_hover(&d, pos))); host.end_request(request); - result + let hovers = hovers.unwrap_or_default(); + if hovers.is_empty() { + return result; + } + let server = match result { + Ok(server) => server, + Err(e) if matches!(e.kind, ErrorKind::NotFound | ErrorKind::Unavailable) => None, + Err(e) => return Err(e), + }; + Ok(crate::plugins::merge_hover(server, hovers)) } pub async fn lsp_signature_help(host: &Host, window: &str, doc: DocId, pos: Pos) -> IdeResult> { diff --git a/crates/ide-host/src/plugins/hover.rs b/crates/ide-host/src/plugins/hover.rs new file mode 100644 index 000000000..dec0ef022 --- /dev/null +++ b/crates/ide-host/src/plugins/hover.rs @@ -0,0 +1,94 @@ +use ide_lsp::types::{Hover, Markup}; +use ide_plugin::PluginHover; + +pub(crate) fn merge(server: Option, plugin: Vec) -> Option { + if plugin.is_empty() { + return server; + } + let mut parts = Vec::with_capacity(plugin.len() + 1); + let mut range = None; + if let Some(h) = server { + range = h.range; + if !h.contents.value.trim().is_empty() { + parts.push(if h.contents.kind == "markdown" { h.contents.value } else { escape(&h.contents.value) }); + } + } + for h in plugin { + range = range.or(h.range); + parts.push(h.markdown); + } + Some(Hover { contents: Markup { kind: "markdown".into(), value: parts.join("\n\n---\n\n") }, range }) +} + +fn escape(plain: &str) -> String { + let mut out = String::with_capacity(plain.len() + plain.len() / 4); + for c in plain.chars() { + match c { + '\n' => out.push_str(" \n"), + '\\' | '`' | '*' | '_' | '{' | '}' | '[' | ']' | '<' | '>' | '(' | ')' | '#' | '+' | '-' | '.' | '!' + | '|' => { + out.push('\\'); + out.push(c); + } + _ => out.push(c), + } + } + out +} + +#[cfg(test)] +mod tests { + use super::*; + use ide_core::{Pos, Range}; + + fn range(line: u32, from: u32, to: u32) -> Range { + Range::new(Pos::new(line, from), Pos::new(line, to)) + } + + fn plugin(markdown: &str, at: Option) -> PluginHover { + PluginHover { markdown: markdown.into(), range: at } + } + + fn server(kind: &str, value: &str, at: Option) -> Hover { + Hover { contents: Markup { kind: kind.into(), value: value.into() }, range: at } + } + + #[test] + fn plugin_content_follows_the_server_hover() { + let merged = merge( + Some(server("markdown", "```php\nfunction route()\n```", Some(range(3, 4, 9)))), + vec![plugin("**orders.index** · `GET /orders`", Some(range(3, 10, 22)))], + ) + .unwrap(); + assert_eq!(merged.contents.kind, "markdown"); + assert_eq!(merged.contents.value, "```php\nfunction route()\n```\n\n---\n\n**orders.index** · `GET /orders`"); + assert_eq!(merged.range, Some(range(3, 4, 9))); + } + + #[test] + fn without_a_server_hover_the_plugins_answer_alone() { + let merged = merge(None, vec![plugin("one", None), plugin("two", Some(range(1, 0, 3)))]).unwrap(); + assert_eq!(merged.contents.value, "one\n\n---\n\ntwo"); + assert_eq!(merged.range, Some(range(1, 0, 3))); + assert!(merge(None, Vec::new()).is_none()); + } + + #[test] + fn plain_text_from_the_server_keeps_its_characters() { + let merged = merge(Some(server("plaintext", "a_b *c*\nd", None)), vec![plugin("x", None)]).unwrap(); + assert_eq!(merged.contents.value, "a\\_b \\*c\\* \nd\n\n---\n\nx"); + } + + #[test] + fn an_empty_server_hover_is_left_out() { + let merged = merge(Some(server("markdown", " ", None)), vec![plugin("x", None)]).unwrap(); + assert_eq!(merged.contents.value, "x"); + } + + #[test] + fn no_plugin_content_keeps_the_server_hover() { + let kept = merge(Some(server("plaintext", "a_b", Some(range(0, 0, 1)))), Vec::new()).unwrap(); + assert_eq!((kept.contents.kind.as_str(), kept.contents.value.as_str()), ("plaintext", "a_b")); + assert_eq!(kept.range, Some(range(0, 0, 1))); + } +} diff --git a/crates/ide-host/src/plugins/mod.rs b/crates/ide-host/src/plugins/mod.rs index 7d2d13c38..527142e71 100644 --- a/crates/ide-host/src/plugins/mod.rs +++ b/crates/ide-host/src/plugins/mod.rs @@ -16,6 +16,7 @@ //! projects) and folders linked with `openidea-plugin link` (`links.json`). mod api; +mod hover; mod icon_extensions; mod icon_patterns; mod icon_settings; @@ -38,6 +39,7 @@ pub mod views; pub mod web; pub use api::{FileSecretStore, HostWorkspace, PluginWorkspace, SecretStore}; +pub(crate) use hover::merge as merge_hover; pub use icons::{ MAX_PLUGIN_ICON, map_asset as icon_theme_map_asset, plugin_icon, sanitize_svg, svg_prefix as icon_theme_svg_prefix, }; diff --git a/crates/ide-host/src/plugins/providers.rs b/crates/ide-host/src/plugins/providers.rs index 7b8d15b82..b262b709a 100644 --- a/crates/ide-host/src/plugins/providers.rs +++ b/crates/ide-host/src/plugins/providers.rs @@ -38,6 +38,7 @@ pub enum ProviderKind { Definition, LineMarkers, SemanticTokens, + Hover, } #[derive(Clone, Debug, Deserialize)] @@ -116,6 +117,9 @@ impl ExtensionPoint for ProvidersPoint { if d.kinds.contains(&ProviderKind::SemanticTokens) { pkg.manifest.needs_api("contributes.providers: semantic-tokens", 3)?; } + if d.kinds.contains(&ProviderKind::Hover) { + pkg.manifest.needs_api("contributes.providers: hover", 3)?; + } for l in &d.languages { if l != "*" && (l.is_empty() @@ -164,7 +168,7 @@ pub(super) fn secret(_pkg: &Package, decl: toml::Value) -> Result Vec { + let d = Arc::new(doc.clone()); + self.edit_calls(ProviderKind::Hover, &doc.language, move |rt, timeout| rt.provide_hover(&d, at, timeout)) + .into_iter() + .filter_map(|(_, hover)| hover) + .collect() + } + /// Ghost text from the first plugin that has some. Not bound by the /// edit-path soft deadline (it may need the network) but by `cancel`: /// the editor cancels a request as soon as the user types on. diff --git a/crates/ide-host/tests/plugin_hover.rs b/crates/ide-host/tests/plugin_hover.rs new file mode 100644 index 000000000..dc90fef73 --- /dev/null +++ b/crates/ide-host/tests/plugin_hover.rs @@ -0,0 +1,55 @@ +use ide_core::Pos; +use ide_host::plugins::{Consent, PluginHost}; +use ide_plugin::{Limits, ProviderDocument}; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use std::time::Duration; + +fn fixture_dir() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("../ide-plugin/tests/fixtures/hover") +} + +fn host(dir: &Path) -> PluginHost { + let limits = Limits { edit_soft: Duration::from_secs(5), edit_hard: Duration::from_secs(10), ..Limits::default() }; + PluginHost::new(dir.join("plugins"), Arc::new(|_: Option<&str>, _| {}), limits).unwrap() +} + +fn doc(dir: &Path, language: &str, text: &str) -> ProviderDocument { + ProviderDocument { path: dir.join("Order.php"), language: language.into(), version: 1, text: text.into() } +} + +const ORDER: &str = "user;\n$order->total;\n"; + +#[test] +fn a_declared_name_gets_the_plugins_hover() { + let tmp = tempfile::tempdir().unwrap(); + let host = host(tmp.path()); + host.install_from_dir(&fixture_dir(), &Consent::all()).unwrap(); + let hovers = host.provide_hover(&doc(tmp.path(), "php", ORDER), Pos::new(5, 10)); + assert_eq!(hovers.len(), 1); + assert_eq!(hovers[0].markdown, "**user** · declared on line 4"); + let range = hovers[0].range.unwrap(); + assert_eq!((range.start.line, range.start.col, range.end.line, range.end.col), (5, 8, 5, 12)); + assert!(host.provide_hover(&doc(tmp.path(), "php", ORDER), Pos::new(6, 10)).is_empty()); +} + +#[test] +fn only_declared_languages_are_asked() { + let tmp = tempfile::tempdir().unwrap(); + let host = host(tmp.path()); + host.install_from_dir(&fixture_dir(), &Consent::all()).unwrap(); + assert!(host.provide_hover(&doc(tmp.path(), "javascript", ORDER), Pos::new(5, 10)).is_empty()); +} + +#[test] +fn the_kind_needs_plugin_api_1_3() { + let tmp = tempfile::tempdir().unwrap(); + let host = host(tmp.path()); + let pkg = tmp.path().join("old"); + std::fs::create_dir_all(pkg.join("dist")).unwrap(); + std::fs::copy(fixture_dir().join("dist/plugin.wasm"), pkg.join("dist/plugin.wasm")).unwrap(); + let manifest = std::fs::read_to_string(fixture_dir().join("plugin.toml")).unwrap(); + std::fs::write(pkg.join("plugin.toml"), manifest.replace("\"^1.3\"", "\"^1.0\"")).unwrap(); + let err = host.install_from_dir(&pkg, &Consent::all()).unwrap_err(); + assert!(err.message.contains("new in plugin API 1.3"), "{}", err.message); +} diff --git a/crates/ide-plugin/src/runtime.rs b/crates/ide-plugin/src/runtime.rs index c66014520..085a63950 100644 --- a/crates/ide-plugin/src/runtime.rs +++ b/crates/ide-plugin/src/runtime.rs @@ -4,7 +4,7 @@ //! themselves are in `wit_host`. use crate::types::{ - CodeAction, CompletionItem, InlineSuggestion, LineMarker, Location, ProviderDocument, SemanticToken, + CodeAction, CompletionItem, InlineSuggestion, LineMarker, Location, PluginHover, ProviderDocument, SemanticToken, }; use crate::types::{ EventKind, HostError, HttpRequest, HttpResponse, Invocation, Limits, NotificationLevel, ProcessOutput, Prompt, @@ -15,8 +15,8 @@ use crate::types::{ ViewField, ViewListItem, ViewTable, ViewTreeItem, Viewport, }; use crate::wit_host::{ - bindings, indexing_bindings, providers_bindings, semantic_bindings, services_bindings, syntax_bindings, - views_bindings, + bindings, hover_bindings, indexing_bindings, providers_bindings, semantic_bindings, services_bindings, + syntax_bindings, views_bindings, }; use ide_core::CancellationToken; use ide_core::ipc_path::to_ipc; @@ -33,6 +33,7 @@ use wasmtime_wasi::{WasiCtx, WasiCtxView, WasiView}; use bindings::openidea::plugin::events as wit_events; use bindings::openidea::plugin::types as wit_types; +use hover_bindings::exports::openidea::plugin::hover_provider as wit_hover; use indexing_bindings::exports::openidea::plugin::document_indexer as wit_index; use providers_bindings::exports::openidea::plugin::providers as wit_prov; use semantic_bindings::exports::openidea::plugin::semantic_tokens_provider as wit_sem; @@ -524,6 +525,7 @@ struct Exports { views: Option, semantic: Option, services: Option, + hover: Option, } /// Most results one syntax call may return (more are dropped). @@ -605,6 +607,7 @@ impl Runtime { let view_indices = wit_view::GuestIndices::new(&pre).ok(); let semantic_indices = wit_sem::GuestIndices::new(&pre).ok(); let service_indices = wit_svc::GuestIndices::new(&pre).ok(); + let hover_indices = wit_hover::GuestIndices::new(&pre).ok(); let instance = pre.instantiate(&mut store).map_err(api_hint)?; let plugin = plugin_indices.load(&mut store, &instance).map_err(api_hint)?; let syntax = match syntax_indices { @@ -631,9 +634,13 @@ impl Runtime { Some(indices) => Some(indices.load(&mut store, &instance).map_err(api_hint)?), None => None, }; + let hover = match hover_indices { + Some(indices) => Some(indices.load(&mut store, &instance).map_err(api_hint)?), + None => None, + }; Ok(Self { store, - exports: Exports { plugin, syntax, providers, views, indexer, semantic, services }, + exports: Exports { plugin, syntax, providers, views, indexer, semantic, services, hover }, limits: setup.limits, version: setup.version, }) @@ -891,6 +898,29 @@ impl Runtime { .collect()) } + pub fn provide_hover( + &mut self, + doc: &ProviderDocument, + at: ide_core::Pos, + timeout: Duration, + ) -> Result, CallError> { + if self.exports.hover.is_none() { + return Err(CallError::missing_export("hover-provider")); + } + let d = wit_hover::Document { + path: to_ipc(&doc.path), + language: doc.language.clone(), + version: doc.version, + text: doc.text.clone(), + }; + let hover = self.call(Invocation::default(), timeout, |e, store| { + e.hover.as_ref().expect("checked").call_hover(store, &d, pos_to_wit(at)) + })?; + Ok(hover + .filter(|h| !h.markdown.trim().is_empty()) + .map(|h| PluginHover { markdown: clip(h.markdown, MAX_HOVER_BYTES), range: h.range.map(range_from_wit) })) + } + pub fn activate(&mut self) -> Result<(), CallError> { let data = self.store.data(); let ctx = wit_types::Context { @@ -1068,6 +1098,7 @@ impl Runtime { const MAX_PROVIDER_ITEMS: usize = 10_000; const MAX_SEMANTIC_TOKENS: usize = 1 << 16; const MAX_TOKEN_MODIFIERS: usize = 8; +const MAX_HOVER_BYTES: usize = 64 * 1024; fn pos_to_wit(p: ide_core::Pos) -> wit_types::Position { wit_types::Position { line: p.line, character: p.col } diff --git a/crates/ide-plugin/src/types.rs b/crates/ide-plugin/src/types.rs index cd101ab59..f0d2989a9 100644 --- a/crates/ide-plugin/src/types.rs +++ b/crates/ide-plugin/src/types.rs @@ -426,6 +426,12 @@ pub struct SemanticToken { pub modifiers: Vec, } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PluginHover { + pub markdown: String, + pub range: Option, +} + // ---- declarative views (wit/views.wit) --------------------------------------- #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)] diff --git a/crates/ide-plugin/src/wit_host.rs b/crates/ide-plugin/src/wit_host.rs index f2326035a..43ee90b66 100644 --- a/crates/ide-plugin/src/wit_host.rs +++ b/crates/ide-plugin/src/wit_host.rs @@ -834,3 +834,10 @@ pub(crate) mod semantic_bindings { with: { "openidea:plugin/types": super::bindings::openidea::plugin::types }, }); } + +pub(crate) mod hover_bindings { + wasmtime::component::bindgen!({ + world: "hover", path: "../../wit", + with: { "openidea:plugin/types": super::bindings::openidea::plugin::types }, + }); +} diff --git a/crates/ide-plugin/tests/api_compat.rs b/crates/ide-plugin/tests/api_compat.rs index 1b33ccf35..40ec97c09 100644 --- a/crates/ide-plugin/tests/api_compat.rs +++ b/crates/ide-plugin/tests/api_compat.rs @@ -29,6 +29,7 @@ fn components() -> Vec { repo().join("crates/ide-plugin/tests/fixtures/hello/dist/plugin.wasm"), repo().join("crates/ide-plugin/tests/fixtures/indexer/dist/plugin.wasm"), repo().join("crates/ide-plugin/tests/fixtures/semantic-tokens/dist/plugin.wasm"), + repo().join("crates/ide-plugin/tests/fixtures/hover/dist/plugin.wasm"), ]; let official = repo().join("plugins/official"); let mut dirs: Vec<_> = std::fs::read_dir(&official).unwrap().map(|e| e.unwrap().path()).collect(); diff --git a/crates/ide-plugin/tests/fixtures/hello/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/hello/dist/plugin.wasm index 8baad7a88..457c8acf2 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 aaa350d53..b05b93efe 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 @@ -670aec4d79e92537efb7048698bbb8f236cd6006524c74459891b03c9b613450 +885badd4727639ad3205b2551d2b8406490fc05b72c684c5d17f09aadd9541bd diff --git a/crates/ide-plugin/tests/fixtures/hover/Cargo.lock b/crates/ide-plugin/tests/fixtures/hover/Cargo.lock new file mode 100644 index 000000000..244bd084f --- /dev/null +++ b/crates/ide-plugin/tests/fixtures/hover/Cargo.lock @@ -0,0 +1,333 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "equivalent" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "log" +version = "0.4.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d15f86d12da44cd8d4ec3c73984537eb7f4cadcb9776a575a0a8a05ca11005c" + +[[package]] +name = "macro-string" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e7b3a5bbb2f63aaa223a671ea1bd0e7bc16bd30ce387324cf63995deaddbbd2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "openidea-plugin" +version = "0.1.0" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "openidea-test-hover" +version = "0.1.0" +dependencies = [ + "openidea-plugin", +] + +[[package]] +name = "prettyplease" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bfe0f4c752e450fc2faf62654f1c134747922825d5b04ca717b8874f41a40c0" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "wasm-encoder" +version = "0.262.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4495a6f85dd94c411c749303efcbc5defade0b0cbdbc0541495241ab917a9b5a" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.262.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c86ff6668190d9fb70372b306b4f9fe8930f0e2f57ce72e4922bf2f117900949" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.262.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "974d643b125b272607e2fff8012ea14a8328acd7621e4bafa7afb32500052b1b" +dependencies = [ + "bitflags", + "hashbrown", + "indexmap", + "semver", +] + +[[package]] +name = "wit-bindgen" +version = "0.63.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b85a987ab61d365382d25c7d47e26edd48941b3bf6e3fbf76eff918bc71dab06" +dependencies = [ + "bitflags", + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.63.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775e122e390603abfacd548027016cd0113a6e2bf2f8ce6bdd3658b013456ea8" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.63.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84e26e13b9d972026778d2ffb7985736de116026fdc8d9464f966e12df2eb240" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.63.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16a008a0d6b4840677326beb4edbeb210a2f1b7d0183fb7cc33f466b90fd7fb6" +dependencies = [ + "anyhow", + "macro-string", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.262.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078def49fdb49cab405c3006e05793b3fdaf7069126ba6b9c97d01ec8c98fe4c" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.262.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00f0dc051466ffe46787721fca3d088536f158ae400be044f1b1c386ba9397ad" +dependencies = [ + "anyhow", + "hashbrown", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-ident", + "wasmparser", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/crates/ide-plugin/tests/fixtures/hover/Cargo.toml b/crates/ide-plugin/tests/fixtures/hover/Cargo.toml new file mode 100644 index 000000000..ddcabdff4 --- /dev/null +++ b/crates/ide-plugin/tests/fixtures/hover/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "openidea-test-hover" +version = "0.1.0" +edition = "2024" +publish = false + +[workspace] + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +openidea-plugin = { path = "../../../../../sdk/rust/openidea-plugin" } + +[profile.release] +opt-level = "s" +lto = true +strip = true +codegen-units = 1 +panic = "abort" diff --git a/crates/ide-plugin/tests/fixtures/hover/README.md b/crates/ide-plugin/tests/fixtures/hover/README.md new file mode 100644 index 000000000..97c92d793 --- /dev/null +++ b/crates/ide-plugin/tests/fixtures/hover/README.md @@ -0,0 +1,7 @@ +# Hover test fixture + +Over a name the PHP document declares as `function (`, shows +`**** · declared on line ` for the name's range; nothing elsewhere. + +Used by `cargo test -p ide-host --test plugin_hover` to exercise the optional +`hover-provider` export. It is not a marketplace plugin. diff --git a/crates/ide-plugin/tests/fixtures/hover/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/hover/dist/plugin.wasm new file mode 100644 index 000000000..5ca3b5c9c Binary files /dev/null and b/crates/ide-plugin/tests/fixtures/hover/dist/plugin.wasm differ diff --git a/crates/ide-plugin/tests/fixtures/hover/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/hover/dist/plugin.wasm.sources new file mode 100644 index 000000000..5f1033926 --- /dev/null +++ b/crates/ide-plugin/tests/fixtures/hover/dist/plugin.wasm.sources @@ -0,0 +1 @@ +ff1b39ac7850fbe4495e274dd78cca4c906a20705e8b9a975f58cf564e8ff137 diff --git a/crates/ide-plugin/tests/fixtures/hover/plugin.toml b/crates/ide-plugin/tests/fixtures/hover/plugin.toml new file mode 100644 index 000000000..ff75a6374 --- /dev/null +++ b/crates/ide-plugin/tests/fixtures/hover/plugin.toml @@ -0,0 +1,9 @@ +id = "test.hover" +name = "Hover fixture" +version = "0.1.0" +engines.openidea = "^1.3" +wasm = "dist/plugin.wasm" + +[[contributes.providers]] +kinds = ["hover"] +languages = ["php"] diff --git a/crates/ide-plugin/tests/fixtures/hover/src/lib.rs b/crates/ide-plugin/tests/fixtures/hover/src/lib.rs new file mode 100644 index 000000000..77482baf5 --- /dev/null +++ b/crates/ide-plugin/tests/fixtures/hover/src/lib.rs @@ -0,0 +1,31 @@ +use openidea_plugin::hover::{Document, Hover, HoverProvider}; +use openidea_plugin::{Error, Invocation, Plugin, Position, Range, register_plugin, text}; + +#[derive(Default)] +struct Fixture; + +impl Plugin for Fixture { + fn run_command(&mut self, id: &str, _: &Invocation, _: Option<&str>) -> Result, Error> { + Err(Error::NotFound(id.into())) + } +} + +impl HoverProvider for Fixture { + fn hover(&mut self, doc: &Document, at: Position) -> Option { + let text = doc.text.as_str(); + let offset = text::offset(text, at)?; + let word = |c: char| c.is_ascii_alphanumeric() || c == '_'; + let start = text[..offset].rfind(|c: char| !word(c)).map_or(0, |i| i + 1); + let end = text[offset..].find(|c: char| !word(c)).map_or(text.len(), |n| offset + n); + let name = &text[start..end]; + if name.is_empty() { + return None; + } + let declared = text.find(&format!("function {name}("))?; + let line = text::position(text, declared).line + 1; + let range = Range { start: text::position(text, start), end: text::position(text, end) }; + Some(Hover::new(format!("**{name}** · declared on line {line}")).range(range)) + } +} + +register_plugin!(Fixture, hover); diff --git a/crates/ide-plugin/tests/fixtures/indexer/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/indexer/dist/plugin.wasm index 473c8cfe1..a76441e22 100644 Binary files a/crates/ide-plugin/tests/fixtures/indexer/dist/plugin.wasm and b/crates/ide-plugin/tests/fixtures/indexer/dist/plugin.wasm differ diff --git a/crates/ide-plugin/tests/fixtures/indexer/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/indexer/dist/plugin.wasm.sources index 14301c47f..7d1c7e22a 100644 --- a/crates/ide-plugin/tests/fixtures/indexer/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/indexer/dist/plugin.wasm.sources @@ -1 +1 @@ -c913c2c94fecb43d00f73f3171cc2fda54caf7a28f170424b0addb1d40ed4cae +c9bb4bc3603d9e7c770cf13ad413f5b3a787e78dbeed8f41a3b8537c0740275d diff --git a/crates/ide-plugin/tests/fixtures/jobs/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/jobs/dist/plugin.wasm index 7c3273700..43c324559 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 cb880be2a..fa9bc67f9 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 @@ -911388ae62843d7b1e4c40d91307fbe507535c60680a9626cc07d724b75a9dfb +a9cb7302b86e12e05cbbf87fb402609e2f88d3a463f6c6e96b254e643b76183d diff --git a/crates/ide-plugin/tests/fixtures/misbehave/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/misbehave/dist/plugin.wasm index f7e985dc9..1c84e19be 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 f968ef132..8b5ca350f 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 @@ -a3818b6ec40df8f141290537a7c024aadb02d7be1eb063ca9170799ca3271898 +0be22488fe8f20a59f48fc61089b3db791a5d723d71e4d654bfd008038c01003 diff --git a/crates/ide-plugin/tests/fixtures/semantic-tokens/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/semantic-tokens/dist/plugin.wasm index cfedbe513..fcaf30d35 100644 Binary files a/crates/ide-plugin/tests/fixtures/semantic-tokens/dist/plugin.wasm and b/crates/ide-plugin/tests/fixtures/semantic-tokens/dist/plugin.wasm differ diff --git a/crates/ide-plugin/tests/fixtures/semantic-tokens/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/semantic-tokens/dist/plugin.wasm.sources index cadc9c206..083d74781 100644 --- a/crates/ide-plugin/tests/fixtures/semantic-tokens/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/semantic-tokens/dist/plugin.wasm.sources @@ -1 +1 @@ -9e4c12fdf905227128d97e92639cf336124929c37384e5f5796e27ebfc5a7b37 +92ca920847c51036ac6dc7acd299ff9afaef9b4037e0112a0159bf50e822c7f3 diff --git a/plugins/README.md b/plugins/README.md index e0637f12b..337acad84 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -34,7 +34,7 @@ plugins/ ├─ regex-tester/ # html view talking to its code, settings, the editor's selection └─ license-header/ # file-opened events, quick fix, project scan in a task sdk/rust/openidea-plugin/ # the Rust SDK (host API, Plugin trait, mock host) -crates/ide-plugin/tests/fixtures/ # test plugins: hello, jobs, language-toy, misbehave, semantic-tokens +crates/ide-plugin/tests/fixtures/ # test plugins: hello, jobs, language-toy, misbehave, semantic-tokens, hover ``` The plugin API is defined in [`wit/`](../wit/) (`openidea:plugin@1.3.0`, world @@ -402,7 +402,7 @@ declaration matches the file's language are called (and loaded): ```toml [[contributes.providers]] -kinds = ["definition", "line-markers", "completion"] # also "inline-completion", "code-actions", "semantic-tokens" +kinds = ["definition", "line-markers", "completion"] # also "inline-completion", "code-actions", "semantic-tokens", "hover" languages = ["java", "xml"] # editor language ids; "*" for any ``` @@ -457,6 +457,26 @@ the language server's semantic tokens and win where they overlap one, so the editor's colour scheme styles them like the server's. They are asked again after each edit, under the edit-path budget; at most 65,536 per document. +#### Hover + +The `hover` kind (API 1.3) adds Markdown to the hover of documents of the +declared languages, here too including languages the plugin doesn't +contribute. Register with `register_plugin!(MyPlugin, hover)` and implement +`openidea_plugin::hover::HoverProvider`: + +```rust +impl HoverProvider for MyPlugin { + fn hover(&mut self, doc: &Document, at: Position) -> Option { + Some(Hover::new("**orders.index** · `GET /orders`")) + } +} +``` + +The host asks the language server and the plugins together, under the +edit-path budget, and shows the server's hover first, then each plugin's +below a rule. `range`, when given, is the text the hover describes (used when +the server names none). At most 64 KiB of Markdown per plugin. + ### Network and secrets `host::fetch` makes an HTTPS request (plain HTTP only to `localhost`) to a host diff --git a/plugins/official/ai-completion/dist/plugin.wasm b/plugins/official/ai-completion/dist/plugin.wasm index c91fb19f8..31eeb2c1d 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 10714c313..ee8de7458 100644 --- a/plugins/official/ai-completion/dist/plugin.wasm.sources +++ b/plugins/official/ai-completion/dist/plugin.wasm.sources @@ -1 +1 @@ -eb76da4bc9db84dcbad34143bac18c9ecf974276f793009791a15aab2eee291d +9384d8964c7c05d3df06521e0d724a932fdd17608cbcede13ec2acadfab3783c diff --git a/plugins/official/checkstyle/dist/plugin.wasm b/plugins/official/checkstyle/dist/plugin.wasm index 49d583963..1b5f253f3 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 346ce1ca0..dcbf2f781 100644 --- a/plugins/official/checkstyle/dist/plugin.wasm.sources +++ b/plugins/official/checkstyle/dist/plugin.wasm.sources @@ -1 +1 @@ -0a19948927a23de81b2a453b9992e021744093cbe41ea92daa9feb2b1bc9a2a9 +9d7e857ef367eaa9748d1a2b05b7b45226c254aa578c29e1ed987c7353738acf diff --git a/plugins/official/csv-editor/dist/plugin.wasm b/plugins/official/csv-editor/dist/plugin.wasm index c32998eac..ddf8435b6 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 129770a9f..f2b224e82 100644 --- a/plugins/official/csv-editor/dist/plugin.wasm.sources +++ b/plugins/official/csv-editor/dist/plugin.wasm.sources @@ -1 +1 @@ -564bfd00694f27f35f09eb0b81d341207a755528b5fe7c46c96717b3868d0f65 +3e11039d36306f51c728bf9d08030fe620de461fa5b0037700aea95c4d27714c diff --git a/plugins/official/git-toolbox/dist/plugin.wasm b/plugins/official/git-toolbox/dist/plugin.wasm index d12bd47c9..860581e9e 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 630ba0c84..22b2d4551 100644 --- a/plugins/official/git-toolbox/dist/plugin.wasm.sources +++ b/plugins/official/git-toolbox/dist/plugin.wasm.sources @@ -1 +1 @@ -f3f447366fa38f9bf4911f35b6e899405b0e072ceee3ad5ea2fe90639d70fa53 +ef50957a407c9aff9999fdd197a5e72c90b7ba77aedbd7569408262ab7e158be diff --git a/plugins/official/graphviz/dist/plugin.wasm b/plugins/official/graphviz/dist/plugin.wasm index 0806128d3..fac2c1dd2 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 5f9e986af..f35411f09 100644 --- a/plugins/official/graphviz/dist/plugin.wasm.sources +++ b/plugins/official/graphviz/dist/plugin.wasm.sources @@ -1 +1 @@ -85cad466adb94f2b58e62263617e885d1c47cf70db5e8b38942bd415f0d779af +7160b309f39fe973e67bb20d361384955aed6afe98f00db329c3026ffe923759 diff --git a/plugins/official/key-promoter/dist/plugin.wasm b/plugins/official/key-promoter/dist/plugin.wasm index bba7cd927..e1e98c0b9 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 613dd1d17..6ace343a1 100644 --- a/plugins/official/key-promoter/dist/plugin.wasm.sources +++ b/plugins/official/key-promoter/dist/plugin.wasm.sources @@ -1 +1 @@ -659446eda0b50aa0fb08f6af68f7d355fcaf3342847e2e579999dcf52a7a7399 +1f1afa285a64a7f7758f08a80306d6397c8aee4d2ac7ed43369e77aae61add04 diff --git a/plugins/official/license-header/dist/plugin.wasm b/plugins/official/license-header/dist/plugin.wasm index 3906ca057..65069c48a 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 aaf5f027a..9d7fa01ca 100644 --- a/plugins/official/license-header/dist/plugin.wasm.sources +++ b/plugins/official/license-header/dist/plugin.wasm.sources @@ -1 +1 @@ -51ef9d43315ca9d7239999c222fbf7a2c01bc85c2b40f53f34a1a8e5d31baa6d +ddd9205da9fc947215ab90c505d8b443d0937b1dd11734e9ce94d277517c77a6 diff --git a/plugins/official/mybatis/dist/plugin.wasm b/plugins/official/mybatis/dist/plugin.wasm index 5201faad7..35a01268f 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 bab71bf24..7e3fadea1 100644 --- a/plugins/official/mybatis/dist/plugin.wasm.sources +++ b/plugins/official/mybatis/dist/plugin.wasm.sources @@ -1 +1 @@ -bdf9455c6e7c8c340c54d783f351220b38cffed36907e1b5a1f2e11801e6f5bb +c61a0ec6ffcbd71e590e5a16a6f08f2d2772481497edc77e11914cfb3d0bfcd1 diff --git a/plugins/official/regex-tester/dist/plugin.wasm b/plugins/official/regex-tester/dist/plugin.wasm index bbc75cf59..6354495ec 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 894025033..556c3a33c 100644 --- a/plugins/official/regex-tester/dist/plugin.wasm.sources +++ b/plugins/official/regex-tester/dist/plugin.wasm.sources @@ -1 +1 @@ -7e4d00e1ea63b2eca48b9297667e04c05a9f9832f1dee2262272801d516b9a16 +e11cd0b664ae058274808bc2b2152bac2738207979758d13bc0c0cd96c38ef4f diff --git a/plugins/official/solidity/dist/plugin.wasm b/plugins/official/solidity/dist/plugin.wasm index c04b1123a..13de9cab7 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 953175777..da046972e 100644 --- a/plugins/official/solidity/dist/plugin.wasm.sources +++ b/plugins/official/solidity/dist/plugin.wasm.sources @@ -1 +1 @@ -00cbd2dd7f0937554633ba5e533f19be85f70579c7c6fedb6fe0228f8f63bf4c +59319cf4be14448b0126b07d074cc08c351cfd12a3de26f111c2445fe8bff664 diff --git a/plugins/official/string-tools/dist/plugin.wasm b/plugins/official/string-tools/dist/plugin.wasm index c4cc0b12e..7bc58b5a0 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 094c8bb6e..c3e2eaa71 100644 --- a/plugins/official/string-tools/dist/plugin.wasm.sources +++ b/plugins/official/string-tools/dist/plugin.wasm.sources @@ -1 +1 @@ -eecf2cf561dd5e3e3edbf9f67ccb479573dd538a75d4426a1d086edf0a310043 +59844ae52aa765af404b43f7895db94d0063a3a6fce5bd54bf9af680ccac357b diff --git a/sdk/rust/openidea-plugin/src/hover.rs b/sdk/rust/openidea-plugin/src/hover.rs new file mode 100644 index 000000000..0c2647d4d --- /dev/null +++ b/sdk/rust/openidea-plugin/src/hover.rs @@ -0,0 +1,38 @@ +use crate::types::{Position, Range}; + +pub use crate::providers::Document; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Hover { + pub markdown: String, + pub range: Option, +} + +impl Hover { + pub fn new(markdown: impl Into) -> Self { + Self { markdown: markdown.into(), range: None } + } + + pub fn range(mut self, range: Range) -> Self { + self.range = Some(range); + self + } +} + +pub trait HoverProvider { + fn hover(&mut self, doc: &Document, at: Position) -> Option; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_hover_can_name_the_range_it_describes() { + let range = Range { start: Position { line: 2, character: 4 }, end: Position { line: 2, character: 9 } }; + let hover = Hover::new("**route** `orders.index`").range(range); + assert_eq!(hover.markdown, "**route** `orders.index`"); + assert_eq!(hover.range, Some(range)); + assert_eq!(Hover::new("x").range, None); + } +} diff --git a/sdk/rust/openidea-plugin/src/lib.rs b/sdk/rust/openidea-plugin/src/lib.rs index 0c0a44d31..61625dbbd 100644 --- a/sdk/rust/openidea-plugin/src/lib.rs +++ b/sdk/rust/openidea-plugin/src/lib.rs @@ -40,6 +40,7 @@ //! wasm32-wasip2`); see `plugins/README.md`. pub mod host; +pub mod hover; pub mod indexing; #[cfg(not(target_arch = "wasm32"))] pub mod mock; @@ -155,6 +156,17 @@ pub mod semantic_bindings { }); } +#[cfg(target_arch = "wasm32")] +#[doc(hidden)] +pub mod hover_bindings { + wit_bindgen::generate!({ + world: "hover", path: "wit", pub_export_macro: true, + export_macro_name: "__export_hover_world", + default_bindings_module: "openidea_plugin::hover_bindings", + with: { "openidea:plugin/types@1.3.0": crate::bindings::openidea::plugin::types }, + }); +} + /// The plugin API version this SDK targets. pub const API_VERSION: &str = "1.3"; @@ -215,6 +227,9 @@ macro_rules! __optional_exports { (services, $plugin:ty, $component:ident) => { $crate::__services_exports!($plugin, $component); }; + (hover, $plugin:ty, $component:ident) => { + $crate::__hover_exports!($plugin, $component); + }; (syntax, $plugin:ty, $component:ident) => { impl $crate::syntax_bindings::exports::openidea::plugin::syntax_provider::Guest for $component { fn highlights( @@ -384,6 +399,22 @@ macro_rules! __semantic_tokens_exports { }; } +#[doc(hidden)] +#[macro_export] +macro_rules! __hover_exports { + ($plugin:ty, $component:ident) => { + impl $crate::hover_bindings::exports::openidea::plugin::hover_provider::Guest for $component { + fn hover( + doc: $crate::hover_bindings::exports::openidea::plugin::hover_provider::Document, + at: $crate::bindings::openidea::plugin::types::Position, + ) -> ::core::option::Option<$crate::hover_bindings::exports::openidea::plugin::hover_provider::HoverContent> { + $crate::__rt::hover::<$plugin>(doc, at) + } + } + $crate::hover_bindings::__export_hover_world!($component with_types_in $crate::hover_bindings); + }; +} + #[doc(hidden)] #[macro_export] macro_rules! __plugin_exports { @@ -639,6 +670,16 @@ pub mod __rt { .collect() } + pub fn hover( + d: crate::hover_bindings::exports::openidea::plugin::hover_provider::Document, + pos: b::types::Position, + ) -> Option { + use crate::hover_bindings::exports::openidea::plugin::hover_provider as w; + let doc = crate::providers::Document { path: d.path, language: d.language, version: d.version, text: d.text }; + with::(|p| p.hover(&doc, at(pos))) + .map(|h| w::HoverContent { markdown: h.markdown, range: h.range.map(crate::host::__range_to) }) + } + // ---- views ---- use crate::views as v; diff --git a/sdk/rust/openidea-plugin/wit/hover.wit b/sdk/rust/openidea-plugin/wit/hover.wit new file mode 100644 index 000000000..ddc0d174a --- /dev/null +++ b/sdk/rust/openidea-plugin/wit/hover.wit @@ -0,0 +1,19 @@ +package openidea:plugin@1.3.0; + +interface hover-provider { + use types.{position, range}; + + record document { + path: string, + language: string, + version: u64, + text: string, + } + + record hover-content { + markdown: string, + range: option, + } + + hover: func(doc: document, at: position) -> option; +} diff --git a/sdk/rust/openidea-plugin/wit/plugin.wit b/sdk/rust/openidea-plugin/wit/plugin.wit index 559abf97e..f6d5970b1 100644 --- a/sdk/rust/openidea-plugin/wit/plugin.wit +++ b/sdk/rust/openidea-plugin/wit/plugin.wit @@ -61,6 +61,11 @@ world plugin-semantic-tokens { export semantic-tokens-provider; } +world plugin-hover { + include plugin; + export hover-provider; +} + /// A plugin whose views show in the Services tool window /// (`location = "services"`): their nodes' detail pane. world plugin-services { @@ -93,6 +98,10 @@ world semantic-tokens { export semantic-tokens-provider; } +world hover { + export hover-provider; +} + /// Only the `service-provider` export, generated next to `plugin` like `syntax`. world services { export service-provider; diff --git a/wit/hover.wit b/wit/hover.wit new file mode 100644 index 000000000..ddc0d174a --- /dev/null +++ b/wit/hover.wit @@ -0,0 +1,19 @@ +package openidea:plugin@1.3.0; + +interface hover-provider { + use types.{position, range}; + + record document { + path: string, + language: string, + version: u64, + text: string, + } + + record hover-content { + markdown: string, + range: option, + } + + hover: func(doc: document, at: position) -> option; +} diff --git a/wit/plugin.wit b/wit/plugin.wit index 559abf97e..f6d5970b1 100644 --- a/wit/plugin.wit +++ b/wit/plugin.wit @@ -61,6 +61,11 @@ world plugin-semantic-tokens { export semantic-tokens-provider; } +world plugin-hover { + include plugin; + export hover-provider; +} + /// A plugin whose views show in the Services tool window /// (`location = "services"`): their nodes' detail pane. world plugin-services { @@ -93,6 +98,10 @@ world semantic-tokens { export semantic-tokens-provider; } +world hover { + export hover-provider; +} + /// Only the `service-provider` export, generated next to `plugin` like `syntax`. world services { export service-provider;