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