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