1use 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
27pub const MAX_SYMBOL_NAME_BYTES: usize = 256;
29
30const MAX_CANDIDATES: usize = 50;
32
33const MAX_FLATTENED_SYMBOLS: usize = 20_000;
35
36const MAX_WINDOW_LINES: u32 = 500;
38
39const MAX_SCAN_LINES: usize = 50_000;
41
42#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct SymbolName(BoundedText<MAX_SYMBOL_NAME_BYTES>);
56
57impl SymbolName {
58 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#[derive(Debug, Clone, PartialEq, Eq)]
74pub struct SymbolQuery {
75 pub name: SymbolName,
77 pub kind: Option<SymbolKind>,
79 pub container: Option<SymbolName>,
81}
82
83#[derive(Debug, Clone, PartialEq, Eq)]
85pub enum SymbolTarget {
86 Position(Position),
88 Name(SymbolQuery),
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum AddressableTool {
95 Hover,
97 Definition,
99 References,
101 Implementation,
103 TypeDefinition,
105 PrepareCallHierarchy,
107 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, JsonSchema)]
127#[serde(rename_all = "snake_case")]
128pub enum PositionSource {
129 SelectionRange,
131 Inferred,
135}
136
137#[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema)]
140pub struct ResolvedSymbol {
141 pub name: String,
143 pub kind: u32,
145 pub container: Option<String>,
147 pub position: Position2D,
149 pub position_source: PositionSource,
151}
152
153#[derive(Debug, Clone, Serialize, JsonSchema)]
156pub struct Addressed<T> {
157 #[serde(flatten)]
159 pub result: T,
160 #[serde(skip_serializing_if = "Option::is_none")]
162 pub resolved_symbol: Option<ResolvedSymbol>,
163}
164
165#[derive(Debug, Clone, PartialEq, Eq)]
167pub struct ResolvedTarget {
168 pub position: Position,
170 pub resolved: Option<ResolvedSymbol>,
172}
173
174#[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#[derive(Debug, Clone, PartialEq, Eq)]
225struct NameParts {
226 simple: String,
227 qualifier: Option<String>,
228}
229
230fn 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
243fn 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
271fn 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
283fn 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
322fn 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
336fn 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
364fn 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
381struct Selection<'a> {
383 matched: Vec<&'a SymbolEntry>,
384 excluded_by_filters: usize,
385}
386
387fn 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
423enum IdentifierPosition {
425 Verified(Position2D, PositionSource),
426 Unverified(Position2D),
427}
428
429fn 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 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 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 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 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 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}