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    HoverResult, IndexCoverage, Location, LocationsResult, Position, PositionDegradation,
12    ReferencesResult,
13};
14use super::enclosing::{Contextualized, ResultContext};
15use super::encoding_ctx::EncodingCtx;
16use super::positioned::{FromPosition, GotoRequest, 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    ) -> Result<NormalizedLocations>
524    where
525        R: GotoRequest + lsp_types::Request<Result = Option<T>>,
526        R::Params: FromPosition<Extra = ()>,
527        T: GotoResponse,
528    {
529        let Positioned {
530            result: response,
531            ctx,
532            doc: _doc,
533        } = self
534            .position_request::<R>(
535                file_path,
536                position,
537                IndexingGate::Required(R::CAPABILITY),
538                (),
539            )
540            .await?;
541
542        Ok(goto_response_to_locations(response, &ctx).await)
543    }
544
545    /// Handle definition request.
546    ///
547    /// # Errors
548    ///
549    /// Returns an error if the LSP request fails, the file cannot be opened,
550    /// the routed server does not advertise `definitionProvider` support, or
551    /// the server is still indexing the workspace after
552    /// `INDEXING_READY_TIMEOUT`.
553    pub async fn handle_definition(
554        &self,
555        file_path: ClientPath,
556        position: Position,
557        context: ResultContext,
558    ) -> Result<LocationsResult> {
559        let normalized = self
560            .handle_goto::<lsp_types::DefinitionRequest, _>(&file_path, position)
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,
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        let result = self.contextualize_into(normalized, context).await;
597        let index_coverage = result
598            .locations
599            .is_empty()
600            .then(|| self.index_coverage(doc.server_id()));
601        Ok(ReferencesResult {
602            result,
603            index_coverage,
604        })
605    }
606
607    /// What an empty answer from `server` covers, from tracker state alone.
608    pub(super) fn index_coverage(&self, server: &ServerId) -> IndexCoverage {
609        let tracker = &self.document_tracker;
610        IndexCoverage {
611            synced_documents: u32::try_from(tracker.synced_document_count(server))
612                .unwrap_or(u32::MAX),
613            server_restarted: tracker.sync_history_forgotten(server),
614        }
615    }
616
617    /// Handle go-to-implementation request (`textDocument/implementation`).
618    ///
619    /// Returns the locations of trait method or interface member implementations.
620    ///
621    /// # Errors
622    ///
623    /// Returns an error if the LSP request fails, the file cannot be opened,
624    /// the routed server does not advertise `implementationProvider`
625    /// support, or the server is still indexing the workspace after
626    /// `INDEXING_READY_TIMEOUT`.
627    pub async fn handle_implementation(
628        &self,
629        file_path: ClientPath,
630        position: Position,
631        context: ResultContext,
632    ) -> Result<LocationsResult> {
633        let normalized = self
634            .handle_goto::<lsp_types::ImplementationRequest, _>(&file_path, position)
635            .await?;
636        Ok(self.contextualize_into(normalized, context).await)
637    }
638
639    /// Handle go-to-type-definition request (`textDocument/typeDefinition`).
640    ///
641    /// Returns the type definition location of the expression at position. Distinct
642    /// from go-to-definition for variable bindings where definition and type differ.
643    ///
644    /// # Errors
645    ///
646    /// Returns an error if the LSP request fails, the file cannot be opened,
647    /// the routed server does not advertise `typeDefinitionProvider`
648    /// support, or the server is still indexing the workspace after
649    /// `INDEXING_READY_TIMEOUT`.
650    pub async fn handle_type_definition(
651        &self,
652        file_path: ClientPath,
653        position: Position,
654        context: ResultContext,
655    ) -> Result<LocationsResult> {
656        let normalized = self
657            .handle_goto::<lsp_types::TypeDefinitionRequest, _>(&file_path, position)
658            .await?;
659        Ok(self.contextualize_into(normalized, context).await)
660    }
661
662    /// Handle go-to-declaration request (`textDocument/declaration`).
663    ///
664    /// Returns the declaration location of the symbol at position. Differs
665    /// from go-to-definition for languages that separate declaration from
666    /// definition (C/C++ headers, interface members).
667    ///
668    /// # Errors
669    ///
670    /// Returns an error if the LSP request fails, the file cannot be opened,
671    /// the routed server does not advertise `declarationProvider` support, or
672    /// the server is still indexing the workspace after
673    /// `INDEXING_READY_TIMEOUT`.
674    pub async fn handle_declaration(
675        &self,
676        file_path: ClientPath,
677        position: Position,
678        context: ResultContext,
679    ) -> Result<LocationsResult> {
680        let normalized = self
681            .handle_goto::<lsp_types::DeclarationRequest, _>(&file_path, position)
682            .await?;
683        Ok(self.contextualize_into(normalized, context).await)
684    }
685}
686
687#[cfg(test)]
688#[allow(deprecated, reason = "LSP servers still send this deprecated field")]
689mod tests {
690    use std::sync::Arc;
691    use std::time::Duration;
692    use std::{assert_matches, fs};
693
694    use tempfile::TempDir;
695    use tokio::io::BufReader;
696    use tokio::sync::Mutex;
697    use tokio::time::timeout;
698    use url::Url;
699
700    use super::*;
701    use crate::bridge::encoding::PositionEncoding;
702    use crate::bridge::translator::testing::*;
703    use crate::bridge::{NotificationCache, path_to_uri};
704    use crate::config::{IndexingReadyTimeoutSecs, ServerId};
705    use crate::test_lsp::client_path;
706    use crate::util::lock_std;
707
708    // -----------------------------------------------------------------
709    // Indexing readiness gate (`Translator::wait_for_indexing_ready`)
710    // -----------------------------------------------------------------
711
712    #[tokio::test]
713    async fn test_wait_for_indexing_ready_without_cache_is_noop() {
714        // No wired cache (most fixtures) must never block -- see `Translator::notification_cache`'s field doc.
715        let translator = Translator::new();
716        let server_id = ServerId::from_static("rust");
717
718        translator
719            .wait_for_indexing_ready(&server_id)
720            .await
721            .unwrap();
722    }
723
724    #[tokio::test]
725    async fn test_wait_for_indexing_ready_unknown_state_is_noop() {
726        let translator = Translator::new()
727            .with_notification_cache(Arc::new(Mutex::new(NotificationCache::new())));
728        let server_id = ServerId::from_static("rust");
729
730        translator
731            .wait_for_indexing_ready(&server_id)
732            .await
733            .unwrap();
734    }
735
736    #[tokio::test]
737    async fn test_wait_for_indexing_ready_ready_state_is_noop() {
738        let cache = Arc::new(Mutex::new(NotificationCache::new()));
739        let server_id = ServerId::from_static("rust");
740        cache.lock().await.observe_indexing_signal(
741            &server_id,
742            "experimental/serverStatus",
743            Some(&serde_json::json!({"quiescent": true})),
744        );
745        let translator = Translator::new().with_notification_cache(cache);
746
747        translator
748            .wait_for_indexing_ready(&server_id)
749            .await
750            .unwrap();
751    }
752
753    /// #424: `with_indexing_ready_timeout` must actually change the bound
754    /// `wait_for_indexing_ready` (the public entry point, not the
755    /// timeout-injectable `_with` test helper) waits before giving up --
756    /// pins the config wiring end-to-end rather than only the constructor
757    /// storing the value.
758    #[tokio::test(start_paused = true)]
759    async fn test_wait_for_indexing_ready_uses_configured_timeout_override() {
760        let cache = Arc::new(Mutex::new(NotificationCache::new()));
761        let server_id = ServerId::from_static("rust");
762        cache.lock().await.observe_indexing_signal(
763            &server_id,
764            "experimental/serverStatus",
765            Some(&serde_json::json!({"quiescent": false})),
766        );
767        let translator = Translator::new()
768            .with_notification_cache(cache)
769            .with_indexing_ready_timeout(IndexingReadyTimeoutSecs::new(5).unwrap());
770
771        let start = Instant::now();
772        let err = translator
773            .wait_for_indexing_ready(&server_id)
774            .await
775            .unwrap_err();
776
777        assert_matches!(err, Error::WorkspaceIndexing { elapsed_secs, .. } if elapsed_secs == 5);
778        assert_eq!(start.elapsed(), Duration::from_secs(5));
779    }
780
781    #[tokio::test]
782    async fn test_wait_for_indexing_ready_loading_times_out() {
783        let cache = Arc::new(Mutex::new(NotificationCache::new()));
784        let server_id = ServerId::from_static("rust");
785        cache.lock().await.observe_indexing_signal(
786            &server_id,
787            "experimental/serverStatus",
788            Some(&serde_json::json!({"quiescent": false})),
789        );
790        let translator = Translator::new().with_notification_cache(cache);
791
792        let err = translator
793            .wait_for_indexing_ready_with(
794                &server_id,
795                Duration::from_millis(50),
796                Duration::from_millis(10),
797            )
798            .await
799            .unwrap_err();
800
801        assert_matches!(
802            err,
803            Error::WorkspaceIndexing { server_id: id, .. } if id == ServerId::from_static("rust")
804        );
805    }
806
807    #[tokio::test]
808    async fn test_wait_for_indexing_ready_returns_ok_once_signaled_ready() {
809        let cache = Arc::new(Mutex::new(NotificationCache::new()));
810        let server_id = ServerId::from_static("rust");
811        cache.lock().await.observe_indexing_signal(
812            &server_id,
813            "experimental/serverStatus",
814            Some(&serde_json::json!({"quiescent": false})),
815        );
816        let translator = Translator::new().with_notification_cache(Arc::clone(&cache));
817
818        let waiter = {
819            let server_id = server_id.clone();
820            tokio::spawn(async move {
821                translator
822                    .wait_for_indexing_ready_with(
823                        &server_id,
824                        Duration::from_secs(5),
825                        Duration::from_millis(10),
826                    )
827                    .await
828            })
829        };
830
831        tokio::time::sleep(Duration::from_millis(30)).await;
832        cache.lock().await.observe_indexing_signal(
833            &server_id,
834            "experimental/serverStatus",
835            Some(&serde_json::json!({"quiescent": true})),
836        );
837
838        timeout(Duration::from_secs(1), waiter)
839            .await
840            .expect("waiter task timed out")
841            .expect("waiter task panicked")
842            .expect("expected Ok once quiescent");
843    }
844
845    /// End-to-end: a real handler (`handle_hover`) must surface
846    /// `Error::WorkspaceIndexing` -- not an empty/`null` result -- when the
847    /// routed server is still `Loading`, without ever reaching the fake LSP
848    /// server. Runs under paused virtual time so it does not actually wait
849    /// out the real `INDEXING_READY_TIMEOUT`.
850    #[tokio::test(start_paused = true)]
851    async fn test_handle_hover_returns_workspace_indexing_error_when_loading() {
852        let dir = TempDir::new().unwrap();
853        let server_id = ServerId::from_static("rust");
854        let caps = lsp_types::ServerCapabilities {
855            hover_provider: Some(lsp_types::HoverProvider::Bool(true)),
856            ..Default::default()
857        };
858        let (translator, _server) = translator_with_capabilities(&dir, &server_id, caps);
859
860        let cache = Arc::new(Mutex::new(NotificationCache::new()));
861        cache.lock().await.observe_indexing_signal(
862            &server_id,
863            "experimental/serverStatus",
864            Some(&serde_json::json!({"quiescent": false})),
865        );
866        let translator = translator.with_notification_cache(cache);
867
868        let path = dir.path().join("main.rs");
869        fs::write(&path, "fn main() {}").unwrap();
870
871        let err = translator
872            .handle_hover(client_path(path.to_string_lossy().into_owned()), pos(1, 1))
873            .await
874            .unwrap_err();
875
876        assert_matches!(
877            err,
878            Error::WorkspaceIndexing { server_id: id, elapsed_secs: 30 } if id == server_id
879        );
880    }
881
882    /// Companion to the timeout test above: when the cache reports `Ready`,
883    /// `handle_hover` must dispatch normally with no added delay.
884    #[tokio::test]
885    async fn test_handle_hover_dispatches_when_indexing_ready() {
886        let dir = TempDir::new().unwrap();
887        let server_id = ServerId::from_static("rust");
888        let caps = lsp_types::ServerCapabilities {
889            hover_provider: Some(lsp_types::HoverProvider::Bool(true)),
890            ..Default::default()
891        };
892        let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
893
894        let cache = Arc::new(Mutex::new(NotificationCache::new()));
895        cache.lock().await.observe_indexing_signal(
896            &server_id,
897            "experimental/serverStatus",
898            Some(&serde_json::json!({"quiescent": true})),
899        );
900        let translator = Arc::new(translator.with_notification_cache(cache));
901
902        let path = dir.path().join("main.rs");
903        fs::write(&path, "fn main() {}").unwrap();
904
905        let handle = {
906            let translator = Arc::clone(&translator);
907            let path = path.to_string_lossy().to_string();
908            tokio::spawn(async move { translator.handle_hover(client_path(path), pos(1, 1)).await })
909        };
910
911        let mut wire = BufReader::new(&mut server.write_stdout);
912        let opened = read_framed_message(&mut wire).await;
913        assert_eq!(opened["method"], "textDocument/didOpen");
914        let request = read_framed_message(&mut wire).await;
915        assert_eq!(request["method"], "textDocument/hover");
916
917        write_response(
918            &mut server.read_half_stdin,
919            &request["id"],
920            serde_json::json!({
921                "contents": {"kind": "markdown", "value": "hover text"}
922            }),
923        )
924        .await;
925
926        let result = handle.await.unwrap().unwrap();
927        assert_eq!(result.contents, "hover text");
928    }
929
930    /// #518: a hover that times out while awaiting the server's reply must
931    /// leave the bridge usable -- the document stays tracked, so the next
932    /// hover goes straight to the request without a second `didOpen`.
933    #[tokio::test(start_paused = true)]
934    async fn test_handle_hover_timeout_keeps_document_open_for_next_request() {
935        let dir = TempDir::new().unwrap();
936        let server_id = ServerId::from_static("rust");
937        let caps = lsp_types::ServerCapabilities {
938            hover_provider: Some(lsp_types::HoverProvider::Bool(true)),
939            ..Default::default()
940        };
941        let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
942
943        let cache = Arc::new(Mutex::new(NotificationCache::new()));
944        cache.lock().await.observe_indexing_signal(
945            &server_id,
946            "experimental/serverStatus",
947            Some(&serde_json::json!({"quiescent": true})),
948        );
949        let translator = Arc::new(translator.with_notification_cache(cache));
950
951        let path = dir.path().join("main.rs");
952        fs::write(&path, "fn main() {}").unwrap();
953        let path = path.to_string_lossy().to_string();
954
955        let spawn_hover = || {
956            let translator = Arc::clone(&translator);
957            let path = path.clone();
958            tokio::spawn(async move { translator.handle_hover(client_path(path), pos(1, 1)).await })
959        };
960
961        let mut wire = BufReader::new(&mut server.write_stdout);
962
963        let first = spawn_hover();
964        let opened = read_framed_message(&mut wire).await;
965        assert_eq!(opened["method"], "textDocument/didOpen");
966        let first_request = read_framed_message(&mut wire).await;
967        assert_eq!(first_request["method"], "textDocument/hover");
968
969        let timeout_secs = crate::config::LspServerConfig::rust_analyzer().request_timeout_seconds;
970        tokio::time::advance(Duration::from_secs(timeout_secs.get() + 1)).await;
971        assert_matches!(first.await.unwrap().unwrap_err(), Error::Timeout(_));
972        let cancel = read_framed_message(&mut wire).await;
973        assert_eq!(cancel["method"], "$/cancelRequest");
974        assert_eq!(cancel["params"]["id"], first_request["id"]);
975
976        let second = spawn_hover();
977        let second_request = read_framed_message(&mut wire).await;
978        assert_eq!(
979            second_request["method"], "textDocument/hover",
980            "a timed-out hover must not cause the document to be re-opened"
981        );
982        write_response(
983            &mut server.read_half_stdin,
984            &second_request["id"],
985            serde_json::json!({"contents": {"kind": "markdown", "value": "after timeout"}}),
986        )
987        .await;
988
989        assert_eq!(second.await.unwrap().unwrap().contents, "after timeout");
990    }
991
992    /// End-to-end: `handle_definition` must surface `Error::WorkspaceIndexing`
993    /// while the routed server is still `Loading`, without reaching the fake
994    /// LSP server.
995    #[tokio::test(start_paused = true)]
996    async fn test_handle_definition_returns_workspace_indexing_error_when_loading() {
997        let dir = TempDir::new().unwrap();
998        let server_id = ServerId::from_static("rust");
999        let caps = lsp_types::ServerCapabilities {
1000            definition_provider: Some(lsp_types::DefinitionProvider::Bool(true)),
1001            ..Default::default()
1002        };
1003        let (translator, _server) = translator_with_capabilities(&dir, &server_id, caps);
1004
1005        let cache = Arc::new(Mutex::new(NotificationCache::new()));
1006        cache.lock().await.observe_indexing_signal(
1007            &server_id,
1008            "experimental/serverStatus",
1009            Some(&serde_json::json!({"quiescent": false})),
1010        );
1011        let translator = translator.with_notification_cache(cache);
1012
1013        let path = dir.path().join("main.rs");
1014        fs::write(&path, "fn main() {}").unwrap();
1015
1016        let err = translator
1017            .handle_definition(
1018                client_path(path.to_string_lossy().into_owned()),
1019                pos(1, 1),
1020                ResultContext::None,
1021            )
1022            .await
1023            .unwrap_err();
1024
1025        assert_matches!(
1026            err,
1027            Error::WorkspaceIndexing { server_id: id, .. } if id == server_id
1028        );
1029    }
1030
1031    /// Companion: when the cache reports `Ready`, `handle_definition` must
1032    /// dispatch normally.
1033    #[tokio::test]
1034    async fn test_handle_definition_dispatches_when_indexing_ready() {
1035        let dir = TempDir::new().unwrap();
1036        let server_id = ServerId::from_static("rust");
1037        let caps = lsp_types::ServerCapabilities {
1038            definition_provider: Some(lsp_types::DefinitionProvider::Bool(true)),
1039            ..Default::default()
1040        };
1041        let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
1042
1043        let cache = Arc::new(Mutex::new(NotificationCache::new()));
1044        cache.lock().await.observe_indexing_signal(
1045            &server_id,
1046            "experimental/serverStatus",
1047            Some(&serde_json::json!({"quiescent": true})),
1048        );
1049        let translator = Arc::new(translator.with_notification_cache(cache));
1050
1051        let path = dir.path().join("main.rs");
1052        fs::write(&path, "fn main() {}").unwrap();
1053
1054        let handle = {
1055            let translator = Arc::clone(&translator);
1056            let path = path.to_string_lossy().to_string();
1057            tokio::spawn(async move {
1058                translator
1059                    .handle_definition(client_path(path), pos(1, 1), ResultContext::None)
1060                    .await
1061            })
1062        };
1063
1064        let mut wire = BufReader::new(&mut server.write_stdout);
1065        let opened = read_framed_message(&mut wire).await;
1066        assert_eq!(opened["method"], "textDocument/didOpen");
1067        let request = read_framed_message(&mut wire).await;
1068        assert_eq!(request["method"], "textDocument/definition");
1069
1070        write_response(
1071            &mut server.read_half_stdin,
1072            &request["id"],
1073            serde_json::Value::Null,
1074        )
1075        .await;
1076
1077        let result = handle.await.unwrap().unwrap();
1078        assert!(result.locations.is_empty());
1079    }
1080
1081    /// End-to-end: `handle_references` must surface `Error::WorkspaceIndexing`
1082    /// while the routed server is still `Loading`, without reaching the fake
1083    /// LSP server.
1084    #[tokio::test(start_paused = true)]
1085    async fn test_handle_references_returns_workspace_indexing_error_when_loading() {
1086        let dir = TempDir::new().unwrap();
1087        let server_id = ServerId::from_static("rust");
1088        let caps = lsp_types::ServerCapabilities {
1089            references_provider: Some(lsp_types::ReferencesProvider::Bool(true)),
1090            ..Default::default()
1091        };
1092        let (translator, _server) = translator_with_capabilities(&dir, &server_id, caps);
1093
1094        let cache = Arc::new(Mutex::new(NotificationCache::new()));
1095        cache.lock().await.observe_indexing_signal(
1096            &server_id,
1097            "experimental/serverStatus",
1098            Some(&serde_json::json!({"quiescent": false})),
1099        );
1100        let translator = translator.with_notification_cache(cache);
1101
1102        let path = dir.path().join("main.rs");
1103        fs::write(&path, "fn main() {}").unwrap();
1104
1105        let err = translator
1106            .handle_references(
1107                client_path(path.to_string_lossy().into_owned()),
1108                pos(1, 1),
1109                true,
1110                ResultContext::None,
1111            )
1112            .await
1113            .unwrap_err();
1114
1115        assert_matches!(
1116            err,
1117            Error::WorkspaceIndexing { server_id: id, .. } if id == server_id
1118        );
1119    }
1120
1121    /// Companion: when the cache reports `Ready`, `handle_references` must
1122    /// dispatch normally.
1123    #[tokio::test]
1124    async fn test_handle_references_dispatches_when_indexing_ready() {
1125        let dir = TempDir::new().unwrap();
1126        let server_id = ServerId::from_static("rust");
1127        let caps = lsp_types::ServerCapabilities {
1128            references_provider: Some(lsp_types::ReferencesProvider::Bool(true)),
1129            ..Default::default()
1130        };
1131        let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
1132
1133        let cache = Arc::new(Mutex::new(NotificationCache::new()));
1134        cache.lock().await.observe_indexing_signal(
1135            &server_id,
1136            "experimental/serverStatus",
1137            Some(&serde_json::json!({"quiescent": true})),
1138        );
1139        let translator = Arc::new(translator.with_notification_cache(cache));
1140
1141        let path = dir.path().join("main.rs");
1142        fs::write(&path, "fn main() {}").unwrap();
1143
1144        let handle = {
1145            let translator = Arc::clone(&translator);
1146            let path = path.to_string_lossy().to_string();
1147            tokio::spawn(async move {
1148                translator
1149                    .handle_references(client_path(path), pos(1, 1), true, ResultContext::None)
1150                    .await
1151            })
1152        };
1153
1154        let mut wire = BufReader::new(&mut server.write_stdout);
1155        let opened = read_framed_message(&mut wire).await;
1156        assert_eq!(opened["method"], "textDocument/didOpen");
1157        let request = read_framed_message(&mut wire).await;
1158        assert_eq!(request["method"], "textDocument/references");
1159
1160        write_response(
1161            &mut server.read_half_stdin,
1162            &request["id"],
1163            serde_json::Value::Null,
1164        )
1165        .await;
1166
1167        let result = handle.await.unwrap().unwrap();
1168        assert!(result.result.locations.is_empty());
1169        assert_eq!(
1170            result.index_coverage,
1171            Some(IndexCoverage {
1172                synced_documents: 1,
1173                server_restarted: false,
1174            })
1175        );
1176        let json = serde_json::to_value(&result).unwrap();
1177        assert_eq!(
1178            json["index_coverage"],
1179            serde_json::json!({"synced_documents": 1})
1180        );
1181
1182        translator.document_tracker.forget_server(&server_id);
1183        assert_eq!(
1184            translator.index_coverage(&server_id),
1185            IndexCoverage {
1186                synced_documents: 0,
1187                server_restarted: true,
1188            }
1189        );
1190    }
1191
1192    /// S3 fix: `handle_implementation` shares `handle_goto` with
1193    /// `handle_definition` and must now be gated the same way --
1194    /// `textDocument/implementation` needs the whole-crate trait-impl index,
1195    /// which is at least as index-dependent as `definition`.
1196    #[tokio::test(start_paused = true)]
1197    async fn test_handle_implementation_returns_workspace_indexing_error_when_loading() {
1198        let dir = TempDir::new().unwrap();
1199        let server_id = ServerId::from_static("rust");
1200        let caps = lsp_types::ServerCapabilities {
1201            implementation_provider: Some(lsp_types::ImplementationProvider::Bool(true)),
1202            ..Default::default()
1203        };
1204        let (translator, _server) = translator_with_capabilities(&dir, &server_id, caps);
1205
1206        let cache = Arc::new(Mutex::new(NotificationCache::new()));
1207        cache.lock().await.observe_indexing_signal(
1208            &server_id,
1209            "experimental/serverStatus",
1210            Some(&serde_json::json!({"quiescent": false})),
1211        );
1212        let translator = translator.with_notification_cache(cache);
1213
1214        let path = dir.path().join("main.rs");
1215        fs::write(&path, "fn main() {}").unwrap();
1216
1217        let err = translator
1218            .handle_implementation(
1219                client_path(path.to_string_lossy().into_owned()),
1220                pos(1, 1),
1221                ResultContext::None,
1222            )
1223            .await
1224            .unwrap_err();
1225
1226        assert_matches!(
1227            err,
1228            Error::WorkspaceIndexing { server_id: id, .. } if id == server_id
1229        );
1230    }
1231
1232    /// S3 fix, companion for `handle_type_definition`.
1233    #[tokio::test(start_paused = true)]
1234    async fn test_handle_type_definition_returns_workspace_indexing_error_when_loading() {
1235        let dir = TempDir::new().unwrap();
1236        let server_id = ServerId::from_static("rust");
1237        let caps = lsp_types::ServerCapabilities {
1238            type_definition_provider: Some(lsp_types::TypeDefinitionProvider::Bool(true)),
1239            ..Default::default()
1240        };
1241        let (translator, _server) = translator_with_capabilities(&dir, &server_id, caps);
1242
1243        let cache = Arc::new(Mutex::new(NotificationCache::new()));
1244        cache.lock().await.observe_indexing_signal(
1245            &server_id,
1246            "experimental/serverStatus",
1247            Some(&serde_json::json!({"quiescent": false})),
1248        );
1249        let translator = translator.with_notification_cache(cache);
1250
1251        let path = dir.path().join("main.rs");
1252        fs::write(&path, "fn main() {}").unwrap();
1253
1254        let err = translator
1255            .handle_type_definition(
1256                client_path(path.to_string_lossy().into_owned()),
1257                pos(1, 1),
1258                ResultContext::None,
1259            )
1260            .await
1261            .unwrap_err();
1262
1263        assert_matches!(
1264            err,
1265            Error::WorkspaceIndexing { server_id: id, .. } if id == server_id
1266        );
1267    }
1268
1269    /// A timed-out wait must return `Error::WorkspaceIndexing` to its own
1270    /// caller without mutating the shared cache entry -- a fixed-in-review
1271    /// regression had the timeout handler reset the entry to `Unknown`,
1272    /// which released every other concurrent/later caller early (see
1273    /// `test_wait_for_indexing_ready_one_callers_timeout_does_not_release_another`
1274    /// for the direct reproduction). Self-healing for a genuinely stuck
1275    /// signal now lives entirely in
1276    /// `NotificationCache::indexing_state`'s own staleness check.
1277    #[tokio::test]
1278    async fn test_wait_for_indexing_ready_timeout_does_not_mutate_shared_state() {
1279        let cache = Arc::new(Mutex::new(NotificationCache::new()));
1280        let server_id = ServerId::from_static("rust");
1281        cache.lock().await.observe_indexing_signal(
1282            &server_id,
1283            "experimental/serverStatus",
1284            Some(&serde_json::json!({"quiescent": false})),
1285        );
1286        let translator = Translator::new().with_notification_cache(Arc::clone(&cache));
1287
1288        translator
1289            .wait_for_indexing_ready_with(
1290                &server_id,
1291                Duration::from_millis(30),
1292                Duration::from_millis(10),
1293            )
1294            .await
1295            .unwrap_err();
1296
1297        assert_eq!(
1298            cache.lock().await.indexing_state(&server_id),
1299            IndexingState::Loading,
1300            "a timed-out wait must not touch the shared entry -- it is still fresh, so it must \
1301             still read as Loading for any other caller"
1302        );
1303    }
1304
1305    /// Direct reproduction of the self-heal race: a short-timeout waiter's
1306    /// own timeout must never resolve a concurrent long-timeout waiter's
1307    /// independent wait early. Before the fix, both waiters observed the
1308    /// same shared `IndexingState`, and the short waiter's timeout handler
1309    /// reset it to `Unknown` as a side effect -- silently un-gating the
1310    /// long waiter tens of seconds before its own deadline.
1311    #[tokio::test]
1312    async fn test_wait_for_indexing_ready_one_callers_timeout_does_not_release_another() {
1313        let cache = Arc::new(Mutex::new(NotificationCache::new()));
1314        let server_id = ServerId::from_static("rust");
1315        cache.lock().await.observe_indexing_signal(
1316            &server_id,
1317            "experimental/serverStatus",
1318            Some(&serde_json::json!({"quiescent": false})),
1319        );
1320        let translator = Arc::new(Translator::new().with_notification_cache(Arc::clone(&cache)));
1321
1322        let short = {
1323            let translator = Arc::clone(&translator);
1324            let server_id = server_id.clone();
1325            tokio::spawn(async move {
1326                translator
1327                    .wait_for_indexing_ready_with(
1328                        &server_id,
1329                        Duration::from_millis(80),
1330                        Duration::from_millis(10),
1331                    )
1332                    .await
1333            })
1334        };
1335        let long = {
1336            let translator = Arc::clone(&translator);
1337            let server_id = server_id.clone();
1338            tokio::spawn(async move {
1339                translator
1340                    .wait_for_indexing_ready_with(
1341                        &server_id,
1342                        Duration::from_secs(30),
1343                        Duration::from_millis(10),
1344                    )
1345                    .await
1346            })
1347        };
1348
1349        let short_result = short.await.unwrap();
1350        assert_matches!(
1351            short_result,
1352            Err(Error::WorkspaceIndexing { .. }),
1353            "the short-timeout waiter must time out on its own schedule, got {short_result:?}"
1354        );
1355
1356        // Well past the short waiter's 80ms deadline, nowhere near the long
1357        // waiter's 30s one.
1358        tokio::time::sleep(Duration::from_millis(150)).await;
1359        assert!(
1360            !long.is_finished(),
1361            "a concurrent caller's short timeout must never resolve another caller's \
1362             independent wait early"
1363        );
1364        long.abort();
1365    }
1366
1367    #[test]
1368    fn test_extract_hover_contents_string() {
1369        let marked_string = lsp_types::MarkedString::String("Test hover".to_string());
1370        let contents = lsp_types::Contents::MarkedString(marked_string);
1371        let result = extract_hover_contents(contents);
1372        assert_eq!(result, "Test hover");
1373    }
1374
1375    #[test]
1376    fn test_extract_hover_contents_language_string() {
1377        let marked_string = lsp_types::MarkedString::MarkedStringWithLanguage(
1378            lsp_types::MarkedStringWithLanguage {
1379                language: "rust".to_string(),
1380                value: "fn main() {}".to_string(),
1381            },
1382        );
1383        let contents = lsp_types::Contents::MarkedString(marked_string);
1384        let result = extract_hover_contents(contents);
1385        assert_eq!(result, "```rust\nfn main() {}\n```");
1386    }
1387
1388    #[test]
1389    fn test_extract_hover_contents_markup() {
1390        let markup = lsp_types::MarkupContent {
1391            kind: lsp_types::MarkupKind::Markdown,
1392            value: "# Documentation".to_string(),
1393        };
1394        let contents = lsp_types::Contents::MarkupContent(markup);
1395        let result = extract_hover_contents(contents);
1396        assert_eq!(result, "# Documentation");
1397    }
1398
1399    /// Success-path coverage for `handle_definition` through the
1400    /// `Definition::Location` -> `GotoKind::Definition` arm, pinning the
1401    /// `GotoResponse` impl for `DefinitionResponse`.
1402    #[tokio::test]
1403    async fn test_handle_definition_flattens_single_location() {
1404        let dir = TempDir::new().unwrap();
1405        let server_id = ServerId::from_static("rust");
1406        let caps = lsp_types::ServerCapabilities {
1407            definition_provider: Some(lsp_types::DefinitionProvider::Bool(true)),
1408            ..Default::default()
1409        };
1410        let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
1411
1412        let path = canonical_dir(&dir).join("main.rs");
1413        fs::write(&path, "fn main() {}").unwrap();
1414        let target_path = canonical_dir(&dir).join("target.rs");
1415        fs::write(&target_path, "fn target() {}").unwrap();
1416        let target_uri = Url::from_file_path(&target_path).unwrap().to_string();
1417
1418        let translator = Arc::new(translator);
1419        let handle = {
1420            let translator = Arc::clone(&translator);
1421            let path = path.to_string_lossy().to_string();
1422            tokio::spawn(async move {
1423                translator
1424                    .handle_definition(client_path(path), Position::at(1, 1), ResultContext::None)
1425                    .await
1426            })
1427        };
1428
1429        let mut wire = BufReader::new(&mut server.write_stdout);
1430        let opened = read_framed_message(&mut wire).await;
1431        assert_eq!(opened["method"], "textDocument/didOpen");
1432        let request = read_framed_message(&mut wire).await;
1433        assert_eq!(request["method"], "textDocument/definition");
1434
1435        write_response(
1436            &mut server.read_half_stdin,
1437            &request["id"],
1438            serde_json::json!({
1439                "uri": target_uri,
1440                "range": {
1441                    "start": {"line": 0, "character": 0},
1442                    "end": {"line": 0, "character": 6}
1443                }
1444            }),
1445        )
1446        .await;
1447
1448        let result = timeout(Duration::from_secs(2), handle)
1449            .await
1450            .expect("handle_definition should not hang")
1451            .unwrap()
1452            .unwrap();
1453
1454        assert_eq!(result.locations.len(), 1);
1455        assert_eq!(result.locations[0].uri, target_uri);
1456        assert!(
1457            !result.locations[0].out_of_workspace,
1458            "a definition location inside the workspace root must not be marked out_of_workspace"
1459        );
1460    }
1461
1462    /// #415 (revised per critic C1): a definition location whose URI falls
1463    /// outside every configured workspace root must still be returned --
1464    /// goto-definition into the standard library or a crates.io dependency
1465    /// is normal, expected navigation, not an attack. The untrusted-URI
1466    /// concern is instead covered downstream, by the inbound
1467    /// `WorkspaceRoots::validate` gate any subsequent open/read of the
1468    /// path would hit.
1469    #[tokio::test]
1470    async fn test_handle_definition_does_not_filter_out_of_workspace_location() {
1471        let dir = TempDir::new().unwrap();
1472        let server_id = ServerId::from_static("rust");
1473        let caps = lsp_types::ServerCapabilities {
1474            definition_provider: Some(lsp_types::DefinitionProvider::Bool(true)),
1475            ..Default::default()
1476        };
1477        let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
1478
1479        let path = dir.path().join("main.rs");
1480        fs::write(&path, "fn main() {}").unwrap();
1481        let outside_uri = "file:///outside/workspace/stdlib.rs";
1482
1483        let translator = Arc::new(translator);
1484        let handle = {
1485            let translator = Arc::clone(&translator);
1486            let path = path.to_string_lossy().to_string();
1487            tokio::spawn(async move {
1488                translator
1489                    .handle_definition(client_path(path), Position::at(1, 1), ResultContext::None)
1490                    .await
1491            })
1492        };
1493
1494        let mut wire = BufReader::new(&mut server.write_stdout);
1495        let opened = read_framed_message(&mut wire).await;
1496        assert_eq!(opened["method"], "textDocument/didOpen");
1497        let request = read_framed_message(&mut wire).await;
1498        assert_eq!(request["method"], "textDocument/definition");
1499
1500        write_response(
1501            &mut server.read_half_stdin,
1502            &request["id"],
1503            serde_json::json!({
1504                "uri": outside_uri,
1505                "range": {
1506                    "start": {"line": 0, "character": 0},
1507                    "end": {"line": 0, "character": 6}
1508                }
1509            }),
1510        )
1511        .await;
1512
1513        let result = timeout(Duration::from_secs(2), handle)
1514            .await
1515            .expect("handle_definition should not hang")
1516            .unwrap()
1517            .unwrap();
1518
1519        assert_eq!(
1520            result.locations.len(),
1521            1,
1522            "an out-of-workspace definition location (e.g. stdlib/a dependency) must be \
1523             returned, not dropped"
1524        );
1525        assert_eq!(result.locations[0].uri, outside_uri);
1526        assert!(
1527            result.locations[0].out_of_workspace,
1528            "a definition location outside every workspace root must be marked out_of_workspace"
1529        );
1530    }
1531
1532    /// #415 (revised per critic C1) companion for `handle_references`: an
1533    /// out-of-workspace location must pass through unfiltered, same as an
1534    /// in-workspace one -- see `test_handle_definition_does_not_filter_out_of_workspace_location`.
1535    #[tokio::test]
1536    async fn test_handle_references_does_not_filter_out_of_workspace_location() {
1537        let dir = TempDir::new().unwrap();
1538        let server_id = ServerId::from_static("rust");
1539        let caps = lsp_types::ServerCapabilities {
1540            references_provider: Some(lsp_types::ReferencesProvider::Bool(true)),
1541            ..Default::default()
1542        };
1543        let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
1544
1545        let path = canonical_dir(&dir).join("main.rs");
1546        fs::write(&path, "fn main() {}").unwrap();
1547        let inside_path = canonical_dir(&dir).join("inside.rs");
1548        fs::write(&inside_path, "fn used() {}").unwrap();
1549        let inside_uri = Url::from_file_path(&inside_path).unwrap().to_string();
1550        let outside_uri = "file:///outside/workspace/stdlib.rs";
1551
1552        let translator = Arc::new(translator);
1553        let handle = {
1554            let translator = Arc::clone(&translator);
1555            let path = path.to_string_lossy().to_string();
1556            tokio::spawn(async move {
1557                translator
1558                    .handle_references(client_path(path), pos(1, 1), true, ResultContext::None)
1559                    .await
1560            })
1561        };
1562
1563        let mut wire = BufReader::new(&mut server.write_stdout);
1564        let opened = read_framed_message(&mut wire).await;
1565        assert_eq!(opened["method"], "textDocument/didOpen");
1566        let request = read_framed_message(&mut wire).await;
1567        assert_eq!(request["method"], "textDocument/references");
1568
1569        write_response(
1570            &mut server.read_half_stdin,
1571            &request["id"],
1572            serde_json::json!([
1573                {
1574                    "uri": inside_uri,
1575                    "range": {
1576                        "start": {"line": 0, "character": 0},
1577                        "end": {"line": 0, "character": 4}
1578                    }
1579                },
1580                {
1581                    "uri": outside_uri,
1582                    "range": {
1583                        "start": {"line": 0, "character": 0},
1584                        "end": {"line": 0, "character": 4}
1585                    }
1586                }
1587            ]),
1588        )
1589        .await;
1590
1591        let result = timeout(Duration::from_secs(2), handle)
1592            .await
1593            .expect("handle_references should not hang")
1594            .unwrap()
1595            .unwrap();
1596        assert!(result.index_coverage.is_none());
1597        assert!(
1598            serde_json::to_value(&result)
1599                .unwrap()
1600                .get("index_coverage")
1601                .is_none()
1602        );
1603        let result = result.result;
1604
1605        assert_eq!(
1606            result.locations.len(),
1607            2,
1608            "both the in-workspace and out-of-workspace locations must survive"
1609        );
1610        assert!(result.locations.iter().any(|l| l.uri == inside_uri));
1611        assert!(result.locations.iter().any(|l| l.uri == outside_uri));
1612        assert!(
1613            !result
1614                .locations
1615                .iter()
1616                .find(|l| l.uri == inside_uri)
1617                .unwrap()
1618                .out_of_workspace,
1619            "an in-workspace reference location must not be marked out_of_workspace"
1620        );
1621        assert!(
1622            result
1623                .locations
1624                .iter()
1625                .find(|l| l.uri == outside_uri)
1626                .unwrap()
1627                .out_of_workspace,
1628            "an out-of-workspace reference location must be marked out_of_workspace"
1629        );
1630    }
1631
1632    /// Regression for #474/M4: `get_references`' tool description no longer
1633    /// promises "all" references, since a response past
1634    /// `MAX_NORMALIZED_LOCATIONS` is capped -- the client must be able to
1635    /// detect that via `LocationsResult::truncated` rather than silently
1636    /// receiving a partial result that looks complete.
1637    #[tokio::test]
1638    async fn test_handle_references_sets_truncated_flag_past_cap() {
1639        let dir = TempDir::new().unwrap();
1640        let server_id = ServerId::from_static("rust");
1641        let caps = lsp_types::ServerCapabilities {
1642            references_provider: Some(lsp_types::ReferencesProvider::Bool(true)),
1643            ..Default::default()
1644        };
1645        let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
1646
1647        let path = dir.path().join("main.rs");
1648        fs::write(&path, "fn main() {}").unwrap();
1649        let uri = Url::from_file_path(&path).unwrap().to_string();
1650
1651        let translator = Arc::new(translator);
1652        let handle = {
1653            let translator = Arc::clone(&translator);
1654            let path = path.to_string_lossy().to_string();
1655            tokio::spawn(async move {
1656                translator
1657                    .handle_references(client_path(path), pos(1, 1), true, ResultContext::None)
1658                    .await
1659            })
1660        };
1661
1662        let mut wire = BufReader::new(&mut server.write_stdout);
1663        let opened = read_framed_message(&mut wire).await;
1664        assert_eq!(opened["method"], "textDocument/didOpen");
1665        let request = read_framed_message(&mut wire).await;
1666        assert_eq!(request["method"], "textDocument/references");
1667
1668        let locations: Vec<serde_json::Value> = (0..MAX_NORMALIZED_LOCATIONS + 500)
1669            .map(|_| {
1670                serde_json::json!({
1671                    "uri": uri,
1672                    "range": {
1673                        "start": {"line": 0, "character": 0},
1674                        "end": {"line": 0, "character": 4}
1675                    }
1676                })
1677            })
1678            .collect();
1679        write_response(
1680            &mut server.read_half_stdin,
1681            &request["id"],
1682            serde_json::json!(locations),
1683        )
1684        .await;
1685
1686        let result = timeout(Duration::from_secs(5), handle)
1687            .await
1688            .expect("handle_references should not hang")
1689            .unwrap()
1690            .unwrap()
1691            .result;
1692
1693        assert_eq!(result.locations.len(), MAX_NORMALIZED_LOCATIONS);
1694        assert!(
1695            result.truncated,
1696            "a references response past MAX_NORMALIZED_LOCATIONS must set truncated: true"
1697        );
1698    }
1699
1700    /// Success-path coverage for `handle_implementation` through the
1701    /// `Definition::LocationList` -> `GotoKind::Definition` arm, pinning the
1702    /// `GotoResponse` impl for `ImplementationResponse`.
1703    #[tokio::test]
1704    async fn test_handle_implementation_flattens_location_list() {
1705        let dir = TempDir::new().unwrap();
1706        let server_id = ServerId::from_static("rust");
1707        let caps = lsp_types::ServerCapabilities {
1708            implementation_provider: Some(lsp_types::ImplementationProvider::Bool(true)),
1709            ..Default::default()
1710        };
1711        let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
1712
1713        let path = dir.path().join("main.rs");
1714        fs::write(&path, "fn main() {}").unwrap();
1715        let first_impl_path = dir.path().join("impl_a.rs");
1716        fs::write(&first_impl_path, "struct A;").unwrap();
1717        let first_impl_uri = Url::from_file_path(&first_impl_path).unwrap().to_string();
1718        let second_impl_path = dir.path().join("impl_b.rs");
1719        fs::write(&second_impl_path, "struct B;").unwrap();
1720        let second_impl_uri = Url::from_file_path(&second_impl_path).unwrap().to_string();
1721
1722        let translator = Arc::new(translator);
1723        let handle = {
1724            let translator = Arc::clone(&translator);
1725            let path = path.to_string_lossy().to_string();
1726            tokio::spawn(async move {
1727                translator
1728                    .handle_implementation(
1729                        client_path(path),
1730                        Position::at(1, 1),
1731                        ResultContext::None,
1732                    )
1733                    .await
1734            })
1735        };
1736
1737        let mut wire = BufReader::new(&mut server.write_stdout);
1738        let opened = read_framed_message(&mut wire).await;
1739        assert_eq!(opened["method"], "textDocument/didOpen");
1740        let request = read_framed_message(&mut wire).await;
1741        assert_eq!(request["method"], "textDocument/implementation");
1742
1743        write_response(
1744            &mut server.read_half_stdin,
1745            &request["id"],
1746            serde_json::json!([
1747                {
1748                    "uri": first_impl_uri,
1749                    "range": {
1750                        "start": {"line": 0, "character": 0},
1751                        "end": {"line": 0, "character": 9}
1752                    }
1753                },
1754                {
1755                    "uri": second_impl_uri,
1756                    "range": {
1757                        "start": {"line": 0, "character": 0},
1758                        "end": {"line": 0, "character": 9}
1759                    }
1760                }
1761            ]),
1762        )
1763        .await;
1764
1765        let result = timeout(Duration::from_secs(2), handle)
1766            .await
1767            .expect("handle_implementation should not hang")
1768            .unwrap()
1769            .unwrap();
1770
1771        assert_eq!(result.locations.len(), 2);
1772        assert_eq!(result.locations[0].uri, first_impl_uri);
1773        assert_eq!(result.locations[1].uri, second_impl_uri);
1774    }
1775
1776    /// `handle_declaration` flattens a plain `Declaration::LocationList` and
1777    /// sends `textDocument/declaration`.
1778    #[tokio::test]
1779    async fn test_handle_declaration_flattens_location_list() {
1780        let dir = TempDir::new().unwrap();
1781        let server_id = ServerId::from_static("rust");
1782        let caps = lsp_types::ServerCapabilities {
1783            declaration_provider: Some(lsp_types::DeclarationProvider::Bool(true)),
1784            ..Default::default()
1785        };
1786        let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
1787
1788        let path = dir.path().join("main.rs");
1789        fs::write(&path, "fn main() {}").unwrap();
1790        let decl_path = dir.path().join("decl.rs");
1791        fs::write(&decl_path, "fn decl();").unwrap();
1792        let decl_uri = Url::from_file_path(&decl_path).unwrap().to_string();
1793
1794        let translator = Arc::new(translator);
1795        let handle = {
1796            let translator = Arc::clone(&translator);
1797            let path = client_path(&path);
1798            tokio::spawn(async move {
1799                translator
1800                    .handle_declaration(path, pos(1, 1), ResultContext::None)
1801                    .await
1802            })
1803        };
1804
1805        let mut wire = BufReader::new(&mut server.write_stdout);
1806        let opened = read_framed_message(&mut wire).await;
1807        assert_eq!(opened["method"], "textDocument/didOpen");
1808        let request = read_framed_message(&mut wire).await;
1809        assert_eq!(request["method"], "textDocument/declaration");
1810
1811        write_response(
1812            &mut server.read_half_stdin,
1813            &request["id"],
1814            serde_json::json!([{
1815                "uri": decl_uri,
1816                "range": {
1817                    "start": {"line": 0, "character": 3},
1818                    "end": {"line": 0, "character": 7}
1819                }
1820            }]),
1821        )
1822        .await;
1823
1824        let result = timeout(Duration::from_secs(2), handle)
1825            .await
1826            .expect("handle_declaration should not hang")
1827            .unwrap()
1828            .unwrap();
1829
1830        assert_eq!(result.locations.len(), 1);
1831        assert_eq!(result.locations[0].uri, decl_uri);
1832    }
1833
1834    /// `handle_declaration` maps `DeclarationLink[]` through `targetSelectionRange`.
1835    #[tokio::test]
1836    async fn test_handle_declaration_flattens_link_list() {
1837        let dir = TempDir::new().unwrap();
1838        let server_id = ServerId::from_static("rust");
1839        let caps = lsp_types::ServerCapabilities {
1840            declaration_provider: Some(lsp_types::DeclarationProvider::Bool(true)),
1841            ..Default::default()
1842        };
1843        let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
1844
1845        let path = dir.path().join("main.rs");
1846        fs::write(&path, "fn main() {}").unwrap();
1847        let target_path = dir.path().join("target.h");
1848        fs::write(&target_path, "struct TargetType;").unwrap();
1849        let target_uri = Url::from_file_path(&target_path).unwrap().to_string();
1850
1851        let translator = Arc::new(translator);
1852        let handle = {
1853            let translator = Arc::clone(&translator);
1854            let path = client_path(&path);
1855            tokio::spawn(async move {
1856                translator
1857                    .handle_declaration(path, pos(1, 1), ResultContext::None)
1858                    .await
1859            })
1860        };
1861
1862        let mut wire = BufReader::new(&mut server.write_stdout);
1863        let _opened = read_framed_message(&mut wire).await;
1864        let request = read_framed_message(&mut wire).await;
1865        assert_eq!(request["method"], "textDocument/declaration");
1866
1867        write_response(
1868            &mut server.read_half_stdin,
1869            &request["id"],
1870            serde_json::json!([{
1871                "targetUri": target_uri,
1872                "targetRange": {
1873                    "start": {"line": 0, "character": 0},
1874                    "end": {"line": 0, "character": 18}
1875                },
1876                "targetSelectionRange": {
1877                    "start": {"line": 0, "character": 7},
1878                    "end": {"line": 0, "character": 17}
1879                }
1880            }]),
1881        )
1882        .await;
1883
1884        let result = timeout(Duration::from_secs(2), handle)
1885            .await
1886            .expect("handle_declaration should not hang")
1887            .unwrap()
1888            .unwrap();
1889
1890        assert_eq!(result.locations.len(), 1);
1891        assert_eq!(result.locations[0].range.start.character, 8);
1892    }
1893
1894    /// An empty declaration answer is an empty result, not an error (FR-003).
1895    #[tokio::test]
1896    async fn test_handle_declaration_null_response_is_empty() {
1897        let dir = TempDir::new().unwrap();
1898        let server_id = ServerId::from_static("rust");
1899        let caps = lsp_types::ServerCapabilities {
1900            declaration_provider: Some(lsp_types::DeclarationProvider::Bool(true)),
1901            ..Default::default()
1902        };
1903        let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
1904
1905        let path = dir.path().join("main.rs");
1906        fs::write(&path, "fn main() {}").unwrap();
1907
1908        let translator = Arc::new(translator);
1909        let handle = {
1910            let translator = Arc::clone(&translator);
1911            let path = client_path(&path);
1912            tokio::spawn(async move {
1913                translator
1914                    .handle_declaration(path, pos(1, 1), ResultContext::None)
1915                    .await
1916            })
1917        };
1918
1919        let mut wire = BufReader::new(&mut server.write_stdout);
1920        let _opened = read_framed_message(&mut wire).await;
1921        let request = read_framed_message(&mut wire).await;
1922        write_response(
1923            &mut server.read_half_stdin,
1924            &request["id"],
1925            serde_json::Value::Null,
1926        )
1927        .await;
1928
1929        let result = timeout(Duration::from_secs(2), handle)
1930            .await
1931            .expect("handle_declaration should not hang")
1932            .unwrap()
1933            .unwrap();
1934        assert!(result.locations.is_empty());
1935    }
1936
1937    /// `handle_declaration` is gated by the indexing readiness check like definition.
1938    #[tokio::test(start_paused = true)]
1939    async fn test_handle_declaration_returns_workspace_indexing_error_when_loading() {
1940        let dir = TempDir::new().unwrap();
1941        let server_id = ServerId::from_static("rust");
1942        let caps = lsp_types::ServerCapabilities {
1943            declaration_provider: Some(lsp_types::DeclarationProvider::Bool(true)),
1944            ..Default::default()
1945        };
1946        let (translator, _server) = translator_with_capabilities(&dir, &server_id, caps);
1947
1948        let cache = Arc::new(Mutex::new(NotificationCache::new()));
1949        cache.lock().await.observe_indexing_signal(
1950            &server_id,
1951            "experimental/serverStatus",
1952            Some(&serde_json::json!({"quiescent": false})),
1953        );
1954        let translator = translator.with_notification_cache(cache);
1955
1956        let path = dir.path().join("main.rs");
1957        fs::write(&path, "fn main() {}").unwrap();
1958
1959        let err = translator
1960            .handle_declaration(client_path(&path), pos(1, 1), ResultContext::None)
1961            .await
1962            .unwrap_err();
1963
1964        assert_matches!(
1965            err,
1966            Error::WorkspaceIndexing { server_id: id, .. } if id == server_id
1967        );
1968    }
1969
1970    /// Success-path coverage for `handle_type_definition` through the
1971    /// `DefinitionLinkList` -> `GotoKind::DefinitionLinkList` arm, pinning
1972    /// the `GotoResponse` impl for `TypeDefinitionResponse` and the
1973    /// `definition_link_to_location` mapping (`target_selection_range`, not
1974    /// `target_range`).
1975    #[tokio::test]
1976    async fn test_handle_type_definition_flattens_definition_link_list() {
1977        let dir = TempDir::new().unwrap();
1978        let server_id = ServerId::from_static("rust");
1979        let caps = lsp_types::ServerCapabilities {
1980            type_definition_provider: Some(lsp_types::TypeDefinitionProvider::Bool(true)),
1981            ..Default::default()
1982        };
1983        let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
1984
1985        let path = dir.path().join("main.rs");
1986        fs::write(&path, "fn main() {}").unwrap();
1987        let target_path = dir.path().join("target_type.rs");
1988        fs::write(&target_path, "struct TargetType;").unwrap();
1989        let target_uri = Url::from_file_path(&target_path).unwrap().to_string();
1990
1991        let translator = Arc::new(translator);
1992        let handle = {
1993            let translator = Arc::clone(&translator);
1994            let path = path.to_string_lossy().to_string();
1995            tokio::spawn(async move {
1996                translator
1997                    .handle_type_definition(
1998                        client_path(path),
1999                        Position::at(1, 1),
2000                        ResultContext::None,
2001                    )
2002                    .await
2003            })
2004        };
2005
2006        let mut wire = BufReader::new(&mut server.write_stdout);
2007        let opened = read_framed_message(&mut wire).await;
2008        assert_eq!(opened["method"], "textDocument/didOpen");
2009        let request = read_framed_message(&mut wire).await;
2010        assert_eq!(request["method"], "textDocument/typeDefinition");
2011
2012        write_response(
2013            &mut server.read_half_stdin,
2014            &request["id"],
2015            serde_json::json!([{
2016                "targetUri": target_uri,
2017                "targetRange": {
2018                    "start": {"line": 0, "character": 0},
2019                    "end": {"line": 0, "character": 18}
2020                },
2021                "targetSelectionRange": {
2022                    "start": {"line": 0, "character": 7},
2023                    "end": {"line": 0, "character": 17}
2024                }
2025            }]),
2026        )
2027        .await;
2028
2029        let result = timeout(Duration::from_secs(2), handle)
2030            .await
2031            .expect("handle_type_definition should not hang")
2032            .unwrap()
2033            .unwrap();
2034
2035        assert_eq!(result.locations.len(), 1);
2036        assert_eq!(result.locations[0].uri, target_uri);
2037        assert_eq!(result.locations[0].range.start.character, 8);
2038    }
2039
2040    // -----------------------------------------------------------------
2041    // Resource-amplification defenses (#474)
2042    // -----------------------------------------------------------------
2043
2044    /// Regression for #474's exact attack scenario: many `Location`s
2045    /// clustered onto a handful of distinct `(file, line)` pairs must cost
2046    /// one disk read per distinct pair, not one per location. Proven through
2047    /// the real `lsp_locations_to_mcp` entry point shared by `handle_goto`
2048    /// and `handle_references` -- not by calling the cache-backed helper
2049    /// directly -- and via the cache's own size, which is a direct count of
2050    /// how many times the disk-read fallback actually ran.
2051    #[tokio::test]
2052    async fn test_lsp_locations_to_mcp_reads_disk_once_per_distinct_file_line() {
2053        let dir = TempDir::new().unwrap();
2054        let mut uris = Vec::new();
2055        for i in 0..3 {
2056            let path = dir.path().join(format!("file{i}.rs"));
2057            fs::write(&path, "hello").unwrap();
2058            uris.push(path_to_uri(&path).unwrap());
2059        }
2060
2061        let ctx = test_ctx_with(PositionEncoding::Utf8);
2062        // 300 locations, but only 3 distinct (file, line) pairs.
2063        let locs: Vec<lsp_types::Location> = (0..300)
2064            .map(|i| lsp_types::Location {
2065                uri: uris[i % 3].clone(),
2066                range: lsp_types::Range {
2067                    start: lsp_types::Position {
2068                        line: 0,
2069                        character: 0,
2070                    },
2071                    end: lsp_types::Position {
2072                        line: 0,
2073                        character: 3,
2074                    },
2075                },
2076            })
2077            .collect();
2078
2079        let result = lsp_locations_to_mcp(locs, &ctx).await;
2080
2081        assert_eq!(result.locations.len(), 300);
2082        assert!(!result.truncated);
2083        assert!(
2084            result.locations.iter().all(|l| l.range.end.character == 4),
2085            "MCP columns are 1-based, so LSP byte offset 3 in all-ASCII \"hello\" must convert \
2086             to 4"
2087        );
2088        assert_eq!(
2089            lock_std(&ctx.line_cache).entries.len(),
2090            3,
2091            "300 locations across 3 distinct files must populate the cache with exactly 3 \
2092             entries (one disk read per distinct file/line), not one per location"
2093        );
2094    }
2095
2096    /// Regression for #474: without a cap, a response naming an unbounded
2097    /// number of locations would drive an unbounded number of range
2098    /// conversions. `Utf16` needs no disk read at all (see `test_ctx`), so
2099    /// this isolates the truncation itself from I/O cost -- a response well
2100    /// past `MAX_NORMALIZED_LOCATIONS` must be truncated to it, not hang,
2101    /// OOM, or panic.
2102    #[tokio::test]
2103    async fn test_lsp_locations_to_mcp_truncates_to_max_normalized_locations() {
2104        let ctx = test_ctx();
2105        let uri = test_uri();
2106        let locs: Vec<lsp_types::Location> = (0..MAX_NORMALIZED_LOCATIONS + 500)
2107            .map(|_| lsp_types::Location {
2108                uri: uri.clone(),
2109                range: lsp_types::Range {
2110                    start: lsp_types::Position {
2111                        line: 0,
2112                        character: 0,
2113                    },
2114                    end: lsp_types::Position {
2115                        line: 0,
2116                        character: 1,
2117                    },
2118                },
2119            })
2120            .collect();
2121
2122        let result = lsp_locations_to_mcp(locs, &ctx).await;
2123
2124        assert_eq!(result.locations.len(), MAX_NORMALIZED_LOCATIONS);
2125        assert!(
2126            result.truncated,
2127            "a response naming more than MAX_NORMALIZED_LOCATIONS must report truncated: true"
2128        );
2129    }
2130
2131    #[test]
2132    fn test_item_budget_admit_spends_shared_allowance() {
2133        let mut budget = ItemBudget::new();
2134        let first = budget.admit(vec![0u8; MAX_NORMALIZED_LOCATIONS - 1]);
2135        assert_eq!(first.len(), MAX_NORMALIZED_LOCATIONS - 1);
2136        assert!(!budget.truncated());
2137
2138        let second = budget.admit(vec![0u8; 5]);
2139        assert_eq!(second.len(), 1);
2140        assert!(budget.truncated());
2141
2142        assert_eq!(budget.admit(vec![0u8; 3]).len(), 0);
2143    }
2144
2145    #[test]
2146    fn test_item_budget_admit_whole_never_splits_a_list() {
2147        let mut budget = ItemBudget::new();
2148        assert!(
2149            budget
2150                .admit_whole(vec![0u8; MAX_NORMALIZED_LOCATIONS - 2])
2151                .is_some()
2152        );
2153        assert!(budget.admit_whole(vec![0u8; 3]).is_none());
2154        assert!(budget.truncated());
2155        assert!(budget.admit_whole(vec![0u8; 2]).is_some());
2156    }
2157
2158    #[test]
2159    fn test_item_budget_with_cap_admits_a_lazy_sequence_up_to_the_cap() {
2160        let mut budget = ItemBudget::with_cap(3);
2161        assert_eq!(budget.admit_iter(0..3), [0, 1, 2]);
2162        assert!(!budget.truncated());
2163
2164        let mut budget = ItemBudget::with_cap(3);
2165        assert_eq!(budget.admit_iter(0..), [0, 1, 2]);
2166        assert!(budget.truncated());
2167        assert_eq!(budget.admit_iter(0..2).len(), 0);
2168    }
2169
2170    #[test]
2171    fn test_item_budget_exactly_at_cap_is_not_truncated() {
2172        let mut budget = ItemBudget::new();
2173        let items = budget.admit(vec![0u8; MAX_NORMALIZED_LOCATIONS]);
2174        assert_eq!(items.len(), MAX_NORMALIZED_LOCATIONS);
2175        assert!(!budget.truncated());
2176    }
2177}