Skip to main content

mcpls_core/bridge/translator/
navigation.rs

1//! Hover, go-to-definition/implementation/type-definition, and references
2//! handlers.
3
4use std::time::Duration;
5
6use lsp_types::ReferenceContext;
7use tokio::time::Instant;
8
9use super::Translator;
10use super::dto::{
11    DefinitionResult, HoverResult, Location, LocationsResult, Position, PositionDegradation,
12    ReferencesResult,
13};
14use super::enclosing::{Contextualized, ResultContext};
15use super::encoding_ctx::EncodingCtx;
16use super::positioned::{FromPosition, Positioned};
17use super::routing::{Capability, IndexingGate};
18use crate::bridge::indexing::{
19    DEFAULT_INDEXING_READY_TIMEOUT_SECS, INDEXING_STALENESS_BOUND, PROGRESS_LATCH_IDLE,
20    PROGRESS_SETTLE,
21};
22use crate::bridge::{ClientPath, IndexingState};
23use crate::config::ServerId;
24use crate::error::{Error, Result};
25
26/// Default maximum time [`Translator::wait_for_indexing_ready`] waits for a
27/// routed LSP server to report it has finished its initial workspace load,
28/// once a readiness signal has shown indexing is actually in progress.
29/// Matches the timeout already used throughout the rust-analyzer integration
30/// test suite's own (test-only) indexing-readiness helper.
31///
32/// This is only the built-in default (used by [`Translator::new`]) --
33/// overridable per `Translator` via [`Translator::with_indexing_ready_timeout`],
34/// wired from `workspace.indexing_ready_timeout_seconds` in `mcpls.toml`
35/// (#424). The compile-time invariants below are checked against this
36/// default; the same invariants are re-checked against a configured override
37/// at `ServerConfig::validate` time, since a runtime value can't be asserted
38/// at compile time.
39pub(super) const INDEXING_READY_TIMEOUT: Duration =
40    Duration::from_secs(DEFAULT_INDEXING_READY_TIMEOUT_SECS);
41
42/// Poll interval used while waiting out [`INDEXING_READY_TIMEOUT`]. A single
43/// mutex lock plus map lookup, not a network round trip, so a short
44/// interval adds no meaningful overhead relative to the LSP request that
45/// follows once the wait resolves.
46const INDEXING_POLL_INTERVAL: Duration = Duration::from_millis(100);
47
48/// `INDEXING_STALENESS_BOUND` must stay larger than `INDEXING_READY_TIMEOUT`,
49/// or the read-time staleness self-heal could fire within a single caller's
50/// own wait -- reintroducing the cross-caller self-heal race this bound
51/// exists to prevent.
52const _: () = assert!(
53    INDEXING_STALENESS_BOUND.as_nanos() > INDEXING_READY_TIMEOUT.as_nanos(),
54    "INDEXING_STALENESS_BOUND must be greater than INDEXING_READY_TIMEOUT"
55);
56
57/// `PROGRESS_SETTLE` (the read-path settle window) must stay shorter than
58/// `PROGRESS_LATCH_IDLE` (the write-path latch threshold) -- see
59/// `bridge::indexing::PROGRESS_LATCH_IDLE`'s doc for why collapsing the two
60/// into a single threshold reintroduces a real regression (N1).
61const _: () = assert!(
62    PROGRESS_SETTLE.as_nanos() < PROGRESS_LATCH_IDLE.as_nanos(),
63    "PROGRESS_SETTLE must be less than PROGRESS_LATCH_IDLE"
64);
65
66/// A settle window longer than the gate's own wait timeout would let
67/// `wait_for_indexing_ready` time out while the entry is merely mid-settle,
68/// not actually still loading.
69const _: () = assert!(
70    PROGRESS_SETTLE.as_nanos() < INDEXING_READY_TIMEOUT.as_nanos(),
71    "PROGRESS_SETTLE must be less than INDEXING_READY_TIMEOUT"
72);
73
74/// Flattens a `Definition` (`Location` or `Location[]`) into an owned `Vec`.
75fn definition_to_locations(definition: lsp_types::Definition) -> Vec<lsp_types::Location> {
76    match definition {
77        lsp_types::Definition::Location(loc) => vec![loc],
78        lsp_types::Definition::LocationList(locs) => locs,
79    }
80}
81
82/// Converts a `DefinitionLink` into a plain `Location` pointing at its target.
83fn definition_link_to_location(link: lsp_types::DefinitionLink) -> lsp_types::Location {
84    lsp_types::Location {
85        uri: link.target_uri,
86        range: link.target_selection_range,
87    }
88}
89
90/// Hard cap on the number of items a single call normalizes (`goto`,
91/// `references`, `workspace_symbol_search`, call hierarchy, inlay hints,
92/// workspace-edit entries). Without a limit, a response naming an unbounded
93/// number of items turns one MCP tool call into an unbounded number of range
94/// conversions -- each one a potential disk read on a cache miss -- letting a
95/// hostile or misbehaving LSP server amplify one request into massive I/O
96/// (see #474, #487). Applied before normalization, not after, so it bounds
97/// the work actually done rather than just the size of the returned list.
98/// Also used by `Translator::handle_workspace_symbol` to clamp its
99/// caller-supplied `limit`, which otherwise has no upper bound of its own.
100pub(super) const MAX_NORMALIZED_LOCATIONS: usize = 10_000;
101
102/// Per-response allowance of items, [`MAX_NORMALIZED_LOCATIONS`] unless built
103/// with [`ItemBudget::with_cap`].
104///
105/// Every item a handler normalizes must first pass through [`Self::admit`],
106/// so nested loops (e.g. call hierarchy `fromRanges`, workspace-edit
107/// per-file edit lists) share one budget and total work stays within the cap
108/// rather than multiplying per level (#487).
109#[derive(Debug)]
110pub(super) struct ItemBudget {
111    remaining: usize,
112    cap: usize,
113    truncated: bool,
114}
115
116impl ItemBudget {
117    /// A fresh budget holding the full [`MAX_NORMALIZED_LOCATIONS`].
118    pub(super) const fn new() -> Self {
119        Self::with_cap(MAX_NORMALIZED_LOCATIONS)
120    }
121
122    /// A fresh budget holding `cap` items.
123    pub(super) const fn with_cap(cap: usize) -> Self {
124        Self {
125            remaining: cap,
126            cap,
127            truncated: false,
128        }
129    }
130
131    /// Keeps at most the remaining allowance of `items`, spending it, and
132    /// records whether anything was dropped. Logs one `warn!` on the first
133    /// drop of the response.
134    pub(super) fn admit<T>(&mut self, mut items: Vec<T>) -> Vec<T> {
135        if items.len() > self.remaining {
136            self.record_drop(items.len());
137            items.truncate(self.remaining);
138        }
139        self.remaining = self.remaining.saturating_sub(items.len());
140        items
141    }
142
143    /// Like [`Self::admit`] for a lazy sequence: pulls at most the remaining
144    /// allowance plus one item, so the cost stays bounded by the cap however
145    /// long the sequence is.
146    pub(super) fn admit_iter<T>(&mut self, items: impl IntoIterator<Item = T>) -> Vec<T> {
147        let mut items = items.into_iter();
148        let kept: Vec<T> = items.by_ref().take(self.remaining).collect();
149        if items.next().is_some() {
150            self.record_drop(kept.len().saturating_add(1));
151        }
152        self.remaining = self.remaining.saturating_sub(kept.len());
153        kept
154    }
155
156    /// Admits `items` only if all of them fit the remaining allowance;
157    /// otherwise admits none and records the drop. For lists that must not be
158    /// cut midway, such as one file's workspace edits.
159    pub(super) fn admit_whole<T>(&mut self, items: Vec<T>) -> Option<Vec<T>> {
160        self.spend_whole(items.len()).then_some(items)
161    }
162
163    /// Spends `count` items of the allowance if all of them fit, otherwise
164    /// spends nothing and records the drop. For a group, such as a call and
165    /// its ranges, that must be kept or dropped together.
166    pub(super) fn spend_whole(&mut self, count: usize) -> bool {
167        if count > self.remaining {
168            self.record_drop(count);
169            return false;
170        }
171        self.remaining = self.remaining.saturating_sub(count);
172        true
173    }
174
175    fn record_drop(&mut self, reported: usize) {
176        if !self.truncated {
177            tracing::warn!(
178                reported,
179                cap = self.cap,
180                "LSP response item count exceeds the item cap; truncating"
181            );
182        }
183        self.truncated = true;
184    }
185
186    /// Whether any admission ([`Self::admit`], [`Self::admit_whole`], or
187    /// [`Self::spend_whole`]) dropped items.
188    pub(super) const fn truncated(&self) -> bool {
189        self.truncated
190    }
191}
192
193/// Converts raw LSP locations into MCP-facing `Location` values, normalizing
194/// each range into the caller's 1-based coordinate space.
195///
196/// Truncates to [`MAX_NORMALIZED_LOCATIONS`] first -- see its doc. Logs a
197/// single `warn!` when that truncation actually drops locations, so a
198/// response silently capped below what the LSP server reported is at least
199/// visible in logs (see #474).
200///
201/// Deliberately not filtered to workspace roots: unlike a write-bearing
202/// `WorkspaceEdit` (see `edits.rs`), a goto-X/references location is
203/// read-only, and legitimate results routinely point outside the workspace
204/// (e.g. the standard library or a crates.io dependency) -- dropping those
205/// would break ordinary navigation. Any subsequent attempt to open or read
206/// the path this location names still goes through the inbound
207/// `WorkspaceRoots::validate` gate (`mcp/server.rs`), which fails closed,
208/// so the untrusted-URI concern is already covered downstream.
209async fn lsp_locations_to_mcp(
210    locs: Vec<lsp_types::Location>,
211    ctx: &EncodingCtx,
212) -> NormalizedLocations {
213    let mut budget = ItemBudget::new();
214    let locs = budget.admit(locs);
215    let truncated = budget.truncated();
216    let mut locations = Vec::with_capacity(locs.len());
217    for loc in locs {
218        locations.push(Location {
219            uri: loc.uri.to_string(),
220            range: ctx.normalize_range(&loc.uri, loc.range).await,
221            out_of_workspace: ctx.is_out_of_workspace(&loc.uri),
222        });
223    }
224    NormalizedLocations {
225        locations,
226        truncated,
227        positions_degraded: ctx.positions_degraded(),
228    }
229}
230
231/// [`lsp_locations_to_mcp`]'s result: the normalized locations plus whether
232/// [`MAX_NORMALIZED_LOCATIONS`] actually dropped any of the LSP server's
233/// reported locations -- surfaced to the MCP caller via each result DTO's
234/// `truncated` field, since `references`'/goto-X's tool descriptions
235/// otherwise imply a complete result (see #474) -- and whether any position
236/// among them could not be resolved for encoding conversion while
237/// normalizing (disk-read budget exhaustion, an unresolvable path, a line
238/// past EOF, or invalid UTF-8 -- see `ctx.positions_degraded()`), surfaced
239/// via each result DTO's `positions_degraded` field (#497).
240struct NormalizedLocations {
241    locations: Vec<Location>,
242    truncated: bool,
243    positions_degraded: Option<PositionDegradation>,
244}
245
246/// The two response shapes shared by `textDocument/definition`,
247/// `textDocument/implementation`, and `textDocument/typeDefinition`: either a
248/// single `Definition` (`Location` or `Location[]`), or a `DefinitionLink[]`
249/// from clients that opted into `LinkSupport`.
250enum GotoKind {
251    /// Plain locations, as returned to clients without `LinkSupport`.
252    Locations(Vec<lsp_types::Location>),
253    /// A `LocationLink[]`, as returned to clients with `LinkSupport`.
254    Links(Vec<lsp_types::LocationLink>),
255}
256
257/// Implemented once per go-to-X response enum so [`goto_response_to_locations`]
258/// can normalize all three through one code path instead of three near-identical
259/// match arms.
260trait GotoResponse {
261    /// Reduce the response enum down to the two variants shared by every
262    /// go-to-X LSP response.
263    fn into_kind(self) -> GotoKind;
264}
265
266impl GotoResponse for lsp_types::DefinitionResponse {
267    fn into_kind(self) -> GotoKind {
268        match self {
269            Self::Definition(def) => GotoKind::Locations(definition_to_locations(def)),
270            Self::DefinitionLinkList(links) => GotoKind::Links(links),
271        }
272    }
273}
274
275impl GotoResponse for lsp_types::ImplementationResponse {
276    fn into_kind(self) -> GotoKind {
277        match self {
278            Self::Definition(def) => GotoKind::Locations(definition_to_locations(def)),
279            Self::DefinitionLinkList(links) => GotoKind::Links(links),
280        }
281    }
282}
283
284impl GotoResponse for lsp_types::TypeDefinitionResponse {
285    fn into_kind(self) -> GotoKind {
286        match self {
287            Self::Definition(def) => GotoKind::Locations(definition_to_locations(def)),
288            Self::DefinitionLinkList(links) => GotoKind::Links(links),
289        }
290    }
291}
292
293impl GotoResponse for lsp_types::DeclarationResponse {
294    fn into_kind(self) -> GotoKind {
295        match self {
296            Self::Declaration(lsp_types::Declaration::Location(loc)) => {
297                GotoKind::Locations(vec![loc])
298            }
299            Self::Declaration(lsp_types::Declaration::LocationList(locs)) => {
300                GotoKind::Locations(locs)
301            }
302            Self::DeclarationLinkList(links) => GotoKind::Links(links),
303        }
304    }
305}
306
307/// Normalize a go-to-X response (`textDocument/definition`,
308/// `textDocument/implementation`, `textDocument/typeDefinition`, or
309/// `textDocument/declaration`) into a
310/// flat list of MCP `Location` values.
311async fn goto_response_to_locations<R: GotoResponse>(
312    response: Option<R>,
313    ctx: &EncodingCtx,
314) -> NormalizedLocations {
315    let lsp_locs = match response.map(GotoResponse::into_kind) {
316        Some(GotoKind::Locations(locs)) => locs,
317        Some(GotoKind::Links(links)) => {
318            links.into_iter().map(definition_link_to_location).collect()
319        }
320        None => vec![],
321    };
322    lsp_locations_to_mcp(lsp_locs, ctx).await
323}
324
325/// Extracts hover contents as a plain string.
326///
327/// `MarkedString` is `#[deprecated]` in favor of `MarkupContent`, but LSP
328/// 3.17 servers may still send it inside `Hover.contents` -- dropping
329/// support would silently discard hover text from those servers, so this
330/// (and `marked_string_to_string`) carry a narrow, scoped allow rather than
331/// rewriting to `MarkupContent`-only.
332#[allow(deprecated, reason = "LSP servers still send this deprecated field")]
333fn extract_hover_contents(contents: lsp_types::Contents) -> String {
334    match contents {
335        lsp_types::Contents::MarkedString(marked_string) => marked_string_to_string(marked_string),
336        lsp_types::Contents::MarkedStringList(marked_strings) => marked_strings
337            .into_iter()
338            .map(marked_string_to_string)
339            .collect::<Vec<_>>()
340            .join("\n\n"),
341        lsp_types::Contents::MarkupContent(markup) => markup.value,
342    }
343}
344
345/// Convert a marked string to a plain string.
346#[allow(deprecated, reason = "LSP servers still send this deprecated field")]
347fn marked_string_to_string(marked: lsp_types::MarkedString) -> String {
348    match marked {
349        lsp_types::MarkedString::String(s) => s,
350        lsp_types::MarkedString::MarkedStringWithLanguage(ls) => {
351            format!("```{}\n{}\n```", ls.language, ls.value)
352        }
353    }
354}
355
356impl Translator {
357    /// Wait for the routed server `server_id` to finish its initial
358    /// workspace-load/indexing phase before a whole-workspace query (hover,
359    /// definition, implementation, type definition, references, rename,
360    /// completions, code actions) reaches it. Called from
361    /// [`Translator::prepare_gated_document`] for every call site declared
362    /// [`IndexingGate::Required`].
363    ///
364    /// Returns immediately, without waiting, unless
365    /// [`crate::bridge::NotificationCache::indexing_state`] currently
366    /// reports [`IndexingState::Loading`] for `server_id` -- i.e. a
367    /// recognized signal has positively indicated indexing is in progress.
368    /// A server that has never reported any readiness signal
369    /// ([`IndexingState::Unknown`]) is treated the same as
370    /// [`IndexingState::Ready`]: without evidence indexing is happening,
371    /// waiting would only add latency for servers and workspaces that have
372    /// no indexing phase at all.
373    ///
374    /// # Errors
375    ///
376    /// Returns [`Error::WorkspaceIndexing`] if the server is still
377    /// [`IndexingState::Loading`] after the translator's configured
378    /// indexing-ready timeout (default [`INDEXING_READY_TIMEOUT`],
379    /// overridable via [`Self::with_indexing_ready_timeout`]) elapses.
380    pub(super) async fn wait_for_indexing_ready(&self, server_id: &ServerId) -> Result<()> {
381        self.wait_for_indexing_ready_with(
382            server_id,
383            self.indexing_ready_timeout,
384            INDEXING_POLL_INTERVAL,
385        )
386        .await
387    }
388
389    /// [`Self::wait_for_indexing_ready`] with an injectable timeout and poll
390    /// interval, so tests can exercise the timeout path without waiting out
391    /// the real default.
392    ///
393    /// On timeout this returns [`Error::WorkspaceIndexing`] to the caller
394    /// *without* mutating any shared state -- self-healing for a stuck
395    /// `Loading` signal (a dropped `quiescent: true` notification, or a
396    /// server that stalls mid-index) is handled entirely by
397    /// [`crate::bridge::NotificationCache::indexing_state`]'s own
398    /// read-time staleness check, keyed to the signal's age rather than
399    /// this call's. Earlier revisions reset the shared entry here on
400    /// timeout, which let one caller's short timeout silently un-gate
401    /// every other concurrent or later caller before its own deadline;
402    /// never reintroduce a write here.
403    async fn wait_for_indexing_ready_with(
404        &self,
405        server_id: &ServerId,
406        timeout: Duration,
407        poll_interval: Duration,
408    ) -> Result<()> {
409        let Some(cache) = self.notification_cache.as_ref() else {
410            return Ok(());
411        };
412
413        let start = Instant::now();
414        #[allow(
415            clippy::arithmetic_side_effects,
416            reason = "production passes the fixed INDEXING_READY_TIMEOUT"
417        )]
418        let deadline = start + timeout;
419        loop {
420            let state = cache.lock().await.indexing_state(server_id);
421            if state != IndexingState::Loading {
422                return Ok(());
423            }
424            let remaining = deadline.saturating_duration_since(Instant::now());
425            if remaining.is_zero() {
426                return Err(Error::WorkspaceIndexing {
427                    server_id: server_id.clone(),
428                    elapsed_secs: start.elapsed().as_secs(),
429                });
430            }
431            tokio::time::sleep(poll_interval.min(remaining)).await;
432        }
433    }
434
435    /// Handle hover request.
436    ///
437    /// # Errors
438    ///
439    /// Returns an error if the LSP request fails, the file cannot be opened,
440    /// the routed server does not advertise `hoverProvider` support, or the
441    /// server is still indexing the workspace after
442    /// `INDEXING_READY_TIMEOUT`.
443    pub async fn handle_hover(
444        &self,
445        file_path: ClientPath,
446        position: Position,
447    ) -> Result<HoverResult> {
448        let Positioned {
449            result: response,
450            ctx,
451            doc,
452        } = self
453            .position_request::<lsp_types::HoverRequest>(
454                &file_path,
455                position,
456                IndexingGate::Required(Capability::Hover),
457                (),
458            )
459            .await?;
460        let response_uri = doc.uri();
461
462        let result = match response {
463            Some(hover) => {
464                let contents = extract_hover_contents(hover.contents);
465                let range = match hover.range {
466                    Some(r) => Some(ctx.normalize_range(response_uri, r).await),
467                    None => None,
468                };
469                HoverResult {
470                    contents,
471                    range,
472                    positions_degraded: ctx.positions_degraded(),
473                }
474            }
475            None => HoverResult {
476                contents: "No hover information available".to_string(),
477                range: None,
478                positions_degraded: ctx.positions_degraded(),
479            },
480        };
481
482        Ok(result)
483    }
484
485    async fn contextualize_into(
486        &self,
487        normalized: NormalizedLocations,
488        context: ResultContext,
489    ) -> LocationsResult {
490        let truncated = normalized.truncated;
491        let Contextualized {
492            items: locations,
493            enrichment,
494            positions_degraded,
495        } = self
496            .contextualize_locations(normalized.locations, context, normalized.positions_degraded)
497            .await;
498        LocationsResult {
499            locations,
500            truncated,
501            positions_degraded,
502            enrichment,
503        }
504    }
505
506    /// Shared implementation of the go-to-X handlers (`textDocument/definition`,
507    /// `textDocument/implementation`, `textDocument/typeDefinition`): gate on
508    /// the request's capability and on indexing readiness, translate the MCP
509    /// position into LSP coordinates, dispatch the LSP request, and flatten
510    /// the response into MCP locations. Each public handler supplies its
511    /// request type via `R` plus the capability key/predicate specific to
512    /// it.
513    ///
514    /// # Errors
515    ///
516    /// Returns an error if the LSP request fails, the file cannot be opened,
517    /// the routed server does not advertise `capability` support, or the
518    /// server is still indexing the workspace after `INDEXING_READY_TIMEOUT`.
519    async fn handle_goto<R, T>(
520        &self,
521        file_path: &ClientPath,
522        position: Position,
523        capability: Capability,
524    ) -> Result<NormalizedLocations>
525    where
526        R: lsp_types::Request<Result = Option<T>>,
527        R::Params: FromPosition<Extra = ()>,
528        T: GotoResponse,
529    {
530        let Positioned {
531            result: response,
532            ctx,
533            doc: _doc,
534        } = self
535            .position_request::<R>(file_path, position, IndexingGate::Required(capability), ())
536            .await?;
537
538        Ok(goto_response_to_locations(response, &ctx).await)
539    }
540
541    /// Handle definition request.
542    ///
543    /// # Errors
544    ///
545    /// Returns an error if the LSP request fails, the file cannot be opened,
546    /// the routed server does not advertise `definitionProvider` support, or
547    /// the server is still indexing the workspace after
548    /// `INDEXING_READY_TIMEOUT`.
549    pub async fn handle_definition(
550        &self,
551        file_path: ClientPath,
552        position: Position,
553        context: ResultContext,
554    ) -> Result<DefinitionResult> {
555        let normalized = self
556            .handle_goto::<lsp_types::DefinitionRequest, _>(
557                &file_path,
558                position,
559                Capability::Definition,
560            )
561            .await?;
562        Ok(self.contextualize_into(normalized, context).await)
563    }
564
565    /// Handle references request.
566    ///
567    /// # Errors
568    ///
569    /// Returns an error if the LSP request fails, the file cannot be opened,
570    /// the routed server does not advertise `referencesProvider` support, or
571    /// the server is still indexing the workspace after
572    /// `INDEXING_READY_TIMEOUT`.
573    pub async fn handle_references(
574        &self,
575        file_path: ClientPath,
576        position: Position,
577        include_declaration: bool,
578        context: ResultContext,
579    ) -> Result<ReferencesResult> {
580        let Positioned {
581            result: response,
582            ctx,
583            doc: _doc,
584        } = self
585            .position_request::<lsp_types::ReferencesRequest>(
586                &file_path,
587                position,
588                IndexingGate::Required(Capability::References),
589                ReferenceContext {
590                    include_declaration,
591                },
592            )
593            .await?;
594
595        let normalized = lsp_locations_to_mcp(response.unwrap_or_default(), &ctx).await;
596        Ok(self.contextualize_into(normalized, context).await)
597    }
598
599    /// Handle go-to-implementation request (`textDocument/implementation`).
600    ///
601    /// Returns the locations of trait method or interface member implementations.
602    ///
603    /// # Errors
604    ///
605    /// Returns an error if the LSP request fails, the file cannot be opened,
606    /// the routed server does not advertise `implementationProvider`
607    /// support, or the server is still indexing the workspace after
608    /// `INDEXING_READY_TIMEOUT`.
609    pub async fn handle_implementation(
610        &self,
611        file_path: ClientPath,
612        position: Position,
613        context: ResultContext,
614    ) -> Result<LocationsResult> {
615        let normalized = self
616            .handle_goto::<lsp_types::ImplementationRequest, _>(
617                &file_path,
618                position,
619                Capability::Implementation,
620            )
621            .await?;
622        Ok(self.contextualize_into(normalized, context).await)
623    }
624
625    /// Handle go-to-type-definition request (`textDocument/typeDefinition`).
626    ///
627    /// Returns the type definition location of the expression at position. Distinct
628    /// from go-to-definition for variable bindings where definition and type differ.
629    ///
630    /// # Errors
631    ///
632    /// Returns an error if the LSP request fails, the file cannot be opened,
633    /// the routed server does not advertise `typeDefinitionProvider`
634    /// support, or the server is still indexing the workspace after
635    /// `INDEXING_READY_TIMEOUT`.
636    pub async fn handle_type_definition(
637        &self,
638        file_path: ClientPath,
639        position: Position,
640        context: ResultContext,
641    ) -> Result<LocationsResult> {
642        let normalized = self
643            .handle_goto::<lsp_types::TypeDefinitionRequest, _>(
644                &file_path,
645                position,
646                Capability::TypeDefinition,
647            )
648            .await?;
649        Ok(self.contextualize_into(normalized, context).await)
650    }
651
652    /// Handle go-to-declaration request (`textDocument/declaration`).
653    ///
654    /// Returns the declaration location of the symbol at position. Differs
655    /// from go-to-definition for languages that separate declaration from
656    /// definition (C/C++ headers, interface members).
657    ///
658    /// # Errors
659    ///
660    /// Returns an error if the LSP request fails, the file cannot be opened,
661    /// the routed server does not advertise `declarationProvider` support, or
662    /// the server is still indexing the workspace after
663    /// `INDEXING_READY_TIMEOUT`.
664    pub async fn handle_declaration(
665        &self,
666        file_path: ClientPath,
667        position: Position,
668        context: ResultContext,
669    ) -> Result<LocationsResult> {
670        let normalized = self
671            .handle_goto::<lsp_types::DeclarationRequest, _>(
672                &file_path,
673                position,
674                Capability::Declaration,
675            )
676            .await?;
677        Ok(self.contextualize_into(normalized, context).await)
678    }
679}
680
681#[cfg(test)]
682#[allow(deprecated, reason = "LSP servers still send this deprecated field")]
683mod tests {
684    use std::sync::Arc;
685    use std::time::Duration;
686    use std::{assert_matches, fs};
687
688    use tempfile::TempDir;
689    use tokio::io::BufReader;
690    use tokio::sync::Mutex;
691    use tokio::time::timeout;
692    use url::Url;
693
694    use super::*;
695    use crate::bridge::encoding::PositionEncoding;
696    use crate::bridge::translator::testing::*;
697    use crate::bridge::{NotificationCache, path_to_uri};
698    use crate::config::{IndexingReadyTimeoutSecs, ServerId};
699    use crate::test_lsp::client_path;
700    use crate::util::lock_std;
701
702    // -----------------------------------------------------------------
703    // Indexing readiness gate (`Translator::wait_for_indexing_ready`)
704    // -----------------------------------------------------------------
705
706    #[tokio::test]
707    async fn test_wait_for_indexing_ready_without_cache_is_noop() {
708        // No wired cache (most fixtures) must never block -- see `Translator::notification_cache`'s field doc.
709        let translator = Translator::new();
710        let server_id = ServerId::from_static("rust");
711
712        translator
713            .wait_for_indexing_ready(&server_id)
714            .await
715            .unwrap();
716    }
717
718    #[tokio::test]
719    async fn test_wait_for_indexing_ready_unknown_state_is_noop() {
720        let translator = Translator::new()
721            .with_notification_cache(Arc::new(Mutex::new(NotificationCache::new())));
722        let server_id = ServerId::from_static("rust");
723
724        translator
725            .wait_for_indexing_ready(&server_id)
726            .await
727            .unwrap();
728    }
729
730    #[tokio::test]
731    async fn test_wait_for_indexing_ready_ready_state_is_noop() {
732        let cache = Arc::new(Mutex::new(NotificationCache::new()));
733        let server_id = ServerId::from_static("rust");
734        cache.lock().await.observe_indexing_signal(
735            &server_id,
736            "experimental/serverStatus",
737            Some(&serde_json::json!({"quiescent": true})),
738        );
739        let translator = Translator::new().with_notification_cache(cache);
740
741        translator
742            .wait_for_indexing_ready(&server_id)
743            .await
744            .unwrap();
745    }
746
747    /// #424: `with_indexing_ready_timeout` must actually change the bound
748    /// `wait_for_indexing_ready` (the public entry point, not the
749    /// timeout-injectable `_with` test helper) waits before giving up --
750    /// pins the config wiring end-to-end rather than only the constructor
751    /// storing the value.
752    #[tokio::test(start_paused = true)]
753    async fn test_wait_for_indexing_ready_uses_configured_timeout_override() {
754        let cache = Arc::new(Mutex::new(NotificationCache::new()));
755        let server_id = ServerId::from_static("rust");
756        cache.lock().await.observe_indexing_signal(
757            &server_id,
758            "experimental/serverStatus",
759            Some(&serde_json::json!({"quiescent": false})),
760        );
761        let translator = Translator::new()
762            .with_notification_cache(cache)
763            .with_indexing_ready_timeout(IndexingReadyTimeoutSecs::new(5).unwrap());
764
765        let start = Instant::now();
766        let err = translator
767            .wait_for_indexing_ready(&server_id)
768            .await
769            .unwrap_err();
770
771        assert_matches!(err, Error::WorkspaceIndexing { elapsed_secs, .. } if elapsed_secs == 5);
772        assert_eq!(start.elapsed(), Duration::from_secs(5));
773    }
774
775    #[tokio::test]
776    async fn test_wait_for_indexing_ready_loading_times_out() {
777        let cache = Arc::new(Mutex::new(NotificationCache::new()));
778        let server_id = ServerId::from_static("rust");
779        cache.lock().await.observe_indexing_signal(
780            &server_id,
781            "experimental/serverStatus",
782            Some(&serde_json::json!({"quiescent": false})),
783        );
784        let translator = Translator::new().with_notification_cache(cache);
785
786        let err = translator
787            .wait_for_indexing_ready_with(
788                &server_id,
789                Duration::from_millis(50),
790                Duration::from_millis(10),
791            )
792            .await
793            .unwrap_err();
794
795        assert_matches!(
796            err,
797            Error::WorkspaceIndexing { server_id: id, .. } if id == ServerId::from_static("rust")
798        );
799    }
800
801    #[tokio::test]
802    async fn test_wait_for_indexing_ready_returns_ok_once_signaled_ready() {
803        let cache = Arc::new(Mutex::new(NotificationCache::new()));
804        let server_id = ServerId::from_static("rust");
805        cache.lock().await.observe_indexing_signal(
806            &server_id,
807            "experimental/serverStatus",
808            Some(&serde_json::json!({"quiescent": false})),
809        );
810        let translator = Translator::new().with_notification_cache(Arc::clone(&cache));
811
812        let waiter = {
813            let server_id = server_id.clone();
814            tokio::spawn(async move {
815                translator
816                    .wait_for_indexing_ready_with(
817                        &server_id,
818                        Duration::from_secs(5),
819                        Duration::from_millis(10),
820                    )
821                    .await
822            })
823        };
824
825        tokio::time::sleep(Duration::from_millis(30)).await;
826        cache.lock().await.observe_indexing_signal(
827            &server_id,
828            "experimental/serverStatus",
829            Some(&serde_json::json!({"quiescent": true})),
830        );
831
832        timeout(Duration::from_secs(1), waiter)
833            .await
834            .expect("waiter task timed out")
835            .expect("waiter task panicked")
836            .expect("expected Ok once quiescent");
837    }
838
839    /// End-to-end: a real handler (`handle_hover`) must surface
840    /// `Error::WorkspaceIndexing` -- not an empty/`null` result -- when the
841    /// routed server is still `Loading`, without ever reaching the fake LSP
842    /// server. Runs under paused virtual time so it does not actually wait
843    /// out the real `INDEXING_READY_TIMEOUT`.
844    #[tokio::test(start_paused = true)]
845    async fn test_handle_hover_returns_workspace_indexing_error_when_loading() {
846        let dir = TempDir::new().unwrap();
847        let server_id = ServerId::from_static("rust");
848        let caps = lsp_types::ServerCapabilities {
849            hover_provider: Some(lsp_types::HoverProvider::Bool(true)),
850            ..Default::default()
851        };
852        let (translator, _server) = translator_with_capabilities(&dir, &server_id, caps);
853
854        let cache = Arc::new(Mutex::new(NotificationCache::new()));
855        cache.lock().await.observe_indexing_signal(
856            &server_id,
857            "experimental/serverStatus",
858            Some(&serde_json::json!({"quiescent": false})),
859        );
860        let translator = translator.with_notification_cache(cache);
861
862        let path = dir.path().join("main.rs");
863        fs::write(&path, "fn main() {}").unwrap();
864
865        let err = translator
866            .handle_hover(client_path(path.to_string_lossy().into_owned()), pos(1, 1))
867            .await
868            .unwrap_err();
869
870        assert_matches!(
871            err,
872            Error::WorkspaceIndexing { server_id: id, elapsed_secs: 30 } if id == server_id
873        );
874    }
875
876    /// Companion to the timeout test above: when the cache reports `Ready`,
877    /// `handle_hover` must dispatch normally with no added delay.
878    #[tokio::test]
879    async fn test_handle_hover_dispatches_when_indexing_ready() {
880        let dir = TempDir::new().unwrap();
881        let server_id = ServerId::from_static("rust");
882        let caps = lsp_types::ServerCapabilities {
883            hover_provider: Some(lsp_types::HoverProvider::Bool(true)),
884            ..Default::default()
885        };
886        let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
887
888        let cache = Arc::new(Mutex::new(NotificationCache::new()));
889        cache.lock().await.observe_indexing_signal(
890            &server_id,
891            "experimental/serverStatus",
892            Some(&serde_json::json!({"quiescent": true})),
893        );
894        let translator = Arc::new(translator.with_notification_cache(cache));
895
896        let path = dir.path().join("main.rs");
897        fs::write(&path, "fn main() {}").unwrap();
898
899        let handle = {
900            let translator = Arc::clone(&translator);
901            let path = path.to_string_lossy().to_string();
902            tokio::spawn(async move { translator.handle_hover(client_path(path), pos(1, 1)).await })
903        };
904
905        let mut wire = BufReader::new(&mut server.write_stdout);
906        let opened = read_framed_message(&mut wire).await;
907        assert_eq!(opened["method"], "textDocument/didOpen");
908        let request = read_framed_message(&mut wire).await;
909        assert_eq!(request["method"], "textDocument/hover");
910
911        write_response(
912            &mut server.read_half_stdin,
913            &request["id"],
914            serde_json::json!({
915                "contents": {"kind": "markdown", "value": "hover text"}
916            }),
917        )
918        .await;
919
920        let result = handle.await.unwrap().unwrap();
921        assert_eq!(result.contents, "hover text");
922    }
923
924    /// #518: a hover that times out while awaiting the server's reply must
925    /// leave the bridge usable -- the document stays tracked, so the next
926    /// hover goes straight to the request without a second `didOpen`.
927    #[tokio::test(start_paused = true)]
928    async fn test_handle_hover_timeout_keeps_document_open_for_next_request() {
929        let dir = TempDir::new().unwrap();
930        let server_id = ServerId::from_static("rust");
931        let caps = lsp_types::ServerCapabilities {
932            hover_provider: Some(lsp_types::HoverProvider::Bool(true)),
933            ..Default::default()
934        };
935        let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
936
937        let cache = Arc::new(Mutex::new(NotificationCache::new()));
938        cache.lock().await.observe_indexing_signal(
939            &server_id,
940            "experimental/serverStatus",
941            Some(&serde_json::json!({"quiescent": true})),
942        );
943        let translator = Arc::new(translator.with_notification_cache(cache));
944
945        let path = dir.path().join("main.rs");
946        fs::write(&path, "fn main() {}").unwrap();
947        let path = path.to_string_lossy().to_string();
948
949        let spawn_hover = || {
950            let translator = Arc::clone(&translator);
951            let path = path.clone();
952            tokio::spawn(async move { translator.handle_hover(client_path(path), pos(1, 1)).await })
953        };
954
955        let mut wire = BufReader::new(&mut server.write_stdout);
956
957        let first = spawn_hover();
958        let opened = read_framed_message(&mut wire).await;
959        assert_eq!(opened["method"], "textDocument/didOpen");
960        let first_request = read_framed_message(&mut wire).await;
961        assert_eq!(first_request["method"], "textDocument/hover");
962
963        let timeout_secs = crate::config::LspServerConfig::rust_analyzer().request_timeout_seconds;
964        tokio::time::advance(Duration::from_secs(timeout_secs.get() + 1)).await;
965        assert_matches!(first.await.unwrap().unwrap_err(), Error::Timeout(_));
966
967        let second = spawn_hover();
968        let second_request = read_framed_message(&mut wire).await;
969        assert_eq!(
970            second_request["method"], "textDocument/hover",
971            "a timed-out hover must not cause the document to be re-opened"
972        );
973        write_response(
974            &mut server.read_half_stdin,
975            &second_request["id"],
976            serde_json::json!({"contents": {"kind": "markdown", "value": "after timeout"}}),
977        )
978        .await;
979
980        assert_eq!(second.await.unwrap().unwrap().contents, "after timeout");
981    }
982
983    /// End-to-end: `handle_definition` must surface `Error::WorkspaceIndexing`
984    /// while the routed server is still `Loading`, without reaching the fake
985    /// LSP server.
986    #[tokio::test(start_paused = true)]
987    async fn test_handle_definition_returns_workspace_indexing_error_when_loading() {
988        let dir = TempDir::new().unwrap();
989        let server_id = ServerId::from_static("rust");
990        let caps = lsp_types::ServerCapabilities {
991            definition_provider: Some(lsp_types::DefinitionProvider::Bool(true)),
992            ..Default::default()
993        };
994        let (translator, _server) = translator_with_capabilities(&dir, &server_id, caps);
995
996        let cache = Arc::new(Mutex::new(NotificationCache::new()));
997        cache.lock().await.observe_indexing_signal(
998            &server_id,
999            "experimental/serverStatus",
1000            Some(&serde_json::json!({"quiescent": false})),
1001        );
1002        let translator = translator.with_notification_cache(cache);
1003
1004        let path = dir.path().join("main.rs");
1005        fs::write(&path, "fn main() {}").unwrap();
1006
1007        let err = translator
1008            .handle_definition(
1009                client_path(path.to_string_lossy().into_owned()),
1010                pos(1, 1),
1011                ResultContext::None,
1012            )
1013            .await
1014            .unwrap_err();
1015
1016        assert_matches!(
1017            err,
1018            Error::WorkspaceIndexing { server_id: id, .. } if id == server_id
1019        );
1020    }
1021
1022    /// Companion: when the cache reports `Ready`, `handle_definition` must
1023    /// dispatch normally.
1024    #[tokio::test]
1025    async fn test_handle_definition_dispatches_when_indexing_ready() {
1026        let dir = TempDir::new().unwrap();
1027        let server_id = ServerId::from_static("rust");
1028        let caps = lsp_types::ServerCapabilities {
1029            definition_provider: Some(lsp_types::DefinitionProvider::Bool(true)),
1030            ..Default::default()
1031        };
1032        let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
1033
1034        let cache = Arc::new(Mutex::new(NotificationCache::new()));
1035        cache.lock().await.observe_indexing_signal(
1036            &server_id,
1037            "experimental/serverStatus",
1038            Some(&serde_json::json!({"quiescent": true})),
1039        );
1040        let translator = Arc::new(translator.with_notification_cache(cache));
1041
1042        let path = dir.path().join("main.rs");
1043        fs::write(&path, "fn main() {}").unwrap();
1044
1045        let handle = {
1046            let translator = Arc::clone(&translator);
1047            let path = path.to_string_lossy().to_string();
1048            tokio::spawn(async move {
1049                translator
1050                    .handle_definition(client_path(path), pos(1, 1), ResultContext::None)
1051                    .await
1052            })
1053        };
1054
1055        let mut wire = BufReader::new(&mut server.write_stdout);
1056        let opened = read_framed_message(&mut wire).await;
1057        assert_eq!(opened["method"], "textDocument/didOpen");
1058        let request = read_framed_message(&mut wire).await;
1059        assert_eq!(request["method"], "textDocument/definition");
1060
1061        write_response(
1062            &mut server.read_half_stdin,
1063            &request["id"],
1064            serde_json::Value::Null,
1065        )
1066        .await;
1067
1068        let result = handle.await.unwrap().unwrap();
1069        assert!(result.locations.is_empty());
1070    }
1071
1072    /// End-to-end: `handle_references` must surface `Error::WorkspaceIndexing`
1073    /// while the routed server is still `Loading`, without reaching the fake
1074    /// LSP server.
1075    #[tokio::test(start_paused = true)]
1076    async fn test_handle_references_returns_workspace_indexing_error_when_loading() {
1077        let dir = TempDir::new().unwrap();
1078        let server_id = ServerId::from_static("rust");
1079        let caps = lsp_types::ServerCapabilities {
1080            references_provider: Some(lsp_types::ReferencesProvider::Bool(true)),
1081            ..Default::default()
1082        };
1083        let (translator, _server) = translator_with_capabilities(&dir, &server_id, caps);
1084
1085        let cache = Arc::new(Mutex::new(NotificationCache::new()));
1086        cache.lock().await.observe_indexing_signal(
1087            &server_id,
1088            "experimental/serverStatus",
1089            Some(&serde_json::json!({"quiescent": false})),
1090        );
1091        let translator = translator.with_notification_cache(cache);
1092
1093        let path = dir.path().join("main.rs");
1094        fs::write(&path, "fn main() {}").unwrap();
1095
1096        let err = translator
1097            .handle_references(
1098                client_path(path.to_string_lossy().into_owned()),
1099                pos(1, 1),
1100                true,
1101                ResultContext::None,
1102            )
1103            .await
1104            .unwrap_err();
1105
1106        assert_matches!(
1107            err,
1108            Error::WorkspaceIndexing { server_id: id, .. } if id == server_id
1109        );
1110    }
1111
1112    /// Companion: when the cache reports `Ready`, `handle_references` must
1113    /// dispatch normally.
1114    #[tokio::test]
1115    async fn test_handle_references_dispatches_when_indexing_ready() {
1116        let dir = TempDir::new().unwrap();
1117        let server_id = ServerId::from_static("rust");
1118        let caps = lsp_types::ServerCapabilities {
1119            references_provider: Some(lsp_types::ReferencesProvider::Bool(true)),
1120            ..Default::default()
1121        };
1122        let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
1123
1124        let cache = Arc::new(Mutex::new(NotificationCache::new()));
1125        cache.lock().await.observe_indexing_signal(
1126            &server_id,
1127            "experimental/serverStatus",
1128            Some(&serde_json::json!({"quiescent": true})),
1129        );
1130        let translator = Arc::new(translator.with_notification_cache(cache));
1131
1132        let path = dir.path().join("main.rs");
1133        fs::write(&path, "fn main() {}").unwrap();
1134
1135        let handle = {
1136            let translator = Arc::clone(&translator);
1137            let path = path.to_string_lossy().to_string();
1138            tokio::spawn(async move {
1139                translator
1140                    .handle_references(client_path(path), pos(1, 1), true, ResultContext::None)
1141                    .await
1142            })
1143        };
1144
1145        let mut wire = BufReader::new(&mut server.write_stdout);
1146        let opened = read_framed_message(&mut wire).await;
1147        assert_eq!(opened["method"], "textDocument/didOpen");
1148        let request = read_framed_message(&mut wire).await;
1149        assert_eq!(request["method"], "textDocument/references");
1150
1151        write_response(
1152            &mut server.read_half_stdin,
1153            &request["id"],
1154            serde_json::Value::Null,
1155        )
1156        .await;
1157
1158        let result = handle.await.unwrap().unwrap();
1159        assert!(result.locations.is_empty());
1160    }
1161
1162    /// S3 fix: `handle_implementation` shares `handle_goto` with
1163    /// `handle_definition` and must now be gated the same way --
1164    /// `textDocument/implementation` needs the whole-crate trait-impl index,
1165    /// which is at least as index-dependent as `definition`.
1166    #[tokio::test(start_paused = true)]
1167    async fn test_handle_implementation_returns_workspace_indexing_error_when_loading() {
1168        let dir = TempDir::new().unwrap();
1169        let server_id = ServerId::from_static("rust");
1170        let caps = lsp_types::ServerCapabilities {
1171            implementation_provider: Some(lsp_types::ImplementationProvider::Bool(true)),
1172            ..Default::default()
1173        };
1174        let (translator, _server) = translator_with_capabilities(&dir, &server_id, caps);
1175
1176        let cache = Arc::new(Mutex::new(NotificationCache::new()));
1177        cache.lock().await.observe_indexing_signal(
1178            &server_id,
1179            "experimental/serverStatus",
1180            Some(&serde_json::json!({"quiescent": false})),
1181        );
1182        let translator = translator.with_notification_cache(cache);
1183
1184        let path = dir.path().join("main.rs");
1185        fs::write(&path, "fn main() {}").unwrap();
1186
1187        let err = translator
1188            .handle_implementation(
1189                client_path(path.to_string_lossy().into_owned()),
1190                pos(1, 1),
1191                ResultContext::None,
1192            )
1193            .await
1194            .unwrap_err();
1195
1196        assert_matches!(
1197            err,
1198            Error::WorkspaceIndexing { server_id: id, .. } if id == server_id
1199        );
1200    }
1201
1202    /// S3 fix, companion for `handle_type_definition`.
1203    #[tokio::test(start_paused = true)]
1204    async fn test_handle_type_definition_returns_workspace_indexing_error_when_loading() {
1205        let dir = TempDir::new().unwrap();
1206        let server_id = ServerId::from_static("rust");
1207        let caps = lsp_types::ServerCapabilities {
1208            type_definition_provider: Some(lsp_types::TypeDefinitionProvider::Bool(true)),
1209            ..Default::default()
1210        };
1211        let (translator, _server) = translator_with_capabilities(&dir, &server_id, caps);
1212
1213        let cache = Arc::new(Mutex::new(NotificationCache::new()));
1214        cache.lock().await.observe_indexing_signal(
1215            &server_id,
1216            "experimental/serverStatus",
1217            Some(&serde_json::json!({"quiescent": false})),
1218        );
1219        let translator = translator.with_notification_cache(cache);
1220
1221        let path = dir.path().join("main.rs");
1222        fs::write(&path, "fn main() {}").unwrap();
1223
1224        let err = translator
1225            .handle_type_definition(
1226                client_path(path.to_string_lossy().into_owned()),
1227                pos(1, 1),
1228                ResultContext::None,
1229            )
1230            .await
1231            .unwrap_err();
1232
1233        assert_matches!(
1234            err,
1235            Error::WorkspaceIndexing { server_id: id, .. } if id == server_id
1236        );
1237    }
1238
1239    /// A timed-out wait must return `Error::WorkspaceIndexing` to its own
1240    /// caller without mutating the shared cache entry -- a fixed-in-review
1241    /// regression had the timeout handler reset the entry to `Unknown`,
1242    /// which released every other concurrent/later caller early (see
1243    /// `test_wait_for_indexing_ready_one_callers_timeout_does_not_release_another`
1244    /// for the direct reproduction). Self-healing for a genuinely stuck
1245    /// signal now lives entirely in
1246    /// `NotificationCache::indexing_state`'s own staleness check.
1247    #[tokio::test]
1248    async fn test_wait_for_indexing_ready_timeout_does_not_mutate_shared_state() {
1249        let cache = Arc::new(Mutex::new(NotificationCache::new()));
1250        let server_id = ServerId::from_static("rust");
1251        cache.lock().await.observe_indexing_signal(
1252            &server_id,
1253            "experimental/serverStatus",
1254            Some(&serde_json::json!({"quiescent": false})),
1255        );
1256        let translator = Translator::new().with_notification_cache(Arc::clone(&cache));
1257
1258        translator
1259            .wait_for_indexing_ready_with(
1260                &server_id,
1261                Duration::from_millis(30),
1262                Duration::from_millis(10),
1263            )
1264            .await
1265            .unwrap_err();
1266
1267        assert_eq!(
1268            cache.lock().await.indexing_state(&server_id),
1269            IndexingState::Loading,
1270            "a timed-out wait must not touch the shared entry -- it is still fresh, so it must \
1271             still read as Loading for any other caller"
1272        );
1273    }
1274
1275    /// Direct reproduction of the self-heal race: a short-timeout waiter's
1276    /// own timeout must never resolve a concurrent long-timeout waiter's
1277    /// independent wait early. Before the fix, both waiters observed the
1278    /// same shared `IndexingState`, and the short waiter's timeout handler
1279    /// reset it to `Unknown` as a side effect -- silently un-gating the
1280    /// long waiter tens of seconds before its own deadline.
1281    #[tokio::test]
1282    async fn test_wait_for_indexing_ready_one_callers_timeout_does_not_release_another() {
1283        let cache = Arc::new(Mutex::new(NotificationCache::new()));
1284        let server_id = ServerId::from_static("rust");
1285        cache.lock().await.observe_indexing_signal(
1286            &server_id,
1287            "experimental/serverStatus",
1288            Some(&serde_json::json!({"quiescent": false})),
1289        );
1290        let translator = Arc::new(Translator::new().with_notification_cache(Arc::clone(&cache)));
1291
1292        let short = {
1293            let translator = Arc::clone(&translator);
1294            let server_id = server_id.clone();
1295            tokio::spawn(async move {
1296                translator
1297                    .wait_for_indexing_ready_with(
1298                        &server_id,
1299                        Duration::from_millis(80),
1300                        Duration::from_millis(10),
1301                    )
1302                    .await
1303            })
1304        };
1305        let long = {
1306            let translator = Arc::clone(&translator);
1307            let server_id = server_id.clone();
1308            tokio::spawn(async move {
1309                translator
1310                    .wait_for_indexing_ready_with(
1311                        &server_id,
1312                        Duration::from_secs(30),
1313                        Duration::from_millis(10),
1314                    )
1315                    .await
1316            })
1317        };
1318
1319        let short_result = short.await.unwrap();
1320        assert_matches!(
1321            short_result,
1322            Err(Error::WorkspaceIndexing { .. }),
1323            "the short-timeout waiter must time out on its own schedule, got {short_result:?}"
1324        );
1325
1326        // Well past the short waiter's 80ms deadline, nowhere near the long
1327        // waiter's 30s one.
1328        tokio::time::sleep(Duration::from_millis(150)).await;
1329        assert!(
1330            !long.is_finished(),
1331            "a concurrent caller's short timeout must never resolve another caller's \
1332             independent wait early"
1333        );
1334        long.abort();
1335    }
1336
1337    #[test]
1338    fn test_extract_hover_contents_string() {
1339        let marked_string = lsp_types::MarkedString::String("Test hover".to_string());
1340        let contents = lsp_types::Contents::MarkedString(marked_string);
1341        let result = extract_hover_contents(contents);
1342        assert_eq!(result, "Test hover");
1343    }
1344
1345    #[test]
1346    fn test_extract_hover_contents_language_string() {
1347        let marked_string = lsp_types::MarkedString::MarkedStringWithLanguage(
1348            lsp_types::MarkedStringWithLanguage {
1349                language: "rust".to_string(),
1350                value: "fn main() {}".to_string(),
1351            },
1352        );
1353        let contents = lsp_types::Contents::MarkedString(marked_string);
1354        let result = extract_hover_contents(contents);
1355        assert_eq!(result, "```rust\nfn main() {}\n```");
1356    }
1357
1358    #[test]
1359    fn test_extract_hover_contents_markup() {
1360        let markup = lsp_types::MarkupContent {
1361            kind: lsp_types::MarkupKind::Markdown,
1362            value: "# Documentation".to_string(),
1363        };
1364        let contents = lsp_types::Contents::MarkupContent(markup);
1365        let result = extract_hover_contents(contents);
1366        assert_eq!(result, "# Documentation");
1367    }
1368
1369    /// Success-path coverage for `handle_definition` through the
1370    /// `Definition::Location` -> `GotoKind::Definition` arm, pinning the
1371    /// `GotoResponse` impl for `DefinitionResponse`.
1372    #[tokio::test]
1373    async fn test_handle_definition_flattens_single_location() {
1374        let dir = TempDir::new().unwrap();
1375        let server_id = ServerId::from_static("rust");
1376        let caps = lsp_types::ServerCapabilities {
1377            definition_provider: Some(lsp_types::DefinitionProvider::Bool(true)),
1378            ..Default::default()
1379        };
1380        let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
1381
1382        let path = canonical_dir(&dir).join("main.rs");
1383        fs::write(&path, "fn main() {}").unwrap();
1384        let target_path = canonical_dir(&dir).join("target.rs");
1385        fs::write(&target_path, "fn target() {}").unwrap();
1386        let target_uri = Url::from_file_path(&target_path).unwrap().to_string();
1387
1388        let translator = Arc::new(translator);
1389        let handle = {
1390            let translator = Arc::clone(&translator);
1391            let path = path.to_string_lossy().to_string();
1392            tokio::spawn(async move {
1393                translator
1394                    .handle_definition(client_path(path), Position::at(1, 1), ResultContext::None)
1395                    .await
1396            })
1397        };
1398
1399        let mut wire = BufReader::new(&mut server.write_stdout);
1400        let opened = read_framed_message(&mut wire).await;
1401        assert_eq!(opened["method"], "textDocument/didOpen");
1402        let request = read_framed_message(&mut wire).await;
1403        assert_eq!(request["method"], "textDocument/definition");
1404
1405        write_response(
1406            &mut server.read_half_stdin,
1407            &request["id"],
1408            serde_json::json!({
1409                "uri": target_uri,
1410                "range": {
1411                    "start": {"line": 0, "character": 0},
1412                    "end": {"line": 0, "character": 6}
1413                }
1414            }),
1415        )
1416        .await;
1417
1418        let result = timeout(Duration::from_secs(2), handle)
1419            .await
1420            .expect("handle_definition should not hang")
1421            .unwrap()
1422            .unwrap();
1423
1424        assert_eq!(result.locations.len(), 1);
1425        assert_eq!(result.locations[0].uri, target_uri);
1426        assert!(
1427            !result.locations[0].out_of_workspace,
1428            "a definition location inside the workspace root must not be marked out_of_workspace"
1429        );
1430    }
1431
1432    /// #415 (revised per critic C1): a definition location whose URI falls
1433    /// outside every configured workspace root must still be returned --
1434    /// goto-definition into the standard library or a crates.io dependency
1435    /// is normal, expected navigation, not an attack. The untrusted-URI
1436    /// concern is instead covered downstream, by the inbound
1437    /// `WorkspaceRoots::validate` gate any subsequent open/read of the
1438    /// path would hit.
1439    #[tokio::test]
1440    async fn test_handle_definition_does_not_filter_out_of_workspace_location() {
1441        let dir = TempDir::new().unwrap();
1442        let server_id = ServerId::from_static("rust");
1443        let caps = lsp_types::ServerCapabilities {
1444            definition_provider: Some(lsp_types::DefinitionProvider::Bool(true)),
1445            ..Default::default()
1446        };
1447        let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
1448
1449        let path = dir.path().join("main.rs");
1450        fs::write(&path, "fn main() {}").unwrap();
1451        let outside_uri = "file:///outside/workspace/stdlib.rs";
1452
1453        let translator = Arc::new(translator);
1454        let handle = {
1455            let translator = Arc::clone(&translator);
1456            let path = path.to_string_lossy().to_string();
1457            tokio::spawn(async move {
1458                translator
1459                    .handle_definition(client_path(path), Position::at(1, 1), ResultContext::None)
1460                    .await
1461            })
1462        };
1463
1464        let mut wire = BufReader::new(&mut server.write_stdout);
1465        let opened = read_framed_message(&mut wire).await;
1466        assert_eq!(opened["method"], "textDocument/didOpen");
1467        let request = read_framed_message(&mut wire).await;
1468        assert_eq!(request["method"], "textDocument/definition");
1469
1470        write_response(
1471            &mut server.read_half_stdin,
1472            &request["id"],
1473            serde_json::json!({
1474                "uri": outside_uri,
1475                "range": {
1476                    "start": {"line": 0, "character": 0},
1477                    "end": {"line": 0, "character": 6}
1478                }
1479            }),
1480        )
1481        .await;
1482
1483        let result = timeout(Duration::from_secs(2), handle)
1484            .await
1485            .expect("handle_definition should not hang")
1486            .unwrap()
1487            .unwrap();
1488
1489        assert_eq!(
1490            result.locations.len(),
1491            1,
1492            "an out-of-workspace definition location (e.g. stdlib/a dependency) must be \
1493             returned, not dropped"
1494        );
1495        assert_eq!(result.locations[0].uri, outside_uri);
1496        assert!(
1497            result.locations[0].out_of_workspace,
1498            "a definition location outside every workspace root must be marked out_of_workspace"
1499        );
1500    }
1501
1502    /// #415 (revised per critic C1) companion for `handle_references`: an
1503    /// out-of-workspace location must pass through unfiltered, same as an
1504    /// in-workspace one -- see `test_handle_definition_does_not_filter_out_of_workspace_location`.
1505    #[tokio::test]
1506    async fn test_handle_references_does_not_filter_out_of_workspace_location() {
1507        let dir = TempDir::new().unwrap();
1508        let server_id = ServerId::from_static("rust");
1509        let caps = lsp_types::ServerCapabilities {
1510            references_provider: Some(lsp_types::ReferencesProvider::Bool(true)),
1511            ..Default::default()
1512        };
1513        let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
1514
1515        let path = canonical_dir(&dir).join("main.rs");
1516        fs::write(&path, "fn main() {}").unwrap();
1517        let inside_path = canonical_dir(&dir).join("inside.rs");
1518        fs::write(&inside_path, "fn used() {}").unwrap();
1519        let inside_uri = Url::from_file_path(&inside_path).unwrap().to_string();
1520        let outside_uri = "file:///outside/workspace/stdlib.rs";
1521
1522        let translator = Arc::new(translator);
1523        let handle = {
1524            let translator = Arc::clone(&translator);
1525            let path = path.to_string_lossy().to_string();
1526            tokio::spawn(async move {
1527                translator
1528                    .handle_references(client_path(path), pos(1, 1), true, ResultContext::None)
1529                    .await
1530            })
1531        };
1532
1533        let mut wire = BufReader::new(&mut server.write_stdout);
1534        let opened = read_framed_message(&mut wire).await;
1535        assert_eq!(opened["method"], "textDocument/didOpen");
1536        let request = read_framed_message(&mut wire).await;
1537        assert_eq!(request["method"], "textDocument/references");
1538
1539        write_response(
1540            &mut server.read_half_stdin,
1541            &request["id"],
1542            serde_json::json!([
1543                {
1544                    "uri": inside_uri,
1545                    "range": {
1546                        "start": {"line": 0, "character": 0},
1547                        "end": {"line": 0, "character": 4}
1548                    }
1549                },
1550                {
1551                    "uri": outside_uri,
1552                    "range": {
1553                        "start": {"line": 0, "character": 0},
1554                        "end": {"line": 0, "character": 4}
1555                    }
1556                }
1557            ]),
1558        )
1559        .await;
1560
1561        let result = timeout(Duration::from_secs(2), handle)
1562            .await
1563            .expect("handle_references should not hang")
1564            .unwrap()
1565            .unwrap();
1566
1567        assert_eq!(
1568            result.locations.len(),
1569            2,
1570            "both the in-workspace and out-of-workspace locations must survive"
1571        );
1572        assert!(result.locations.iter().any(|l| l.uri == inside_uri));
1573        assert!(result.locations.iter().any(|l| l.uri == outside_uri));
1574        assert!(
1575            !result
1576                .locations
1577                .iter()
1578                .find(|l| l.uri == inside_uri)
1579                .unwrap()
1580                .out_of_workspace,
1581            "an in-workspace reference location must not be marked out_of_workspace"
1582        );
1583        assert!(
1584            result
1585                .locations
1586                .iter()
1587                .find(|l| l.uri == outside_uri)
1588                .unwrap()
1589                .out_of_workspace,
1590            "an out-of-workspace reference location must be marked out_of_workspace"
1591        );
1592    }
1593
1594    /// Regression for #474/M4: `get_references`' tool description no longer
1595    /// promises "all" references, since a response past
1596    /// `MAX_NORMALIZED_LOCATIONS` is capped -- the client must be able to
1597    /// detect that via `ReferencesResult::truncated` rather than silently
1598    /// receiving a partial result that looks complete.
1599    #[tokio::test]
1600    async fn test_handle_references_sets_truncated_flag_past_cap() {
1601        let dir = TempDir::new().unwrap();
1602        let server_id = ServerId::from_static("rust");
1603        let caps = lsp_types::ServerCapabilities {
1604            references_provider: Some(lsp_types::ReferencesProvider::Bool(true)),
1605            ..Default::default()
1606        };
1607        let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
1608
1609        let path = dir.path().join("main.rs");
1610        fs::write(&path, "fn main() {}").unwrap();
1611        let uri = Url::from_file_path(&path).unwrap().to_string();
1612
1613        let translator = Arc::new(translator);
1614        let handle = {
1615            let translator = Arc::clone(&translator);
1616            let path = path.to_string_lossy().to_string();
1617            tokio::spawn(async move {
1618                translator
1619                    .handle_references(client_path(path), pos(1, 1), true, ResultContext::None)
1620                    .await
1621            })
1622        };
1623
1624        let mut wire = BufReader::new(&mut server.write_stdout);
1625        let opened = read_framed_message(&mut wire).await;
1626        assert_eq!(opened["method"], "textDocument/didOpen");
1627        let request = read_framed_message(&mut wire).await;
1628        assert_eq!(request["method"], "textDocument/references");
1629
1630        let locations: Vec<serde_json::Value> = (0..MAX_NORMALIZED_LOCATIONS + 500)
1631            .map(|_| {
1632                serde_json::json!({
1633                    "uri": uri,
1634                    "range": {
1635                        "start": {"line": 0, "character": 0},
1636                        "end": {"line": 0, "character": 4}
1637                    }
1638                })
1639            })
1640            .collect();
1641        write_response(
1642            &mut server.read_half_stdin,
1643            &request["id"],
1644            serde_json::json!(locations),
1645        )
1646        .await;
1647
1648        let result = timeout(Duration::from_secs(5), handle)
1649            .await
1650            .expect("handle_references should not hang")
1651            .unwrap()
1652            .unwrap();
1653
1654        assert_eq!(result.locations.len(), MAX_NORMALIZED_LOCATIONS);
1655        assert!(
1656            result.truncated,
1657            "a references response past MAX_NORMALIZED_LOCATIONS must set truncated: true"
1658        );
1659    }
1660
1661    /// Success-path coverage for `handle_implementation` through the
1662    /// `Definition::LocationList` -> `GotoKind::Definition` arm, pinning the
1663    /// `GotoResponse` impl for `ImplementationResponse`.
1664    #[tokio::test]
1665    async fn test_handle_implementation_flattens_location_list() {
1666        let dir = TempDir::new().unwrap();
1667        let server_id = ServerId::from_static("rust");
1668        let caps = lsp_types::ServerCapabilities {
1669            implementation_provider: Some(lsp_types::ImplementationProvider::Bool(true)),
1670            ..Default::default()
1671        };
1672        let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
1673
1674        let path = dir.path().join("main.rs");
1675        fs::write(&path, "fn main() {}").unwrap();
1676        let first_impl_path = dir.path().join("impl_a.rs");
1677        fs::write(&first_impl_path, "struct A;").unwrap();
1678        let first_impl_uri = Url::from_file_path(&first_impl_path).unwrap().to_string();
1679        let second_impl_path = dir.path().join("impl_b.rs");
1680        fs::write(&second_impl_path, "struct B;").unwrap();
1681        let second_impl_uri = Url::from_file_path(&second_impl_path).unwrap().to_string();
1682
1683        let translator = Arc::new(translator);
1684        let handle = {
1685            let translator = Arc::clone(&translator);
1686            let path = path.to_string_lossy().to_string();
1687            tokio::spawn(async move {
1688                translator
1689                    .handle_implementation(
1690                        client_path(path),
1691                        Position::at(1, 1),
1692                        ResultContext::None,
1693                    )
1694                    .await
1695            })
1696        };
1697
1698        let mut wire = BufReader::new(&mut server.write_stdout);
1699        let opened = read_framed_message(&mut wire).await;
1700        assert_eq!(opened["method"], "textDocument/didOpen");
1701        let request = read_framed_message(&mut wire).await;
1702        assert_eq!(request["method"], "textDocument/implementation");
1703
1704        write_response(
1705            &mut server.read_half_stdin,
1706            &request["id"],
1707            serde_json::json!([
1708                {
1709                    "uri": first_impl_uri,
1710                    "range": {
1711                        "start": {"line": 0, "character": 0},
1712                        "end": {"line": 0, "character": 9}
1713                    }
1714                },
1715                {
1716                    "uri": second_impl_uri,
1717                    "range": {
1718                        "start": {"line": 0, "character": 0},
1719                        "end": {"line": 0, "character": 9}
1720                    }
1721                }
1722            ]),
1723        )
1724        .await;
1725
1726        let result = timeout(Duration::from_secs(2), handle)
1727            .await
1728            .expect("handle_implementation should not hang")
1729            .unwrap()
1730            .unwrap();
1731
1732        assert_eq!(result.locations.len(), 2);
1733        assert_eq!(result.locations[0].uri, first_impl_uri);
1734        assert_eq!(result.locations[1].uri, second_impl_uri);
1735    }
1736
1737    /// `handle_declaration` flattens a plain `Declaration::LocationList` and
1738    /// sends `textDocument/declaration`.
1739    #[tokio::test]
1740    async fn test_handle_declaration_flattens_location_list() {
1741        let dir = TempDir::new().unwrap();
1742        let server_id = ServerId::from_static("rust");
1743        let caps = lsp_types::ServerCapabilities {
1744            declaration_provider: Some(lsp_types::DeclarationProvider::Bool(true)),
1745            ..Default::default()
1746        };
1747        let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
1748
1749        let path = dir.path().join("main.rs");
1750        fs::write(&path, "fn main() {}").unwrap();
1751        let decl_path = dir.path().join("decl.rs");
1752        fs::write(&decl_path, "fn decl();").unwrap();
1753        let decl_uri = Url::from_file_path(&decl_path).unwrap().to_string();
1754
1755        let translator = Arc::new(translator);
1756        let handle = {
1757            let translator = Arc::clone(&translator);
1758            let path = client_path(&path);
1759            tokio::spawn(async move {
1760                translator
1761                    .handle_declaration(path, pos(1, 1), ResultContext::None)
1762                    .await
1763            })
1764        };
1765
1766        let mut wire = BufReader::new(&mut server.write_stdout);
1767        let opened = read_framed_message(&mut wire).await;
1768        assert_eq!(opened["method"], "textDocument/didOpen");
1769        let request = read_framed_message(&mut wire).await;
1770        assert_eq!(request["method"], "textDocument/declaration");
1771
1772        write_response(
1773            &mut server.read_half_stdin,
1774            &request["id"],
1775            serde_json::json!([{
1776                "uri": decl_uri,
1777                "range": {
1778                    "start": {"line": 0, "character": 3},
1779                    "end": {"line": 0, "character": 7}
1780                }
1781            }]),
1782        )
1783        .await;
1784
1785        let result = timeout(Duration::from_secs(2), handle)
1786            .await
1787            .expect("handle_declaration should not hang")
1788            .unwrap()
1789            .unwrap();
1790
1791        assert_eq!(result.locations.len(), 1);
1792        assert_eq!(result.locations[0].uri, decl_uri);
1793    }
1794
1795    /// `handle_declaration` maps `DeclarationLink[]` through `targetSelectionRange`.
1796    #[tokio::test]
1797    async fn test_handle_declaration_flattens_link_list() {
1798        let dir = TempDir::new().unwrap();
1799        let server_id = ServerId::from_static("rust");
1800        let caps = lsp_types::ServerCapabilities {
1801            declaration_provider: Some(lsp_types::DeclarationProvider::Bool(true)),
1802            ..Default::default()
1803        };
1804        let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
1805
1806        let path = dir.path().join("main.rs");
1807        fs::write(&path, "fn main() {}").unwrap();
1808        let target_path = dir.path().join("target.h");
1809        fs::write(&target_path, "struct TargetType;").unwrap();
1810        let target_uri = Url::from_file_path(&target_path).unwrap().to_string();
1811
1812        let translator = Arc::new(translator);
1813        let handle = {
1814            let translator = Arc::clone(&translator);
1815            let path = client_path(&path);
1816            tokio::spawn(async move {
1817                translator
1818                    .handle_declaration(path, pos(1, 1), ResultContext::None)
1819                    .await
1820            })
1821        };
1822
1823        let mut wire = BufReader::new(&mut server.write_stdout);
1824        let _opened = read_framed_message(&mut wire).await;
1825        let request = read_framed_message(&mut wire).await;
1826        assert_eq!(request["method"], "textDocument/declaration");
1827
1828        write_response(
1829            &mut server.read_half_stdin,
1830            &request["id"],
1831            serde_json::json!([{
1832                "targetUri": target_uri,
1833                "targetRange": {
1834                    "start": {"line": 0, "character": 0},
1835                    "end": {"line": 0, "character": 18}
1836                },
1837                "targetSelectionRange": {
1838                    "start": {"line": 0, "character": 7},
1839                    "end": {"line": 0, "character": 17}
1840                }
1841            }]),
1842        )
1843        .await;
1844
1845        let result = timeout(Duration::from_secs(2), handle)
1846            .await
1847            .expect("handle_declaration should not hang")
1848            .unwrap()
1849            .unwrap();
1850
1851        assert_eq!(result.locations.len(), 1);
1852        assert_eq!(result.locations[0].range.start.character, 8);
1853    }
1854
1855    /// An empty declaration answer is an empty result, not an error (FR-003).
1856    #[tokio::test]
1857    async fn test_handle_declaration_null_response_is_empty() {
1858        let dir = TempDir::new().unwrap();
1859        let server_id = ServerId::from_static("rust");
1860        let caps = lsp_types::ServerCapabilities {
1861            declaration_provider: Some(lsp_types::DeclarationProvider::Bool(true)),
1862            ..Default::default()
1863        };
1864        let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
1865
1866        let path = dir.path().join("main.rs");
1867        fs::write(&path, "fn main() {}").unwrap();
1868
1869        let translator = Arc::new(translator);
1870        let handle = {
1871            let translator = Arc::clone(&translator);
1872            let path = client_path(&path);
1873            tokio::spawn(async move {
1874                translator
1875                    .handle_declaration(path, pos(1, 1), ResultContext::None)
1876                    .await
1877            })
1878        };
1879
1880        let mut wire = BufReader::new(&mut server.write_stdout);
1881        let _opened = read_framed_message(&mut wire).await;
1882        let request = read_framed_message(&mut wire).await;
1883        write_response(
1884            &mut server.read_half_stdin,
1885            &request["id"],
1886            serde_json::Value::Null,
1887        )
1888        .await;
1889
1890        let result = timeout(Duration::from_secs(2), handle)
1891            .await
1892            .expect("handle_declaration should not hang")
1893            .unwrap()
1894            .unwrap();
1895        assert!(result.locations.is_empty());
1896    }
1897
1898    /// `handle_declaration` is gated by the indexing readiness check like definition.
1899    #[tokio::test(start_paused = true)]
1900    async fn test_handle_declaration_returns_workspace_indexing_error_when_loading() {
1901        let dir = TempDir::new().unwrap();
1902        let server_id = ServerId::from_static("rust");
1903        let caps = lsp_types::ServerCapabilities {
1904            declaration_provider: Some(lsp_types::DeclarationProvider::Bool(true)),
1905            ..Default::default()
1906        };
1907        let (translator, _server) = translator_with_capabilities(&dir, &server_id, caps);
1908
1909        let cache = Arc::new(Mutex::new(NotificationCache::new()));
1910        cache.lock().await.observe_indexing_signal(
1911            &server_id,
1912            "experimental/serverStatus",
1913            Some(&serde_json::json!({"quiescent": false})),
1914        );
1915        let translator = translator.with_notification_cache(cache);
1916
1917        let path = dir.path().join("main.rs");
1918        fs::write(&path, "fn main() {}").unwrap();
1919
1920        let err = translator
1921            .handle_declaration(client_path(&path), pos(1, 1), ResultContext::None)
1922            .await
1923            .unwrap_err();
1924
1925        assert_matches!(
1926            err,
1927            Error::WorkspaceIndexing { server_id: id, .. } if id == server_id
1928        );
1929    }
1930
1931    /// Success-path coverage for `handle_type_definition` through the
1932    /// `DefinitionLinkList` -> `GotoKind::DefinitionLinkList` arm, pinning
1933    /// the `GotoResponse` impl for `TypeDefinitionResponse` and the
1934    /// `definition_link_to_location` mapping (`target_selection_range`, not
1935    /// `target_range`).
1936    #[tokio::test]
1937    async fn test_handle_type_definition_flattens_definition_link_list() {
1938        let dir = TempDir::new().unwrap();
1939        let server_id = ServerId::from_static("rust");
1940        let caps = lsp_types::ServerCapabilities {
1941            type_definition_provider: Some(lsp_types::TypeDefinitionProvider::Bool(true)),
1942            ..Default::default()
1943        };
1944        let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
1945
1946        let path = dir.path().join("main.rs");
1947        fs::write(&path, "fn main() {}").unwrap();
1948        let target_path = dir.path().join("target_type.rs");
1949        fs::write(&target_path, "struct TargetType;").unwrap();
1950        let target_uri = Url::from_file_path(&target_path).unwrap().to_string();
1951
1952        let translator = Arc::new(translator);
1953        let handle = {
1954            let translator = Arc::clone(&translator);
1955            let path = path.to_string_lossy().to_string();
1956            tokio::spawn(async move {
1957                translator
1958                    .handle_type_definition(
1959                        client_path(path),
1960                        Position::at(1, 1),
1961                        ResultContext::None,
1962                    )
1963                    .await
1964            })
1965        };
1966
1967        let mut wire = BufReader::new(&mut server.write_stdout);
1968        let opened = read_framed_message(&mut wire).await;
1969        assert_eq!(opened["method"], "textDocument/didOpen");
1970        let request = read_framed_message(&mut wire).await;
1971        assert_eq!(request["method"], "textDocument/typeDefinition");
1972
1973        write_response(
1974            &mut server.read_half_stdin,
1975            &request["id"],
1976            serde_json::json!([{
1977                "targetUri": target_uri,
1978                "targetRange": {
1979                    "start": {"line": 0, "character": 0},
1980                    "end": {"line": 0, "character": 18}
1981                },
1982                "targetSelectionRange": {
1983                    "start": {"line": 0, "character": 7},
1984                    "end": {"line": 0, "character": 17}
1985                }
1986            }]),
1987        )
1988        .await;
1989
1990        let result = timeout(Duration::from_secs(2), handle)
1991            .await
1992            .expect("handle_type_definition should not hang")
1993            .unwrap()
1994            .unwrap();
1995
1996        assert_eq!(result.locations.len(), 1);
1997        assert_eq!(result.locations[0].uri, target_uri);
1998        assert_eq!(result.locations[0].range.start.character, 8);
1999    }
2000
2001    // -----------------------------------------------------------------
2002    // Resource-amplification defenses (#474)
2003    // -----------------------------------------------------------------
2004
2005    /// Regression for #474's exact attack scenario: many `Location`s
2006    /// clustered onto a handful of distinct `(file, line)` pairs must cost
2007    /// one disk read per distinct pair, not one per location. Proven through
2008    /// the real `lsp_locations_to_mcp` entry point shared by `handle_goto`
2009    /// and `handle_references` -- not by calling the cache-backed helper
2010    /// directly -- and via the cache's own size, which is a direct count of
2011    /// how many times the disk-read fallback actually ran.
2012    #[tokio::test]
2013    async fn test_lsp_locations_to_mcp_reads_disk_once_per_distinct_file_line() {
2014        let dir = TempDir::new().unwrap();
2015        let mut uris = Vec::new();
2016        for i in 0..3 {
2017            let path = dir.path().join(format!("file{i}.rs"));
2018            fs::write(&path, "hello").unwrap();
2019            uris.push(path_to_uri(&path).unwrap());
2020        }
2021
2022        let ctx = test_ctx_with(PositionEncoding::Utf8);
2023        // 300 locations, but only 3 distinct (file, line) pairs.
2024        let locs: Vec<lsp_types::Location> = (0..300)
2025            .map(|i| lsp_types::Location {
2026                uri: uris[i % 3].clone(),
2027                range: lsp_types::Range {
2028                    start: lsp_types::Position {
2029                        line: 0,
2030                        character: 0,
2031                    },
2032                    end: lsp_types::Position {
2033                        line: 0,
2034                        character: 3,
2035                    },
2036                },
2037            })
2038            .collect();
2039
2040        let result = lsp_locations_to_mcp(locs, &ctx).await;
2041
2042        assert_eq!(result.locations.len(), 300);
2043        assert!(!result.truncated);
2044        assert!(
2045            result.locations.iter().all(|l| l.range.end.character == 4),
2046            "MCP columns are 1-based, so LSP byte offset 3 in all-ASCII \"hello\" must convert \
2047             to 4"
2048        );
2049        assert_eq!(
2050            lock_std(&ctx.line_cache).entries.len(),
2051            3,
2052            "300 locations across 3 distinct files must populate the cache with exactly 3 \
2053             entries (one disk read per distinct file/line), not one per location"
2054        );
2055    }
2056
2057    /// Regression for #474: without a cap, a response naming an unbounded
2058    /// number of locations would drive an unbounded number of range
2059    /// conversions. `Utf16` needs no disk read at all (see `test_ctx`), so
2060    /// this isolates the truncation itself from I/O cost -- a response well
2061    /// past `MAX_NORMALIZED_LOCATIONS` must be truncated to it, not hang,
2062    /// OOM, or panic.
2063    #[tokio::test]
2064    async fn test_lsp_locations_to_mcp_truncates_to_max_normalized_locations() {
2065        let ctx = test_ctx();
2066        let uri = test_uri();
2067        let locs: Vec<lsp_types::Location> = (0..MAX_NORMALIZED_LOCATIONS + 500)
2068            .map(|_| lsp_types::Location {
2069                uri: uri.clone(),
2070                range: lsp_types::Range {
2071                    start: lsp_types::Position {
2072                        line: 0,
2073                        character: 0,
2074                    },
2075                    end: lsp_types::Position {
2076                        line: 0,
2077                        character: 1,
2078                    },
2079                },
2080            })
2081            .collect();
2082
2083        let result = lsp_locations_to_mcp(locs, &ctx).await;
2084
2085        assert_eq!(result.locations.len(), MAX_NORMALIZED_LOCATIONS);
2086        assert!(
2087            result.truncated,
2088            "a response naming more than MAX_NORMALIZED_LOCATIONS must report truncated: true"
2089        );
2090    }
2091
2092    #[test]
2093    fn test_item_budget_admit_spends_shared_allowance() {
2094        let mut budget = ItemBudget::new();
2095        let first = budget.admit(vec![0u8; MAX_NORMALIZED_LOCATIONS - 1]);
2096        assert_eq!(first.len(), MAX_NORMALIZED_LOCATIONS - 1);
2097        assert!(!budget.truncated());
2098
2099        let second = budget.admit(vec![0u8; 5]);
2100        assert_eq!(second.len(), 1);
2101        assert!(budget.truncated());
2102
2103        assert_eq!(budget.admit(vec![0u8; 3]).len(), 0);
2104    }
2105
2106    #[test]
2107    fn test_item_budget_admit_whole_never_splits_a_list() {
2108        let mut budget = ItemBudget::new();
2109        assert!(
2110            budget
2111                .admit_whole(vec![0u8; MAX_NORMALIZED_LOCATIONS - 2])
2112                .is_some()
2113        );
2114        assert!(budget.admit_whole(vec![0u8; 3]).is_none());
2115        assert!(budget.truncated());
2116        assert!(budget.admit_whole(vec![0u8; 2]).is_some());
2117    }
2118
2119    #[test]
2120    fn test_item_budget_with_cap_admits_a_lazy_sequence_up_to_the_cap() {
2121        let mut budget = ItemBudget::with_cap(3);
2122        assert_eq!(budget.admit_iter(0..3), [0, 1, 2]);
2123        assert!(!budget.truncated());
2124
2125        let mut budget = ItemBudget::with_cap(3);
2126        assert_eq!(budget.admit_iter(0..), [0, 1, 2]);
2127        assert!(budget.truncated());
2128        assert!(budget.admit_iter(0..2).is_empty());
2129    }
2130
2131    #[test]
2132    fn test_item_budget_exactly_at_cap_is_not_truncated() {
2133        let mut budget = ItemBudget::new();
2134        let items = budget.admit(vec![0u8; MAX_NORMALIZED_LOCATIONS]);
2135        assert_eq!(items.len(), MAX_NORMALIZED_LOCATIONS);
2136        assert!(!budget.truncated());
2137    }
2138}