1use lsp_types::{
4 CallHierarchyIncomingCallsParams, CallHierarchyItem, CallHierarchyOutgoingCallsParams,
5 CallHierarchyPrepareParams as LspCallHierarchyPrepareParams, PartialResultParams,
6 TextDocumentIdentifier, TextDocumentPositionParams, WorkDoneProgressParams,
7};
8
9use super::Translator;
10use super::dto::{
11 CallHierarchyItemResult, CallHierarchyPrepareResult, IncomingCall, IncomingCallsResult,
12 OutgoingCall, OutgoingCallsResult, Position, lsp_kind_to_u32,
13};
14use super::encoding_ctx::EncodingCtx;
15use super::routing::{Capability, IndexingGate, MAX_POSITION_VALUE};
16use crate::config::ToolKind;
17use crate::error::{Error, Result};
18
19struct ParsedCallHierarchyItem {
25 uri: lsp_types::Uri,
26 mcp: CallHierarchyItemResult,
27}
28
29fn parse_mcp_call_hierarchy_item(item: serde_json::Value) -> Result<ParsedCallHierarchyItem> {
35 let mcp: CallHierarchyItemResult = serde_json::from_value(item)
36 .map_err(|e| Error::InvalidToolParams(format!("Invalid call hierarchy item: {e}")))?;
37
38 let uri = lsp_types::Uri::from(mcp.uri.as_str());
44
45 Ok(ParsedCallHierarchyItem { uri, mcp })
46}
47
48async fn call_hierarchy_item_to_lsp(
51 parsed: ParsedCallHierarchyItem,
52 ctx: &EncodingCtx,
53) -> CallHierarchyItem {
54 let ParsedCallHierarchyItem { uri, mcp } = parsed;
55
56 let kind = lsp_types::SymbolKind::from(mcp.kind);
60 let range = ctx.denormalize_range(&uri, &mcp.range).await;
61 let selection_range = ctx.denormalize_range(&uri, &mcp.selection_range).await;
62
63 CallHierarchyItem {
64 name: mcp.name,
65 kind,
66 tags: None,
67 detail: mcp.detail,
68 uri,
69 range,
70 selection_range,
71 data: mcp.data,
72 }
73}
74
75async fn convert_call_hierarchy_item(
77 item: CallHierarchyItem,
78 ctx: &EncodingCtx,
79) -> CallHierarchyItemResult {
80 let out_of_workspace = ctx.is_out_of_workspace(&item.uri);
81 let range = ctx.normalize_range(&item.uri, item.range).await;
82 let selection_range = ctx.normalize_range(&item.uri, item.selection_range).await;
83
84 CallHierarchyItemResult {
85 name: item.name,
86 kind: lsp_kind_to_u32(item.kind),
87 detail: item.detail,
88 uri: item.uri.to_string(),
89 range,
90 selection_range,
91 data: item.data,
92 out_of_workspace,
93 }
94}
95
96impl Translator {
97 pub async fn handle_call_hierarchy_prepare(
104 &self,
105 file_path: String,
106 position: Position,
107 ) -> Result<CallHierarchyPrepareResult> {
108 let Position { line, character } = position;
109 if line < 1 || character < 1 {
111 return Err(Error::InvalidToolParams(
112 "Line and character positions must be >= 1".to_string(),
113 ));
114 }
115
116 if line > MAX_POSITION_VALUE || character > MAX_POSITION_VALUE {
117 return Err(Error::InvalidToolParams(format!(
118 "Position values must be <= {MAX_POSITION_VALUE}"
119 )));
120 }
121
122 let (server_id, client, uri) = self
123 .prepare_gated_document(
124 &file_path,
125 ToolKind::CallHierarchy,
126 Capability::CallHierarchy,
127 IndexingGate::NotRequired,
128 )
129 .await?;
130 let ctx = self.encoding_ctx(&server_id);
131 let lsp_position = ctx.to_lsp(&uri, line, character).await;
132
133 let params = LspCallHierarchyPrepareParams {
134 text_document_position_params: TextDocumentPositionParams {
135 text_document: TextDocumentIdentifier { uri },
136 position: lsp_position,
137 },
138 work_done_progress_params: WorkDoneProgressParams::default(),
139 };
140
141 let response = client
142 .request_typed::<lsp_types::CallHierarchyPrepareRequest>(
143 params,
144 client.request_timeout(),
145 )
146 .await?;
147
148 let lsp_items = response.unwrap_or_default();
153 let mut items = Vec::with_capacity(lsp_items.len());
154 for item in lsp_items {
155 items.push(convert_call_hierarchy_item(item, &ctx).await);
156 }
157
158 Ok(CallHierarchyPrepareResult {
159 items,
160 positions_degraded: ctx.positions_degraded(),
161 })
162 }
163
164 pub async fn handle_incoming_calls(
181 &self,
182 item: serde_json::Value,
183 ) -> Result<IncomingCallsResult> {
184 let parsed = parse_mcp_call_hierarchy_item(item)?;
186
187 let path = self.parse_file_uri(&parsed.uri)?;
189 let (server_id, client, _uri) = self
190 .prepare_gated_document_for_path(
191 &path,
192 ToolKind::CallHierarchy,
193 Capability::CallHierarchy,
194 IndexingGate::Required,
195 )
196 .await?;
197 let ctx = self.encoding_ctx(&server_id);
198 let lsp_item = call_hierarchy_item_to_lsp(parsed, &ctx).await;
199
200 let params = CallHierarchyIncomingCallsParams {
201 item: lsp_item,
202 work_done_progress_params: WorkDoneProgressParams::default(),
203 partial_result_params: PartialResultParams::default(),
204 };
205
206 let response = client
207 .request_typed::<lsp_types::CallHierarchyIncomingCallsRequest>(
208 params,
209 client.request_timeout(),
210 )
211 .await?;
212
213 let lsp_calls = response.unwrap_or_default();
216 let mut calls = Vec::with_capacity(lsp_calls.len());
217
218 for call in lsp_calls {
219 let from_uri = call.from.uri.clone();
222 let from_ranges = {
223 let mut ranges = Vec::with_capacity(call.from_ranges.len());
224 for range in call.from_ranges {
225 ranges.push(ctx.normalize_range(&from_uri, range).await);
226 }
227 ranges
228 };
229
230 calls.push(IncomingCall {
231 from: convert_call_hierarchy_item(call.from, &ctx).await,
232 from_ranges,
233 });
234 }
235
236 Ok(IncomingCallsResult {
237 calls,
238 positions_degraded: ctx.positions_degraded(),
239 })
240 }
241
242 pub async fn handle_outgoing_calls(
254 &self,
255 item: serde_json::Value,
256 ) -> Result<OutgoingCallsResult> {
257 let parsed = parse_mcp_call_hierarchy_item(item)?;
259
260 let path = self.parse_file_uri(&parsed.uri)?;
264 let (server_id, client, _uri) = self
265 .prepare_gated_document_for_path(
266 &path,
267 ToolKind::CallHierarchy,
268 Capability::CallHierarchy,
269 IndexingGate::Required,
270 )
271 .await?;
272 let ctx = self.encoding_ctx(&server_id);
273 let source_uri = parsed.uri.clone();
276 let lsp_item = call_hierarchy_item_to_lsp(parsed, &ctx).await;
277
278 let params = CallHierarchyOutgoingCallsParams {
279 item: lsp_item,
280 work_done_progress_params: WorkDoneProgressParams::default(),
281 partial_result_params: PartialResultParams::default(),
282 };
283
284 let response = client
285 .request_typed::<lsp_types::CallHierarchyOutgoingCallsRequest>(
286 params,
287 client.request_timeout(),
288 )
289 .await?;
290
291 let lsp_calls = response.unwrap_or_default();
294 let mut calls = Vec::with_capacity(lsp_calls.len());
295
296 for call in lsp_calls {
297 let from_ranges = {
298 let mut ranges = Vec::with_capacity(call.from_ranges.len());
299 for range in call.from_ranges {
300 ranges.push(ctx.normalize_range(&source_uri, range).await);
301 }
302 ranges
303 };
304
305 calls.push(OutgoingCall {
306 to: convert_call_hierarchy_item(call.to, &ctx).await,
307 from_ranges,
308 });
309 }
310
311 Ok(OutgoingCallsResult {
312 calls,
313 positions_degraded: ctx.positions_degraded(),
314 })
315 }
316}
317
318#[cfg(test)]
319#[allow(clippy::unwrap_used, clippy::expect_used)]
320mod tests {
321 use std::fs;
322 use std::sync::Arc;
323 use std::time::Duration;
324
325 use tempfile::TempDir;
326 use tokio::io::BufReader;
327 use tokio::sync::Mutex;
328 use tokio::time::timeout;
329 use url::Url;
330
331 use super::*;
332 use crate::bridge::NotificationCache;
333 use crate::bridge::translator::dto::{Position, Position2D, Range};
334 use crate::bridge::translator::testing::*;
335 use crate::config::ServerId;
336
337 #[tokio::test]
338 async fn test_handle_call_hierarchy_prepare_invalid_position_zero() {
339 let translator = Translator::new();
340 let result = translator
341 .handle_call_hierarchy_prepare(
342 "/tmp/test.rs".to_string(),
343 Position {
344 line: 0,
345 character: 1,
346 },
347 )
348 .await;
349 assert!(matches!(result, Err(Error::InvalidToolParams(_))));
350
351 let result = translator
352 .handle_call_hierarchy_prepare(
353 "/tmp/test.rs".to_string(),
354 Position {
355 line: 1,
356 character: 0,
357 },
358 )
359 .await;
360 assert!(matches!(result, Err(Error::InvalidToolParams(_))));
361 }
362
363 #[tokio::test]
364 async fn test_handle_call_hierarchy_prepare_invalid_position_too_large() {
365 let translator = Translator::new();
366 let result = translator
367 .handle_call_hierarchy_prepare(
368 "/tmp/test.rs".to_string(),
369 Position {
370 line: 1_000_001,
371 character: 1,
372 },
373 )
374 .await;
375 assert!(matches!(result, Err(Error::InvalidToolParams(_))));
376
377 let result = translator
378 .handle_call_hierarchy_prepare(
379 "/tmp/test.rs".to_string(),
380 Position {
381 line: 1,
382 character: 1_000_001,
383 },
384 )
385 .await;
386 assert!(matches!(result, Err(Error::InvalidToolParams(_))));
387 }
388
389 #[tokio::test]
390 async fn test_handle_incoming_calls_invalid_json() {
391 let translator = Translator::new();
392 let invalid_item = serde_json::json!({"invalid": "structure"});
393 let result = translator.handle_incoming_calls(invalid_item).await;
394 assert!(matches!(result, Err(Error::InvalidToolParams(_))));
395 }
396
397 #[tokio::test]
398 async fn test_handle_outgoing_calls_invalid_json() {
399 let translator = Translator::new();
400 let invalid_item = serde_json::json!({"invalid": "structure"});
401 let result = translator.handle_outgoing_calls(invalid_item).await;
402 assert!(matches!(result, Err(Error::InvalidToolParams(_))));
403 }
404
405 fn call_hierarchy_item_json(uri: &str) -> serde_json::Value {
409 serde_json::to_value(CallHierarchyItemResult {
410 name: "queried_fn".to_string(),
411 kind: 12,
412 detail: None,
413 uri: uri.to_string(),
414 range: Range {
415 start: Position2D {
416 line: 1,
417 character: 1,
418 },
419 end: Position2D {
420 line: 1,
421 character: 4,
422 },
423 },
424 selection_range: Range {
425 start: Position2D {
426 line: 1,
427 character: 1,
428 },
429 end: Position2D {
430 line: 1,
431 character: 4,
432 },
433 },
434 data: None,
435 out_of_workspace: false,
436 })
437 .unwrap()
438 }
439
440 #[tokio::test(start_paused = true)]
445 async fn test_handle_incoming_calls_returns_workspace_indexing_error_when_loading() {
446 let dir = TempDir::new().unwrap();
447 let server_id = ServerId::from("rust");
448 let caps = lsp_types::ServerCapabilities {
449 call_hierarchy_provider: Some(lsp_types::CallHierarchyProvider::Bool(true)),
450 ..Default::default()
451 };
452 let (translator, _server) = translator_with_capabilities(&dir, &server_id, caps);
453
454 let cache = Arc::new(Mutex::new(NotificationCache::new()));
455 cache.lock().await.observe_indexing_signal(
456 &server_id,
457 "experimental/serverStatus",
458 Some(&serde_json::json!({"quiescent": false})),
459 );
460 let translator = translator.with_notification_cache(cache);
461
462 let path = dir.path().join("queried.rs");
463 fs::write(&path, "fn queried() {}").unwrap();
464 let uri = Url::from_file_path(&path).unwrap().to_string();
465
466 let err = translator
467 .handle_incoming_calls(call_hierarchy_item_json(&uri))
468 .await
469 .unwrap_err();
470
471 assert!(matches!(
472 err,
473 Error::WorkspaceIndexing { server_id: id, .. } if id == server_id
474 ));
475 }
476
477 #[tokio::test(start_paused = true)]
480 async fn test_handle_outgoing_calls_returns_workspace_indexing_error_when_loading() {
481 let dir = TempDir::new().unwrap();
482 let server_id = ServerId::from("rust");
483 let caps = lsp_types::ServerCapabilities {
484 call_hierarchy_provider: Some(lsp_types::CallHierarchyProvider::Bool(true)),
485 ..Default::default()
486 };
487 let (translator, _server) = translator_with_capabilities(&dir, &server_id, caps);
488
489 let cache = Arc::new(Mutex::new(NotificationCache::new()));
490 cache.lock().await.observe_indexing_signal(
491 &server_id,
492 "experimental/serverStatus",
493 Some(&serde_json::json!({"quiescent": false})),
494 );
495 let translator = translator.with_notification_cache(cache);
496
497 let path = dir.path().join("queried.rs");
498 fs::write(&path, "fn queried() {}").unwrap();
499 let uri = Url::from_file_path(&path).unwrap().to_string();
500
501 let err = translator
502 .handle_outgoing_calls(call_hierarchy_item_json(&uri))
503 .await
504 .unwrap_err();
505
506 assert!(matches!(
507 err,
508 Error::WorkspaceIndexing { server_id: id, .. } if id == server_id
509 ));
510 }
511
512 #[tokio::test]
524 async fn test_handle_incoming_calls_with_nonexistent_file_uri_returns_file_io_not_invalid_uri()
525 {
526 let mut translator = Translator::new();
527 #[cfg(windows)]
530 translator.set_workspace_roots(vec![std::path::PathBuf::from(r"C:\")]);
531 #[cfg(not(windows))]
532 translator.set_workspace_roots(vec![std::path::PathBuf::from("/")]);
533 #[cfg(windows)]
540 let uri = "file:///C:/this/path/does/not/exist/anywhere.rs";
541 #[cfg(not(windows))]
542 let uri = "file:///this/path/does/not/exist/anywhere.rs";
543 let item = serde_json::json!({
544 "name": "foo",
545 "kind": 12,
546 "uri": uri,
547 "range": {
548 "start": {"line": 1, "character": 1},
549 "end": {"line": 1, "character": 1}
550 },
551 "selectionRange": {
552 "start": {"line": 1, "character": 1},
553 "end": {"line": 1, "character": 1}
554 }
555 });
556
557 let result = translator.handle_incoming_calls(item).await;
558
559 assert!(
560 matches!(result, Err(Error::FileIo { .. })),
561 "expected Error::FileIo from the canonicalize() failure now that Uri construction \
562 cannot itself reject a malformed uri, got {result:?}"
563 );
564 }
565
566 #[tokio::test]
569 async fn test_handle_outgoing_calls_with_nonexistent_file_uri_returns_file_io_not_invalid_uri()
570 {
571 let mut translator = Translator::new();
572 #[cfg(windows)]
575 translator.set_workspace_roots(vec![std::path::PathBuf::from(r"C:\")]);
576 #[cfg(not(windows))]
577 translator.set_workspace_roots(vec![std::path::PathBuf::from("/")]);
578 #[cfg(windows)]
579 let uri = "file:///C:/this/path/does/not/exist/anywhere.rs";
580 #[cfg(not(windows))]
581 let uri = "file:///this/path/does/not/exist/anywhere.rs";
582 let item = serde_json::json!({
583 "name": "foo",
584 "kind": 12,
585 "uri": uri,
586 "range": {
587 "start": {"line": 1, "character": 1},
588 "end": {"line": 1, "character": 1}
589 },
590 "selectionRange": {
591 "start": {"line": 1, "character": 1},
592 "end": {"line": 1, "character": 1}
593 }
594 });
595
596 let result = translator.handle_outgoing_calls(item).await;
597
598 assert!(
599 matches!(result, Err(Error::FileIo { .. })),
600 "expected Error::FileIo from the canonicalize() failure now that Uri construction \
601 cannot itself reject a malformed uri, got {result:?}"
602 );
603 }
604
605 #[tokio::test]
606 async fn test_convert_call_hierarchy_item_kind_is_numeric() {
607 let item = lsp_types::CallHierarchyItem {
608 name: "my_fn".to_string(),
609 kind: lsp_types::SymbolKind::Function,
610 tags: None,
611 detail: None,
612 uri: lsp_types::Uri::from("file:///tmp/test.rs"),
613 range: lsp_types::Range {
614 start: lsp_types::Position {
615 line: 0,
616 character: 0,
617 },
618 end: lsp_types::Position {
619 line: 0,
620 character: 5,
621 },
622 },
623 selection_range: lsp_types::Range {
624 start: lsp_types::Position {
625 line: 0,
626 character: 0,
627 },
628 end: lsp_types::Position {
629 line: 0,
630 character: 5,
631 },
632 },
633 data: None,
634 };
635 let result = convert_call_hierarchy_item(item, &test_ctx()).await;
636 assert_eq!(result.kind, 12u32);
638 assert_eq!(result.name, "my_fn");
639 }
640
641 #[tokio::test]
647 async fn test_handle_incoming_calls_round_trips_non_function_kind() {
648 let dir = TempDir::new().unwrap();
649 let server_id = ServerId::from("rust");
650 let caps = lsp_types::ServerCapabilities {
651 call_hierarchy_provider: Some(lsp_types::CallHierarchyProvider::Bool(true)),
652 ..Default::default()
653 };
654 let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
655
656 let queried_path = dir.path().join("queried.rs");
657 fs::write(&queried_path, "fn queried() {}").unwrap();
658 let queried_uri = Url::from_file_path(&queried_path).unwrap().to_string();
659
660 let item = CallHierarchyItemResult {
661 name: "queried_method".to_string(),
662 kind: 6, 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 = 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 assert_eq!(
703 request["params"]["item"]["kind"], 6,
704 "the reverse u32 -> SymbolKind conversion must round-trip a non-Function kind \
705 exactly, not fall back to Function (12)"
706 );
707
708 write_response(
709 &mut server.read_half_stdin,
710 &request["id"],
711 serde_json::json!([]),
712 )
713 .await;
714
715 let result = timeout(Duration::from_secs(2), handle)
716 .await
717 .expect("handler call should not hang")
718 .unwrap()
719 .unwrap();
720 assert!(result.calls.is_empty());
721 }
722
723 #[tokio::test]
733 async fn test_handle_incoming_calls_from_ranges_convert_against_callers_own_uri() {
734 let dir = TempDir::new().unwrap();
735 let server_id = ServerId::from("rust");
736 let caps = lsp_types::ServerCapabilities {
737 call_hierarchy_provider: Some(lsp_types::CallHierarchyProvider::Bool(true)),
738 ..Default::default()
739 };
740 let (translator, mut server) = translator_with_capabilities_and_encoding(
741 &dir,
742 &server_id,
743 caps,
744 lsp_types::PositionEncodingKind::UTF8,
745 );
746
747 let queried_path = dir.path().join("queried.rs");
748 fs::write(&queried_path, "abc").unwrap();
749 let queried_uri = Url::from_file_path(&queried_path).unwrap().to_string();
750
751 let caller_path = dir.path().join("caller.rs");
752 fs::write(&caller_path, "aöb").unwrap();
753 let caller_uri = Url::from_file_path(&caller_path).unwrap().to_string();
754
755 let item = CallHierarchyItemResult {
756 name: "queried_fn".to_string(),
757 kind: 12,
758 detail: None,
759 uri: queried_uri,
760 range: Range {
761 start: Position2D {
762 line: 1,
763 character: 1,
764 },
765 end: Position2D {
766 line: 1,
767 character: 4,
768 },
769 },
770 selection_range: Range {
771 start: Position2D {
772 line: 1,
773 character: 1,
774 },
775 end: Position2D {
776 line: 1,
777 character: 4,
778 },
779 },
780 data: None,
781 out_of_workspace: false,
782 };
783
784 let translator = Arc::new(translator);
785 let handle = {
786 let translator = Arc::clone(&translator);
787 let item = serde_json::to_value(item).unwrap();
788 tokio::spawn(async move { translator.handle_incoming_calls(item).await })
789 };
790
791 let mut wire = BufReader::new(&mut server.write_stdout);
792 let opened = read_framed_message(&mut wire).await;
793 assert_eq!(opened["method"], "textDocument/didOpen");
794
795 let request = read_framed_message(&mut wire).await;
796 assert_eq!(request["method"], "callHierarchy/incomingCalls");
797
798 write_response(
799 &mut server.read_half_stdin,
800 &request["id"],
801 serde_json::json!([{
802 "from": {
803 "name": "caller_fn",
804 "kind": 12,
805 "uri": caller_uri,
806 "range": {
807 "start": {"line": 0, "character": 0},
808 "end": {"line": 0, "character": 1}
809 },
810 "selectionRange": {
811 "start": {"line": 0, "character": 0},
812 "end": {"line": 0, "character": 1}
813 }
814 },
815 "fromRanges": [{
816 "start": {"line": 0, "character": 0},
817 "end": {"line": 0, "character": 3}
818 }]
819 }]),
820 )
821 .await;
822
823 let result = timeout(Duration::from_secs(2), handle)
824 .await
825 .expect("handler call should not hang")
826 .unwrap()
827 .unwrap();
828
829 assert_eq!(result.calls.len(), 1);
830 let from_range = &result.calls[0].from_ranges[0];
831 assert_eq!(
832 from_range.end.character, 3,
833 "fromRanges must convert against the caller's own file (\"aöb\"), not the queried \
834 item's (\"abc\") -- a byte offset of 3 is UTF-16 column 3 in the former, 4 in the \
835 latter"
836 );
837 assert!(
838 !result.calls[0].from.out_of_workspace,
839 "an in-workspace caller must not be marked out_of_workspace"
840 );
841 }
842
843 #[tokio::test]
847 async fn test_handle_outgoing_calls_from_ranges_convert_against_queried_uri() {
848 let dir = TempDir::new().unwrap();
849 let server_id = ServerId::from("rust");
850 let caps = lsp_types::ServerCapabilities {
851 call_hierarchy_provider: Some(lsp_types::CallHierarchyProvider::Bool(true)),
852 ..Default::default()
853 };
854 let (translator, mut server) = translator_with_capabilities_and_encoding(
855 &dir,
856 &server_id,
857 caps,
858 lsp_types::PositionEncodingKind::UTF8,
859 );
860
861 let queried_path = dir.path().join("queried.rs");
862 fs::write(&queried_path, "aöb").unwrap();
863 let queried_uri = Url::from_file_path(&queried_path).unwrap().to_string();
864
865 let callee_path = dir.path().join("callee.rs");
866 fs::write(&callee_path, "abc").unwrap();
867 let callee_uri = Url::from_file_path(&callee_path).unwrap().to_string();
868
869 let item = CallHierarchyItemResult {
870 name: "queried_fn".to_string(),
871 kind: 12,
872 detail: None,
873 uri: queried_uri,
874 range: Range {
875 start: Position2D {
876 line: 1,
877 character: 1,
878 },
879 end: Position2D {
880 line: 1,
881 character: 4,
882 },
883 },
884 selection_range: Range {
885 start: Position2D {
886 line: 1,
887 character: 1,
888 },
889 end: Position2D {
890 line: 1,
891 character: 4,
892 },
893 },
894 data: None,
895 out_of_workspace: false,
896 };
897
898 let translator = Arc::new(translator);
899 let handle = {
900 let translator = Arc::clone(&translator);
901 let item = serde_json::to_value(item).unwrap();
902 tokio::spawn(async move { translator.handle_outgoing_calls(item).await })
903 };
904
905 let mut wire = BufReader::new(&mut server.write_stdout);
906 let opened = read_framed_message(&mut wire).await;
907 assert_eq!(opened["method"], "textDocument/didOpen");
908
909 let request = read_framed_message(&mut wire).await;
910 assert_eq!(request["method"], "callHierarchy/outgoingCalls");
911
912 write_response(
913 &mut server.read_half_stdin,
914 &request["id"],
915 serde_json::json!([{
916 "to": {
917 "name": "callee_fn",
918 "kind": 12,
919 "uri": callee_uri,
920 "range": {
921 "start": {"line": 0, "character": 0},
922 "end": {"line": 0, "character": 1}
923 },
924 "selectionRange": {
925 "start": {"line": 0, "character": 0},
926 "end": {"line": 0, "character": 1}
927 }
928 },
929 "fromRanges": [{
930 "start": {"line": 0, "character": 0},
931 "end": {"line": 0, "character": 3}
932 }]
933 }]),
934 )
935 .await;
936
937 let result = timeout(Duration::from_secs(2), handle)
938 .await
939 .expect("handler call should not hang")
940 .unwrap()
941 .unwrap();
942
943 assert_eq!(result.calls.len(), 1);
944 let from_range = &result.calls[0].from_ranges[0];
945 assert_eq!(
946 from_range.end.character, 3,
947 "fromRanges must convert against the queried item's own file (\"aöb\"), not the \
948 callee's (\"abc\") -- a byte offset of 3 is UTF-16 column 3 in the former, 4 in \
949 the latter"
950 );
951 }
952
953 #[tokio::test]
961 async fn test_handle_incoming_calls_does_not_filter_out_of_workspace_caller() {
962 let dir = TempDir::new().unwrap();
963 let server_id = ServerId::from("rust");
964 let caps = lsp_types::ServerCapabilities {
965 call_hierarchy_provider: Some(lsp_types::CallHierarchyProvider::Bool(true)),
966 ..Default::default()
967 };
968 let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
969
970 let queried_path = dir.path().join("queried.rs");
971 fs::write(&queried_path, "fn queried() {}").unwrap();
972 let queried_uri = Url::from_file_path(&queried_path).unwrap().to_string();
973 let outside_uri = "file:///outside/workspace/stdlib.rs";
974
975 let item = CallHierarchyItemResult {
976 name: "queried_fn".to_string(),
977 kind: 12,
978 detail: None,
979 uri: queried_uri,
980 range: Range {
981 start: Position2D {
982 line: 1,
983 character: 1,
984 },
985 end: Position2D {
986 line: 1,
987 character: 4,
988 },
989 },
990 selection_range: Range {
991 start: Position2D {
992 line: 1,
993 character: 1,
994 },
995 end: Position2D {
996 line: 1,
997 character: 4,
998 },
999 },
1000 data: None,
1001 out_of_workspace: false,
1002 };
1003
1004 let translator = Arc::new(translator);
1005 let handle = {
1006 let translator = Arc::clone(&translator);
1007 let item = serde_json::to_value(item).unwrap();
1008 tokio::spawn(async move { translator.handle_incoming_calls(item).await })
1009 };
1010
1011 let mut wire = BufReader::new(&mut server.write_stdout);
1012 let opened = read_framed_message(&mut wire).await;
1013 assert_eq!(opened["method"], "textDocument/didOpen");
1014
1015 let request = read_framed_message(&mut wire).await;
1016 assert_eq!(request["method"], "callHierarchy/incomingCalls");
1017
1018 write_response(
1019 &mut server.read_half_stdin,
1020 &request["id"],
1021 serde_json::json!([{
1022 "from": {
1023 "name": "caller_fn",
1024 "kind": 12,
1025 "uri": outside_uri,
1026 "range": {
1027 "start": {"line": 0, "character": 0},
1028 "end": {"line": 0, "character": 1}
1029 },
1030 "selectionRange": {
1031 "start": {"line": 0, "character": 0},
1032 "end": {"line": 0, "character": 1}
1033 }
1034 },
1035 "fromRanges": [{
1036 "start": {"line": 0, "character": 0},
1037 "end": {"line": 0, "character": 1}
1038 }]
1039 }]),
1040 )
1041 .await;
1042
1043 let result = timeout(Duration::from_secs(2), handle)
1044 .await
1045 .expect("handler call should not hang")
1046 .unwrap()
1047 .unwrap();
1048
1049 assert_eq!(
1050 result.calls.len(),
1051 1,
1052 "an out-of-workspace caller must be returned, not dropped"
1053 );
1054 assert_eq!(result.calls[0].from.uri, outside_uri);
1055 assert!(
1056 result.calls[0].from.out_of_workspace,
1057 "an out-of-workspace caller must be marked out_of_workspace"
1058 );
1059 }
1060
1061 #[tokio::test]
1065 async fn test_handle_outgoing_calls_does_not_filter_out_of_workspace_callee() {
1066 let dir = TempDir::new().unwrap();
1067 let server_id = ServerId::from("rust");
1068 let caps = lsp_types::ServerCapabilities {
1069 call_hierarchy_provider: Some(lsp_types::CallHierarchyProvider::Bool(true)),
1070 ..Default::default()
1071 };
1072 let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
1073
1074 let queried_path = dir.path().join("queried.rs");
1075 fs::write(&queried_path, "fn queried() {}").unwrap();
1076 let queried_uri = Url::from_file_path(&queried_path).unwrap().to_string();
1077 let outside_uri = "file:///outside/workspace/stdlib.rs";
1078
1079 let item = CallHierarchyItemResult {
1080 name: "queried_fn".to_string(),
1081 kind: 12,
1082 detail: None,
1083 uri: queried_uri,
1084 range: Range {
1085 start: Position2D {
1086 line: 1,
1087 character: 1,
1088 },
1089 end: Position2D {
1090 line: 1,
1091 character: 4,
1092 },
1093 },
1094 selection_range: Range {
1095 start: Position2D {
1096 line: 1,
1097 character: 1,
1098 },
1099 end: Position2D {
1100 line: 1,
1101 character: 4,
1102 },
1103 },
1104 data: None,
1105 out_of_workspace: false,
1106 };
1107
1108 let translator = Arc::new(translator);
1109 let handle = {
1110 let translator = Arc::clone(&translator);
1111 let item = serde_json::to_value(item).unwrap();
1112 tokio::spawn(async move { translator.handle_outgoing_calls(item).await })
1113 };
1114
1115 let mut wire = BufReader::new(&mut server.write_stdout);
1116 let opened = read_framed_message(&mut wire).await;
1117 assert_eq!(opened["method"], "textDocument/didOpen");
1118
1119 let request = read_framed_message(&mut wire).await;
1120 assert_eq!(request["method"], "callHierarchy/outgoingCalls");
1121
1122 write_response(
1123 &mut server.read_half_stdin,
1124 &request["id"],
1125 serde_json::json!([{
1126 "to": {
1127 "name": "callee_fn",
1128 "kind": 12,
1129 "uri": outside_uri,
1130 "range": {
1131 "start": {"line": 0, "character": 0},
1132 "end": {"line": 0, "character": 1}
1133 },
1134 "selectionRange": {
1135 "start": {"line": 0, "character": 0},
1136 "end": {"line": 0, "character": 1}
1137 }
1138 },
1139 "fromRanges": [{
1140 "start": {"line": 0, "character": 0},
1141 "end": {"line": 0, "character": 1}
1142 }]
1143 }]),
1144 )
1145 .await;
1146
1147 let result = timeout(Duration::from_secs(2), handle)
1148 .await
1149 .expect("handler call should not hang")
1150 .unwrap()
1151 .unwrap();
1152
1153 assert_eq!(
1154 result.calls.len(),
1155 1,
1156 "an out-of-workspace callee must be returned, not dropped"
1157 );
1158 assert_eq!(result.calls[0].to.uri, outside_uri);
1159 assert!(
1160 result.calls[0].to.out_of_workspace,
1161 "an out-of-workspace callee must be marked out_of_workspace"
1162 );
1163 }
1164
1165 #[tokio::test]
1171 async fn test_prepare_then_incoming_calls_round_trip_percent_encoded_path() {
1172 let dir = TempDir::new().unwrap();
1173 let server_id = ServerId::from("rust");
1174 let caps = lsp_types::ServerCapabilities {
1175 call_hierarchy_provider: Some(lsp_types::CallHierarchyProvider::Bool(true)),
1176 ..Default::default()
1177 };
1178 let (translator, mut server) = translator_with_capabilities_and_encoding(
1179 &dir,
1180 &server_id,
1181 caps,
1182 lsp_types::PositionEncodingKind::UTF8,
1183 );
1184
1185 let file_path = dir.path().join("my file café.rs");
1186 fs::write(&file_path, "fn foo() {}").unwrap();
1187 let file_uri = Url::from_file_path(&file_path).unwrap().to_string();
1188 assert!(
1189 file_uri.contains("%20"),
1190 "test fixture must exercise percent-encoding"
1191 );
1192
1193 let translator = Arc::new(translator);
1194 let mut wire = BufReader::new(&mut server.write_stdout);
1195
1196 let prepare_handle = {
1197 let translator = Arc::clone(&translator);
1198 let path = file_path.to_string_lossy().into_owned();
1199 tokio::spawn(async move {
1200 translator
1201 .handle_call_hierarchy_prepare(path, pos(1, 1))
1202 .await
1203 })
1204 };
1205
1206 let opened = read_framed_message(&mut wire).await;
1207 assert_eq!(opened["method"], "textDocument/didOpen");
1208 let request = read_framed_message(&mut wire).await;
1209 assert_eq!(request["method"], "textDocument/prepareCallHierarchy");
1210
1211 write_response(
1212 &mut server.read_half_stdin,
1213 &request["id"],
1214 serde_json::json!([{
1215 "name": "foo",
1216 "kind": 12,
1217 "uri": file_uri,
1218 "range": {
1219 "start": {"line": 0, "character": 0},
1220 "end": {"line": 0, "character": 11}
1221 },
1222 "selectionRange": {
1223 "start": {"line": 0, "character": 3},
1224 "end": {"line": 0, "character": 6}
1225 }
1226 }]),
1227 )
1228 .await;
1229
1230 let prepare_result = timeout(Duration::from_secs(2), prepare_handle)
1231 .await
1232 .expect("prepare should not hang")
1233 .unwrap()
1234 .unwrap();
1235 assert_eq!(prepare_result.items.len(), 1);
1236 let item = prepare_result.items[0].clone();
1237 assert!(item.uri.contains("%20"));
1238
1239 let incoming_handle = {
1240 let translator = Arc::clone(&translator);
1241 let item = serde_json::to_value(item).unwrap();
1242 tokio::spawn(async move { translator.handle_incoming_calls(item).await })
1243 };
1244
1245 let request = read_framed_message(&mut wire).await;
1246 assert_eq!(request["method"], "callHierarchy/incomingCalls");
1247
1248 write_response(
1249 &mut server.read_half_stdin,
1250 &request["id"],
1251 serde_json::json!([]),
1252 )
1253 .await;
1254
1255 let incoming_result = timeout(Duration::from_secs(2), incoming_handle)
1256 .await
1257 .expect("incoming calls should not hang")
1258 .unwrap();
1259
1260 assert!(
1261 incoming_result.is_ok(),
1262 "expected success resolving the percent-encoded path, got {incoming_result:?}"
1263 );
1264 }
1265}