T M4.5: inlay hints (textDocument/inlayHint)

Independent LSP feature (not part of the L1-L4 cross-file arc),
shipped in the same shape as every sibling: typed store + async
request + Lua surface + command + modeline summary, with the
inline renderer deferred as its own milestone.

- src/inlay_hint.rs: parse InlayHint[]|null — position, label
  (string OR InlayHintLabelPart[] flattened), kind (type/
  parameter), paddingLeft/Right, tooltip (string|MarkupContent).
  Store keyed (server, uri). 5 unit tests.
- src/lsp.rs: inlay_hint_store + accessor, ResponseRoute::InlayHint
  + absorb, request_inlay_hint (range params), textDocument.
  inlayHint client capability (no resolveSupport/refreshSupport —
  full hints, on-demand re-query is the v1 model).
- src/lua_bindings.rs: _request_inlay_hint_raw,
  pmacs.inlay_hint.{hints,clear}.
- pmacs_fake_lsp.rs: textDocument/inlayHint arm returning a
  string-label type hint and a label-parts parameter hint.
- builtin/runtime/lsp.lua: pmacs.lsp.inlay_hints() requests over
  the whole-buffer range, stores, modeline summary;
  lsp.inlay-hints command + C-c i; scope header notes the inline
  renderer is a later milestone.
- tests/m4_acceptance.rs: m4_16 drives the request via the Lua
  surface, asserts both label shapes / kinds / padding parsed.

Deferred (scoping, not a regression): inline virtual-text
rendering. The VirtualCellOverlay model only overwrites existing
cells; rendering hints inline needs a column-inserting/reflowing
renderer — a rendering milestone, not an LSP task — staged like
the hover panel / references list. pmacs.inlay_hint is the data
surface a future render layer subscribes to.

Gates: lib 1279/0, m4 71/0, m8_1 10/0, m8_9 26/0, m8_10 19/0,
m9_1 18/0, m5_8 5/0, m11_5 (--features crdt) 2/0; fmt + clippy
clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Levi Neuwirth 2026-05-19 12:58:36 -04:00
parent 06580967b5
commit 1a0ccd0247
7 changed files with 586 additions and 3 deletions

View File

