Skip to main content

mcpls_core/bridge/translator/
dto.rs

1//! Public MCP-facing result/data-transfer types returned by the tool-call
2//! handlers in the sibling domain modules.
3
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6
7/// Convert an LSP integer-valued enum (`SymbolKind`, `CompletionItemKind`,
8/// `InlayHintKind`, ...) to its wire-format `u32`.
9///
10/// Infallible and needs no fallback value, unlike a `serde_json` roundtrip.
11pub(super) fn lsp_kind_to_u32<T: Into<u32>>(kind: T) -> u32 {
12    kind.into()
13}
14
15/// Position in a document (1-based for MCP).
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
17pub struct Position2D {
18    /// Line number (1-based).
19    pub line: u32,
20    /// Character offset (1-based).
21    pub character: u32,
22}
23
24/// A 1-based MCP position taken as input by `Translator::handle_*` methods.
25///
26/// Kept distinct from [`Position2D`] (which carries an *output* position back
27/// to the caller) so passing a position into a handler always goes through a
28/// named-field struct literal (`Position { line, character }`) instead of two
29/// adjacent bare `u32` arguments -- a call site that swaps `line` and
30/// `character` no longer compiles instead of silently sending a wrong
31/// position to the LSP server (#322).
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub struct Position {
34    /// Line number (1-based).
35    pub line: u32,
36    /// Character offset (1-based).
37    pub character: u32,
38}
39
40/// Range in a document (1-based for MCP).
41#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
42pub struct Range {
43    /// Start position.
44    pub start: Position2D,
45    /// End position.
46    pub end: Position2D,
47}
48
49/// Location in a document.
50#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
51pub struct Location {
52    /// URI of the document.
53    pub uri: String,
54    /// Range within the document.
55    pub range: Range,
56    /// Whether this location is not provably inside any configured
57    /// workspace root (e.g. the standard library or a crates.io dependency).
58    ///
59    /// Advisory only, not a security/safety guarantee: read-only navigation
60    /// results are never filtered by workspace containment (see
61    /// `bridge::uri_in_workspace_roots`'s docs for why), so callers that
62    /// want to apply their own policy toward out-of-workspace locations can
63    /// check this flag. The underlying check is purely lexical -- it does
64    /// not resolve symlinks -- so a location reached through a symlinked
65    /// workspace root (e.g. macOS's `/var` -> `/private/var`, or a package
66    /// manager's symlinked dependency store) can read `true` even though it
67    /// is genuinely inside the workspace. Also always `true` when no
68    /// workspace roots are configured, consistent with
69    /// `bridge::uri_in_workspace_roots`'s fail-closed convention: without a
70    /// configured root, nothing can be vouched for as inside the workspace.
71    /// Omitted (defaults to `false`) when serialized.
72    #[serde(default, skip_serializing_if = "is_false")]
73    pub out_of_workspace: bool,
74}
75
76/// `skip_serializing_if` predicate for a `bool` field that should be omitted
77/// from the serialized output when `false`.
78///
79/// Takes `&bool` rather than `bool` because serde's `skip_serializing_if`
80/// always calls the predicate with a field reference.
81#[allow(clippy::trivially_copy_pass_by_ref)]
82const fn is_false(value: &bool) -> bool {
83    !*value
84}
85
86/// Result of a hover request.
87#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct HoverResult {
89    /// Hover contents as markdown string.
90    pub contents: String,
91    /// Optional range the hover applies to.
92    pub range: Option<Range>,
93    /// Whether a position in this response could not be resolved for
94    /// encoding conversion -- disk-read budget exhaustion, an unresolvable
95    /// server-supplied path, a line past EOF, or invalid UTF-8 content can
96    /// each cause this -- meaning `range`'s `character` values may be
97    /// wrong for a non-UTF-16 LSP server (#497). Omitted (defaults to
98    /// `false`) when serialized.
99    #[serde(default, skip_serializing_if = "is_false")]
100    pub positions_degraded: bool,
101}
102
103/// Result of a definition request.
104#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
105pub struct DefinitionResult {
106    /// Locations of the definition.
107    pub locations: Vec<Location>,
108    /// Whether `locations` was capped below the LSP server's full response
109    /// (see `MAX_NORMALIZED_LOCATIONS`, #474) -- if `true`, more locations
110    /// exist than are returned here. Omitted (defaults to `false`) when
111    /// serialized.
112    #[serde(default, skip_serializing_if = "is_false")]
113    pub truncated: bool,
114    /// Whether a position in this response could not be resolved for
115    /// encoding conversion -- disk-read budget exhaustion, an unresolvable
116    /// server-supplied path, a line past EOF, or invalid UTF-8 content can
117    /// each cause this -- meaning some `locations` entries' `character` values may be
118    /// wrong for a non-UTF-16 LSP server (#497). Omitted (defaults to
119    /// `false`) when serialized.
120    #[serde(default, skip_serializing_if = "is_false")]
121    pub positions_degraded: bool,
122}
123
124/// Result of a references request.
125#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
126pub struct ReferencesResult {
127    /// Locations of all references.
128    pub locations: Vec<Location>,
129    /// Whether `locations` was capped below the LSP server's full response
130    /// (see `MAX_NORMALIZED_LOCATIONS`, #474) -- if `true`, more references
131    /// exist than are returned here. Omitted (defaults to `false`) when
132    /// serialized.
133    #[serde(default, skip_serializing_if = "is_false")]
134    pub truncated: bool,
135    /// Whether a position in this response could not be resolved for
136    /// encoding conversion -- disk-read budget exhaustion, an unresolvable
137    /// server-supplied path, a line past EOF, or invalid UTF-8 content can
138    /// each cause this -- meaning some `locations` entries' `character` values may be
139    /// wrong for a non-UTF-16 LSP server (#497). Omitted (defaults to
140    /// `false`) when serialized.
141    #[serde(default, skip_serializing_if = "is_false")]
142    pub positions_degraded: bool,
143}
144
145/// Diagnostic severity.
146#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
147#[serde(rename_all = "lowercase")]
148pub enum DiagnosticSeverity {
149    /// Error diagnostic.
150    Error,
151    /// Warning diagnostic.
152    Warning,
153    /// Informational diagnostic.
154    Information,
155    /// Hint diagnostic.
156    Hint,
157}
158
159/// A single diagnostic.
160#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
161pub struct Diagnostic {
162    /// Range where the diagnostic applies.
163    pub range: Range,
164    /// Severity of the diagnostic.
165    pub severity: DiagnosticSeverity,
166    /// Diagnostic message.
167    pub message: String,
168    /// Optional diagnostic code.
169    pub code: Option<String>,
170}
171
172/// Result of a diagnostics request.
173#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
174pub struct DiagnosticsResult {
175    /// List of diagnostics for the document.
176    pub diagnostics: Vec<Diagnostic>,
177    /// Whether a position in this response could not be resolved for
178    /// encoding conversion -- disk-read budget exhaustion, an unresolvable
179    /// server-supplied path, a line past EOF, or invalid UTF-8 content can
180    /// each cause this -- meaning some `diagnostics` ranges' `character` values may be
181    /// wrong for a non-UTF-16 LSP server (#497). Omitted (defaults to
182    /// `false`) when serialized.
183    #[serde(default, skip_serializing_if = "is_false")]
184    pub positions_degraded: bool,
185}
186
187/// A text edit operation.
188#[derive(Debug, Clone, Serialize, Deserialize)]
189pub struct TextEdit {
190    /// Range to replace.
191    pub range: Range,
192    /// New text.
193    pub new_text: String,
194}
195
196/// Changes to a document.
197#[derive(Debug, Clone, Serialize, Deserialize)]
198pub struct DocumentChanges {
199    /// URI of the document.
200    pub uri: String,
201    /// List of edits to apply.
202    pub edits: Vec<TextEdit>,
203}
204
205/// Result of a rename request.
206#[derive(Debug, Clone, Serialize, Deserialize)]
207pub struct RenameResult {
208    /// Changes to apply across documents.
209    pub changes: Vec<DocumentChanges>,
210    /// Entries withheld from `changes` -- see [`DroppedEdits`]. A non-empty
211    /// value means the rename is incomplete even if `changes` is non-empty,
212    /// and callers must not treat this result as the full rename otherwise.
213    #[serde(default, skip_serializing_if = "DroppedEdits::is_empty")]
214    pub dropped: DroppedEdits,
215    /// Whether a position in this response could not be resolved for
216    /// encoding conversion -- disk-read budget exhaustion, an unresolvable
217    /// server-supplied path, a line past EOF, or invalid UTF-8 content can
218    /// each cause this -- meaning some `changes` edit ranges' `character` values may be
219    /// wrong for a non-UTF-16 LSP server (#497). Omitted (defaults to
220    /// `false`) when serialized.
221    #[serde(default, skip_serializing_if = "is_false")]
222    pub positions_degraded: bool,
223}
224
225/// Counts of `WorkspaceEdit` entries withheld during conversion to MCP DTOs,
226/// broken down by reason (#475).
227///
228/// `convert_workspace_edit` silently discarded such entries with only a
229/// `tracing` log line, so a client applying a [`RenameResult`] or
230/// `WorkspaceEditDescription` straight to disk could not tell "nothing to
231/// rename" apart from "some of the rename was withheld" -- this makes that
232/// distinction visible in the result itself.
233#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
234pub struct DroppedEdits {
235    /// Entries referencing a URI outside every configured workspace root.
236    #[serde(default, skip_serializing_if = "is_zero")]
237    pub out_of_workspace: usize,
238    /// `CreateFile`/`RenameFile`/`DeleteFile` document changes, which mcpls
239    /// does not translate into MCP DTOs.
240    #[serde(default, skip_serializing_if = "is_zero")]
241    pub unsupported_file_operation: usize,
242    /// `SnippetTextEdit` entries, which mcpls does not translate since it
243    /// advertises no `snippetEditSupport`.
244    #[serde(default, skip_serializing_if = "is_zero")]
245    pub unsupported_snippet_edit: usize,
246}
247
248impl DroppedEdits {
249    /// Whether no entries were withheld.
250    #[must_use]
251    pub fn is_empty(&self) -> bool {
252        *self == Self::default()
253    }
254}
255
256// Signature required by `#[serde(skip_serializing_if = "is_zero")]` on a `usize` field.
257#[allow(clippy::trivially_copy_pass_by_ref)]
258const fn is_zero(count: &usize) -> bool {
259    *count == 0
260}
261
262/// A completion item.
263#[derive(Debug, Clone, Serialize, Deserialize)]
264pub struct Completion {
265    /// Label of the completion.
266    pub label: String,
267    /// LSP numeric completion-item kind (e.g. 3 for Function).
268    pub kind: Option<u32>,
269    /// Detail information.
270    pub detail: Option<String>,
271    /// Documentation.
272    pub documentation: Option<String>,
273}
274
275/// Result of a completions request.
276#[derive(Debug, Clone, Serialize, Deserialize)]
277pub struct CompletionsResult {
278    /// List of completion items.
279    pub items: Vec<Completion>,
280}
281
282/// A document symbol.
283#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
284pub struct Symbol {
285    /// Name of the symbol.
286    pub name: String,
287    /// LSP numeric symbol kind (e.g. 12 for Function).
288    pub kind: u32,
289    /// Range of the symbol.
290    pub range: Range,
291    /// Selection range (identifier location).
292    pub selection_range: Range,
293    /// Child symbols.
294    #[serde(skip_serializing_if = "Option::is_none")]
295    pub children: Option<Vec<Self>>,
296}
297
298/// Result of a document symbols request.
299#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
300pub struct DocumentSymbolsResult {
301    /// List of symbols in the document.
302    pub symbols: Vec<Symbol>,
303    /// Whether a position in this response could not be resolved for
304    /// encoding conversion -- disk-read budget exhaustion, an unresolvable
305    /// server-supplied path, a line past EOF, or invalid UTF-8 content can
306    /// each cause this -- meaning some `symbols` ranges' `character` values may be
307    /// wrong for a non-UTF-16 LSP server (#497). Omitted (defaults to
308    /// `false`) when serialized.
309    #[serde(default, skip_serializing_if = "is_false")]
310    pub positions_degraded: bool,
311}
312
313/// Result of a format document request.
314#[derive(Debug, Clone, Serialize, Deserialize)]
315pub struct FormatDocumentResult {
316    /// List of edits to format the document.
317    pub edits: Vec<TextEdit>,
318    /// Whether a position in this response could not be resolved for
319    /// encoding conversion -- disk-read budget exhaustion, an unresolvable
320    /// server-supplied path, a line past EOF, or invalid UTF-8 content can
321    /// each cause this -- meaning some `edits` ranges' `character` values may be
322    /// wrong for a non-UTF-16 LSP server (#497). Omitted (defaults to
323    /// `false`) when serialized.
324    #[serde(default, skip_serializing_if = "is_false")]
325    pub positions_degraded: bool,
326}
327
328/// A workspace symbol.
329#[derive(Debug, Clone, Serialize, Deserialize)]
330pub struct WorkspaceSymbol {
331    /// Name of the symbol.
332    pub name: String,
333    /// LSP numeric symbol kind (e.g. 12 for Function).
334    pub kind: u32,
335    /// Location of the symbol.
336    pub location: Location,
337    /// Optional container name (parent scope).
338    #[serde(skip_serializing_if = "Option::is_none")]
339    pub container_name: Option<String>,
340}
341
342/// Result of workspace symbol search.
343#[derive(Debug, Clone, Serialize, Deserialize)]
344pub struct WorkspaceSymbolResult {
345    /// List of symbols found.
346    pub symbols: Vec<WorkspaceSymbol>,
347    /// Whether more symbols matched than are returned in `symbols` -- set
348    /// whenever any are dropped, whether by the caller's own smaller
349    /// `limit` or by the server-side maximum it's clamped to (see
350    /// `MAX_NORMALIZED_LOCATIONS`, #474); this does not distinguish which of
351    /// the two caused it. Omitted (defaults to `false`) when serialized.
352    #[serde(default, skip_serializing_if = "is_false")]
353    pub truncated: bool,
354    /// Whether a position in this response could not be resolved for
355    /// encoding conversion -- disk-read budget exhaustion, an unresolvable
356    /// server-supplied path, a line past EOF, or invalid UTF-8 content can
357    /// each cause this -- meaning some `symbols` locations' `character` values may be
358    /// wrong for a non-UTF-16 LSP server (#497). Omitted (defaults to
359    /// `false`) when serialized.
360    #[serde(default, skip_serializing_if = "is_false")]
361    pub positions_degraded: bool,
362}
363
364/// A single code action.
365#[derive(Debug, Clone, Serialize, Deserialize)]
366pub struct CodeAction {
367    /// Title of the code action.
368    pub title: String,
369    /// Kind of code action (quickfix, refactor, etc.).
370    #[serde(skip_serializing_if = "Option::is_none")]
371    pub kind: Option<String>,
372    /// Diagnostics that this action resolves.
373    #[serde(skip_serializing_if = "Vec::is_empty", default)]
374    pub diagnostics: Vec<Diagnostic>,
375    /// Workspace edit to apply.
376    #[serde(skip_serializing_if = "Option::is_none")]
377    pub edit: Option<WorkspaceEditDescription>,
378    /// Command to execute.
379    #[serde(skip_serializing_if = "Option::is_none")]
380    pub command: Option<CommandDescription>,
381    /// Whether this is the preferred action.
382    #[serde(default)]
383    pub is_preferred: bool,
384}
385
386/// Description of a workspace edit.
387#[derive(Debug, Clone, Serialize, Deserialize)]
388pub struct WorkspaceEditDescription {
389    /// Changes to apply to documents.
390    pub changes: Vec<DocumentChanges>,
391    /// Entries withheld from `changes` -- see [`DroppedEdits`].
392    #[serde(default, skip_serializing_if = "DroppedEdits::is_empty")]
393    pub dropped: DroppedEdits,
394}
395
396/// Description of a command.
397#[derive(Debug, Clone, Serialize, Deserialize)]
398pub struct CommandDescription {
399    /// Title of the command.
400    pub title: String,
401    /// Command identifier.
402    pub command: String,
403    /// Command arguments.
404    #[serde(skip_serializing_if = "Vec::is_empty", default)]
405    pub arguments: Vec<serde_json::Value>,
406}
407
408/// Result of code actions request.
409#[derive(Debug, Clone, Serialize, Deserialize)]
410pub struct CodeActionsResult {
411    /// Available code actions.
412    pub actions: Vec<CodeAction>,
413    /// Whether a position in this response could not be resolved for
414    /// encoding conversion -- disk-read budget exhaustion, an unresolvable
415    /// server-supplied path, a line past EOF, or invalid UTF-8 content can
416    /// each cause this -- meaning some `actions` diagnostic/edit ranges'
417    /// `character` values may be wrong for a non-UTF-16 LSP server (#497).
418    /// Omitted (defaults to `false`) when serialized.
419    #[serde(default, skip_serializing_if = "is_false")]
420    pub positions_degraded: bool,
421}
422
423/// A call hierarchy item.
424#[derive(Debug, Clone, Serialize, Deserialize)]
425pub struct CallHierarchyItemResult {
426    /// Name of the symbol.
427    pub name: String,
428    /// LSP numeric symbol kind (e.g. 12 for Function).
429    pub kind: u32,
430    /// More detail for this item.
431    #[serde(skip_serializing_if = "Option::is_none")]
432    pub detail: Option<String>,
433    /// URI of the document.
434    pub uri: String,
435    /// Range of the symbol.
436    pub range: Range,
437    /// Selection range (identifier location).
438    ///
439    /// Serialized as `selectionRange` (camelCase) so that the value returned by
440    /// `prepare_call_hierarchy` round-trips correctly when the MCP client passes
441    /// it back to `get_incoming_calls` / `get_outgoing_calls`, which deserialize
442    /// it as `lsp_types::CallHierarchyItem` (camelCase).
443    #[serde(rename = "selectionRange")]
444    pub selection_range: Range,
445    /// Opaque data to pass to incoming/outgoing calls.
446    #[serde(skip_serializing_if = "Option::is_none")]
447    pub data: Option<serde_json::Value>,
448    /// Whether this item is not provably inside any configured workspace
449    /// root -- see [`Location::out_of_workspace`] for the exact semantics
450    /// and caveats (advisory only, lexical, symlink-unaware).
451    #[serde(default, skip_serializing_if = "is_false")]
452    pub out_of_workspace: bool,
453}
454
455/// Result of call hierarchy prepare request.
456#[derive(Debug, Clone, Serialize, Deserialize)]
457pub struct CallHierarchyPrepareResult {
458    /// List of callable items at the position.
459    pub items: Vec<CallHierarchyItemResult>,
460    /// Whether a position in this response could not be resolved for
461    /// encoding conversion -- disk-read budget exhaustion, an unresolvable
462    /// server-supplied path, a line past EOF, or invalid UTF-8 content can
463    /// each cause this -- meaning some `items` ranges' `character` values may be
464    /// wrong for a non-UTF-16 LSP server (#497). Omitted (defaults to
465    /// `false`) when serialized.
466    #[serde(default, skip_serializing_if = "is_false")]
467    pub positions_degraded: bool,
468}
469
470/// An incoming call (caller of the current item).
471#[derive(Debug, Clone, Serialize, Deserialize)]
472pub struct IncomingCall {
473    /// The item that calls the current item.
474    pub from: CallHierarchyItemResult,
475    /// Ranges where the call occurs.
476    pub from_ranges: Vec<Range>,
477}
478
479/// Result of incoming calls request.
480#[derive(Debug, Clone, Serialize, Deserialize)]
481pub struct IncomingCallsResult {
482    /// List of incoming calls.
483    pub calls: Vec<IncomingCall>,
484    /// Whether a position in this response could not be resolved for
485    /// encoding conversion -- disk-read budget exhaustion, an unresolvable
486    /// server-supplied path, a line past EOF, or invalid UTF-8 content can
487    /// each cause this -- meaning some `calls` ranges' `character` values may be
488    /// wrong for a non-UTF-16 LSP server (#497). Omitted (defaults to
489    /// `false`) when serialized.
490    #[serde(default, skip_serializing_if = "is_false")]
491    pub positions_degraded: bool,
492}
493
494/// An outgoing call (callee from the current item).
495#[derive(Debug, Clone, Serialize, Deserialize)]
496pub struct OutgoingCall {
497    /// The item being called.
498    pub to: CallHierarchyItemResult,
499    /// Ranges where the call occurs.
500    pub from_ranges: Vec<Range>,
501}
502
503/// Result of outgoing calls request.
504#[derive(Debug, Clone, Serialize, Deserialize)]
505pub struct OutgoingCallsResult {
506    /// List of outgoing calls.
507    pub calls: Vec<OutgoingCall>,
508    /// Whether a position in this response could not be resolved for
509    /// encoding conversion -- disk-read budget exhaustion, an unresolvable
510    /// server-supplied path, a line past EOF, or invalid UTF-8 content can
511    /// each cause this -- meaning some `calls` ranges' `character` values may be
512    /// wrong for a non-UTF-16 LSP server (#497). Omitted (defaults to
513    /// `false`) when serialized.
514    #[serde(default, skip_serializing_if = "is_false")]
515    pub positions_degraded: bool,
516}
517
518/// Result of server logs request.
519#[derive(Debug, Clone, Serialize, Deserialize)]
520pub struct ServerLogsResult {
521    /// List of log entries.
522    pub logs: Vec<crate::bridge::notifications::LogEntry>,
523}
524
525/// Result of server messages request.
526#[derive(Debug, Clone, Serialize, Deserialize)]
527pub struct ServerMessagesResult {
528    /// List of server messages.
529    pub messages: Vec<crate::bridge::notifications::ServerMessage>,
530}
531
532/// A single parameter in a signature.
533#[derive(Debug, Clone, Serialize, Deserialize)]
534pub struct SignatureParameter {
535    /// Label of the parameter.
536    pub label: String,
537    /// Optional documentation for the parameter.
538    #[serde(skip_serializing_if = "Option::is_none")]
539    pub documentation: Option<String>,
540}
541
542/// A single signature overload.
543#[derive(Debug, Clone, Serialize, Deserialize)]
544pub struct SignatureInfo {
545    /// Full label of the signature.
546    pub label: String,
547    /// Optional documentation for the signature.
548    #[serde(skip_serializing_if = "Option::is_none")]
549    pub documentation: Option<String>,
550    /// Parameters of the signature.
551    pub parameters: Vec<SignatureParameter>,
552}
553
554/// Result of a signature help request.
555#[derive(Debug, Clone, Serialize, Deserialize)]
556pub struct SignatureHelpResult {
557    /// Available signatures.
558    pub signatures: Vec<SignatureInfo>,
559    /// Index of the active signature.
560    #[serde(skip_serializing_if = "Option::is_none")]
561    pub active_signature: Option<u32>,
562    /// Index of the active parameter within the active signature.
563    #[serde(skip_serializing_if = "Option::is_none")]
564    pub active_parameter: Option<u32>,
565}
566
567/// Result of a go-to-implementation or go-to-type-definition request.
568#[derive(Debug, Clone, Serialize, Deserialize)]
569pub struct LocationsResult {
570    /// Locations found.
571    pub locations: Vec<Location>,
572    /// Whether `locations` was capped below the LSP server's full response
573    /// (see `MAX_NORMALIZED_LOCATIONS`, #474) -- if `true`, more locations
574    /// exist than are returned here. Omitted (defaults to `false`) when
575    /// serialized.
576    #[serde(default, skip_serializing_if = "is_false")]
577    pub truncated: bool,
578    /// Whether a position in this response could not be resolved for
579    /// encoding conversion -- disk-read budget exhaustion, an unresolvable
580    /// server-supplied path, a line past EOF, or invalid UTF-8 content can
581    /// each cause this -- meaning some `locations` entries' `character` values may be
582    /// wrong for a non-UTF-16 LSP server (#497). Omitted (defaults to
583    /// `false`) when serialized.
584    #[serde(default, skip_serializing_if = "is_false")]
585    pub positions_degraded: bool,
586}
587
588/// A single inlay hint entry.
589#[derive(Debug, Clone, Serialize, Deserialize)]
590pub struct InlayHintEntry {
591    /// Position of the hint (1-based MCP).
592    pub position: Position2D,
593    /// Label text for the hint.
594    pub label: String,
595    /// LSP numeric inlay-hint kind (1 = Type, 2 = Parameter, or a
596    /// server-defined custom value).
597    #[serde(skip_serializing_if = "Option::is_none")]
598    pub kind: Option<u32>,
599    /// Whether to add a space before the hint.
600    #[serde(skip_serializing_if = "Option::is_none")]
601    pub padding_left: Option<bool>,
602    /// Whether to add a space after the hint.
603    #[serde(skip_serializing_if = "Option::is_none")]
604    pub padding_right: Option<bool>,
605    /// Tooltip text.
606    #[serde(skip_serializing_if = "Option::is_none")]
607    pub tooltip: Option<String>,
608}
609
610/// Result of an inlay hints request.
611#[derive(Debug, Clone, Serialize, Deserialize)]
612pub struct InlayHintsResult {
613    /// List of inlay hints.
614    pub hints: Vec<InlayHintEntry>,
615    /// Whether a position in this response could not be resolved for
616    /// encoding conversion -- disk-read budget exhaustion, an unresolvable
617    /// server-supplied path, a line past EOF, or invalid UTF-8 content can
618    /// each cause this -- meaning some `hints` positions' `character` values may be
619    /// wrong for a non-UTF-16 LSP server (#497). Omitted (defaults to
620    /// `false`) when serialized.
621    #[serde(default, skip_serializing_if = "is_false")]
622    pub positions_degraded: bool,
623}
624
625#[cfg(test)]
626#[allow(clippy::unwrap_used, clippy::expect_used)]
627mod tests {
628    use super::lsp_kind_to_u32;
629
630    /// #467 regression: the old `Option<u8>` narrowing silently dropped any
631    /// `InlayHintKind::Custom(n)` with `n > 255` to `None`, indistinguishable
632    /// from "server sent no kind". `lsp_kind_to_u32` must preserve the full
633    /// `u32` value losslessly.
634    #[test]
635    fn test_lsp_kind_to_u32_preserves_custom_values_above_u8_range() {
636        let kind = lsp_types::InlayHintKind::Custom(300);
637        assert_eq!(lsp_kind_to_u32(kind), 300u32);
638    }
639}