mcpls-core 0.6.0

Core library for MCP to LSP protocol translation
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
//! Public MCP-facing result/data-transfer types returned by the tool-call
//! handlers in the sibling domain modules.

use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

/// Convert an LSP integer-valued enum (`SymbolKind`, `CompletionItemKind`,
/// `InlayHintKind`, ...) to its wire-format `u32`.
///
/// Infallible and needs no fallback value, unlike a `serde_json` roundtrip.
pub(super) fn lsp_kind_to_u32<T: Into<u32>>(kind: T) -> u32 {
    kind.into()
}

/// Position in a document (1-based for MCP).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct Position2D {
    /// Line number (1-based).
    pub line: u32,
    /// Character offset (1-based).
    pub character: u32,
}

/// A 1-based MCP position taken as input by `Translator::handle_*` methods.
///
/// Kept distinct from [`Position2D`] (which carries an *output* position back
/// to the caller) so passing a position into a handler always goes through a
/// named-field struct literal (`Position { line, character }`) instead of two
/// adjacent bare `u32` arguments -- a call site that swaps `line` and
/// `character` no longer compiles instead of silently sending a wrong
/// position to the LSP server (#322).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Position {
    /// Line number (1-based).
    pub line: u32,
    /// Character offset (1-based).
    pub character: u32,
}

/// Range in a document (1-based for MCP).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct Range {
    /// Start position.
    pub start: Position2D,
    /// End position.
    pub end: Position2D,
}

/// Location in a document.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct Location {
    /// URI of the document.
    pub uri: String,
    /// Range within the document.
    pub range: Range,
    /// Whether this location is not provably inside any configured
    /// workspace root (e.g. the standard library or a crates.io dependency).
    ///
    /// Advisory only, not a security/safety guarantee: read-only navigation
    /// results are never filtered by workspace containment (see
    /// `bridge::uri_in_workspace_roots`'s docs for why), so callers that
    /// want to apply their own policy toward out-of-workspace locations can
    /// check this flag. The underlying check is purely lexical -- it does
    /// not resolve symlinks -- so a location reached through a symlinked
    /// workspace root (e.g. macOS's `/var` -> `/private/var`, or a package
    /// manager's symlinked dependency store) can read `true` even though it
    /// is genuinely inside the workspace. Also always `true` when no
    /// workspace roots are configured, consistent with
    /// `bridge::uri_in_workspace_roots`'s fail-closed convention: without a
    /// configured root, nothing can be vouched for as inside the workspace.
    /// Omitted (defaults to `false`) when serialized.
    #[serde(default, skip_serializing_if = "is_false")]
    pub out_of_workspace: bool,
}

/// `skip_serializing_if` predicate for a `bool` field that should be omitted
/// from the serialized output when `false`.
///
/// Takes `&bool` rather than `bool` because serde's `skip_serializing_if`
/// always calls the predicate with a field reference.
#[allow(clippy::trivially_copy_pass_by_ref)]
const fn is_false(value: &bool) -> bool {
    !*value
}

/// Result of a hover request.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HoverResult {
    /// Hover contents as markdown string.
    pub contents: String,
    /// Optional range the hover applies to.
    pub range: Option<Range>,
    /// Whether a position in this response could not be resolved for
    /// encoding conversion -- disk-read budget exhaustion, an unresolvable
    /// server-supplied path, a line past EOF, or invalid UTF-8 content can
    /// each cause this -- meaning `range`'s `character` values may be
    /// wrong for a non-UTF-16 LSP server (#497). Omitted (defaults to
    /// `false`) when serialized.
    #[serde(default, skip_serializing_if = "is_false")]
    pub positions_degraded: bool,
}

/// Result of a definition request.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct DefinitionResult {
    /// Locations of the definition.
    pub locations: Vec<Location>,
    /// Whether `locations` was capped below the LSP server's full response
    /// (see `MAX_NORMALIZED_LOCATIONS`, #474) -- if `true`, more locations
    /// exist than are returned here. Omitted (defaults to `false`) when
    /// serialized.
    #[serde(default, skip_serializing_if = "is_false")]
    pub truncated: bool,
    /// Whether a position in this response could not be resolved for
    /// encoding conversion -- disk-read budget exhaustion, an unresolvable
    /// server-supplied path, a line past EOF, or invalid UTF-8 content can
    /// each cause this -- meaning some `locations` entries' `character` values may be
    /// wrong for a non-UTF-16 LSP server (#497). Omitted (defaults to
    /// `false`) when serialized.
    #[serde(default, skip_serializing_if = "is_false")]
    pub positions_degraded: bool,
}

