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,
13};
14use super::encoding_ctx::EncodingCtx;
15use super::routing::MAX_POSITION_VALUE;
16use crate::config::ToolKind;
17use crate::error::{Error, Result};
18
19const fn call_hierarchy_provider_supported(caps: &lsp_types::ServerCapabilities) -> bool {
24 matches!(
25 caps.call_hierarchy_provider,
26 Some(
27 lsp_types::CallHierarchyProvider::Bool(true)
28 | lsp_types::CallHierarchyProvider::CallHierarchyOptions(_)
29 | lsp_types::CallHierarchyProvider::CallHierarchyRegistrationOptions(_)
30 )
31 )
32}
33
34struct ParsedCallHierarchyItem {
40 uri: lsp_types::Uri,
41 mcp: CallHierarchyItemResult,
42}
43
44fn parse_mcp_call_hierarchy_item(item: serde_json::Value) -> Result<ParsedCallHierarchyItem> {
50 let mcp: CallHierarchyItemResult = serde_json::from_value(item)
51 .map_err(|e| Error::InvalidToolParams(format!("Invalid call hierarchy item: {e}")))?;
52
53 let uri = lsp_types::Uri::from(mcp.uri.as_str());
59
60 Ok(ParsedCallHierarchyItem { uri, mcp })
61}
62
63async fn call_hierarchy_item_to_lsp(
66 parsed: ParsedCallHierarchyItem,
67 ctx: &EncodingCtx,
68) -> CallHierarchyItem {
69 let ParsedCallHierarchyItem { uri, mcp } = parsed;
70
71 let kind: lsp_types::SymbolKind = serde_json::from_value(serde_json::json!(mcp.kind))
74 .unwrap_or(lsp_types::SymbolKind::Function);
75 let range = ctx.denormalize_range(&uri, &mcp.range).await;
76 let selection_range = ctx.denormalize_range(&uri, &mcp.selection_range).await;
77
78 CallHierarchyItem {
79 name: mcp.name,
80 kind,
81 tags: None,
82 detail: mcp.detail,
83 uri,
84 range,
85 selection_range,
86 data: mcp.data,
87 }
88}
89
90async fn convert_call_hierarchy_item(
92 item: CallHierarchyItem,
93 ctx: &EncodingCtx,
94) -> CallHierarchyItemResult {
95 let range = ctx.normalize_range(&item.uri, item.range).await;
96 let selection_range = ctx.normalize_range(&item.uri, item.selection_range).await;
97
98 CallHierarchyItemResult {
99 name: item.name,
100 kind: serde_json::to_value(item.kind)
101 .ok()
102 .and_then(|v| v.as_u64())
103 .and_then(|n| u32::try_from(n).ok())
104 .unwrap_or(0),
105 detail: item.detail,
106 uri: item.uri.to_string(),
107 range,
108 selection_range,
109 data: item.data,
110 }
111}
112
113impl Translator {
114 pub async fn handle_call_hierarchy_prepare(
121 &self,
122 file_path: String,
123 position: Position,
124 ) -> Result<CallHierarchyPrepareResult> {
125 let Position { line, character } = position;
126 if line < 1 || character < 1 {
128 return Err(Error::InvalidToolParams(
129 "Line and character positions must be >= 1".to_string(),
130 ));
131 }
132
133 if line > MAX_POSITION_VALUE || character > MAX_POSITION_VALUE {
134 return Err(Error::InvalidToolParams(format!(
135 "Position values must be <= {MAX_POSITION_VALUE}"
136 )));
137 }
138
139 let (server_id, client, uri) = self
140 .prepare_gated_document(
141 &file_path,
142 ToolKind::CallHierarchy,
143 "callHierarchyProvider",
144 call_hierarchy_provider_supported,
145 )
146 .await?;
147 let ctx = self.encoding_ctx(&server_id);
148 let lsp_position = ctx.to_lsp(&uri, line, character).await;
149
150 let params = LspCallHierarchyPrepareParams {
151 text_document_position_params: TextDocumentPositionParams {
152 text_document: TextDocumentIdentifier { uri },
153 position: lsp_position,
154 },
155 work_done_progress_params: WorkDoneProgressParams::default(),
156 };
157
158 let response = client
159 .request_typed::<lsp_types::CallHierarchyPrepareRequest>(
160 params,
161 client.request_timeout(),
162 )
163 .await?;
164
165 let lsp_items = response.unwrap_or_default();
167 let mut items = Vec::with_capacity(lsp_items.len());
168 for item in lsp_items {
169 items.push(convert_call_hierarchy_item(item, &ctx).await);
170 }
171
172 Ok(CallHierarchyPrepareResult { items })
173 }
174
175 pub async fn handle_incoming_calls(
182 &self,
183 item: serde_json::Value,
184 ) -> Result<IncomingCallsResult> {
185 let parsed = parse_mcp_call_hierarchy_item(item)?;
187
188 let path = self.parse_file_uri(&parsed.uri)?;
194 let (server_id, client) = self
195 .resolve_client_for_file(&path, ToolKind::CallHierarchy)
196 .await?;
197 self.require_capability(
198 &server_id,
199 "callHierarchyProvider",
200 call_hierarchy_provider_supported,
201 )?;
202 let ctx = self.encoding_ctx(&server_id);
203 let lsp_item = call_hierarchy_item_to_lsp(parsed, &ctx).await;
204
205 let params = CallHierarchyIncomingCallsParams {
206 item: lsp_item,
207 work_done_progress_params: WorkDoneProgressParams::default(),
208 partial_result_params: PartialResultParams::default(),
209 };
210
211 let response = client
212 .request_typed::<lsp_types::CallHierarchyIncomingCallsRequest>(
213 params,
214 client.request_timeout(),
215 )
216 .await?;
217
218 let lsp_calls = response.unwrap_or_default();
220 let mut calls = Vec::with_capacity(lsp_calls.len());
221
222 for call in lsp_calls {
223 let from_uri = call.from.uri.clone();
226 let from_ranges = {
227 let mut ranges = Vec::with_capacity(call.from_ranges.len());
228 for range in call.from_ranges {
229 ranges.push(ctx.normalize_range(&from_uri, range).await);
230 }
231 ranges
232 };
233
234 calls.push(IncomingCall {
235 from: convert_call_hierarchy_item(call.from, &ctx).await,
236 from_ranges,
237 });
238 }
239
240 Ok(IncomingCallsResult { calls })
241 }
242
243 pub async fn handle_outgoing_calls(
250 &self,
251 item: serde_json::Value,
252 ) -> Result<OutgoingCallsResult> {
253 let parsed = parse_mcp_call_hierarchy_item(item)?;
255
256 let path = self.parse_file_uri(&parsed.uri)?;
259 let (server_id, client) = self
260 .resolve_client_for_file(&path, ToolKind::CallHierarchy)
261 .await?;
262 self.require_capability(
263 &server_id,
264 "callHierarchyProvider",
265 call_hierarchy_provider_supported,
266 )?;
267 let ctx = self.encoding_ctx(&server_id);
268 let source_uri = parsed.uri.clone();
271 let lsp_item = call_hierarchy_item_to_lsp(parsed, &ctx).await;
272
273 let params = CallHierarchyOutgoingCallsParams {
274 item: lsp_item,
275 work_done_progress_params: WorkDoneProgressParams::default(),
276 partial_result_params: PartialResultParams::default(),
277 };
278
279 let response = client
280 .request_typed::<lsp_types::CallHierarchyOutgoingCallsRequest>(
281 params,
282 client.request_timeout(),
283 )
284 .await?;
285
286 let lsp_calls = response.unwrap_or_default();
288 let mut calls = Vec::with_capacity(lsp_calls.len());
289
290 for call in lsp_calls {
291 let from_ranges = {
292 let mut ranges = Vec::with_capacity(call.from_ranges.len());
293 for range in call.from_ranges {
294 ranges.push(ctx.normalize_range(&source_uri, range).await);
295 }
296 ranges
297 };
298
299 calls.push(OutgoingCall {
300 to: convert_call_hierarchy_item(call.to, &ctx).await,
301 from_ranges,
302 });
303 }
304
305 Ok(OutgoingCallsResult { calls })
306 }
307}
308
309#[cfg(test)]
310#[allow(clippy::unwrap_used, clippy::expect_used)]
311mod tests {
312 use std::fs;
313 use std::sync::Arc;
314 use std::time::Duration;
315
316 use tempfile::TempDir;
317 use tokio::io::BufReader;
318 use tokio::time::timeout;
319 use url::Url;
320
321 use super::*;
322 use crate::bridge::translator::dto::{Position, Position2D, Range};
323 use crate::bridge::translator::testing::*;
324 use crate::config::ServerId;
325
326 #[tokio::test]
327 async fn test_handle_call_hierarchy_prepare_invalid_position_zero() {
328 let translator = Translator::new();
329 let result = translator
330 .handle_call_hierarchy_prepare(
331 "/tmp/test.rs".to_string(),
332 Position {
333 line: 0,
334 character: 1,
335 },
336 )
337 .await;
338 assert!(matches!(result, Err(Error::InvalidToolParams(_))));
339
340 let result = translator
341 .handle_call_hierarchy_prepare(
342 "/tmp/test.rs".to_string(),
343 Position {
344 line: 1,
345 character: 0,
346 },
347 )
348 .await;
349 assert!(matches!(result, Err(Error::InvalidToolParams(_))));
350 }
351
352 #[tokio::test]
353 async fn test_handle_call_hierarchy_prepare_invalid_position_too_large() {
354 let translator = Translator::new();
355 let result = translator
356 .handle_call_hierarchy_prepare(
357 "/tmp/test.rs".to_string(),
358 Position {
359 line: 1_000_001,
360 character: 1,
361 },
362 )
363 .await;
364 assert!(matches!(result, Err(Error::InvalidToolParams(_))));
365
366 let result = translator
367 .handle_call_hierarchy_prepare(
368 "/tmp/test.rs".to_string(),
369 Position {
370 line: 1,
371 character: 1_000_001,
372 },
373 )
374 .await;
375 assert!(matches!(result, Err(Error::InvalidToolParams(_))));
376 }
377
378 #[tokio::test]
379 async fn test_handle_incoming_calls_invalid_json() {
380 let translator = Translator::new();
381 let invalid_item = serde_json::json!({"invalid": "structure"});
382 let result = translator.handle_incoming_calls(invalid_item).await;
383 assert!(matches!(result, Err(Error::InvalidToolParams(_))));
384 }
385
386 #[tokio::test]
387 async fn test_handle_outgoing_calls_invalid_json() {
388 let translator = Translator::new();
389 let invalid_item = serde_json::json!({"invalid": "structure"});
390 let result = translator.handle_outgoing_calls(invalid_item).await;
391 assert!(matches!(result, Err(Error::InvalidToolParams(_))));
392 }
393
394 #[tokio::test]
406 async fn test_handle_incoming_calls_with_nonexistent_file_uri_returns_file_io_not_invalid_uri()
407 {
408 let translator = Translator::new();
409 let item = serde_json::json!({
410 "name": "foo",
411 "kind": 12,
412 "uri": "file:///this/path/does/not/exist/anywhere.rs",
413 "range": {
414 "start": {"line": 1, "character": 1},
415 "end": {"line": 1, "character": 1}
416 },
417 "selectionRange": {
418 "start": {"line": 1, "character": 1},
419 "end": {"line": 1, "character": 1}
420 }
421 });
422
423 let result = translator.handle_incoming_calls(item).await;
424
425 assert!(
426 matches!(result, Err(Error::FileIo { .. })),
427 "expected Error::FileIo from the canonicalize() failure now that Uri construction \
428 cannot itself reject a malformed uri, got {result:?}"
429 );
430 }
431
432 #[tokio::test]
435 async fn test_handle_outgoing_calls_with_nonexistent_file_uri_returns_file_io_not_invalid_uri()
436 {
437 let translator = Translator::new();
438 let item = serde_json::json!({
439 "name": "foo",
440 "kind": 12,
441 "uri": "file:///this/path/does/not/exist/anywhere.rs",
442 "range": {
443 "start": {"line": 1, "character": 1},
444 "end": {"line": 1, "character": 1}
445 },
446 "selectionRange": {
447 "start": {"line": 1, "character": 1},
448 "end": {"line": 1, "character": 1}
449 }
450 });
451
452 let result = translator.handle_outgoing_calls(item).await;
453
454 assert!(
455 matches!(result, Err(Error::FileIo { .. })),
456 "expected Error::FileIo from the canonicalize() failure now that Uri construction \
457 cannot itself reject a malformed uri, got {result:?}"
458 );
459 }
460
461 #[tokio::test]
462 async fn test_convert_call_hierarchy_item_kind_is_numeric() {
463 let item = lsp_types::CallHierarchyItem {
464 name: "my_fn".to_string(),
465 kind: lsp_types::SymbolKind::Function,
466 tags: None,
467 detail: None,
468 uri: lsp_types::Uri::from("file:///tmp/test.rs"),
469 range: lsp_types::Range {
470 start: lsp_types::Position {
471 line: 0,
472 character: 0,
473 },
474 end: lsp_types::Position {
475 line: 0,
476 character: 5,
477 },
478 },
479 selection_range: lsp_types::Range {
480 start: lsp_types::Position {
481 line: 0,
482 character: 0,
483 },
484 end: lsp_types::Position {
485 line: 0,
486 character: 5,
487 },
488 },
489 data: None,
490 };
491 let result = convert_call_hierarchy_item(item, &test_ctx()).await;
492 assert_eq!(result.kind, 12u32);
494 assert_eq!(result.name, "my_fn");
495 }
496
497 #[tokio::test]
507 async fn test_handle_incoming_calls_from_ranges_convert_against_callers_own_uri() {
508 let dir = TempDir::new().unwrap();
509 let server_id = ServerId::from("rust");
510 let caps = lsp_types::ServerCapabilities {
511 call_hierarchy_provider: Some(lsp_types::CallHierarchyProvider::Bool(true)),
512 ..Default::default()
513 };
514 let (translator, mut server) = translator_with_capabilities_and_encoding(
515 &dir,
516 &server_id,
517 caps,
518 lsp_types::PositionEncodingKind::UTF8,
519 );
520
521 let queried_path = dir.path().join("queried.rs");
522 fs::write(&queried_path, "abc").unwrap();
523 let queried_uri = Url::from_file_path(&queried_path).unwrap().to_string();
524
525 let caller_path = dir.path().join("caller.rs");
526 fs::write(&caller_path, "aöb").unwrap();
527 let caller_uri = Url::from_file_path(&caller_path).unwrap().to_string();
528
529 let item = CallHierarchyItemResult {
530 name: "queried_fn".to_string(),
531 kind: 12,
532 detail: None,
533 uri: queried_uri,
534 range: Range {
535 start: Position2D {
536 line: 1,
537 character: 1,
538 },
539 end: Position2D {
540 line: 1,
541 character: 4,
542 },
543 },
544 selection_range: Range {
545 start: Position2D {
546 line: 1,
547 character: 1,
548 },
549 end: Position2D {
550 line: 1,
551 character: 4,
552 },
553 },
554 data: None,
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 { translator.handle_incoming_calls(item).await })
562 };
563
564 let mut wire = BufReader::new(&mut server.write_stdout);
565 let request = read_framed_message(&mut wire).await;
566 assert_eq!(request["method"], "callHierarchy/incomingCalls");
567
568 write_response(
569 &mut server.read_half_stdin,
570 &request["id"],
571 serde_json::json!([{
572 "from": {
573 "name": "caller_fn",
574 "kind": 12,
575 "uri": caller_uri,
576 "range": {
577 "start": {"line": 0, "character": 0},
578 "end": {"line": 0, "character": 1}
579 },
580 "selectionRange": {
581 "start": {"line": 0, "character": 0},
582 "end": {"line": 0, "character": 1}
583 }
584 },
585 "fromRanges": [{
586 "start": {"line": 0, "character": 0},
587 "end": {"line": 0, "character": 3}
588 }]
589 }]),
590 )
591 .await;
592
593 let result = timeout(Duration::from_secs(2), handle)
594 .await
595 .expect("handler call should not hang")
596 .unwrap()
597 .unwrap();
598
599 assert_eq!(result.calls.len(), 1);
600 let from_range = &result.calls[0].from_ranges[0];
601 assert_eq!(
602 from_range.end.character, 3,
603 "fromRanges must convert against the caller's own file (\"aöb\"), not the queried \
604 item's (\"abc\") -- a byte offset of 3 is UTF-16 column 3 in the former, 4 in the \
605 latter"
606 );
607 }
608
609 #[tokio::test]
613 async fn test_handle_outgoing_calls_from_ranges_convert_against_queried_uri() {
614 let dir = TempDir::new().unwrap();
615 let server_id = ServerId::from("rust");
616 let caps = lsp_types::ServerCapabilities {
617 call_hierarchy_provider: Some(lsp_types::CallHierarchyProvider::Bool(true)),
618 ..Default::default()
619 };
620 let (translator, mut server) = translator_with_capabilities_and_encoding(
621 &dir,
622 &server_id,
623 caps,
624 lsp_types::PositionEncodingKind::UTF8,
625 );
626
627 let queried_path = dir.path().join("queried.rs");
628 fs::write(&queried_path, "aöb").unwrap();
629 let queried_uri = Url::from_file_path(&queried_path).unwrap().to_string();
630
631 let callee_path = dir.path().join("callee.rs");
632 fs::write(&callee_path, "abc").unwrap();
633 let callee_uri = Url::from_file_path(&callee_path).unwrap().to_string();
634
635 let item = CallHierarchyItemResult {
636 name: "queried_fn".to_string(),
637 kind: 12,
638 detail: None,
639 uri: queried_uri,
640 range: Range {
641 start: Position2D {
642 line: 1,
643 character: 1,
644 },
645 end: Position2D {
646 line: 1,
647 character: 4,
648 },
649 },
650 selection_range: Range {
651 start: Position2D {
652 line: 1,
653 character: 1,
654 },
655 end: Position2D {
656 line: 1,
657 character: 4,
658 },
659 },
660 data: None,
661 };
662
663 let translator = Arc::new(translator);
664 let handle = {
665 let translator = Arc::clone(&translator);
666 let item = serde_json::to_value(item).unwrap();
667 tokio::spawn(async move { translator.handle_outgoing_calls(item).await })
668 };
669
670 let mut wire = BufReader::new(&mut server.write_stdout);
671 let request = read_framed_message(&mut wire).await;
672 assert_eq!(request["method"], "callHierarchy/outgoingCalls");
673
674 write_response(
675 &mut server.read_half_stdin,
676 &request["id"],
677 serde_json::json!([{
678 "to": {
679 "name": "callee_fn",
680 "kind": 12,
681 "uri": callee_uri,
682 "range": {
683 "start": {"line": 0, "character": 0},
684 "end": {"line": 0, "character": 1}
685 },
686 "selectionRange": {
687 "start": {"line": 0, "character": 0},
688 "end": {"line": 0, "character": 1}
689 }
690 },
691 "fromRanges": [{
692 "start": {"line": 0, "character": 0},
693 "end": {"line": 0, "character": 3}
694 }]
695 }]),
696 )
697 .await;
698
699 let result = timeout(Duration::from_secs(2), handle)
700 .await
701 .expect("handler call should not hang")
702 .unwrap()
703 .unwrap();
704
705 assert_eq!(result.calls.len(), 1);
706 let from_range = &result.calls[0].from_ranges[0];
707 assert_eq!(
708 from_range.end.character, 3,
709 "fromRanges must convert against the queried item's own file (\"aöb\"), not the \
710 callee's (\"abc\") -- a byte offset of 3 is UTF-16 column 3 in the former, 4 in \
711 the latter"
712 );
713 }
714}