1use lsp_types::{
4 DocumentSymbol, DocumentSymbolParams, PartialResultParams, TextDocumentIdentifier,
5 WorkDoneProgressParams, WorkspaceSymbolParams as LspWorkspaceSymbolParams,
6};
7
8use super::Translator;
9use super::dto::{
10 DocumentSymbolsResult, Location, Symbol, WorkspaceSymbol, WorkspaceSymbolResult,
11 lsp_kind_to_u32,
12};
13use super::encoding_ctx::EncodingCtx;
14use super::kind_filter::SymbolKindFilter;
15use super::navigation::MAX_NORMALIZED_LOCATIONS;
16use super::routing::{
17 Capability, FileLocalCapability, IndexingGate, PreparedDocument, WorkspaceRouteLookup,
18 lookup_workspace_route,
19};
20use crate::bridge::ClientPath;
21use crate::config::ToolKind;
22use crate::error::{Error, Result};
23use crate::util::lock_std;
24
25fn validate_query_length(query: &str) -> Result<()> {
27 const MAX_QUERY_LENGTH: usize = 1000;
28
29 if query.len() > MAX_QUERY_LENGTH {
30 return Err(Error::InvalidToolParams(format!(
31 "Query too long: {} bytes (max {MAX_QUERY_LENGTH})",
32 query.len()
33 )));
34 }
35
36 Ok(())
37}
38
39pub(super) struct FetchedSymbols {
43 pub(super) doc: PreparedDocument,
44 pub(super) ctx: EncodingCtx,
45 pub(super) response: Option<lsp_types::DocumentSymbolResponse>,
46}
47
48fn convert_document_symbol<'a>(
55 symbol: DocumentSymbol,
56 ctx: &'a EncodingCtx,
57 uri: &'a lsp_types::Uri,
58) -> futures::future::BoxFuture<'a, Symbol> {
59 Box::pin(async move {
60 let range = ctx.normalize_range(uri, symbol.range).await;
61 let selection_range = ctx.normalize_range(uri, symbol.selection_range).await;
62 let children = match symbol.children {
63 Some(children) => {
64 let mut result = Vec::with_capacity(children.len());
65 for child in children {
66 result.push(convert_document_symbol(child, ctx, uri).await);
67 }
68 Some(result)
69 }
70 None => None,
71 };
72
73 Symbol {
74 name: symbol.name,
75 kind: lsp_kind_to_u32(symbol.kind),
76 range,
77 selection_range,
78 children,
79 }
80 })
81}
82
83#[derive(Debug)]
85pub(super) enum DocumentSymbolTree {
86 Hierarchical(Vec<Symbol>),
88 Flat(Vec<FlatSymbol>),
90}
91
92impl DocumentSymbolTree {
93 fn into_symbols(self) -> Vec<Symbol> {
95 match self {
96 Self::Hierarchical(symbols) => symbols,
97 Self::Flat(symbols) => symbols.into_iter().map(|flat| flat.symbol).collect(),
98 }
99 }
100}
101
102#[derive(Debug)]
104pub(super) struct FlatSymbol {
105 pub(super) symbol: Symbol,
106 pub(super) container_name: Option<String>,
107}
108
109pub(super) async fn symbol_tree(
118 response: Option<lsp_types::DocumentSymbolResponse>,
119 ctx: &EncodingCtx,
120 uri: &lsp_types::Uri,
121) -> DocumentSymbolTree {
122 match response {
123 Some(lsp_types::DocumentSymbolResponse::SymbolInformationList(symbols)) => {
124 let mut result = Vec::with_capacity(symbols.len());
125 for sym in symbols {
126 let range = ctx.normalize_range(uri, sym.location.range).await;
127 let selection_range = range.clone();
128 result.push(FlatSymbol {
129 symbol: Symbol {
130 name: sym.base_symbol_information.name,
131 kind: lsp_kind_to_u32(sym.base_symbol_information.kind),
132 range,
133 selection_range,
134 children: None,
135 },
136 container_name: sym.base_symbol_information.container_name,
137 });
138 }
139 DocumentSymbolTree::Flat(result)
140 }
141 Some(lsp_types::DocumentSymbolResponse::DocumentSymbolList(symbols)) => {
142 let mut result = Vec::with_capacity(symbols.len());
143 for sym in symbols {
144 result.push(convert_document_symbol(sym, ctx, uri).await);
145 }
146 DocumentSymbolTree::Hierarchical(result)
147 }
148 None => DocumentSymbolTree::Hierarchical(vec![]),
149 }
150}
151
152impl Translator {
153 pub async fn handle_document_symbols(
160 &self,
161 file_path: ClientPath,
162 ) -> Result<DocumentSymbolsResult> {
163 let FetchedSymbols { doc, ctx, response } =
164 self.request_document_symbols(&file_path).await?;
165 let tree = symbol_tree(response, &ctx, doc.uri()).await;
166
167 Ok(DocumentSymbolsResult {
168 symbols: tree.into_symbols(),
169 positions_degraded: ctx.positions_degraded(),
170 })
171 }
172
173 pub(super) async fn request_document_symbols(
181 &self,
182 file_path: &ClientPath,
183 ) -> Result<FetchedSymbols> {
184 let doc = self
185 .prepare_gated_document(
186 file_path,
187 IndexingGate::FileLocal(FileLocalCapability::DocumentSymbols),
188 )
189 .await?;
190 self.fetch_document_symbols(doc).await
191 }
192
193 pub(super) async fn fetch_document_symbols(
200 &self,
201 doc: PreparedDocument,
202 ) -> Result<FetchedSymbols> {
203 let ctx = self.encoding_ctx(doc.server_id());
204 let params = DocumentSymbolParams {
205 text_document: TextDocumentIdentifier {
206 uri: doc.uri().clone(),
207 },
208 work_done_progress_params: WorkDoneProgressParams::default(),
209 partial_result_params: PartialResultParams::default(),
210 };
211 let client = doc.client();
212 let response = client
213 .request_typed::<lsp_types::DocumentSymbolRequest>(params, client.request_timeout())
214 .await?;
215 Ok(FetchedSymbols { doc, ctx, response })
216 }
217
218 #[expect(
233 clippy::too_many_lines,
234 reason = "linear request/response flow; splitting would scatter the steps"
235 )]
236 pub async fn handle_workspace_symbol(
237 &self,
238 query: String,
239 kind_filter: Option<SymbolKindFilter>,
240 limit: u32,
241 ) -> Result<WorkspaceSymbolResult> {
242 validate_query_length(&query)?;
243 let kind_filter = kind_filter.map(|filter| u32::from(filter.kind()));
244
245 let router = self.router_snapshot();
250 let lookup = {
251 let servers = lock_std(&self.servers);
252 lookup_workspace_route(
253 || router.resolve_any(ToolKind::WorkspaceSymbols).cloned(),
254 |id| servers.client(id).is_some(),
255 |id| servers.is_expected(id),
256 || !servers.any_expected(),
257 )
258 };
259 let server_id = match lookup {
260 WorkspaceRouteLookup::Registered(id) => id,
261 WorkspaceRouteLookup::Initializing(server_id) => {
262 return Err(Error::ServerInitializing { server_id });
263 }
264 WorkspaceRouteLookup::AllInitializing => {
265 return Err(Error::WorkspaceServersInitializing);
266 }
267 WorkspaceRouteLookup::Dangling(_) => return Err(Error::NoServerConfigured),
268 WorkspaceRouteLookup::NothingConfigured => {
269 let failures = self.startup_failures();
270 return Err(if failures.is_empty() {
271 Error::NoServerConfigured
272 } else {
273 Error::AllServersFailedToInit { failures }
274 });
275 }
276 WorkspaceRouteLookup::NoClaimant => {
279 return Err(self
280 .configured_router
281 .resolve_any(ToolKind::WorkspaceSymbols)
282 .ok()
283 .and_then(|id| self.startup_failure(id))
284 .map_or(
285 Error::NoServerForWorkspaceTool {
286 tool: ToolKind::WorkspaceSymbols,
287 },
288 |failure| Error::ServerFailedToStart(Box::new(failure)),
289 ));
290 }
291 };
292 self.respawn_if_dead(&server_id).await?;
293 let (client, expected) = {
294 let servers = lock_std(&self.servers);
295 (servers.client(&server_id), servers.is_expected(&server_id))
296 };
297 let client = client.ok_or_else(|| {
298 if expected {
299 Error::ServerInitializing {
300 server_id: server_id.clone(),
301 }
302 } else {
303 Error::NoServerConfigured
304 }
305 })?;
306 self.require_capability(&server_id, Capability::WorkspaceSymbols)?;
307
308 let params = LspWorkspaceSymbolParams {
309 query,
310 work_done_progress_params: WorkDoneProgressParams::default(),
311 partial_result_params: PartialResultParams::default(),
312 };
313
314 let response = client
315 .request_typed::<lsp_types::WorkspaceSymbolRequest>(params, client.request_timeout())
316 .await?;
317
318 let ctx = self.encoding_ctx(&server_id);
319
320 let mut raw_symbols: Vec<RawWorkspaceSymbol> = Vec::new();
326 match response {
327 Some(lsp_types::WorkspaceSymbolResponse::SymbolInformationList(list)) => {
334 for sym in list {
335 raw_symbols.push(RawWorkspaceSymbol {
336 name: sym.base_symbol_information.name,
337 kind: lsp_kind_to_u32(sym.base_symbol_information.kind),
338 container_name: sym.base_symbol_information.container_name,
339 out_of_workspace: ctx.is_out_of_workspace(&sym.location.uri),
340 uri: sym.location.uri,
341 range: sym.location.range,
342 });
343 }
344 }
345 Some(lsp_types::WorkspaceSymbolResponse::WorkspaceSymbolList(list)) => {
346 for sym in list {
347 let (uri, range) = match sym.location {
348 lsp_types::WorkspaceSymbolLocation::Location(loc) => (loc.uri, loc.range),
349 lsp_types::WorkspaceSymbolLocation::LocationUriOnly(_) => continue,
357 };
358 raw_symbols.push(RawWorkspaceSymbol {
359 name: sym.base_symbol_information.name,
360 kind: lsp_kind_to_u32(sym.base_symbol_information.kind),
361 container_name: sym.base_symbol_information.container_name,
362 out_of_workspace: ctx.is_out_of_workspace(&uri),
363 uri,
364 range,
365 });
366 }
367 }
368 None => {}
369 }
370
371 if let Some(target) = kind_filter {
372 raw_symbols.retain(|s| s.kind == target);
373 }
374 let effective_limit = (limit as usize).min(MAX_NORMALIZED_LOCATIONS);
377 let truncated = raw_symbols.len() > effective_limit;
378 raw_symbols.truncate(effective_limit);
379
380 let mut symbols = Vec::with_capacity(raw_symbols.len());
381 for raw in raw_symbols {
382 let range = ctx.normalize_range(&raw.uri, raw.range).await;
383 symbols.push(WorkspaceSymbol {
384 name: raw.name,
385 kind: raw.kind,
386 location: Location {
387 uri: raw.uri.to_string(),
388 range,
389 out_of_workspace: raw.out_of_workspace,
390 },
391 container_name: raw.container_name,
392 });
393 }
394
395 Ok(WorkspaceSymbolResult {
396 symbols,
397 truncated,
398 positions_degraded: ctx.positions_degraded(),
399 })
400 }
401}
402
403struct RawWorkspaceSymbol {
408 name: String,
409 kind: u32,
410 container_name: Option<String>,
411 out_of_workspace: bool,
412 uri: lsp_types::Uri,
413 range: lsp_types::Range,
414}
415
416#[cfg(test)]
417mod tests {
418 use std::collections::HashSet;
419 use std::sync::Arc;
420 use std::time::Duration;
421 use std::{assert_matches, fs};
422
423 use tempfile::TempDir;
424 use tokio::io::BufReader;
425 use tokio::time::timeout;
426 use url::Url;
427
428 use super::*;
429 use crate::bridge::translator::dto::PositionDegradation;
430 use crate::bridge::translator::testing::*;
431 use crate::config::{LanguageId, ServerCommand, ServerId, TimeoutSecs, ToolRouter, ToolSet};
432 use crate::test_lsp::client_path;
433
434 #[test]
442 fn test_symbol_kind_debug_rendering_is_pinned() {
443 assert_eq!(
444 format!("{:?}", lsp_types::SymbolKind::EnumMember),
445 "EnumMember"
446 );
447 }
448
449 #[tokio::test]
454 async fn test_convert_document_symbol_kind_is_numeric() {
455 let symbol = DocumentSymbol {
456 name: "my_enum_member".to_string(),
457 detail: None,
458 kind: lsp_types::SymbolKind::EnumMember,
459 tags: None,
460 #[allow(
461 deprecated,
462 reason = "`deprecated` is a deprecated field of the LSP struct literal"
463 )]
464 deprecated: None,
465 range: lsp_types::Range {
466 start: lsp_types::Position {
467 line: 0,
468 character: 0,
469 },
470 end: lsp_types::Position {
471 line: 0,
472 character: 5,
473 },
474 },
475 selection_range: lsp_types::Range {
476 start: lsp_types::Position {
477 line: 0,
478 character: 0,
479 },
480 end: lsp_types::Position {
481 line: 0,
482 character: 5,
483 },
484 },
485 children: None,
486 };
487 let ctx = test_ctx();
488 let uri = lsp_types::Uri::from("file:///tmp/test.rs");
489 let result = convert_document_symbol(symbol, &ctx, &uri).await;
490 assert_eq!(result.kind, 22u32);
492 }
493
494 #[tokio::test]
495 async fn test_handle_workspace_symbol_no_server() {
496 let translator = Translator::new();
497 let result = translator
498 .handle_workspace_symbol("test".to_string(), None, 100)
499 .await;
500 assert_matches!(result, Err(Error::NoServerConfigured));
501 }
502
503 #[tokio::test]
509 async fn test_handle_workspace_symbol_reports_initializing_when_expected_but_not_registered() {
510 let translator = Translator::new();
511 translator.set_expected_servers(HashSet::from([ServerId::from_static("pyright")]));
512
513 let result = translator
514 .handle_workspace_symbol("test".to_string(), None, 100)
515 .await;
516 assert_matches!(result, Err(Error::WorkspaceServersInitializing));
517 }
518
519 #[tokio::test]
522 async fn test_handle_workspace_symbol_reports_startup_failures() {
523 let translator = Translator::new();
524 translator.record_startup_failures(&[crate::error::ServerSpawnFailure {
525 server_id: ServerId::from_static("pyright"),
526 language_id: LanguageId::from_static("python"),
527 command: ServerCommand::from_static("pyright-langserver"),
528 reason: crate::error::StartupFailure::InitTaskPanicked,
529 }]);
530
531 let result = translator
532 .handle_workspace_symbol("test".to_string(), None, 100)
533 .await;
534
535 let Err(Error::AllServersFailedToInit { failures }) = result else {
536 panic!("expected AllServersFailedToInit, got {result:?}");
537 };
538 assert_eq!(failures.len(), 1);
539 assert_eq!(failures[0].server_id, ServerId::from_static("pyright"));
540 }
541
542 #[tokio::test]
545 async fn test_handle_workspace_symbol_reports_failed_claimant() {
546 let mut live = crate::config::LspServerConfig::pyright();
547 live.name = Some(ServerId::from_static("live"));
548 live.handles = Some(ToolSet::new(vec![ToolKind::Hover]).unwrap());
549 let mut failing = crate::config::LspServerConfig::rust_analyzer();
550 failing.name = Some(ServerId::from_static("failing"));
551 failing.handles = Some(ToolSet::new(vec![ToolKind::WorkspaceSymbols]).unwrap());
552 let router = ToolRouter::from_configs([&live, &failing]).unwrap();
553 let failing_id = ServerId::from_static("failing");
554
555 let translator = Translator::new().with_router(router);
556 translator.record_startup_failures(&[crate::error::ServerSpawnFailure {
557 server_id: failing_id.clone(),
558 language_id: LanguageId::from_static("rust"),
559 command: ServerCommand::from_static("rust-analyzer"),
560 reason: crate::error::StartupFailure::InitTaskPanicked,
561 }]);
562 translator.rebind_router(&HashSet::from([ServerId::from_static("live")]));
563 translator.clear_expected_servers();
564
565 let result = translator
566 .handle_workspace_symbol("test".to_string(), None, 100)
567 .await;
568
569 assert_matches!(&result, Err(Error::ServerFailedToStart(f)) if f.server_id == failing_id,
570 "got {result:?}"
571 );
572 }
573
574 #[tokio::test]
576 async fn test_handle_workspace_symbol_initializing_wins_over_recorded_failure() {
577 let translator = Translator::new();
578 translator.set_expected_servers(HashSet::from([ServerId::from_static("pyright")]));
579 translator.record_startup_failures(&[crate::error::ServerSpawnFailure {
580 server_id: ServerId::from_static("other"),
581 language_id: LanguageId::from_static("go"),
582 command: ServerCommand::from_static("gopls"),
583 reason: crate::error::StartupFailure::InitTaskPanicked,
584 }]);
585
586 let result = translator
587 .handle_workspace_symbol("test".to_string(), None, 100)
588 .await;
589
590 assert_matches!(result, Err(Error::WorkspaceServersInitializing));
591 }
592
593 #[tokio::test]
599 async fn test_handle_workspace_symbol_no_claimant_names_tool() {
600 let configs = vec![crate::config::LspServerConfig {
601 language_id: LanguageId::from_static("python"),
602 command: ServerCommand::from_static("pyright-langserver").into(),
603 args: vec![],
604 env: crate::config::ServerEnv::default(),
605 file_patterns: vec![],
606 initialization_options: None,
607 settings: None,
608 timeout_seconds: TimeoutSecs::new(30).unwrap(),
609 request_timeout_seconds: TimeoutSecs::new(30).unwrap(),
610 heuristics: None,
611 name: Some(ServerId::from_static("pyright")),
612 handles: Some(ToolSet::new(vec![ToolKind::Hover]).unwrap()),
613 indexing: crate::bridge::IndexingPolicy::Auto,
614 }];
615 let router = ToolRouter::from_configs(&configs).unwrap();
616 let translator = Translator::new().with_router(router);
617
618 let result = translator
619 .handle_workspace_symbol("test".to_string(), None, 100)
620 .await;
621 assert_matches!(
622 result,
623 Err(Error::NoServerForWorkspaceTool {
624 tool: ToolKind::WorkspaceSymbols
625 })
626 );
627 }
628
629 #[tokio::test]
635 async fn test_handle_document_symbols_flat_response_selection_range_matches_range() {
636 let dir = TempDir::new().unwrap();
637 let server_id = ServerId::from_static("rust");
638 let (translator, mut server) = translator_with_capabilities(
639 &dir,
640 &server_id,
641 lsp_types::ServerCapabilities {
642 document_symbol_provider: Some(lsp_types::DocumentSymbolProvider::Bool(true)),
643 ..Default::default()
644 },
645 );
646
647 let path = dir.path().join("main.rs");
648 fs::write(&path, "fn main() {}\n").unwrap();
649 let path_str = path.to_string_lossy().to_string();
650 let uri = Url::from_file_path(&path).unwrap().to_string();
651
652 let translator = Arc::new(translator);
653 let handle = {
654 let translator = Arc::clone(&translator);
655 tokio::spawn(async move {
656 translator
657 .handle_document_symbols(client_path(path_str))
658 .await
659 })
660 };
661
662 let mut wire = BufReader::new(&mut server.write_stdout);
663 let opened = read_framed_message(&mut wire).await;
664 assert_eq!(opened["method"], "textDocument/didOpen");
665 let symbol_request = read_framed_message(&mut wire).await;
666 assert_eq!(symbol_request["method"], "textDocument/documentSymbol");
667
668 write_response(
669 &mut server.read_half_stdin,
670 &symbol_request["id"],
671 serde_json::json!([{
672 "name": "main",
673 "kind": 12,
674 "location": {
675 "uri": uri,
676 "range": {
677 "start": {"line": 0, "character": 0},
678 "end": {"line": 0, "character": 12},
679 },
680 },
681 }]),
682 )
683 .await;
684
685 let result = timeout(Duration::from_secs(2), handle)
686 .await
687 .expect("handler call should not hang")
688 .unwrap()
689 .expect("flat document symbol response should succeed");
690
691 assert_eq!(result.symbols.len(), 1);
692 assert_eq!(result.symbols[0].range, result.symbols[0].selection_range);
693 }
694
695 #[tokio::test]
706 async fn test_handle_document_symbols_flat_response_normalizes_against_response_uri() {
707 let dir = TempDir::new().unwrap();
708 let server_id = ServerId::from_static("rust");
709 let (translator, mut server) = translator_with_capabilities_and_encoding(
710 &dir,
711 &server_id,
712 lsp_types::ServerCapabilities {
713 document_symbol_provider: Some(lsp_types::DocumentSymbolProvider::Bool(true)),
714 ..Default::default()
715 },
716 lsp_types::PositionEncodingKind::UTF8,
717 );
718
719 let path = dir.path().join("main.rs");
720 fs::write(&path, "aöb").unwrap();
721 let path_str = path.to_string_lossy().to_string();
722 let outside_uri = "file:///outside/workspace/does-not-exist.rs";
723
724 let translator = Arc::new(translator);
725 let handle = {
726 let translator = Arc::clone(&translator);
727 tokio::spawn(async move {
728 translator
729 .handle_document_symbols(client_path(path_str))
730 .await
731 })
732 };
733
734 let mut wire = BufReader::new(&mut server.write_stdout);
735 let opened = read_framed_message(&mut wire).await;
736 assert_eq!(opened["method"], "textDocument/didOpen");
737 let symbol_request = read_framed_message(&mut wire).await;
738 assert_eq!(symbol_request["method"], "textDocument/documentSymbol");
739
740 write_response(
741 &mut server.read_half_stdin,
742 &symbol_request["id"],
743 serde_json::json!([{
744 "name": "sym",
745 "kind": 12,
746 "location": {
747 "uri": outside_uri,
748 "range": {
749 "start": {"line": 0, "character": 0},
750 "end": {"line": 0, "character": 3},
751 },
752 },
753 }]),
754 )
755 .await;
756
757 let result = timeout(Duration::from_secs(2), handle)
758 .await
759 .expect("handler call should not hang")
760 .unwrap()
761 .expect("flat document symbol response should succeed");
762
763 assert_eq!(result.symbols.len(), 1);
764 assert_eq!(
765 result.symbols[0].range.end.character, 3,
766 "must convert against the queried document's own content (\"aöb\"), not fail to \
767 read the entry's own (nonexistent, out-of-workspace) location.uri and fall back to \
768 the raw byte offset"
769 );
770 }
771
772 #[tokio::test]
779 async fn test_handle_document_symbols_sets_positions_degraded_for_unresolvable_line() {
780 let dir = TempDir::new().unwrap();
781 let server_id = ServerId::from_static("rust");
782 let (translator, mut server) = translator_with_capabilities_and_encoding(
783 &dir,
784 &server_id,
785 lsp_types::ServerCapabilities {
786 document_symbol_provider: Some(lsp_types::DocumentSymbolProvider::Bool(true)),
787 ..Default::default()
788 },
789 lsp_types::PositionEncodingKind::UTF8,
790 );
791
792 let path = dir.path().join("main.rs");
793 fs::write(&path, "aöb").unwrap();
794 let path_str = path.to_string_lossy().to_string();
795
796 let translator = Arc::new(translator);
797 let handle = {
798 let translator = Arc::clone(&translator);
799 tokio::spawn(async move {
800 translator
801 .handle_document_symbols(client_path(path_str))
802 .await
803 })
804 };
805
806 let mut wire = BufReader::new(&mut server.write_stdout);
807 let opened = read_framed_message(&mut wire).await;
808 assert_eq!(opened["method"], "textDocument/didOpen");
809 let symbol_request = read_framed_message(&mut wire).await;
810 assert_eq!(symbol_request["method"], "textDocument/documentSymbol");
811
812 write_response(
813 &mut server.read_half_stdin,
814 &symbol_request["id"],
815 serde_json::json!([{
816 "name": "sym",
817 "kind": 12,
818 "range": {
821 "start": {"line": 5, "character": 0},
822 "end": {"line": 5, "character": 1},
823 },
824 "selectionRange": {
825 "start": {"line": 5, "character": 0},
826 "end": {"line": 5, "character": 1},
827 },
828 }]),
829 )
830 .await;
831
832 let result = timeout(Duration::from_secs(2), handle)
833 .await
834 .expect("handler call should not hang")
835 .unwrap()
836 .expect("document symbol response should still succeed, just degraded");
837
838 assert_eq!(result.symbols.len(), 1);
839 assert_eq!(
840 result.positions_degraded,
841 Some(PositionDegradation::Response),
842 "a range whose line can't be resolved must mark the result degraded"
843 );
844 }
845
846 #[tokio::test]
852 async fn test_handle_workspace_symbol_drops_location_uri_only_entries() {
853 let dir = TempDir::new().unwrap();
854 let server_id = ServerId::from_static("rust");
855 let caps = lsp_types::ServerCapabilities {
856 workspace_symbol_provider: Some(lsp_types::WorkspaceSymbolProvider::Bool(true)),
857 ..Default::default()
858 };
859 let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
860 let a_uri = Url::from_file_path(dir.path().join("a.rs"))
861 .unwrap()
862 .to_string();
863 let b_uri = Url::from_file_path(dir.path().join("b.rs"))
864 .unwrap()
865 .to_string();
866
867 let translator = Arc::new(translator);
868 let handle = {
869 let translator = Arc::clone(&translator);
870 tokio::spawn(async move {
871 translator
872 .handle_workspace_symbol("foo".to_string(), None, 100)
873 .await
874 })
875 };
876
877 let mut wire = BufReader::new(&mut server.write_stdout);
878 let request = read_framed_message(&mut wire).await;
879 assert_eq!(request["method"], "workspace/symbol");
880
881 write_response(
888 &mut server.read_half_stdin,
889 &request["id"],
890 serde_json::json!([
891 {
892 "name": "with_range",
893 "kind": 12,
894 "location": {
895 "uri": a_uri,
896 "range": {
897 "start": {"line": 0, "character": 0},
898 "end": {"line": 0, "character": 5}
899 }
900 }
901 },
902 {
903 "name": "without_range",
904 "kind": 12,
905 "location": { "uri": b_uri }
906 }
907 ]),
908 )
909 .await;
910
911 let result = timeout(Duration::from_secs(2), handle)
912 .await
913 .expect("handler call should not hang")
914 .unwrap()
915 .unwrap();
916
917 assert_eq!(
918 result.symbols.len(),
919 1,
920 "the range-less LocationUriOnly symbol must be dropped, not fabricated"
921 );
922 assert_eq!(result.symbols[0].name, "with_range");
923 }
924
925 #[tokio::test]
931 async fn test_handle_workspace_symbol_does_not_filter_out_of_workspace_location() {
932 let dir = TempDir::new().unwrap();
933 let server_id = ServerId::from_static("rust");
934 let caps = lsp_types::ServerCapabilities {
935 workspace_symbol_provider: Some(lsp_types::WorkspaceSymbolProvider::Bool(true)),
936 ..Default::default()
937 };
938 let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
939 let inside_uri = Url::from_file_path(canonical_dir(&dir).join("inside.rs"))
940 .unwrap()
941 .to_string();
942 let outside_uri = "file:///outside/workspace/evil.rs";
943
944 let translator = Arc::new(translator);
945 let handle = {
946 let translator = Arc::clone(&translator);
947 tokio::spawn(async move {
948 translator
949 .handle_workspace_symbol("foo".to_string(), None, 100)
950 .await
951 })
952 };
953
954 let mut wire = BufReader::new(&mut server.write_stdout);
955 let request = read_framed_message(&mut wire).await;
956 assert_eq!(request["method"], "workspace/symbol");
957
958 write_response(
959 &mut server.read_half_stdin,
960 &request["id"],
961 serde_json::json!([
962 {
963 "name": "inside",
964 "kind": 12,
965 "location": {
966 "uri": inside_uri,
967 "range": {
968 "start": {"line": 0, "character": 0},
969 "end": {"line": 0, "character": 5}
970 }
971 }
972 },
973 {
974 "name": "outside",
975 "kind": 12,
976 "location": {
977 "uri": outside_uri,
978 "range": {
979 "start": {"line": 0, "character": 0},
980 "end": {"line": 0, "character": 4}
981 }
982 }
983 }
984 ]),
985 )
986 .await;
987
988 let result = timeout(Duration::from_secs(2), handle)
989 .await
990 .expect("handler call should not hang")
991 .unwrap()
992 .unwrap();
993
994 assert_eq!(
995 result.symbols.len(),
996 2,
997 "both the in-workspace and out-of-workspace symbols must be returned"
998 );
999 assert!(result.symbols.iter().any(|s| s.name == "inside"));
1000 assert!(result.symbols.iter().any(|s| s.name == "outside"));
1001 assert!(
1002 !result
1003 .symbols
1004 .iter()
1005 .find(|s| s.name == "inside")
1006 .unwrap()
1007 .location
1008 .out_of_workspace,
1009 "an in-workspace symbol location must not be marked out_of_workspace"
1010 );
1011 assert!(
1012 result
1013 .symbols
1014 .iter()
1015 .find(|s| s.name == "outside")
1016 .unwrap()
1017 .location
1018 .out_of_workspace,
1019 "an out-of-workspace symbol location must be marked out_of_workspace"
1020 );
1021 }
1022
1023 #[tokio::test]
1028 async fn test_handle_workspace_symbol_clamps_limit_to_max_normalized_locations() {
1029 let dir = TempDir::new().unwrap();
1030 let server_id = ServerId::from_static("rust");
1031 let caps = lsp_types::ServerCapabilities {
1032 workspace_symbol_provider: Some(lsp_types::WorkspaceSymbolProvider::Bool(true)),
1033 ..Default::default()
1034 };
1035 let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
1036 let uri = Url::from_file_path(dir.path().join("many.rs"))
1037 .unwrap()
1038 .to_string();
1039
1040 let translator = Arc::new(translator);
1041 let handle = {
1042 let translator = Arc::clone(&translator);
1043 tokio::spawn(async move {
1044 translator
1045 .handle_workspace_symbol("foo".to_string(), None, u32::MAX)
1046 .await
1047 })
1048 };
1049
1050 let mut wire = BufReader::new(&mut server.write_stdout);
1051 let request = read_framed_message(&mut wire).await;
1052 assert_eq!(request["method"], "workspace/symbol");
1053
1054 let symbols: Vec<serde_json::Value> = (0..MAX_NORMALIZED_LOCATIONS + 500)
1055 .map(|i| {
1056 serde_json::json!({
1057 "name": format!("sym{i}"),
1058 "kind": 12,
1059 "location": {
1060 "uri": uri,
1061 "range": {
1062 "start": {"line": 0, "character": 0},
1063 "end": {"line": 0, "character": 3}
1064 }
1065 }
1066 })
1067 })
1068 .collect();
1069 write_response(
1070 &mut server.read_half_stdin,
1071 &request["id"],
1072 serde_json::json!(symbols),
1073 )
1074 .await;
1075
1076 let result = timeout(Duration::from_secs(5), handle)
1077 .await
1078 .expect("handler call should not hang")
1079 .unwrap()
1080 .unwrap();
1081
1082 assert_eq!(
1083 result.symbols.len(),
1084 MAX_NORMALIZED_LOCATIONS,
1085 "limit: u32::MAX must be clamped to MAX_NORMALIZED_LOCATIONS, not left unbounded"
1086 );
1087 assert!(
1088 result.truncated,
1089 "a limit clamped below what the caller asked for must set truncated: true"
1090 );
1091 }
1092
1093 #[tokio::test]
1099 async fn test_handle_document_symbols_dispatches_while_indexing_loading() {
1100 use crate::bridge::NotificationCache;
1101
1102 let dir = TempDir::new().unwrap();
1103 let server_id = ServerId::from_static("rust");
1104 let caps = lsp_types::ServerCapabilities {
1105 document_symbol_provider: Some(lsp_types::DocumentSymbolProvider::Bool(true)),
1106 ..Default::default()
1107 };
1108 let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
1109
1110 let cache = Arc::new(tokio::sync::Mutex::new(NotificationCache::new()));
1111 cache.lock().await.observe_indexing_signal(
1112 &server_id,
1113 "experimental/serverStatus",
1114 Some(&serde_json::json!({"quiescent": false})),
1115 );
1116 let translator = Arc::new(translator.with_notification_cache(cache));
1117
1118 let path = dir.path().join("main.rs");
1119 fs::write(&path, "fn main() {}").unwrap();
1120
1121 let handle = {
1122 let translator = Arc::clone(&translator);
1123 let path = path.to_string_lossy().to_string();
1124 tokio::spawn(async move { translator.handle_document_symbols(client_path(path)).await })
1125 };
1126
1127 let mut wire = BufReader::new(&mut server.write_stdout);
1128 let opened = read_framed_message(&mut wire).await;
1129 assert_eq!(opened["method"], "textDocument/didOpen");
1130 let request = read_framed_message(&mut wire).await;
1131 assert_eq!(request["method"], "textDocument/documentSymbol");
1132
1133 write_response(
1134 &mut server.read_half_stdin,
1135 &request["id"],
1136 serde_json::json!([]),
1137 )
1138 .await;
1139
1140 let result = timeout(Duration::from_secs(2), handle)
1141 .await
1142 .expect("handler call should not hang -- document_symbols must not be gated")
1143 .unwrap()
1144 .unwrap();
1145 assert!(result.symbols.is_empty());
1146 }
1147}