/// Result of a references request.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ReferencesResult {
    /// Locations of all references.
    pub locations: Vec<Location>,
    /// Whether `locations` was capped below the LSP server's full response
    /// (see `MAX_NORMALIZED_LOCATIONS`, #474) -- if `true`, more references
    /// exist than are returned here. Omitted (defaults to `false`) when
    /// serialized.
    #[serde(default, skip_serializing_if = "is_false")]
    pub truncated: bool,
    /// Whether a position in this response could not be resolved for
    /// encoding conversion -- disk-read budget exhaustion, an unresolvable
    /// server-supplied path, a line past EOF, or invalid UTF-8 content can
    /// each cause this -- meaning some `locations` entries' `character` values may be
    /// wrong for a non-UTF-16 LSP server (#497). Omitted (defaults to
    /// `false`) when serialized.
    #[serde(default, skip_serializing_if = "is_false")]
    pub positions_degraded: bool,
}

/// Diagnostic severity.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum DiagnosticSeverity {
    /// Error diagnostic.
    Error,
    /// Warning diagnostic.
    Warning,
    /// Informational diagnostic.
    Information,
    /// Hint diagnostic.
    Hint,
}

/// A single diagnostic.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct Diagnostic {
    /// Range where the diagnostic applies.
    pub range: Range,
    /// Severity of the diagnostic.
    pub severity: DiagnosticSeverity,
    /// Diagnostic message.
    pub message: String,
    /// Optional diagnostic code.
    pub code: Option<String>,
}

/// Result of a diagnostics request.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct DiagnosticsResult {
    /// List of diagnostics for the document.
    pub diagnostics: Vec<Diagnostic>,
    /// Whether a position in this response could not be resolved for
    /// encoding conversion -- disk-read budget exhaustion, an unresolvable
    /// server-supplied path, a line past EOF, or invalid UTF-8 content can
    /// each cause this -- meaning some `diagnostics` ranges' `character` values may be
    /// wrong for a non-UTF-16 LSP server (#497). Omitted (defaults to
    /// `false`) when serialized.
    #[serde(default, skip_serializing_if = "is_false")]
    pub positions_degraded: bool,
}

/// A text edit operation.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TextEdit {
    /// Range to replace.
    pub range: Range,
    /// New text.
    pub new_text: String,
}

/// Changes to a document.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DocumentChanges {
    /// URI of the document.
    pub uri: String,
    /// List of edits to apply.
    pub edits: Vec<TextEdit>,
}

/// Result of a rename request.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RenameResult {
    /// Changes to apply across documents.
    pub changes: Vec<DocumentChanges>,
    /// Entries withheld from `changes` -- see [`DroppedEdits`]. A non-empty
    /// value means the rename is incomplete even if `changes` is non-empty,
    /// and callers must not treat this result as the full rename otherwise.
    #[serde(default, skip_serializing_if = "DroppedEdits::is_empty")]
    pub dropped: DroppedEdits,
    /// Whether a position in this response could not be resolved for
    /// encoding conversion -- disk-read budget exhaustion, an unresolvable
    /// server-supplied path, a line past EOF, or invalid UTF-8 content can
    /// each cause this -- meaning some `changes` edit ranges' `character` values may be
    /// wrong for a non-UTF-16 LSP server (#497). Omitted (defaults to
    /// `false`) when serialized.
    #[serde(default, skip_serializing_if = "is_false")]
    pub positions_degraded: bool,
}

/// Counts of `WorkspaceEdit` entries withheld during conversion to MCP DTOs,
/// broken down by reason (#475).
///
/// `convert_workspace_edit` silently discarded such entries with only a
/// `tracing` log line, so a client applying a [`RenameResult`] or
/// `WorkspaceEditDescription` straight to disk could not tell "nothing to
/// rename" apart from "some of the rename was withheld" -- this makes that
/// distinction visible in the result itself.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct DroppedEdits {
    /// Entries referencing a URI outside every configured workspace root.
    #[serde(default, skip_serializing_if = "is_zero")]
    pub out_of_workspace: usize,
    /// `CreateFile`/`RenameFile`/`DeleteFile` document changes, which mcpls
    /// does not translate into MCP DTOs.
    #[serde(default, skip_serializing_if = "is_zero")]
    pub unsupported_file_operation: usize,
    /// `SnippetTextEdit` entries, which mcpls does not translate since it
    /// advertises no `snippetEditSupport`.
    #[serde(default, skip_serializing_if = "is_zero")]
    pub unsupported_snippet_edit: usize,
}