@ -5,7 +5,8 @@
-- * Auto-attach + did_open / did_change / did_close on buffer events.
-- * `pmacs.lsp.go_to_definition` / `pmacs.lsp.format_buffer` /
-- `pmacs.lsp.hover_at_cursor` / `pmacs.lsp.signature_help_at_cursor`
-- / `pmacs.lsp.rename` / `pmacs.lsp.code_actions`, bound to
-- / `pmacs.lsp.rename` / `pmacs.lsp.code_actions` /
-- `pmacs.lsp.inlay_hints`, bound to
-- default chords below.
--
-- Scope: one server per language across all buffers; async-await
@ -14,8 +15,9 @@
-- (L2), code actions + `workspace/executeCommand` + server→client
-- `workspace/applyEdit` (L3), ordered resource-op edits
-- (create/rename/delete file) with buffer-registry reconciliation
-- (L4). Inlay hints, semantic tokens, and file-watch capability
-- registration are later layers.
-- (L4), and inlay hints (data + modeline; inline virtual-text
-- rendering is a later milestone). Semantic tokens and file-watch
-- capability registration are later layers.
pmacs.lsp = pmacs.lsp or {}
pmacs.lsp.config = pmacs.lsp.config or {}
@ -270,6 +272,7 @@ pmacs.lsp.request_document_highlight = wrap_request(pmacs.lsp._request_document_
pmacs.lsp.request_rename = wrap_request(pmacs.lsp._request_rename_raw)
pmacs.lsp.request_code_action = wrap_request(pmacs.lsp._request_code_action_raw)
pmacs.lsp.request_execute_command = wrap_request(pmacs.lsp._request_execute_command_raw)
pmacs.lsp.request_inlay_hint = wrap_request(pmacs.lsp._request_inlay_hint_raw)
-- Render an `:await()` failure into a modeline-friendly reason.
-- `Handle:await()` raises `{ tag = "cancelled", ... }` when the
@ -620,6 +623,47 @@ function pmacs.lsp.document_symbols()
end)
end
-- T M4.5 — inlay hints for the whole buffer. Requests over a range
-- spanning the document, stores the parsed hints, and surfaces a
-- modeline summary (count + first). Inline virtual-text rendering is
-- a separate milestone (the cell-overlay model does not yet reflow
-- real glyphs around inserted columns); a render layer subscribes to
-- the same `pmacs.inlay_hint` store when it lands — same staged
-- approach as the references list / hover panel.
function pmacs.lsp.inlay_hints()
local rec = attached_for_active()
if not rec then
pmacs.editor.set_status("LSP: no server for active buffer")
return
end
-- Whole-document range: (0,0) .. (one past the last line, 0). An
-- over-wide end line is fine — servers clamp to the document.
local text = active_buffer_text()
local nl = 0
for _ in text:gmatch("\n") do nl = nl + 1 end
pmacs.inlay_hint.clear(rec.server, rec.uri)
pmacs.async(function()
local ok, err = pcall(function()
pmacs.lsp.request_inlay_hint(
rec.server, rec.uri, 0, 0, nl + 1, 0):await()
end)
if not ok then
pmacs.editor.set_status("LSP: " .. lsp_await_error(err))
return
end
local hints = pmacs.inlay_hint.hints(rec.server, rec.uri)
if not hints or #hints == 0 then
pmacs.editor.set_status("LSP: no inlay hints")
return
end
local first = hints[1]
pmacs.editor.set_status(string.format(
"LSP: %d inlay hint%s; first '%s' at %d:%d",
#hints, (#hints == 1 and "" or "s"),
first.label, first.line + 1, first.col + 1))
end)
end
function pmacs.lsp.format_buffer()
local rec = attached_for_active()
if not rec then
@ -871,6 +915,12 @@ pmacs.command.define {
fn = pmacs.lsp.code_actions,
}
pmacs.command.define {
name = "lsp.inlay-hints",
description = "Fetch inlay hints (inferred types / parameter names) for the buffer (LSP).",
fn = pmacs.lsp.inlay_hints,
}
-- T M4.5 L1 — unwind the cross-file jump ring. Pairs with the
-- `pmacs.editor.push_jump()` every navigation action records before
-- it moves the cursor.
@ -894,6 +944,7 @@ pmacs.keymap.bind { scope = "global", sequence = "M-,", command = "lsp.jump-ba
pmacs.keymap.bind { scope = "global", sequence = "C-c o", command = "lsp.document-symbols" }
pmacs.keymap.bind { scope = "global", sequence = "C-c r", command = "lsp.rename" }
pmacs.keymap.bind { scope = "global", sequence = "C-c a", command = "lsp.code-actions" }
pmacs.keymap.bind { scope = "global", sequence = "C-c i", command = "lsp.inlay-hints" }
pmacs.keymap.bind { scope = "global", sequence = "C-c h", command = "lsp.hover" }
pmacs.keymap.bind { scope = "global", sequence = "C-c s", command = "lsp.signature-help" }
pmacs.keymap.bind { scope = "global", sequence = "C-c f", command = "lsp.format-buffer" }

View File

@ -584,6 +584,33 @@ fn main() {
});
write_frame(&mut stdout, &resp);
}
("textDocument/inlayHint", Some(idv)) => {
// T M4.5: a type hint (string label, kind 1) and a
// parameter hint (label *parts*, kind 2) so both
// label shapes are exercised.
let resp = serde_json::json!({
"jsonrpc": "2.0",
"id": idv,
"result": [
{
"position": { "line": 0, "character": 9 },
"label": ": i32",
"kind": 1,
"paddingLeft": false,
"paddingRight": false,
"tooltip": "inferred type"
},
{
"position": { "line": 1, "character": 4 },
"label": [ { "value": "count" }, { "value": ":" } ],
"kind": 2,
"paddingLeft": false,
"paddingRight": true
}
]
});
write_frame(&mut stdout, &resp);
}
// T M4.5 symbols/highlight. documentSymbol returns the
// *hierarchical* DocumentSymbol shape (exercises tree
// flatten + depth + parent); workspace/symbol the flat

