1use lsp_types::{
4 CallHierarchyIncomingCall, CallHierarchyIncomingCallsParams, CallHierarchyItem,
5 CallHierarchyOutgoingCall, CallHierarchyOutgoingCallsParams,
6 CallHierarchyPrepareParams as LspCallHierarchyPrepareParams, PartialResultParams,
7 TextDocumentIdentifier, TextDocumentPositionParams, WorkDoneProgressParams,
8};
9
10use super::Translator;
11use super::dto::{
12 CallHierarchyItemResult, CallHierarchyPrepareResult, IncomingCall, IncomingCallsResult,
13 OutgoingCall, OutgoingCallsResult,
14};
15use super::encoding_ctx::EncodingCtx;
16use super::routing::MAX_POSITION_VALUE;
17use crate::config::ToolKind;
18use crate::error::{Error, Result};
19
20const fn call_hierarchy_provider_supported(caps: &lsp_types::ServerCapabilities) -> bool {
25 matches!(
26 caps.call_hierarchy_provider,
27 Some(
28 lsp_types::CallHierarchyServerCapability::Simple(true)
29 | lsp_types::CallHierarchyServerCapability::Options(_)
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 = mcp.uri.parse::<lsp_types::Uri>().map_err(|e| {
54 Error::InvalidToolParams(format!("Invalid URI in call hierarchy item: {e}"))
55 })?;
56
57 Ok(ParsedCallHierarchyItem { uri, mcp })
58}
59
60async fn call_hierarchy_item_to_lsp(
63 parsed: ParsedCallHierarchyItem,
64 ctx: &EncodingCtx,
65) -> CallHierarchyItem {
66 let ParsedCallHierarchyItem { uri, mcp } = parsed;
67
68 let kind: lsp_types::SymbolKind = serde_json::from_value(serde_json::json!(mcp.kind))
71 .unwrap_or(lsp_types::SymbolKind::FUNCTION);
72 let range = ctx.denormalize_range(&uri, &mcp.range).await;
73 let selection_range = ctx.denormalize_range(&uri, &mcp.selection_range).await;
74
75 CallHierarchyItem {
76 name: mcp.name,
77 kind,
78 tags: None,
79 detail: mcp.detail,
80 uri,
81 range,
82 selection_range,
83 data: mcp.data,
84 }
85}
86
87async fn convert_call_hierarchy_item(
89 item: CallHierarchyItem,
90 ctx: &EncodingCtx,
91) -> CallHierarchyItemResult {
92 let range = ctx.normalize_range(&item.uri, item.range).await;
93 let selection_range = ctx.normalize_range(&item.uri, item.selection_range).await;
94
95 CallHierarchyItemResult {
96 name: item.name,
97 kind: serde_json::to_value(item.kind)
98 .ok()
99 .and_then(|v| v.as_u64())
100 .and_then(|n| u32::try_from(n).ok())
101 .unwrap_or(0),
102 detail: item.detail,
103 uri: item.uri.to_string(),
104 range,
105 selection_range,
106 data: item.data,
107 }
108}
109
110impl Translator {
111 pub async fn handle_call_hierarchy_prepare(
118 &self,
119 file_path: String,
120 line: u32,
121 character: u32,
122 ) -> Result<CallHierarchyPrepareResult> {
123 if line < 1 || character < 1 {
125 return Err(Error::InvalidToolParams(
126 "Line and character positions must be >= 1".to_string(),
127 ));
128 }
129
130 if line > MAX_POSITION_VALUE || character > MAX_POSITION_VALUE {
131 return Err(Error::InvalidToolParams(format!(
132 "Position values must be <= {MAX_POSITION_VALUE}"
133 )));
134 }
135
136 let (server_id, client, uri) = self
137 .prepare_gated_document(
138 &file_path,
139 ToolKind::CallHierarchy,
140 "callHierarchyProvider",
141 call_hierarchy_provider_supported,
142 )
143 .await?;
144 let ctx = self.encoding_ctx(&server_id);
145 let lsp_position = ctx.to_lsp(&uri, line, character).await;
146
147 let params = LspCallHierarchyPrepareParams {
148 text_document_position_params: TextDocumentPositionParams {
149 text_document: TextDocumentIdentifier { uri },
150 position: lsp_position,
151 },
152 work_done_progress_params: WorkDoneProgressParams::default(),
153 };
154
155 let response: Option<Vec<CallHierarchyItem>> = client
156 .request(
157 "textDocument/prepareCallHierarchy",
158 params,
159 client.request_timeout(),
160 )
161 .await?;
162
163 let lsp_items = response.unwrap_or_default();
165 let mut items = Vec::with_capacity(lsp_items.len());
166 for item in lsp_items {
167 items.push(convert_call_hierarchy_item(item, &ctx).await);
168 }
169
170 Ok(CallHierarchyPrepareResult { items })
171 }
172
173 pub async fn handle_incoming_calls(
180 &self,
181 item: serde_json::Value,
182 ) -> Result<IncomingCallsResult> {
183 let parsed = parse_mcp_call_hierarchy_item(item)?;
185
186 let path = self.parse_file_uri(&parsed.uri)?;
192 let (server_id, client) = self
193 .resolve_client_for_file(&path, ToolKind::CallHierarchy)
194 .await?;
195 self.require_capability(
196 &server_id,
197 "callHierarchyProvider",
198 call_hierarchy_provider_supported,
199 )?;
200 let ctx = self.encoding_ctx(&server_id);
201 let lsp_item = call_hierarchy_item_to_lsp(parsed, &ctx).await;
202
203 let params = CallHierarchyIncomingCallsParams {
204 item: lsp_item,
205 work_done_progress_params: WorkDoneProgressParams::default(),
206 partial_result_params: PartialResultParams::default(),
207 };
208
209 let response: Option<Vec<CallHierarchyIncomingCall>> = client
210 .request(
211 "callHierarchy/incomingCalls",
212 params,
213 client.request_timeout(),
214 )
215 .await?;
216
217 let lsp_calls = response.unwrap_or_default();
219 let mut calls = Vec::with_capacity(lsp_calls.len());
220
221 for call in lsp_calls {
222 let from_uri = call.from.uri.clone();
225 let from_ranges = {
226 let mut ranges = Vec::with_capacity(call.from_ranges.len());
227 for range in call.from_ranges {
228 ranges.push(ctx.normalize_range(&from_uri, range).await);
229 }
230 ranges
231 };
232
233 calls.push(IncomingCall {
234 from: convert_call_hierarchy_item(call.from, &ctx).await,
235 from_ranges,
236 });
237 }
238
239 Ok(IncomingCallsResult { calls })
240 }
241
242 pub async fn handle_outgoing_calls(
249 &self,
250 item: serde_json::Value,
251 ) -> Result<OutgoingCallsResult> {
252 let parsed = parse_mcp_call_hierarchy_item(item)?;
254
255 let path = self.parse_file_uri(&parsed.uri)?;
258 let (server_id, client) = self
259 .resolve_client_for_file(&path, ToolKind::CallHierarchy)
260 .await?;
261 self.require_capability(
262 &server_id,
263 "callHierarchyProvider",
264 call_hierarchy_provider_supported,
265 )?;
266 let ctx = self.encoding_ctx(&server_id);
267 let source_uri = parsed.uri.clone();
270 let lsp_item = call_hierarchy_item_to_lsp(parsed, &ctx).await;
271
272 let params = CallHierarchyOutgoingCallsParams {
273 item: lsp_item,
274 work_done_progress_params: WorkDoneProgressParams::default(),
275 partial_result_params: PartialResultParams::default(),
276 };
277
278 let response: Option<Vec<CallHierarchyOutgoingCall>> = client
279 .request(
280 "callHierarchy/outgoingCalls",
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::{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("/tmp/test.rs".to_string(), 0, 1)
331 .await;
332 assert!(matches!(result, Err(Error::InvalidToolParams(_))));
333
334 let result = translator
335 .handle_call_hierarchy_prepare("/tmp/test.rs".to_string(), 1, 0)
336 .await;
337 assert!(matches!(result, Err(Error::InvalidToolParams(_))));
338 }
339
340 #[tokio::test]
341 async fn test_handle_call_hierarchy_prepare_invalid_position_too_large() {
342 let translator = Translator::new();
343 let result = translator
344 .handle_call_hierarchy_prepare("/tmp/test.rs".to_string(), 1_000_001, 1)
345 .await;
346 assert!(matches!(result, Err(Error::InvalidToolParams(_))));
347
348 let result = translator
349 .handle_call_hierarchy_prepare("/tmp/test.rs".to_string(), 1, 1_000_001)
350 .await;
351 assert!(matches!(result, Err(Error::InvalidToolParams(_))));
352 }
353
354 #[tokio::test]
355 async fn test_handle_incoming_calls_invalid_json() {
356 let translator = Translator::new();
357 let invalid_item = serde_json::json!({"invalid": "structure"});
358 let result = translator.handle_incoming_calls(invalid_item).await;
359 assert!(matches!(result, Err(Error::InvalidToolParams(_))));
360 }
361
362 #[tokio::test]
363 async fn test_handle_outgoing_calls_invalid_json() {
364 let translator = Translator::new();
365 let invalid_item = serde_json::json!({"invalid": "structure"});
366 let result = translator.handle_outgoing_calls(invalid_item).await;
367 assert!(matches!(result, Err(Error::InvalidToolParams(_))));
368 }
369
370 #[tokio::test]
371 async fn test_convert_call_hierarchy_item_kind_is_numeric() {
372 let item = lsp_types::CallHierarchyItem {
373 name: "my_fn".to_string(),
374 kind: lsp_types::SymbolKind::FUNCTION,
375 tags: None,
376 detail: None,
377 uri: "file:///tmp/test.rs".parse().unwrap(),
378 range: lsp_types::Range {
379 start: lsp_types::Position {
380 line: 0,
381 character: 0,
382 },
383 end: lsp_types::Position {
384 line: 0,
385 character: 5,
386 },
387 },
388 selection_range: lsp_types::Range {
389 start: lsp_types::Position {
390 line: 0,
391 character: 0,
392 },
393 end: lsp_types::Position {
394 line: 0,
395 character: 5,
396 },
397 },
398 data: None,
399 };
400 let result = convert_call_hierarchy_item(item, &test_ctx()).await;
401 assert_eq!(result.kind, 12u32);
403 assert_eq!(result.name, "my_fn");
404 }
405
406 #[tokio::test]
416 async fn test_handle_incoming_calls_from_ranges_convert_against_callers_own_uri() {
417 let dir = TempDir::new().unwrap();
418 let server_id = ServerId::from("rust");
419 let caps = lsp_types::ServerCapabilities {
420 call_hierarchy_provider: Some(lsp_types::CallHierarchyServerCapability::Simple(true)),
421 ..Default::default()
422 };
423 let (translator, mut server) = translator_with_capabilities_and_encoding(
424 &dir,
425 &server_id,
426 caps,
427 lsp_types::PositionEncodingKind::UTF8,
428 );
429
430 let queried_path = dir.path().join("queried.rs");
431 fs::write(&queried_path, "abc").unwrap();
432 let queried_uri = Url::from_file_path(&queried_path).unwrap().to_string();
433
434 let caller_path = dir.path().join("caller.rs");
435 fs::write(&caller_path, "aöb").unwrap();
436 let caller_uri = Url::from_file_path(&caller_path).unwrap().to_string();
437
438 let item = CallHierarchyItemResult {
439 name: "queried_fn".to_string(),
440 kind: 12,
441 detail: None,
442 uri: queried_uri,
443 range: Range {
444 start: Position2D {
445 line: 1,
446 character: 1,
447 },
448 end: Position2D {
449 line: 1,
450 character: 4,
451 },
452 },
453 selection_range: Range {
454 start: Position2D {
455 line: 1,
456 character: 1,
457 },
458 end: Position2D {
459 line: 1,
460 character: 4,
461 },
462 },
463 data: None,
464 };
465
466 let translator = Arc::new(translator);
467 let handle = {
468 let translator = Arc::clone(&translator);
469 let item = serde_json::to_value(item).unwrap();
470 tokio::spawn(async move { translator.handle_incoming_calls(item).await })
471 };
472
473 let mut wire = BufReader::new(&mut server.write_stdout);
474 let request = read_framed_message(&mut wire).await;
475 assert_eq!(request["method"], "callHierarchy/incomingCalls");
476
477 write_response(
478 &mut server.read_half_stdin,
479 &request["id"],
480 serde_json::json!([{
481 "from": {
482 "name": "caller_fn",
483 "kind": 12,
484 "uri": caller_uri,
485 "range": {
486 "start": {"line": 0, "character": 0},
487 "end": {"line": 0, "character": 1}
488 },
489 "selectionRange": {
490 "start": {"line": 0, "character": 0},
491 "end": {"line": 0, "character": 1}
492 }
493 },
494 "fromRanges": [{
495 "start": {"line": 0, "character": 0},
496 "end": {"line": 0, "character": 3}
497 }]
498 }]),
499 )
500 .await;
501
502 let result = timeout(Duration::from_secs(2), handle)
503 .await
504 .expect("handler call should not hang")
505 .unwrap()
506 .unwrap();
507
508 assert_eq!(result.calls.len(), 1);
509 let from_range = &result.calls[0].from_ranges[0];
510 assert_eq!(
511 from_range.end.character, 3,
512 "fromRanges must convert against the caller's own file (\"aöb\"), not the queried \
513 item's (\"abc\") -- a byte offset of 3 is UTF-16 column 3 in the former, 4 in the \
514 latter"
515 );
516 }
517
518 #[tokio::test]
522 async fn test_handle_outgoing_calls_from_ranges_convert_against_queried_uri() {
523 let dir = TempDir::new().unwrap();
524 let server_id = ServerId::from("rust");
525 let caps = lsp_types::ServerCapabilities {
526 call_hierarchy_provider: Some(lsp_types::CallHierarchyServerCapability::Simple(true)),
527 ..Default::default()
528 };
529 let (translator, mut server) = translator_with_capabilities_and_encoding(
530 &dir,
531 &server_id,
532 caps,
533 lsp_types::PositionEncodingKind::UTF8,
534 );
535
536 let queried_path = dir.path().join("queried.rs");
537 fs::write(&queried_path, "aöb").unwrap();
538 let queried_uri = Url::from_file_path(&queried_path).unwrap().to_string();
539
540 let callee_path = dir.path().join("callee.rs");
541 fs::write(&callee_path, "abc").unwrap();
542 let callee_uri = Url::from_file_path(&callee_path).unwrap().to_string();
543
544 let item = CallHierarchyItemResult {
545 name: "queried_fn".to_string(),
546 kind: 12,
547 detail: None,
548 uri: queried_uri,
549 range: Range {
550 start: Position2D {
551 line: 1,
552 character: 1,
553 },
554 end: Position2D {
555 line: 1,
556 character: 4,
557 },
558 },
559 selection_range: Range {
560 start: Position2D {
561 line: 1,
562 character: 1,
563 },
564 end: Position2D {
565 line: 1,
566 character: 4,
567 },
568 },
569 data: None,
570 };
571
572 let translator = Arc::new(translator);
573 let handle = {
574 let translator = Arc::clone(&translator);
575 let item = serde_json::to_value(item).unwrap();
576 tokio::spawn(async move { translator.handle_outgoing_calls(item).await })
577 };
578
579 let mut wire = BufReader::new(&mut server.write_stdout);
580 let request = read_framed_message(&mut wire).await;
581 assert_eq!(request["method"], "callHierarchy/outgoingCalls");
582
583 write_response(
584 &mut server.read_half_stdin,
585 &request["id"],
586 serde_json::json!([{
587 "to": {
588 "name": "callee_fn",
589 "kind": 12,
590 "uri": callee_uri,
591 "range": {
592 "start": {"line": 0, "character": 0},
593 "end": {"line": 0, "character": 1}
594 },
595 "selectionRange": {
596 "start": {"line": 0, "character": 0},
597 "end": {"line": 0, "character": 1}
598 }
599 },
600 "fromRanges": [{
601 "start": {"line": 0, "character": 0},
602 "end": {"line": 0, "character": 3}
603 }]
604 }]),
605 )
606 .await;
607
608 let result = timeout(Duration::from_secs(2), handle)
609 .await
610 .expect("handler call should not hang")
611 .unwrap()
612 .unwrap();
613
614 assert_eq!(result.calls.len(), 1);
615 let from_range = &result.calls[0].from_ranges[0];
616 assert_eq!(
617 from_range.end.character, 3,
618 "fromRanges must convert against the queried item's own file (\"aöb\"), not the \
619 callee's (\"abc\") -- a byte offset of 3 is UTF-16 column 3 in the former, 4 in \
620 the latter"
621 );
622 }
623}