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