1use lsp_types::CallHierarchyItem;
4
5use super::Translator;
6use super::dto::{
7 CallHierarchyPrepareResult, CheckedHierarchyItem, HierarchyItem, IncomingCall,
8 IncomingCallsResult, OutgoingCall, OutgoingCallsResult, Position, PositionDegradation, Range,
9};
10use super::encoding_ctx::EncodingCtx;
11use super::hierarchy::hierarchy_item_to_mcp;
12use super::navigation::ItemBudget;
13use super::positioned::Positioned;
14use super::routing::PreparedDocument;
15use crate::bridge::{ClientPath, Indexed};
16use crate::error::Result;
17
18async fn convert_call_hierarchy_item(item: CallHierarchyItem, ctx: &EncodingCtx) -> HierarchyItem {
20 hierarchy_item_to_mcp(item, ctx).await
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25enum CallDirection {
26 Incoming,
27 Outgoing,
28}
29
30struct WalkedCall {
33 peer: HierarchyItem,
34 from_ranges: Vec<Range>,
35}
36
37struct CallWalk {
39 calls: Vec<WalkedCall>,
40 truncated: bool,
41 positions_degraded: Option<PositionDegradation>,
42}
43
44struct RawCall {
47 peer: CallHierarchyItem,
48 from_ranges: Vec<lsp_types::Range>,
49 ranges_uri: lsp_types::Uri,
50}
51
52impl Translator {
53 pub async fn handle_call_hierarchy_prepare(
60 &self,
61 file_path: ClientPath,
62 position: Position,
63 ) -> Result<Indexed<CallHierarchyPrepareResult>> {
64 let Positioned {
65 result:
66 Indexed {
67 result: response,
68 indexing,
69 },
70 ctx,
71 doc: _doc,
72 } = self
73 .disclosed_position_request::<lsp_types::CallHierarchyPrepareRequest>(
74 &file_path,
75 position,
76 (),
77 )
78 .await?;
79
80 let mut budget = ItemBudget::new();
85 let lsp_items = budget.admit(response.unwrap_or_default());
86 let mut items = Vec::with_capacity(lsp_items.len());
87 for item in lsp_items {
88 items.push(convert_call_hierarchy_item(item, &ctx).await);
89 }
90
91 Ok(Indexed::new(
92 CallHierarchyPrepareResult {
93 items,
94 truncated: budget.truncated(),
95 positions_degraded: ctx.positions_degraded(),
96 },
97 indexing,
98 ))
99 }
100
101 pub async fn handle_incoming_calls(
117 &self,
118 item: CheckedHierarchyItem,
119 ) -> Result<IncomingCallsResult> {
120 let walk = self.walk_calls(item, CallDirection::Incoming).await?;
121 Ok(IncomingCallsResult {
122 calls: walk
123 .calls
124 .into_iter()
125 .map(|call| IncomingCall {
126 from: call.peer,
127 from_ranges: call.from_ranges,
128 })
129 .collect(),
130 truncated: walk.truncated,
131 positions_degraded: walk.positions_degraded,
132 })
133 }
134
135 pub async fn handle_outgoing_calls(
144 &self,
145 item: CheckedHierarchyItem,
146 ) -> Result<OutgoingCallsResult> {
147 let walk = self.walk_calls(item, CallDirection::Outgoing).await?;
148 Ok(OutgoingCallsResult {
149 calls: walk
150 .calls
151 .into_iter()
152 .map(|call| OutgoingCall {
153 to: call.peer,
154 from_ranges: call.from_ranges,
155 })
156 .collect(),
157 truncated: walk.truncated,
158 positions_degraded: walk.positions_degraded,
159 })
160 }
161
162 async fn walk_calls(
164 &self,
165 item: CheckedHierarchyItem,
166 direction: CallDirection,
167 ) -> Result<CallWalk> {
168 let (raw, ctx, _doc): (Vec<RawCall>, EncodingCtx, PreparedDocument) = match direction {
169 CallDirection::Incoming => {
170 let Positioned { result, ctx, doc } = self
171 .item_request::<lsp_types::CallHierarchyIncomingCallsRequest>(item)
172 .await?;
173 let raw = result
174 .unwrap_or_default()
175 .into_iter()
176 .map(|call| RawCall {
177 ranges_uri: call.from.uri.clone(),
180 peer: call.from,
181 from_ranges: call.from_ranges,
182 })
183 .collect();
184 (raw, ctx, doc)
185 }
186 CallDirection::Outgoing => {
187 let Positioned { result, ctx, doc } = self
188 .item_request::<lsp_types::CallHierarchyOutgoingCallsRequest>(item)
189 .await?;
190 let source_uri = doc.uri().clone();
191 let raw = result
192 .unwrap_or_default()
193 .into_iter()
194 .map(|call| RawCall {
195 ranges_uri: source_uri.clone(),
198 peer: call.to,
199 from_ranges: call.from_ranges,
200 })
201 .collect();
202 (raw, ctx, doc)
203 }
204 };
205
206 let mut budget = ItemBudget::new();
208 let mut calls = Vec::new();
209 for call in raw {
210 if !budget.spend_whole(call.from_ranges.len().saturating_add(1)) {
212 continue;
213 }
214 let mut from_ranges = Vec::with_capacity(call.from_ranges.len());
215 for range in call.from_ranges {
216 from_ranges.push(ctx.normalize_range(&call.ranges_uri, range).await);
217 }
218 calls.push(WalkedCall {
219 peer: convert_call_hierarchy_item(call.peer, &ctx).await,
220 from_ranges,
221 });
222 }
223
224 Ok(CallWalk {
225 calls,
226 truncated: budget.truncated(),
227 positions_degraded: ctx.positions_degraded(),
228 })
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use std::sync::Arc;
235 use std::time::Duration;
236 use std::{assert_matches, fs};
237
238 use tempfile::TempDir;
239 use tokio::io::BufReader;
240 use tokio::sync::Mutex;
241 use tokio::time::timeout;
242 use url::Url;
243
244 use super::*;
245 use crate::bridge::translator::dto::{Position2D, Range};
246 use crate::bridge::translator::testing::*;
247 use crate::bridge::{NotificationCache, WorkspaceRoots};
248 use crate::config::ServerId;
249 use crate::error::Error;
250 use crate::test_lsp::client_path;
251
252 fn call_hierarchy_item_json(uri: &str) -> serde_json::Value {
256 serde_json::to_value(HierarchyItem {
257 name: "queried_fn".to_string(),
258 kind: 12,
259 detail: None,
260 uri: uri.to_string(),
261 range: Range {
262 start: Position2D {
263 line: 1,
264 character: 1,
265 },
266 end: Position2D {
267 line: 1,
268 character: 4,
269 },
270 },
271 selection_range: Range {
272 start: Position2D {
273 line: 1,
274 character: 1,
275 },
276 end: Position2D {
277 line: 1,
278 character: 4,
279 },
280 },
281 data: None,
282 out_of_workspace: false,
283 })
284 .unwrap()
285 }
286
287 #[tokio::test(start_paused = true)]
292 async fn test_handle_incoming_calls_returns_workspace_indexing_error_when_loading() {
293 let dir = TempDir::new().unwrap();
294 let server_id = ServerId::from_static("rust");
295 let caps = lsp_types::ServerCapabilities {
296 call_hierarchy_provider: Some(lsp_types::CallHierarchyProvider::Bool(true)),
297 ..Default::default()
298 };
299 let (translator, _server) = translator_with_capabilities(&dir, &server_id, caps);
300
301 let cache = Arc::new(Mutex::new(NotificationCache::new()));
302 cache.lock().await.observe_indexing_signal(
303 &server_id,
304 "experimental/serverStatus",
305 Some(&serde_json::json!({"quiescent": false})),
306 );
307 let translator = translator.with_notification_cache(cache);
308
309 let path = dir.path().join("queried.rs");
310 fs::write(&path, "fn queried() {}").unwrap();
311 let uri = Url::from_file_path(&path).unwrap().to_string();
312
313 let err = translator
314 .handle_incoming_calls(checked_hierarchy_item(call_hierarchy_item_json(&uri)))
315 .await
316 .unwrap_err();
317
318 assert_matches!(
319 err,
320 Error::WorkspaceIndexing { server_id: id, .. } if id == server_id
321 );
322 }
323
324 #[tokio::test(start_paused = true)]
327 async fn test_handle_outgoing_calls_returns_workspace_indexing_error_when_loading() {
328 let dir = TempDir::new().unwrap();
329 let server_id = ServerId::from_static("rust");
330 let caps = lsp_types::ServerCapabilities {
331 call_hierarchy_provider: Some(lsp_types::CallHierarchyProvider::Bool(true)),
332 ..Default::default()
333 };
334 let (translator, _server) = translator_with_capabilities(&dir, &server_id, caps);
335
336 let cache = Arc::new(Mutex::new(NotificationCache::new()));
337 cache.lock().await.observe_indexing_signal(
338 &server_id,
339 "experimental/serverStatus",
340 Some(&serde_json::json!({"quiescent": false})),
341 );
342 let translator = translator.with_notification_cache(cache);
343
344 let path = dir.path().join("queried.rs");
345 fs::write(&path, "fn queried() {}").unwrap();
346 let uri = Url::from_file_path(&path).unwrap().to_string();
347
348 let err = translator
349 .handle_outgoing_calls(checked_hierarchy_item(call_hierarchy_item_json(&uri)))
350 .await
351 .unwrap_err();
352
353 assert_matches!(
354 err,
355 Error::WorkspaceIndexing { server_id: id, .. } if id == server_id
356 );
357 }
358
359 #[tokio::test]
371 async fn test_handle_incoming_calls_with_nonexistent_file_uri_returns_file_io_not_invalid_uri()
372 {
373 let mut translator = Translator::new();
374 #[cfg(windows)]
377 translator.set_workspace_roots(
378 WorkspaceRoots::from_paths(&[std::path::PathBuf::from(r"C:\")]).unwrap(),
379 );
380 #[cfg(not(windows))]
381 translator.set_workspace_roots(
382 WorkspaceRoots::from_paths(&[std::path::PathBuf::from("/")]).unwrap(),
383 );
384 #[cfg(windows)]
391 let uri = "file:///C:/this/path/does/not/exist/anywhere.rs";
392 #[cfg(not(windows))]
393 let uri = "file:///this/path/does/not/exist/anywhere.rs";
394 let item = serde_json::json!({
395 "name": "foo",
396 "kind": 12,
397 "uri": uri,
398 "range": {
399 "start": {"line": 1, "character": 1},
400 "end": {"line": 1, "character": 1}
401 },
402 "selectionRange": {
403 "start": {"line": 1, "character": 1},
404 "end": {"line": 1, "character": 1}
405 }
406 });
407
408 let result = translator
409 .handle_incoming_calls(checked_hierarchy_item(item))
410 .await;
411
412 assert_matches!(
413 result,
414 Err(Error::FileIo { .. }),
415 "expected Error::FileIo from the canonicalize() failure now that Uri construction \
416 cannot itself reject a malformed uri, got {result:?}"
417 );
418 }
419
420 #[tokio::test]
424 async fn test_handle_incoming_calls_bounds_the_echo_of_a_non_file_uri() {
425 let translator = Translator::new();
426 let uri = format!("x\n{}", "Z".repeat(1 << 20));
427 let item = serde_json::json!({
428 "name": "foo",
429 "kind": 12,
430 "uri": uri,
431 "range": {
432 "start": {"line": 1, "character": 1},
433 "end": {"line": 1, "character": 1}
434 },
435 "selectionRange": {
436 "start": {"line": 1, "character": 1},
437 "end": {"line": 1, "character": 1}
438 }
439 });
440
441 let result = translator
442 .handle_incoming_calls(checked_hierarchy_item(item))
443 .await;
444
445 let Err(Error::InvalidToolParams(message)) = result else {
446 panic!("expected InvalidToolParams, got {result:?}");
447 };
448 assert!(
449 message.len() <= 2 * crate::util::MAX_ECHOED_PATH_BYTES,
450 "{}",
451 message.len()
452 );
453 assert!(
454 message.ends_with(crate::util::TRUNCATION_MARKER),
455 "{message:.80}"
456 );
457 assert!(!message.contains('\n'));
458 }
459
460 #[tokio::test]
463 async fn test_handle_outgoing_calls_with_nonexistent_file_uri_returns_file_io_not_invalid_uri()
464 {
465 let mut translator = Translator::new();
466 #[cfg(windows)]
469 translator.set_workspace_roots(
470 WorkspaceRoots::from_paths(&[std::path::PathBuf::from(r"C:\")]).unwrap(),
471 );
472 #[cfg(not(windows))]
473 translator.set_workspace_roots(
474 WorkspaceRoots::from_paths(&[std::path::PathBuf::from("/")]).unwrap(),
475 );
476 #[cfg(windows)]
477 let uri = "file:///C:/this/path/does/not/exist/anywhere.rs";
478 #[cfg(not(windows))]
479 let uri = "file:///this/path/does/not/exist/anywhere.rs";
480 let item = serde_json::json!({
481 "name": "foo",
482 "kind": 12,
483 "uri": uri,
484 "range": {
485 "start": {"line": 1, "character": 1},
486 "end": {"line": 1, "character": 1}
487 },
488 "selectionRange": {
489 "start": {"line": 1, "character": 1},
490 "end": {"line": 1, "character": 1}
491 }
492 });
493
494 let result = translator
495 .handle_outgoing_calls(checked_hierarchy_item(item))
496 .await;
497
498 assert_matches!(
499 result,
500 Err(Error::FileIo { .. }),
501 "expected Error::FileIo from the canonicalize() failure now that Uri construction \
502 cannot itself reject a malformed uri, got {result:?}"
503 );
504 }
505
506 #[tokio::test]
507 async fn test_convert_call_hierarchy_item_kind_is_numeric() {
508 let item = lsp_types::CallHierarchyItem {
509 name: "my_fn".to_string(),
510 kind: lsp_types::SymbolKind::Function,
511 tags: None,
512 detail: None,
513 uri: lsp_types::Uri::from("file:///tmp/test.rs"),
514 range: lsp_types::Range {
515 start: lsp_types::Position {
516 line: 0,
517 character: 0,
518 },
519 end: lsp_types::Position {
520 line: 0,
521 character: 5,
522 },
523 },
524 selection_range: lsp_types::Range {
525 start: lsp_types::Position {
526 line: 0,
527 character: 0,
528 },
529 end: lsp_types::Position {
530 line: 0,
531 character: 5,
532 },
533 },
534 data: None,
535 };
536 let result = convert_call_hierarchy_item(item, &test_ctx()).await;
537 assert_eq!(result.kind, 12u32);
539 assert_eq!(result.name, "my_fn");
540 }
541
542 #[tokio::test]
548 async fn test_handle_incoming_calls_round_trips_non_function_kind() {
549 let dir = TempDir::new().unwrap();
550 let server_id = ServerId::from_static("rust");
551 let caps = lsp_types::ServerCapabilities {
552 call_hierarchy_provider: Some(lsp_types::CallHierarchyProvider::Bool(true)),
553 ..Default::default()
554 };
555 let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
556
557 let queried_path = dir.path().join("queried.rs");
558 fs::write(&queried_path, "fn queried() {}").unwrap();
559 let queried_uri = Url::from_file_path(&queried_path).unwrap().to_string();
560
561 let item = HierarchyItem {
562 name: "queried_method".to_string(),
563 kind: 6, detail: None,
565 uri: queried_uri,
566 range: Range {
567 start: Position2D {
568 line: 1,
569 character: 1,
570 },
571 end: Position2D {
572 line: 1,
573 character: 4,
574 },
575 },
576 selection_range: Range {
577 start: Position2D {
578 line: 1,
579 character: 1,
580 },
581 end: Position2D {
582 line: 1,
583 character: 4,
584 },
585 },
586 data: None,
587 out_of_workspace: false,
588 };
589
590 let translator = Arc::new(translator);
591 let handle = {
592 let translator = Arc::clone(&translator);
593 let item = serde_json::to_value(item).unwrap();
594 tokio::spawn(async move {
595 translator
596 .handle_incoming_calls(checked_hierarchy_item(item))
597 .await
598 })
599 };
600
601 let mut wire = BufReader::new(&mut server.write_stdout);
602 let opened = read_framed_message(&mut wire).await;
603 assert_eq!(opened["method"], "textDocument/didOpen");
604
605 let request = read_framed_message(&mut wire).await;
606 assert_eq!(request["method"], "callHierarchy/incomingCalls");
607 assert_eq!(
608 request["params"]["item"]["kind"], 6,
609 "the reverse u32 -> SymbolKind conversion must round-trip a non-Function kind \
610 exactly, not fall back to Function (12)"
611 );
612
613 write_response(
614 &mut server.read_half_stdin,
615 &request["id"],
616 serde_json::json!([]),
617 )
618 .await;
619
620 let result = timeout(Duration::from_secs(2), handle)
621 .await
622 .expect("handler call should not hang")
623 .unwrap()
624 .unwrap();
625 assert!(result.calls.is_empty());
626 }
627
628 #[tokio::test]
638 async fn test_handle_incoming_calls_from_ranges_convert_against_callers_own_uri() {
639 let dir = TempDir::new().unwrap();
640 let server_id = ServerId::from_static("rust");
641 let caps = lsp_types::ServerCapabilities {
642 call_hierarchy_provider: Some(lsp_types::CallHierarchyProvider::Bool(true)),
643 ..Default::default()
644 };
645 let (translator, mut server) = translator_with_capabilities_and_encoding(
646 &dir,
647 &server_id,
648 caps,
649 lsp_types::PositionEncodingKind::UTF8,
650 );
651
652 let queried_path = canonical_dir(&dir).join("queried.rs");
653 fs::write(&queried_path, "abc").unwrap();
654 let queried_uri = Url::from_file_path(&queried_path).unwrap().to_string();
655
656 let caller_path = canonical_dir(&dir).join("caller.rs");
657 fs::write(&caller_path, "aöb").unwrap();
658 let caller_uri = Url::from_file_path(&caller_path).unwrap().to_string();
659
660 let item = HierarchyItem {
661 name: "queried_fn".to_string(),
662 kind: 12,
663 detail: None,
664 uri: queried_uri,
665 range: Range {
666 start: Position2D {
667 line: 1,
668 character: 1,
669 },
670 end: Position2D {
671 line: 1,
672 character: 4,
673 },
674 },
675 selection_range: Range {
676 start: Position2D {
677 line: 1,
678 character: 1,
679 },
680 end: Position2D {
681 line: 1,
682 character: 4,
683 },
684 },
685 data: None,
686 out_of_workspace: false,
687 };
688
689 let translator = Arc::new(translator);
690 let handle = {
691 let translator = Arc::clone(&translator);
692 let item = checked_hierarchy_item(serde_json::to_value(item).unwrap());
693 tokio::spawn(async move { translator.handle_incoming_calls(item).await })
694 };
695
696 let mut wire = BufReader::new(&mut server.write_stdout);
697 let opened = read_framed_message(&mut wire).await;
698 assert_eq!(opened["method"], "textDocument/didOpen");
699
700 let request = read_framed_message(&mut wire).await;
701 assert_eq!(request["method"], "callHierarchy/incomingCalls");
702
703 write_response(
704 &mut server.read_half_stdin,
705 &request["id"],
706 serde_json::json!([{
707 "from": {
708 "name": "caller_fn",
709 "kind": 12,
710 "uri": caller_uri,
711 "range": {
712 "start": {"line": 0, "character": 0},
713 "end": {"line": 0, "character": 1}
714 },
715 "selectionRange": {
716 "start": {"line": 0, "character": 0},
717 "end": {"line": 0, "character": 1}
718 }
719 },
720 "fromRanges": [{
721 "start": {"line": 0, "character": 0},
722 "end": {"line": 0, "character": 3}
723 }]
724 }]),
725 )
726 .await;
727
728 let result = timeout(Duration::from_secs(2), handle)
729 .await
730 .expect("handler call should not hang")
731 .unwrap()
732 .unwrap();
733
734 assert_eq!(result.calls.len(), 1);
735 let from_range = &result.calls[0].from_ranges[0];
736 assert_eq!(
737 from_range.end.character, 3,
738 "fromRanges must convert against the caller's own file (\"aöb\"), not the queried \
739 item's (\"abc\") -- a byte offset of 3 is UTF-16 column 3 in the former, 4 in the \
740 latter"
741 );
742 assert!(
743 !result.calls[0].from.out_of_workspace,
744 "an in-workspace caller must not be marked out_of_workspace"
745 );
746 }
747
748 #[tokio::test]
752 async fn test_handle_outgoing_calls_from_ranges_convert_against_queried_uri() {
753 let dir = TempDir::new().unwrap();
754 let server_id = ServerId::from_static("rust");
755 let caps = lsp_types::ServerCapabilities {
756 call_hierarchy_provider: Some(lsp_types::CallHierarchyProvider::Bool(true)),
757 ..Default::default()
758 };
759 let (translator, mut server) = translator_with_capabilities_and_encoding(
760 &dir,
761 &server_id,
762 caps,
763 lsp_types::PositionEncodingKind::UTF8,
764 );
765
766 let queried_path = dir.path().join("queried.rs");
767 fs::write(&queried_path, "aöb").unwrap();
768 let queried_uri = Url::from_file_path(&queried_path).unwrap().to_string();
769
770 let callee_path = dir.path().join("callee.rs");
771 fs::write(&callee_path, "abc").unwrap();
772 let callee_uri = Url::from_file_path(&callee_path).unwrap().to_string();
773
774 let item = HierarchyItem {
775 name: "queried_fn".to_string(),
776 kind: 12,
777 detail: None,
778 uri: queried_uri,
779 range: Range {
780 start: Position2D {
781 line: 1,
782 character: 1,
783 },
784 end: Position2D {
785 line: 1,
786 character: 4,
787 },
788 },
789 selection_range: Range {
790 start: Position2D {
791 line: 1,
792 character: 1,
793 },
794 end: Position2D {
795 line: 1,
796 character: 4,
797 },
798 },
799 data: None,
800 out_of_workspace: false,
801 };
802
803 let translator = Arc::new(translator);
804 let handle = {
805 let translator = Arc::clone(&translator);
806 let item = serde_json::to_value(item).unwrap();
807 tokio::spawn(async move {
808 translator
809 .handle_outgoing_calls(checked_hierarchy_item(item))
810 .await
811 })
812 };
813
814 let mut wire = BufReader::new(&mut server.write_stdout);
815 let opened = read_framed_message(&mut wire).await;
816 assert_eq!(opened["method"], "textDocument/didOpen");
817
818 let request = read_framed_message(&mut wire).await;
819 assert_eq!(request["method"], "callHierarchy/outgoingCalls");
820
821 write_response(
822 &mut server.read_half_stdin,
823 &request["id"],
824 serde_json::json!([{
825 "to": {
826 "name": "callee_fn",
827 "kind": 12,
828 "uri": callee_uri,
829 "range": {
830 "start": {"line": 0, "character": 0},
831 "end": {"line": 0, "character": 1}
832 },
833 "selectionRange": {
834 "start": {"line": 0, "character": 0},
835 "end": {"line": 0, "character": 1}
836 }
837 },
838 "fromRanges": [{
839 "start": {"line": 0, "character": 0},
840 "end": {"line": 0, "character": 3}
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 .unwrap();
851
852 assert_eq!(result.calls.len(), 1);
853 let from_range = &result.calls[0].from_ranges[0];
854 assert_eq!(
855 from_range.end.character, 3,
856 "fromRanges must convert against the queried item's own file (\"aöb\"), not the \
857 callee's (\"abc\") -- a byte offset of 3 is UTF-16 column 3 in the former, 4 in \
858 the latter"
859 );
860 }
861
862 #[tokio::test]
870 async fn test_handle_incoming_calls_does_not_filter_out_of_workspace_caller() {
871 let dir = TempDir::new().unwrap();
872 let server_id = ServerId::from_static("rust");
873 let caps = lsp_types::ServerCapabilities {
874 call_hierarchy_provider: Some(lsp_types::CallHierarchyProvider::Bool(true)),
875 ..Default::default()
876 };
877 let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
878
879 let queried_path = dir.path().join("queried.rs");
880 fs::write(&queried_path, "fn queried() {}").unwrap();
881 let queried_uri = Url::from_file_path(&queried_path).unwrap().to_string();
882 let outside_uri = "file:///outside/workspace/stdlib.rs";
883
884 let item = HierarchyItem {
885 name: "queried_fn".to_string(),
886 kind: 12,
887 detail: None,
888 uri: queried_uri,
889 range: Range {
890 start: Position2D {
891 line: 1,
892 character: 1,
893 },
894 end: Position2D {
895 line: 1,
896 character: 4,
897 },
898 },
899 selection_range: Range {
900 start: Position2D {
901 line: 1,
902 character: 1,
903 },
904 end: Position2D {
905 line: 1,
906 character: 4,
907 },
908 },
909 data: None,
910 out_of_workspace: false,
911 };
912
913 let translator = Arc::new(translator);
914 let handle = {
915 let translator = Arc::clone(&translator);
916 let item = serde_json::to_value(item).unwrap();
917 tokio::spawn(async move {
918 translator
919 .handle_incoming_calls(checked_hierarchy_item(item))
920 .await
921 })
922 };
923
924 let mut wire = BufReader::new(&mut server.write_stdout);
925 let opened = read_framed_message(&mut wire).await;
926 assert_eq!(opened["method"], "textDocument/didOpen");
927
928 let request = read_framed_message(&mut wire).await;
929 assert_eq!(request["method"], "callHierarchy/incomingCalls");
930
931 write_response(
932 &mut server.read_half_stdin,
933 &request["id"],
934 serde_json::json!([{
935 "from": {
936 "name": "caller_fn",
937 "kind": 12,
938 "uri": outside_uri,
939 "range": {
940 "start": {"line": 0, "character": 0},
941 "end": {"line": 0, "character": 1}
942 },
943 "selectionRange": {
944 "start": {"line": 0, "character": 0},
945 "end": {"line": 0, "character": 1}
946 }
947 },
948 "fromRanges": [{
949 "start": {"line": 0, "character": 0},
950 "end": {"line": 0, "character": 1}
951 }]
952 }]),
953 )
954 .await;
955
956 let result = timeout(Duration::from_secs(2), handle)
957 .await
958 .expect("handler call should not hang")
959 .unwrap()
960 .unwrap();
961
962 assert_eq!(
963 result.calls.len(),
964 1,
965 "an out-of-workspace caller must be returned, not dropped"
966 );
967 assert_eq!(result.calls[0].from.uri, outside_uri);
968 assert!(
969 result.calls[0].from.out_of_workspace,
970 "an out-of-workspace caller must be marked out_of_workspace"
971 );
972 }
973
974 #[tokio::test]
978 async fn test_handle_outgoing_calls_does_not_filter_out_of_workspace_callee() {
979 let dir = TempDir::new().unwrap();
980 let server_id = ServerId::from_static("rust");
981 let caps = lsp_types::ServerCapabilities {
982 call_hierarchy_provider: Some(lsp_types::CallHierarchyProvider::Bool(true)),
983 ..Default::default()
984 };
985 let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
986
987 let queried_path = dir.path().join("queried.rs");
988 fs::write(&queried_path, "fn queried() {}").unwrap();
989 let queried_uri = Url::from_file_path(&queried_path).unwrap().to_string();
990 let outside_uri = "file:///outside/workspace/stdlib.rs";
991
992 let item = HierarchyItem {
993 name: "queried_fn".to_string(),
994 kind: 12,
995 detail: None,
996 uri: queried_uri,
997 range: Range {
998 start: Position2D {
999 line: 1,
1000 character: 1,
1001 },
1002 end: Position2D {
1003 line: 1,
1004 character: 4,
1005 },
1006 },
1007 selection_range: Range {
1008 start: Position2D {
1009 line: 1,
1010 character: 1,
1011 },
1012 end: Position2D {
1013 line: 1,
1014 character: 4,
1015 },
1016 },
1017 data: None,
1018 out_of_workspace: false,
1019 };
1020
1021 let translator = Arc::new(translator);
1022 let handle = {
1023 let translator = Arc::clone(&translator);
1024 let item = serde_json::to_value(item).unwrap();
1025 tokio::spawn(async move {
1026 translator
1027 .handle_outgoing_calls(checked_hierarchy_item(item))
1028 .await
1029 })
1030 };
1031
1032 let mut wire = BufReader::new(&mut server.write_stdout);
1033 let opened = read_framed_message(&mut wire).await;
1034 assert_eq!(opened["method"], "textDocument/didOpen");
1035
1036 let request = read_framed_message(&mut wire).await;
1037 assert_eq!(request["method"], "callHierarchy/outgoingCalls");
1038
1039 write_response(
1040 &mut server.read_half_stdin,
1041 &request["id"],
1042 serde_json::json!([{
1043 "to": {
1044 "name": "callee_fn",
1045 "kind": 12,
1046 "uri": outside_uri,
1047 "range": {
1048 "start": {"line": 0, "character": 0},
1049 "end": {"line": 0, "character": 1}
1050 },
1051 "selectionRange": {
1052 "start": {"line": 0, "character": 0},
1053 "end": {"line": 0, "character": 1}
1054 }
1055 },
1056 "fromRanges": [{
1057 "start": {"line": 0, "character": 0},
1058 "end": {"line": 0, "character": 1}
1059 }]
1060 }]),
1061 )
1062 .await;
1063
1064 let result = timeout(Duration::from_secs(2), handle)
1065 .await
1066 .expect("handler call should not hang")
1067 .unwrap()
1068 .unwrap();
1069
1070 assert_eq!(
1071 result.calls.len(),
1072 1,
1073 "an out-of-workspace callee must be returned, not dropped"
1074 );
1075 assert_eq!(result.calls[0].to.uri, outside_uri);
1076 assert!(
1077 result.calls[0].to.out_of_workspace,
1078 "an out-of-workspace callee must be marked out_of_workspace"
1079 );
1080 }
1081
1082 #[tokio::test]
1088 async fn test_prepare_then_incoming_calls_round_trip_percent_encoded_path() {
1089 let dir = TempDir::new().unwrap();
1090 let server_id = ServerId::from_static("rust");
1091 let caps = lsp_types::ServerCapabilities {
1092 call_hierarchy_provider: Some(lsp_types::CallHierarchyProvider::Bool(true)),
1093 ..Default::default()
1094 };
1095 let (translator, mut server) = translator_with_capabilities_and_encoding(
1096 &dir,
1097 &server_id,
1098 caps,
1099 lsp_types::PositionEncodingKind::UTF8,
1100 );
1101
1102 let file_path = dir.path().join("my file café.rs");
1103 fs::write(&file_path, "fn foo() {}").unwrap();
1104 let file_uri = Url::from_file_path(&file_path).unwrap().to_string();
1105 assert!(
1106 file_uri.contains("%20"),
1107 "test fixture must exercise percent-encoding"
1108 );
1109
1110 let translator = Arc::new(translator);
1111 let mut wire = BufReader::new(&mut server.write_stdout);
1112
1113 let prepare_handle = {
1114 let translator = Arc::clone(&translator);
1115 let path = file_path.to_string_lossy().into_owned();
1116 tokio::spawn(async move {
1117 translator
1118 .handle_call_hierarchy_prepare(client_path(path), pos(1, 1))
1119 .await
1120 })
1121 };
1122
1123 let opened = read_framed_message(&mut wire).await;
1124 assert_eq!(opened["method"], "textDocument/didOpen");
1125 let request = read_framed_message(&mut wire).await;
1126 assert_eq!(request["method"], "textDocument/prepareCallHierarchy");
1127
1128 write_response(
1129 &mut server.read_half_stdin,
1130 &request["id"],
1131 serde_json::json!([{
1132 "name": "foo",
1133 "kind": 12,
1134 "uri": file_uri,
1135 "range": {
1136 "start": {"line": 0, "character": 0},
1137 "end": {"line": 0, "character": 11}
1138 },
1139 "selectionRange": {
1140 "start": {"line": 0, "character": 3},
1141 "end": {"line": 0, "character": 6}
1142 }
1143 }]),
1144 )
1145 .await;
1146
1147 let prepare_result = timeout(Duration::from_secs(2), prepare_handle)
1148 .await
1149 .expect("prepare should not hang")
1150 .unwrap()
1151 .unwrap()
1152 .result;
1153 assert_eq!(prepare_result.items.len(), 1);
1154 let item = prepare_result.items[0].clone();
1155 assert!(item.uri.contains("%20"));
1156
1157 let incoming_handle = {
1158 let translator = Arc::clone(&translator);
1159 let item = serde_json::to_value(item).unwrap();
1160 tokio::spawn(async move {
1161 translator
1162 .handle_incoming_calls(checked_hierarchy_item(item))
1163 .await
1164 })
1165 };
1166
1167 let request = read_framed_message(&mut wire).await;
1168 assert_eq!(request["method"], "callHierarchy/incomingCalls");
1169
1170 write_response(
1171 &mut server.read_half_stdin,
1172 &request["id"],
1173 serde_json::json!([]),
1174 )
1175 .await;
1176
1177 let incoming_result = timeout(Duration::from_secs(2), incoming_handle)
1178 .await
1179 .expect("incoming calls should not hang")
1180 .unwrap();
1181
1182 assert!(
1183 incoming_result.is_ok(),
1184 "expected success resolving the percent-encoded path, got {incoming_result:?}"
1185 );
1186 }
1187
1188 #[derive(Clone, Copy)]
1189 enum Direction {
1190 Incoming,
1191 Outgoing,
1192 }
1193
1194 impl Direction {
1195 const fn lsp_method(self) -> &'static str {
1196 match self {
1197 Self::Incoming => "callHierarchy/incomingCalls",
1198 Self::Outgoing => "callHierarchy/outgoingCalls",
1199 }
1200 }
1201
1202 const fn item_field(self) -> &'static str {
1204 match self {
1205 Self::Incoming => "from",
1206 Self::Outgoing => "to",
1207 }
1208 }
1209 }
1210
1211 async fn calls_with_response(
1214 direction: Direction,
1215 response: impl FnOnce(&str) -> serde_json::Value,
1216 ) -> serde_json::Value {
1217 let dir = TempDir::new().unwrap();
1218 let server_id = ServerId::from_static("rust");
1219 let caps = lsp_types::ServerCapabilities {
1220 call_hierarchy_provider: Some(lsp_types::CallHierarchyProvider::Bool(true)),
1221 ..Default::default()
1222 };
1223 let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
1224
1225 let queried_path = dir.path().join("queried.rs");
1226 fs::write(&queried_path, "fn queried() {}").unwrap();
1227 let queried_uri = Url::from_file_path(&queried_path).unwrap().to_string();
1228 let item = call_hierarchy_item_json(&queried_uri);
1229
1230 let translator = Arc::new(translator);
1231 let handle = {
1232 let translator = Arc::clone(&translator);
1233 tokio::spawn(async move {
1234 match direction {
1235 Direction::Incoming => serde_json::to_value(
1236 translator
1237 .handle_incoming_calls(checked_hierarchy_item(item))
1238 .await
1239 .unwrap(),
1240 ),
1241 Direction::Outgoing => serde_json::to_value(
1242 translator
1243 .handle_outgoing_calls(checked_hierarchy_item(item))
1244 .await
1245 .unwrap(),
1246 ),
1247 }
1248 .unwrap()
1249 })
1250 };
1251
1252 let mut wire = BufReader::new(&mut server.write_stdout);
1253 let opened = read_framed_message(&mut wire).await;
1254 assert_eq!(opened["method"], "textDocument/didOpen");
1255 let request = read_framed_message(&mut wire).await;
1256 assert_eq!(request["method"], direction.lsp_method());
1257
1258 write_response(
1259 &mut server.read_half_stdin,
1260 &request["id"],
1261 response(&queried_uri),
1262 )
1263 .await;
1264
1265 timeout(Duration::from_secs(30), handle)
1266 .await
1267 .expect("handler call should not hang")
1268 .unwrap()
1269 }
1270
1271 fn hierarchy_call_json(uri: &str, direction: Direction, ranges: usize) -> serde_json::Value {
1272 let range = serde_json::json!({
1273 "start": {"line": 0, "character": 0},
1274 "end": {"line": 0, "character": 1}
1275 });
1276 serde_json::json!({
1277 direction.item_field(): {
1278 "name": "f", "kind": 12, "uri": uri,
1279 "range": range, "selectionRange": range
1280 },
1281 "fromRanges": vec![range; ranges]
1282 })
1283 }
1284
1285 fn range_count(call: &serde_json::Value) -> usize {
1286 call["from_ranges"].as_array().unwrap().len()
1287 }
1288
1289 #[tokio::test]
1293 async fn test_handle_incoming_calls_drops_call_whole_when_ranges_exceed_budget() {
1294 use crate::bridge::translator::navigation::MAX_NORMALIZED_LOCATIONS;
1295
1296 let result = calls_with_response(Direction::Incoming, |uri| {
1297 serde_json::json!([hierarchy_call_json(
1298 uri,
1299 Direction::Incoming,
1300 MAX_NORMALIZED_LOCATIONS
1301 )])
1302 })
1303 .await;
1304
1305 assert_eq!(result["truncated"], true);
1306 assert_eq!(result["calls"].as_array().unwrap().len(), 0);
1307 }
1308
1309 #[tokio::test]
1312 async fn test_handle_incoming_calls_never_returns_call_with_ranges_cut() {
1313 use crate::bridge::translator::navigation::MAX_NORMALIZED_LOCATIONS;
1314
1315 let ranges_per_call = MAX_NORMALIZED_LOCATIONS / 2;
1316 let result = calls_with_response(Direction::Incoming, |uri| {
1317 let call = hierarchy_call_json(uri, Direction::Incoming, ranges_per_call);
1318 serde_json::Value::Array(vec![call; 3])
1319 })
1320 .await;
1321
1322 assert_eq!(result["truncated"], true);
1323 let calls = result["calls"].as_array().unwrap();
1324 assert_eq!(calls.len(), 1);
1325 assert_eq!(range_count(&calls[0]), ranges_per_call);
1326 }
1327
1328 #[tokio::test]
1331 async fn test_handle_incoming_calls_exactly_at_cap_is_not_truncated() {
1332 use crate::bridge::translator::navigation::MAX_NORMALIZED_LOCATIONS;
1333
1334 let result = calls_with_response(Direction::Incoming, |uri| {
1335 serde_json::json!([hierarchy_call_json(
1336 uri,
1337 Direction::Incoming,
1338 MAX_NORMALIZED_LOCATIONS - 1
1339 )])
1340 })
1341 .await;
1342
1343 assert!(result.get("truncated").is_none());
1344 assert_eq!(
1345 range_count(&result["calls"][0]),
1346 MAX_NORMALIZED_LOCATIONS - 1
1347 );
1348 }
1349
1350 #[tokio::test]
1352 async fn test_handle_outgoing_calls_caps_item_count_and_reports_truncation() {
1353 use crate::bridge::translator::navigation::MAX_NORMALIZED_LOCATIONS;
1354
1355 let result = calls_with_response(Direction::Outgoing, |uri| {
1356 let call = hierarchy_call_json(uri, Direction::Outgoing, 0);
1357 serde_json::Value::Array(vec![call; MAX_NORMALIZED_LOCATIONS + 500])
1358 })
1359 .await;
1360
1361 assert_eq!(result["truncated"], true);
1362 assert_eq!(
1363 result["calls"].as_array().unwrap().len(),
1364 MAX_NORMALIZED_LOCATIONS
1365 );
1366 }
1367
1368 #[tokio::test]
1370 async fn test_handle_incoming_calls_within_cap_is_not_truncated() {
1371 let result = calls_with_response(Direction::Incoming, |uri| {
1372 serde_json::json!([hierarchy_call_json(uri, Direction::Incoming, 0)])
1373 })
1374 .await;
1375
1376 assert!(result.get("truncated").is_none());
1377 }
1378
1379 #[tokio::test]
1382 async fn test_handle_call_hierarchy_prepare_caps_items_and_reports_truncation() {
1383 use crate::bridge::translator::navigation::MAX_NORMALIZED_LOCATIONS;
1384
1385 for (count, truncated) in [
1386 (MAX_NORMALIZED_LOCATIONS + 10, true),
1387 (MAX_NORMALIZED_LOCATIONS, false),
1388 ] {
1389 let dir = TempDir::new().unwrap();
1390 let server_id = ServerId::from_static("rust");
1391 let caps = lsp_types::ServerCapabilities {
1392 call_hierarchy_provider: Some(lsp_types::CallHierarchyProvider::Bool(true)),
1393 ..Default::default()
1394 };
1395 let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
1396 let path = dir.path().join("queried.rs");
1397 fs::write(&path, "fn queried() {}").unwrap();
1398 let uri = Url::from_file_path(&path).unwrap().to_string();
1399
1400 let translator = Arc::new(translator);
1401 let handle = {
1402 let translator = Arc::clone(&translator);
1403 let path = path.to_string_lossy().into_owned();
1404 tokio::spawn(async move {
1405 translator
1406 .handle_call_hierarchy_prepare(client_path(path), pos(1, 1))
1407 .await
1408 })
1409 };
1410
1411 let mut wire = BufReader::new(&mut server.write_stdout);
1412 let opened = read_framed_message(&mut wire).await;
1413 assert_eq!(opened["method"], "textDocument/didOpen");
1414 let request = read_framed_message(&mut wire).await;
1415 assert_eq!(request["method"], "textDocument/prepareCallHierarchy");
1416
1417 let range = serde_json::json!({
1418 "start": {"line": 0, "character": 0},
1419 "end": {"line": 0, "character": 1}
1420 });
1421 let item = serde_json::json!({
1422 "name": "f", "kind": 12, "uri": uri,
1423 "range": range, "selectionRange": range
1424 });
1425 write_response(
1426 &mut server.read_half_stdin,
1427 &request["id"],
1428 serde_json::Value::Array(vec![item; count]),
1429 )
1430 .await;
1431
1432 let result = timeout(Duration::from_secs(30), handle)
1433 .await
1434 .expect("handler call should not hang")
1435 .unwrap()
1436 .unwrap()
1437 .result;
1438 assert_eq!(result.items.len(), MAX_NORMALIZED_LOCATIONS);
1439 assert_eq!(result.truncated, truncated);
1440 let wire = serde_json::to_value(&result).unwrap();
1441 assert_eq!(wire.get("truncated").is_some(), truncated);
1442 }
1443 }
1444}