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::navigation::MAX_NORMALIZED_LOCATIONS;
15use super::routing::{Capability, IndexingGate};
16use crate::bridge::lock_std;
17use crate::config::{NoServerReason, ToolKind};
18use crate::error::{Error, Result};
19use crate::lsp::SUPPORTED_SYMBOL_KINDS;
20
21fn validate_query_length(query: &str) -> Result<()> {
23 const MAX_QUERY_LENGTH: usize = 1000;
24
25 if query.len() > MAX_QUERY_LENGTH {
26 return Err(Error::InvalidToolParams(format!(
27 "Query too long: {} bytes (max {MAX_QUERY_LENGTH})",
28 query.len()
29 )));
30 }
31
32 Ok(())
33}
34
35fn resolve_kind_filter(kind: &str) -> Result<u32> {
45 if let Ok(numeric) = kind.parse::<u32>() {
46 return Ok(numeric);
47 }
48
49 SUPPORTED_SYMBOL_KINDS
50 .iter()
51 .find(|k| format!("{k:?}").eq_ignore_ascii_case(kind))
52 .map(|&k| u32::from(k))
53 .ok_or_else(|| {
54 let valid: Vec<String> = SUPPORTED_SYMBOL_KINDS
55 .iter()
56 .map(|k| format!("{k:?}"))
57 .collect();
58 Error::InvalidToolParams(format!(
59 "Invalid kind_filter: '{kind}'. Valid values: {valid:?}, or the numeric LSP \
60 SymbolKind value"
61 ))
62 })
63}
64
65fn convert_document_symbol<'a>(
72 symbol: DocumentSymbol,
73 ctx: &'a EncodingCtx,
74 uri: &'a lsp_types::Uri,
75) -> futures::future::BoxFuture<'a, Symbol> {
76 Box::pin(async move {
77 let range = ctx.normalize_range(uri, symbol.range).await;
78 let selection_range = ctx.normalize_range(uri, symbol.selection_range).await;
79 let children = match symbol.children {
80 Some(children) => {
81 let mut result = Vec::with_capacity(children.len());
82 for child in children {
83 result.push(convert_document_symbol(child, ctx, uri).await);
84 }
85 Some(result)
86 }
87 None => None,
88 };
89
90 Symbol {
91 name: symbol.name,
92 kind: lsp_kind_to_u32(symbol.kind),
93 range,
94 selection_range,
95 children,
96 }
97 })
98}
99
100impl Translator {
101 pub async fn handle_document_symbols(
108 &self,
109 file_path: String,
110 ) -> Result<DocumentSymbolsResult> {
111 let (server_id, client, uri) = self
112 .prepare_gated_document(
113 &file_path,
114 ToolKind::DocumentSymbols,
115 Capability::DocumentSymbols,
116 IndexingGate::NotRequired,
117 )
118 .await?;
119 let ctx = self.encoding_ctx(&server_id);
120 let response_uri = uri.clone();
121
122 let params = DocumentSymbolParams {
123 text_document: TextDocumentIdentifier { uri },
124 work_done_progress_params: WorkDoneProgressParams::default(),
125 partial_result_params: PartialResultParams::default(),
126 };
127
128 let response = client
129 .request_typed::<lsp_types::DocumentSymbolRequest>(params, client.request_timeout())
130 .await?;
131
132 let symbols = match response {
133 Some(lsp_types::DocumentSymbolResponse::SymbolInformationList(symbols)) => {
134 let mut result = Vec::with_capacity(symbols.len());
145 for sym in symbols {
146 let range = ctx.normalize_range(&response_uri, sym.location.range).await;
147 let selection_range = range.clone();
148 result.push(Symbol {
149 name: sym.base_symbol_information.name,
150 kind: lsp_kind_to_u32(sym.base_symbol_information.kind),
151 range,
152 selection_range,
153 children: None,
154 });
155 }
156 result
157 }
158 Some(lsp_types::DocumentSymbolResponse::DocumentSymbolList(symbols)) => {
159 let mut result = Vec::with_capacity(symbols.len());
160 for sym in symbols {
161 result.push(convert_document_symbol(sym, &ctx, &response_uri).await);
162 }
163 result
164 }
165 None => vec![],
166 };
167
168 Ok(DocumentSymbolsResult {
169 symbols,
170 positions_degraded: ctx.positions_degraded(),
171 })
172 }
173
174 #[allow(clippy::too_many_lines)]
189 pub async fn handle_workspace_symbol(
190 &self,
191 query: String,
192 kind_filter: Option<String>,
193 limit: u32,
194 ) -> Result<WorkspaceSymbolResult> {
195 validate_query_length(&query)?;
196 let kind_filter = kind_filter
197 .as_deref()
198 .map(resolve_kind_filter)
199 .transpose()?;
200
201 let server_id = lock_std(&self.router)
206 .resolve_any(ToolKind::WorkspaceSymbols)
207 .cloned()
208 .map_err(|reason| match reason {
209 NoServerReason::NothingRegistered => {
215 if lock_std(&self.expected_servers).is_empty() {
216 Error::NoServerConfigured
217 } else {
218 Error::WorkspaceServersInitializing
219 }
220 }
221 NoServerReason::NoClaimant => Error::NoServerForWorkspaceTool {
222 tool: ToolKind::WorkspaceSymbols,
223 },
224 })?;
225 self.respawn_if_dead(&server_id).await?;
226 let client = lock_std(&self.lsp_clients).get(&server_id).cloned();
227 let client = client.ok_or_else(|| {
228 if lock_std(&self.expected_servers).contains(&server_id) {
229 Error::ServerInitializing {
230 server_id: server_id.clone(),
231 }
232 } else {
233 Error::NoServerConfigured
234 }
235 })?;
236 self.require_capability(&server_id, Capability::WorkspaceSymbols)?;
237
238 let params = LspWorkspaceSymbolParams {
239 query,
240 work_done_progress_params: WorkDoneProgressParams::default(),
241 partial_result_params: PartialResultParams::default(),
242 };
243
244 let response = client
245 .request_typed::<lsp_types::WorkspaceSymbolRequest>(params, client.request_timeout())
246 .await?;
247
248 let ctx = self.encoding_ctx(&server_id);
249
250 let mut raw_symbols: Vec<RawWorkspaceSymbol> = Vec::new();
256 match response {
257 Some(lsp_types::WorkspaceSymbolResponse::SymbolInformationList(list)) => {
264 for sym in list {
265 raw_symbols.push(RawWorkspaceSymbol {
266 name: sym.base_symbol_information.name,
267 kind: lsp_kind_to_u32(sym.base_symbol_information.kind),
268 container_name: sym.base_symbol_information.container_name,
269 out_of_workspace: ctx.is_out_of_workspace(&sym.location.uri),
270 uri: sym.location.uri,
271 range: sym.location.range,
272 });
273 }
274 }
275 Some(lsp_types::WorkspaceSymbolResponse::WorkspaceSymbolList(list)) => {
276 for sym in list {
277 let (uri, range) = match sym.location {
278 lsp_types::WorkspaceSymbolLocation::Location(loc) => (loc.uri, loc.range),
279 lsp_types::WorkspaceSymbolLocation::LocationUriOnly(_) => continue,
287 };
288 raw_symbols.push(RawWorkspaceSymbol {
289 name: sym.base_symbol_information.name,
290 kind: lsp_kind_to_u32(sym.base_symbol_information.kind),
291 container_name: sym.base_symbol_information.container_name,
292 out_of_workspace: ctx.is_out_of_workspace(&uri),
293 uri,
294 range,
295 });
296 }
297 }
298 None => {}
299 }
300
301 if let Some(target) = kind_filter {
302 raw_symbols.retain(|s| s.kind == target);
303 }
304 let effective_limit = (limit as usize).min(MAX_NORMALIZED_LOCATIONS);
307 let truncated = raw_symbols.len() > effective_limit;
308 raw_symbols.truncate(effective_limit);
309
310 let mut symbols = Vec::with_capacity(raw_symbols.len());
311 for raw in raw_symbols {
312 let range = ctx.normalize_range(&raw.uri, raw.range).await;
313 symbols.push(WorkspaceSymbol {
314 name: raw.name,
315 kind: raw.kind,
316 location: Location {
317 uri: raw.uri.to_string(),
318 range,
319 out_of_workspace: raw.out_of_workspace,
320 },
321 container_name: raw.container_name,
322 });
323 }
324
325 Ok(WorkspaceSymbolResult {
326 symbols,
327 truncated,
328 positions_degraded: ctx.positions_degraded(),
329 })
330 }
331}
332
333struct RawWorkspaceSymbol {
338 name: String,
339 kind: u32,
340 container_name: Option<String>,
341 out_of_workspace: bool,
342 uri: lsp_types::Uri,
343 range: lsp_types::Range,
344}
345
346#[cfg(test)]
347#[allow(clippy::unwrap_used, clippy::expect_used)]
348mod tests {
349 use std::collections::{HashMap, HashSet};
350 use std::fs;
351 use std::sync::Arc;
352 use std::time::Duration;
353
354 use tempfile::TempDir;
355 use tokio::io::BufReader;
356 use tokio::time::timeout;
357 use url::Url;
358
359 use super::*;
360 use crate::bridge::translator::testing::*;
361 use crate::config::{ServerId, ToolRouter};
362
363 #[test]
371 fn test_symbol_kind_debug_rendering_is_pinned() {
372 assert_eq!(
373 format!("{:?}", lsp_types::SymbolKind::EnumMember),
374 "EnumMember"
375 );
376 }
377
378 #[tokio::test]
383 async fn test_convert_document_symbol_kind_is_numeric() {
384 let symbol = DocumentSymbol {
385 name: "my_enum_member".to_string(),
386 detail: None,
387 kind: lsp_types::SymbolKind::EnumMember,
388 tags: None,
389 #[allow(deprecated)]
390 deprecated: None,
391 range: lsp_types::Range {
392 start: lsp_types::Position {
393 line: 0,
394 character: 0,
395 },
396 end: lsp_types::Position {
397 line: 0,
398 character: 5,
399 },
400 },
401 selection_range: lsp_types::Range {
402 start: lsp_types::Position {
403 line: 0,
404 character: 0,
405 },
406 end: lsp_types::Position {
407 line: 0,
408 character: 5,
409 },
410 },
411 children: None,
412 };
413 let ctx = test_ctx();
414 let uri = lsp_types::Uri::from("file:///tmp/test.rs");
415 let result = convert_document_symbol(symbol, &ctx, &uri).await;
416 assert_eq!(result.kind, 22u32);
418 }
419
420 #[test]
421 fn test_resolve_kind_filter_accepts_known_name() {
422 assert_eq!(resolve_kind_filter("EnumMember").unwrap(), 22u32);
423 }
424
425 #[test]
429 fn test_resolve_kind_filter_accepts_numeric_value() {
430 assert_eq!(resolve_kind_filter("22").unwrap(), 22u32);
431 }
432
433 #[test]
434 fn test_resolve_kind_filter_rejects_unknown_name() {
435 let result = resolve_kind_filter("NotAKind");
436 assert!(matches!(result, Err(Error::InvalidToolParams(_))));
437 }
438
439 #[tokio::test]
440 async fn test_handle_workspace_symbol_no_server() {
441 let translator = Translator::new();
442 let result = translator
443 .handle_workspace_symbol("test".to_string(), None, 100)
444 .await;
445 assert!(matches!(result, Err(Error::NoServerConfigured)));
446 }
447
448 #[tokio::test]
454 async fn test_handle_workspace_symbol_reports_initializing_when_expected_but_not_registered() {
455 let translator = Translator::new();
456 translator.set_expected_servers(HashSet::from([ServerId::from("pyright")]));
457
458 let result = translator
459 .handle_workspace_symbol("test".to_string(), None, 100)
460 .await;
461 assert!(matches!(result, Err(Error::WorkspaceServersInitializing)));
462 }
463
464 #[tokio::test]
470 async fn test_handle_workspace_symbol_no_claimant_names_tool() {
471 let configs = vec![crate::config::LspServerConfig {
472 language_id: "python".to_string(),
473 command: "pyright-langserver".to_string(),
474 args: vec![],
475 env: HashMap::new(),
476 file_patterns: vec![],
477 initialization_options: None,
478 timeout_seconds: 30,
479 request_timeout_seconds: 30,
480 heuristics: None,
481 name: Some("pyright".to_string()),
482 handles: Some(vec![ToolKind::Hover]),
483 indexing: crate::bridge::IndexingPolicy::Auto,
484 }];
485 let router = ToolRouter::from_configs(&configs).unwrap();
486 let translator = Translator::new().with_router(router);
487
488 let result = translator
489 .handle_workspace_symbol("test".to_string(), None, 100)
490 .await;
491 assert!(matches!(
492 result,
493 Err(Error::NoServerForWorkspaceTool {
494 tool: ToolKind::WorkspaceSymbols
495 })
496 ));
497 }
498
499 #[tokio::test]
505 async fn test_handle_document_symbols_flat_response_selection_range_matches_range() {
506 let dir = TempDir::new().unwrap();
507 let server_id = ServerId::from("rust");
508 let (translator, mut server) = translator_with_capabilities(
509 &dir,
510 &server_id,
511 lsp_types::ServerCapabilities {
512 document_symbol_provider: Some(lsp_types::DocumentSymbolProvider::Bool(true)),
513 ..Default::default()
514 },
515 );
516
517 let path = dir.path().join("main.rs");
518 fs::write(&path, "fn main() {}\n").unwrap();
519 let path_str = path.to_string_lossy().to_string();
520 let uri = Url::from_file_path(&path).unwrap().to_string();
521
522 let translator = Arc::new(translator);
523 let handle = {
524 let translator = Arc::clone(&translator);
525 tokio::spawn(async move { translator.handle_document_symbols(path_str).await })
526 };
527
528 let mut wire = BufReader::new(&mut server.write_stdout);
529 let opened = read_framed_message(&mut wire).await;
530 assert_eq!(opened["method"], "textDocument/didOpen");
531 let symbol_request = read_framed_message(&mut wire).await;
532 assert_eq!(symbol_request["method"], "textDocument/documentSymbol");
533
534 write_response(
535 &mut server.read_half_stdin,
536 &symbol_request["id"],
537 serde_json::json!([{
538 "name": "main",
539 "kind": 12,
540 "location": {
541 "uri": uri,
542 "range": {
543 "start": {"line": 0, "character": 0},
544 "end": {"line": 0, "character": 12},
545 },
546 },
547 }]),
548 )
549 .await;
550
551 let result = timeout(Duration::from_secs(2), handle)
552 .await
553 .expect("handler call should not hang")
554 .unwrap()
555 .expect("flat document symbol response should succeed");
556
557 assert_eq!(result.symbols.len(), 1);
558 assert_eq!(result.symbols[0].range, result.symbols[0].selection_range);
559 }
560
561 #[tokio::test]
572 async fn test_handle_document_symbols_flat_response_normalizes_against_response_uri() {
573 let dir = TempDir::new().unwrap();
574 let server_id = ServerId::from("rust");
575 let (translator, mut server) = translator_with_capabilities_and_encoding(
576 &dir,
577 &server_id,
578 lsp_types::ServerCapabilities {
579 document_symbol_provider: Some(lsp_types::DocumentSymbolProvider::Bool(true)),
580 ..Default::default()
581 },
582 lsp_types::PositionEncodingKind::UTF8,
583 );
584
585 let path = dir.path().join("main.rs");
586 fs::write(&path, "aöb").unwrap();
587 let path_str = path.to_string_lossy().to_string();
588 let outside_uri = "file:///outside/workspace/does-not-exist.rs";
589
590 let translator = Arc::new(translator);
591 let handle = {
592 let translator = Arc::clone(&translator);
593 tokio::spawn(async move { translator.handle_document_symbols(path_str).await })
594 };
595
596 let mut wire = BufReader::new(&mut server.write_stdout);
597 let opened = read_framed_message(&mut wire).await;
598 assert_eq!(opened["method"], "textDocument/didOpen");
599 let symbol_request = read_framed_message(&mut wire).await;
600 assert_eq!(symbol_request["method"], "textDocument/documentSymbol");
601
602 write_response(
603 &mut server.read_half_stdin,
604 &symbol_request["id"],
605 serde_json::json!([{
606 "name": "sym",
607 "kind": 12,
608 "location": {
609 "uri": outside_uri,
610 "range": {
611 "start": {"line": 0, "character": 0},
612 "end": {"line": 0, "character": 3},
613 },
614 },
615 }]),
616 )
617 .await;
618
619 let result = timeout(Duration::from_secs(2), handle)
620 .await
621 .expect("handler call should not hang")
622 .unwrap()
623 .expect("flat document symbol response should succeed");
624
625 assert_eq!(result.symbols.len(), 1);
626 assert_eq!(
627 result.symbols[0].range.end.character, 3,
628 "must convert against the queried document's own content (\"aöb\"), not fail to \
629 read the entry's own (nonexistent, out-of-workspace) location.uri and fall back to \
630 the raw byte offset"
631 );
632 }
633
634 #[tokio::test]
641 async fn test_handle_document_symbols_sets_positions_degraded_for_unresolvable_line() {
642 let dir = TempDir::new().unwrap();
643 let server_id = ServerId::from("rust");
644 let (translator, mut server) = translator_with_capabilities_and_encoding(
645 &dir,
646 &server_id,
647 lsp_types::ServerCapabilities {
648 document_symbol_provider: Some(lsp_types::DocumentSymbolProvider::Bool(true)),
649 ..Default::default()
650 },
651 lsp_types::PositionEncodingKind::UTF8,
652 );
653
654 let path = dir.path().join("main.rs");
655 fs::write(&path, "aöb").unwrap();
656 let path_str = path.to_string_lossy().to_string();
657
658 let translator = Arc::new(translator);
659 let handle = {
660 let translator = Arc::clone(&translator);
661 tokio::spawn(async move { translator.handle_document_symbols(path_str).await })
662 };
663
664 let mut wire = BufReader::new(&mut server.write_stdout);
665 let opened = read_framed_message(&mut wire).await;
666 assert_eq!(opened["method"], "textDocument/didOpen");
667 let symbol_request = read_framed_message(&mut wire).await;
668 assert_eq!(symbol_request["method"], "textDocument/documentSymbol");
669
670 write_response(
671 &mut server.read_half_stdin,
672 &symbol_request["id"],
673 serde_json::json!([{
674 "name": "sym",
675 "kind": 12,
676 "range": {
679 "start": {"line": 5, "character": 0},
680 "end": {"line": 5, "character": 1},
681 },
682 "selectionRange": {
683 "start": {"line": 5, "character": 0},
684 "end": {"line": 5, "character": 1},
685 },
686 }]),
687 )
688 .await;
689
690 let result = timeout(Duration::from_secs(2), handle)
691 .await
692 .expect("handler call should not hang")
693 .unwrap()
694 .expect("document symbol response should still succeed, just degraded");
695
696 assert_eq!(result.symbols.len(), 1);
697 assert!(
698 result.positions_degraded,
699 "a range whose line can't be resolved must mark the result degraded"
700 );
701 }
702
703 #[tokio::test]
709 async fn test_handle_workspace_symbol_drops_location_uri_only_entries() {
710 let dir = TempDir::new().unwrap();
711 let server_id = ServerId::from("rust");
712 let caps = lsp_types::ServerCapabilities {
713 workspace_symbol_provider: Some(lsp_types::WorkspaceSymbolProvider::Bool(true)),
714 ..Default::default()
715 };
716 let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
717 let a_uri = Url::from_file_path(dir.path().join("a.rs"))
718 .unwrap()
719 .to_string();
720 let b_uri = Url::from_file_path(dir.path().join("b.rs"))
721 .unwrap()
722 .to_string();
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_workspace_symbol("foo".to_string(), None, 100)
730 .await
731 })
732 };
733
734 let mut wire = BufReader::new(&mut server.write_stdout);
735 let request = read_framed_message(&mut wire).await;
736 assert_eq!(request["method"], "workspace/symbol");
737
738 write_response(
745 &mut server.read_half_stdin,
746 &request["id"],
747 serde_json::json!([
748 {
749 "name": "with_range",
750 "kind": 12,
751 "location": {
752 "uri": a_uri,
753 "range": {
754 "start": {"line": 0, "character": 0},
755 "end": {"line": 0, "character": 5}
756 }
757 }
758 },
759 {
760 "name": "without_range",
761 "kind": 12,
762 "location": { "uri": b_uri }
763 }
764 ]),
765 )
766 .await;
767
768 let result = timeout(Duration::from_secs(2), handle)
769 .await
770 .expect("handler call should not hang")
771 .unwrap()
772 .unwrap();
773
774 assert_eq!(
775 result.symbols.len(),
776 1,
777 "the range-less LocationUriOnly symbol must be dropped, not fabricated"
778 );
779 assert_eq!(result.symbols[0].name, "with_range");
780 }
781
782 #[tokio::test]
788 async fn test_handle_workspace_symbol_does_not_filter_out_of_workspace_location() {
789 let dir = TempDir::new().unwrap();
790 let server_id = ServerId::from("rust");
791 let caps = lsp_types::ServerCapabilities {
792 workspace_symbol_provider: Some(lsp_types::WorkspaceSymbolProvider::Bool(true)),
793 ..Default::default()
794 };
795 let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
796 let inside_uri = Url::from_file_path(dir.path().join("inside.rs"))
797 .unwrap()
798 .to_string();
799 let outside_uri = "file:///outside/workspace/evil.rs";
800
801 let translator = Arc::new(translator);
802 let handle = {
803 let translator = Arc::clone(&translator);
804 tokio::spawn(async move {
805 translator
806 .handle_workspace_symbol("foo".to_string(), None, 100)
807 .await
808 })
809 };
810
811 let mut wire = BufReader::new(&mut server.write_stdout);
812 let request = read_framed_message(&mut wire).await;
813 assert_eq!(request["method"], "workspace/symbol");
814
815 write_response(
816 &mut server.read_half_stdin,
817 &request["id"],
818 serde_json::json!([
819 {
820 "name": "inside",
821 "kind": 12,
822 "location": {
823 "uri": inside_uri,
824 "range": {
825 "start": {"line": 0, "character": 0},
826 "end": {"line": 0, "character": 5}
827 }
828 }
829 },
830 {
831 "name": "outside",
832 "kind": 12,
833 "location": {
834 "uri": outside_uri,
835 "range": {
836 "start": {"line": 0, "character": 0},
837 "end": {"line": 0, "character": 4}
838 }
839 }
840 }
841 ]),
842 )
843 .await;
844
845 let result = timeout(Duration::from_secs(2), handle)
846 .await
847 .expect("handler call should not hang")
848 .unwrap()
849 .unwrap();
850
851 assert_eq!(
852 result.symbols.len(),
853 2,
854 "both the in-workspace and out-of-workspace symbols must be returned"
855 );
856 assert!(result.symbols.iter().any(|s| s.name == "inside"));
857 assert!(result.symbols.iter().any(|s| s.name == "outside"));
858 assert!(
859 !result
860 .symbols
861 .iter()
862 .find(|s| s.name == "inside")
863 .unwrap()
864 .location
865 .out_of_workspace,
866 "an in-workspace symbol location must not be marked out_of_workspace"
867 );
868 assert!(
869 result
870 .symbols
871 .iter()
872 .find(|s| s.name == "outside")
873 .unwrap()
874 .location
875 .out_of_workspace,
876 "an out-of-workspace symbol location must be marked out_of_workspace"
877 );
878 }
879
880 #[tokio::test]
885 async fn test_handle_workspace_symbol_clamps_limit_to_max_normalized_locations() {
886 let dir = TempDir::new().unwrap();
887 let server_id = ServerId::from("rust");
888 let caps = lsp_types::ServerCapabilities {
889 workspace_symbol_provider: Some(lsp_types::WorkspaceSymbolProvider::Bool(true)),
890 ..Default::default()
891 };
892 let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
893 let uri = Url::from_file_path(dir.path().join("many.rs"))
894 .unwrap()
895 .to_string();
896
897 let translator = Arc::new(translator);
898 let handle = {
899 let translator = Arc::clone(&translator);
900 tokio::spawn(async move {
901 translator
902 .handle_workspace_symbol("foo".to_string(), None, u32::MAX)
903 .await
904 })
905 };
906
907 let mut wire = BufReader::new(&mut server.write_stdout);
908 let request = read_framed_message(&mut wire).await;
909 assert_eq!(request["method"], "workspace/symbol");
910
911 let symbols: Vec<serde_json::Value> = (0..MAX_NORMALIZED_LOCATIONS + 500)
912 .map(|i| {
913 serde_json::json!({
914 "name": format!("sym{i}"),
915 "kind": 12,
916 "location": {
917 "uri": uri,
918 "range": {
919 "start": {"line": 0, "character": 0},
920 "end": {"line": 0, "character": 3}
921 }
922 }
923 })
924 })
925 .collect();
926 write_response(
927 &mut server.read_half_stdin,
928 &request["id"],
929 serde_json::json!(symbols),
930 )
931 .await;
932
933 let result = timeout(Duration::from_secs(5), handle)
934 .await
935 .expect("handler call should not hang")
936 .unwrap()
937 .unwrap();
938
939 assert_eq!(
940 result.symbols.len(),
941 MAX_NORMALIZED_LOCATIONS,
942 "limit: u32::MAX must be clamped to MAX_NORMALIZED_LOCATIONS, not left unbounded"
943 );
944 assert!(
945 result.truncated,
946 "a limit clamped below what the caller asked for must set truncated: true"
947 );
948 }
949
950 #[tokio::test]
956 async fn test_handle_document_symbols_dispatches_while_indexing_loading() {
957 use crate::bridge::NotificationCache;
958
959 let dir = TempDir::new().unwrap();
960 let server_id = ServerId::from("rust");
961 let caps = lsp_types::ServerCapabilities {
962 document_symbol_provider: Some(lsp_types::DocumentSymbolProvider::Bool(true)),
963 ..Default::default()
964 };
965 let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
966
967 let cache = Arc::new(tokio::sync::Mutex::new(NotificationCache::new()));
968 cache.lock().await.observe_indexing_signal(
969 &server_id,
970 "experimental/serverStatus",
971 Some(&serde_json::json!({"quiescent": false})),
972 );
973 let translator = Arc::new(translator.with_notification_cache(cache));
974
975 let path = dir.path().join("main.rs");
976 fs::write(&path, "fn main() {}").unwrap();
977
978 let handle = {
979 let translator = Arc::clone(&translator);
980 let path = path.to_string_lossy().to_string();
981 tokio::spawn(async move { translator.handle_document_symbols(path).await })
982 };
983
984 let mut wire = BufReader::new(&mut server.write_stdout);
985 let opened = read_framed_message(&mut wire).await;
986 assert_eq!(opened["method"], "textDocument/didOpen");
987 let request = read_framed_message(&mut wire).await;
988 assert_eq!(request["method"], "textDocument/documentSymbol");
989
990 write_response(
991 &mut server.read_half_stdin,
992 &request["id"],
993 serde_json::json!([]),
994 )
995 .await;
996
997 let result = timeout(Duration::from_secs(2), handle)
998 .await
999 .expect("handler call should not hang -- document_symbols must not be gated")
1000 .unwrap()
1001 .unwrap();
1002 assert!(result.symbols.is_empty());
1003 }
1004}