294
src/inlay_hint.rs Normal file
View File

@ -0,0 +1,294 @@
// 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());
}
}

View File

@ -63,6 +63,7 @@ pub mod help;
pub mod highlight;
pub mod hook;
pub mod hover;
pub mod inlay_hint;
pub mod instance_buffer;
pub mod instance_render;
pub mod key;

View File

@ -763,6 +763,10 @@ pub struct LspManager {
/// `textDocument/codeAction` response lands, keyed by `(server,
/// uri)`.
code_action_store: crate::code_action::SharedCodeActionStore,
/// T M4.5 inlay-hint store. Populated when a
/// `textDocument/inlayHint` response lands, keyed by `(server,
/// uri)`.
inlay_hint_store: crate::inlay_hint::SharedInlayHintStore,
/// Per-server request id → response routing target.
/// `request_completion` etc. record an entry here; `handle_response`
/// consumes it to absorb the response into the correct store.
@ -826,6 +830,9 @@ enum ResponseRoute {
/// Absorb a `textDocument/codeAction` response into
/// [`crate::code_action::CodeActionStore`] at `(server, uri)`.
CodeAction { uri: String },
/// Absorb a `textDocument/inlayHint` response into
/// [`crate::inlay_hint::InlayHintStore`] at `(server, uri)`.
InlayHint { uri: String },
/// Absorb a Location-shaped nav response (references / declaration
/// / typeDefinition / implementation) into
/// [`crate::locations::LocationsStore`] at `(server, uri, kind)`.
@ -869,6 +876,7 @@ impl ResponseRoute {
| ResponseRoute::Formatting { uri }
| ResponseRoute::Rename { uri }
| ResponseRoute::CodeAction { uri }
| ResponseRoute::InlayHint { uri }
| ResponseRoute::Locations { uri, .. }
| ResponseRoute::DocumentSymbol { uri }
| ResponseRoute::DocumentHighlight { uri } => uri,
@ -952,6 +960,7 @@ impl LspManager {
formatting_store: crate::formatting::make_shared_store(),
rename_store: crate::rename::make_shared_store(),
code_action_store: crate::code_action::make_shared_store(),
inlay_hint_store: crate::inlay_hint::make_shared_store(),
pending_routes: HashMap::new(),
status_tracker: crate::lsp_status::LspStatusTracker::new(),
project_servers: HashMap::new(),
@ -1030,6 +1039,12 @@ impl LspManager {
self.code_action_store.clone()
}
/// Shared inlay-hint store (T M4.5).
#[must_use]
pub fn inlay_hint_store(&self) -> crate::inlay_hint::SharedInlayHintStore {
self.inlay_hint_store.clone()
}
/// T M4.8: per-server status snapshot, derived from the LSP event
/// stream. The modeline reads its label from this.
#[must_use]
@ -1808,6 +1823,36 @@ impl LspManager {
Ok(job_id)
}
/// Send `textDocument/inlayHint` over `[start, end]` in `uri`
/// (the visible/whole-buffer range the caller wants annotated).
/// The response is absorbed into the inlay-hint store at `(sid,
/// uri)`. Returns the async-runtime [`JobId`] the response will
/// settle.
#[allow(clippy::too_many_arguments)]
pub fn request_inlay_hint(
&mut self,
sid: LspServerId,
uri: impl Into<String>,
start_line: u32,
start_col: u32,
end_line: u32,
end_col: u32,
) -> Result<JobId, String> {
let uri = uri.into();
let params = json!({
"textDocument": { "uri": uri.clone() },
"range": {
"start": { "line": start_line, "character": start_col },
"end": { "line": end_line, "character": end_col },
},
});
let req_id = self.send_request(sid, "textDocument/inlayHint", params)?;
let job_id = self.register_awaiter(sid, req_id, "textDocument/inlayHint", &uri);
self.pending_routes
.insert((sid, req_id), ResponseRoute::InlayHint { uri });
Ok(job_id)
}
/// Send `workspace/executeCommand`. No response route is
/// registered: the command result is usually `null` and the real
/// effect arrives as a server→client `workspace/applyEdit` (the
@ -2426,6 +2471,15 @@ impl LspManager {
.expect("code action store mutex poisoned");
guard.set(key, resp);
}
ResponseRoute::InlayHint { uri } => {
let resp = crate::inlay_hint::InlayHintResponse::from_lsp_value(result);
let key = crate::inlay_hint::InlayHintKey::new(server_key, uri.clone());
let mut guard = self
.inlay_hint_store
.lock()
.expect("inlay hint store mutex poisoned");
guard.set(key, resp);
}
}
}
@ -2870,6 +2924,13 @@ fn default_capabilities() -> Value {
},
},
},
// T M4.5 inlay hints. No `resolveSupport` — pmacs
// requests full hints (label/tooltip already populated),
// not lazily-resolved stubs. `refreshSupport` is left
// false (default) so servers don't send
// `workspace/inlayHint/refresh`; on-demand re-query is
// the v1 model.
"inlayHint": { "dynamicRegistration": false },
"publishDiagnostics": { "relatedInformation": true },
},
})

