Skip to main content

mcpls_core/bridge/translator/
call_hierarchy.rs

1//! Call hierarchy prepare/incoming/outgoing handlers.
2
3use lsp_types::CallHierarchyItem;
4
5use super::Translator;
6use super::dto::{
7    CallHierarchyPrepareResult, CheckedHierarchyItem, HierarchyItem, IncomingCall,
8    IncomingCallsResult, OutgoingCall, OutgoingCallsResult, Position, PositionDegradation, Range,
9};
10use super::encoding_ctx::EncodingCtx;
11use super::hierarchy::hierarchy_item_to_mcp;
12use super::navigation::ItemBudget;
13use super::positioned::Positioned;
14use super::routing::PreparedDocument;
15use crate::bridge::{ClientPath, Indexed};
16use crate::error::Result;
17
18/// Convert LSP call hierarchy item to MCP call hierarchy item.
19async fn convert_call_hierarchy_item(item: CallHierarchyItem, ctx: &EncodingCtx) -> HierarchyItem {
20    hierarchy_item_to_mcp(item, ctx).await
21}
22
23/// Which side of the queried item a call walk follows.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25enum CallDirection {
26    Incoming,
27    Outgoing,
28}
29
30/// One call of a walk: the peer item and where in the call's document the
31/// call occurs.
32struct WalkedCall {
33    peer: HierarchyItem,
34    from_ranges: Vec<Range>,
35}
36
37/// The calls of one walk, capped by an [`ItemBudget`].
38struct CallWalk {
39    calls: Vec<WalkedCall>,
40    truncated: bool,
41    positions_degraded: Option<PositionDegradation>,
42}
43
44/// An LSP call before conversion: the peer, its ranges, and the document those
45/// ranges belong to.
46struct RawCall {
47    peer: CallHierarchyItem,
48    from_ranges: Vec<lsp_types::Range>,
49    ranges_uri: lsp_types::Uri,
50}
51
52impl Translator {
53    /// Handle call hierarchy prepare request.
54    ///
55    /// # Errors
56    ///
57    /// Returns an error if the LSP request fails, the file cannot be opened,
58    /// or the routed server does not advertise `callHierarchyProvider` support.
59    pub async fn handle_call_hierarchy_prepare(
60        &self,
61        file_path: ClientPath,
62        position: Position,
63    ) -> Result<Indexed<CallHierarchyPrepareResult>> {
64        let Positioned {
65            result:
66                Indexed {
67                    result: response,
68                    indexing,
69                },
70            ctx,
71            doc: _doc,
72        } = self
73            .disclosed_position_request::<lsp_types::CallHierarchyPrepareRequest>(
74                &file_path,
75                position,
76                (),
77            )
78            .await?;
79
80        // Pre-allocate and build result. Not filtered to workspace roots --
81        // see `lsp_locations_to_mcp`'s doc comment in `navigation.rs` for why
82        // a read-only location outside the workspace (stdlib, a dependency)
83        // is normal, expected navigation rather than something to drop.
84        let mut budget = ItemBudget::new();
85        let lsp_items = budget.admit(response.unwrap_or_default());
86        let mut items = Vec::with_capacity(lsp_items.len());
87        for item in lsp_items {
88            items.push(convert_call_hierarchy_item(item, &ctx).await);
89        }
90
91        Ok(Indexed::new(
92            CallHierarchyPrepareResult {
93                items,
94                truncated: budget.truncated(),
95                positions_degraded: ctx.positions_degraded(),
96            },
97            indexing,
98        ))
99    }
100
101    /// Handle incoming calls request.
102    ///
103    /// Routing through `prepare_gated_document_for_path` means this now
104    /// stats, reads, and `didOpen`s the item's own file as a side effect,
105    /// even though a call-hierarchy item is opaque per the LSP spec and
106    /// needs no open document -- accepted for chokepoint/gating
107    /// consistency with `handle_references` and the other whole-workspace
108    /// tools; it does mean a replayed item whose file has since been
109    /// deleted now fails on that stat instead of just proceeding.
110    ///
111    /// # Errors
112    ///
113    /// Returns an error if the LSP request fails, the routed server does not
114    /// advertise `callHierarchyProvider` support, or the server is still
115    /// indexing the workspace after `INDEXING_READY_TIMEOUT`.
116    pub async fn handle_incoming_calls(
117        &self,
118        item: CheckedHierarchyItem,
119    ) -> Result<IncomingCallsResult> {
120        let walk = self.walk_calls(item, CallDirection::Incoming).await?;
121        Ok(IncomingCallsResult {
122            calls: walk
123                .calls
124                .into_iter()
125                .map(|call| IncomingCall {
126                    from: call.peer,
127                    from_ranges: call.from_ranges,
128                })
129                .collect(),
130            truncated: walk.truncated,
131            positions_degraded: walk.positions_degraded,
132        })
133    }
134
135    /// Handle outgoing calls request.
136    ///
137    /// Same `didOpen`-as-side-effect trade-off as `handle_incoming_calls` --
138    /// see that method's doc.
139    ///
140    /// # Errors
141    ///
142    /// See [`Self::handle_incoming_calls`].
143    pub async fn handle_outgoing_calls(
144        &self,
145        item: CheckedHierarchyItem,
146    ) -> Result<OutgoingCallsResult> {
147        let walk = self.walk_calls(item, CallDirection::Outgoing).await?;
148        Ok(OutgoingCallsResult {
149            calls: walk
150                .calls
151                .into_iter()
152                .map(|call| OutgoingCall {
153                    to: call.peer,
154                    from_ranges: call.from_ranges,
155                })
156                .collect(),
157            truncated: walk.truncated,
158            positions_degraded: walk.positions_degraded,
159        })
160    }
161
162    /// Resolves, gates and queries one level of calls around `item`.
163    async fn walk_calls(
164        &self,
165        item: CheckedHierarchyItem,
166        direction: CallDirection,
167    ) -> Result<CallWalk> {
168        let (raw, ctx, _doc): (Vec<RawCall>, EncodingCtx, PreparedDocument) = match direction {
169            CallDirection::Incoming => {
170                let Positioned { result, ctx, doc } = self
171                    .item_request::<lsp_types::CallHierarchyIncomingCallsRequest>(item)
172                    .await?;
173                let raw = result
174                    .unwrap_or_default()
175                    .into_iter()
176                    .map(|call| RawCall {
177                        // Per the LSP spec, `fromRanges` are ranges within the
178                        // *caller's* document, not the queried item's.
179                        ranges_uri: call.from.uri.clone(),
180                        peer: call.from,
181                        from_ranges: call.from_ranges,
182                    })
183                    .collect();
184                (raw, ctx, doc)
185            }
186            CallDirection::Outgoing => {
187                let Positioned { result, ctx, doc } = self
188                    .item_request::<lsp_types::CallHierarchyOutgoingCallsRequest>(item)
189                    .await?;
190                let source_uri = doc.uri().clone();
191                let raw = result
192                    .unwrap_or_default()
193                    .into_iter()
194                    .map(|call| RawCall {
195                        // An outgoing call's `fromRanges` are ranges within the
196                        // *queried* item's own document, not the callee's.
197                        ranges_uri: source_uri.clone(),
198                        peer: call.to,
199                        from_ranges: call.from_ranges,
200                    })
201                    .collect();
202                (raw, ctx, doc)
203            }
204        };
205
206        // Not filtered to workspace roots -- see `handle_call_hierarchy_prepare`.
207        let mut budget = ItemBudget::new();
208        let mut calls = Vec::new();
209        for call in raw {
210            // A call is kept together with all its ranges or not at all.
211            if !budget.spend_whole(call.from_ranges.len().saturating_add(1)) {
212                continue;
213            }
214            let mut from_ranges = Vec::with_capacity(call.from_ranges.len());
215            for range in call.from_ranges {
216                from_ranges.push(ctx.normalize_range(&call.ranges_uri, range).await);
217            }
218            calls.push(WalkedCall {
219                peer: convert_call_hierarchy_item(call.peer, &ctx).await,
220                from_ranges,
221            });
222        }
223
224        Ok(CallWalk {
225            calls,
226            truncated: budget.truncated(),
227            positions_degraded: ctx.positions_degraded(),
228        })
229    }
230}
231
232#[cfg(test)]
233mod tests {
234    use std::sync::Arc;
235    use std::time::Duration;
236    use std::{assert_matches, fs};
237
238    use tempfile::TempDir;
239    use tokio::io::BufReader;
240    use tokio::sync::Mutex;
241    use tokio::time::timeout;
242    use url::Url;
243
244    use super::*;
245    use crate::bridge::translator::dto::{Position2D, Range};
246    use crate::bridge::translator::testing::*;
247    use crate::bridge::{NotificationCache, WorkspaceRoots};
248    use crate::config::ServerId;
249    use crate::error::Error;
250    use crate::test_lsp::client_path;
251
252    /// Builds a `HierarchyItem` JSON value pointing at `path`, for
253    /// driving `handle_incoming_calls`/`handle_outgoing_calls` directly
254    /// without a preceding `prepare_call_hierarchy` round trip.
255    fn call_hierarchy_item_json(uri: &str) -> serde_json::Value {
256        serde_json::to_value(HierarchyItem {
257            name: "queried_fn".to_string(),
258            kind: 12,
259            detail: None,
260            uri: uri.to_string(),
261            range: Range {
262                start: Position2D {
263                    line: 1,
264                    character: 1,
265                },
266                end: Position2D {
267                    line: 1,
268                    character: 4,
269                },
270            },
271            selection_range: Range {
272                start: Position2D {
273                    line: 1,
274                    character: 1,
275                },
276                end: Position2D {
277                    line: 1,
278                    character: 4,
279                },
280            },
281            data: None,
282            out_of_workspace: false,
283        })
284        .unwrap()
285    }
286
287    /// #423 regression: `handle_incoming_calls` is a whole-workspace query of
288    /// the same class as `references` and must be gated on indexing
289    /// readiness the same way, instead of bypassing `prepare_gated_document`
290    /// entirely.
291    #[tokio::test(start_paused = true)]
292    async fn test_handle_incoming_calls_returns_workspace_indexing_error_when_loading() {
293        let dir = TempDir::new().unwrap();
294        let server_id = ServerId::from_static("rust");
295        let caps = lsp_types::ServerCapabilities {
296            call_hierarchy_provider: Some(lsp_types::CallHierarchyProvider::Bool(true)),
297            ..Default::default()
298        };
299        let (translator, _server) = translator_with_capabilities(&dir, &server_id, caps);
300
301        let cache = Arc::new(Mutex::new(NotificationCache::new()));
302        cache.lock().await.observe_indexing_signal(
303            &server_id,
304            "experimental/serverStatus",
305            Some(&serde_json::json!({"quiescent": false})),
306        );
307        let translator = translator.with_notification_cache(cache);
308
309        let path = dir.path().join("queried.rs");
310        fs::write(&path, "fn queried() {}").unwrap();
311        let uri = Url::from_file_path(&path).unwrap().to_string();
312
313        let err = translator
314            .handle_incoming_calls(checked_hierarchy_item(call_hierarchy_item_json(&uri)))
315            .await
316            .unwrap_err();
317
318        assert_matches!(
319            err,
320            Error::WorkspaceIndexing { server_id: id, .. } if id == server_id
321        );
322    }
323
324    /// #423 regression: companion for `handle_outgoing_calls` -- see
325    /// `test_handle_incoming_calls_returns_workspace_indexing_error_when_loading`.
326    #[tokio::test(start_paused = true)]
327    async fn test_handle_outgoing_calls_returns_workspace_indexing_error_when_loading() {
328        let dir = TempDir::new().unwrap();
329        let server_id = ServerId::from_static("rust");
330        let caps = lsp_types::ServerCapabilities {
331            call_hierarchy_provider: Some(lsp_types::CallHierarchyProvider::Bool(true)),
332            ..Default::default()
333        };
334        let (translator, _server) = translator_with_capabilities(&dir, &server_id, caps);
335
336        let cache = Arc::new(Mutex::new(NotificationCache::new()));
337        cache.lock().await.observe_indexing_signal(
338            &server_id,
339            "experimental/serverStatus",
340            Some(&serde_json::json!({"quiescent": false})),
341        );
342        let translator = translator.with_notification_cache(cache);
343
344        let path = dir.path().join("queried.rs");
345        fs::write(&path, "fn queried() {}").unwrap();
346        let uri = Url::from_file_path(&path).unwrap().to_string();
347
348        let err = translator
349            .handle_outgoing_calls(checked_hierarchy_item(call_hierarchy_item_json(&uri)))
350            .await
351            .unwrap_err();
352
353        assert_matches!(
354            err,
355            Error::WorkspaceIndexing { server_id: id, .. } if id == server_id
356        );
357    }
358
359    /// S4 lock-in for the documented `Uri`-validation-loss behavior change
360    /// (see the CHANGELOG entry for #297): the call hierarchy item
361    /// can no longer reject a malformed `uri` field at construction time
362    /// (`gen-lsp-types`'s `Uri` has no validating parse). This drives a
363    /// structurally-valid item whose `uri` field is `file://`-prefixed (so
364    /// `parse_file_uri`'s scheme check still passes, same as before the
365    /// migration) but points at a path that does not exist on disk, through
366    /// the real `handle_incoming_calls`/`handle_outgoing_calls` handlers, and
367    /// pins the actual resulting error: `Error::FileIo` from
368    /// `validate_path`'s `canonicalize()` call, not the old construction-time
369    /// `Error::InvalidToolParams`.
370    #[tokio::test]
371    async fn test_handle_incoming_calls_with_nonexistent_file_uri_returns_file_io_not_invalid_uri()
372    {
373        let mut translator = Translator::new();
374        // A workspace root is required so `validate_path` reaches
375        // `canonicalize()` instead of failing closed on `NoWorkspaceRoots`.
376        #[cfg(windows)]
377        translator.set_workspace_roots(
378            WorkspaceRoots::from_paths(&[std::path::PathBuf::from(r"C:\")]).unwrap(),
379        );
380        #[cfg(not(windows))]
381        translator.set_workspace_roots(
382            WorkspaceRoots::from_paths(&[std::path::PathBuf::from("/")]).unwrap(),
383        );
384        // `Url::to_file_path` on Windows requires a drive-letter first path
385        // segment; a Unix-style path with none fails to convert at all
386        // (`uri_to_path` returns `None`, i.e. `Error::InvalidToolParams`)
387        // before ever reaching `canonicalize()`, trivially failing this
388        // assertion for the wrong reason. Use a drive-letter path so the
389        // test exercises the intended `FileIo` path on every platform.
390        #[cfg(windows)]
391        let uri = "file:///C:/this/path/does/not/exist/anywhere.rs";
392        #[cfg(not(windows))]
393        let uri = "file:///this/path/does/not/exist/anywhere.rs";
394        let item = serde_json::json!({
395            "name": "foo",
396            "kind": 12,
397            "uri": uri,
398            "range": {
399                "start": {"line": 1, "character": 1},
400                "end": {"line": 1, "character": 1}
401            },
402            "selectionRange": {
403                "start": {"line": 1, "character": 1},
404                "end": {"line": 1, "character": 1}
405            }
406        });
407
408        let result = translator
409            .handle_incoming_calls(checked_hierarchy_item(item))
410            .await;
411
412        assert_matches!(
413            result,
414            Err(Error::FileIo { .. }),
415            "expected Error::FileIo from the canonicalize() failure now that Uri construction \
416             cannot itself reject a malformed uri, got {result:?}"
417        );
418    }
419
420    /// #749: a hierarchy item whose `uri` is not a `file:` URI is rejected
421    /// with a message that does not grow with the `uri`, and a control
422    /// character in it is escaped.
423    #[tokio::test]
424    async fn test_handle_incoming_calls_bounds_the_echo_of_a_non_file_uri() {
425        let translator = Translator::new();
426        let uri = format!("x\n{}", "Z".repeat(1 << 20));
427        let item = serde_json::json!({
428            "name": "foo",
429            "kind": 12,
430            "uri": uri,
431            "range": {
432                "start": {"line": 1, "character": 1},
433                "end": {"line": 1, "character": 1}
434            },
435            "selectionRange": {
436                "start": {"line": 1, "character": 1},
437                "end": {"line": 1, "character": 1}
438            }
439        });
440
441        let result = translator
442            .handle_incoming_calls(checked_hierarchy_item(item))
443            .await;
444
445        let Err(Error::InvalidToolParams(message)) = result else {
446            panic!("expected InvalidToolParams, got {result:?}");
447        };
448        assert!(
449            message.len() <= 2 * crate::util::MAX_ECHOED_PATH_BYTES,
450            "{}",
451            message.len()
452        );
453        assert!(
454            message.ends_with(crate::util::TRUNCATION_MARKER),
455            "{message:.80}"
456        );
457        assert!(!message.contains('\n'));
458    }
459
460    /// As above, through `handle_outgoing_calls` -- same
461    /// item handling, different caller.
462    #[tokio::test]
463    async fn test_handle_outgoing_calls_with_nonexistent_file_uri_returns_file_io_not_invalid_uri()
464    {
465        let mut translator = Translator::new();
466        // A workspace root is required so `validate_path` reaches
467        // `canonicalize()` instead of failing closed on `NoWorkspaceRoots`.
468        #[cfg(windows)]
469        translator.set_workspace_roots(
470            WorkspaceRoots::from_paths(&[std::path::PathBuf::from(r"C:\")]).unwrap(),
471        );
472        #[cfg(not(windows))]
473        translator.set_workspace_roots(
474            WorkspaceRoots::from_paths(&[std::path::PathBuf::from("/")]).unwrap(),
475        );
476        #[cfg(windows)]
477        let uri = "file:///C:/this/path/does/not/exist/anywhere.rs";
478        #[cfg(not(windows))]
479        let uri = "file:///this/path/does/not/exist/anywhere.rs";
480        let item = serde_json::json!({
481            "name": "foo",
482            "kind": 12,
483            "uri": uri,
484            "range": {
485                "start": {"line": 1, "character": 1},
486                "end": {"line": 1, "character": 1}
487            },
488            "selectionRange": {
489                "start": {"line": 1, "character": 1},
490                "end": {"line": 1, "character": 1}
491            }
492        });
493
494        let result = translator
495            .handle_outgoing_calls(checked_hierarchy_item(item))
496            .await;
497
498        assert_matches!(
499            result,
500            Err(Error::FileIo { .. }),
501            "expected Error::FileIo from the canonicalize() failure now that Uri construction \
502             cannot itself reject a malformed uri, got {result:?}"
503        );
504    }
505
506    #[tokio::test]
507    async fn test_convert_call_hierarchy_item_kind_is_numeric() {
508        let item = lsp_types::CallHierarchyItem {
509            name: "my_fn".to_string(),
510            kind: lsp_types::SymbolKind::Function,
511            tags: None,
512            detail: None,
513            uri: lsp_types::Uri::from("file:///tmp/test.rs"),
514            range: lsp_types::Range {
515                start: lsp_types::Position {
516                    line: 0,
517                    character: 0,
518                },
519                end: lsp_types::Position {
520                    line: 0,
521                    character: 5,
522                },
523            },
524            selection_range: lsp_types::Range {
525                start: lsp_types::Position {
526                    line: 0,
527                    character: 0,
528                },
529                end: lsp_types::Position {
530                    line: 0,
531                    character: 5,
532                },
533            },
534            data: None,
535        };
536        let result = convert_call_hierarchy_item(item, &test_ctx()).await;
537        // SymbolKind::Function is LSP integer 12
538        assert_eq!(result.kind, 12u32);
539        assert_eq!(result.name, "my_fn");
540    }
541
542    /// #467 M2/tester gap 2: pins that `call_hierarchy_item_to_lsp`'s reverse
543    /// `u32 -> SymbolKind` conversion round-trips a non-`Function` kind
544    /// through to the outbound wire request unchanged -- not the
545    /// `unwrap_or(Function)` fallback it used to fabricate on a conversion
546    /// failure that could not actually occur.
547    #[tokio::test]
548    async fn test_handle_incoming_calls_round_trips_non_function_kind() {
549        let dir = TempDir::new().unwrap();
550        let server_id = ServerId::from_static("rust");
551        let caps = lsp_types::ServerCapabilities {
552            call_hierarchy_provider: Some(lsp_types::CallHierarchyProvider::Bool(true)),
553            ..Default::default()
554        };
555        let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
556
557        let queried_path = dir.path().join("queried.rs");
558        fs::write(&queried_path, "fn queried() {}").unwrap();
559        let queried_uri = Url::from_file_path(&queried_path).unwrap().to_string();
560
561        let item = HierarchyItem {
562            name: "queried_method".to_string(),
563            kind: 6, // SymbolKind::Method
564            detail: None,
565            uri: queried_uri,
566            range: Range {
567                start: Position2D {
568                    line: 1,
569                    character: 1,
570                },
571                end: Position2D {
572                    line: 1,
573                    character: 4,
574                },
575            },
576            selection_range: Range {
577                start: Position2D {
578                    line: 1,
579                    character: 1,
580                },
581                end: Position2D {
582                    line: 1,
583                    character: 4,
584                },
585            },
586            data: None,
587            out_of_workspace: false,
588        };
589
590        let translator = Arc::new(translator);
591        let handle = {
592            let translator = Arc::clone(&translator);
593            let item = serde_json::to_value(item).unwrap();
594            tokio::spawn(async move {
595                translator
596                    .handle_incoming_calls(checked_hierarchy_item(item))
597                    .await
598            })
599        };
600
601        let mut wire = BufReader::new(&mut server.write_stdout);
602        let opened = read_framed_message(&mut wire).await;
603        assert_eq!(opened["method"], "textDocument/didOpen");
604
605        let request = read_framed_message(&mut wire).await;
606        assert_eq!(request["method"], "callHierarchy/incomingCalls");
607        assert_eq!(
608            request["params"]["item"]["kind"], 6,
609            "the reverse u32 -> SymbolKind conversion must round-trip a non-Function kind \
610             exactly, not fall back to Function (12)"
611        );
612
613        write_response(
614            &mut server.read_half_stdin,
615            &request["id"],
616            serde_json::json!([]),
617        )
618        .await;
619
620        let result = timeout(Duration::from_secs(2), handle)
621            .await
622            .expect("handler call should not hang")
623            .unwrap()
624            .unwrap();
625        assert!(result.calls.is_empty());
626    }
627
628    /// Per the LSP spec, an incoming call's `fromRanges` are ranges within
629    /// the *caller's* document (`call.from.uri`), not the queried item's
630    /// document -- `handle_incoming_calls` must convert them against
631    /// `caller.rs`'s own content, not `queried.rs`'s. Uses a UTF-8-negotiated
632    /// server and two files with different multibyte content, so converting
633    /// against the wrong file's line text produces a different, wrong
634    /// answer: `"aöb"` (caller) puts LSP byte offset 3 at UTF-16 column 3
635    /// (`ö` is 2 UTF-8 bytes / 1 UTF-16 unit), while the ASCII `"abc"`
636    /// (queried item) would put the same byte offset at column 4.
637    #[tokio::test]
638    async fn test_handle_incoming_calls_from_ranges_convert_against_callers_own_uri() {
639        let dir = TempDir::new().unwrap();
640        let server_id = ServerId::from_static("rust");
641        let caps = lsp_types::ServerCapabilities {
642            call_hierarchy_provider: Some(lsp_types::CallHierarchyProvider::Bool(true)),
643            ..Default::default()
644        };
645        let (translator, mut server) = translator_with_capabilities_and_encoding(
646            &dir,
647            &server_id,
648            caps,
649            lsp_types::PositionEncodingKind::UTF8,
650        );
651
652        let queried_path = canonical_dir(&dir).join("queried.rs");
653        fs::write(&queried_path, "abc").unwrap();
654        let queried_uri = Url::from_file_path(&queried_path).unwrap().to_string();
655
656        let caller_path = canonical_dir(&dir).join("caller.rs");
657        fs::write(&caller_path, "aöb").unwrap();
658        let caller_uri = Url::from_file_path(&caller_path).unwrap().to_string();
659
660        let item = HierarchyItem {
661            name: "queried_fn".to_string(),
662            kind: 12,
663            detail: None,
664            uri: queried_uri,
665            range: Range {
666                start: Position2D {
667                    line: 1,
668                    character: 1,
669                },
670                end: Position2D {
671                    line: 1,
672                    character: 4,
673                },
674            },
675            selection_range: Range {
676                start: Position2D {
677                    line: 1,
678                    character: 1,
679                },
680                end: Position2D {
681                    line: 1,
682                    character: 4,
683                },
684            },
685            data: None,
686            out_of_workspace: false,
687        };
688
689        let translator = Arc::new(translator);
690        let handle = {
691            let translator = Arc::clone(&translator);
692            let item = checked_hierarchy_item(serde_json::to_value(item).unwrap());
693            tokio::spawn(async move { translator.handle_incoming_calls(item).await })
694        };
695
696        let mut wire = BufReader::new(&mut server.write_stdout);
697        let opened = read_framed_message(&mut wire).await;
698        assert_eq!(opened["method"], "textDocument/didOpen");
699
700        let request = read_framed_message(&mut wire).await;
701        assert_eq!(request["method"], "callHierarchy/incomingCalls");
702
703        write_response(
704            &mut server.read_half_stdin,
705            &request["id"],
706            serde_json::json!([{
707                "from": {
708                    "name": "caller_fn",
709                    "kind": 12,
710                    "uri": caller_uri,
711                    "range": {
712                        "start": {"line": 0, "character": 0},
713                        "end": {"line": 0, "character": 1}
714                    },
715                    "selectionRange": {
716                        "start": {"line": 0, "character": 0},
717                        "end": {"line": 0, "character": 1}
718                    }
719                },
720                "fromRanges": [{
721                    "start": {"line": 0, "character": 0},
722                    "end": {"line": 0, "character": 3}
723                }]
724            }]),
725        )
726        .await;
727
728        let result = timeout(Duration::from_secs(2), handle)
729            .await
730            .expect("handler call should not hang")
731            .unwrap()
732            .unwrap();
733
734        assert_eq!(result.calls.len(), 1);
735        let from_range = &result.calls[0].from_ranges[0];
736        assert_eq!(
737            from_range.end.character, 3,
738            "fromRanges must convert against the caller's own file (\"aöb\"), not the queried \
739             item's (\"abc\") -- a byte offset of 3 is UTF-16 column 3 in the former, 4 in the \
740             latter"
741        );
742        assert!(
743            !result.calls[0].from.out_of_workspace,
744            "an in-workspace caller must not be marked out_of_workspace"
745        );
746    }
747
748    /// Per the LSP spec, an outgoing call's `fromRanges` are ranges within
749    /// the *queried* item's own document, not the callee's (`call.to.uri`) --
750    /// the inverse directional convention from incoming calls, tested above.
751    #[tokio::test]
752    async fn test_handle_outgoing_calls_from_ranges_convert_against_queried_uri() {
753        let dir = TempDir::new().unwrap();
754        let server_id = ServerId::from_static("rust");
755        let caps = lsp_types::ServerCapabilities {
756            call_hierarchy_provider: Some(lsp_types::CallHierarchyProvider::Bool(true)),
757            ..Default::default()
758        };
759        let (translator, mut server) = translator_with_capabilities_and_encoding(
760            &dir,
761            &server_id,
762            caps,
763            lsp_types::PositionEncodingKind::UTF8,
764        );
765
766        let queried_path = dir.path().join("queried.rs");
767        fs::write(&queried_path, "aöb").unwrap();
768        let queried_uri = Url::from_file_path(&queried_path).unwrap().to_string();
769
770        let callee_path = dir.path().join("callee.rs");
771        fs::write(&callee_path, "abc").unwrap();
772        let callee_uri = Url::from_file_path(&callee_path).unwrap().to_string();
773
774        let item = HierarchyItem {
775            name: "queried_fn".to_string(),
776            kind: 12,
777            detail: None,
778            uri: queried_uri,
779            range: Range {
780                start: Position2D {
781                    line: 1,
782                    character: 1,
783                },
784                end: Position2D {
785                    line: 1,
786                    character: 4,
787                },
788            },
789            selection_range: Range {
790                start: Position2D {
791                    line: 1,
792                    character: 1,
793                },
794                end: Position2D {
795                    line: 1,
796                    character: 4,
797                },
798            },
799            data: None,
800            out_of_workspace: false,
801        };
802
803        let translator = Arc::new(translator);
804        let handle = {
805            let translator = Arc::clone(&translator);
806            let item = serde_json::to_value(item).unwrap();
807            tokio::spawn(async move {
808                translator
809                    .handle_outgoing_calls(checked_hierarchy_item(item))
810                    .await
811            })
812        };
813
814        let mut wire = BufReader::new(&mut server.write_stdout);
815        let opened = read_framed_message(&mut wire).await;
816        assert_eq!(opened["method"], "textDocument/didOpen");
817
818        let request = read_framed_message(&mut wire).await;
819        assert_eq!(request["method"], "callHierarchy/outgoingCalls");
820
821        write_response(
822            &mut server.read_half_stdin,
823            &request["id"],
824            serde_json::json!([{
825                "to": {
826                    "name": "callee_fn",
827                    "kind": 12,
828                    "uri": callee_uri,
829                    "range": {
830                        "start": {"line": 0, "character": 0},
831                        "end": {"line": 0, "character": 1}
832                    },
833                    "selectionRange": {
834                        "start": {"line": 0, "character": 0},
835                        "end": {"line": 0, "character": 1}
836                    }
837                },
838                "fromRanges": [{
839                    "start": {"line": 0, "character": 0},
840                    "end": {"line": 0, "character": 3}
841                }]
842            }]),
843        )
844        .await;
845
846        let result = timeout(Duration::from_secs(2), handle)
847            .await
848            .expect("handler call should not hang")
849            .unwrap()
850            .unwrap();
851
852        assert_eq!(result.calls.len(), 1);
853        let from_range = &result.calls[0].from_ranges[0];
854        assert_eq!(
855            from_range.end.character, 3,
856            "fromRanges must convert against the queried item's own file (\"aöb\"), not the \
857             callee's (\"abc\") -- a byte offset of 3 is UTF-16 column 3 in the former, 4 in \
858             the latter"
859        );
860    }
861
862    /// #415 (revised per critic C1): an incoming call whose caller
863    /// (`call.from.uri`) lies outside every configured workspace root must
864    /// still be returned -- a caller in the standard library or a
865    /// crates.io dependency is normal, expected call-hierarchy navigation,
866    /// not an attack. See `navigation.rs`'s
867    /// `test_handle_definition_does_not_filter_out_of_workspace_location`
868    /// for the same policy on goto-X locations.
869    #[tokio::test]
870    async fn test_handle_incoming_calls_does_not_filter_out_of_workspace_caller() {
871        let dir = TempDir::new().unwrap();
872        let server_id = ServerId::from_static("rust");
873        let caps = lsp_types::ServerCapabilities {
874            call_hierarchy_provider: Some(lsp_types::CallHierarchyProvider::Bool(true)),
875            ..Default::default()
876        };
877        let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
878
879        let queried_path = dir.path().join("queried.rs");
880        fs::write(&queried_path, "fn queried() {}").unwrap();
881        let queried_uri = Url::from_file_path(&queried_path).unwrap().to_string();
882        let outside_uri = "file:///outside/workspace/stdlib.rs";
883
884        let item = HierarchyItem {
885            name: "queried_fn".to_string(),
886            kind: 12,
887            detail: None,
888            uri: queried_uri,
889            range: Range {
890                start: Position2D {
891                    line: 1,
892                    character: 1,
893                },
894                end: Position2D {
895                    line: 1,
896                    character: 4,
897                },
898            },
899            selection_range: Range {
900                start: Position2D {
901                    line: 1,
902                    character: 1,
903                },
904                end: Position2D {
905                    line: 1,
906                    character: 4,
907                },
908            },
909            data: None,
910            out_of_workspace: false,
911        };
912
913        let translator = Arc::new(translator);
914        let handle = {
915            let translator = Arc::clone(&translator);
916            let item = serde_json::to_value(item).unwrap();
917            tokio::spawn(async move {
918                translator
919                    .handle_incoming_calls(checked_hierarchy_item(item))
920                    .await
921            })
922        };
923
924        let mut wire = BufReader::new(&mut server.write_stdout);
925        let opened = read_framed_message(&mut wire).await;
926        assert_eq!(opened["method"], "textDocument/didOpen");
927
928        let request = read_framed_message(&mut wire).await;
929        assert_eq!(request["method"], "callHierarchy/incomingCalls");
930
931        write_response(
932            &mut server.read_half_stdin,
933            &request["id"],
934            serde_json::json!([{
935                "from": {
936                    "name": "caller_fn",
937                    "kind": 12,
938                    "uri": outside_uri,
939                    "range": {
940                        "start": {"line": 0, "character": 0},
941                        "end": {"line": 0, "character": 1}
942                    },
943                    "selectionRange": {
944                        "start": {"line": 0, "character": 0},
945                        "end": {"line": 0, "character": 1}
946                    }
947                },
948                "fromRanges": [{
949                    "start": {"line": 0, "character": 0},
950                    "end": {"line": 0, "character": 1}
951                }]
952            }]),
953        )
954        .await;
955
956        let result = timeout(Duration::from_secs(2), handle)
957            .await
958            .expect("handler call should not hang")
959            .unwrap()
960            .unwrap();
961
962        assert_eq!(
963            result.calls.len(),
964            1,
965            "an out-of-workspace caller must be returned, not dropped"
966        );
967        assert_eq!(result.calls[0].from.uri, outside_uri);
968        assert!(
969            result.calls[0].from.out_of_workspace,
970            "an out-of-workspace caller must be marked out_of_workspace"
971        );
972    }
973
974    /// #415 (revised per critic C1) companion for outgoing calls: a callee
975    /// (`call.to.uri`) outside every configured workspace root must still be
976    /// returned -- see the incoming-calls test above for the rationale.
977    #[tokio::test]
978    async fn test_handle_outgoing_calls_does_not_filter_out_of_workspace_callee() {
979        let dir = TempDir::new().unwrap();
980        let server_id = ServerId::from_static("rust");
981        let caps = lsp_types::ServerCapabilities {
982            call_hierarchy_provider: Some(lsp_types::CallHierarchyProvider::Bool(true)),
983            ..Default::default()
984        };
985        let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
986
987        let queried_path = dir.path().join("queried.rs");
988        fs::write(&queried_path, "fn queried() {}").unwrap();
989        let queried_uri = Url::from_file_path(&queried_path).unwrap().to_string();
990        let outside_uri = "file:///outside/workspace/stdlib.rs";
991
992        let item = HierarchyItem {
993            name: "queried_fn".to_string(),
994            kind: 12,
995            detail: None,
996            uri: queried_uri,
997            range: Range {
998                start: Position2D {
999                    line: 1,
1000                    character: 1,
1001                },
1002                end: Position2D {
1003                    line: 1,
1004                    character: 4,
1005                },
1006            },
1007            selection_range: Range {
1008                start: Position2D {
1009                    line: 1,
1010                    character: 1,
1011                },
1012                end: Position2D {
1013                    line: 1,
1014                    character: 4,
1015                },
1016            },
1017            data: None,
1018            out_of_workspace: false,
1019        };
1020
1021        let translator = Arc::new(translator);
1022        let handle = {
1023            let translator = Arc::clone(&translator);
1024            let item = serde_json::to_value(item).unwrap();
1025            tokio::spawn(async move {
1026                translator
1027                    .handle_outgoing_calls(checked_hierarchy_item(item))
1028                    .await
1029            })
1030        };
1031
1032        let mut wire = BufReader::new(&mut server.write_stdout);
1033        let opened = read_framed_message(&mut wire).await;
1034        assert_eq!(opened["method"], "textDocument/didOpen");
1035
1036        let request = read_framed_message(&mut wire).await;
1037        assert_eq!(request["method"], "callHierarchy/outgoingCalls");
1038
1039        write_response(
1040            &mut server.read_half_stdin,
1041            &request["id"],
1042            serde_json::json!([{
1043                "to": {
1044                    "name": "callee_fn",
1045                    "kind": 12,
1046                    "uri": outside_uri,
1047                    "range": {
1048                        "start": {"line": 0, "character": 0},
1049                        "end": {"line": 0, "character": 1}
1050                    },
1051                    "selectionRange": {
1052                        "start": {"line": 0, "character": 0},
1053                        "end": {"line": 0, "character": 1}
1054                    }
1055                },
1056                "fromRanges": [{
1057                    "start": {"line": 0, "character": 0},
1058                    "end": {"line": 0, "character": 1}
1059                }]
1060            }]),
1061        )
1062        .await;
1063
1064        let result = timeout(Duration::from_secs(2), handle)
1065            .await
1066            .expect("handler call should not hang")
1067            .unwrap()
1068            .unwrap();
1069
1070        assert_eq!(
1071            result.calls.len(),
1072            1,
1073            "an out-of-workspace callee must be returned, not dropped"
1074        );
1075        assert_eq!(result.calls[0].to.uri, outside_uri);
1076        assert!(
1077            result.calls[0].to.out_of_workspace,
1078            "an out-of-workspace callee must be marked out_of_workspace"
1079        );
1080    }
1081
1082    /// #411 regression: `prepare_call_hierarchy` -> `get_incoming_calls`
1083    /// round trip must resolve a percent-encoded URI (space, non-ASCII)
1084    /// back to the file it names. Before the fix, `parse_file_uri`
1085    /// raw-sliced the URI instead of decoding it, so `canonicalize()`
1086    /// failed with `ENOENT` even though the file exists.
1087    #[tokio::test]
1088    async fn test_prepare_then_incoming_calls_round_trip_percent_encoded_path() {
1089        let dir = TempDir::new().unwrap();
1090        let server_id = ServerId::from_static("rust");
1091        let caps = lsp_types::ServerCapabilities {
1092            call_hierarchy_provider: Some(lsp_types::CallHierarchyProvider::Bool(true)),
1093            ..Default::default()
1094        };
1095        let (translator, mut server) = translator_with_capabilities_and_encoding(
1096            &dir,
1097            &server_id,
1098            caps,
1099            lsp_types::PositionEncodingKind::UTF8,
1100        );
1101
1102        let file_path = dir.path().join("my file café.rs");
1103        fs::write(&file_path, "fn foo() {}").unwrap();
1104        let file_uri = Url::from_file_path(&file_path).unwrap().to_string();
1105        assert!(
1106            file_uri.contains("%20"),
1107            "test fixture must exercise percent-encoding"
1108        );
1109
1110        let translator = Arc::new(translator);
1111        let mut wire = BufReader::new(&mut server.write_stdout);
1112
1113        let prepare_handle = {
1114            let translator = Arc::clone(&translator);
1115            let path = file_path.to_string_lossy().into_owned();
1116            tokio::spawn(async move {
1117                translator
1118                    .handle_call_hierarchy_prepare(client_path(path), pos(1, 1))
1119                    .await
1120            })
1121        };
1122
1123        let opened = read_framed_message(&mut wire).await;
1124        assert_eq!(opened["method"], "textDocument/didOpen");
1125        let request = read_framed_message(&mut wire).await;
1126        assert_eq!(request["method"], "textDocument/prepareCallHierarchy");
1127
1128        write_response(
1129            &mut server.read_half_stdin,
1130            &request["id"],
1131            serde_json::json!([{
1132                "name": "foo",
1133                "kind": 12,
1134                "uri": file_uri,
1135                "range": {
1136                    "start": {"line": 0, "character": 0},
1137                    "end": {"line": 0, "character": 11}
1138                },
1139                "selectionRange": {
1140                    "start": {"line": 0, "character": 3},
1141                    "end": {"line": 0, "character": 6}
1142                }
1143            }]),
1144        )
1145        .await;
1146
1147        let prepare_result = timeout(Duration::from_secs(2), prepare_handle)
1148            .await
1149            .expect("prepare should not hang")
1150            .unwrap()
1151            .unwrap()
1152            .result;
1153        assert_eq!(prepare_result.items.len(), 1);
1154        let item = prepare_result.items[0].clone();
1155        assert!(item.uri.contains("%20"));
1156
1157        let incoming_handle = {
1158            let translator = Arc::clone(&translator);
1159            let item = serde_json::to_value(item).unwrap();
1160            tokio::spawn(async move {
1161                translator
1162                    .handle_incoming_calls(checked_hierarchy_item(item))
1163                    .await
1164            })
1165        };
1166
1167        let request = read_framed_message(&mut wire).await;
1168        assert_eq!(request["method"], "callHierarchy/incomingCalls");
1169
1170        write_response(
1171            &mut server.read_half_stdin,
1172            &request["id"],
1173            serde_json::json!([]),
1174        )
1175        .await;
1176
1177        let incoming_result = timeout(Duration::from_secs(2), incoming_handle)
1178            .await
1179            .expect("incoming calls should not hang")
1180            .unwrap();
1181
1182        assert!(
1183            incoming_result.is_ok(),
1184            "expected success resolving the percent-encoded path, got {incoming_result:?}"
1185        );
1186    }
1187
1188    #[derive(Clone, Copy)]
1189    enum Direction {
1190        Incoming,
1191        Outgoing,
1192    }
1193
1194    impl Direction {
1195        const fn lsp_method(self) -> &'static str {
1196            match self {
1197                Self::Incoming => "callHierarchy/incomingCalls",
1198                Self::Outgoing => "callHierarchy/outgoingCalls",
1199            }
1200        }
1201
1202        /// Name of the call's item field in the LSP response (`from`/`to`).
1203        const fn item_field(self) -> &'static str {
1204            match self {
1205                Self::Incoming => "from",
1206                Self::Outgoing => "to",
1207            }
1208        }
1209    }
1210
1211    /// Drives `handle_incoming_calls`/`handle_outgoing_calls` against a
1212    /// server answering with `response`, returning the JSON-serialized result.
1213    async fn calls_with_response(
1214        direction: Direction,
1215        response: impl FnOnce(&str) -> serde_json::Value,
1216    ) -> serde_json::Value {
1217        let dir = TempDir::new().unwrap();
1218        let server_id = ServerId::from_static("rust");
1219        let caps = lsp_types::ServerCapabilities {
1220            call_hierarchy_provider: Some(lsp_types::CallHierarchyProvider::Bool(true)),
1221            ..Default::default()
1222        };
1223        let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
1224
1225        let queried_path = dir.path().join("queried.rs");
1226        fs::write(&queried_path, "fn queried() {}").unwrap();
1227        let queried_uri = Url::from_file_path(&queried_path).unwrap().to_string();
1228        let item = call_hierarchy_item_json(&queried_uri);
1229
1230        let translator = Arc::new(translator);
1231        let handle = {
1232            let translator = Arc::clone(&translator);
1233            tokio::spawn(async move {
1234                match direction {
1235                    Direction::Incoming => serde_json::to_value(
1236                        translator
1237                            .handle_incoming_calls(checked_hierarchy_item(item))
1238                            .await
1239                            .unwrap(),
1240                    ),
1241                    Direction::Outgoing => serde_json::to_value(
1242                        translator
1243                            .handle_outgoing_calls(checked_hierarchy_item(item))
1244                            .await
1245                            .unwrap(),
1246                    ),
1247                }
1248                .unwrap()
1249            })
1250        };
1251
1252        let mut wire = BufReader::new(&mut server.write_stdout);
1253        let opened = read_framed_message(&mut wire).await;
1254        assert_eq!(opened["method"], "textDocument/didOpen");
1255        let request = read_framed_message(&mut wire).await;
1256        assert_eq!(request["method"], direction.lsp_method());
1257
1258        write_response(
1259            &mut server.read_half_stdin,
1260            &request["id"],
1261            response(&queried_uri),
1262        )
1263        .await;
1264
1265        timeout(Duration::from_secs(30), handle)
1266            .await
1267            .expect("handler call should not hang")
1268            .unwrap()
1269    }
1270
1271    fn hierarchy_call_json(uri: &str, direction: Direction, ranges: usize) -> serde_json::Value {
1272        let range = serde_json::json!({
1273            "start": {"line": 0, "character": 0},
1274            "end": {"line": 0, "character": 1}
1275        });
1276        serde_json::json!({
1277            direction.item_field(): {
1278                "name": "f", "kind": 12, "uri": uri,
1279                "range": range, "selectionRange": range
1280            },
1281            "fromRanges": vec![range; ranges]
1282        })
1283    }
1284
1285    fn range_count(call: &serde_json::Value) -> usize {
1286        call["from_ranges"].as_array().unwrap().len()
1287    }
1288
1289    /// #487: a call and its `fromRanges` draw on one budget; a call that does
1290    /// not fit with all its ranges is dropped whole, never returned with
1291    /// fewer (or no) ranges.
1292    #[tokio::test]
1293    async fn test_handle_incoming_calls_drops_call_whole_when_ranges_exceed_budget() {
1294        use crate::bridge::translator::navigation::MAX_NORMALIZED_LOCATIONS;
1295
1296        let result = calls_with_response(Direction::Incoming, |uri| {
1297            serde_json::json!([hierarchy_call_json(
1298                uri,
1299                Direction::Incoming,
1300                MAX_NORMALIZED_LOCATIONS
1301            )])
1302        })
1303        .await;
1304
1305        assert_eq!(result["truncated"], true);
1306        assert_eq!(result["calls"].as_array().unwrap().len(), 0);
1307    }
1308
1309    /// #487 I1: when several calls jointly exceed the cap, every returned call
1310    /// still carries all its ranges and the rest are dropped.
1311    #[tokio::test]
1312    async fn test_handle_incoming_calls_never_returns_call_with_ranges_cut() {
1313        use crate::bridge::translator::navigation::MAX_NORMALIZED_LOCATIONS;
1314
1315        let ranges_per_call = MAX_NORMALIZED_LOCATIONS / 2;
1316        let result = calls_with_response(Direction::Incoming, |uri| {
1317            let call = hierarchy_call_json(uri, Direction::Incoming, ranges_per_call);
1318            serde_json::Value::Array(vec![call; 3])
1319        })
1320        .await;
1321
1322        assert_eq!(result["truncated"], true);
1323        let calls = result["calls"].as_array().unwrap();
1324        assert_eq!(calls.len(), 1);
1325        assert_eq!(range_count(&calls[0]), ranges_per_call);
1326    }
1327
1328    /// #487: one call plus `MAX_NORMALIZED_LOCATIONS - 1` ranges is exactly
1329    /// the cap and must not be flagged.
1330    #[tokio::test]
1331    async fn test_handle_incoming_calls_exactly_at_cap_is_not_truncated() {
1332        use crate::bridge::translator::navigation::MAX_NORMALIZED_LOCATIONS;
1333
1334        let result = calls_with_response(Direction::Incoming, |uri| {
1335            serde_json::json!([hierarchy_call_json(
1336                uri,
1337                Direction::Incoming,
1338                MAX_NORMALIZED_LOCATIONS - 1
1339            )])
1340        })
1341        .await;
1342
1343        assert!(result.get("truncated").is_none());
1344        assert_eq!(
1345            range_count(&result["calls"][0]),
1346            MAX_NORMALIZED_LOCATIONS - 1
1347        );
1348    }
1349
1350    /// #487: outgoing calls beyond the cap are dropped and reported.
1351    #[tokio::test]
1352    async fn test_handle_outgoing_calls_caps_item_count_and_reports_truncation() {
1353        use crate::bridge::translator::navigation::MAX_NORMALIZED_LOCATIONS;
1354
1355        let result = calls_with_response(Direction::Outgoing, |uri| {
1356            let call = hierarchy_call_json(uri, Direction::Outgoing, 0);
1357            serde_json::Value::Array(vec![call; MAX_NORMALIZED_LOCATIONS + 500])
1358        })
1359        .await;
1360
1361        assert_eq!(result["truncated"], true);
1362        assert_eq!(
1363            result["calls"].as_array().unwrap().len(),
1364            MAX_NORMALIZED_LOCATIONS
1365        );
1366    }
1367
1368    /// #487: a response within the cap is not flagged.
1369    #[tokio::test]
1370    async fn test_handle_incoming_calls_within_cap_is_not_truncated() {
1371        let result = calls_with_response(Direction::Incoming, |uri| {
1372            serde_json::json!([hierarchy_call_json(uri, Direction::Incoming, 0)])
1373        })
1374        .await;
1375
1376        assert!(result.get("truncated").is_none());
1377    }
1378
1379    /// #516: a prepare response past `MAX_NORMALIZED_LOCATIONS` is cut and
1380    /// flagged, one within it is not.
1381    #[tokio::test]
1382    async fn test_handle_call_hierarchy_prepare_caps_items_and_reports_truncation() {
1383        use crate::bridge::translator::navigation::MAX_NORMALIZED_LOCATIONS;
1384
1385        for (count, truncated) in [
1386            (MAX_NORMALIZED_LOCATIONS + 10, true),
1387            (MAX_NORMALIZED_LOCATIONS, false),
1388        ] {
1389            let dir = TempDir::new().unwrap();
1390            let server_id = ServerId::from_static("rust");
1391            let caps = lsp_types::ServerCapabilities {
1392                call_hierarchy_provider: Some(lsp_types::CallHierarchyProvider::Bool(true)),
1393                ..Default::default()
1394            };
1395            let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
1396            let path = dir.path().join("queried.rs");
1397            fs::write(&path, "fn queried() {}").unwrap();
1398            let uri = Url::from_file_path(&path).unwrap().to_string();
1399
1400            let translator = Arc::new(translator);
1401            let handle = {
1402                let translator = Arc::clone(&translator);
1403                let path = path.to_string_lossy().into_owned();
1404                tokio::spawn(async move {
1405                    translator
1406                        .handle_call_hierarchy_prepare(client_path(path), pos(1, 1))
1407                        .await
1408                })
1409            };
1410
1411            let mut wire = BufReader::new(&mut server.write_stdout);
1412            let opened = read_framed_message(&mut wire).await;
1413            assert_eq!(opened["method"], "textDocument/didOpen");
1414            let request = read_framed_message(&mut wire).await;
1415            assert_eq!(request["method"], "textDocument/prepareCallHierarchy");
1416
1417            let range = serde_json::json!({
1418                "start": {"line": 0, "character": 0},
1419                "end": {"line": 0, "character": 1}
1420            });
1421            let item = serde_json::json!({
1422                "name": "f", "kind": 12, "uri": uri,
1423                "range": range, "selectionRange": range
1424            });
1425            write_response(
1426                &mut server.read_half_stdin,
1427                &request["id"],
1428                serde_json::Value::Array(vec![item; count]),
1429            )
1430            .await;
1431
1432            let result = timeout(Duration::from_secs(30), handle)
1433                .await
1434                .expect("handler call should not hang")
1435                .unwrap()
1436                .unwrap()
1437                .result;
1438            assert_eq!(result.items.len(), MAX_NORMALIZED_LOCATIONS);
1439            assert_eq!(result.truncated, truncated);
1440            let wire = serde_json::to_value(&result).unwrap();
1441            assert_eq!(wire.get("truncated").is_some(), truncated);
1442        }
1443    }
1444}