1use lsp_types::{
4 BaseSymbolInformation, DocumentSymbol, PartialResultParams, WorkDoneProgressParams,
5 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::positioned::{budgeted, document_request};
17use super::routing::{
18 Capability, FileLocalCapability, IndexingGate, PreparedDocument, WorkspaceRouteLookup,
19 lookup_workspace_route,
20};
21use crate::bridge::ClientPath;
22use crate::config::{ServerId, ToolKind};
23use crate::error::{Error, Result};
24use crate::util::lock_std;
25
26fn validate_query_length(query: &str) -> Result<()> {
28 const MAX_QUERY_LENGTH: usize = 1000;
29
30 if query.len() > MAX_QUERY_LENGTH {
31 return Err(Error::InvalidToolParams(format!(
32 "Query too long: {} bytes (max {MAX_QUERY_LENGTH})",
33 query.len()
34 )));
35 }
36
37 Ok(())
38}
39
40pub(super) struct FetchedSymbols {
44 pub(super) doc: PreparedDocument,
45 pub(super) ctx: EncodingCtx,
46 pub(super) response: Option<lsp_types::DocumentSymbolResponse>,
47}
48
49fn convert_document_symbol<'a>(
56 symbol: DocumentSymbol,
57 ctx: &'a EncodingCtx,
58 uri: &'a lsp_types::Uri,
59) -> futures::future::BoxFuture<'a, Symbol> {
60 Box::pin(async move {
61 let range = ctx.normalize_range(uri, symbol.range).await;
62 let selection_range = ctx.normalize_range(uri, symbol.selection_range).await;
63 let children = match symbol.children {
64 Some(children) => {
65 let mut result = Vec::with_capacity(children.len());
66 for child in children {
67 result.push(convert_document_symbol(child, ctx, uri).await);
68 }
69 Some(result)
70 }
71 None => None,
72 };
73
74 Symbol {
75 name: symbol.name,
76 kind: lsp_kind_to_u32(symbol.kind),
77 range,
78 selection_range,
79 children,
80 }
81 })
82}
83
84#[derive(Debug)]
86pub(super) enum DocumentSymbolTree {
87 Hierarchical(Vec<Symbol>),
89 Flat(Vec<FlatSymbol>),
91}
92
93impl DocumentSymbolTree {
94 fn into_symbols(self) -> Vec<Symbol> {
96 match self {
97 Self::Hierarchical(symbols) => symbols,
98 Self::Flat(symbols) => symbols.into_iter().map(|flat| flat.symbol).collect(),
99 }
100 }
101}
102
103#[derive(Debug)]
105pub(super) struct FlatSymbol {
106 pub(super) symbol: Symbol,
107 pub(super) container_name: Option<String>,
108}
109
110pub(super) async fn symbol_tree(
119 response: Option<lsp_types::DocumentSymbolResponse>,
120 ctx: &EncodingCtx,
121 uri: &lsp_types::Uri,
122) -> DocumentSymbolTree {
123 match response {
124 Some(lsp_types::DocumentSymbolResponse::SymbolInformationList(symbols)) => {
125 let mut result = Vec::with_capacity(symbols.len());
126 for sym in symbols {
127 let range = ctx.normalize_range(uri, sym.location.range).await;
128 let selection_range = range.clone();
129 result.push(FlatSymbol {
130 symbol: Symbol {
131 name: sym.base_symbol_information.name,
132 kind: lsp_kind_to_u32(sym.base_symbol_information.kind),
133 range,
134 selection_range,
135 children: None,
136 },
137 container_name: sym.base_symbol_information.container_name,
138 });
139 }
140 DocumentSymbolTree::Flat(result)
141 }
142 Some(lsp_types::DocumentSymbolResponse::DocumentSymbolList(symbols)) => {
143 let mut result = Vec::with_capacity(symbols.len());
144 for sym in symbols {
145 result.push(convert_document_symbol(sym, ctx, uri).await);
146 }
147 DocumentSymbolTree::Hierarchical(result)
148 }
149 None => DocumentSymbolTree::Hierarchical(vec![]),
150 }
151}
152
153impl Translator {
154 pub async fn handle_document_symbols(
161 &self,
162 file_path: ClientPath,
163 ) -> Result<DocumentSymbolsResult> {
164 let FetchedSymbols { doc, ctx, response } =
165 self.request_document_symbols(&file_path).await?;
166 let tree = symbol_tree(response, &ctx, doc.uri()).await;
167
168 Ok(DocumentSymbolsResult {
169 symbols: tree.into_symbols(),
170 positions_degraded: ctx.positions_degraded(),
171 })
172 }
173
174 pub(super) async fn request_document_symbols(
182 &self,
183 file_path: &ClientPath,
184 ) -> Result<FetchedSymbols> {
185 let doc = self
186 .prepare_gated_document(
187 file_path,
188 IndexingGate::FileLocal(FileLocalCapability::DocumentSymbols),
189 )
190 .await?;
191 self.fetch_document_symbols(doc).await
192 }
193
194 pub(super) async fn fetch_document_symbols(
201 &self,
202 doc: PreparedDocument,
203 ) -> Result<FetchedSymbols> {
204 let ctx = self.encoding_ctx(doc.server_id());
205 let response = document_request::<lsp_types::DocumentSymbolRequest>(&doc, ()).await?;
206 Ok(FetchedSymbols { doc, ctx, response })
207 }
208
209 pub async fn handle_workspace_symbol(
224 &self,
225 query: String,
226 kind_filter: Option<SymbolKindFilter>,
227 limit: u32,
228 ) -> Result<WorkspaceSymbolResult> {
229 validate_query_length(&query)?;
230 let kind_filter = kind_filter.map(|filter| u32::from(filter.kind()));
231
232 let server_id = self.workspace_symbol_server()?;
233 self.respawn_if_dead(&server_id).await?;
234 let (client, expected) = {
235 let servers = lock_std(&self.servers);
236 (servers.client(&server_id), servers.is_expected(&server_id))
237 };
238 let client = client.ok_or_else(|| {
239 if expected {
240 Error::ServerInitializing {
241 server_id: server_id.clone(),
242 }
243 } else {
244 Error::NoServerConfigured
245 }
246 })?;
247 self.require_capability(&server_id, Capability::WorkspaceSymbols)?;
248
249 let params = LspWorkspaceSymbolParams {
250 query,
251 work_done_progress_params: WorkDoneProgressParams::default(),
252 partial_result_params: PartialResultParams::default(),
253 };
254
255 let response = budgeted::<lsp_types::WorkspaceSymbolRequest>(&client, params).await?;
256
257 let ctx = self.encoding_ctx(&server_id);
258
259 let mut raw_symbols = raw_workspace_symbols(response, &ctx);
265 let server_answered_empty = raw_symbols.is_empty();
266
267 if let Some(target) = kind_filter {
268 raw_symbols.retain(|s| s.kind == target);
269 }
270 let effective_limit = (limit as usize).min(MAX_NORMALIZED_LOCATIONS);
273 let truncated = raw_symbols.len() > effective_limit;
274 raw_symbols.truncate(effective_limit);
275
276 let mut symbols = Vec::with_capacity(raw_symbols.len());
277 for raw in raw_symbols {
278 let range = ctx.normalize_range(&raw.uri, raw.range).await;
279 symbols.push(WorkspaceSymbol {
280 name: raw.name,
281 kind: raw.kind,
282 location: Location {
283 uri: raw.uri.to_string(),
284 range,
285 out_of_workspace: raw.out_of_workspace,
286 },
287 container_name: raw.container_name,
288 });
289 }
290
291 let index_coverage = server_answered_empty.then(|| self.index_coverage(&server_id));
292 Ok(WorkspaceSymbolResult {
293 symbols,
294 truncated,
295 positions_degraded: ctx.positions_degraded(),
296 index_coverage,
297 })
298 }
299
300 fn workspace_symbol_server(&self) -> Result<ServerId> {
307 let router = self.router_snapshot();
308 let lookup = {
309 let servers = lock_std(&self.servers);
310 lookup_workspace_route(
311 || router.resolve_any(ToolKind::WorkspaceSymbols).cloned(),
312 |id| servers.client(id).is_some(),
313 |id| servers.is_expected(id),
314 || !servers.any_expected(),
315 )
316 };
317 let server_id = match lookup {
318 WorkspaceRouteLookup::Registered(id) => id,
319 WorkspaceRouteLookup::Initializing(server_id) => {
320 return Err(Error::ServerInitializing { server_id });
321 }
322 WorkspaceRouteLookup::AllInitializing => {
323 return Err(Error::WorkspaceServersInitializing);
324 }
325 WorkspaceRouteLookup::Dangling(_) => return Err(Error::NoServerConfigured),
326 WorkspaceRouteLookup::NothingConfigured => {
327 let failures = self.startup_failures();
328 return Err(if failures.is_empty() {
329 Error::NoServerConfigured
330 } else {
331 Error::AllServersFailedToInit { failures }
332 });
333 }
334 WorkspaceRouteLookup::NoClaimant => {
337 return Err(self
338 .configured_router
339 .resolve_any(ToolKind::WorkspaceSymbols)
340 .ok()
341 .and_then(|id| self.startup_failure(id))
342 .map_or(
343 Error::NoServerForWorkspaceTool {
344 tool: ToolKind::WorkspaceSymbols,
345 },
346 |failure| Error::ServerFailedToStart(Box::new(failure)),
347 ));
348 }
349 };
350 Ok(server_id)
351 }
352}
353
354struct RawWorkspaceSymbol {
359 name: String,
360 kind: u32,
361 container_name: Option<String>,
362 out_of_workspace: bool,
363 uri: lsp_types::Uri,
364 range: lsp_types::Range,
365}
366
367impl RawWorkspaceSymbol {
368 fn new(
369 base: BaseSymbolInformation,
370 uri: lsp_types::Uri,
371 range: lsp_types::Range,
372 ctx: &EncodingCtx,
373 ) -> Self {
374 Self {
375 name: base.name,
376 kind: lsp_kind_to_u32(base.kind),
377 container_name: base.container_name,
378 out_of_workspace: ctx.is_out_of_workspace(&uri),
379 uri,
380 range,
381 }
382 }
383}
384
385fn raw_workspace_symbols(
393 response: Option<lsp_types::WorkspaceSymbolResponse>,
394 ctx: &EncodingCtx,
395) -> Vec<RawWorkspaceSymbol> {
396 match response {
397 Some(lsp_types::WorkspaceSymbolResponse::SymbolInformationList(list)) => list
398 .into_iter()
399 .map(|sym| {
400 RawWorkspaceSymbol::new(
401 sym.base_symbol_information,
402 sym.location.uri,
403 sym.location.range,
404 ctx,
405 )
406 })
407 .collect(),
408 Some(lsp_types::WorkspaceSymbolResponse::WorkspaceSymbolList(list)) => list
409 .into_iter()
410 .filter_map(|sym| match sym.location {
411 lsp_types::WorkspaceSymbolLocation::Location(loc) => Some(RawWorkspaceSymbol::new(
412 sym.base_symbol_information,
413 loc.uri,
414 loc.range,
415 ctx,
416 )),
417 lsp_types::WorkspaceSymbolLocation::LocationUriOnly(_) => None,
424 })
425 .collect(),
426 None => Vec::new(),
427 }
428}
429
430#[cfg(test)]
431mod tests {
432 use std::collections::HashSet;
433 use std::sync::Arc;
434 use std::time::Duration;
435 use std::{assert_matches, fs};
436
437 use tempfile::TempDir;
438 use tokio::io::BufReader;
439 use tokio::time::timeout;
440 use url::Url;
441
442 use super::*;
443 use crate::bridge::translator::dto::{IndexCoverage, PositionDegradation};
444 use crate::bridge::translator::testing::*;
445 use crate::config::{LanguageId, ServerCommand, ServerId, TimeoutSecs, ToolRouter, ToolSet};
446 use crate::test_lsp::client_path;
447
448 #[test]
456 fn test_symbol_kind_debug_rendering_is_pinned() {
457 assert_eq!(
458 format!("{:?}", lsp_types::SymbolKind::EnumMember),
459 "EnumMember"
460 );
461 }
462
463 #[tokio::test]
468 async fn test_convert_document_symbol_kind_is_numeric() {
469 let symbol = DocumentSymbol {
470 name: "my_enum_member".to_string(),
471 detail: None,
472 kind: lsp_types::SymbolKind::EnumMember,
473 tags: None,
474 #[allow(
475 deprecated,
476 reason = "`deprecated` is a deprecated field of the LSP struct literal"
477 )]
478 deprecated: None,
479 range: lsp_types::Range {
480 start: lsp_types::Position {
481 line: 0,
482 character: 0,
483 },
484 end: lsp_types::Position {
485 line: 0,
486 character: 5,
487 },
488 },
489 selection_range: lsp_types::Range {
490 start: lsp_types::Position {
491 line: 0,
492 character: 0,
493 },
494 end: lsp_types::Position {
495 line: 0,
496 character: 5,
497 },
498 },
499 children: None,
500 };
501 let ctx = test_ctx();
502 let uri = lsp_types::Uri::from("file:///tmp/test.rs");
503 let result = convert_document_symbol(symbol, &ctx, &uri).await;
504 assert_eq!(result.kind, 22u32);
506 }
507
508 #[tokio::test]
509 async fn test_handle_workspace_symbol_no_server() {
510 let translator = Translator::new();
511 let result = translator
512 .handle_workspace_symbol("test".to_string(), None, 100)
513 .await;
514 assert_matches!(result, Err(Error::NoServerConfigured));
515 }
516
517 #[tokio::test]
523 async fn test_handle_workspace_symbol_reports_initializing_when_expected_but_not_registered() {
524 let translator = Translator::new();
525 translator.set_expected_servers(HashSet::from([ServerId::from_static("pyright")]));
526
527 let result = translator
528 .handle_workspace_symbol("test".to_string(), None, 100)
529 .await;
530 assert_matches!(result, Err(Error::WorkspaceServersInitializing));
531 }
532
533 #[tokio::test]
536 async fn test_handle_workspace_symbol_reports_startup_failures() {
537 let translator = Translator::new();
538 translator.record_startup_failures(&[crate::error::ServerSpawnFailure {
539 server_id: ServerId::from_static("pyright"),
540 language_id: LanguageId::from_static("python"),
541 command: ServerCommand::from_static("pyright-langserver"),
542 reason: crate::error::StartupFailure::InitTaskPanicked,
543 }]);
544
545 let result = translator
546 .handle_workspace_symbol("test".to_string(), None, 100)
547 .await;
548
549 let Err(Error::AllServersFailedToInit { failures }) = result else {
550 panic!("expected AllServersFailedToInit, got {result:?}");
551 };
552 assert_eq!(failures.len(), 1);
553 assert_eq!(failures[0].server_id, ServerId::from_static("pyright"));
554 }
555
556 #[tokio::test]
559 async fn test_handle_workspace_symbol_reports_failed_claimant() {
560 let mut live = crate::config::LspServerConfig::pyright();
561 live.name = Some(ServerId::from_static("live"));
562 live.handles = Some(ToolSet::new(vec![ToolKind::Hover]).unwrap());
563 let mut failing = crate::config::LspServerConfig::rust_analyzer();
564 failing.name = Some(ServerId::from_static("failing"));
565 failing.handles = Some(ToolSet::new(vec![ToolKind::WorkspaceSymbols]).unwrap());
566 let router = ToolRouter::from_configs([&live, &failing]).unwrap();
567 let failing_id = ServerId::from_static("failing");
568
569 let translator = Translator::new().with_router(router);
570 translator.record_startup_failures(&[crate::error::ServerSpawnFailure {
571 server_id: failing_id.clone(),
572 language_id: LanguageId::from_static("rust"),
573 command: ServerCommand::from_static("rust-analyzer"),
574 reason: crate::error::StartupFailure::InitTaskPanicked,
575 }]);
576 translator.rebind_router(&HashSet::from([ServerId::from_static("live")]));
577 translator.clear_expected_servers();
578
579 let result = translator
580 .handle_workspace_symbol("test".to_string(), None, 100)
581 .await;
582
583 assert_matches!(&result, Err(Error::ServerFailedToStart(f)) if f.server_id == failing_id,
584 "got {result:?}"
585 );
586 }
587
588 #[tokio::test]
590 async fn test_handle_workspace_symbol_initializing_wins_over_recorded_failure() {
591 let translator = Translator::new();
592 translator.set_expected_servers(HashSet::from([ServerId::from_static("pyright")]));
593 translator.record_startup_failures(&[crate::error::ServerSpawnFailure {
594 server_id: ServerId::from_static("other"),
595 language_id: LanguageId::from_static("go"),
596 command: ServerCommand::from_static("gopls"),
597 reason: crate::error::StartupFailure::InitTaskPanicked,
598 }]);
599
600 let result = translator
601 .handle_workspace_symbol("test".to_string(), None, 100)
602 .await;
603
604 assert_matches!(result, Err(Error::WorkspaceServersInitializing));
605 }
606
607 #[tokio::test]
613 async fn test_handle_workspace_symbol_no_claimant_names_tool() {
614 let configs = vec![crate::config::LspServerConfig {
615 language_id: LanguageId::from_static("python"),
616 command: ServerCommand::from_static("pyright-langserver").into(),
617 args: crate::config::ServerArgs::default(),
618 env: crate::config::ServerEnv::default(),
619 file_patterns: vec![],
620 initialization_options: None,
621 settings: None,
622 timeout_seconds: TimeoutSecs::new(30).unwrap(),
623 request_timeout_seconds: TimeoutSecs::new(30).unwrap(),
624 heuristics: None,
625 name: Some(ServerId::from_static("pyright")),
626 handles: Some(ToolSet::new(vec![ToolKind::Hover]).unwrap()),
627 indexing: crate::bridge::IndexingPolicy::Auto,
628 }];
629 let router = ToolRouter::from_configs(&configs).unwrap();
630 let translator = Translator::new().with_router(router);
631
632 let result = translator
633 .handle_workspace_symbol("test".to_string(), None, 100)
634 .await;
635 assert_matches!(
636 result,
637 Err(Error::NoServerForWorkspaceTool {
638 tool: ToolKind::WorkspaceSymbols
639 })
640 );
641 }
642
643 #[tokio::test]
649 async fn test_handle_document_symbols_flat_response_selection_range_matches_range() {
650 let dir = TempDir::new().unwrap();
651 let server_id = ServerId::from_static("rust");
652 let (translator, mut server) = translator_with_capabilities(
653 &dir,
654 &server_id,
655 lsp_types::ServerCapabilities {
656 document_symbol_provider: Some(lsp_types::DocumentSymbolProvider::Bool(true)),
657 ..Default::default()
658 },
659 );
660
661 let path = dir.path().join("main.rs");
662 fs::write(&path, "fn main() {}\n").unwrap();
663 let path_str = path.to_string_lossy().to_string();
664 let uri = Url::from_file_path(&path).unwrap().to_string();
665
666 let translator = Arc::new(translator);
667 let handle = {
668 let translator = Arc::clone(&translator);
669 tokio::spawn(async move {
670 translator
671 .handle_document_symbols(client_path(path_str))
672 .await
673 })
674 };
675
676 let mut wire = BufReader::new(&mut server.write_stdout);
677 let opened = read_framed_message(&mut wire).await;
678 assert_eq!(opened["method"], "textDocument/didOpen");
679 let symbol_request = read_framed_message(&mut wire).await;
680 assert_eq!(symbol_request["method"], "textDocument/documentSymbol");
681
682 write_response(
683 &mut server.read_half_stdin,
684 &symbol_request["id"],
685 serde_json::json!([{
686 "name": "main",
687 "kind": 12,
688 "location": {
689 "uri": uri,
690 "range": {
691 "start": {"line": 0, "character": 0},
692 "end": {"line": 0, "character": 12},
693 },
694 },
695 }]),
696 )
697 .await;
698
699 let result = timeout(Duration::from_secs(2), handle)
700 .await
701 .expect("handler call should not hang")
702 .unwrap()
703 .expect("flat document symbol response should succeed");
704
705 assert_eq!(result.symbols.len(), 1);
706 assert_eq!(result.symbols[0].range, result.symbols[0].selection_range);
707 }
708
709 #[tokio::test]
720 async fn test_handle_document_symbols_flat_response_normalizes_against_response_uri() {
721 let dir = TempDir::new().unwrap();
722 let server_id = ServerId::from_static("rust");
723 let (translator, mut server) = translator_with_capabilities_and_encoding(
724 &dir,
725 &server_id,
726 lsp_types::ServerCapabilities {
727 document_symbol_provider: Some(lsp_types::DocumentSymbolProvider::Bool(true)),
728 ..Default::default()
729 },
730 lsp_types::PositionEncodingKind::UTF8,
731 );
732
733 let path = dir.path().join("main.rs");
734 fs::write(&path, "aöb").unwrap();
735 let path_str = path.to_string_lossy().to_string();
736 let outside_uri = "file:///outside/workspace/does-not-exist.rs";
737
738 let translator = Arc::new(translator);
739 let handle = {
740 let translator = Arc::clone(&translator);
741 tokio::spawn(async move {
742 translator
743 .handle_document_symbols(client_path(path_str))
744 .await
745 })
746 };
747
748 let mut wire = BufReader::new(&mut server.write_stdout);
749 let opened = read_framed_message(&mut wire).await;
750 assert_eq!(opened["method"], "textDocument/didOpen");
751 let symbol_request = read_framed_message(&mut wire).await;
752 assert_eq!(symbol_request["method"], "textDocument/documentSymbol");
753
754 write_response(
755 &mut server.read_half_stdin,
756 &symbol_request["id"],
757 serde_json::json!([{
758 "name": "sym",
759 "kind": 12,
760 "location": {
761 "uri": outside_uri,
762 "range": {
763 "start": {"line": 0, "character": 0},
764 "end": {"line": 0, "character": 3},
765 },
766 },
767 }]),
768 )
769 .await;
770
771 let result = timeout(Duration::from_secs(2), handle)
772 .await
773 .expect("handler call should not hang")
774 .unwrap()
775 .expect("flat document symbol response should succeed");
776
777 assert_eq!(result.symbols.len(), 1);
778 assert_eq!(
779 result.symbols[0].range.end.character, 3,
780 "must convert against the queried document's own content (\"aöb\"), not fail to \
781 read the entry's own (nonexistent, out-of-workspace) location.uri and fall back to \
782 the raw byte offset"
783 );
784 }
785
786 #[tokio::test]
793 async fn test_handle_document_symbols_sets_positions_degraded_for_unresolvable_line() {
794 let dir = TempDir::new().unwrap();
795 let server_id = ServerId::from_static("rust");
796 let (translator, mut server) = translator_with_capabilities_and_encoding(
797 &dir,
798 &server_id,
799 lsp_types::ServerCapabilities {
800 document_symbol_provider: Some(lsp_types::DocumentSymbolProvider::Bool(true)),
801 ..Default::default()
802 },
803 lsp_types::PositionEncodingKind::UTF8,
804 );
805
806 let path = dir.path().join("main.rs");
807 fs::write(&path, "aöb").unwrap();
808 let path_str = path.to_string_lossy().to_string();
809
810 let translator = Arc::new(translator);
811 let handle = {
812 let translator = Arc::clone(&translator);
813 tokio::spawn(async move {
814 translator
815 .handle_document_symbols(client_path(path_str))
816 .await
817 })
818 };
819
820 let mut wire = BufReader::new(&mut server.write_stdout);
821 let opened = read_framed_message(&mut wire).await;
822 assert_eq!(opened["method"], "textDocument/didOpen");
823 let symbol_request = read_framed_message(&mut wire).await;
824 assert_eq!(symbol_request["method"], "textDocument/documentSymbol");
825
826 write_response(
827 &mut server.read_half_stdin,
828 &symbol_request["id"],
829 serde_json::json!([{
830 "name": "sym",
831 "kind": 12,
832 "range": {
835 "start": {"line": 5, "character": 0},
836 "end": {"line": 5, "character": 1},
837 },
838 "selectionRange": {
839 "start": {"line": 5, "character": 0},
840 "end": {"line": 5, "character": 1},
841 },
842 }]),
843 )
844 .await;
845
846 let result = timeout(Duration::from_secs(2), handle)
847 .await
848 .expect("handler call should not hang")
849 .unwrap()
850 .expect("document symbol response should still succeed, just degraded");
851
852 assert_eq!(result.symbols.len(), 1);
853 assert_eq!(
854 result.positions_degraded,
855 Some(PositionDegradation::Response),
856 "a range whose line can't be resolved must mark the result degraded"
857 );
858 }
859
860 #[tokio::test]
866 async fn test_handle_workspace_symbol_drops_location_uri_only_entries() {
867 let dir = TempDir::new().unwrap();
868 let server_id = ServerId::from_static("rust");
869 let caps = lsp_types::ServerCapabilities {
870 workspace_symbol_provider: Some(lsp_types::WorkspaceSymbolProvider::Bool(true)),
871 ..Default::default()
872 };
873 let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
874 let a_uri = Url::from_file_path(dir.path().join("a.rs"))
875 .unwrap()
876 .to_string();
877 let b_uri = Url::from_file_path(dir.path().join("b.rs"))
878 .unwrap()
879 .to_string();
880
881 let translator = Arc::new(translator);
882 let handle = {
883 let translator = Arc::clone(&translator);
884 tokio::spawn(async move {
885 translator
886 .handle_workspace_symbol("foo".to_string(), None, 100)
887 .await
888 })
889 };
890
891 let mut wire = BufReader::new(&mut server.write_stdout);
892 let request = read_framed_message(&mut wire).await;
893 assert_eq!(request["method"], "workspace/symbol");
894
895 write_response(
902 &mut server.read_half_stdin,
903 &request["id"],
904 serde_json::json!([
905 {
906 "name": "with_range",
907 "kind": 12,
908 "location": {
909 "uri": a_uri,
910 "range": {
911 "start": {"line": 0, "character": 0},
912 "end": {"line": 0, "character": 5}
913 }
914 }
915 },
916 {
917 "name": "without_range",
918 "kind": 12,
919 "location": { "uri": b_uri }
920 }
921 ]),
922 )
923 .await;
924
925 let result = timeout(Duration::from_secs(2), handle)
926 .await
927 .expect("handler call should not hang")
928 .unwrap()
929 .unwrap();
930
931 assert_eq!(
932 result.symbols.len(),
933 1,
934 "the range-less LocationUriOnly symbol must be dropped, not fabricated"
935 );
936 assert_eq!(result.symbols[0].name, "with_range");
937 assert!(
938 result.index_coverage.is_none(),
939 "a non-empty result carries no coverage note"
940 );
941 }
942
943 #[tokio::test]
945 async fn test_handle_workspace_symbol_empty_result_carries_index_coverage() {
946 let dir = TempDir::new().unwrap();
947 let server_id = ServerId::from_static("rust");
948 let caps = lsp_types::ServerCapabilities {
949 workspace_symbol_provider: Some(lsp_types::WorkspaceSymbolProvider::Bool(true)),
950 ..Default::default()
951 };
952 let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
953 let translator = Arc::new(translator);
954 let handle = {
955 let translator = Arc::clone(&translator);
956 tokio::spawn(async move {
957 translator
958 .handle_workspace_symbol("foo".to_string(), None, 100)
959 .await
960 })
961 };
962
963 let mut wire = BufReader::new(&mut server.write_stdout);
964 let request = read_framed_message(&mut wire).await;
965 write_response(
966 &mut server.read_half_stdin,
967 &request["id"],
968 serde_json::Value::Null,
969 )
970 .await;
971
972 let result = handle.await.unwrap().unwrap();
973 assert!(result.symbols.is_empty());
974 assert_eq!(
975 result.index_coverage,
976 Some(IndexCoverage {
977 synced_documents: 0,
978 server_restarted: false,
979 })
980 );
981 }
982
983 #[tokio::test]
985 async fn test_handle_workspace_symbol_limit_zero_carries_no_index_coverage() {
986 let dir = TempDir::new().unwrap();
987 let server_id = ServerId::from_static("rust");
988 let caps = lsp_types::ServerCapabilities {
989 workspace_symbol_provider: Some(lsp_types::WorkspaceSymbolProvider::Bool(true)),
990 ..Default::default()
991 };
992 let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
993 let uri = Url::from_file_path(dir.path().join("a.rs"))
994 .unwrap()
995 .to_string();
996 let translator = Arc::new(translator);
997 let handle = {
998 let translator = Arc::clone(&translator);
999 tokio::spawn(async move {
1000 translator
1001 .handle_workspace_symbol("foo".to_string(), None, 0)
1002 .await
1003 })
1004 };
1005
1006 let mut wire = BufReader::new(&mut server.write_stdout);
1007 let request = read_framed_message(&mut wire).await;
1008 write_response(
1009 &mut server.read_half_stdin,
1010 &request["id"],
1011 serde_json::json!([{
1012 "name": "foo",
1013 "kind": 12,
1014 "location": {
1015 "uri": uri,
1016 "range": {
1017 "start": {"line": 0, "character": 0},
1018 "end": {"line": 0, "character": 3}
1019 }
1020 }
1021 }]),
1022 )
1023 .await;
1024
1025 let result = handle.await.unwrap().unwrap();
1026 assert!(result.symbols.is_empty());
1027 assert!(result.truncated);
1028 assert!(result.index_coverage.is_none());
1029 }
1030
1031 #[tokio::test]
1037 async fn test_handle_workspace_symbol_does_not_filter_out_of_workspace_location() {
1038 let dir = TempDir::new().unwrap();
1039 let server_id = ServerId::from_static("rust");
1040 let caps = lsp_types::ServerCapabilities {
1041 workspace_symbol_provider: Some(lsp_types::WorkspaceSymbolProvider::Bool(true)),
1042 ..Default::default()
1043 };
1044 let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
1045 let inside_uri = Url::from_file_path(canonical_dir(&dir).join("inside.rs"))
1046 .unwrap()
1047 .to_string();
1048 let outside_uri = "file:///outside/workspace/evil.rs";
1049
1050 let translator = Arc::new(translator);
1051 let handle = {
1052 let translator = Arc::clone(&translator);
1053 tokio::spawn(async move {
1054 translator
1055 .handle_workspace_symbol("foo".to_string(), None, 100)
1056 .await
1057 })
1058 };
1059
1060 let mut wire = BufReader::new(&mut server.write_stdout);
1061 let request = read_framed_message(&mut wire).await;
1062 assert_eq!(request["method"], "workspace/symbol");
1063
1064 write_response(
1065 &mut server.read_half_stdin,
1066 &request["id"],
1067 serde_json::json!([
1068 {
1069 "name": "inside",
1070 "kind": 12,
1071 "location": {
1072 "uri": inside_uri,
1073 "range": {
1074 "start": {"line": 0, "character": 0},
1075 "end": {"line": 0, "character": 5}
1076 }
1077 }
1078 },
1079 {
1080 "name": "outside",
1081 "kind": 12,
1082 "location": {
1083 "uri": outside_uri,
1084 "range": {
1085 "start": {"line": 0, "character": 0},
1086 "end": {"line": 0, "character": 4}
1087 }
1088 }
1089 }
1090 ]),
1091 )
1092 .await;
1093
1094 let result = timeout(Duration::from_secs(2), handle)
1095 .await
1096 .expect("handler call should not hang")
1097 .unwrap()
1098 .unwrap();
1099
1100 assert_eq!(
1101 result.symbols.len(),
1102 2,
1103 "both the in-workspace and out-of-workspace symbols must be returned"
1104 );
1105 assert!(result.symbols.iter().any(|s| s.name == "inside"));
1106 assert!(result.symbols.iter().any(|s| s.name == "outside"));
1107 assert!(
1108 !result
1109 .symbols
1110 .iter()
1111 .find(|s| s.name == "inside")
1112 .unwrap()
1113 .location
1114 .out_of_workspace,
1115 "an in-workspace symbol location must not be marked out_of_workspace"
1116 );
1117 assert!(
1118 result
1119 .symbols
1120 .iter()
1121 .find(|s| s.name == "outside")
1122 .unwrap()
1123 .location
1124 .out_of_workspace,
1125 "an out-of-workspace symbol location must be marked out_of_workspace"
1126 );
1127 }
1128
1129 #[tokio::test]
1134 async fn test_handle_workspace_symbol_clamps_limit_to_max_normalized_locations() {
1135 let dir = TempDir::new().unwrap();
1136 let server_id = ServerId::from_static("rust");
1137 let caps = lsp_types::ServerCapabilities {
1138 workspace_symbol_provider: Some(lsp_types::WorkspaceSymbolProvider::Bool(true)),
1139 ..Default::default()
1140 };
1141 let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
1142 let uri = Url::from_file_path(dir.path().join("many.rs"))
1143 .unwrap()
1144 .to_string();
1145
1146 let translator = Arc::new(translator);
1147 let handle = {
1148 let translator = Arc::clone(&translator);
1149 tokio::spawn(async move {
1150 translator
1151 .handle_workspace_symbol("foo".to_string(), None, u32::MAX)
1152 .await
1153 })
1154 };
1155
1156 let mut wire = BufReader::new(&mut server.write_stdout);
1157 let request = read_framed_message(&mut wire).await;
1158 assert_eq!(request["method"], "workspace/symbol");
1159
1160 let symbols: Vec<serde_json::Value> = (0..MAX_NORMALIZED_LOCATIONS + 500)
1161 .map(|i| {
1162 serde_json::json!({
1163 "name": format!("sym{i}"),
1164 "kind": 12,
1165 "location": {
1166 "uri": uri,
1167 "range": {
1168 "start": {"line": 0, "character": 0},
1169 "end": {"line": 0, "character": 3}
1170 }
1171 }
1172 })
1173 })
1174 .collect();
1175 write_response(
1176 &mut server.read_half_stdin,
1177 &request["id"],
1178 serde_json::json!(symbols),
1179 )
1180 .await;
1181
1182 let result = timeout(Duration::from_secs(5), handle)
1183 .await
1184 .expect("handler call should not hang")
1185 .unwrap()
1186 .unwrap();
1187
1188 assert_eq!(
1189 result.symbols.len(),
1190 MAX_NORMALIZED_LOCATIONS,
1191 "limit: u32::MAX must be clamped to MAX_NORMALIZED_LOCATIONS, not left unbounded"
1192 );
1193 assert!(
1194 result.truncated,
1195 "a limit clamped below what the caller asked for must set truncated: true"
1196 );
1197 }
1198
1199 #[tokio::test]
1205 async fn test_handle_document_symbols_dispatches_while_indexing_loading() {
1206 use crate::bridge::NotificationCache;
1207
1208 let dir = TempDir::new().unwrap();
1209 let server_id = ServerId::from_static("rust");
1210 let caps = lsp_types::ServerCapabilities {
1211 document_symbol_provider: Some(lsp_types::DocumentSymbolProvider::Bool(true)),
1212 ..Default::default()
1213 };
1214 let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
1215
1216 let cache = Arc::new(tokio::sync::Mutex::new(NotificationCache::new()));
1217 cache.lock().await.observe_indexing_signal(
1218 &server_id,
1219 "experimental/serverStatus",
1220 Some(&serde_json::json!({"quiescent": false})),
1221 );
1222 let translator = Arc::new(translator.with_notification_cache(cache));
1223
1224 let path = dir.path().join("main.rs");
1225 fs::write(&path, "fn main() {}").unwrap();
1226
1227 let handle = {
1228 let translator = Arc::clone(&translator);
1229 let path = path.to_string_lossy().to_string();
1230 tokio::spawn(async move { translator.handle_document_symbols(client_path(path)).await })
1231 };
1232
1233 let mut wire = BufReader::new(&mut server.write_stdout);
1234 let opened = read_framed_message(&mut wire).await;
1235 assert_eq!(opened["method"], "textDocument/didOpen");
1236 let request = read_framed_message(&mut wire).await;
1237 assert_eq!(request["method"], "textDocument/documentSymbol");
1238
1239 write_response(
1240 &mut server.read_half_stdin,
1241 &request["id"],
1242 serde_json::json!([]),
1243 )
1244 .await;
1245
1246 let result = timeout(Duration::from_secs(2), handle)
1247 .await
1248 .expect("handler call should not hang -- document_symbols must not be gated")
1249 .unwrap()
1250 .unwrap();
1251 assert!(result.symbols.is_empty());
1252 }
1253}