pmacs/src/inlay_hint.rs

295 lines
8.3 KiB
Rust

// inlay_hint.rs --- T M4.5 LSP inlay hints.
//! `textDocument/inlayHint` response state.
//!
//! An inlay hint is a small annotation the server wants drawn *inline*
//! at a position (a parameter name before an argument, an inferred
//! type after a `let`). The label is either a plain string or an
//! array of `InlayHintLabelPart`s (each carrying its own `value`,
//! tooltip, location, command); this module flattens the parts'
//! `value`s into one display string — the part-level interactivity
//! (go-to-def on a type in a hint) is later UX, like the hover panel.
//!
//! Mirrors [`crate::formatting`] / [`crate::code_action`]: a parsed,
//! per-`(server, uri)` store. Nothing here renders: the inline
//! virtual-text renderer is a separate milestone (the cell-overlay
//! model does not yet reflow real glyphs around inserted columns).
//! Lua reads [`InlayHintStore`] and surfaces hints; a render layer can
//! subscribe to the same store when it lands.
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use serde_json::Value;
/// LSP `InlayHintKind`.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum InlayHintKind {
/// `1` — an inferred type.
Type,
/// `2` — a parameter name.
Parameter,
}
impl InlayHintKind {
fn from_lsp(n: u64) -> Option<Self> {
match n {
1 => Some(Self::Type),
2 => Some(Self::Parameter),
_ => None,
}
}
/// Lowercase wire-ish label for the Lua surface.
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Type => "type",
Self::Parameter => "parameter",
}
}
}
/// One parsed inlay hint.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct InlayHint {
/// Zero-based line the hint sits on.
pub line: u32,
/// Zero-based column (UTF-16 code units, per LSP).
pub col: u32,
/// Display text (label string, or all label parts concatenated).
pub label: String,
/// Hint kind, if the server classified it.
pub kind: Option<InlayHintKind>,
/// Render a space before the label.
pub padding_left: bool,
/// Render a space after the label.
pub padding_right: bool,
/// Plain-text tooltip, if any (`MarkupContent` is flattened to
/// its `value`).
pub tooltip: Option<String>,
}
/// Parsed `textDocument/inlayHint` response.
#[derive(Clone, Debug, Default)]
pub struct InlayHintResponse {
/// Hints in server order.
pub hints: Vec<InlayHint>,
}
impl InlayHintResponse {
/// Parse `InlayHint[] | null`. A `null` / non-array result yields
/// an empty list.
#[must_use]
pub fn from_lsp_value(v: &Value) -> Self {
let Some(arr) = v.as_array() else {
return Self::default();
};
let mut hints = Vec::with_capacity(arr.len());
for item in arr {
if let Some(h) = parse_hint(item) {
hints.push(h);
}
}
Self { hints }
}
/// True iff the server returned no hints.
#[must_use]
pub fn is_empty(&self) -> bool {
self.hints.is_empty()
}
}
/// `label: string | InlayHintLabelPart[]` → one string.
fn parse_label(v: &Value) -> Option<String> {
if let Some(s) = v.as_str() {
return Some(s.to_owned());
}
let parts = v.as_array()?;
let mut out = String::new();
for p in parts {
if let Some(s) = p.get("value").and_then(Value::as_str) {
out.push_str(s);
}
}
Some(out)
}
/// `tooltip: string | MarkupContent` → plain text.
fn parse_tooltip(v: &Value) -> Option<String> {
if let Some(s) = v.as_str() {
return Some(s.to_owned());
}
v.get("value").and_then(Value::as_str).map(str::to_owned)
}
fn parse_hint(v: &Value) -> Option<InlayHint> {
let pos = v.get("position")?;
let line = pos.get("line")?.as_u64()? as u32;
let col = pos.get("character")?.as_u64()? as u32;
let label = parse_label(v.get("label")?)?;
Some(InlayHint {
line,
col,
label,
kind: v
.get("kind")
.and_then(Value::as_u64)
.and_then(InlayHintKind::from_lsp),
padding_left: v
.get("paddingLeft")
.and_then(Value::as_bool)
.unwrap_or(false),
padding_right: v
.get("paddingRight")
.and_then(Value::as_bool)
.unwrap_or(false),
tooltip: v.get("tooltip").and_then(parse_tooltip),
})
}
/// Per-server, per-uri inlay-hint state.
#[derive(Default)]
pub struct InlayHintStore {
by_key: HashMap<InlayHintKey, InlayHintResponse>,
}
/// Key into [`InlayHintStore`].
#[derive(Clone, Eq, PartialEq, Hash, Debug)]
pub struct InlayHintKey {
/// Decimal LSP server id.
pub server: String,
/// Document URI the request was made on.
pub uri: String,
}
impl InlayHintKey {
/// Construct a key.
#[must_use]
pub fn new(server: impl Into<String>, uri: impl Into<String>) -> Self {
Self {
server: server.into(),
uri: uri.into(),
}
}
}
impl InlayHintStore {
/// Empty store.
#[must_use]
pub fn new() -> Self {
Self::default()
}
/// Replace the response at `key`.
pub fn set(&mut self, key: InlayHintKey, response: InlayHintResponse) {
self.by_key.insert(key, response);
}
/// Drop the entry at `key`.
pub fn clear(&mut self, key: &InlayHintKey) {
self.by_key.remove(key);
}
/// Look up the entry at `key`.
#[must_use]
pub fn get(&self, key: &InlayHintKey) -> Option<&InlayHintResponse> {
self.by_key.get(key)
}
}
/// Cheaply-cloneable shared handle.
pub type SharedInlayHintStore = Arc<Mutex<InlayHintStore>>;
/// Build a fresh shared store.
#[must_use]
pub fn make_shared_store() -> SharedInlayHintStore {
Arc::new(Mutex::new(InlayHintStore::new()))
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn parses_string_label_type_hint() {
let v = json!([{
"position": { "line": 3, "character": 12 },
"label": ": String",
"kind": 1,
"paddingLeft": false,
"paddingRight": true,
"tooltip": "inferred type"
}]);
let r = InlayHintResponse::from_lsp_value(&v);
assert_eq!(r.hints.len(), 1);
let h = &r.hints[0];
assert_eq!((h.line, h.col), (3, 12));
assert_eq!(h.label, ": String");
assert_eq!(h.kind, Some(InlayHintKind::Type));
assert!(!h.padding_left);
assert!(h.padding_right);
assert_eq!(h.tooltip.as_deref(), Some("inferred type"));
}
#[test]
fn concatenates_label_parts_for_parameter_hint() {
let v = json!([{
"position": { "line": 0, "character": 7 },
"label": [ { "value": "name" }, { "value": ":" } ],
"kind": 2,
"tooltip": { "kind": "markdown", "value": "the param" }
}]);
let r = InlayHintResponse::from_lsp_value(&v);
let h = &r.hints[0];
assert_eq!(h.label, "name:");
assert_eq!(h.kind, Some(InlayHintKind::Parameter));
assert_eq!(h.tooltip.as_deref(), Some("the param"));
}
#[test]
fn unknown_kind_and_missing_optionals_default() {
let v = json!([{
"position": { "line": 1, "character": 1 },
"label": "x",
"kind": 99
}]);
let r = InlayHintResponse::from_lsp_value(&v);
let h = &r.hints[0];
assert_eq!(h.kind, None);
assert!(!h.padding_left);
assert!(!h.padding_right);
assert!(h.tooltip.is_none());
}
#[test]
fn null_response_is_empty() {
assert!(InlayHintResponse::from_lsp_value(&Value::Null).is_empty());
}
#[test]
fn store_set_get_clear() {
let mut s = InlayHintStore::new();
let key = InlayHintKey::new("1", "file:///a");
s.set(
key.clone(),
InlayHintResponse {
hints: vec![InlayHint {
line: 0,
col: 0,
label: "h".into(),
kind: None,
padding_left: false,
padding_right: false,
tooltip: None,
}],
},
);
assert_eq!(s.get(&key).unwrap().hints.len(), 1);
s.clear(&key);
assert!(s.get(&key).is_none());
}
}