feat(plugins): let plugins show inlay hints in any language (#1937)

A new optional inlay-hints-provider export (worlds plugin-inlay-hints / inlay-hints), declared as the inlay-hints provider kind for any language (API 1.3). The host asks the language server and the plugins together for the visible range and returns both in position order; with no server hints the plugins' hints still show. Kinds map to the Inlay Hints settings; labels, tooltips and counts are capped.

Co-authored-by: yzPeedro <[email protected]>
Цей коміт міститься в:
Pedro Pessoa authored and GitHub committed 2026-11-07 19:25:10 +08:00
1 джерело c614355753
коміт 56796fb2c6
60 files changed
+953 -27

No files matched your search

+16 -1
Переглянути файл
@@ -677,7 +677,22 @@ pub async fn lsp_format(
}
pub async fn lsp_inlay_hints(host: &Host, window: &str, doc: DocId, range: Range) -> IdeResult<Vec<InlayHint>> {
host.project(window)?.lsp.inlay_hints(doc, range).await
let lsp = host.project(window)?.lsp.clone();
let Some((plugins, d)) = plugin_providers(host, window, doc, ProviderKind::InlayHints) else {
return lsp.inlay_hints(doc, range).await;
};
let (result, hints) =
tokio::join!(lsp.inlay_hints(doc, range), plugin_call(move || plugins.provide_inlay_hints(&d, range)));
let hints = hints.unwrap_or_default();
if hints.is_empty() {
return result;
}
let server = match result {
Ok(server) => server,
Err(e) if matches!(e.kind, ErrorKind::NotFound | ErrorKind::Unavailable) => Vec::new(),
Err(e) => return Err(e),
};
Ok(crate::plugins::merge_inlay_hints(server, hints, range))
}
pub async fn lsp_semantic_tokens(host: &Host, window: &str, doc: DocId) -> IdeResult<Option<SemanticTokens>> {
+94
Переглянути файл
@@ -0,0 +1,94 @@
use ide_core::Range;
use ide_lsp::types::{InlayHint, InlayKind};
use ide_plugin::PluginInlayHint;
pub(crate) fn merge(server: Vec<InlayHint>, plugin: Vec<PluginInlayHint>, range: Range) -> Vec<InlayHint> {
let inside = |h: &PluginInlayHint| {
let at = (h.position.line, h.position.col);
(range.start.line, range.start.col) <= at && at <= (range.end.line, range.end.col)
};
let mut out = server;
out.extend(plugin.into_iter().filter(inside).map(|h| InlayHint {
position: h.position,
label: h.label,
kind: match h.kind.as_str() {
"type" => InlayKind::Type,
"parameter" => InlayKind::Parameter,
_ => InlayKind::Other,
},
padding_left: h.padding_left,
padding_right: h.padding_right,
tooltip: h.tooltip,
parts: Vec::new(),
}));
out.sort_by_key(|h| (h.position.line, h.position.col));
out
}
#[cfg(test)]
mod tests {
use super::*;
use ide_core::Pos;
fn plugin(line: u32, col: u32, label: &str, kind: &str) -> PluginInlayHint {
PluginInlayHint {
position: Pos::new(line, col),
label: label.into(),
kind: kind.into(),
tooltip: Some("from the plugin".into()),
padding_left: true,
padding_right: false,
}
}
fn server(line: u32, col: u32, label: &str) -> InlayHint {
InlayHint {
position: Pos::new(line, col),
label: label.into(),
kind: InlayKind::Parameter,
padding_left: false,
padding_right: true,
tooltip: None,
parts: Vec::new(),
}
}
fn summary(hints: &[InlayHint]) -> Vec<(u32, u32, String)> {
hints.iter().map(|h| (h.position.line, h.position.col, h.label.clone())).collect()
}
#[test]
fn plugin_hints_join_the_servers_in_position_order() {
let range = Range::new(Pos::new(0, 0), Pos::new(10, 0));
let merged = merge(
vec![server(2, 4, "name:"), server(5, 1, "id:")],
vec![plugin(3, 9, ": string", "type"), plugin(2, 1, "GET /orders", "other")],
range,
);
assert_eq!(
summary(&merged),
[
(2, 1, "GET /orders".to_owned()),
(2, 4, "name:".to_owned()),
(3, 9, ": string".to_owned()),
(5, 1, "id:".to_owned())
]
);
let typed = merged.iter().find(|h| h.label == ": string").unwrap();
assert!(matches!(typed.kind, InlayKind::Type));
assert!(typed.padding_left && !typed.padding_right);
assert_eq!(typed.tooltip.as_deref(), Some("from the plugin"));
}
#[test]
fn unknown_kinds_are_other_and_hints_outside_the_range_are_dropped() {
let range = Range::new(Pos::new(2, 5), Pos::new(4, 3));
let merged = merge(
Vec::new(),
vec![plugin(2, 4, "before", "type"), plugin(3, 0, "in", "route"), plugin(4, 4, "after", "type")],
range,
);
assert_eq!(summary(&merged), [(3, 0, "in".to_owned())]);
assert!(matches!(merged[0].kind, InlayKind::Other));
}
}
+2
Переглянути файл
@@ -23,6 +23,7 @@ mod icon_patterns;
mod icon_settings;
mod icons;
pub mod index_ops;
mod inlay_hints;
mod install;
pub mod intellij;
mod market;
@@ -44,6 +45,7 @@ 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,
};
pub(crate) use inlay_hints::merge as merge_inlay_hints;
pub use market::{
INDEX_FILE as MARKET_INDEX_FILE, MarketPlugin, OFFICIAL_INDEX as OFFICIAL_MARKET_INDEX, OFFICIAL_PUBLISHER,
read_url as read_market_url, resolve as resolve_market_href,
+15 -1
Переглянути файл
@@ -39,6 +39,7 @@ pub enum ProviderKind {
LineMarkers,
SemanticTokens,
Hover,
InlayHints,
}
#[derive(Clone, Debug, Deserialize)]
@@ -120,6 +121,9 @@ impl ExtensionPoint for ProvidersPoint {
if d.kinds.contains(&ProviderKind::Hover) {
pkg.manifest.needs_api("contributes.providers: hover", 3)?;
}
if d.kinds.contains(&ProviderKind::InlayHints) {
pkg.manifest.needs_api("contributes.providers: inlay-hints", 3)?;
}
for l in &d.languages {
if l != "*"
&& (l.is_empty()
@@ -169,7 +173,7 @@ use super::{LoadedPlugin, PluginHost, PluginStatus};
use ide_core::CancellationToken;
use ide_plugin::{
ActorError, CallError, CodeAction, CompletionItem, InlineSuggestion, LineMarker, Location, Pending, PluginHover,
ProviderDocument, Runtime, SemanticToken,
PluginInlayHint, ProviderDocument, Runtime, SemanticToken,
};
use std::sync::Weak;
use std::time::{Duration, Instant};
@@ -357,6 +361,16 @@ impl PluginHost {
.collect()
}
pub fn provide_inlay_hints(&self, doc: &ProviderDocument, range: ide_core::Range) -> Vec<PluginInlayHint> {
let d = Arc::new(doc.clone());
self.edit_calls(ProviderKind::InlayHints, &doc.language, move |rt, timeout| {
rt.provide_inlay_hints(&d, range, timeout)
})
.into_iter()
.flat_map(|(_, hints)| hints)
.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.
+59
Переглянути файл
@@ -0,0 +1,59 @@
use ide_core::{Pos, Range};
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/inlay-hints")
}
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 = "<?php\nclass Order\n{\n public function user(): ?User {}\n}\n$order->user();\n$order->total();\n$order->user;\n";
fn all() -> Range {
Range::new(Pos::new(0, 0), Pos::new(100, 0))
}
#[test]
fn calls_of_declared_methods_get_a_type_hint() {
let tmp = tempfile::tempdir().unwrap();
let host = host(tmp.path());
host.install_from_dir(&fixture_dir(), &Consent::all()).unwrap();
let hints = host.provide_inlay_hints(&doc(tmp.path(), "php", ORDER), all());
let found: Vec<_> =
hints.iter().map(|h| (h.position.line, h.position.col, h.label.as_str(), h.kind.as_str())).collect();
assert_eq!(found, [(5, 14, ": ?User", "type")]);
assert_eq!(hints[0].tooltip.as_deref(), Some("user() returns ?User"));
assert!(!hints[0].padding_left && !hints[0].padding_right);
}
#[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_inlay_hints(&doc(tmp.path(), "javascript", ORDER), all()).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);
}
+56 -4
Переглянути файл
@@ -5,7 +5,7 @@
use crate::types::{
CodeAction, CommitDraft, CommitProblem, CompletionItem, InlineSuggestion, LineMarker, Location, PluginHover,
ProviderDocument, SemanticToken,
PluginInlayHint, ProviderDocument, SemanticToken,
};
use crate::types::{
EventKind, HostError, HttpRequest, HttpResponse, Invocation, Limits, NotificationLevel, ProcessOutput, Prompt,
@@ -16,8 +16,8 @@ use crate::types::{
ViewField, ViewListItem, ViewTable, ViewTreeItem, Viewport,
};
use crate::wit_host::{
bindings, commit_checks_bindings, details_bindings, hover_bindings, indexing_bindings, providers_bindings,
semantic_bindings, syntax_bindings, views_bindings,
bindings, commit_checks_bindings, details_bindings, hover_bindings, indexing_bindings, inlay_bindings,
providers_bindings, semantic_bindings, syntax_bindings, views_bindings,
};
use ide_core::CancellationToken;
use ide_core::ipc_path::to_ipc;
@@ -39,6 +39,7 @@ use details_bindings::exports::openidea::plugin::detail_provider as wit_detail;
use details_bindings::openidea::plugin::detail_types as wit_dt;
use hover_bindings::exports::openidea::plugin::hover_provider as wit_hover;
use indexing_bindings::exports::openidea::plugin::document_indexer as wit_index;
use inlay_bindings::exports::openidea::plugin::inlay_hints_provider as wit_inlay;
use providers_bindings::exports::openidea::plugin::providers as wit_prov;
use semantic_bindings::exports::openidea::plugin::semantic_tokens_provider as wit_sem;
use syntax_bindings::exports::openidea::plugin::syntax_provider as wit_syntax;
@@ -545,6 +546,7 @@ struct Exports {
details: Option<wit_detail::Guest>,
hover: Option<wit_hover::Guest>,
commit_checks: Option<wit_cc::Guest>,
inlay: Option<wit_inlay::Guest>,
}
/// Most results one syntax call may return (more are dropped).
@@ -628,6 +630,7 @@ impl Runtime {
let detail_indices = wit_detail::GuestIndices::new(&pre).ok();
let hover_indices = wit_hover::GuestIndices::new(&pre).ok();
let commit_check_indices = wit_cc::GuestIndices::new(&pre).ok();
let inlay_indices = wit_inlay::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 {
@@ -662,9 +665,24 @@ impl Runtime {
Some(indices) => Some(indices.load(&mut store, &instance).map_err(api_hint)?),
None => None,
};
let inlay = match inlay_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, details, hover, commit_checks },
exports: Exports {
plugin,
syntax,
providers,
views,
indexer,
semantic,
details,
hover,
commit_checks,
inlay,
},
limits: setup.limits,
version: setup.version,
})
@@ -945,6 +963,40 @@ impl Runtime {
.map(|h| PluginHover { markdown: clip(h.markdown, MAX_HOVER_BYTES), range: h.range.map(range_from_wit) }))
}
pub fn provide_inlay_hints(
&mut self,
doc: &ProviderDocument,
range: ide_core::Range,
timeout: Duration,
) -> Result<Vec<PluginInlayHint>, CallError> {
if self.exports.inlay.is_none() {
return Err(CallError::missing_export("inlay-hints-provider"));
}
let d = wit_inlay::Document {
path: to_ipc(&doc.path),
language: doc.language.clone(),
version: doc.version,
text: doc.text.clone(),
};
let r = wit_types::Range { start: pos_to_wit(range.start), end: pos_to_wit(range.end) };
let hints = self.call(Invocation::default(), timeout, |e, store| {
e.inlay.as_ref().expect("checked").call_inlay_hints(store, &d, r)
})?;
Ok(hints
.into_iter()
.filter(|h| !h.label.trim().is_empty())
.take(MAX_PROVIDER_ITEMS)
.map(|h| PluginInlayHint {
position: ide_core::Pos::new(h.position.line, h.position.character),
label: clip(h.label, 256),
kind: clip(h.kind, 32),
tooltip: h.tooltip.map(|t| clip(t, 1024)),
padding_left: h.padding_left,
padding_right: h.padding_right,
})
.collect())
}
pub fn activate(&mut self) -> Result<(), CallError> {
let data = self.store.data();
let ctx = wit_types::Context {
+10
Переглянути файл
@@ -466,6 +466,16 @@ pub struct PluginHover {
pub range: Option<ide_core::Range>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PluginInlayHint {
pub position: ide_core::Pos,
pub label: String,
pub kind: String,
pub tooltip: Option<String>,
pub padding_left: bool,
pub padding_right: bool,
}
// ---- declarative views (wit/views.wit) ---------------------------------------
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)]
+7
Переглянути файл
@@ -968,3 +968,10 @@ pub(crate) mod hover_bindings {
with: { "openidea:plugin/types": super::bindings::openidea::plugin::types },
});
}
pub(crate) mod inlay_bindings {
wasmtime::component::bindgen!({
world: "inlay-hints", path: "../../wit",
with: { "openidea:plugin/types": super::bindings::openidea::plugin::types },
});
}
+1
Переглянути файл
@@ -30,6 +30,7 @@ fn components() -> Vec<PathBuf> {
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"),
repo().join("crates/ide-plugin/tests/fixtures/inlay-hints/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();
Бінарний файл не відображається.
+1 -1
Переглянути файл
@@ -1 +1 @@
09a64c18ef137348f70121c088419040ee606089d9f27b0dbe63256d5de49cd6
9cdb9b328e2b72c2f8849548b1d5bc119044acb4136dac614319138db2e4890d
Бінарний файл не відображається.
+1 -1
Переглянути файл
@@ -1 +1 @@
8eba458c25d7722ffc1d396812bd83114a67c1176b9dd6fa3bfbbf02c0ca0976
32634dd8c1172c6980cd1c4f9345fb2ed7b2e5cbacfd061bcefaa7fa9aad162e
Бінарний файл не відображається.
+1 -1
Переглянути файл
@@ -1 +1 @@
5f7112055543becca6fc9ad9a33850c355efe38b3ab019ecb4fb26346f3ac9b5
b7b63faf536d558cafc375a17765823015863e47d897e2128d453fbc8975e19c
Бінарний файл не відображається.
+1 -1
Переглянути файл
@@ -1 +1 @@
1f571177a22c2f07055b38385c0e5f44c063c520b9e1d7be3d528e4cfb3b8b43
809180a1c4ba29988af7b1ead1c31c1a3018fe3a1c266930ba588f34de1d3319
+333
Переглянути файл
@@ -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-inlay-hints"
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"
+20
Переглянути файл
@@ -0,0 +1,20 @@
[package]
name = "openidea-test-inlay-hints"
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"
+7
Переглянути файл
@@ -0,0 +1,7 @@
# Inlay hints test fixture
After every `->name()` call in a PHP document that declares
`function name(): Type`, shows `: Type` as a type hint.
Used by `cargo test -p ide-host --test plugin_inlay_hints` to exercise the
optional `inlay-hints-provider` export. It is not a marketplace plugin.
Бінарний файл не відображається.
+1
Переглянути файл
@@ -0,0 +1 @@
6fd9e638afa22170ef2e7a6bd7351bd5e0200d6726cf5d5835be7c5c7cfa19dc
+9
Переглянути файл
@@ -0,0 +1,9 @@
id = "test.inlay-hints"
name = "Inlay hints fixture"
version = "0.1.0"
engines.openidea = "^1.3"
wasm = "dist/plugin.wasm"
[[contributes.providers]]
kinds = ["inlay-hints"]
languages = ["php"]
+47
Переглянути файл
@@ -0,0 +1,47 @@
use openidea_plugin::inlay_hints::{Document, InlayHint, InlayHints, InlayKind};
use openidea_plugin::{Error, Invocation, Plugin, Range, register_plugin, text};
#[derive(Default)]
struct Fixture;
impl Plugin for Fixture {
fn run_command(&mut self, id: &str, _: &Invocation, _: Option<&str>) -> Result<Option<String>, Error> {
Err(Error::NotFound(id.into()))
}
}
impl InlayHints for Fixture {
fn inlay_hints(&mut self, doc: &Document, _range: Range) -> Vec<InlayHint> {
let text = doc.text.as_str();
let mut out = Vec::new();
let mut rest = 0;
while let Some(i) = text[rest..].find("->") {
let start = rest + i + 2;
let end = text[start..]
.find(|c: char| !(c.is_ascii_alphanumeric() || c == '_'))
.map_or(text.len(), |n| start + n);
rest = end;
if end == start || !text[end..].starts_with("()") {
continue;
}
let name = &text[start..end];
let declared = format!("function {name}(): ");
let Some(at) = text.find(&declared) else { continue };
let ty: String = text[at + declared.len()..]
.chars()
.take_while(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '\\' | '?'))
.collect();
if ty.is_empty() {
continue;
}
out.push(
InlayHint::new(text::position(text, end + 2), format!(": {ty}"))
.kind(InlayKind::Type)
.tooltip(format!("{name}() returns {ty}")),
);
}
out
}
}
register_plugin!(Fixture, inlay_hints);
Бінарний файл не відображається.
+1 -1
Переглянути файл
@@ -1 +1 @@
165f1dc7ebf47b3910fd8e23eb72cecfe47156c3498d2b06f5d6c39ecd299e2a
81a1c6a7d78cf6ca6e463ffff6a4e243d668230a8d802028262ed5fc752f2f27
+1 -1
Переглянути файл
@@ -1 +1 @@
5be61cbf46d28a7d06c73fb2562e81f3d1a86e16c5c6389d402cbeaf703e331a
c3b3e3c96d62106398299a6704e6a39fb18cfa734be877d1137f005d16553b4b
Бінарний файл не відображається.
+1 -1
Переглянути файл
@@ -1 +1 @@
81db471a9be4aefb97d21c3937bc0afd637114cc2703fa2cc815b0ae61608467
fac2994442eb96b09dea926546d2bacde333615392bc59fd70df73999ae84f62
Бінарний файл не відображається.
+1 -1
Переглянути файл
@@ -1 +1 @@
cbb5a54e287ef3ed11391985c286e5d4e24646f2fd84b8b969fe825bbd44c350
2a97235992302d0dfda55a1774883c046fe8aed6353b0f047aafa8bfe0155fa3
+23 -2
Переглянути файл
@@ -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, hover, commit
crates/ide-plugin/tests/fixtures/ # test plugins: hello, jobs, language-toy, misbehave, semantic-tokens, hover, commit, inlay-hints
```
The plugin API is defined in [`wit/`](../wit/) (`openidea:[email protected]`, world
@@ -448,7 +448,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", "hover"
kinds = ["definition", "line-markers", "completion"] # also "inline-completion", "code-actions", "semantic-tokens", "hover", "inlay-hints"
languages = ["java", "xml"] # editor language ids; "*" for any
```
@@ -523,6 +523,27 @@ 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.
#### Inlay hints
The `inlay-hints` kind (API 1.3) shows short labels inside the code of
documents of the declared languages, such as a type after a call. Register
with `register_plugin!(MyPlugin, inlay_hints)` and implement
`openidea_plugin::inlay_hints::InlayHints`:
```rust
impl InlayHints for MyPlugin {
fn inlay_hints(&mut self, doc: &Document, range: Range) -> Vec<InlayHint> {
vec![InlayHint::new(range.start, ": string").kind(InlayKind::Type).tooltip("returns string")]
}
}
```
`range` is the visible part of the document; hints outside it are dropped. A
hint's kind (`type`, `parameter` or `other`) decides which Inlay Hints setting
shows it. The host asks the language server and the plugins together, under
the edit-path budget, and shows both; the editor asks again after edits and
scrolling. Labels are cut at 256 bytes and tooltips at 1 KiB.
### Network and secrets
`host::fetch` makes an HTTPS request (plain HTTP only to `localhost`) to a host
Бінарний файл не відображається.
+1 -1
Переглянути файл
@@ -1 +1 @@
2e34ee9cb774344d4dedf1ec077d776a051abd08e435f6df3dca6c7d12fa94fb
5de6d0c1cb0ee1a92d36b57ec18590f03829b2c29cc1c306be453931c528c692
Бінарний файл не відображається.
+1 -1
Переглянути файл
@@ -1 +1 @@
8ee84d6a4a835ec7104877fa105e593b47e8091339b3419690b018f3e8877b94
9a89ec93d158e460998cf472837d7e0e8aa76d5441cb1dd24c3184bd814a29c1
Бінарний файл не відображається.
+1 -1
Переглянути файл
@@ -1 +1 @@
2af4ae2589bebf9703afa6b7b24bc073334344e6836b0ea29aab259338b38762
652557045106ab5bd1126cb8e05965d030c4aa28c01ede31be3fe285aab98a67
Бінарний файл не відображається.
+1 -1
Переглянути файл
@@ -1 +1 @@
a7d656941097113fdde3e64a07446d1780f045702a935a5e5153ac1f540d6b06
7c8c97e02f754676ff512d10f454a7451468e2a98c2e992e790096215dd28658
Бінарний файл не відображається.
+1 -1
Переглянути файл
@@ -1 +1 @@
3f6549848f81cc999a42ba0fa04e0efb43640e09a67ab28d799d4f79832f6178
1b6e3f30cc49d9d67d95f7655cbe337c5f8465c6ef7b8709cfab4734c3c40fdf
Бінарний файл не відображається.
+1 -1
Переглянути файл
@@ -1 +1 @@
31ff37106dc0829e226de01b5148f6ee67b1d776acd6def4729edf4c63946219
3225880631e679780c3c7eb09683b598bb3803fcf7be7b6940369eada633c48f
Бінарний файл не відображається.
+1 -1
Переглянути файл
@@ -1 +1 @@
eb4d09a79172cb46857c7bb3e50e7fbd6f4b6be379bb1c79a41ea23a5643f875
a59623542b4df024f5a85bc2cd0e046189044a0c292c793384fdb5465e8eec48
Бінарний файл не відображається.
+1 -1
Переглянути файл
@@ -1 +1 @@
4f2e5d5c877554b111bb454603d002e50bbafb3dca3cca5768c052fbc48f73ab
f230ae32c2d5ab469f23c6851fc7830a6ecce7538c7d435be39b5bbebe38a64f
Бінарний файл не відображається.
+1 -1
Переглянути файл
@@ -1 +1 @@
0cae1d9dd2075b1a107d894854f0d01ab81331b67793f2ad972835a8ab34d3a0
63f52f8cf8a90affc751ac7e19352a89f0726fc7c8dbbb303172ebd42e23af8c
Бінарний файл не відображається.
+1 -1
Переглянути файл
@@ -1 +1 @@
13e34d174abf5187bf7c191736974305afb0af257fc103eec96fe8fa8edb8c88
178862630cf39bca56f5d54cd135dcac4a1ab74a3e10356163d00e5a718ae728
Бінарний файл не відображається.
+1 -1
Переглянути файл
@@ -1 +1 @@
7e010175c19a159a22c42a76ca07ebc663a27ce9143c21e742ca99435a538f83
a6c188aed4fc57cd9955d6b47ef715c52dc3f8c7b69f7d18ea71b3932888710f
+84
Переглянути файл
@@ -0,0 +1,84 @@
use crate::types::{Position, Range};
pub use crate::providers::Document;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum InlayKind {
Type,
Parameter,
Other,
}
impl InlayKind {
pub fn as_str(self) -> &'static str {
match self {
Self::Type => "type",
Self::Parameter => "parameter",
Self::Other => "other",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct InlayHint {
pub position: Position,
pub label: String,
pub kind: InlayKind,
pub tooltip: Option<String>,
pub padding_left: bool,
pub padding_right: bool,
}
impl InlayHint {
pub fn new(position: Position, label: impl Into<String>) -> Self {
Self {
position,
label: label.into(),
kind: InlayKind::Other,
tooltip: None,
padding_left: false,
padding_right: false,
}
}
pub fn kind(mut self, kind: InlayKind) -> Self {
self.kind = kind;
self
}
pub fn tooltip(mut self, tooltip: impl Into<String>) -> Self {
self.tooltip = Some(tooltip.into());
self
}
pub fn padding_left(mut self) -> Self {
self.padding_left = true;
self
}
pub fn padding_right(mut self) -> Self {
self.padding_right = true;
self
}
}
pub trait InlayHints {
fn inlay_hints(&mut self, doc: &Document, range: Range) -> Vec<InlayHint>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_hint_is_built_step_by_step() {
let at = Position { line: 3, character: 14 };
let hint = InlayHint::new(at, "GET /orders").kind(InlayKind::Type).tooltip("orders.index").padding_left();
assert_eq!(hint.position, at);
assert_eq!(hint.kind.as_str(), "type");
assert_eq!(hint.tooltip.as_deref(), Some("orders.index"));
assert!(hint.padding_left && !hint.padding_right);
assert_eq!(InlayHint::new(at, "x").kind, InlayKind::Other);
}
}
+50
Переглянути файл
@@ -44,6 +44,7 @@ pub mod details;
pub mod host;
pub mod hover;
pub mod indexing;
pub mod inlay_hints;
#[cfg(not(target_arch = "wasm32"))]
pub mod mock;
pub mod providers;
@@ -185,6 +186,17 @@ pub mod commit_checks_bindings {
});
}
#[cfg(target_arch = "wasm32")]
#[doc(hidden)]
pub mod inlay_bindings {
wit_bindgen::generate!({
world: "inlay-hints", path: "wit", pub_export_macro: true,
export_macro_name: "__export_inlay_hints_world",
default_bindings_module: "openidea_plugin::inlay_bindings",
with: { "openidea:plugin/[email protected]": crate::bindings::openidea::plugin::types },
});
}
/// The plugin API version this SDK targets.
pub const API_VERSION: &str = "1.3";
@@ -251,6 +263,9 @@ macro_rules! __optional_exports {
(commit_checks, $plugin:ty, $component:ident) => {
$crate::__commit_checks_exports!($plugin, $component);
};
(inlay_hints, $plugin:ty, $component:ident) => {
$crate::__inlay_hints_exports!($plugin, $component);
};
(syntax, $plugin:ty, $component:ident) => {
impl $crate::syntax_bindings::exports::openidea::plugin::syntax_provider::Guest for $component {
fn highlights(
@@ -457,6 +472,22 @@ macro_rules! __hover_exports {
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! __inlay_hints_exports {
($plugin:ty, $component:ident) => {
impl $crate::inlay_bindings::exports::openidea::plugin::inlay_hints_provider::Guest for $component {
fn inlay_hints(
doc: $crate::inlay_bindings::exports::openidea::plugin::inlay_hints_provider::Document,
range: $crate::bindings::openidea::plugin::types::Range,
) -> ::std::vec::Vec<$crate::inlay_bindings::exports::openidea::plugin::inlay_hints_provider::InlayHint> {
$crate::__rt::inlay_hints::<$plugin>(doc, range)
}
}
$crate::inlay_bindings::__export_inlay_hints_world!($component with_types_in $crate::inlay_bindings);
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! __plugin_exports {
@@ -722,6 +753,25 @@ pub mod __rt {
.map(|h| w::HoverContent { markdown: h.markdown, range: h.range.map(crate::host::__range_to) })
}
pub fn inlay_hints<T: Plugin + crate::inlay_hints::InlayHints + Default>(
d: crate::inlay_bindings::exports::openidea::plugin::inlay_hints_provider::Document,
range: b::types::Range,
) -> Vec<crate::inlay_bindings::exports::openidea::plugin::inlay_hints_provider::InlayHint> {
use crate::inlay_bindings::exports::openidea::plugin::inlay_hints_provider as w;
let doc = crate::providers::Document { path: d.path, language: d.language, version: d.version, text: d.text };
with::<T, _>(|p| p.inlay_hints(&doc, crate::host::__range_from(range)))
.into_iter()
.map(|h| w::InlayHint {
position: b::types::Position { line: h.position.line, character: h.position.character },
label: h.label,
kind: h.kind.as_str().into(),
tooltip: h.tooltip,
padding_left: h.padding_left,
padding_right: h.padding_right,
})
.collect()
}
// ---- views ----
use crate::views as v;
+39
Переглянути файл
@@ -0,0 +1,39 @@
package openidea:[email protected];
/// Optional export (world `plugin-inlay-hints`): inline hints in documents of
/// the languages declared with the `inlay-hints` provider kind, including
/// languages the plugin doesn't contribute. They are shown next to the
/// language server's hints.
interface inlay-hints-provider {
use types.{position, range};
/// The document the hints are for (the editor's text while it is open).
record document {
path: string,
language: string,
version: u64,
text: string,
}
/// A hint shown at `position`, before the character there.
record inlay-hint {
/// UTF-16 position (0-based line and column).
position: position,
/// The text shown, e.g. `GET /orders` or `: string`.
label: string,
/// `type` or `parameter` follow the Inlay Hints settings of that kind;
/// anything else counts as `other`.
kind: string,
/// Shown when the pointer rests on the hint.
tooltip: option<string>,
/// A space before the label.
padding-left: bool,
/// A space after the label.
padding-right: bool,
}
/// The hints within `range` (the visible part of the document), under the
/// edit-path budget. Hints outside `range` are dropped; at most 10,000 per
/// call.
inlay-hints: func(doc: document, range: range) -> list<inlay-hint>;
}
+11
Переглянути файл
@@ -67,6 +67,12 @@ world plugin-hover {
export hover-provider;
}
/// A plugin with inlay hints (`inlay-hints` under `[[contributes.providers]]`).
world plugin-inlay-hints {
include plugin;
export inlay-hints-provider;
}
/// A plugin whose views show in the Services tool window
/// (`location = "services"`): their nodes' detail pane.
world plugin-details {
@@ -109,6 +115,11 @@ world hover {
export hover-provider;
}
/// Only the `inlay-hints-provider` export, generated next to `plugin` like `syntax`.
world inlay-hints {
export inlay-hints-provider;
}
/// Only the `detail-provider` export, generated next to `plugin` like `syntax`.
world details {
export detail-provider;
+39
Переглянути файл
@@ -0,0 +1,39 @@
package openidea:[email protected];
/// Optional export (world `plugin-inlay-hints`): inline hints in documents of
/// the languages declared with the `inlay-hints` provider kind, including
/// languages the plugin doesn't contribute. They are shown next to the
/// language server's hints.
interface inlay-hints-provider {
use types.{position, range};
/// The document the hints are for (the editor's text while it is open).
record document {
path: string,
language: string,
version: u64,
text: string,
}
/// A hint shown at `position`, before the character there.
record inlay-hint {
/// UTF-16 position (0-based line and column).
position: position,
/// The text shown, e.g. `GET /orders` or `: string`.
label: string,
/// `type` or `parameter` follow the Inlay Hints settings of that kind;
/// anything else counts as `other`.
kind: string,
/// Shown when the pointer rests on the hint.
tooltip: option<string>,
/// A space before the label.
padding-left: bool,
/// A space after the label.
padding-right: bool,
}
/// The hints within `range` (the visible part of the document), under the
/// edit-path budget. Hints outside `range` are dropped; at most 10,000 per
/// call.
inlay-hints: func(doc: document, range: range) -> list<inlay-hint>;
}
+11
Переглянути файл
@@ -67,6 +67,12 @@ world plugin-hover {
export hover-provider;
}
/// A plugin with inlay hints (`inlay-hints` under `[[contributes.providers]]`).
world plugin-inlay-hints {
include plugin;
export inlay-hints-provider;
}
/// A plugin whose views show in the Services tool window
/// (`location = "services"`): their nodes' detail pane.
world plugin-details {
@@ -109,6 +115,11 @@ world hover {
export hover-provider;
}
/// Only the `inlay-hints-provider` export, generated next to `plugin` like `syntax`.
world inlay-hints {
export inlay-hints-provider;
}
/// Only the `detail-provider` export, generated next to `plugin` like `syntax`.
world details {
export detail-provider;