Skip to main content

mcpls_core/bridge/translator/
addressing.rs

1//! Symbol-name addressing for position-based tools.
2//!
3//! A caller may name a symbol instead of counting a line and column. The name
4//! is resolved against the file's `textDocument/documentSymbol` answer, the
5//! identifier position of the one matching symbol is verified against the
6//! tracked document text, and the tool then runs at that position exactly as
7//! if the caller had supplied it. A name that matches several symbols, none,
8//! or whose identifier cannot be located unambiguously yields a typed
9//! [`SymbolResolutionData`] error instead of a guess.
10
11use lsp_types::{DocumentSymbolResponse, SymbolKind};
12use schemars::JsonSchema;
13use serde::Serialize;
14
15use super::Translator;
16use super::dto::{Position, Position2D};
17use super::encoding_ctx::EncodingCtx;
18use super::routing::Capability;
19use crate::bridge::ClientPath;
20use crate::bridge::encoding::{EncodingConverter, PositionEncoding};
21use crate::bridge::state::uri_to_path;
22use crate::error::{Error, Result, SymbolCandidate, SymbolResolutionData};
23use crate::redaction::{Redactions, ServerText};
24
25/// Longest accepted symbol name, container or kind text, in bytes.
26pub const MAX_SYMBOL_NAME_BYTES: usize = 256;
27
28/// Candidates listed in an ambiguity error; more are reported as truncated.
29const MAX_CANDIDATES: usize = 50;
30
31/// Symbols examined in one document-symbol answer.
32const MAX_FLATTENED_SYMBOLS: usize = 20_000;
33
34/// Lines of a symbol's range searched for its identifier.
35const MAX_WINDOW_LINES: u32 = 500;
36
37/// Lines of the document scanned to tell "referenced" from "absent".
38const MAX_SCAN_LINES: usize = 50_000;
39
40/// Why [`SymbolName::try_new`] rejected a name.
41#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
42pub enum SymbolNameError {
43    /// The name is empty or only whitespace.
44    #[error("symbol names must not be blank")]
45    Blank,
46    /// The name is longer than [`MAX_SYMBOL_NAME_BYTES`].
47    #[error("symbol names must be at most {MAX_SYMBOL_NAME_BYTES} bytes")]
48    TooLong,
49}
50
51/// A validated symbol or container name: surrounding whitespace trimmed,
52/// non-blank and bounded.
53///
54/// # Examples
55///
56/// ```
57/// use mcpls_core::bridge::SymbolName;
58///
59/// assert_eq!(SymbolName::try_new("parse_config").unwrap().as_str(), "parse_config");
60/// assert_eq!(SymbolName::try_new(" parse_config ").unwrap().as_str(), "parse_config");
61/// assert!(SymbolName::try_new("  ").is_err());
62/// ```
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct SymbolName(String);
65
66impl SymbolName {
67    /// Validate `raw` after trimming surrounding whitespace, which is not
68    /// part of the name.
69    ///
70    /// # Errors
71    ///
72    /// [`SymbolNameError::Blank`] or [`SymbolNameError::TooLong`] (judged on
73    /// the trimmed text).
74    pub fn try_new(raw: impl Into<String>) -> std::result::Result<Self, SymbolNameError> {
75        let raw = raw.into().trim().to_string();
76        if raw.is_empty() {
77            return Err(SymbolNameError::Blank);
78        }
79        if raw.len() > MAX_SYMBOL_NAME_BYTES {
80            return Err(SymbolNameError::TooLong);
81        }
82        Ok(Self(raw))
83    }
84
85    /// The name text.
86    #[must_use]
87    pub fn as_str(&self) -> &str {
88        &self.0
89    }
90}
91
92/// A symbol named by the caller, optionally narrowed by kind and container.
93#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct SymbolQuery {
95    /// The symbol's name, optionally qualified (`Type::method`, `Type.method`).
96    pub name: SymbolName,
97    /// Keep only symbols of this kind.
98    pub kind: Option<SymbolKind>,
99    /// Keep only symbols directly inside a container of this name.
100    pub container: Option<SymbolName>,
101}
102
103/// How a tool addresses its target: by 1-based position or by symbol name.
104#[derive(Debug, Clone, PartialEq, Eq)]
105pub enum SymbolTarget {
106    /// An exact 1-based position.
107    Position(Position),
108    /// A symbol resolved by name within the file.
109    Name(SymbolQuery),
110}
111
112/// The tools that accept a [`SymbolTarget`].
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
114pub enum AddressableTool {
115    /// `get_hover`.
116    Hover,
117    /// `get_definition`.
118    Definition,
119    /// `get_references`.
120    References,
121    /// `go_to_implementation`.
122    Implementation,
123    /// `go_to_type_definition`.
124    TypeDefinition,
125    /// `prepare_call_hierarchy`.
126    PrepareCallHierarchy,
127    /// `rename_symbol`.
128    Rename,
129}
130
131impl AddressableTool {
132    const fn capability(self) -> Capability {
133        match self {
134            Self::Hover => Capability::Hover,
135            Self::Definition => Capability::Definition,
136            Self::References => Capability::References,
137            Self::Implementation => Capability::Implementation,
138            Self::TypeDefinition => Capability::TypeDefinition,
139            Self::PrepareCallHierarchy => Capability::CallHierarchy,
140            Self::Rename => Capability::Rename,
141        }
142    }
143}
144
145/// Where the queried position of a name-addressed call came from.
146#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, JsonSchema)]
147#[serde(rename_all = "snake_case")]
148pub enum PositionSource {
149    /// The server's `selectionRange` start, verified to spell the name.
150    SelectionRange,
151    /// The only whole-word occurrence of the name inside the symbol's range,
152    /// found in the document text because the server gave no usable
153    /// `selectionRange`.
154    Inferred,
155}
156
157/// The symbol a name resolved to, reported so the caller can audit and reuse
158/// the queried position.
159#[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema)]
160pub struct ResolvedSymbol {
161    /// The symbol's name as the server reports it.
162    pub name: String,
163    /// Numeric LSP `SymbolKind`.
164    pub kind: u32,
165    /// Enclosing symbol, when the server reports one.
166    pub container: Option<String>,
167    /// The 1-based position that was queried.
168    pub position: Position2D,
169    /// How `position` was determined.
170    pub position_source: PositionSource,
171}
172
173/// A tool result together with the symbol its name resolved to, when the call
174/// was name-addressed.
175#[derive(Debug, Clone, Serialize, JsonSchema)]
176pub struct Addressed<T> {
177    /// The tool's own result.
178    #[serde(flatten)]
179    pub result: T,
180    /// Set only for a name-addressed call.
181    #[serde(skip_serializing_if = "Option::is_none")]
182    pub resolved_symbol: Option<ResolvedSymbol>,
183}
184
185/// The position a tool is to run at, and how it was obtained.
186#[derive(Debug, Clone, PartialEq, Eq)]
187pub struct ResolvedTarget {
188    /// The 1-based position to query.
189    pub position: Position,
190    /// The resolved symbol; `None` when the caller gave the position.
191    pub resolved: Option<ResolvedSymbol>,
192}
193
194/// One symbol of a document-symbol answer, flattened.
195#[derive(Debug, Clone)]
196struct SymbolEntry {
197    name: String,
198    kind: SymbolKind,
199    container: Option<String>,
200    range: lsp_types::Range,
201    selection_range: Option<lsp_types::Range>,
202}
203
204fn flatten_symbols(response: DocumentSymbolResponse) -> Vec<SymbolEntry> {
205    let mut entries = Vec::new();
206    match response {
207        DocumentSymbolResponse::SymbolInformationList(symbols) => {
208            for symbol in symbols.into_iter().take(MAX_FLATTENED_SYMBOLS) {
209                let base = symbol.base_symbol_information;
210                entries.push(SymbolEntry {
211                    name: base.name,
212                    kind: base.kind,
213                    container: base.container_name,
214                    range: symbol.location.range,
215                    selection_range: None,
216                });
217            }
218        }
219        DocumentSymbolResponse::DocumentSymbolList(symbols) => {
220            let mut stack: Vec<(lsp_types::DocumentSymbol, Option<String>)> =
221                symbols.into_iter().rev().map(|s| (s, None)).collect();
222            while let Some((symbol, parent)) = stack.pop() {
223                if entries.len() >= MAX_FLATTENED_SYMBOLS {
224                    break;
225                }
226                let children = symbol.children.unwrap_or_default();
227                for child in children.into_iter().rev() {
228                    stack.push((child, Some(symbol.name.clone())));
229                }
230                entries.push(SymbolEntry {
231                    name: symbol.name,
232                    kind: symbol.kind,
233                    container: parent,
234                    range: symbol.range,
235                    selection_range: Some(symbol.selection_range),
236                });
237            }
238        }
239    }
240    entries
241}
242
243/// The simple identifier of `raw` and the qualifier written before it.
244#[derive(Debug, Clone, PartialEq, Eq)]
245struct NameParts {
246    simple: String,
247    qualifier: Option<String>,
248}
249
250/// Whether `raw` contains the C++ `operator` keyword as a word, so
251/// `operator<` and `Foo::operator()` qualify but `operatorCount(int)` and
252/// `get_operator(x)` do not.
253fn names_an_operator(raw: &str) -> bool {
254    raw.match_indices("operator").any(|(start, keyword)| {
255        let before = raw.get(..start).and_then(|head| head.chars().next_back());
256        let after = raw
257            .get(start.saturating_add(keyword.len())..)
258            .and_then(|tail| tail.chars().next());
259        !before.is_some_and(is_word_char) && !after.is_some_and(is_word_char)
260    })
261}
262
263/// `raw` without generic arguments, or `raw` itself when its angle brackets
264/// do not pair up or it names an operator (`operator<`, `operator<=>`).
265fn strip_generics(raw: &str) -> String {
266    if names_an_operator(raw) {
267        return raw.to_string();
268    }
269    let mut depth = 0_u32;
270    let mut stripped = String::with_capacity(raw.len());
271    for ch in raw.chars() {
272        match ch {
273            '<' => depth = depth.saturating_add(1),
274            '>' => {
275                if depth == 0 {
276                    return raw.to_string();
277                }
278                depth = depth.saturating_sub(1);
279            }
280            _ if depth == 0 => stripped.push(ch),
281            _ => {}
282        }
283    }
284    if depth == 0 {
285        stripped
286    } else {
287        raw.to_string()
288    }
289}
290
291/// `receiver` of a Go-style `(*T).Method` name, without `*`/`&`.
292fn receiver_and_rest(raw: &str) -> Option<(String, &str)> {
293    let inner = raw.strip_prefix('(')?;
294    let close = inner.find(')')?;
295    let rest = inner.get(close.saturating_add(1)..)?.strip_prefix('.')?;
296    let receiver = strip_generics(inner.get(..close)?)
297        .trim_start_matches(['*', '&'])
298        .trim()
299        .to_string();
300    Some((receiver, rest))
301}
302
303/// `raw` without a trailing balanced parameter list (`bar(int)` is `bar`), or
304/// `raw` itself when there is none or it names an operator (`operator()`).
305fn strip_params(raw: &str) -> &str {
306    if names_an_operator(raw) || !raw.ends_with(')') {
307        return raw;
308    }
309    let mut depth = 0_u32;
310    for (index, ch) in raw.char_indices().rev() {
311        match ch {
312            ')' => depth = depth.saturating_add(1),
313            '(' => {
314                depth = depth.saturating_sub(1);
315                if depth == 0 {
316                    return raw
317                        .get(..index)
318                        .filter(|head| !head.is_empty())
319                        .unwrap_or(raw);
320                }
321            }
322            _ => {}
323        }
324    }
325    raw
326}
327
328fn name_parts(raw: &str) -> NameParts {
329    if let Some((receiver, rest)) = receiver_and_rest(raw) {
330        return NameParts {
331            simple: strip_generics(strip_params(rest)),
332            qualifier: Some(receiver),
333        };
334    }
335    let stripped = strip_generics(strip_params(raw));
336    let mut segments = stripped.rsplit("::").flat_map(|part| part.rsplit('.'));
337    let simple = segments.next().unwrap_or_default().to_string();
338    let qualifier = segments.next().map(str::to_string);
339    NameParts { simple, qualifier }
340}
341
342/// The simple name a container is known by: `impl<T> Tr for Foo<T>` is `Foo`.
343fn container_name(raw: &str) -> String {
344    let trimmed = raw.trim();
345    let target = match trimmed.strip_prefix("impl") {
346        Some(rest) if rest.starts_with(|c: char| c.is_whitespace() || c == '<') => {
347            let rest = strip_generics_prefix(rest);
348            rest.rsplit_once(" for ")
349                .map_or_else(|| rest.trim(), |(_, ty)| ty.trim())
350        }
351        _ => trimmed,
352    };
353    name_parts(target).simple
354}
355
356/// `rest` without a leading `<...>` parameter list.
357fn strip_generics_prefix(rest: &str) -> &str {
358    let Some(inner) = rest.strip_prefix('<') else {
359        return rest.trim_start();
360    };
361    let mut depth = 1_u32;
362    for (index, ch) in inner.char_indices() {
363        match ch {
364            '<' => depth = depth.saturating_add(1),
365            '>' => {
366                depth = depth.saturating_sub(1);
367                if depth == 0 {
368                    return inner
369                        .get(index.saturating_add(1)..)
370                        .unwrap_or_default()
371                        .trim_start();
372                }
373            }
374            _ => {}
375        }
376    }
377    rest
378}
379
380fn is_word_char(ch: char) -> bool {
381    ch.is_alphanumeric() || ch == '_'
382}
383
384/// Byte offsets of the whole-word occurrences of `word` in `text`.
385fn whole_word_occurrences(text: &str, word: &str) -> Vec<usize> {
386    if word.is_empty() {
387        return Vec::new();
388    }
389    text.match_indices(word)
390        .filter(|(start, _)| {
391            let before = text.get(..*start).and_then(|head| head.chars().next_back());
392            let after = text
393                .get(start.saturating_add(word.len())..)
394                .and_then(|tail| tail.chars().next());
395            !before.is_some_and(is_word_char) && !after.is_some_and(is_word_char)
396        })
397        .map(|(start, _)| start)
398        .collect()
399}
400
401/// The result of matching a query against a document's symbols.
402struct Selection<'a> {
403    matched: Vec<&'a SymbolEntry>,
404    excluded_by_filters: usize,
405}
406
407/// Names a symbol is nested under: its parent/`containerName` plus any
408/// qualifier written in its own name.
409fn effective_containers(entry: &SymbolEntry) -> Vec<String> {
410    let mut containers: Vec<String> = entry.container.iter().map(|c| container_name(c)).collect();
411    containers.extend(name_parts(&entry.name).qualifier);
412    containers
413}
414
415fn select_candidates<'a>(entries: &'a [SymbolEntry], query: &SymbolQuery) -> Selection<'a> {
416    let wanted = name_parts(query.name.as_str());
417    let named: Vec<&SymbolEntry> = entries
418        .iter()
419        .filter(|entry| name_parts(&entry.name).simple == wanted.simple)
420        .collect();
421
422    let required: Vec<String> = query
423        .container
424        .iter()
425        .map(|c| container_name(c.as_str()))
426        .chain(wanted.qualifier)
427        .collect();
428    let named_count = named.len();
429    let matched: Vec<&SymbolEntry> = named
430        .into_iter()
431        .filter(|entry| query.kind.is_none_or(|kind| entry.kind == kind))
432        .filter(|entry| {
433            let containers = effective_containers(entry);
434            required.iter().all(|need| containers.contains(need))
435        })
436        .collect();
437    Selection {
438        excluded_by_filters: named_count.saturating_sub(matched.len()),
439        matched,
440    }
441}
442
443/// Where an entry's identifier is.
444enum IdentifierPosition {
445    Verified(Position2D, PositionSource),
446    Unverified(Position2D),
447}
448
449/// Whether the document text spells `word` at the MCP `position`.
450fn spells_at(line_text: &str, character: u32, word: &str) -> bool {
451    let utf16 = EncodingConverter::new(PositionEncoding::Utf16);
452    let Ok(byte) = utf16.character_to_byte_offset(line_text, character.saturating_sub(1)) else {
453        return false;
454    };
455    whole_word_occurrences(line_text, word).contains(&byte)
456}
457
458async fn locate_identifier(
459    translator: &Translator,
460    ctx: &EncodingCtx,
461    uri: &lsp_types::Uri,
462    entry: &SymbolEntry,
463) -> IdentifierPosition {
464    let simple = name_parts(&entry.name).simple;
465    let Some(path) = uri_to_path(uri) else {
466        return IdentifierPosition::Unverified(ctx.to_mcp(uri, entry.range.start).await);
467    };
468    let tracker = translator.document_tracker();
469
470    if let Some(selection) = entry.selection_range {
471        let position = ctx.to_mcp(uri, selection.start).await;
472        let spelled = tracker
473            .line_text(&path, position.line.saturating_sub(1))
474            .is_some_and(|text| spells_at(&text, position.character, &simple));
475        if spelled {
476            return IdentifierPosition::Verified(position, PositionSource::SelectionRange);
477        }
478    }
479
480    let start = ctx.to_mcp(uri, entry.range.start).await;
481    let end = ctx.to_mcp(uri, entry.range.end).await;
482    let last_line = end
483        .line
484        .min(start.line.saturating_add(MAX_WINDOW_LINES - 1));
485    let utf16 = EncodingConverter::new(PositionEncoding::Utf16);
486    let window = usize::try_from(last_line.saturating_sub(start.line).saturating_add(1))
487        .unwrap_or(usize::MAX);
488    let text = tracker
489        .line_window(&path, start.line.saturating_sub(1), window)
490        .unwrap_or_default();
491    let mut found = Vec::new();
492    for (line, text) in (start.line..).zip(text.split('\n')) {
493        for byte in whole_word_occurrences(text, &simple) {
494            let Ok(column) = utf16.byte_offset_to_character(text, byte) else {
495                continue;
496            };
497            let character = column.saturating_add(1);
498            let after_start = (line, character) >= (start.line, start.character);
499            let before_end = (line, character) < (end.line, end.character);
500            if after_start && before_end {
501                found.push(Position2D { line, character });
502            }
503        }
504        if found.len() > 1 {
505            break;
506        }
507    }
508    match <[Position2D; 1]>::try_from(found) {
509        Ok([position]) => IdentifierPosition::Verified(position, PositionSource::Inferred),
510        Err(_) => IdentifierPosition::Unverified(start),
511    }
512}
513
514fn candidate(entry: &SymbolEntry, position: &Position2D) -> SymbolCandidate {
515    SymbolCandidate {
516        name: entry.name.clone(),
517        kind: u32::from(entry.kind),
518        kind_name: format!("{:?}", entry.kind),
519        container: entry.container.clone(),
520        line: position.line,
521        character: position.character,
522    }
523}
524
525const fn identifier_position(located: &IdentifierPosition) -> &Position2D {
526    match located {
527        IdentifierPosition::Verified(position, _) | IdentifierPosition::Unverified(position) => {
528            position
529        }
530    }
531}
532
533fn resolution_error(data: SymbolResolutionData) -> Error {
534    Error::SymbolResolution(Box::new(data))
535}
536
537impl Translator {
538    /// Resolve how a tool is to be aimed: a position passes through
539    /// untouched, a name is resolved against the file's document symbols.
540    ///
541    /// The routed server's capability for `tool` is checked before anything
542    /// else, so a name-addressed call fails like the position form does, and
543    /// no document-symbol request is made for a tool that could not run.
544    ///
545    /// # Errors
546    ///
547    /// The routing, capability and document errors of the position form, the
548    /// document-symbol request's own errors, and [`Error::SymbolResolution`]
549    /// when the name does not resolve to exactly one verifiable position.
550    ///
551    /// # Examples
552    ///
553    /// ```no_run
554    /// use std::path::PathBuf;
555    ///
556    /// use mcpls_core::bridge::{
557    ///     AddressableTool, ClientPath, Position, SymbolTarget, Translator,
558    /// };
559    ///
560    /// # async fn run(translator: &Translator) -> Result<(), Box<dyn std::error::Error>> {
561    /// let file = ClientPath::try_from(PathBuf::from("/ws/src/lib.rs"))?;
562    /// let target = SymbolTarget::Position(Position::from_client(3, 5)?);
563    /// let resolved = translator
564    ///     .resolve_symbol_target(&file, target, AddressableTool::Hover)
565    ///     .await?;
566    /// assert!(resolved.resolved.is_none());
567    /// # Ok(())
568    /// # }
569    /// ```
570    pub async fn resolve_symbol_target(
571        &self,
572        file_path: &ClientPath,
573        target: SymbolTarget,
574        tool: AddressableTool,
575    ) -> Result<ResolvedTarget> {
576        let query = match target {
577            SymbolTarget::Position(position) => {
578                return Ok(ResolvedTarget {
579                    position,
580                    resolved: None,
581                });
582            }
583            SymbolTarget::Name(query) => query,
584        };
585
586        self.require_routed_capability(file_path, tool.capability())
587            .await?;
588        let fetched = self.request_document_symbols(file_path).await?;
589        let uri = fetched.doc.uri().clone();
590        let entries = fetched.response.map_or_default(flatten_symbols);
591        let selection = select_candidates(&entries, &query);
592
593        match selection.matched.as_slice() {
594            [] => Err(self.missing_symbol_error(&query, &selection, &uri)),
595            [entry] => {
596                let located = locate_identifier(self, &fetched.ctx, &uri, entry).await;
597                match located {
598                    IdentifierPosition::Verified(position, source) => Ok(ResolvedTarget {
599                        position: Position::from_server_output(&position),
600                        resolved: Some(ResolvedSymbol {
601                            name: entry.name.clone(),
602                            kind: u32::from(entry.kind),
603                            container: entry.container.clone(),
604                            position,
605                            position_source: source,
606                        }),
607                    }),
608                    IdentifierPosition::Unverified(position) => {
609                        Err(resolution_error(SymbolResolutionData::PositionUnverified {
610                            name: query.name.as_str().to_string(),
611                            candidate: candidate(entry, &position),
612                        }))
613                    }
614                }
615            }
616            many => {
617                let mut candidates = Vec::with_capacity(many.len().min(MAX_CANDIDATES));
618                for entry in many.iter().take(MAX_CANDIDATES) {
619                    let located = locate_identifier(self, &fetched.ctx, &uri, entry).await;
620                    candidates.push(candidate(entry, identifier_position(&located)));
621                }
622                Err(resolution_error(SymbolResolutionData::Ambiguous {
623                    name: query.name.as_str().to_string(),
624                    truncated: many.len() > MAX_CANDIDATES,
625                    candidates,
626                }))
627            }
628        }
629    }
630
631    /// The error for a name with no matching symbol: filtered out, merely
632    /// referenced in the file, or absent.
633    fn missing_symbol_error(
634        &self,
635        query: &SymbolQuery,
636        selection: &Selection<'_>,
637        uri: &lsp_types::Uri,
638    ) -> Error {
639        let name = query.name.as_str().to_string();
640        if selection.excluded_by_filters == 0
641            && self.occurs_in_document(uri, &name_parts(&name).simple)
642        {
643            return resolution_error(SymbolResolutionData::NotDefinedInFile { name });
644        }
645        resolution_error(SymbolResolutionData::NotFound {
646            name,
647            excluded_by_filters: selection.excluded_by_filters,
648        })
649    }
650
651    /// Whether `word` occurs as a whole word in the tracked document text.
652    fn occurs_in_document(&self, uri: &lsp_types::Uri, word: &str) -> bool {
653        let Some(path) = uri_to_path(uri) else {
654            return false;
655        };
656        self.document_tracker()
657            .line_window(&path, 0, MAX_SCAN_LINES)
658            .is_some_and(|text| {
659                text.split('\n')
660                    .any(|line| !whole_word_occurrences(line, word).is_empty())
661            })
662    }
663
664    /// Run `call` at the position `target` resolves to for `tool`, attaching
665    /// the resolved symbol to the result when the call was name-addressed.
666    ///
667    /// # Errors
668    ///
669    /// As [`Self::resolve_symbol_target`], then whatever `call` returns.
670    pub async fn with_resolved_target<T, F, Fut>(
671        &self,
672        file_path: ClientPath,
673        target: SymbolTarget,
674        tool: AddressableTool,
675        call: F,
676    ) -> Result<Addressed<T>>
677    where
678        F: FnOnce(ClientPath, Position) -> Fut,
679        Fut: std::future::Future<Output = Result<T>>,
680    {
681        let resolved = self.resolve_symbol_target(&file_path, target, tool).await?;
682        let result = call(file_path, resolved.position).await?;
683        Ok(Addressed {
684            result,
685            resolved_symbol: resolved.resolved,
686        })
687    }
688}
689
690impl ServerText for ResolvedSymbol {
691    fn redact_server_text(&mut self, redactions: &Redactions) {
692        let Self {
693            name,
694            kind: _,
695            container,
696            position: _,
697            position_source: _,
698        } = self;
699        redactions.note_payload(name);
700        if let Some(container) = container {
701            redactions.note_payload(container);
702        }
703    }
704}
705
706impl<T: ServerText> ServerText for Addressed<T> {
707    fn redact_server_text(&mut self, redactions: &Redactions) {
708        let Self {
709            result,
710            resolved_symbol,
711        } = self;
712        result.redact_server_text(redactions);
713        resolved_symbol.redact_server_text(redactions);
714    }
715}
716
717#[cfg(test)]
718#[allow(deprecated, reason = "LSP servers still send this deprecated field")]
719mod tests {
720    use super::*;
721
722    fn entry(name: &str, kind: SymbolKind, container: Option<&str>) -> SymbolEntry {
723        let range = lsp_types::Range::default();
724        SymbolEntry {
725            name: name.to_string(),
726            kind,
727            container: container.map(str::to_string),
728            range,
729            selection_range: Some(range),
730        }
731    }
732
733    fn query(name: &str, kind: Option<SymbolKind>, container: Option<&str>) -> SymbolQuery {
734        SymbolQuery {
735            name: SymbolName::try_new(name).unwrap(),
736            kind,
737            container: container.map(|c| SymbolName::try_new(c).unwrap()),
738        }
739    }
740
741    #[test]
742    fn symbol_names_reject_blank_and_oversized_text() {
743        assert_eq!(SymbolName::try_new("  parse ").unwrap().as_str(), "parse");
744        assert_eq!(SymbolName::try_new(""), Err(SymbolNameError::Blank));
745        assert_eq!(SymbolName::try_new(" \t"), Err(SymbolNameError::Blank));
746        assert_eq!(
747            SymbolName::try_new("x".repeat(MAX_SYMBOL_NAME_BYTES + 1)),
748            Err(SymbolNameError::TooLong)
749        );
750        assert!(SymbolName::try_new("x".repeat(MAX_SYMBOL_NAME_BYTES)).is_ok());
751    }
752
753    #[test]
754    fn name_parts_strip_generics_receivers_and_qualifiers() {
755        let parts = |raw| name_parts(raw);
756        assert_eq!(parts("bar").simple, "bar");
757        assert_eq!(
758            parts("Foo::bar"),
759            NameParts {
760                simple: "bar".to_string(),
761                qualifier: Some("Foo".to_string())
762            }
763        );
764        assert_eq!(parts("Vec<T>::push").qualifier.as_deref(), Some("Vec"));
765        assert_eq!(parts("Foo.method").simple, "method");
766        assert_eq!(
767            parts("(*Server).Start"),
768            NameParts {
769                simple: "Start".to_string(),
770                qualifier: Some("Server".to_string())
771            }
772        );
773        assert_eq!(parts("map<K, V>").simple, "map");
774    }
775
776    #[test]
777    fn generic_stripping_tracks_depth_and_leaves_operators_intact() {
778        assert_eq!(strip_generics("Foo<T: Into<Vec<u8>>>"), "Foo");
779        assert_eq!(strip_generics("operator<"), "operator<");
780        assert_eq!(strip_generics("operator<<"), "operator<<");
781        assert_eq!(strip_generics("operator<=>"), "operator<=>");
782        assert_eq!(strip_generics("Foo::operator<<"), "Foo::operator<<");
783        assert_eq!(strip_generics("operatorCount<T>"), "operatorCount");
784        assert_eq!(strip_generics("get_operator<T>"), "get_operator");
785        assert_eq!(strip_generics("Foo<T"), "Foo<T");
786        assert_eq!(strip_generics("a>b"), "a>b");
787    }
788
789    #[test]
790    fn container_names_unwrap_impl_blocks() {
791        assert_eq!(container_name("impl Foo"), "Foo");
792        assert_eq!(container_name("impl<T> Foo<T>"), "Foo");
793        assert_eq!(container_name("impl<T: Into<Vec<u8>>> Foo<T>"), "Foo");
794        assert_eq!(container_name("impl Display for Foo"), "Foo");
795        assert_eq!(container_name("impl<T> Tr<T> for Foo<T>"), "Foo");
796        assert_eq!(container_name("Config"), "Config");
797        assert_eq!(container_name("implementation"), "implementation");
798    }
799
800    #[test]
801    fn whole_word_search_is_unicode_aware() {
802        assert_eq!(
803            whole_word_occurrences("foo foo_bar xfoo foo", "foo"),
804            [0, 17]
805        );
806        assert!(whole_word_occurrences("éfoo", "foo").is_empty());
807        assert!(whole_word_occurrences("foo日本", "foo").is_empty());
808        assert_eq!(whole_word_occurrences("(foo)", "foo"), [1]);
809    }
810
811    #[test]
812    fn an_unqualified_name_matches_every_symbol_with_that_simple_name() {
813        let entries = [
814            entry("Handle", SymbolKind::Function, None),
815            entry("(*Server).Handle", SymbolKind::Method, None),
816            entry("Foo::new", SymbolKind::Method, None),
817        ];
818        let selection = select_candidates(&entries, &query("Handle", None, None));
819        assert_eq!(
820            selection.matched.len(),
821            2,
822            "a free function must not shadow a method"
823        );
824
825        let narrowed = select_candidates(&entries, &query("Handle", None, Some("Server")));
826        assert_eq!(narrowed.matched.len(), 1);
827        assert_eq!(narrowed.matched[0].name, "(*Server).Handle");
828    }
829
830    #[test]
831    fn parameter_lists_are_stripped_so_overloads_match_by_simple_name() {
832        assert_eq!(name_parts("bar(int)").simple, "bar");
833        assert_eq!(name_parts("foo(java.util.List)").simple, "foo");
834        assert_eq!(
835            name_parts("Foo.bar(int, String)").qualifier.as_deref(),
836            Some("Foo")
837        );
838        assert_eq!(name_parts("operator()").simple, "operator()");
839        assert_eq!(name_parts("Foo::operator()").simple, "operator()");
840        assert_eq!(name_parts("operatorCount(int)").simple, "operatorCount");
841        assert_eq!(name_parts("get_operator(x)").simple, "get_operator");
842        assert_eq!(name_parts("(*T).M(x)").simple, "M");
843        let entries = [
844            entry("bar(int)", SymbolKind::Method, Some("Foo")),
845            entry("bar(String)", SymbolKind::Method, Some("Foo")),
846            entry("Foo(int)", SymbolKind::Constructor, Some("Foo")),
847        ];
848        assert_eq!(
849            select_candidates(&entries, &query("bar", None, None))
850                .matched
851                .len(),
852            2
853        );
854        assert_eq!(
855            select_candidates(&entries, &query("Foo.bar", None, None))
856                .matched
857                .len(),
858            2
859        );
860        assert_eq!(
861            select_candidates(&entries, &query("Foo", None, None))
862                .matched
863                .len(),
864            1
865        );
866    }
867
868    #[test]
869    fn simple_name_matches_when_no_exact_name_exists() {
870        let entries = [
871            entry("Foo::new", SymbolKind::Method, None),
872            entry("Bar::new", SymbolKind::Method, None),
873        ];
874        let selection = select_candidates(&entries, &query("new", None, None));
875        assert_eq!(selection.matched.len(), 2);
876    }
877
878    #[test]
879    fn container_and_kind_narrow_before_ambiguity() {
880        let entries = [
881            entry("new", SymbolKind::Method, Some("impl Foo<T>")),
882            entry("new", SymbolKind::Method, Some("impl Bar")),
883            entry("new", SymbolKind::Function, None),
884        ];
885        let by_container = select_candidates(&entries, &query("new", None, Some("Foo")));
886        assert_eq!(by_container.matched.len(), 1);
887        assert_eq!(by_container.excluded_by_filters, 2);
888
889        let by_kind = select_candidates(&entries, &query("new", Some(SymbolKind::Function), None));
890        assert_eq!(by_kind.matched.len(), 1);
891
892        let by_qualifier = select_candidates(&entries, &query("Bar::new", None, None));
893        assert_eq!(by_qualifier.matched.len(), 1);
894        assert_eq!(
895            by_qualifier.matched[0].container.as_deref(),
896            Some("impl Bar")
897        );
898    }
899
900    #[test]
901    fn flattening_keeps_document_order_and_parent_names() {
902        let range = lsp_types::Range::default();
903        let child = lsp_types::DocumentSymbol {
904            name: "method".to_string(),
905            detail: None,
906            kind: SymbolKind::Method,
907            tags: None,
908            deprecated: None,
909            range,
910            selection_range: range,
911            children: None,
912        };
913        let parent = lsp_types::DocumentSymbol {
914            name: "Type".to_string(),
915            kind: SymbolKind::Struct,
916            children: Some(vec![child]),
917            ..lsp_types::DocumentSymbol {
918                name: String::new(),
919                detail: None,
920                kind: SymbolKind::Struct,
921                tags: None,
922                deprecated: None,
923                range,
924                selection_range: range,
925                children: None,
926            }
927        };
928        let entries = flatten_symbols(DocumentSymbolResponse::DocumentSymbolList(vec![parent]));
929        assert_eq!(
930            entries
931                .iter()
932                .map(|e| (e.name.as_str(), e.container.as_deref()))
933                .collect::<Vec<_>>(),
934            [("Type", None), ("method", Some("Type"))]
935        );
936    }
937
938    #[test]
939    fn spells_at_checks_the_column_in_utf16_units() {
940        let line = "let 日本 = foo;";
941        let col = u32::try_from(
942            line.encode_utf16()
943                .position(|u| u == u16::from(b'f'))
944                .unwrap(),
945        )
946        .unwrap();
947        assert!(spells_at(line, col + 1, "foo"));
948        assert!(!spells_at(line, col, "foo"));
949        assert!(!spells_at(line, 999, "foo"));
950    }
951}
952
953#[cfg(test)]
954mod resolver_tests {
955    use std::sync::Arc;
956    use std::{assert_matches, fs};
957
958    use serde_json::{Value, json};
959    use tempfile::TempDir;
960    use tokio::io::BufReader;
961    use url::Url;
962
963    use super::super::testing::{
964        pos, read_framed_message, translator_with_capabilities, write_response,
965    };
966    use super::*;
967    use crate::config::ServerId;
968    use crate::test_lsp::client_path;
969
970    fn caps(hover: bool) -> lsp_types::ServerCapabilities {
971        lsp_types::ServerCapabilities {
972            hover_provider: hover.then_some(lsp_types::HoverProvider::Bool(true)),
973            document_symbol_provider: Some(lsp_types::DocumentSymbolProvider::Bool(true)),
974            ..Default::default()
975        }
976    }
977
978    fn range(l1: u32, c1: u32, l2: u32, c2: u32) -> Value {
979        json!({"start": {"line": l1, "character": c1}, "end": {"line": l2, "character": c2}})
980    }
981
982    fn hierarchical(
983        name: &str,
984        kind: u32,
985        whole: Value,
986        selection: Value,
987        children: Value,
988    ) -> Value {
989        let mut symbol = json!({"name": name, "kind": kind});
990        symbol["range"] = whole;
991        symbol["selectionRange"] = selection;
992        symbol["children"] = children;
993        symbol
994    }
995
996    fn query_target(name: &str, kind: Option<SymbolKind>, container: Option<&str>) -> SymbolTarget {
997        SymbolTarget::Name(SymbolQuery {
998            name: SymbolName::try_new(name).unwrap(),
999            kind,
1000            container: container.map(|c| SymbolName::try_new(c).unwrap()),
1001        })
1002    }
1003
1004    /// Runs a name resolution against a fake server answering
1005    /// `documentSymbol` with `symbols` for a file holding `source`.
1006    async fn resolve(
1007        source: &str,
1008        symbols: impl FnOnce(&Url) -> Value,
1009        target: SymbolTarget,
1010    ) -> Result<ResolvedTarget> {
1011        let dir = TempDir::new().unwrap();
1012        let id = ServerId::from_static("rust");
1013        let (translator, mut server) = translator_with_capabilities(&dir, &id, caps(true));
1014        let path = dir.path().join("main.rs");
1015        fs::write(&path, source).unwrap();
1016        let uri = Url::from_file_path(dunce::canonicalize(&path).unwrap()).unwrap();
1017        let answer = symbols(&uri);
1018
1019        let translator = Arc::new(translator);
1020        let handle = {
1021            let translator = Arc::clone(&translator);
1022            let path = client_path(&path);
1023            tokio::spawn(async move {
1024                translator
1025                    .resolve_symbol_target(&path, target, AddressableTool::Hover)
1026                    .await
1027            })
1028        };
1029
1030        let mut wire = BufReader::new(&mut server.write_stdout);
1031        let opened = read_framed_message(&mut wire).await;
1032        assert_eq!(opened["method"], "textDocument/didOpen");
1033        let request = read_framed_message(&mut wire).await;
1034        assert_eq!(request["method"], "textDocument/documentSymbol");
1035        write_response(&mut server.read_half_stdin, &request["id"], answer).await;
1036        handle.await.unwrap()
1037    }
1038
1039    fn resolution(result: Result<ResolvedTarget>) -> SymbolResolutionData {
1040        match result.unwrap_err() {
1041            Error::SymbolResolution(data) => *data,
1042            other => panic!("expected a symbol resolution error, got {other:?}"),
1043        }
1044    }
1045
1046    #[tokio::test]
1047    async fn test_unique_hierarchical_symbol_resolves_to_its_verified_selection_range() {
1048        let resolved = resolve(
1049            "pub fn parse_config() {}\n",
1050            |_| {
1051                json!([hierarchical(
1052                    "parse_config",
1053                    12,
1054                    range(0, 0, 0, 24),
1055                    range(0, 7, 0, 19),
1056                    json!([])
1057                )])
1058            },
1059            query_target("parse_config", None, None),
1060        )
1061        .await
1062        .unwrap();
1063
1064        assert_eq!(resolved.position, pos(1, 8));
1065        let symbol = resolved.resolved.unwrap();
1066        assert_eq!(symbol.position_source, PositionSource::SelectionRange);
1067        assert_eq!(symbol.kind, 12);
1068    }
1069
1070    #[tokio::test]
1071    async fn test_flat_symbol_is_located_by_its_only_whole_word_occurrence() {
1072        let resolved = resolve(
1073            "pub fn foo() {}\n",
1074            |uri| {
1075                json!([{
1076                    "name": "foo", "kind": 12,
1077                    "location": {"uri": uri.as_str(), "range": range(0, 0, 0, 15)}
1078                }])
1079            },
1080            query_target("foo", None, None),
1081        )
1082        .await
1083        .unwrap();
1084
1085        assert_eq!(resolved.position, pos(1, 8));
1086        assert_eq!(
1087            resolved.resolved.unwrap().position_source,
1088            PositionSource::Inferred
1089        );
1090    }
1091
1092    #[tokio::test]
1093    async fn test_a_string_literal_before_the_identifier_never_becomes_the_position() {
1094        let result = resolve(
1095            "#[cfg(feature = \"foo\")]\npub fn foo() {}\n",
1096            |uri| {
1097                json!([{
1098                    "name": "foo", "kind": 12,
1099                    "location": {"uri": uri.as_str(), "range": range(0, 0, 1, 15)}
1100                }])
1101            },
1102            query_target("foo", None, None),
1103        )
1104        .await;
1105
1106        assert_matches!(
1107            resolution(result),
1108            SymbolResolutionData::PositionUnverified { .. }
1109        );
1110    }
1111
1112    #[tokio::test]
1113    async fn test_a_wrong_selection_range_falls_back_to_the_unique_occurrence() {
1114        let resolved = resolve(
1115            "pub fn foo() {}\n",
1116            |_| {
1117                json!([hierarchical(
1118                    "foo",
1119                    12,
1120                    range(0, 0, 0, 15),
1121                    range(0, 0, 0, 3),
1122                    json!([])
1123                )])
1124            },
1125            query_target("foo", None, None),
1126        )
1127        .await
1128        .unwrap();
1129
1130        assert_eq!(resolved.position, pos(1, 8));
1131        assert_eq!(
1132            resolved.resolved.unwrap().position_source,
1133            PositionSource::Inferred
1134        );
1135    }
1136
1137    #[tokio::test]
1138    async fn test_same_named_methods_are_listed_with_their_positions_not_picked() {
1139        let result = resolve(
1140            "impl Foo {\n    fn new() {}\n}\nimpl Bar {\n    fn new() {}\n}\n",
1141            |_| {
1142                json!([
1143                    hierarchical(
1144                        "impl Foo",
1145                        5,
1146                        range(0, 0, 2, 1),
1147                        range(0, 5, 0, 8),
1148                        json!([hierarchical(
1149                            "new",
1150                            6,
1151                            range(1, 4, 1, 15),
1152                            range(1, 7, 1, 10),
1153                            json!([])
1154                        )])
1155                    ),
1156                    hierarchical(
1157                        "impl Bar",
1158                        5,
1159                        range(3, 0, 5, 1),
1160                        range(3, 5, 3, 8),
1161                        json!([hierarchical(
1162                            "new",
1163                            6,
1164                            range(4, 4, 4, 15),
1165                            range(4, 7, 4, 10),
1166                            json!([])
1167                        )])
1168                    ),
1169                ])
1170            },
1171            query_target("new", None, None),
1172        )
1173        .await;
1174
1175        let SymbolResolutionData::Ambiguous {
1176            candidates,
1177            truncated,
1178            ..
1179        } = resolution(result)
1180        else {
1181            panic!("expected an ambiguity");
1182        };
1183        assert!(!truncated);
1184        assert_eq!(
1185            candidates
1186                .iter()
1187                .map(|c| (c.container.as_deref(), c.line, c.character))
1188                .collect::<Vec<_>>(),
1189            [(Some("impl Foo"), 2, 8), (Some("impl Bar"), 5, 8)]
1190        );
1191    }
1192
1193    #[tokio::test]
1194    async fn test_container_qualifier_narrows_to_one_symbol() {
1195        let resolved = resolve(
1196            "impl<T> Foo<T> {\n    fn new() {}\n}\nimpl Bar {\n    fn new() {}\n}\n",
1197            |_| {
1198                json!([
1199                    hierarchical(
1200                        "impl<T> Foo<T>",
1201                        5,
1202                        range(0, 0, 2, 1),
1203                        range(0, 8, 0, 14),
1204                        json!([hierarchical(
1205                            "new",
1206                            6,
1207                            range(1, 4, 1, 15),
1208                            range(1, 7, 1, 10),
1209                            json!([])
1210                        )])
1211                    ),
1212                    hierarchical(
1213                        "impl Bar",
1214                        5,
1215                        range(3, 0, 5, 1),
1216                        range(3, 5, 3, 8),
1217                        json!([hierarchical(
1218                            "new",
1219                            6,
1220                            range(4, 4, 4, 15),
1221                            range(4, 7, 4, 10),
1222                            json!([])
1223                        )])
1224                    ),
1225                ])
1226            },
1227            query_target("new", None, Some("Foo")),
1228        )
1229        .await
1230        .unwrap();
1231
1232        assert_eq!(resolved.position, pos(2, 8));
1233    }
1234
1235    #[tokio::test]
1236    async fn test_a_method_named_with_its_parameter_list_resolves_and_overloads_are_ambiguous() {
1237        let source =
1238            "class Foo {\n  void bar(int a) {}\n  void bar(String s) {}\n  void baz(int a) {}\n}\n";
1239        let symbols = |_: &Url| {
1240            json!([hierarchical(
1241                "Foo",
1242                5,
1243                range(0, 0, 4, 1),
1244                range(0, 6, 0, 9),
1245                json!([
1246                    hierarchical(
1247                        "bar(int)",
1248                        6,
1249                        range(1, 2, 1, 19),
1250                        range(1, 7, 1, 10),
1251                        json!([])
1252                    ),
1253                    hierarchical(
1254                        "bar(String)",
1255                        6,
1256                        range(2, 2, 2, 22),
1257                        range(2, 7, 2, 10),
1258                        json!([])
1259                    ),
1260                    hierarchical(
1261                        "baz(int)",
1262                        6,
1263                        range(3, 2, 3, 19),
1264                        range(3, 7, 3, 10),
1265                        json!([])
1266                    ),
1267                ])
1268            )])
1269        };
1270
1271        let single = resolve(source, symbols, query_target("baz", None, None))
1272            .await
1273            .unwrap();
1274        assert_eq!(single.position, pos(4, 8));
1275
1276        let overloads = resolve(source, symbols, query_target("Foo.bar", None, None)).await;
1277        let SymbolResolutionData::Ambiguous { candidates, .. } = resolution(overloads) else {
1278            panic!("overloads must be ambiguous, never not_defined_in_file");
1279        };
1280        assert_eq!(candidates.len(), 2);
1281    }
1282
1283    #[tokio::test]
1284    async fn test_a_free_function_does_not_shadow_a_same_named_method() {
1285        let source = "func Handle() {}\nfunc (s *Server) Handle() {}\n";
1286        let symbols = |_: &Url| {
1287            json!([
1288                hierarchical(
1289                    "Handle",
1290                    12,
1291                    range(0, 0, 0, 16),
1292                    range(0, 5, 0, 11),
1293                    json!([])
1294                ),
1295                hierarchical(
1296                    "(*Server).Handle",
1297                    6,
1298                    range(1, 0, 1, 30),
1299                    range(1, 17, 1, 23),
1300                    json!([])
1301                ),
1302            ])
1303        };
1304
1305        let unqualified = resolve(source, symbols, query_target("Handle", None, None)).await;
1306        assert_matches!(
1307            resolution(unqualified),
1308            SymbolResolutionData::Ambiguous { .. }
1309        );
1310
1311        let narrowed = resolve(
1312            source,
1313            symbols,
1314            query_target("Handle", None, Some("Server")),
1315        )
1316        .await
1317        .unwrap();
1318        assert_eq!(narrowed.position, pos(2, 18));
1319    }
1320
1321    #[tokio::test]
1322    async fn test_a_name_only_referenced_in_the_file_is_not_defined_there() {
1323        let result = resolve(
1324            "use config::Config;\nfn main() {}\n",
1325            |_| {
1326                json!([hierarchical(
1327                    "main",
1328                    12,
1329                    range(1, 0, 1, 12),
1330                    range(1, 3, 1, 7),
1331                    json!([])
1332                )])
1333            },
1334            query_target("Config", None, None),
1335        )
1336        .await;
1337
1338        assert_eq!(
1339            resolution(result),
1340            SymbolResolutionData::NotDefinedInFile {
1341                name: "Config".to_string()
1342            }
1343        );
1344    }
1345
1346    #[tokio::test]
1347    async fn test_an_absent_name_is_not_found_and_a_filtered_out_one_says_so() {
1348        let absent = resolve(
1349            "fn main() {}\n",
1350            |_| json!([]),
1351            query_target("nope", None, None),
1352        )
1353        .await;
1354        assert_eq!(
1355            resolution(absent),
1356            SymbolResolutionData::NotFound {
1357                name: "nope".to_string(),
1358                excluded_by_filters: 0
1359            }
1360        );
1361
1362        let filtered = resolve(
1363            "fn main() {}\n",
1364            |_| {
1365                json!([hierarchical(
1366                    "main",
1367                    12,
1368                    range(0, 0, 0, 12),
1369                    range(0, 3, 0, 7),
1370                    json!([])
1371                )])
1372            },
1373            query_target("main", Some(SymbolKind::Class), None),
1374        )
1375        .await;
1376        assert_eq!(
1377            resolution(filtered),
1378            SymbolResolutionData::NotFound {
1379                name: "main".to_string(),
1380                excluded_by_filters: 1
1381            }
1382        );
1383    }
1384
1385    #[tokio::test]
1386    async fn test_a_position_target_passes_through_without_any_request() {
1387        let dir = TempDir::new().unwrap();
1388        let (translator, _server) =
1389            translator_with_capabilities(&dir, &ServerId::from_static("rust"), caps(false));
1390
1391        let resolved = translator
1392            .resolve_symbol_target(
1393                &client_path("/never/opened.rs"),
1394                SymbolTarget::Position(pos(3, 4)),
1395                AddressableTool::Hover,
1396            )
1397            .await
1398            .unwrap();
1399
1400        assert_eq!(resolved.position, pos(3, 4));
1401        assert!(resolved.resolved.is_none());
1402    }
1403
1404    #[tokio::test]
1405    async fn test_a_missing_tool_capability_fails_before_any_symbol_request() {
1406        let dir = TempDir::new().unwrap();
1407        let (translator, _server) =
1408            translator_with_capabilities(&dir, &ServerId::from_static("rust"), caps(false));
1409        let path = dir.path().join("main.rs");
1410        fs::write(&path, "fn main() {}\n").unwrap();
1411
1412        let err = translator
1413            .resolve_symbol_target(
1414                &client_path(&path),
1415                query_target("main", None, None),
1416                AddressableTool::Hover,
1417            )
1418            .await
1419            .unwrap_err();
1420
1421        assert_matches!(
1422            err,
1423            Error::CapabilityNotSupported {
1424                capability: Capability::Hover,
1425                ..
1426            },
1427            "{err:?}"
1428        );
1429    }
1430
1431    #[tokio::test]
1432    async fn test_a_name_in_a_file_outside_the_workspace_fails_containment_not_resolution() {
1433        let dir = TempDir::new().unwrap();
1434        let outside = TempDir::new().unwrap();
1435        let (translator, _server) =
1436            translator_with_capabilities(&dir, &ServerId::from_static("rust"), caps(true));
1437        let path = outside.path().join("main.rs");
1438        fs::write(&path, "fn main() {}\n").unwrap();
1439
1440        let err = translator
1441            .resolve_symbol_target(
1442                &client_path(&path),
1443                query_target("main", None, None),
1444                AddressableTool::Hover,
1445            )
1446            .await
1447            .unwrap_err();
1448
1449        assert_matches!(err, Error::PathOutsideWorkspace(_), "{err:?}");
1450    }
1451}
1452
1453#[cfg(test)]
1454mod server_text_tests {
1455    use super::*;
1456    use crate::bridge::translator::dto::HoverResult;
1457
1458    const SECRET: &str = "SuperSecretValue123";
1459
1460    #[test]
1461    fn test_resolved_symbol_identifiers_pass_through_and_result_is_redacted() {
1462        let set = Redactions::new([("API_TOKEN".to_owned(), SECRET.to_owned())]);
1463        let mut addressed = Addressed {
1464            result: HoverResult {
1465                contents: format!("doc {SECRET}"),
1466                range: None,
1467                positions_degraded: None,
1468            },
1469            resolved_symbol: Some(ResolvedSymbol {
1470                name: format!("sym_{SECRET}"),
1471                kind: 12,
1472                container: Some(format!("c_{SECRET}")),
1473                position: Position2D {
1474                    line: 1,
1475                    character: 1,
1476                },
1477                position_source: PositionSource::SelectionRange,
1478            }),
1479        };
1480
1481        addressed.redact_server_text(&set);
1482
1483        assert_eq!(addressed.result.contents, "doc [redacted:API_TOKEN]");
1484        let symbol = addressed.resolved_symbol.unwrap();
1485        assert_eq!(symbol.name, format!("sym_{SECRET}"));
1486        assert_eq!(symbol.container, Some(format!("c_{SECRET}")));
1487    }
1488}