View File

@ -7529,6 +7529,30 @@ pub fn install_lsp(lua: &Lua, manager: &SharedLspManager) -> mlua::Result<()> {
)?;
}
{
let m = manager.clone();
lsp_mod.set(
"_request_inlay_hint_raw",
lua.create_function(
move |_,
(id, uri, sl, sc, el, ec): (
LspServerIdLua,
String,
u32,
u32,
u32,
u32,
)| {
let job_id = m
.borrow_mut()
.request_inlay_hint(id.0, uri, sl, sc, el, ec)
.map_err(mlua::Error::external)?;
Ok(job_id)
},
)?,
)?;
}
{
let m = manager.clone();
lsp_mod.set(
@ -7804,6 +7828,7 @@ pub fn make_lsp_manager(
install_formatting(lua, &manager)?;
install_rename(lua, &manager)?;
install_code_action(lua, &manager)?;
install_inlay_hint(lua, &manager)?;
Ok(manager)
}
@ -8604,6 +8629,7 @@ use crate::definition::{DefinitionKey, DefinitionLocation, DefinitionResponse};
use crate::document_highlight::{DocumentHighlightKey, Highlight};
use crate::formatting::{FormattingKey, FormattingResponse, TextEdit};
use crate::hover::{Hover, HoverKey};
use crate::inlay_hint::{InlayHint as LspInlayHint, InlayHintKey};
use crate::locations::{LocationKind, LocationsKey};
use crate::rename::{RenameKey, WorkspaceEditResponse, WorkspaceOp};
use crate::signature::{Signature, SignatureHelp, SignatureKey, SignatureParameter};
@ -9483,6 +9509,71 @@ pub fn install_code_action(lua: &Lua, manager: &SharedLspManager) -> mlua::Resul
Ok(())
}
fn inlay_hint_to_lua(lua: &Lua, h: &LspInlayHint) -> mlua::Result<Table> {
let t = lua.create_table_with_capacity(0, 7)?;
t.set("line", h.line)?;
t.set("col", h.col)?;
t.set("label", h.label.as_str())?;
if let Some(k) = h.kind {
t.set("kind", k.as_str())?;
}
t.set("padding_left", h.padding_left)?;
t.set("padding_right", h.padding_right)?;
if let Some(tt) = h.tooltip.as_deref() {
t.set("tooltip", tt)?;
}
Ok(t)
}
/// Install `pmacs.inlay_hint.*` (T M4.5). `hints(sid, uri)` returns
/// `{ { line, col, label, kind?, padding_left, padding_right,
/// tooltip? }, … }` in server order; `clear(sid, uri)` drops the
/// entry. No renderer here — that is a separate milestone; this is
/// the data surface a render layer (or a list view) reads.
pub fn install_inlay_hint(lua: &Lua, manager: &SharedLspManager) -> mlua::Result<()> {
let pmacs: Table = lua.globals().get("pmacs")?;
let m = lua.create_table()?;
{
let mgr = manager.clone();
m.set(
"hints",
lua.create_function(move |lua, (id, uri): (LspServerIdLua, String)| {
let store_handle = mgr.borrow().inlay_hint_store();
let guard = store_handle
.lock()
.expect("inlay hint store mutex poisoned");
let key = InlayHintKey::new(id.0.raw().to_string(), uri);
let out = lua.create_table()?;
if let Some(r) = guard.get(&key) {
for (i, h) in r.hints.iter().enumerate() {
out.set(i + 1, inlay_hint_to_lua(lua, h)?)?;
}
}
Ok(Value::Table(out))
})?,
)?;
}
{
let mgr = manager.clone();
m.set(
"clear",
lua.create_function(move |_, (id, uri): (LspServerIdLua, String)| {
let store_handle = mgr.borrow().inlay_hint_store();
let mut guard = store_handle
.lock()
.expect("inlay hint store mutex poisoned");
guard.clear(&InlayHintKey::new(id.0.raw().to_string(), uri));
Ok(())
})?,
)?;
}
pmacs.set("inlay_hint", m)?;
Ok(())
}
// ---------------------------------------------------------------------------
// pmacs.project: project model (T M4.9)
// ---------------------------------------------------------------------------

View File

@ -3746,6 +3746,64 @@ fn m4_15_workspace_edit_resource_ops_apply_in_order() {
);
}
/// T M4.5 — inlay hints through the Lua surface. Drives
/// `pmacs.lsp.request_inlay_hint` against the fake and asserts the
/// typed `pmacs.inlay_hint` store parsed both label shapes (a
/// string-label type hint and a label-parts parameter hint), the
/// kinds, and `paddingRight`.
#[test]
fn m4_16_lua_surface_drives_inlay_hints() {
let mut s = pmacs::editor::EditorState::new();
spawn_lsp_and_init(&mut s, None);
let uri = "file:///tmp/m4_16_inlay.rs";
s.lua_host
.lua()
.load(format!(
"pmacs.lsp.did_open(_G._lsp, '{uri}', 1, 'let x = 1\\nfn f() {{}}\\n')
pmacs.lsp.request_inlay_hint(_G._lsp, '{uri}', 0, 0, 5, 0)"
))
.exec()
.expect("kick off inlay hint request");
assert!(
pump_lua_flag(
&mut s,
&format!("#pmacs.inlay_hint.hints(_G._lsp, '{uri}') > 0"),
5,
),
"inlay hint response did not land in the store"
);
let (count, l0, c0, label0, kind0, label1, kind1, pad1): (
usize,
u32,
u32,
String,
String,
String,
String,
bool,
) = s
.lua_host
.lua()
.load(format!(
"local h = pmacs.inlay_hint.hints(_G._lsp, '{uri}')
return #h, h[1].line, h[1].col, h[1].label, h[1].kind,
h[2].label, h[2].kind, h[2].padding_right"
))
.eval()
.expect("read inlay hints back");
assert_eq!(count, 2);
assert_eq!((l0, c0), (0, 9));
assert_eq!(label0, ": i32");
assert_eq!(kind0, "type");
// Label parts were concatenated.
assert_eq!(label1, "count:");
assert_eq!(kind1, "parameter");
assert!(pad1, "second hint requested paddingRight");
}
/// Default LSP bundle (`builtin/runtime/lsp.lua`) is wired in: the
/// hooks are defined, the namespace tables exist, the user-facing
/// commands are registered with the command registry, and the default