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 std::num::NonZeroU32;
5
6use schemars::JsonSchema;
7use serde::{Deserialize, Serialize};
8use thiserror::Error;
9
10use super::enclosing::{ContextualDiagnostic, ContextualLocation, EnrichmentSummary};
11use super::kind_filter::{self, KindFilter};
12use crate::bridge::notifications::ReportedSeverity;
13use crate::redaction::{Redactions, ServerText};
14
15/// Convert an LSP integer-valued enum (`SymbolKind`, `CompletionItemKind`,
16/// `InlayHintKind`, ...) to its wire-format `u32`.
17///
18/// Infallible and needs no fallback value, unlike a `serde_json` roundtrip.
19pub(super) fn lsp_kind_to_u32<T: Into<u32>>(kind: T) -> u32 {
20    kind.into()
21}
22
23/// Position in a document (1-based for MCP).
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
25pub struct Position2D {
26    /// Line number (1-based).
27    pub line: u32,
28    /// Character offset (1-based).
29    pub character: u32,
30}
31
32/// Largest line or character value a client may supply.
33pub const MAX_POSITION_VALUE: u32 = 1_000_000;
34
35/// Largest line span of a range given to a range-taking tool.
36pub const MAX_RANGE_LINES: u32 = 10_000;
37
38/// Why a client-supplied position was rejected.
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
40pub enum InvalidPosition {
41    /// A line or character below 1; positions are 1-based.
42    #[error("Line and character positions must be >= 1")]
43    ZeroBased,
44    /// A line or character above [`MAX_POSITION_VALUE`].
45    #[error("Position values must be <= {MAX_POSITION_VALUE}")]
46    TooLarge,
47}
48
49/// Longest accepted `rename_symbol` `new_name`, in bytes.
50///
51/// No real identifier approaches this length in any language mcpls targets.
52pub const MAX_NEW_NAME_LENGTH: usize = 1_000;
53
54/// Why [`NewName::try_new`] rejected a name.
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
56pub enum InvalidNewName {
57    /// The name is empty or only whitespace.
58    #[error("new_name must not be blank")]
59    Blank,
60    /// The name is longer than [`MAX_NEW_NAME_LENGTH`].
61    #[error("new_name too long: {len} bytes (max {MAX_NEW_NAME_LENGTH})")]
62    TooLong {
63        /// The name's length in bytes.
64        len: usize,
65    },
66}
67
68/// The name a `rename_symbol` call renames to: non-blank and at most
69/// [`MAX_NEW_NAME_LENGTH`] bytes, so an unusable name is rejected before any
70/// language server is asked.
71///
72/// # Examples
73///
74/// ```
75/// use mcpls_core::bridge::{InvalidNewName, NewName};
76///
77/// assert_eq!(NewName::try_new("renamed")?.as_str(), "renamed");
78/// assert_eq!(NewName::try_new("  "), Err(InvalidNewName::Blank));
79/// # Ok::<(), InvalidNewName>(())
80/// ```
81#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
82#[serde(try_from = "String")]
83pub struct NewName(String);
84
85impl NewName {
86    /// Validates `raw` as given; surrounding whitespace is kept.
87    ///
88    /// # Errors
89    ///
90    /// [`InvalidNewName::Blank`] or [`InvalidNewName::TooLong`].
91    pub fn try_new(raw: impl Into<String>) -> Result<Self, InvalidNewName> {
92        let raw = raw.into();
93        if raw.trim().is_empty() {
94            return Err(InvalidNewName::Blank);
95        }
96        if raw.len() > MAX_NEW_NAME_LENGTH {
97            return Err(InvalidNewName::TooLong { len: raw.len() });
98        }
99        Ok(Self(raw))
100    }
101
102    /// The name text.
103    #[must_use]
104    pub fn as_str(&self) -> &str {
105        &self.0
106    }
107
108    /// The name text, owned.
109    #[must_use]
110    pub fn into_string(self) -> String {
111        self.0
112    }
113}
114
115impl TryFrom<String> for NewName {
116    type Error = InvalidNewName;
117
118    fn try_from(raw: String) -> Result<Self, Self::Error> {
119        Self::try_new(raw)
120    }
121}
122
123/// Longest accepted `get_completions` `trigger`, in bytes.
124///
125/// LSP defines `triggerCharacter` as a single character; 8 bytes covers any
126/// one Unicode codepoint (at most 4 bytes in UTF-8) and short multi-character
127/// triggers such as `->`, while rejecting anything that is not plausibly a
128/// trigger.
129pub const MAX_TRIGGER_CHARACTER_BYTES: usize = 8;
130
131/// A `get_completions` trigger longer than [`MAX_TRIGGER_CHARACTER_BYTES`].
132#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
133#[error("trigger too long: {len} bytes (max {MAX_TRIGGER_CHARACTER_BYTES})")]
134pub struct InvalidCompletionTrigger {
135    /// The trigger's length in bytes.
136    pub len: usize,
137}
138
139/// The text that triggered a completion request, bounded to
140/// [`MAX_TRIGGER_CHARACTER_BYTES`].
141///
142/// # Examples
143///
144/// ```
145/// use mcpls_core::bridge::CompletionTrigger;
146///
147/// assert_eq!(CompletionTrigger::try_new("->")?.as_str(), "->");
148/// assert!(CompletionTrigger::try_new("123456789").is_err());
149/// # Ok::<(), mcpls_core::bridge::InvalidCompletionTrigger>(())
150/// ```
151#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
152#[serde(try_from = "String")]
153pub struct CompletionTrigger(String);
154
155impl CompletionTrigger {
156    /// Validates the length of `raw`.
157    ///
158    /// # Errors
159    ///
160    /// [`InvalidCompletionTrigger`] when `raw` exceeds
161    /// [`MAX_TRIGGER_CHARACTER_BYTES`].
162    pub fn try_new(raw: impl Into<String>) -> Result<Self, InvalidCompletionTrigger> {
163        let raw = raw.into();
164        if raw.len() > MAX_TRIGGER_CHARACTER_BYTES {
165            return Err(InvalidCompletionTrigger { len: raw.len() });
166        }
167        Ok(Self(raw))
168    }
169
170    /// The trigger text.
171    #[must_use]
172    pub fn as_str(&self) -> &str {
173        &self.0
174    }
175
176    /// The trigger text, owned.
177    #[must_use]
178    pub fn into_string(self) -> String {
179        self.0
180    }
181}
182
183impl TryFrom<String> for CompletionTrigger {
184    type Error = InvalidCompletionTrigger;
185
186    fn try_from(raw: String) -> Result<Self, Self::Error> {
187        Self::try_new(raw)
188    }
189}
190
191/// Largest tab size a client may request for formatting.
192pub const MAX_TAB_SIZE: u32 = 32;
193
194/// Tab size used when a formatting request gives none.
195const DEFAULT_TAB_SIZE: u32 = 4;
196
197/// A tab size outside 1 to [`MAX_TAB_SIZE`].
198#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
199#[error("tab_size must be between 1 and {MAX_TAB_SIZE}, got {0}")]
200pub struct InvalidTabSize(pub u32);
201
202/// The tab size a formatting request asks the server for: 1 to
203/// [`MAX_TAB_SIZE`], 4 by default.
204///
205/// Deserializes from a JSON integer and rejects out-of-range values as
206/// invalid params, so an unbounded `u32` never reaches a language server.
207///
208/// # Examples
209///
210/// ```
211/// use mcpls_core::bridge::{InvalidTabSize, MAX_TAB_SIZE, TabSize};
212///
213/// assert_eq!(TabSize::default().get(), 4);
214/// assert_eq!(TabSize::try_from(MAX_TAB_SIZE)?.get(), MAX_TAB_SIZE);
215/// assert_eq!(TabSize::try_from(0), Err(InvalidTabSize(0)));
216/// assert_eq!(TabSize::try_from(MAX_TAB_SIZE + 1), Err(InvalidTabSize(MAX_TAB_SIZE + 1)));
217/// # Ok::<(), InvalidTabSize>(())
218/// ```
219#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
220#[serde(try_from = "u32", into = "u32")]
221pub struct TabSize(NonZeroU32);
222
223impl TabSize {
224    /// The wrapped tab size, within 1 to [`MAX_TAB_SIZE`].
225    #[must_use]
226    pub const fn get(self) -> u32 {
227        self.0.get()
228    }
229}
230
231impl Default for TabSize {
232    fn default() -> Self {
233        const DEFAULT: TabSize = match NonZeroU32::new(DEFAULT_TAB_SIZE) {
234            Some(size) => TabSize(size),
235            None => panic!("the default tab size must be non-zero"),
236        };
237        DEFAULT
238    }
239}
240
241impl TryFrom<u32> for TabSize {
242    type Error = InvalidTabSize;
243
244    fn try_from(value: u32) -> Result<Self, Self::Error> {
245        match NonZeroU32::new(value) {
246            Some(size) if value <= MAX_TAB_SIZE => Ok(Self(size)),
247            _ => Err(InvalidTabSize(value)),
248        }
249    }
250}
251
252impl From<TabSize> for u32 {
253    fn from(size: TabSize) -> Self {
254        size.get()
255    }
256}
257
258impl JsonSchema for TabSize {
259    fn schema_name() -> std::borrow::Cow<'static, str> {
260        "TabSize".into()
261    }
262
263    fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
264        schemars::json_schema!({
265            "type": "integer",
266            "minimum": 1,
267            "maximum": MAX_TAB_SIZE,
268            "default": DEFAULT_TAB_SIZE,
269        })
270    }
271}
272
273/// Why a client-supplied range was rejected.
274#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
275pub enum InvalidRange {
276    /// The start position is invalid.
277    #[error("invalid range start: {0}")]
278    Start(InvalidPosition),
279    /// The end position is invalid.
280    #[error("invalid range end: {0}")]
281    End(InvalidPosition),
282    /// The start lies after the end.
283    #[error("Start position must be before or equal to end position")]
284    Reversed,
285    /// The range spans more than [`MAX_RANGE_LINES`] lines.
286    #[error("Range size must be <= {MAX_RANGE_LINES} lines")]
287    TooManyLines,
288}
289
290/// A 1-based MCP position taken as input by `Translator::handle_*` methods.
291///
292/// Kept distinct from [`Position2D`] (which carries an *output* position back
293/// to the caller) and made of two [`NonZeroU32`]s, so a zero position cannot
294/// exist and a call site that swaps `line` and `character` still names them
295/// (#322). A position typed by a client goes through [`Self::from_client`],
296/// the only fallible constructor; positions derived from server output
297/// convert infallibly through `Position::from_server_output`.
298///
299/// # Examples
300///
301/// ```
302/// use mcpls_core::bridge::{InvalidPosition, Position};
303///
304/// let position = Position::from_client(3, 5)?;
305/// assert_eq!((position.line().get(), position.character().get()), (3, 5));
306/// assert_eq!(Position::from_client(0, 1), Err(InvalidPosition::ZeroBased));
307/// # Ok::<(), InvalidPosition>(())
308/// ```
309#[derive(Debug, Clone, Copy, PartialEq, Eq)]
310pub struct Position {
311    line: NonZeroU32,
312    character: NonZeroU32,
313}
314
315impl Position {
316    /// Builds a position from client-supplied 1-based numbers.
317    ///
318    /// # Errors
319    ///
320    /// Returns [`InvalidPosition::ZeroBased`] for a zero line or character
321    /// and [`InvalidPosition::TooLarge`] above [`MAX_POSITION_VALUE`].
322    pub const fn from_client(line: u32, character: u32) -> Result<Self, InvalidPosition> {
323        let (Some(line), Some(character)) = (NonZeroU32::new(line), NonZeroU32::new(character))
324        else {
325            return Err(InvalidPosition::ZeroBased);
326        };
327        if line.get() > MAX_POSITION_VALUE || character.get() > MAX_POSITION_VALUE {
328            return Err(InvalidPosition::TooLarge);
329        }
330        Ok(Self { line, character })
331    }
332
333    /// Line number (1-based).
334    #[must_use]
335    pub const fn line(self) -> NonZeroU32 {
336        self.line
337    }
338
339    /// Character offset (1-based).
340    #[must_use]
341    pub const fn character(self) -> NonZeroU32 {
342        self.character
343    }
344
345    /// Zero-based LSP line.
346    #[must_use]
347    pub const fn lsp_line(self) -> u32 {
348        self.line.get().saturating_sub(1)
349    }
350
351    /// Zero-based character offset, still in UTF-16 units.
352    #[must_use]
353    pub const fn lsp_character(self) -> u32 {
354        self.character.get().saturating_sub(1)
355    }
356
357    /// Reuses a position taken from server output, which is already 1-based
358    /// MCP form, as a handler input position. Deliberately not a `From`
359    /// impl: a position typed by a client must go through
360    /// [`Self::from_client`]. A zero, which converted LSP output never
361    /// carries, clamps to 1.
362    pub(crate) fn from_server_output(position: &Position2D) -> Self {
363        let clamp = |value: u32| NonZeroU32::new(value).unwrap_or(NonZeroU32::MIN);
364        Self {
365            line: clamp(position.line),
366            character: clamp(position.character),
367        }
368    }
369
370    /// Builds a fixture position, panicking on an invalid one.
371    #[cfg(test)]
372    pub(crate) fn at(line: u32, character: u32) -> Self {
373        Self::from_client(line, character).expect("fixture position is valid")
374    }
375}
376
377/// An ordered pair of positions, `start <= end`.
378///
379/// # Examples
380///
381/// ```
382/// use mcpls_core::bridge::{InvalidRange, Position, PositionRange};
383///
384/// let start = Position::from_client(1, 1).unwrap();
385/// let end = Position::from_client(2, 4).unwrap();
386/// assert!(PositionRange::new(start, end).is_ok());
387/// assert_eq!(PositionRange::new(end, start), Err(InvalidRange::Reversed));
388/// ```
389#[derive(Debug, Clone, Copy, PartialEq, Eq)]
390pub struct PositionRange {
391    start: Position,
392    end: Position,
393}
394
395impl PositionRange {
396    /// Builds an ordered range.
397    ///
398    /// # Errors
399    ///
400    /// Returns [`InvalidRange::Reversed`] when `start` lies after `end`.
401    pub fn new(start: Position, end: Position) -> Result<Self, InvalidRange> {
402        let ordered = (start.line, start.character) <= (end.line, end.character);
403        if ordered {
404            Ok(Self { start, end })
405        } else {
406            Err(InvalidRange::Reversed)
407        }
408    }
409
410    /// Builds a range from client-supplied 1-based numbers.
411    ///
412    /// # Errors
413    ///
414    /// Returns the invalid endpoint or [`InvalidRange::Reversed`].
415    pub fn from_client(
416        (start_line, start_character): (u32, u32),
417        (end_line, end_character): (u32, u32),
418    ) -> Result<Self, InvalidRange> {
419        let start =
420            Position::from_client(start_line, start_character).map_err(InvalidRange::Start)?;
421        let end = Position::from_client(end_line, end_character).map_err(InvalidRange::End)?;
422        Self::new(start, end)
423    }
424
425    /// First position of the range.
426    #[must_use]
427    pub const fn start(self) -> Position {
428        self.start
429    }
430
431    /// Last position of the range.
432    #[must_use]
433    pub const fn end(self) -> Position {
434        self.end
435    }
436}
437
438/// A [`PositionRange`] spanning at most [`MAX_RANGE_LINES`] lines, taken by
439/// the tools whose server-side cost grows with the range.
440#[derive(Debug, Clone, Copy, PartialEq, Eq)]
441pub struct BoundedRange(PositionRange);
442
443impl BoundedRange {
444    /// The underlying ordered range.
445    #[must_use]
446    pub const fn range(self) -> PositionRange {
447        self.0
448    }
449}
450
451impl TryFrom<PositionRange> for BoundedRange {
452    type Error = InvalidRange;
453
454    fn try_from(range: PositionRange) -> Result<Self, Self::Error> {
455        let span = range.end.line.get().saturating_sub(range.start.line.get());
456        if span > MAX_RANGE_LINES {
457            Err(InvalidRange::TooManyLines)
458        } else {
459            Ok(Self(range))
460        }
461    }
462}
463
464/// Range in a document (1-based for MCP).
465#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
466pub struct Range {
467    /// Start position.
468    pub start: Position2D,
469    /// End position.
470    pub end: Position2D,
471}
472
473/// Location in a document.
474#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
475pub struct Location {
476    /// URI of the document.
477    pub uri: String,
478    /// Range within the document.
479    pub range: Range,
480    /// Whether this location is not provably inside any configured
481    /// workspace root (e.g. the standard library or a crates.io dependency).
482    ///
483    /// Advisory only, not a security/safety guarantee: read-only navigation
484    /// results are never filtered by workspace containment (a legitimate
485    /// result routinely points into the standard library or a dependency),
486    /// so callers that want to apply their own policy toward out-of-workspace
487    /// locations can check this flag. The check is purely lexical -- it does
488    /// not resolve symlinks. A root counts together with the spellings it
489    /// was configured or launched under (for example a symlinked path or the
490    /// shell's logical working directory), but a location reached through any
491    /// other symlink (e.g. a package manager's symlinked dependency store)
492    /// can read `true` even though it is genuinely inside the workspace. Also
493    /// always `true` when no workspace roots are configured: without a
494    /// configured root, nothing can be vouched for as inside the workspace.
495    /// Omitted (defaults to `false`) when serialized.
496    #[serde(default, skip_serializing_if = "is_false")]
497    pub out_of_workspace: bool,
498}
499
500/// `skip_serializing_if` predicate for a `bool` field that should be omitted
501/// from the serialized output when `false`.
502///
503/// Takes `&bool` rather than `bool` because serde's `skip_serializing_if`
504/// always calls the predicate with a field reference.
505#[expect(
506    clippy::trivially_copy_pass_by_ref,
507    reason = "serde's `skip_serializing_if` requires `fn(&T) -> bool`"
508)]
509const fn is_false(value: &bool) -> bool {
510    !*value
511}
512
513/// How a non-UTF-16 server's `character` offsets became inexact in a result.
514// Variant order matters: derived `Ord` makes `Request` > `Response`, so `Option::max` keeps the worse.
515#[derive(
516    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
517)]
518#[serde(rename_all = "snake_case")]
519pub enum PositionDegradation {
520    /// Only offsets in the returned data may be inexact. The result still
521    /// describes the symbol that was asked about; keep it and treat the
522    /// returned `character` values as approximate.
523    Response,
524    /// The queried position was sent to the server unconverted, so the result
525    /// may describe a different symbol. Do not trust it.
526    Request,
527}
528
529/// Result of a hover request.
530#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
531pub struct HoverResult {
532    /// Hover contents as markdown string.
533    pub contents: String,
534    /// Optional range the hover applies to.
535    pub range: Option<Range>,
536    /// Set only when some `character` offsets in this result are inexact (non-UTF-16
537    /// servers only); omitted when all are exact. Tells whether the queried position
538    /// or only the returned offsets are affected.
539    #[serde(default, skip_serializing_if = "Option::is_none")]
540    pub positions_degraded: Option<PositionDegradation>,
541}
542
543/// Diagnostic severity.
544pub type DiagnosticSeverity = ReportedSeverity;
545
546/// A single diagnostic.
547#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
548pub struct Diagnostic {
549    /// Range where the diagnostic applies.
550    pub range: Range,
551    /// Severity of the diagnostic.
552    pub severity: DiagnosticSeverity,
553    /// Diagnostic message.
554    pub message: String,
555    /// Optional diagnostic code.
556    pub code: Option<String>,
557}
558
559/// Result of a diagnostics request.
560#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
561pub struct DiagnosticsResult {
562    /// List of diagnostics for the document.
563    pub diagnostics: Vec<Diagnostic>,
564    /// Set only when some `character` offsets in this result are inexact (non-UTF-16
565    /// servers only); omitted when all are exact. Tells whether the queried position
566    /// or only the returned offsets are affected.
567    #[serde(default, skip_serializing_if = "Option::is_none")]
568    #[schemars(with = "PositionDegradation")]
569    pub positions_degraded: Option<PositionDegradation>,
570}
571
572/// Result of a `get_diagnostics` request: [`DiagnosticsResult`] plus the
573/// opt-in enclosing-symbol context.
574#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
575pub struct DocumentDiagnosticsResult {
576    /// List of diagnostics for the document.
577    pub diagnostics: Vec<ContextualDiagnostic>,
578    /// Set only when some `character` offsets in this result are inexact (non-UTF-16
579    /// servers only); omitted when all are exact. Tells whether the queried position
580    /// or only the returned offsets are affected.
581    #[serde(default, skip_serializing_if = "Option::is_none")]
582    #[schemars(with = "PositionDegradation")]
583    pub positions_degraded: Option<PositionDegradation>,
584    /// Set only when `context: "enclosing_symbol"` was requested and the file has
585    /// diagnostics: how many files were enriched or skipped.
586    #[serde(default, skip_serializing_if = "Option::is_none")]
587    #[schemars(with = "EnrichmentSummary")]
588    pub enrichment: Option<EnrichmentSummary>,
589}
590
591/// A text edit operation.
592#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
593pub struct TextEdit {
594    /// Range to replace.
595    pub range: Range,
596    /// New text.
597    pub new_text: String,
598}
599
600/// Changes to a document.
601#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
602pub struct DocumentChanges {
603    /// URI of the document.
604    pub uri: String,
605    /// List of edits to apply.
606    pub edits: Vec<TextEdit>,
607}
608
609/// Result of a rename request.
610#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
611pub struct RenameResult {
612    /// Changes to apply across documents.
613    pub changes: Vec<DocumentChanges>,
614    /// Entries withheld from `changes` -- see [`DroppedEdits`]. A non-empty
615    /// value means the rename is incomplete even if `changes` is non-empty,
616    /// and callers must not treat this result as the full rename otherwise.
617    #[serde(default, skip_serializing_if = "DroppedEdits::is_empty")]
618    pub dropped: DroppedEdits,
619    /// Set only when some `character` offsets in this result are inexact (non-UTF-16
620    /// servers only); omitted when all are exact. Tells whether the queried position
621    /// or only the returned offsets are affected.
622    #[serde(default, skip_serializing_if = "Option::is_none")]
623    pub positions_degraded: Option<PositionDegradation>,
624}
625
626/// Counts of `WorkspaceEdit` entries withheld during conversion to MCP DTOs,
627/// broken down by reason (#475).
628///
629/// `convert_workspace_edit` silently discarded such entries with only a
630/// `tracing` log line, so a client applying a [`RenameResult`] or
631/// `WorkspaceEditDescription` straight to disk could not tell "nothing to
632/// rename" apart from "some of the rename was withheld" -- this makes that
633/// distinction visible in the result itself.
634#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
635pub struct DroppedEdits {
636    /// Entries referencing a URI outside every configured workspace root.
637    #[serde(default, skip_serializing_if = "is_zero")]
638    pub out_of_workspace: usize,
639    /// `CreateFile`/`RenameFile`/`DeleteFile` document changes, which mcpls
640    /// does not translate into MCP DTOs.
641    #[serde(default, skip_serializing_if = "is_zero")]
642    pub unsupported_file_operation: usize,
643    /// `SnippetTextEdit` entries, which mcpls does not translate since it
644    /// advertises no `snippetEditSupport`.
645    #[serde(default, skip_serializing_if = "is_zero")]
646    pub unsupported_snippet_edit: usize,
647    /// Edits not admitted because the response exceeded
648    /// `MAX_NORMALIZED_LOCATIONS` (#487), including whole files skipped once
649    /// the budget was spent.
650    #[serde(default, skip_serializing_if = "is_zero")]
651    pub exceeds_item_cap: usize,
652    /// `documentChanges` text-document edits ignored because a non-empty
653    /// `changes` map took precedence and did not already name their URI
654    /// (#498).
655    #[serde(default, skip_serializing_if = "is_zero")]
656    pub shadowed_by_changes: usize,
657}
658
659impl DroppedEdits {
660    /// Whether no entries were withheld.
661    #[must_use]
662    pub fn is_empty(&self) -> bool {
663        *self == Self::default()
664    }
665}
666
667#[expect(
668    clippy::trivially_copy_pass_by_ref,
669    reason = "serde's `skip_serializing_if` requires `fn(&T) -> bool`"
670)]
671const fn is_zero(count: &usize) -> bool {
672    *count == 0
673}
674
675/// A completion item.
676#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
677pub struct Completion {
678    /// Label of the completion.
679    pub label: String,
680    /// LSP numeric completion-item kind (e.g. 3 for Function).
681    pub kind: Option<u32>,
682    /// Detail information.
683    pub detail: Option<String>,
684    /// Documentation.
685    pub documentation: Option<String>,
686}
687
688/// Result of a completions request.
689#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
690pub struct CompletionsResult {
691    /// List of completion items.
692    pub items: Vec<Completion>,
693    /// Set only when some `character` offsets in this result are inexact (non-UTF-16
694    /// servers only); omitted when all are exact. Tells whether the queried position
695    /// or only the returned offsets are affected.
696    #[serde(default, skip_serializing_if = "Option::is_none")]
697    pub positions_degraded: Option<PositionDegradation>,
698}
699
700/// A document symbol.
701#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
702pub struct Symbol {
703    /// Name of the symbol.
704    pub name: String,
705    /// LSP numeric symbol kind (e.g. 12 for Function).
706    pub kind: u32,
707    /// Range of the symbol.
708    pub range: Range,
709    /// Selection range (identifier location).
710    pub selection_range: Range,
711    /// Child symbols.
712    #[serde(skip_serializing_if = "Option::is_none")]
713    pub children: Option<Vec<Self>>,
714}
715
716/// Result of a document symbols request.
717#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
718pub struct DocumentSymbolsResult {
719    /// List of symbols in the document.
720    pub symbols: Vec<Symbol>,
721    /// Set only when some `character` offsets in this result are inexact (non-UTF-16
722    /// servers only); omitted when all are exact. Tells whether the queried position
723    /// or only the returned offsets are affected.
724    #[serde(default, skip_serializing_if = "Option::is_none")]
725    #[schemars(with = "PositionDegradation")]
726    pub positions_degraded: Option<PositionDegradation>,
727}
728
729/// Result of a format document request.
730#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
731pub struct FormatDocumentResult {
732    /// List of edits to format the document.
733    pub edits: Vec<TextEdit>,
734    /// Set only when some `character` offsets in this result are inexact (non-UTF-16
735    /// servers only); omitted when all are exact. Tells whether the queried position
736    /// or only the returned offsets are affected.
737    #[serde(default, skip_serializing_if = "Option::is_none")]
738    pub positions_degraded: Option<PositionDegradation>,
739}
740
741/// A workspace symbol.
742#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
743pub struct WorkspaceSymbol {
744    /// Name of the symbol.
745    pub name: String,
746    /// LSP numeric symbol kind (e.g. 12 for Function).
747    pub kind: u32,
748    /// Location of the symbol.
749    pub location: Location,
750    /// Optional container name (parent scope).
751    #[serde(skip_serializing_if = "Option::is_none")]
752    pub container_name: Option<String>,
753}
754
755/// Result of workspace symbol search.
756#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
757pub struct WorkspaceSymbolResult {
758    /// List of symbols found.
759    pub symbols: Vec<WorkspaceSymbol>,
760    /// Whether more symbols matched than are returned in `symbols` -- set
761    /// whenever any are dropped, whether by the caller's own smaller
762    /// `limit` or by the server-side maximum it's clamped to (see
763    /// `MAX_NORMALIZED_LOCATIONS`, #474); this does not distinguish which of
764    /// the two caused it. Omitted (defaults to `false`) when serialized.
765    #[serde(default, skip_serializing_if = "is_false")]
766    pub truncated: bool,
767    /// Set only when some `character` offsets in this result are inexact (non-UTF-16
768    /// servers only); omitted when all are exact. Tells whether the queried position
769    /// or only the returned offsets are affected.
770    #[serde(default, skip_serializing_if = "Option::is_none")]
771    pub positions_degraded: Option<PositionDegradation>,
772}
773
774/// A single code action.
775#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
776pub struct CodeAction {
777    /// Title of the code action.
778    pub title: String,
779    /// Kind of code action (quickfix, refactor, etc.).
780    #[serde(skip_serializing_if = "Option::is_none")]
781    pub kind: Option<String>,
782    /// Diagnostics that this action resolves.
783    #[serde(skip_serializing_if = "Vec::is_empty", default)]
784    pub diagnostics: Vec<Diagnostic>,
785    /// Workspace edit to apply.
786    #[serde(skip_serializing_if = "Option::is_none")]
787    pub edit: Option<WorkspaceEditDescription>,
788    /// Command to execute.
789    #[serde(skip_serializing_if = "Option::is_none")]
790    pub command: Option<CommandDescription>,
791    /// Whether this is the preferred action.
792    #[serde(default)]
793    pub is_preferred: bool,
794}
795
796/// Description of a workspace edit.
797#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
798pub struct WorkspaceEditDescription {
799    /// Changes to apply to documents.
800    pub changes: Vec<DocumentChanges>,
801    /// Entries withheld from `changes` -- see [`DroppedEdits`].
802    #[serde(default, skip_serializing_if = "DroppedEdits::is_empty")]
803    pub dropped: DroppedEdits,
804}
805
806/// Description of a command.
807#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
808pub struct CommandDescription {
809    /// Title of the command.
810    pub title: String,
811    /// Command identifier.
812    pub command: String,
813    /// Command arguments.
814    #[serde(skip_serializing_if = "Vec::is_empty", default)]
815    pub arguments: Vec<serde_json::Value>,
816}
817
818/// Result of code actions request.
819#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
820pub struct CodeActionsResult {
821    /// Available code actions.
822    pub actions: Vec<CodeAction>,
823    /// Whether `actions`, their diagnostics, or their edits were capped below
824    /// the LSP server's full response (see `MAX_NORMALIZED_LOCATIONS`, #487);
825    /// a per-action `dropped.exceeds_item_cap` pinpoints dropped edits.
826    /// Omitted (defaults to `false`) when serialized.
827    #[serde(default, skip_serializing_if = "is_false")]
828    pub truncated: bool,
829    /// Set only when some `character` offsets in this result are inexact (non-UTF-16
830    /// servers only); omitted when all are exact. Tells whether the queried position
831    /// or only the returned offsets are affected.
832    #[serde(default, skip_serializing_if = "Option::is_none")]
833    pub positions_degraded: Option<PositionDegradation>,
834}
835
836/// Result of call hierarchy prepare request.
837#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
838pub struct CallHierarchyPrepareResult {
839    /// List of callable items at the position.
840    pub items: Vec<HierarchyItem>,
841    /// Whether `items` was capped below the LSP server's full response (see
842    /// `MAX_NORMALIZED_LOCATIONS`, #516). Omitted (defaults to `false`) when
843    /// serialized.
844    #[serde(default, skip_serializing_if = "is_false")]
845    pub truncated: bool,
846    /// Set only when some `character` offsets in this result are inexact (non-UTF-16
847    /// servers only); omitted when all are exact. Tells whether the queried position
848    /// or only the returned offsets are affected.
849    #[serde(default, skip_serializing_if = "Option::is_none")]
850    pub positions_degraded: Option<PositionDegradation>,
851}
852
853/// An incoming call (caller of the current item).
854#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
855pub struct IncomingCall {
856    /// The item that calls the current item.
857    pub from: HierarchyItem,
858    /// Ranges where the call occurs.
859    pub from_ranges: Vec<Range>,
860}
861
862/// Result of incoming calls request.
863#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
864pub struct IncomingCallsResult {
865    /// List of incoming calls.
866    pub calls: Vec<IncomingCall>,
867    /// Whether `calls` (together with their `from_ranges`) was capped below
868    /// the LSP server's full response (see `MAX_NORMALIZED_LOCATIONS`, #487).
869    /// Omitted (defaults to `false`) when serialized.
870    #[serde(default, skip_serializing_if = "is_false")]
871    pub truncated: bool,
872    /// Set only when some `character` offsets in this result are inexact (non-UTF-16
873    /// servers only); omitted when all are exact. Tells whether the queried position
874    /// or only the returned offsets are affected.
875    #[serde(default, skip_serializing_if = "Option::is_none")]
876    pub positions_degraded: Option<PositionDegradation>,
877}
878
879/// An outgoing call (callee from the current item).
880#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
881pub struct OutgoingCall {
882    /// The item being called.
883    pub to: HierarchyItem,
884    /// Ranges where the call occurs.
885    pub from_ranges: Vec<Range>,
886}
887
888/// Result of outgoing calls request.
889#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
890pub struct OutgoingCallsResult {
891    /// List of outgoing calls.
892    pub calls: Vec<OutgoingCall>,
893    /// Whether `calls` (together with their `from_ranges`) was capped below
894    /// the LSP server's full response (see `MAX_NORMALIZED_LOCATIONS`, #487).
895    /// Omitted (defaults to `false`) when serialized.
896    #[serde(default, skip_serializing_if = "is_false")]
897    pub truncated: bool,
898    /// Set only when some `character` offsets in this result are inexact (non-UTF-16
899    /// servers only); omitted when all are exact. Tells whether the queried position
900    /// or only the returned offsets are affected.
901    #[serde(default, skip_serializing_if = "Option::is_none")]
902    pub positions_degraded: Option<PositionDegradation>,
903}
904
905/// Result of server logs request.
906#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
907pub struct ServerLogsResult {
908    /// List of log entries.
909    pub logs: Vec<crate::bridge::notifications::LogEntry>,
910}
911
912/// Result of server messages request.
913#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
914pub struct ServerMessagesResult {
915    /// List of server messages.
916    pub messages: Vec<crate::bridge::notifications::ServerMessage>,
917}
918
919/// A single parameter in a signature.
920#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
921pub struct SignatureParameter {
922    /// Label of the parameter, or `None` when the server gave an offset pair
923    /// into the signature label that does not resolve to a substring of it
924    /// (out of range, inside a character, or reversed).
925    #[serde(skip_serializing_if = "Option::is_none")]
926    pub label: Option<String>,
927    /// Optional documentation for the parameter.
928    #[serde(skip_serializing_if = "Option::is_none")]
929    pub documentation: Option<String>,
930}
931
932/// A single signature overload.
933#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
934pub struct SignatureInfo {
935    /// Full label of the signature.
936    pub label: String,
937    /// Optional documentation for the signature.
938    #[serde(skip_serializing_if = "Option::is_none")]
939    pub documentation: Option<String>,
940    /// Parameters of the signature.
941    pub parameters: Vec<SignatureParameter>,
942}
943
944/// Result of a signature help request.
945#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
946pub struct SignatureHelpResult {
947    /// Available signatures.
948    pub signatures: Vec<SignatureInfo>,
949    /// Index of the active signature.
950    #[serde(skip_serializing_if = "Option::is_none")]
951    pub active_signature: Option<u32>,
952    /// Index of the active parameter within the active signature.
953    #[serde(skip_serializing_if = "Option::is_none")]
954    pub active_parameter: Option<u32>,
955    /// Set only when some `character` offsets in this result are inexact (non-UTF-16
956    /// servers only); omitted when all are exact. Tells whether the queried position
957    /// or only the returned offsets are affected.
958    #[serde(default, skip_serializing_if = "Option::is_none")]
959    pub positions_degraded: Option<PositionDegradation>,
960}
961
962/// Locations answering a definition, references, implementation,
963/// type-definition or declaration request.
964#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
965pub struct LocationsResult {
966    /// Locations found.
967    pub locations: Vec<ContextualLocation>,
968    /// Whether `locations` was capped below the LSP server's full response
969    /// (see `MAX_NORMALIZED_LOCATIONS`, #474) -- if `true`, more locations
970    /// exist than are returned here. Omitted (defaults to `false`) when
971    /// serialized.
972    #[serde(default, skip_serializing_if = "is_false")]
973    pub truncated: bool,
974    /// Set only when some `character` offsets in this result are inexact (non-UTF-16
975    /// servers only); omitted when all are exact. Tells whether the queried position
976    /// or only the returned offsets are affected.
977    #[serde(default, skip_serializing_if = "Option::is_none")]
978    #[schemars(with = "PositionDegradation")]
979    pub positions_degraded: Option<PositionDegradation>,
980    /// Set only when `context: "enclosing_symbol"` was requested and at least one
981    /// item was looked up: how many files were enriched or skipped, and whether
982    /// the file cap or time budget cut it short.
983    #[serde(default, skip_serializing_if = "Option::is_none")]
984    #[schemars(with = "EnrichmentSummary")]
985    pub enrichment: Option<EnrichmentSummary>,
986}
987
988/// Result of a definition request.
989pub type DefinitionResult = LocationsResult;
990
991/// Result of a references request.
992pub type ReferencesResult = LocationsResult;
993
994/// A single inlay hint entry.
995#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
996pub struct InlayHintEntry {
997    /// Position of the hint (1-based MCP).
998    pub position: Position2D,
999    /// Label text for the hint.
1000    pub label: String,
1001    /// LSP numeric inlay-hint kind (1 = Type, 2 = Parameter, or a
1002    /// server-defined custom value).
1003    #[serde(skip_serializing_if = "Option::is_none")]
1004    pub kind: Option<u32>,
1005    /// Whether to add a space before the hint.
1006    #[serde(skip_serializing_if = "Option::is_none")]
1007    pub padding_left: Option<bool>,
1008    /// Whether to add a space after the hint.
1009    #[serde(skip_serializing_if = "Option::is_none")]
1010    pub padding_right: Option<bool>,
1011    /// Tooltip text.
1012    #[serde(skip_serializing_if = "Option::is_none")]
1013    pub tooltip: Option<String>,
1014}
1015
1016/// Result of an inlay hints request.
1017#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1018pub struct InlayHintsResult {
1019    /// List of inlay hints.
1020    pub hints: Vec<InlayHintEntry>,
1021    /// Whether `hints` was capped below the LSP server's full response (see
1022    /// `MAX_NORMALIZED_LOCATIONS`, #487). Omitted (defaults to `false`) when
1023    /// serialized.
1024    #[serde(default, skip_serializing_if = "is_false")]
1025    pub truncated: bool,
1026    /// Set only when some `character` offsets in this result are inexact (non-UTF-16
1027    /// servers only); omitted when all are exact. Tells whether the queried position
1028    /// or only the returned offsets are affected.
1029    #[serde(default, skip_serializing_if = "Option::is_none")]
1030    pub positions_degraded: Option<PositionDegradation>,
1031}
1032
1033/// A call or type hierarchy item.
1034///
1035/// Returned by the hierarchy tools and accepted back as the typed `item`
1036/// input of the walking tools (`get_incoming_calls`, `get_outgoing_calls`,
1037/// `get_supertypes`, `get_subtypes`). LSP's call and type hierarchy items carry the same fields, so one shape
1038/// serves both. `data` is opaque to the caller and meaningful only to the
1039/// server that produced the item.
1040///
1041/// Lines and columns above [`MAX_POSITION_VALUE`] cannot be passed back, so
1042/// an item from a `prepare_*` tool whose range crosses that column (for
1043/// example on a minified line) is rejected by the walking tools.
1044///
1045/// # Examples
1046///
1047/// ```
1048/// use mcpls_core::bridge::HierarchyItem;
1049///
1050/// let item: HierarchyItem = serde_json::from_value(serde_json::json!({
1051///     "name": "Base", "kind": 5, "uri": "file:///a.cpp",
1052///     "range": {"start": {"line": 1, "character": 1}, "end": {"line": 2, "character": 1}},
1053///     "selectionRange": {"start": {"line": 1, "character": 7}, "end": {"line": 1, "character": 11}},
1054/// }))
1055/// .unwrap();
1056/// assert_eq!(item.name, "Base");
1057/// assert!(serde_json::from_value::<HierarchyItem>(serde_json::json!({})).is_err());
1058/// ```
1059#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1060pub struct HierarchyItem {
1061    /// Name of the symbol.
1062    pub name: String,
1063    /// LSP numeric symbol kind (e.g. 12 for Function, 5 for Class).
1064    pub kind: u32,
1065    /// More detail for this item.
1066    #[serde(default, skip_serializing_if = "Option::is_none")]
1067    pub detail: Option<String>,
1068    /// URI of the document.
1069    pub uri: String,
1070    /// Range of the symbol.
1071    pub range: Range,
1072    /// Selection range (identifier location).
1073    ///
1074    /// Serialized as `selectionRange` (camelCase) so that a returned item
1075    /// round-trips when the MCP client passes it back to a walking tool.
1076    #[serde(rename = "selectionRange")]
1077    pub selection_range: Range,
1078    /// Opaque data to pass back unchanged.
1079    #[serde(default, skip_serializing_if = "Option::is_none")]
1080    pub data: Option<serde_json::Value>,
1081    /// Whether this item is not provably inside any configured workspace
1082    /// root -- see [`Location::out_of_workspace`] for the exact semantics
1083    /// and caveats (advisory only, lexical, symlink-unaware). Ignored on
1084    /// input.
1085    #[serde(default, skip_serializing_if = "is_false")]
1086    pub out_of_workspace: bool,
1087}
1088
1089/// Why a client-supplied [`HierarchyItem`] was rejected.
1090#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
1091pub enum InvalidHierarchyItem {
1092    /// The item's `range` is invalid.
1093    #[error("invalid hierarchy item range: {0}")]
1094    Range(InvalidRange),
1095    /// The item's `selectionRange` is invalid.
1096    #[error("invalid hierarchy item selectionRange: {0}")]
1097    SelectionRange(InvalidRange),
1098}
1099
1100/// A [`HierarchyItem`] whose ranges passed client-input validation.
1101///
1102/// Taken by the walking handlers instead of the raw wire DTO, so a zero,
1103/// oversized or reversed range cannot reach the LSP conversion. Built only by
1104/// [`Self::from_client`].
1105///
1106/// # Examples
1107///
1108/// ```
1109/// use std::assert_matches;
1110///
1111/// use mcpls_core::bridge::{CheckedHierarchyItem, HierarchyItem, InvalidHierarchyItem};
1112///
1113/// let wire = |line: u32| -> HierarchyItem {
1114///     serde_json::from_value(serde_json::json!({
1115///         "name": "Base", "kind": 5, "uri": "file:///a.cpp",
1116///         "range": {"start": {"line": line, "character": 1}, "end": {"line": 2, "character": 1}},
1117///         "selectionRange": {"start": {"line": 1, "character": 7}, "end": {"line": 1, "character": 11}},
1118///     }))
1119///     .unwrap()
1120/// };
1121/// assert!(CheckedHierarchyItem::from_client(wire(1)).is_ok());
1122/// assert_matches!(
1123///     CheckedHierarchyItem::from_client(wire(0)),
1124///     Err(InvalidHierarchyItem::Range(_))
1125/// );
1126/// ```
1127#[derive(Debug, Clone)]
1128pub struct CheckedHierarchyItem {
1129    pub(super) name: String,
1130    pub(super) kind: u32,
1131    pub(super) detail: Option<String>,
1132    pub(super) uri: String,
1133    pub(super) range: PositionRange,
1134    pub(super) selection_range: PositionRange,
1135    pub(super) data: Option<serde_json::Value>,
1136}
1137
1138impl CheckedHierarchyItem {
1139    /// Validates the ranges of a client-supplied item.
1140    ///
1141    /// # Errors
1142    ///
1143    /// Returns which range was zero-based, above [`MAX_POSITION_VALUE`] or
1144    /// reversed. Containment of `selectionRange` in `range` is not checked.
1145    pub fn from_client(item: HierarchyItem) -> Result<Self, InvalidHierarchyItem> {
1146        let range = checked_range(&item.range).map_err(InvalidHierarchyItem::Range)?;
1147        let selection_range =
1148            checked_range(&item.selection_range).map_err(InvalidHierarchyItem::SelectionRange)?;
1149        Ok(Self {
1150            name: item.name,
1151            kind: item.kind,
1152            detail: item.detail,
1153            uri: item.uri,
1154            range,
1155            selection_range,
1156            data: item.data,
1157        })
1158    }
1159
1160    /// The item's document URI, as the client sent it.
1161    #[must_use]
1162    pub fn uri(&self) -> &str {
1163        &self.uri
1164    }
1165}
1166
1167fn checked_range(range: &Range) -> Result<PositionRange, InvalidRange> {
1168    PositionRange::from_client(
1169        (range.start.line, range.start.character),
1170        (range.end.line, range.end.character),
1171    )
1172}
1173
1174/// Result of a type hierarchy prepare, supertypes or subtypes request.
1175#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1176pub struct TypeHierarchyResult {
1177    /// Type hierarchy items at the position, or the supertypes/subtypes of
1178    /// the queried item.
1179    pub items: Vec<HierarchyItem>,
1180    /// Whether `items` was capped below the LSP server's full response (see
1181    /// `MAX_NORMALIZED_LOCATIONS`). Omitted (defaults to `false`) when
1182    /// serialized.
1183    #[serde(default, skip_serializing_if = "is_false")]
1184    pub truncated: bool,
1185    /// Set only when some `character` offsets in this result are inexact (non-UTF-16
1186    /// servers only); omitted when all are exact. Tells whether the queried position
1187    /// or only the returned offsets are affected.
1188    #[serde(default, skip_serializing_if = "Option::is_none")]
1189    #[schemars(with = "PositionDegradation")]
1190    pub positions_degraded: Option<PositionDegradation>,
1191}
1192
1193/// Whether the symbol at a position can be renamed, as answered by
1194/// `textDocument/prepareRename`.
1195///
1196/// # Examples
1197///
1198/// ```
1199/// use mcpls_core::bridge::PrepareRenameOutcome;
1200///
1201/// let outcome = PrepareRenameOutcome::NotRenameable { server_message: None };
1202/// assert_eq!(
1203///     serde_json::to_value(&outcome).unwrap(),
1204///     serde_json::json!({"status": "not_renameable"})
1205/// );
1206/// ```
1207#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1208#[serde(tag = "status", rename_all = "snake_case")]
1209pub enum PrepareRenameOutcome {
1210    /// The identifier can be renamed.
1211    Renameable {
1212        /// Range of the identifier that a rename would replace.
1213        range: Range,
1214        /// Current name of the identifier, when the server supplies it.
1215        #[serde(default, skip_serializing_if = "Option::is_none")]
1216        placeholder: Option<String>,
1217    },
1218    /// The server accepts a rename here but leaves the identifier range to the
1219    /// client's own word-selection rule; no range is invented.
1220    DefaultBehavior,
1221    /// The position cannot be renamed. Distinct from a transport failure and
1222    /// from a server that does not support rename preparation.
1223    NotRenameable {
1224        /// The server's own explanation, when it reported one as an error.
1225        #[serde(default, skip_serializing_if = "Option::is_none")]
1226        server_message: Option<String>,
1227    },
1228}
1229
1230/// Result of a prepare rename request.
1231#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1232pub struct PrepareRenameResult {
1233    /// The rename-preparation verdict.
1234    #[serde(flatten)]
1235    pub outcome: PrepareRenameOutcome,
1236    /// Set only when some `character` offsets in this result are inexact (non-UTF-16
1237    /// servers only); omitted when all are exact. Tells whether the queried position
1238    /// or only the returned offsets are affected.
1239    #[serde(default, skip_serializing_if = "Option::is_none")]
1240    #[schemars(with = "PositionDegradation")]
1241    pub positions_degraded: Option<PositionDegradation>,
1242}
1243
1244/// How a document highlight occurrence uses its symbol.
1245#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1246#[serde(rename_all = "snake_case")]
1247pub enum DocumentHighlightKind {
1248    /// A textual occurrence; also what a server that omits the kind means.
1249    Text,
1250    /// A read access, such as reading a variable.
1251    Read,
1252    /// A write access, such as assigning a variable.
1253    Write,
1254}
1255
1256/// One occurrence of a symbol within a document.
1257#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1258pub struct DocumentHighlightEntry {
1259    /// Range of the occurrence.
1260    pub range: Range,
1261    /// How the occurrence uses the symbol.
1262    pub kind: DocumentHighlightKind,
1263}
1264
1265/// Most ranges one selection chain returns; a longer chain is cut to its
1266/// innermost ranges.
1267pub const MAX_SELECTION_CHAIN: usize = 32;
1268
1269/// Result of a selection range request: the ranges enclosing a position.
1270///
1271/// # Examples
1272///
1273/// ```
1274/// use mcpls_core::bridge::{MAX_SELECTION_CHAIN, SelectionRangesResult};
1275///
1276/// let result: SelectionRangesResult = serde_json::from_value(serde_json::json!({
1277///     "ranges": [],
1278/// }))?;
1279/// assert!(result.ranges.is_empty() && !result.truncated);
1280/// assert_eq!(MAX_SELECTION_CHAIN, 32);
1281/// # Ok::<(), serde_json::Error>(())
1282/// ```
1283#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1284pub struct SelectionRangesResult {
1285    /// Ranges enclosing the position, innermost first, as the server reported
1286    /// them (not merged, reordered or added).
1287    pub ranges: Vec<Range>,
1288    /// Whether the chain was longer than `MAX_SELECTION_CHAIN` and the
1289    /// outermost ranges were dropped. Omitted when `false`.
1290    #[serde(default, skip_serializing_if = "is_false")]
1291    pub truncated: bool,
1292    /// Set only when the queried position or some returned `character`
1293    /// offsets are inexact (non-UTF-16 servers only); omitted when all are
1294    /// exact.
1295    #[serde(default, skip_serializing_if = "Option::is_none")]
1296    #[schemars(with = "PositionDegradation")]
1297    pub positions_degraded: Option<PositionDegradation>,
1298}
1299
1300/// Largest `collapsed_text` returned for a folding region, in bytes before the
1301/// truncation marker.
1302pub const MAX_COLLAPSED_TEXT_BYTES: usize = 256;
1303
1304/// What a folding region covers, from the LSP's open-string kind.
1305#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1306#[serde(rename_all = "snake_case")]
1307pub enum FoldingKind {
1308    /// A comment block.
1309    Comment,
1310    /// An import group.
1311    Imports,
1312    /// A marked region.
1313    Region,
1314    /// No kind, or a server-defined one.
1315    Unspecified,
1316}
1317
1318/// Which folding regions to return.
1319///
1320/// `Unspecified` regions are returned only by [`Self::All`]. Accepted in any
1321/// case, like every other kind filter.
1322#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1323pub enum FoldingKindFilter {
1324    /// Every region.
1325    #[default]
1326    All,
1327    /// Comment blocks only.
1328    Comment,
1329    /// Import groups only.
1330    Imports,
1331    /// Marked regions only.
1332    Region,
1333}
1334
1335/// Schema of the closed string sets, compact on the wire.
1336impl JsonSchema for FoldingKind {
1337    fn schema_name() -> std::borrow::Cow<'static, str> {
1338        "FoldingKind".into()
1339    }
1340
1341    fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
1342        schemars::json_schema!({
1343            "type": "string",
1344            "enum": ["comment", "imports", "region", "unspecified"],
1345        })
1346    }
1347}
1348
1349impl JsonSchema for FoldingKindFilter {
1350    fn schema_name() -> std::borrow::Cow<'static, str> {
1351        Self::SCHEMA_NAME.into()
1352    }
1353
1354    fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
1355        Self::schema()
1356    }
1357}
1358
1359kind_filter::closed_kind_filter!(FoldingKindFilter, "FoldingKindFilter");
1360
1361impl FoldingKindFilter {
1362    /// Every filter, in documentation order.
1363    pub const ALL: [Self; 4] = [Self::All, Self::Comment, Self::Imports, Self::Region];
1364
1365    /// The lowercase spelling the schema documents.
1366    #[must_use]
1367    pub const fn as_str(self) -> &'static str {
1368        match self {
1369            Self::All => "all",
1370            Self::Comment => "comment",
1371            Self::Imports => "imports",
1372            Self::Region => "region",
1373        }
1374    }
1375
1376    /// Whether a region of `kind` passes this filter.
1377    ///
1378    /// # Examples
1379    ///
1380    /// ```
1381    /// use mcpls_core::bridge::{FoldingKind, FoldingKindFilter};
1382    ///
1383    /// assert!(FoldingKindFilter::All.admits(FoldingKind::Unspecified));
1384    /// assert!(FoldingKindFilter::Imports.admits(FoldingKind::Imports));
1385    /// assert!(!FoldingKindFilter::Imports.admits(FoldingKind::Unspecified));
1386    /// ```
1387    #[must_use]
1388    pub const fn admits(self, kind: FoldingKind) -> bool {
1389        matches!(
1390            (self, kind),
1391            (Self::All, _)
1392                | (Self::Comment, FoldingKind::Comment)
1393                | (Self::Imports, FoldingKind::Imports)
1394                | (Self::Region, FoldingKind::Region)
1395        )
1396    }
1397}
1398
1399/// One foldable region of a file, 1-based.
1400#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1401pub struct FoldingRegion {
1402    /// First line.
1403    pub start_line: u32,
1404    /// Last line.
1405    pub end_line: u32,
1406    /// Where the fold starts, when the server says; absent means line end.
1407    #[serde(default, skip_serializing_if = "Option::is_none")]
1408    pub start_character: Option<u32>,
1409    /// Where the fold ends, when the server says; absent means line end.
1410    #[serde(default, skip_serializing_if = "Option::is_none")]
1411    pub end_character: Option<u32>,
1412    /// Region kind.
1413    pub kind: FoldingKind,
1414    /// Text shown in place of the fold, when the server gives it.
1415    #[serde(default, skip_serializing_if = "Option::is_none")]
1416    pub collapsed_text: Option<String>,
1417}
1418
1419/// Result of a folding range request.
1420#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1421pub struct FoldingRangesResult {
1422    /// The regions, by ascending start line then descending end line.
1423    pub regions: Vec<FoldingRegion>,
1424    /// Whether more matching regions exist than are returned. Omitted when
1425    /// `false`.
1426    #[serde(default, skip_serializing_if = "is_false")]
1427    pub truncated: bool,
1428    /// Set only when some `character` offsets in this result are inexact
1429    /// (non-UTF-16 servers only); omitted when all are exact.
1430    #[serde(default, skip_serializing_if = "Option::is_none")]
1431    #[schemars(with = "PositionDegradation")]
1432    pub positions_degraded: Option<PositionDegradation>,
1433}
1434
1435/// Result of a document highlights request.
1436#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1437pub struct DocumentHighlightsResult {
1438    /// Occurrences of the symbol at the position within the file.
1439    pub highlights: Vec<DocumentHighlightEntry>,
1440    /// Whether `highlights` was capped below the LSP server's full response
1441    /// (see `MAX_NORMALIZED_LOCATIONS`). Omitted (defaults to `false`) when
1442    /// serialized.
1443    #[serde(default, skip_serializing_if = "is_false")]
1444    pub truncated: bool,
1445    /// Set only when some `character` offsets in this result are inexact (non-UTF-16
1446    /// servers only); omitted when all are exact. Tells whether the queried position
1447    /// or only the returned offsets are affected.
1448    #[serde(default, skip_serializing_if = "Option::is_none")]
1449    #[schemars(with = "PositionDegradation")]
1450    pub positions_degraded: Option<PositionDegradation>,
1451}
1452
1453impl ServerText for Location {
1454    fn redact_server_text(&mut self, redactions: &Redactions) {
1455        let Self {
1456            uri,
1457            range: _,
1458            out_of_workspace: _,
1459        } = self;
1460        redactions.note_payload(uri);
1461    }
1462}
1463
1464impl ServerText for HoverResult {
1465    fn redact_server_text(&mut self, redactions: &Redactions) {
1466        let Self {
1467            contents,
1468            range: _,
1469            positions_degraded: _,
1470        } = self;
1471        redactions.redact_in_place(contents);
1472    }
1473}
1474
1475impl ServerText for LocationsResult {
1476    fn redact_server_text(&mut self, redactions: &Redactions) {
1477        let Self {
1478            locations,
1479            truncated: _,
1480            positions_degraded: _,
1481            enrichment: _,
1482        } = self;
1483        locations.redact_server_text(redactions);
1484    }
1485}
1486
1487impl ServerText for Diagnostic {
1488    fn redact_server_text(&mut self, redactions: &Redactions) {
1489        let Self {
1490            range: _,
1491            severity: _,
1492            message,
1493            code,
1494        } = self;
1495        redactions.redact_in_place(message);
1496        if let Some(code) = code {
1497            redactions.redact_in_place(code);
1498        }
1499    }
1500}
1501
1502// Document diagnostics are redacted where they enter the cache or the pull path.
1503impl ServerText for DiagnosticsResult {
1504    fn redact_server_text(&mut self, _redactions: &Redactions) {
1505        let Self {
1506            diagnostics: _,
1507            positions_degraded: _,
1508        } = self;
1509    }
1510}
1511
1512impl ServerText for DocumentDiagnosticsResult {
1513    fn redact_server_text(&mut self, _redactions: &Redactions) {
1514        let Self {
1515            diagnostics: _,
1516            positions_degraded: _,
1517            enrichment: _,
1518        } = self;
1519    }
1520}
1521
1522impl ServerText for TextEdit {
1523    fn redact_server_text(&mut self, redactions: &Redactions) {
1524        let Self { range: _, new_text } = self;
1525        redactions.note_payload(new_text);
1526    }
1527}
1528
1529impl ServerText for DocumentChanges {
1530    fn redact_server_text(&mut self, redactions: &Redactions) {
1531        let Self { uri, edits } = self;
1532        redactions.note_payload(uri);
1533        edits.redact_server_text(redactions);
1534    }
1535}
1536
1537impl ServerText for RenameResult {
1538    fn redact_server_text(&mut self, redactions: &Redactions) {
1539        let Self {
1540            changes,
1541            dropped: _,
1542            positions_degraded: _,
1543        } = self;
1544        changes.redact_server_text(redactions);
1545    }
1546}
1547
1548impl ServerText for Completion {
1549    fn redact_server_text(&mut self, redactions: &Redactions) {
1550        let Self {
1551            label,
1552            kind: _,
1553            detail,
1554            documentation,
1555        } = self;
1556        redactions.note_payload(label);
1557        for prose in [detail, documentation].into_iter().flatten() {
1558            redactions.redact_in_place(prose);
1559        }
1560    }
1561}
1562
1563impl ServerText for CompletionsResult {
1564    fn redact_server_text(&mut self, redactions: &Redactions) {
1565        let Self {
1566            items,
1567            positions_degraded: _,
1568        } = self;
1569        items.redact_server_text(redactions);
1570    }
1571}
1572
1573impl ServerText for Symbol {
1574    fn redact_server_text(&mut self, redactions: &Redactions) {
1575        let Self {
1576            name,
1577            kind: _,
1578            range: _,
1579            selection_range: _,
1580            children,
1581        } = self;
1582        redactions.note_payload(name);
1583        if let Some(children) = children {
1584            children.redact_server_text(redactions);
1585        }
1586    }
1587}
1588
1589impl ServerText for DocumentSymbolsResult {
1590    fn redact_server_text(&mut self, redactions: &Redactions) {
1591        let Self {
1592            symbols,
1593            positions_degraded: _,
1594        } = self;
1595        symbols.redact_server_text(redactions);
1596    }
1597}
1598
1599impl ServerText for FormatDocumentResult {
1600    fn redact_server_text(&mut self, redactions: &Redactions) {
1601        let Self {
1602            edits,
1603            positions_degraded: _,
1604        } = self;
1605        edits.redact_server_text(redactions);
1606    }
1607}
1608
1609impl ServerText for WorkspaceSymbol {
1610    fn redact_server_text(&mut self, redactions: &Redactions) {
1611        let Self {
1612            name,
1613            kind: _,
1614            location,
1615            container_name,
1616        } = self;
1617        redactions.note_payload(name);
1618        if let Some(container) = container_name {
1619            redactions.note_payload(container);
1620        }
1621        location.redact_server_text(redactions);
1622    }
1623}
1624
1625impl ServerText for WorkspaceSymbolResult {
1626    fn redact_server_text(&mut self, redactions: &Redactions) {
1627        let Self {
1628            symbols,
1629            truncated: _,
1630            positions_degraded: _,
1631        } = self;
1632        symbols.redact_server_text(redactions);
1633    }
1634}
1635
1636impl ServerText for CodeAction {
1637    fn redact_server_text(&mut self, redactions: &Redactions) {
1638        let Self {
1639            title,
1640            kind: _,
1641            diagnostics,
1642            edit,
1643            command,
1644            is_preferred: _,
1645        } = self;
1646        redactions.redact_in_place(title);
1647        diagnostics.redact_server_text(redactions);
1648        edit.redact_server_text(redactions);
1649        command.redact_server_text(redactions);
1650    }
1651}
1652
1653impl ServerText for WorkspaceEditDescription {
1654    fn redact_server_text(&mut self, redactions: &Redactions) {
1655        let Self {
1656            changes,
1657            dropped: _,
1658        } = self;
1659        changes.redact_server_text(redactions);
1660    }
1661}
1662
1663impl ServerText for CommandDescription {
1664    fn redact_server_text(&mut self, redactions: &Redactions) {
1665        let Self {
1666            title,
1667            command,
1668            arguments,
1669        } = self;
1670        redactions.redact_in_place(title);
1671        redactions.note_payload(command);
1672        for argument in arguments {
1673            redactions.note_payload_json(argument);
1674        }
1675    }
1676}
1677
1678impl ServerText for CodeActionsResult {
1679    fn redact_server_text(&mut self, redactions: &Redactions) {
1680        let Self {
1681            actions,
1682            truncated: _,
1683            positions_degraded: _,
1684        } = self;
1685        actions.redact_server_text(redactions);
1686    }
1687}
1688
1689impl ServerText for CallHierarchyPrepareResult {
1690    fn redact_server_text(&mut self, redactions: &Redactions) {
1691        let Self {
1692            items,
1693            truncated: _,
1694            positions_degraded: _,
1695        } = self;
1696        items.redact_server_text(redactions);
1697    }
1698}
1699
1700impl ServerText for IncomingCall {
1701    fn redact_server_text(&mut self, redactions: &Redactions) {
1702        let Self {
1703            from,
1704            from_ranges: _,
1705        } = self;
1706        from.redact_server_text(redactions);
1707    }
1708}
1709
1710impl ServerText for IncomingCallsResult {
1711    fn redact_server_text(&mut self, redactions: &Redactions) {
1712        let Self {
1713            calls,
1714            truncated: _,
1715            positions_degraded: _,
1716        } = self;
1717        calls.redact_server_text(redactions);
1718    }
1719}
1720
1721impl ServerText for OutgoingCall {
1722    fn redact_server_text(&mut self, redactions: &Redactions) {
1723        let Self { to, from_ranges: _ } = self;
1724        to.redact_server_text(redactions);
1725    }
1726}
1727
1728impl ServerText for OutgoingCallsResult {
1729    fn redact_server_text(&mut self, redactions: &Redactions) {
1730        let Self {
1731            calls,
1732            truncated: _,
1733            positions_degraded: _,
1734        } = self;
1735        calls.redact_server_text(redactions);
1736    }
1737}
1738
1739// Logs and messages are redacted when the notification enters the cache.
1740impl ServerText for ServerLogsResult {
1741    fn redact_server_text(&mut self, _redactions: &Redactions) {
1742        let Self { logs: _ } = self;
1743    }
1744}
1745
1746impl ServerText for ServerMessagesResult {
1747    fn redact_server_text(&mut self, _redactions: &Redactions) {
1748        let Self { messages: _ } = self;
1749    }
1750}
1751
1752impl ServerText for SignatureParameter {
1753    fn redact_server_text(&mut self, redactions: &Redactions) {
1754        let Self {
1755            label,
1756            documentation,
1757        } = self;
1758        for prose in [label, documentation].into_iter().flatten() {
1759            redactions.redact_in_place(prose);
1760        }
1761    }
1762}
1763
1764impl ServerText for SignatureInfo {
1765    fn redact_server_text(&mut self, redactions: &Redactions) {
1766        let Self {
1767            label,
1768            documentation,
1769            parameters,
1770        } = self;
1771        redactions.redact_in_place(label);
1772        if let Some(documentation) = documentation {
1773            redactions.redact_in_place(documentation);
1774        }
1775        parameters.redact_server_text(redactions);
1776    }
1777}
1778
1779impl ServerText for SignatureHelpResult {
1780    fn redact_server_text(&mut self, redactions: &Redactions) {
1781        let Self {
1782            signatures,
1783            active_signature: _,
1784            active_parameter: _,
1785            positions_degraded: _,
1786        } = self;
1787        signatures.redact_server_text(redactions);
1788    }
1789}
1790
1791impl ServerText for InlayHintEntry {
1792    fn redact_server_text(&mut self, redactions: &Redactions) {
1793        let Self {
1794            position: _,
1795            label,
1796            kind: _,
1797            padding_left: _,
1798            padding_right: _,
1799            tooltip,
1800        } = self;
1801        redactions.redact_in_place(label);
1802        if let Some(tooltip) = tooltip {
1803            redactions.redact_in_place(tooltip);
1804        }
1805    }
1806}
1807
1808impl ServerText for InlayHintsResult {
1809    fn redact_server_text(&mut self, redactions: &Redactions) {
1810        let Self {
1811            hints,
1812            truncated: _,
1813            positions_degraded: _,
1814        } = self;
1815        hints.redact_server_text(redactions);
1816    }
1817}
1818
1819impl ServerText for HierarchyItem {
1820    fn redact_server_text(&mut self, redactions: &Redactions) {
1821        let Self {
1822            name,
1823            kind: _,
1824            detail,
1825            uri,
1826            range: _,
1827            selection_range: _,
1828            data,
1829            out_of_workspace: _,
1830        } = self;
1831        redactions.note_payload(name);
1832        if let Some(detail) = detail {
1833            redactions.note_payload(detail);
1834        }
1835        redactions.note_payload(uri);
1836        if let Some(data) = data {
1837            redactions.note_payload_json(data);
1838        }
1839    }
1840}
1841
1842impl ServerText for TypeHierarchyResult {
1843    fn redact_server_text(&mut self, redactions: &Redactions) {
1844        let Self {
1845            items,
1846            truncated: _,
1847            positions_degraded: _,
1848        } = self;
1849        items.redact_server_text(redactions);
1850    }
1851}
1852
1853impl ServerText for PrepareRenameOutcome {
1854    fn redact_server_text(&mut self, redactions: &Redactions) {
1855        match self {
1856            Self::Renameable {
1857                range: _,
1858                placeholder,
1859            } => {
1860                if let Some(placeholder) = placeholder {
1861                    redactions.note_payload(placeholder);
1862                }
1863            }
1864            Self::DefaultBehavior => {}
1865            Self::NotRenameable { server_message } => {
1866                if let Some(message) = server_message {
1867                    redactions.redact_in_place(message);
1868                }
1869            }
1870        }
1871    }
1872}
1873
1874impl ServerText for PrepareRenameResult {
1875    fn redact_server_text(&mut self, redactions: &Redactions) {
1876        let Self {
1877            outcome,
1878            positions_degraded: _,
1879        } = self;
1880        outcome.redact_server_text(redactions);
1881    }
1882}
1883
1884impl ServerText for DocumentHighlightsResult {
1885    fn redact_server_text(&mut self, _redactions: &Redactions) {
1886        let Self {
1887            highlights: _,
1888            truncated: _,
1889            positions_degraded: _,
1890        } = self;
1891    }
1892}
1893
1894impl ServerText for FoldingRangesResult {
1895    fn redact_server_text(&mut self, redactions: &Redactions) {
1896        let Self {
1897            regions,
1898            truncated: _,
1899            positions_degraded: _,
1900        } = self;
1901        for region in regions {
1902            if let Some(text) = &mut region.collapsed_text {
1903                redactions.redact_in_place(text);
1904                *text =
1905                    crate::util::truncate_string(std::mem::take(text), MAX_COLLAPSED_TEXT_BYTES);
1906            }
1907        }
1908    }
1909}
1910
1911impl ServerText for SelectionRangesResult {
1912    fn redact_server_text(&mut self, _redactions: &Redactions) {
1913        let Self {
1914            ranges: _,
1915            truncated: _,
1916            positions_degraded: _,
1917        } = self;
1918    }
1919}
1920
1921#[cfg(test)]
1922mod tests {
1923    use std::assert_matches;
1924
1925    fn hierarchy_wire(range: [u32; 4], selection: [u32; 4]) -> HierarchyItem {
1926        let at = |line, character| serde_json::json!({"line": line, "character": character});
1927        serde_json::from_value(serde_json::json!({
1928            "name": "x", "kind": 5, "uri": "file:///a.rs",
1929            "range": {"start": at(range[0], range[1]), "end": at(range[2], range[3])},
1930            "selectionRange": {"start": at(selection[0], selection[1]), "end": at(selection[2], selection[3])},
1931        }))
1932        .unwrap()
1933    }
1934
1935    #[test]
1936    fn checked_hierarchy_item_accepts_valid_ranges() {
1937        let item = CheckedHierarchyItem::from_client(hierarchy_wire([1, 1, 2, 1], [1, 7, 1, 9]));
1938        assert_eq!(item.unwrap().uri(), "file:///a.rs");
1939    }
1940
1941    #[test]
1942    fn checked_hierarchy_item_rejects_bad_range_and_names_it() {
1943        for bad in [
1944            [0, 1, 1, 1],
1945            [1, 0, 1, 1],
1946            [1, 1, 1, 1_000_001],
1947            [1, 1, u32::MAX, 1],
1948            [2, 1, 1, 1],
1949        ] {
1950            let err =
1951                CheckedHierarchyItem::from_client(hierarchy_wire(bad, [1, 1, 1, 1])).unwrap_err();
1952            assert_matches!(err, InvalidHierarchyItem::Range(_), "{bad:?}");
1953            let err =
1954                CheckedHierarchyItem::from_client(hierarchy_wire([1, 1, 9, 1], bad)).unwrap_err();
1955            assert_matches!(err, InvalidHierarchyItem::SelectionRange(_), "{bad:?}");
1956        }
1957    }
1958
1959    #[test]
1960    fn checked_hierarchy_item_rejects_selection_column_above_max() {
1961        let err = CheckedHierarchyItem::from_client(hierarchy_wire(
1962            [1, 1, 2, 1],
1963            [1, 1_000_001, 1, 1_000_001],
1964        ))
1965        .unwrap_err();
1966        assert_eq!(
1967            err,
1968            InvalidHierarchyItem::SelectionRange(InvalidRange::Start(InvalidPosition::TooLarge))
1969        );
1970    }
1971
1972    use super::*;
1973
1974    const SECRET: &str = "SuperSecretValue123";
1975
1976    fn range() -> Range {
1977        Range {
1978            start: Position2D {
1979                line: 1,
1980                character: 1,
1981            },
1982            end: Position2D {
1983                line: 1,
1984                character: 2,
1985            },
1986        }
1987    }
1988
1989    #[test]
1990    fn test_server_text_redacts_prose_and_leaves_edit_payload() {
1991        let set = Redactions::new([("API_TOKEN".to_owned(), SECRET.to_owned())]);
1992        let mut actions = vec![CodeAction {
1993            title: format!("fix {SECRET}"),
1994            kind: None,
1995            diagnostics: vec![Diagnostic {
1996                range: range(),
1997                severity: DiagnosticSeverity::Error,
1998                message: format!("bad {SECRET}"),
1999                code: Some(format!("E-{SECRET}")),
2000            }],
2001            edit: Some(WorkspaceEditDescription {
2002                changes: vec![DocumentChanges {
2003                    uri: format!("file:///{SECRET}.rs"),
2004                    edits: vec![TextEdit {
2005                        range: range(),
2006                        new_text: format!("let k = \"{SECRET}\";"),
2007                    }],
2008                }],
2009                dropped: DroppedEdits::default(),
2010            }),
2011            command: None,
2012            is_preferred: false,
2013        }];
2014
2015        actions.redact_server_text(&set);
2016
2017        let action = &actions[0];
2018        assert_eq!(action.title, "fix [redacted:API_TOKEN]");
2019        assert_eq!(action.diagnostics[0].message, "bad [redacted:API_TOKEN]");
2020        assert_eq!(
2021            action.diagnostics[0].code.as_deref(),
2022            Some("E-[redacted:API_TOKEN]")
2023        );
2024        let changes = &action.edit.as_ref().unwrap().changes[0];
2025        assert!(changes.uri.contains(SECRET));
2026        assert!(changes.edits[0].new_text.contains(SECRET));
2027    }
2028
2029    #[test]
2030    fn test_server_text_completion_keeps_label_and_redacts_detail_and_documentation() {
2031        let set = Redactions::new([("API_TOKEN".to_owned(), SECRET.to_owned())]);
2032        let mut completion = Completion {
2033            label: format!("use_{SECRET}"),
2034            kind: Some(3),
2035            detail: Some(format!("fn {SECRET}()")),
2036            documentation: Some(format!("docs {SECRET}")),
2037        };
2038
2039        completion.redact_server_text(&set);
2040
2041        assert_eq!(completion.label, format!("use_{SECRET}"));
2042        assert_eq!(
2043            completion.detail.as_deref(),
2044            Some("fn [redacted:API_TOKEN]()")
2045        );
2046        assert_eq!(
2047            completion.documentation.as_deref(),
2048            Some("docs [redacted:API_TOKEN]")
2049        );
2050    }
2051
2052    #[test]
2053    fn test_server_text_redacts_signature_label_documentation_and_parameters() {
2054        let set = Redactions::new([("API_TOKEN".to_owned(), SECRET.to_owned())]);
2055        let mut result = SignatureHelpResult {
2056            signatures: vec![SignatureInfo {
2057                label: format!("f({SECRET})"),
2058                documentation: Some(format!("doc {SECRET}")),
2059                parameters: vec![SignatureParameter {
2060                    label: Some(format!("p {SECRET}")),
2061                    documentation: Some(format!("pd {SECRET}")),
2062                }],
2063            }],
2064            active_signature: Some(0),
2065            active_parameter: Some(0),
2066            positions_degraded: None,
2067        };
2068
2069        result.redact_server_text(&set);
2070
2071        let json = serde_json::to_string(&result).unwrap();
2072        assert!(!json.contains(SECRET), "{json}");
2073        assert_eq!(json.matches("[redacted:API_TOKEN]").count(), 4, "{json}");
2074    }
2075
2076    #[test]
2077    fn test_server_text_redacts_inlay_label_and_tooltip() {
2078        let set = Redactions::new([("API_TOKEN".to_owned(), SECRET.to_owned())]);
2079        let mut hint = InlayHintEntry {
2080            position: Position2D {
2081                line: 1,
2082                character: 1,
2083            },
2084            label: format!(": {SECRET}"),
2085            kind: None,
2086            padding_left: None,
2087            padding_right: None,
2088            tooltip: Some(format!("tip {SECRET}")),
2089        };
2090
2091        hint.redact_server_text(&set);
2092
2093        assert_eq!(hint.label, ": [redacted:API_TOKEN]");
2094        assert_eq!(hint.tooltip.as_deref(), Some("tip [redacted:API_TOKEN]"));
2095    }
2096
2097    #[test]
2098    fn test_server_text_command_redacts_title_and_keeps_command_and_arguments() {
2099        let set = Redactions::new([("API_TOKEN".to_owned(), SECRET.to_owned())]);
2100        let mut command = CommandDescription {
2101            title: format!("run {SECRET}"),
2102            command: format!("cmd.{SECRET}"),
2103            arguments: vec![serde_json::json!({ "key": SECRET })],
2104        };
2105
2106        command.redact_server_text(&set);
2107
2108        assert_eq!(command.title, "run [redacted:API_TOKEN]");
2109        assert_eq!(command.command, format!("cmd.{SECRET}"));
2110        assert_eq!(
2111            command.arguments,
2112            vec![serde_json::json!({ "key": SECRET })]
2113        );
2114    }
2115
2116    #[test]
2117    fn test_server_text_leaves_symbol_and_hierarchy_identifiers() {
2118        let set = Redactions::new([("API_TOKEN".to_owned(), SECRET.to_owned())]);
2119        let mut symbols = DocumentSymbolsResult {
2120            symbols: vec![Symbol {
2121                name: format!("sym_{SECRET}"),
2122                kind: 12,
2123                range: range(),
2124                selection_range: range(),
2125                children: Some(vec![Symbol {
2126                    name: format!("child_{SECRET}"),
2127                    kind: 12,
2128                    range: range(),
2129                    selection_range: range(),
2130                    children: None,
2131                }]),
2132            }],
2133            positions_degraded: None,
2134        };
2135        let mut item = HierarchyItem {
2136            name: format!("call_{SECRET}"),
2137            kind: 12,
2138            detail: Some(format!("detail {SECRET}")),
2139            uri: format!("file:///{SECRET}.rs"),
2140            range: range(),
2141            selection_range: range(),
2142            data: Some(serde_json::json!({ "id": SECRET })),
2143            out_of_workspace: false,
2144        };
2145        let mut type_item = HierarchyItem {
2146            name: format!("type_{SECRET}"),
2147            kind: 5,
2148            detail: None,
2149            uri: format!("file:///{SECRET}.rs"),
2150            range: range(),
2151            selection_range: range(),
2152            data: None,
2153            out_of_workspace: false,
2154        };
2155        let before = (
2156            serde_json::to_string(&symbols).unwrap(),
2157            serde_json::to_string(&item).unwrap(),
2158            serde_json::to_string(&type_item).unwrap(),
2159        );
2160
2161        symbols.redact_server_text(&set);
2162        item.redact_server_text(&set);
2163        type_item.redact_server_text(&set);
2164
2165        let after = (
2166            serde_json::to_string(&symbols).unwrap(),
2167            serde_json::to_string(&item).unwrap(),
2168            serde_json::to_string(&type_item).unwrap(),
2169        );
2170        assert_eq!(before, after);
2171    }
2172
2173    #[test]
2174    fn test_server_text_with_no_secrets_rewrites_nothing() {
2175        let mut hover = HoverResult {
2176            contents: format!("doc {SECRET}"),
2177            range: None,
2178            positions_degraded: None,
2179        };
2180
2181        hover.redact_server_text(&Redactions::default());
2182
2183        assert_eq!(hover.contents, format!("doc {SECRET}"));
2184    }
2185
2186    #[test]
2187    fn test_server_text_does_not_touch_already_redacted_diagnostics() {
2188        let set = Redactions::new([("API_TOKEN".to_owned(), SECRET.to_owned())]);
2189        let mut result = DocumentDiagnosticsResult {
2190            diagnostics: vec![ContextualDiagnostic::from(Diagnostic {
2191                range: range(),
2192                severity: DiagnosticSeverity::Error,
2193                message: format!("echo {SECRET}"),
2194                code: None,
2195            })],
2196            positions_degraded: None,
2197            enrichment: None,
2198        };
2199
2200        result.redact_server_text(&set);
2201
2202        assert_eq!(result.diagnostics[0].message, format!("echo {SECRET}"));
2203    }
2204
2205    #[test]
2206    fn test_server_text_redacts_hover_and_prepare_rename_message() {
2207        let set = Redactions::new([("API_TOKEN".to_owned(), SECRET.to_owned())]);
2208        let mut hover = HoverResult {
2209            contents: format!("doc {SECRET}"),
2210            range: None,
2211            positions_degraded: None,
2212        };
2213        let mut outcome = PrepareRenameOutcome::NotRenameable {
2214            server_message: Some(format!("no {SECRET}")),
2215        };
2216
2217        hover.redact_server_text(&set);
2218        outcome.redact_server_text(&set);
2219
2220        assert_eq!(hover.contents, "doc [redacted:API_TOKEN]");
2221        assert_eq!(
2222            outcome,
2223            PrepareRenameOutcome::NotRenameable {
2224                server_message: Some("no [redacted:API_TOKEN]".to_owned())
2225            }
2226        );
2227    }
2228
2229    #[test]
2230    fn test_position_degradation_request_outranks_response() {
2231        assert!(PositionDegradation::Request > PositionDegradation::Response);
2232        assert_eq!(
2233            Some(PositionDegradation::Response).max(Some(PositionDegradation::Request)),
2234            Some(PositionDegradation::Request)
2235        );
2236        assert_eq!(
2237            None.max(Some(PositionDegradation::Response)),
2238            Some(PositionDegradation::Response)
2239        );
2240    }
2241
2242    #[test]
2243    fn test_position_degradation_serializes_as_snake_case_string() {
2244        assert_eq!(
2245            serde_json::to_value(PositionDegradation::Request).unwrap(),
2246            "request"
2247        );
2248        assert_eq!(
2249            serde_json::to_value(PositionDegradation::Response).unwrap(),
2250            "response"
2251        );
2252    }
2253
2254    #[test]
2255    fn test_positions_degraded_key_omitted_when_none() {
2256        let result = super::DocumentSymbolsResult {
2257            symbols: Vec::new(),
2258            positions_degraded: None,
2259        };
2260        let value = serde_json::to_value(&result).unwrap();
2261        assert!(value.get("positions_degraded").is_none());
2262
2263        let result = super::DocumentSymbolsResult {
2264            positions_degraded: Some(PositionDegradation::Response),
2265            ..result
2266        };
2267        let value = serde_json::to_value(&result).unwrap();
2268        assert_eq!(value["positions_degraded"], "response");
2269    }
2270
2271    /// #467 regression: the old `Option<u8>` narrowing silently dropped any
2272    /// `InlayHintKind::Custom(n)` with `n > 255` to `None`, indistinguishable
2273    /// from "server sent no kind". `lsp_kind_to_u32` must preserve the full
2274    /// `u32` value losslessly.
2275    #[test]
2276    fn test_lsp_kind_to_u32_preserves_custom_values_above_u8_range() {
2277        let kind = lsp_types::InlayHintKind::Custom(300);
2278        assert_eq!(lsp_kind_to_u32(kind), 300u32);
2279    }
2280
2281    #[test]
2282    fn test_position_from_client_bounds() {
2283        assert_eq!(Position::from_client(0, 1), Err(InvalidPosition::ZeroBased));
2284        assert_eq!(Position::from_client(1, 0), Err(InvalidPosition::ZeroBased));
2285        assert_eq!(
2286            Position::from_client(MAX_POSITION_VALUE + 1, 1),
2287            Err(InvalidPosition::TooLarge)
2288        );
2289        assert_eq!(
2290            Position::from_client(1, MAX_POSITION_VALUE + 1),
2291            Err(InvalidPosition::TooLarge)
2292        );
2293        let max = Position::from_client(MAX_POSITION_VALUE, MAX_POSITION_VALUE).unwrap();
2294        assert_eq!((max.lsp_line(), max.lsp_character()), (999_999, 999_999));
2295    }
2296
2297    #[test]
2298    fn test_position_from_output_position_is_not_capped_and_clamps_zero() {
2299        let big = Position::from_server_output(&Position2D {
2300            line: MAX_POSITION_VALUE + 7,
2301            character: 2_000_000,
2302        });
2303        assert_eq!(big.line().get(), MAX_POSITION_VALUE + 7);
2304        let clamped = Position::from_server_output(&Position2D {
2305            line: 0,
2306            character: 0,
2307        });
2308        assert_eq!((clamped.line().get(), clamped.character().get()), (1, 1));
2309    }
2310
2311    #[test]
2312    fn test_position_range_orders_and_bounds() {
2313        let at = Position::at;
2314        assert!(PositionRange::new(at(1, 5), at(1, 5)).is_ok());
2315        assert_eq!(
2316            PositionRange::new(at(2, 1), at(1, 9)),
2317            Err(InvalidRange::Reversed)
2318        );
2319        assert_eq!(
2320            PositionRange::new(at(1, 5), at(1, 4)),
2321            Err(InvalidRange::Reversed)
2322        );
2323        assert_eq!(
2324            PositionRange::from_client((0, 1), (1, 1)),
2325            Err(InvalidRange::Start(InvalidPosition::ZeroBased))
2326        );
2327        assert_eq!(
2328            PositionRange::from_client((1, 1), (1, 0)),
2329            Err(InvalidRange::End(InvalidPosition::ZeroBased))
2330        );
2331
2332        let within = PositionRange::new(at(1, 1), at(1 + MAX_RANGE_LINES, 1)).unwrap();
2333        assert!(BoundedRange::try_from(within).is_ok());
2334        let beyond = PositionRange::new(at(1, 1), at(2 + MAX_RANGE_LINES, 1)).unwrap();
2335        assert_eq!(
2336            BoundedRange::try_from(beyond),
2337            Err(InvalidRange::TooManyLines)
2338        );
2339    }
2340
2341    #[test]
2342    fn test_folding_kind_schema_lists_exactly_the_serde_names() {
2343        let kinds = |k: FoldingKind| match k {
2344            FoldingKind::Comment
2345            | FoldingKind::Imports
2346            | FoldingKind::Region
2347            | FoldingKind::Unspecified => k,
2348        };
2349        let variants = [
2350            FoldingKind::Comment,
2351            FoldingKind::Imports,
2352            FoldingKind::Region,
2353            FoldingKind::Unspecified,
2354        ]
2355        .map(kinds);
2356        let mut generator = schemars::SchemaGenerator::default();
2357        let schema = FoldingKind::json_schema(&mut generator);
2358        let listed: Vec<String> = schema
2359            .get("enum")
2360            .unwrap()
2361            .as_array()
2362            .unwrap()
2363            .iter()
2364            .map(|name| name.as_str().unwrap().to_owned())
2365            .collect();
2366        let serialized: Vec<String> = variants
2367            .iter()
2368            .map(|k| {
2369                serde_json::to_value(k)
2370                    .unwrap()
2371                    .as_str()
2372                    .unwrap()
2373                    .to_owned()
2374            })
2375            .collect();
2376        assert_eq!(listed, serialized);
2377    }
2378
2379    #[test]
2380    fn test_folding_kind_filter_schema_lists_exactly_the_serde_names() {
2381        let filters = |f: FoldingKindFilter| match f {
2382            FoldingKindFilter::All
2383            | FoldingKindFilter::Comment
2384            | FoldingKindFilter::Imports
2385            | FoldingKindFilter::Region => f,
2386        };
2387        let variants = FoldingKindFilter::ALL.map(filters);
2388        let schema = FoldingKindFilter::schema();
2389        let listed: Vec<FoldingKindFilter> = schema
2390            .get("enum")
2391            .unwrap()
2392            .as_array()
2393            .unwrap()
2394            .iter()
2395            .map(|name| FoldingKindFilter::parse(name.as_str().unwrap()).unwrap())
2396            .collect();
2397        assert_eq!(listed, variants);
2398        assert_eq!(
2399            FoldingKindFilter::parse("IMPORTS"),
2400            Some(FoldingKindFilter::Imports)
2401        );
2402        assert_eq!(FoldingKindFilter::parse("unspecified"), None);
2403    }
2404
2405    #[test]
2406    fn test_folding_collapsed_text_is_redacted_before_it_is_cut() {
2407        use crate::redaction::{Redactions, ServerText as _};
2408
2409        let secret = "bravo-secret-222";
2410        let redactions = Redactions::new([("B_TOKEN".to_owned(), secret.to_owned())]);
2411        let straddling = format!("{}{secret}tail", "x".repeat(MAX_COLLAPSED_TEXT_BYTES - 8));
2412        let region = |text: String| FoldingRegion {
2413            start_line: 1,
2414            end_line: 2,
2415            start_character: None,
2416            end_character: None,
2417            kind: FoldingKind::Region,
2418            collapsed_text: Some(text),
2419        };
2420        let mut result = FoldingRangesResult {
2421            regions: vec![region(straddling), region("short".to_string())],
2422            truncated: false,
2423            positions_degraded: None,
2424        };
2425
2426        result.redact_server_text(&redactions);
2427
2428        let text = result.regions[0].collapsed_text.as_deref().unwrap();
2429        assert!(!text.contains("bravo"), "{text}");
2430        assert!(text.ends_with("... (truncated)"), "{text}");
2431        assert!(text.len() <= MAX_COLLAPSED_TEXT_BYTES + "... (truncated)".len());
2432        assert_eq!(result.regions[1].collapsed_text.as_deref(), Some("short"));
2433    }
2434}