impl DroppedEdits {
    /// Whether no entries were withheld.
    #[must_use]
    pub fn is_empty(&self) -> bool {
        *self == Self::default()
    }
}

// Signature required by `#[serde(skip_serializing_if = "is_zero")]` on a `usize` field.
#[allow(clippy::trivially_copy_pass_by_ref)]
const fn is_zero(count: &usize) -> bool {
    *count == 0
}

/// A completion item.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Completion {
    /// Label of the completion.
    pub label: String,
    /// LSP numeric completion-item kind (e.g. 3 for Function).
    pub kind: Option<u32>,
    /// Detail information.
    pub detail: Option<String>,
    /// Documentation.
    pub documentation: Option<String>,
}

/// Result of a completions request.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompletionsResult {
    /// List of completion items.
    pub items: Vec<Completion>,
}

/// A document symbol.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct Symbol {
    /// Name of the symbol.
    pub name: String,
    /// LSP numeric symbol kind (e.g. 12 for Function).
    pub kind: u32,
    /// Range of the symbol.
    pub range: Range,
    /// Selection range (identifier location).
    pub selection_range: Range,
    /// Child symbols.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub children: Option<Vec<Self>>,
}

/// Result of a document symbols request.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct DocumentSymbolsResult {
    /// List of symbols in the document.
    pub symbols: Vec<Symbol>,
    /// Whether a position in this response could not be resolved for
    /// encoding conversion -- disk-read budget exhaustion, an unresolvable
    /// server-supplied path, a line past EOF, or invalid UTF-8 content can
    /// each cause this -- meaning some `symbols` ranges' `character` values may be
    /// wrong for a non-UTF-16 LSP server (#497). Omitted (defaults to
    /// `false`) when serialized.
    #[serde(default, skip_serializing_if = "is_false")]
    pub positions_degraded: bool,
}

/// Result of a format document request.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FormatDocumentResult {
    /// List of edits to format the document.
    pub edits: Vec<TextEdit>,
    /// Whether a position in this response could not be resolved for
    /// encoding conversion -- disk-read budget exhaustion, an unresolvable
    /// server-supplied path, a line past EOF, or invalid UTF-8 content can
    /// each cause this -- meaning some `edits` ranges' `character` values may be
    /// wrong for a non-UTF-16 LSP server (#497). Omitted (defaults to
    /// `false`) when serialized.
    #[serde(default, skip_serializing_if = "is_false")]
    pub positions_degraded: bool,
}

/// A workspace symbol.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkspaceSymbol {
    /// Name of the symbol.
    pub name: String,
    /// LSP numeric symbol kind (e.g. 12 for Function).
    pub kind: u32,
    /// Location of the symbol.
    pub location: Location,
    /// Optional container name (parent scope).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub container_name: Option<String>,
}

/// Result of workspace symbol search.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkspaceSymbolResult {
    /// List of symbols found.
    pub symbols: Vec<WorkspaceSymbol>,
    /// Whether more symbols matched than are returned in `symbols` -- set
    /// whenever any are dropped, whether by the caller's own smaller
    /// `limit` or by the server-side maximum it's clamped to (see
    /// `MAX_NORMALIZED_LOCATIONS`, #474); this does not distinguish which of
    /// the two caused it. Omitted (defaults to `false`) when serialized.
    #[serde(default, skip_serializing_if = "is_false")]
    pub truncated: bool,
    /// Whether a position in this response could not be resolved for
    /// encoding conversion -- disk-read budget exhaustion, an unresolvable
    /// server-supplied path, a line past EOF, or invalid UTF-8 content can
    /// each cause this -- meaning some `symbols` locations' `character` values may be
    /// wrong for a non-UTF-16 LSP server (#497). Omitted (defaults to
    /// `false`) when serialized.
    #[serde(default, skip_serializing_if = "is_false")]
    pub positions_degraded: bool,
}

/// A single code action.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CodeAction {
    /// Title of the code action.
    pub title: String,
    /// Kind of code action (quickfix, refactor, etc.).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub kind: Option<String>,
    /// Diagnostics that this action resolves.
    #[serde(skip_serializing_if = "Vec::is_empty", default)]
    pub diagnostics: Vec<Diagnostic>,
    /// Workspace edit to apply.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub edit: Option<WorkspaceEditDescription>,
    /// Command to execute.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub command: Option<CommandDescription>,
    /// Whether this is the preferred action.
    #[serde(default)]
    pub is_preferred: bool,
}

/// Description of a workspace edit.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkspaceEditDescription {
    /// Changes to apply to documents.
    pub changes: Vec<DocumentChanges>,
    /// Entries withheld from `changes` -- see [`DroppedEdits`].
    #[serde(default, skip_serializing_if = "DroppedEdits::is_empty")]
    pub dropped: DroppedEdits,
}

/// Description of a command.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommandDescription {
    /// Title of the command.
    pub title: String,
    /// Command identifier.
    pub command: String,
    /// Command arguments.
    #[serde(skip_serializing_if = "Vec::is_empty", default)]
    pub arguments: Vec<serde_json::Value>,
}

/// Result of code actions request.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CodeActionsResult {
    /// Available code actions.
    pub actions: Vec<CodeAction>,
    /// Whether a position in this response could not be resolved for
    /// encoding conversion -- disk-read budget exhaustion, an unresolvable
    /// server-supplied path, a line past EOF, or invalid UTF-8 content can
    /// each cause this -- meaning some `actions` diagnostic/edit ranges'
    /// `character` values may be wrong for a non-UTF-16 LSP server (#497).
    /// Omitted (defaults to `false`) when serialized.
    #[serde(default, skip_serializing_if = "is_false")]
    pub positions_degraded: bool,
}

/// A call hierarchy item.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CallHierarchyItemResult {
    /// Name of the symbol.
    pub name: String,
    /// LSP numeric symbol kind (e.g. 12 for Function).
    pub kind: u32,
    /// More detail for this item.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub detail: Option<String>,
    /// URI of the document.
    pub uri: String,
    /// Range of the symbol.
    pub range: Range,
    /// Selection range (identifier location).
    ///
    /// Serialized as `selectionRange` (camelCase) so that the value returned by
    /// `prepare_call_hierarchy` round-trips correctly when the MCP client passes
    /// it back to `get_incoming_calls` / `get_outgoing_calls`, which deserialize
    /// it as `lsp_types::CallHierarchyItem` (camelCase).
    #[serde(rename = "selectionRange")]
    pub selection_range: Range,
    /// Opaque data to pass to incoming/outgoing calls.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub data: Option<serde_json::Value>,
    /// Whether this item is not provably inside any configured workspace
    /// root -- see [`Location::out_of_workspace`] for the exact semantics
    /// and caveats (advisory only, lexical, symlink-unaware).
    #[serde(default, skip_serializing_if = "is_false")]
    pub out_of_workspace: bool,
}

/// Result of call hierarchy prepare request.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CallHierarchyPrepareResult {
    /// List of callable items at the position.
    pub items: Vec<CallHierarchyItemResult>,
    /// Whether a position in this response could not be resolved for
    /// encoding conversion -- disk-read budget exhaustion, an unresolvable
    /// server-supplied path, a line past EOF, or invalid UTF-8 content can
    /// each cause this -- meaning some `items` ranges' `character` values may be
    /// wrong for a non-UTF-16 LSP server (#497). Omitted (defaults to
    /// `false`) when serialized.
    #[serde(default, skip_serializing_if = "is_false")]
    pub positions_degraded: bool,
}

/// An incoming call (caller of the current item).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IncomingCall {
    /// The item that calls the current item.
    pub from: CallHierarchyItemResult,
    /// Ranges where the call occurs.
    pub from_ranges: Vec<Range>,
}

/// Result of incoming calls request.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IncomingCallsResult {
    /// List of incoming calls.
    pub calls: Vec<IncomingCall>,
    /// Whether a position in this response could not be resolved for
    /// encoding conversion -- disk-read budget exhaustion, an unresolvable
    /// server-supplied path, a line past EOF, or invalid UTF-8 content can
    /// each cause this -- meaning some `calls` ranges' `character` values may be
    /// wrong for a non-UTF-16 LSP server (#497). Omitted (defaults to
    /// `false`) when serialized.
    #[serde(default, skip_serializing_if = "is_false")]
    pub positions_degraded: bool,
}

/// An outgoing call (callee from the current item).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OutgoingCall {
    /// The item being called.
    pub to: CallHierarchyItemResult,
    /// Ranges where the call occurs.
    pub from_ranges: Vec<Range>,
}

/// Result of outgoing calls request.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OutgoingCallsResult {
    /// List of outgoing calls.
    pub calls: Vec<OutgoingCall>,
    /// Whether a position in this response could not be resolved for
    /// encoding conversion -- disk-read budget exhaustion, an unresolvable
    /// server-supplied path, a line past EOF, or invalid UTF-8 content can
    /// each cause this -- meaning some `calls` ranges' `character` values may be
    /// wrong for a non-UTF-16 LSP server (#497). Omitted (defaults to
    /// `false`) when serialized.
    #[serde(default, skip_serializing_if = "is_false")]
    pub positions_degraded: bool,
}

/// Result of server logs request.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerLogsResult {
    /// List of log entries.
    pub logs: Vec<crate::bridge::notifications::LogEntry>,
}

/// Result of server messages request.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerMessagesResult {
    /// List of server messages.
    pub messages: Vec<crate::bridge::notifications::ServerMessage>,
}

/// A single parameter in a signature.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SignatureParameter {
    /// Label of the parameter.
    pub label: String,
    /// Optional documentation for the parameter.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub documentation: Option<String>,
}

/// A single signature overload.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SignatureInfo {
    /// Full label of the signature.
    pub label: String,
    /// Optional documentation for the signature.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub documentation: Option<String>,
    /// Parameters of the signature.
    pub parameters: Vec<SignatureParameter>,
}

/// Result of a signature help request.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SignatureHelpResult {
    /// Available signatures.
    pub signatures: Vec<SignatureInfo>,
    /// Index of the active signature.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub active_signature: Option<u32>,
    /// Index of the active parameter within the active signature.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub active_parameter: Option<u32>,
}

/// Result of a go-to-implementation or go-to-type-definition request.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LocationsResult {
    /// Locations found.
    pub locations: Vec<Location>,
    /// Whether `locations` was capped below the LSP server's full response
    /// (see `MAX_NORMALIZED_LOCATIONS`, #474) -- if `true`, more locations
    /// exist than are returned here. Omitted (defaults to `false`) when
    /// serialized.
    #[serde(default, skip_serializing_if = "is_false")]
    pub truncated: bool,
    /// Whether a position in this response could not be resolved for
    /// encoding conversion -- disk-read budget exhaustion, an unresolvable
    /// server-supplied path, a line past EOF, or invalid UTF-8 content can
    /// each cause this -- meaning some `locations` entries' `character` values may be
    /// wrong for a non-UTF-16 LSP server (#497). Omitted (defaults to
    /// `false`) when serialized.
    #[serde(default, skip_serializing_if = "is_false")]
    pub positions_degraded: bool,
}

/// A single inlay hint entry.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InlayHintEntry {
    /// Position of the hint (1-based MCP).
    pub position: Position2D,
    /// Label text for the hint.
    pub label: String,
    /// LSP numeric inlay-hint kind (1 = Type, 2 = Parameter, or a
    /// server-defined custom value).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub kind: Option<u32>,
    /// Whether to add a space before the hint.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub padding_left: Option<bool>,
    /// Whether to add a space after the hint.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub padding_right: Option<bool>,
    /// Tooltip text.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tooltip: Option<String>,
}

/// Result of an inlay hints request.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InlayHintsResult {
    /// List of inlay hints.
    pub hints: Vec<InlayHintEntry>,
    /// Whether a position in this response could not be resolved for
    /// encoding conversion -- disk-read budget exhaustion, an unresolvable
    /// server-supplied path, a line past EOF, or invalid UTF-8 content can
    /// each cause this -- meaning some `hints` positions' `character` values may be
    /// wrong for a non-UTF-16 LSP server (#497). Omitted (defaults to
    /// `false`) when serialized.
    #[serde(default, skip_serializing_if = "is_false")]
    pub positions_degraded: bool,
}

#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
    use super::lsp_kind_to_u32;

    /// #467 regression: the old `Option<u8>` narrowing silently dropped any
    /// `InlayHintKind::Custom(n)` with `n > 255` to `None`, indistinguishable
    /// from "server sent no kind". `lsp_kind_to_u32` must preserve the full
    /// `u32` value losslessly.
    #[test]
    fn test_lsp_kind_to_u32_preserves_custom_values_above_u8_range() {
        let kind = lsp_types::InlayHintKind::Custom(300);
        assert_eq!(lsp_kind_to_u32(kind), 300u32);
    }
}