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