Merge pull request #54 from levineuwirth/session-6-inline-adornments
Render inline adornments in pmacs-gpu
This commit is contained in:
commit
8dfae48d8f
|
|
@ -228,6 +228,24 @@ local function active_buffer_text()
|
||||||
return b:slice(0, b:len())
|
return b:slice(0, b:len())
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function buffer_text(buf)
|
||||||
|
if not buf then return "" end
|
||||||
|
return buf:slice(0, buf:len())
|
||||||
|
end
|
||||||
|
|
||||||
|
local function document_end_position(text)
|
||||||
|
local line, col = 0, 0
|
||||||
|
for i = 1, #text do
|
||||||
|
if text:byte(i) == 10 then
|
||||||
|
line = line + 1
|
||||||
|
col = 0
|
||||||
|
else
|
||||||
|
col = col + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return line, col
|
||||||
|
end
|
||||||
|
|
||||||
local function active_buffer_path()
|
local function active_buffer_path()
|
||||||
return pmacs.editor.file_path()
|
return pmacs.editor.file_path()
|
||||||
end
|
end
|
||||||
|
|
@ -321,6 +339,29 @@ local function server_is_live(sid)
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function server_is_initialized(sid)
|
||||||
|
local ok, state = pcall(pmacs.lsp.status, sid)
|
||||||
|
return ok and state and state.kind == "initialized"
|
||||||
|
end
|
||||||
|
|
||||||
|
local function server_supports_inlay_hints(sid)
|
||||||
|
local ok, caps = pcall(pmacs.lsp.capabilities, sid)
|
||||||
|
if not ok or not caps then return false end
|
||||||
|
return caps.inlayHintProvider ~= nil and caps.inlayHintProvider ~= false
|
||||||
|
end
|
||||||
|
|
||||||
|
local function pull_inlay_hints_quiet(rec)
|
||||||
|
if not rec or not server_is_initialized(rec.server) then return end
|
||||||
|
if not server_supports_inlay_hints(rec.server) then return end
|
||||||
|
local end_line, end_col = document_end_position(buffer_text(rec.buffer))
|
||||||
|
pmacs.async(function()
|
||||||
|
pcall(function()
|
||||||
|
pmacs.lsp.request_inlay_hint(
|
||||||
|
rec.server, rec.uri, 0, 0, end_line, end_col):await()
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
-- M_B1: buffers that already had an `LspStyleView` overlay pushed,
|
-- M_B1: buffers that already had an `LspStyleView` overlay pushed,
|
||||||
-- so the after-load / on-demand attach paths don't stack duplicate
|
-- so the after-load / on-demand attach paths don't stack duplicate
|
||||||
-- overlays. Mirrors `highlighted_buffers` in `syntax.lua`; the entry
|
-- overlays. Mirrors `highlighted_buffers` in `syntax.lua`; the entry
|
||||||
|
|
@ -348,7 +389,13 @@ local function attach_buffer(buf)
|
||||||
if not sid then return nil end
|
if not sid then return nil end
|
||||||
local uri = file_uri_for(path)
|
local uri = file_uri_for(path)
|
||||||
if not uri then return nil end
|
if not uri then return nil end
|
||||||
local rec = { language = language, server = sid, uri = uri, version = 1 }
|
local rec = {
|
||||||
|
buffer = buf,
|
||||||
|
language = language,
|
||||||
|
server = sid,
|
||||||
|
uri = uri,
|
||||||
|
version = 1,
|
||||||
|
}
|
||||||
attachments[key] = rec
|
attachments[key] = rec
|
||||||
-- did_open is a notification; the manager queues it cleanly even
|
-- did_open is a notification; the manager queues it cleanly even
|
||||||
-- while the server is in `starting` / `initializing`.
|
-- while the server is in `starting` / `initializing`.
|
||||||
|
|
@ -376,6 +423,7 @@ local function attach_buffer(buf)
|
||||||
local ok, attached = pcall(pmacs.diag._attach_view, buf, uri)
|
local ok, attached = pcall(pmacs.diag._attach_view, buf, uri)
|
||||||
if ok and attached then diag_viewed_buffers[key] = true end
|
if ok and attached then diag_viewed_buffers[key] = true end
|
||||||
end
|
end
|
||||||
|
pull_inlay_hints_quiet(rec)
|
||||||
return rec
|
return rec
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -610,7 +658,7 @@ end
|
||||||
local function repull_for_attachments(sid, request_fn)
|
local function repull_for_attachments(sid, request_fn)
|
||||||
for _, rec in pairs(attachments) do
|
for _, rec in pairs(attachments) do
|
||||||
if rec.server == sid and rec.uri then
|
if rec.server == sid and rec.uri then
|
||||||
pcall(request_fn, sid, rec.uri)
|
pcall(request_fn, sid, rec.uri, rec)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -913,8 +961,8 @@ local function handle_server_requests()
|
||||||
-- Result is `null` on success per the LSP spec; then
|
-- Result is `null` on success per the LSP spec; then
|
||||||
-- re-pull so the store reflects the server's new state.
|
-- re-pull so the store reflects the server's new state.
|
||||||
pcall(pmacs.lsp.send_response, sid, ev.request_id, nil)
|
pcall(pmacs.lsp.send_response, sid, ev.request_id, nil)
|
||||||
repull_for_attachments(sid, function(s, uri)
|
repull_for_attachments(sid, function(_, _, rec)
|
||||||
pmacs.lsp.request_inlay_hint(s, uri, 0, 0, 0xFFFFF, 0)
|
pull_inlay_hints_quiet(rec)
|
||||||
end)
|
end)
|
||||||
elseif ev.kind == "request"
|
elseif ev.kind == "request"
|
||||||
and ev.method == "workspace/semanticTokens/refresh" then
|
and ev.method == "workspace/semanticTokens/refresh" then
|
||||||
|
|
@ -931,6 +979,10 @@ local function handle_server_requests()
|
||||||
-- LSP spells the field "unregisterations".
|
-- LSP spells the field "unregisterations".
|
||||||
pcall(unregister_file_watchers, sid,
|
pcall(unregister_file_watchers, sid,
|
||||||
ev.params and ev.params.unregisterations)
|
ev.params and ev.params.unregisterations)
|
||||||
|
elseif ev.kind == "initialized" then
|
||||||
|
repull_for_attachments(sid, function(_, _, rec)
|
||||||
|
pull_inlay_hints_quiet(rec)
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -1093,16 +1145,16 @@ function pmacs.lsp.inlay_hints()
|
||||||
pmacs.editor.set_status("LSP: no server for active buffer")
|
pmacs.editor.set_status("LSP: no server for active buffer")
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
-- Whole-document range: (0,0) .. (one past the last line, 0). An
|
-- Whole-document range: (0,0) .. exact document end. Some servers,
|
||||||
-- over-wide end line is fine — servers clamp to the document.
|
-- including rust-analyzer, reject one-past or otherwise over-wide
|
||||||
|
-- line numbers instead of clamping.
|
||||||
local text = active_buffer_text()
|
local text = active_buffer_text()
|
||||||
local nl = 0
|
local end_line, end_col = document_end_position(text)
|
||||||
for _ in text:gmatch("\n") do nl = nl + 1 end
|
|
||||||
pmacs.inlay_hint.clear(rec.server, rec.uri)
|
pmacs.inlay_hint.clear(rec.server, rec.uri)
|
||||||
pmacs.async(function()
|
pmacs.async(function()
|
||||||
local ok, err = pcall(function()
|
local ok, err = pcall(function()
|
||||||
pmacs.lsp.request_inlay_hint(
|
pmacs.lsp.request_inlay_hint(
|
||||||
rec.server, rec.uri, 0, 0, nl + 1, 0):await()
|
rec.server, rec.uri, 0, 0, end_line, end_col):await()
|
||||||
end)
|
end)
|
||||||
if not ok then
|
if not ok then
|
||||||
pmacs.editor.set_status("LSP: " .. lsp_await_error(err))
|
pmacs.editor.set_status("LSP: " .. lsp_await_error(err))
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,9 @@ use glyphon::{
|
||||||
TextArea, TextAtlas, TextBounds, TextRenderer, Viewport,
|
TextArea, TextAtlas, TextBounds, TextRenderer, Viewport,
|
||||||
};
|
};
|
||||||
use pmacs_protocol::{
|
use pmacs_protocol::{
|
||||||
BufferId, ByteRange, Decoration, DecorationKind, DecorationSegment, InstanceMessage,
|
AdornmentContent, AdornmentPlacement, BufferId, ByteRange, Decoration, DecorationKind,
|
||||||
StyleSegment, StyleSpan, cell::Color as CellColor,
|
DecorationSegment, InlineAdornment, InstanceMessage, StyleSegment, StyleSpan,
|
||||||
|
cell::Color as CellColor,
|
||||||
};
|
};
|
||||||
use wgpu::MultisampleState;
|
use wgpu::MultisampleState;
|
||||||
use winit::application::ApplicationHandler;
|
use winit::application::ApplicationHandler;
|
||||||
|
|
@ -203,6 +204,13 @@ struct State {
|
||||||
/// are not rendered in session 5; see the session-5 design note
|
/// are not rendered in session 5; see the session-5 design note
|
||||||
/// for the deferred quad-pipeline finding.
|
/// for the deferred quad-pipeline finding.
|
||||||
current_decorations: Vec<Decoration>,
|
current_decorations: Vec<Decoration>,
|
||||||
|
/// Inline virtual text for `current_buffer_id` (session 6).
|
||||||
|
/// Producer-side Phase A currently emits LSP inlay hints as
|
||||||
|
/// `AtOffset` text adornments only. The GUI stores the whole scoped
|
||||||
|
/// set and projects it into the shaped rich text without inserting
|
||||||
|
/// bytes into `current_text`; source byte ranges for style spans and
|
||||||
|
/// decorations therefore remain source-relative.
|
||||||
|
current_adornments: Vec<InlineAdornment>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ApplicationHandler<AppEvent> for App {
|
impl ApplicationHandler<AppEvent> for App {
|
||||||
|
|
@ -402,28 +410,32 @@ impl State {
|
||||||
current_buffer_id: None,
|
current_buffer_id: None,
|
||||||
current_spans: Vec::new(),
|
current_spans: Vec::new(),
|
||||||
current_decorations: Vec::new(),
|
current_decorations: Vec::new(),
|
||||||
|
current_adornments: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Replace the rendered text with `text` and request a redraw.
|
/// Replace the rendered text with `text` and request a redraw.
|
||||||
/// No-op when `text` is byte-identical to the current rendering
|
/// Returns `false` when `text` is byte-identical to the current
|
||||||
/// (avoids the re-shape cost when an unchanged buffer ticks).
|
/// rendering (avoids the re-shape cost when an unchanged buffer
|
||||||
|
/// ticks).
|
||||||
///
|
///
|
||||||
/// Replaces the rope text and routes through `reshape` so the
|
/// Replaces the rope text and routes through `reshape` so the
|
||||||
/// rich-text rendering uses the current `current_spans`. When
|
/// rich-text rendering uses the current spans, decorations, and
|
||||||
/// called from the `CrdtOp` path (text shifted under existing
|
/// inline adornments. When called from the `CrdtOp` path (text
|
||||||
/// spans) the spans are momentarily stale relative to the new
|
/// shifted under existing source anchors) those anchors are
|
||||||
/// byte positions — `reshape` clamps via `range.end.min(text_len)`
|
/// momentarily stale relative to the new byte positions —
|
||||||
/// so rendering is safe, but visual styling may be off until the
|
/// `reshape` clamps via `range.end.min(text_len)` so rendering is
|
||||||
/// daemon's next `StyleSpans` frame catches up. A real artifact;
|
/// safe, but visual styling may be off until the daemon's next
|
||||||
/// classified as a session-4 known limitation rather than a bug.
|
/// semantic frame catches up. A real artifact; classified as a
|
||||||
fn set_text(&mut self, text: &str) {
|
/// known Phase A limitation rather than a bug.
|
||||||
|
fn set_text(&mut self, text: &str) -> bool {
|
||||||
if self.current_text == text {
|
if self.current_text == text {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
self.current_text.clear();
|
self.current_text.clear();
|
||||||
self.current_text.push_str(text);
|
self.current_text.push_str(text);
|
||||||
self.reshape();
|
self.reshape();
|
||||||
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Apply one `InstanceMessage`; return a follow-up
|
/// Apply one `InstanceMessage`; return a follow-up
|
||||||
|
|
@ -444,13 +456,17 @@ impl State {
|
||||||
/// search match). Session 5 renders diagnostic kinds as fg color
|
/// search match). Session 5 renders diagnostic kinds as fg color
|
||||||
/// overrides; background-kind decorations are accumulated but
|
/// overrides; background-kind decorations are accumulated but
|
||||||
/// not painted (see session 5's deferred quad-pipeline finding).
|
/// not painted (see session 5's deferred quad-pipeline finding).
|
||||||
|
/// - `InlineAdornments` — replace the scoped virtual-text set and
|
||||||
|
/// reshape the display projection. Session 6 consumes `AtOffset`
|
||||||
|
/// text adornments (LSP inlay hints); other placements/content
|
||||||
|
/// remain explicitly deferred.
|
||||||
/// - `Goodbye` — surfaced via the reader thread's clean-EOF path,
|
/// - `Goodbye` — surfaced via the reader thread's clean-EOF path,
|
||||||
/// not handled here.
|
/// not handled here.
|
||||||
///
|
///
|
||||||
/// Remaining `SemanticFrame` variants (`InlineAdornments`,
|
/// Remaining `SemanticFrame` variants (`FileStyleSummary`) plus
|
||||||
/// `FileStyleSummary`) plus the grid variants (`CellDelta`,
|
/// the grid variants (`CellDelta`, `Cursor`, `CursorByte`) and
|
||||||
/// `Cursor`, `CursorByte`) and presence updates are ignored in
|
/// presence updates are ignored in session 6 — they land in
|
||||||
/// session 5 — they land in subsequent Phase A sessions.
|
/// subsequent Phase A sessions.
|
||||||
fn apply_attach_message(&mut self, msg: InstanceMessage) -> Option<ViewportSend> {
|
fn apply_attach_message(&mut self, msg: InstanceMessage) -> Option<ViewportSend> {
|
||||||
match msg {
|
match msg {
|
||||||
InstanceMessage::BufferSnapshot {
|
InstanceMessage::BufferSnapshot {
|
||||||
|
|
@ -471,7 +487,10 @@ impl State {
|
||||||
// buffer is authoritative.
|
// buffer is authoritative.
|
||||||
self.current_spans.clear();
|
self.current_spans.clear();
|
||||||
self.current_decorations.clear();
|
self.current_decorations.clear();
|
||||||
self.set_text(&text);
|
self.current_adornments.clear();
|
||||||
|
if !self.set_text(&text) {
|
||||||
|
self.reshape();
|
||||||
|
}
|
||||||
Some(ViewportSend {
|
Some(ViewportSend {
|
||||||
buffer_id,
|
buffer_id,
|
||||||
visible: ByteRange {
|
visible: ByteRange {
|
||||||
|
|
@ -514,6 +533,12 @@ impl State {
|
||||||
// *incremental* updates ship dirty-range spans only,
|
// *incremental* updates ship dirty-range spans only,
|
||||||
// and an emptied cache loses the non-dirty viewport
|
// and an emptied cache loses the non-dirty viewport
|
||||||
// styling entirely.
|
// styling entirely.
|
||||||
|
//
|
||||||
|
// InlineAdornments use whole-set suppression rather
|
||||||
|
// than dirty segments, so the same ownership rule
|
||||||
|
// applies here: keep the last set until the producer
|
||||||
|
// sends a replacement. Session 8's temporal probe is
|
||||||
|
// where visible edit-flicker/staleness gets scored.
|
||||||
let text = doc.get_text(LORO_TEXT_CONTAINER).to_string();
|
let text = doc.get_text(LORO_TEXT_CONTAINER).to_string();
|
||||||
self.set_text(&text);
|
self.set_text(&text);
|
||||||
None
|
None
|
||||||
|
|
@ -552,6 +577,15 @@ impl State {
|
||||||
self.reshape();
|
self.reshape();
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
InstanceMessage::InlineAdornments { buffer_id, items } => {
|
||||||
|
if self.current_buffer_id != Some(buffer_id) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
self.current_adornments = items;
|
||||||
|
self.current_adornments.sort_by_key(|a| a.at);
|
||||||
|
self.reshape();
|
||||||
|
None
|
||||||
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -702,17 +736,13 @@ impl State {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Re-build the cosmic-text Buffer from `current_text` +
|
/// Re-build the cosmic-text Buffer from `current_text` +
|
||||||
/// `current_spans` + `current_decorations`. Computes a sorted
|
/// `current_spans` + `current_decorations` +
|
||||||
/// boundary list (every span and decoration edge, plus 0 and
|
/// `current_adornments`. Source styling/decorations remain
|
||||||
/// `text_len`) and emits one chunk per `[boundary_i, boundary_{i+1})`
|
/// byte-indexed into `current_text`; adornments contribute extra
|
||||||
/// interval. Effective color picks the first matching decoration
|
/// rich-text chunks at their anchors without mutating the source
|
||||||
/// kind with a renderable color (diagnostics in session 5; the
|
/// string. That display projection is the central session-6
|
||||||
/// background-needing kinds — `Selection` / `SearchMatch` /
|
/// invariant: virtual text must not shift the source-byte ranges
|
||||||
/// `SearchMatchActive` / `CurrentLine` — produce `None` and fall
|
/// used by `StyleSpans` / `Decorations`.
|
||||||
/// through to span color). The decoration override means a
|
|
||||||
/// diagnostic squiggle's color beats syntax color for the bytes
|
|
||||||
/// it covers, which matches user expectation across both
|
|
||||||
/// reference editors and the pmacs TUI.
|
|
||||||
///
|
///
|
||||||
/// Complexity is O(B × (S + D)) per reshape where B is the boundary
|
/// Complexity is O(B × (S + D)) per reshape where B is the boundary
|
||||||
/// count and S+D is spans+decorations. For viewport-scoped data
|
/// count and S+D is spans+decorations. For viewport-scoped data
|
||||||
|
|
@ -721,61 +751,21 @@ impl State {
|
||||||
/// surfaces in profile data — recorded but not done in session 5.
|
/// surfaces in profile data — recorded but not done in session 5.
|
||||||
fn reshape(&mut self) {
|
fn reshape(&mut self) {
|
||||||
let default_attrs = Attrs::new().family(Family::Name("JetBrains Mono"));
|
let default_attrs = Attrs::new().family(Family::Name("JetBrains Mono"));
|
||||||
let text_len = self.current_text.len() as u64;
|
let chunks: Vec<(String, Attrs<'static>)> = projected_rich_chunks(
|
||||||
|
&self.current_text,
|
||||||
// Collect every interesting byte position. Clamp to text_len
|
&self.current_spans,
|
||||||
// so a stale span/decoration past EOF (CrdtOp→next-frame race)
|
&self.current_decorations,
|
||||||
// can't index out.
|
&self.current_adornments,
|
||||||
let mut boundaries: Vec<u64> = vec![0, text_len];
|
)
|
||||||
for sp in &self.current_spans {
|
.into_iter()
|
||||||
boundaries.push(sp.range.start.min(text_len));
|
.map(|chunk| {
|
||||||
boundaries.push(sp.range.end.min(text_len));
|
|
||||||
}
|
|
||||||
for d in &self.current_decorations {
|
|
||||||
boundaries.push(d.range.start.min(text_len));
|
|
||||||
boundaries.push(d.range.end.min(text_len));
|
|
||||||
}
|
|
||||||
boundaries.sort_unstable();
|
|
||||||
boundaries.dedup();
|
|
||||||
|
|
||||||
let mut chunks: Vec<(String, Attrs<'static>)> = Vec::new();
|
|
||||||
for w in boundaries.windows(2) {
|
|
||||||
let (a, b) = (w[0], w[1]);
|
|
||||||
if a >= b {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// Pick the effective color at byte `a` (which is also the
|
|
||||||
// color for every byte in `[a, b)` since boundaries
|
|
||||||
// bracket every coverage change).
|
|
||||||
let mut color: Option<glyphon::Color> = None;
|
|
||||||
for d in &self.current_decorations {
|
|
||||||
if d.range.start <= a
|
|
||||||
&& a < d.range.end
|
|
||||||
&& let Some(c) = decoration_kind_to_color(d.kind)
|
|
||||||
{
|
|
||||||
color = Some(c);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if color.is_none() {
|
|
||||||
for sp in &self.current_spans {
|
|
||||||
if sp.range.start <= a && a < sp.range.end {
|
|
||||||
color = cell_color_to_glyphon(sp.style.fg);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let mut attrs = default_attrs.clone();
|
let mut attrs = default_attrs.clone();
|
||||||
if let Some(c) = color {
|
if let Some(c) = chunk.color {
|
||||||
attrs = attrs.color(c);
|
attrs = attrs.color(c);
|
||||||
}
|
}
|
||||||
chunks.push((self.current_text[a as usize..b as usize].to_owned(), attrs));
|
(chunk.text, attrs)
|
||||||
}
|
})
|
||||||
// No spans / decorations + empty text ⇒ feed one empty chunk
|
.collect();
|
||||||
// so set_rich_text has something to draw.
|
|
||||||
if chunks.is_empty() {
|
|
||||||
chunks.push((String::new(), default_attrs.clone()));
|
|
||||||
}
|
|
||||||
self.buffer.set_rich_text(
|
self.buffer.set_rich_text(
|
||||||
&mut self.font_system,
|
&mut self.font_system,
|
||||||
chunks.iter().map(|(s, a)| (s.as_str(), a.clone())),
|
chunks.iter().map(|(s, a)| (s.as_str(), a.clone())),
|
||||||
|
|
@ -876,6 +866,128 @@ impl State {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct RichChunk {
|
||||||
|
text: String,
|
||||||
|
color: Option<glyphon::Color>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the rich-text chunks fed to glyphon. Source chunks come from
|
||||||
|
/// `text` and retain source-byte styling; inline adornments create
|
||||||
|
/// extra chunks at their anchors and therefore do not shift any source
|
||||||
|
/// span/decoration range.
|
||||||
|
fn projected_rich_chunks(
|
||||||
|
text: &str,
|
||||||
|
spans: &[StyleSpan],
|
||||||
|
decorations: &[Decoration],
|
||||||
|
adornments: &[InlineAdornment],
|
||||||
|
) -> Vec<RichChunk> {
|
||||||
|
let text_len = text.len() as u64;
|
||||||
|
let mut boundaries: Vec<u64> = vec![0, text_len];
|
||||||
|
for sp in spans {
|
||||||
|
boundaries.push(sp.range.start.min(text_len));
|
||||||
|
boundaries.push(sp.range.end.min(text_len));
|
||||||
|
}
|
||||||
|
for d in decorations {
|
||||||
|
boundaries.push(d.range.start.min(text_len));
|
||||||
|
boundaries.push(d.range.end.min(text_len));
|
||||||
|
}
|
||||||
|
let mut renderable_adornments: Vec<(usize, u64, &InlineAdornment)> = adornments
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(idx, a)| renderable_adornment_anchor(a, text_len).map(|at| (idx, at, a)))
|
||||||
|
.collect();
|
||||||
|
for (_, at, _) in &renderable_adornments {
|
||||||
|
boundaries.push(*at);
|
||||||
|
}
|
||||||
|
boundaries.sort_unstable();
|
||||||
|
boundaries.dedup();
|
||||||
|
renderable_adornments.sort_by_key(|(idx, at, _)| (*at, *idx));
|
||||||
|
|
||||||
|
let mut chunks = Vec::new();
|
||||||
|
let mut adorn_idx = 0usize;
|
||||||
|
for w in boundaries.windows(2) {
|
||||||
|
let (a, b) = (w[0], w[1]);
|
||||||
|
push_adornments_at(&mut chunks, &renderable_adornments, &mut adorn_idx, a);
|
||||||
|
if a < b {
|
||||||
|
chunks.push(RichChunk {
|
||||||
|
text: text[a as usize..b as usize].to_owned(),
|
||||||
|
color: source_color_at(a, spans, decorations),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
push_adornments_at(
|
||||||
|
&mut chunks,
|
||||||
|
&renderable_adornments,
|
||||||
|
&mut adorn_idx,
|
||||||
|
text_len,
|
||||||
|
);
|
||||||
|
if chunks.is_empty() {
|
||||||
|
chunks.push(RichChunk {
|
||||||
|
text: String::new(),
|
||||||
|
color: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
chunks
|
||||||
|
}
|
||||||
|
|
||||||
|
fn renderable_adornment_anchor(adornment: &InlineAdornment, text_len: u64) -> Option<u64> {
|
||||||
|
match (&adornment.placement, &adornment.content) {
|
||||||
|
(AdornmentPlacement::AtOffset, AdornmentContent::Text { .. }) => {
|
||||||
|
Some(adornment.at.min(text_len))
|
||||||
|
}
|
||||||
|
// Session 6 consumes the inlay-hint producer surface only.
|
||||||
|
// Other placements and resource handles need layout/resource
|
||||||
|
// policy, so silently ignore them until their sessions land.
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_adornments_at(
|
||||||
|
chunks: &mut Vec<RichChunk>,
|
||||||
|
adornments: &[(usize, u64, &InlineAdornment)],
|
||||||
|
next: &mut usize,
|
||||||
|
at: u64,
|
||||||
|
) {
|
||||||
|
while let Some((_, anchor, adornment)) = adornments.get(*next).copied() {
|
||||||
|
if anchor != at {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if let AdornmentContent::Text { text, style } = &adornment.content {
|
||||||
|
chunks.push(RichChunk {
|
||||||
|
text: text.clone(),
|
||||||
|
color: Some(adornment_text_color(style.fg)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
*next += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn adornment_text_color(fg: CellColor) -> glyphon::Color {
|
||||||
|
cell_color_to_glyphon(fg).unwrap_or_else(|| glyphon::Color::rgb(130, 130, 140))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn source_color_at(
|
||||||
|
byte: u64,
|
||||||
|
spans: &[StyleSpan],
|
||||||
|
decorations: &[Decoration],
|
||||||
|
) -> Option<glyphon::Color> {
|
||||||
|
for d in decorations {
|
||||||
|
if d.range.start <= byte
|
||||||
|
&& byte < d.range.end
|
||||||
|
&& let Some(c) = decoration_kind_to_color(d.kind)
|
||||||
|
{
|
||||||
|
return Some(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for sp in spans {
|
||||||
|
if sp.range.start <= byte && byte < sp.range.end {
|
||||||
|
return cell_color_to_glyphon(sp.style.fg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
/// Convert a `pmacs-protocol::cell::Color` to a `glyphon::Color`.
|
/// Convert a `pmacs-protocol::cell::Color` to a `glyphon::Color`.
|
||||||
/// Returns `None` for `Default` so the renderer falls back to the
|
/// Returns `None` for `Default` so the renderer falls back to the
|
||||||
/// `Attrs` default color (white-ish in our render) rather than
|
/// `Attrs` default color (white-ish in our render) rather than
|
||||||
|
|
@ -969,3 +1081,133 @@ fn decoration_kind_to_color(kind: DecorationKind) -> Option<glyphon::Color> {
|
||||||
| DecorationKind::CurrentLine => None,
|
| DecorationKind::CurrentLine => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use pmacs_protocol::cell::Style;
|
||||||
|
|
||||||
|
fn style_with_fg(fg: CellColor) -> Style {
|
||||||
|
Style {
|
||||||
|
fg,
|
||||||
|
..Style::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn span(start: u64, end: u64, fg: CellColor) -> StyleSpan {
|
||||||
|
StyleSpan {
|
||||||
|
range: ByteRange { start, end },
|
||||||
|
style: style_with_fg(fg),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn adornment(at: u64, placement: AdornmentPlacement, text: &str) -> InlineAdornment {
|
||||||
|
InlineAdornment {
|
||||||
|
at,
|
||||||
|
placement,
|
||||||
|
content: AdornmentContent::Text {
|
||||||
|
text: text.to_owned(),
|
||||||
|
style: Style::default(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resource_adornment(at: u64, placement: AdornmentPlacement) -> InlineAdornment {
|
||||||
|
InlineAdornment {
|
||||||
|
at,
|
||||||
|
placement,
|
||||||
|
content: AdornmentContent::Resource { handle: 7 },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn chunk_texts(chunks: &[RichChunk]) -> Vec<&str> {
|
||||||
|
chunks.iter().map(|chunk| chunk.text.as_str()).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn projected_rich_chunks_inserts_at_offset_without_source_bytes() {
|
||||||
|
let chunks = projected_rich_chunks(
|
||||||
|
"abcd",
|
||||||
|
&[],
|
||||||
|
&[],
|
||||||
|
&[adornment(2, AdornmentPlacement::AtOffset, "X")],
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(chunk_texts(&chunks), vec!["ab", "X", "cd"]);
|
||||||
|
let rendered: String = chunks.iter().map(|chunk| chunk.text.as_str()).collect();
|
||||||
|
assert_eq!(rendered, "abXcd");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn inline_adornment_does_not_shift_source_style_ranges() {
|
||||||
|
let chunks = projected_rich_chunks(
|
||||||
|
"abcd",
|
||||||
|
&[span(2, 4, CellColor::Indexed(1))],
|
||||||
|
&[],
|
||||||
|
&[adornment(2, AdornmentPlacement::AtOffset, "X")],
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(chunk_texts(&chunks), vec!["ab", "X", "cd"]);
|
||||||
|
assert!(chunks[0].color.is_none());
|
||||||
|
assert!(
|
||||||
|
chunks[1].color.is_some(),
|
||||||
|
"default-styled virtual text should render as muted adornment text"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
chunks[2].color.is_some(),
|
||||||
|
"source styling must still begin at source byte 2"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn inline_adornment_does_not_shift_source_decoration_ranges() {
|
||||||
|
let chunks = projected_rich_chunks(
|
||||||
|
"abcd",
|
||||||
|
&[],
|
||||||
|
&[Decoration {
|
||||||
|
range: ByteRange { start: 2, end: 4 },
|
||||||
|
kind: DecorationKind::DiagnosticError,
|
||||||
|
}],
|
||||||
|
&[adornment(2, AdornmentPlacement::AtOffset, "X")],
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(chunk_texts(&chunks), vec!["ab", "X", "cd"]);
|
||||||
|
assert!(chunks[0].color.is_none());
|
||||||
|
assert!(
|
||||||
|
chunks[1].color.is_some(),
|
||||||
|
"default-styled virtual text should render as muted adornment text"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
chunks[2].color.is_some(),
|
||||||
|
"diagnostic fg override must still begin at source byte 2"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unsupported_adornment_placements_are_ignored_for_session_6() {
|
||||||
|
let chunks = projected_rich_chunks(
|
||||||
|
"abcd",
|
||||||
|
&[],
|
||||||
|
&[],
|
||||||
|
&[
|
||||||
|
adornment(0, AdornmentPlacement::BeforeLine, "before"),
|
||||||
|
adornment(4, AdornmentPlacement::EndOfLine, "end"),
|
||||||
|
resource_adornment(2, AdornmentPlacement::AtOffset),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(chunk_texts(&chunks), vec!["abcd"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn adornment_anchor_past_end_clamps_to_end() {
|
||||||
|
let chunks = projected_rich_chunks(
|
||||||
|
"abcd",
|
||||||
|
&[],
|
||||||
|
&[],
|
||||||
|
&[adornment(99, AdornmentPlacement::AtOffset, "X")],
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(chunk_texts(&chunks), vec!["abcd", "X"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@
|
||||||
//! `PMACS_FAKE_LSP_ROOT_SINK`, so a test can assert the
|
//! `PMACS_FAKE_LSP_ROOT_SINK`, so a test can assert the
|
||||||
//! auto-attach path derives the project root from the opened file.
|
//! auto-attach path derives the project root from the opened file.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::io::{self, Read, Write};
|
use std::io::{self, Read, Write};
|
||||||
|
|
||||||
#[allow(
|
#[allow(
|
||||||
|
|
@ -48,6 +49,7 @@ fn main() {
|
||||||
let mut stdin = io::stdin().lock();
|
let mut stdin = io::stdin().lock();
|
||||||
let mut stdout = io::stdout().lock();
|
let mut stdout = io::stdout().lock();
|
||||||
let mut crashed_after_init = false;
|
let mut crashed_after_init = false;
|
||||||
|
let mut open_docs: HashMap<String, String> = HashMap::new();
|
||||||
loop {
|
loop {
|
||||||
let body = match read_frame(&mut stdin) {
|
let body = match read_frame(&mut stdin) {
|
||||||
Ok(Some(b)) => b,
|
Ok(Some(b)) => b,
|
||||||
|
|
@ -124,6 +126,7 @@ fn main() {
|
||||||
"hoverProvider": true,
|
"hoverProvider": true,
|
||||||
"completionProvider": { "triggerCharacters": ["."] },
|
"completionProvider": { "triggerCharacters": ["."] },
|
||||||
"definitionProvider": true,
|
"definitionProvider": true,
|
||||||
|
"inlayHintProvider": true,
|
||||||
"documentFormattingProvider": true,
|
"documentFormattingProvider": true,
|
||||||
"diagnosticProvider": { "interFileDependencies": false, "workspaceDiagnostics": false },
|
"diagnosticProvider": { "interFileDependencies": false, "workspaceDiagnostics": false },
|
||||||
"semanticTokensProvider": {
|
"semanticTokensProvider": {
|
||||||
|
|
@ -348,6 +351,24 @@ fn main() {
|
||||||
.and_then(|t| t.get("uri"))
|
.and_then(|t| t.get("uri"))
|
||||||
.cloned()
|
.cloned()
|
||||||
.unwrap_or(serde_json::Value::Null);
|
.unwrap_or(serde_json::Value::Null);
|
||||||
|
if let Some(uri_s) = uri.as_str() {
|
||||||
|
let text = if method == "textDocument/didOpen" {
|
||||||
|
params
|
||||||
|
.get("textDocument")
|
||||||
|
.and_then(|t| t.get("text"))
|
||||||
|
.and_then(serde_json::Value::as_str)
|
||||||
|
} else {
|
||||||
|
params
|
||||||
|
.get("contentChanges")
|
||||||
|
.and_then(serde_json::Value::as_array)
|
||||||
|
.and_then(|a| a.first())
|
||||||
|
.and_then(|c| c.get("text"))
|
||||||
|
.and_then(serde_json::Value::as_str)
|
||||||
|
};
|
||||||
|
if let Some(text) = text {
|
||||||
|
open_docs.insert(uri_s.to_owned(), text.to_owned());
|
||||||
|
}
|
||||||
|
}
|
||||||
let echo = serde_json::json!({
|
let echo = serde_json::json!({
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"method": "pmacs/echo",
|
"method": "pmacs/echo",
|
||||||
|
|
@ -756,6 +777,17 @@ fn main() {
|
||||||
write_frame(&mut stdout, &resp);
|
write_frame(&mut stdout, &resp);
|
||||||
}
|
}
|
||||||
("textDocument/inlayHint", Some(idv)) => {
|
("textDocument/inlayHint", Some(idv)) => {
|
||||||
|
if mode == "inlaybounds"
|
||||||
|
&& let Some(message) = inlay_range_error(¶ms, &open_docs)
|
||||||
|
{
|
||||||
|
let resp = serde_json::json!({
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": idv,
|
||||||
|
"error": { "code": -32603, "message": message }
|
||||||
|
});
|
||||||
|
write_frame(&mut stdout, &resp);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
// T M4.5: a type hint (string label, kind 1) and a
|
// T M4.5: a type hint (string label, kind 1) and a
|
||||||
// parameter hint (label *parts*, kind 2) so both
|
// parameter hint (label *parts*, kind 2) so both
|
||||||
// label shapes are exercised.
|
// label shapes are exercised.
|
||||||
|
|
@ -906,3 +938,39 @@ fn write_garbage() {
|
||||||
let _ = stdout.write_all(b"NotAValidLspFrame\r\nGarbageHeader\r\n\r\n{}");
|
let _ = stdout.write_all(b"NotAValidLspFrame\r\nGarbageHeader\r\n\r\n{}");
|
||||||
let _ = stdout.flush();
|
let _ = stdout.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn document_end_position(text: &str) -> (u64, u64) {
|
||||||
|
let mut line = 0;
|
||||||
|
let mut col = 0;
|
||||||
|
for byte in text.bytes() {
|
||||||
|
if byte == b'\n' {
|
||||||
|
line += 1;
|
||||||
|
col = 0;
|
||||||
|
} else {
|
||||||
|
col += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(line, col)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inlay_range_error(
|
||||||
|
params: &serde_json::Value,
|
||||||
|
open_docs: &HashMap<String, String>,
|
||||||
|
) -> Option<String> {
|
||||||
|
let uri = params
|
||||||
|
.get("textDocument")
|
||||||
|
.and_then(|t| t.get("uri"))
|
||||||
|
.and_then(serde_json::Value::as_str)?;
|
||||||
|
let text = open_docs.get(uri)?;
|
||||||
|
let (last_line, last_col) = document_end_position(text);
|
||||||
|
let end = params.get("range")?.get("end")?;
|
||||||
|
let line = end.get("line")?.as_u64()?;
|
||||||
|
let col = end.get("character")?.as_u64()?;
|
||||||
|
if line > last_line || (line == last_line && col > last_col) {
|
||||||
|
Some(format!(
|
||||||
|
"invalid inlay range end {line}:{col}; document ends at {last_line}:{last_col}"
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2037,8 +2037,8 @@ impl LspManager {
|
||||||
let params = json!({
|
let params = json!({
|
||||||
"textDocument": { "uri": uri.clone() },
|
"textDocument": { "uri": uri.clone() },
|
||||||
"range": {
|
"range": {
|
||||||
"start": { "line": start_line, "character": start_col },
|
"start": self.outbound_position(sid, &uri, start_line, start_col),
|
||||||
"end": { "line": end_line, "character": end_col },
|
"end": self.outbound_position(sid, &uri, end_line, end_col),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
let req_id = self.send_request(sid, "textDocument/inlayHint", params)?;
|
let req_id = self.send_request(sid, "textDocument/inlayHint", params)?;
|
||||||
|
|
|
||||||
|
|
@ -3983,14 +3983,14 @@ fn m4_15_workspace_edit_resource_ops_apply_in_order() {
|
||||||
#[test]
|
#[test]
|
||||||
fn m4_16_lua_surface_drives_inlay_hints() {
|
fn m4_16_lua_surface_drives_inlay_hints() {
|
||||||
let mut s = pmacs::editor::EditorState::new();
|
let mut s = pmacs::editor::EditorState::new();
|
||||||
spawn_lsp_and_init(&mut s, None);
|
spawn_lsp_and_init(&mut s, Some("inlaybounds"));
|
||||||
|
|
||||||
let uri = "file:///tmp/m4_16_inlay.rs";
|
let uri = "file:///tmp/m4_16_inlay.rs";
|
||||||
s.lua_host
|
s.lua_host
|
||||||
.lua()
|
.lua()
|
||||||
.load(format!(
|
.load(format!(
|
||||||
"pmacs.lsp.did_open(_G._lsp, '{uri}', 1, 'let x = 1\\nfn f() {{}}\\n')
|
"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)"
|
pmacs.lsp.request_inlay_hint(_G._lsp, '{uri}', 0, 0, 2, 0)"
|
||||||
))
|
))
|
||||||
.exec()
|
.exec()
|
||||||
.expect("kick off inlay hint request");
|
.expect("kick off inlay hint request");
|
||||||
|
|
@ -4151,6 +4151,57 @@ fn m4_18_inlay_hint_refresh_repulls_via_server_request() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Session 6 follow-up: the GPU consumer can only render
|
||||||
|
/// `InlineAdornments` once the LSP inlay-hint store has data. A
|
||||||
|
/// server is not required to send `workspace/inlayHint/refresh` after
|
||||||
|
/// initialize, so the default LSP runtime should pull hints once for
|
||||||
|
/// an attached document when the server reaches `initialized`.
|
||||||
|
#[test]
|
||||||
|
fn m4_18b_inlay_hints_auto_pull_after_initialize() {
|
||||||
|
use pmacs::editor::EditorState;
|
||||||
|
|
||||||
|
let dir = tempfile::tempdir().expect("tempdir");
|
||||||
|
let a_path = dir.path().join("a.rs");
|
||||||
|
std::fs::write(&a_path, b"fn a() {}\n\n").expect("write a");
|
||||||
|
let a_disp = a_path.display().to_string();
|
||||||
|
|
||||||
|
let mut state = EditorState::new();
|
||||||
|
let fake = fake_lsp_path();
|
||||||
|
state
|
||||||
|
.lua_host
|
||||||
|
.lua()
|
||||||
|
.load(format!(
|
||||||
|
"pmacs.lsp.config.rust = {{
|
||||||
|
command = '{fake}',
|
||||||
|
env = {{ PMACS_FAKE_LSP_MODE = 'inlaybounds' }},
|
||||||
|
}}"
|
||||||
|
))
|
||||||
|
.exec()
|
||||||
|
.expect("override rust config");
|
||||||
|
state
|
||||||
|
.lua_host
|
||||||
|
.lua()
|
||||||
|
.load(format!("pmacs.buffer.find_or_open('{a_disp}')"))
|
||||||
|
.exec()
|
||||||
|
.expect("open a.rs");
|
||||||
|
|
||||||
|
let flag = format!(
|
||||||
|
"(function() \
|
||||||
|
local sid \
|
||||||
|
for _,r in ipairs(pmacs.lsp.list()) do \
|
||||||
|
if r.state and r.state.kind=='initialized' then sid=r.id end \
|
||||||
|
end \
|
||||||
|
if not sid then return false end \
|
||||||
|
local h = pmacs.inlay_hint.hints(sid, 'file://{a_disp}') \
|
||||||
|
return h ~= nil and #h > 0 \
|
||||||
|
end)()"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
pump_lua_flag(&mut state, &flag, 5),
|
||||||
|
"initialized inlay-capable server did not auto-pull hints into the store"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// T M4.5 — server-driven semantic-tokens refresh. The
|
/// T M4.5 — server-driven semantic-tokens refresh. The
|
||||||
/// `semantictokensrefresh` fake sends `workspace/semanticTokens/
|
/// `semantictokensrefresh` fake sends `workspace/semanticTokens/
|
||||||
/// refresh` right after `initialized`; the pump must answer it and
|
/// refresh` right after `initialized`; the pump must answer it and
|
||||||
|
|
@ -4965,6 +5016,73 @@ fn m4_28_real_clangd_diagnostics_and_semantic_tokens_via_auto_attach() {
|
||||||
assert_no_lsp_crash(&mut state, "clangd");
|
assert_no_lsp_crash(&mut state, "clangd");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// PATH-gated real-server hardening for Session 6: rust-analyzer
|
||||||
|
/// rejects over-wide `textDocument/inlayHint` ranges instead of
|
||||||
|
/// clamping them. The default-bundle auto-attach path must therefore
|
||||||
|
/// pull inlay hints over the exact document end, otherwise
|
||||||
|
/// `pmacs-gpu` receives no `InlineAdornments` for ordinary Rust files.
|
||||||
|
#[test]
|
||||||
|
fn m4_29_real_rust_analyzer_inlay_hints_via_auto_attach() {
|
||||||
|
use pmacs::editor::EditorState;
|
||||||
|
|
||||||
|
let Ok(rust_analyzer) = which_binary("rust-analyzer") else {
|
||||||
|
eprintln!("rust-analyzer not on PATH; skipping");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let rust_analyzer = rust_analyzer.display().to_string();
|
||||||
|
|
||||||
|
let dir = tempfile::tempdir().expect("tempdir");
|
||||||
|
let root = std::fs::canonicalize(dir.path()).expect("canonicalize");
|
||||||
|
std::fs::create_dir(root.join("src")).expect("mkdir src");
|
||||||
|
std::fs::write(
|
||||||
|
root.join("Cargo.toml"),
|
||||||
|
b"[package]\nname = \"pmacs_ra_inlay_hardening\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
|
||||||
|
)
|
||||||
|
.expect("Cargo.toml");
|
||||||
|
let src = "use std::collections::HashMap;\n\
|
||||||
|
\n\
|
||||||
|
fn main() {\n\
|
||||||
|
\tlet answer = 42;\n\
|
||||||
|
\tlet pi = 3.14;\n\
|
||||||
|
\tlet mut counts = HashMap::new();\n\
|
||||||
|
\tcounts.insert(\"a\", 1);\n\
|
||||||
|
\tlet _g = format_pair(answer, pi);\n\
|
||||||
|
}\n\
|
||||||
|
\n\
|
||||||
|
fn format_pair(n: i32, x: f64) -> String {\n\
|
||||||
|
\tformat!(\"{n}-{x}\")\n\
|
||||||
|
}\n";
|
||||||
|
let file = root.join("src/main.rs");
|
||||||
|
std::fs::write(&file, src).expect("main.rs");
|
||||||
|
|
||||||
|
let root_disp = root.display().to_string();
|
||||||
|
let file_disp = file.display().to_string();
|
||||||
|
let uri = format!("file://{file_disp}");
|
||||||
|
|
||||||
|
let mut state = EditorState::new();
|
||||||
|
real_server_open_and_init(&mut state, "rust", &rust_analyzer, &root_disp, &file_disp);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
pump_lua_flag(
|
||||||
|
&mut state,
|
||||||
|
&format!(
|
||||||
|
"(function() \
|
||||||
|
local sid \
|
||||||
|
for _,r in ipairs(pmacs.lsp.list()) do \
|
||||||
|
if r.state and r.state.kind=='initialized' then sid=r.id end \
|
||||||
|
end \
|
||||||
|
if not sid then return false end \
|
||||||
|
local h = pmacs.inlay_hint.hints(sid, '{uri}') \
|
||||||
|
return h ~= nil and #h > 0 \
|
||||||
|
end)()"
|
||||||
|
),
|
||||||
|
30,
|
||||||
|
),
|
||||||
|
"real rust-analyzer returned no inlay hints via auto-attach"
|
||||||
|
);
|
||||||
|
assert_no_lsp_crash(&mut state, "rust-analyzer");
|
||||||
|
}
|
||||||
|
|
||||||
/// Default LSP bundle (`builtin/runtime/lsp.lua`) is wired in: the
|
/// Default LSP bundle (`builtin/runtime/lsp.lua`) is wired in: the
|
||||||
/// hooks are defined, the namespace tables exist, the user-facing
|
/// hooks are defined, the namespace tables exist, the user-facing
|
||||||
/// commands are registered with the command registry, and the default
|
/// commands are registered with the command registry, and the default
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue