use lsp_types::CallHierarchyItem;
use super::Translator;
use super::dto::{
CallHierarchyPrepareResult, CheckedHierarchyItem, HierarchyItem, IncomingCall,
IncomingCallsResult, OutgoingCall, OutgoingCallsResult, Position, PositionDegradation, Range,
};
use super::encoding_ctx::EncodingCtx;
use super::hierarchy::hierarchy_item_to_mcp;
use super::navigation::ItemBudget;
use super::positioned::Positioned;
use super::routing::PreparedDocument;
use crate::bridge::{ClientPath, Indexed};
use crate::error::Result;
async fn convert_call_hierarchy_item(item: CallHierarchyItem, ctx: &EncodingCtx) -> HierarchyItem {
hierarchy_item_to_mcp(item, ctx).await
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CallDirection {
Incoming,
Outgoing,
}
struct WalkedCall {
peer: HierarchyItem,
from_ranges: Vec<Range>,
}
struct CallWalk {
calls: Vec<WalkedCall>,
truncated: bool,
positions_degraded: Option<PositionDegradation>,
}
struct RawCall {
peer: CallHierarchyItem,
from_ranges: Vec<lsp_types::Range>,
ranges_uri: lsp_types::Uri,
}
impl Translator {
pub async fn handle_call_hierarchy_prepare(
&self,
file_path: ClientPath,
position: Position,
) -> Result<Indexed<CallHierarchyPrepareResult>> {
let Positioned {
result:
Indexed {
result: response,
indexing,
},
ctx,
doc: _doc,
} = self
.disclosed_position_request::<lsp_types::CallHierarchyPrepareRequest>(
&file_path,
position,
(),
)
.await?;
let mut budget = ItemBudget::new();
let lsp_items = budget.admit(response.unwrap_or_default());
let mut items = Vec::with_capacity(lsp_items.len());
for item in lsp_items {
items.push(convert_call_hierarchy_item(item, &ctx).await);
}
Ok(Indexed::new(
CallHierarchyPrepareResult {
items,
truncated: budget.truncated(),
positions_degraded: ctx.positions_degraded(),
},
indexing,
))
}
pub async fn handle_incoming_calls(
&self,
item: CheckedHierarchyItem,
) -> Result<IncomingCallsResult> {
let walk = self.walk_calls(item, CallDirection::Incoming).await?;
Ok(IncomingCallsResult {
calls: walk
.calls
.into_iter()
.map(|call| IncomingCall {
from: call.peer,
from_ranges: call.from_ranges,
})
.collect(),
truncated: walk.truncated,
positions_degraded: walk.positions_degraded,
})
}
pub async fn handle_outgoing_calls(
&self,
item: CheckedHierarchyItem,
) -> Result<OutgoingCallsResult> {
let walk = self.walk_calls(item, CallDirection::Outgoing).await?;
Ok(OutgoingCallsResult {
calls: walk
.calls
.into_iter()
.map(|call| OutgoingCall {
to: call.peer,
from_ranges: call.from_ranges,
})
.collect(),
truncated: walk.truncated,
positions_degraded: walk.positions_degraded,
})
}
async fn walk_calls(
&self,
item: CheckedHierarchyItem,
direction: CallDirection,
) -> Result<CallWalk> {
let (raw, ctx, _doc): (Vec<RawCall>, EncodingCtx, PreparedDocument) = match direction {
CallDirection::Incoming => {
let Positioned { result, ctx, doc } = self
.item_request::<lsp_types::CallHierarchyIncomingCallsRequest>(item)
.await?;
let raw = result
.unwrap_or_default()
.into_iter()
.map(|call| RawCall {
ranges_uri: call.from.uri.clone(),
peer: call.from,
from_ranges: call.from_ranges,
})
.collect();
(raw, ctx, doc)
}
CallDirection::Outgoing => {
let Positioned { result, ctx, doc } = self
.item_request::<lsp_types::CallHierarchyOutgoingCallsRequest>(item)
.await?;
let source_uri = doc.uri().clone();
let raw = result
.unwrap_or_default()
.into_iter()
.map(|call| RawCall {
ranges_uri: source_uri.clone(),
peer: call.to,
from_ranges: call.from_ranges,
})
.collect();
(raw, ctx, doc)
}
};
let mut budget = ItemBudget::new();
let mut calls = Vec::new();
for call in raw {
if !budget.spend_whole(call.from_ranges.len().saturating_add(1)) {
continue;
}
let mut from_ranges = Vec::with_capacity(call.from_ranges.len());
for range in call.from_ranges {
from_ranges.push(ctx.normalize_range(&call.ranges_uri, range).await);
}
calls.push(WalkedCall {
peer: convert_call_hierarchy_item(call.peer, &ctx).await,
from_ranges,
});
}
Ok(CallWalk {
calls,
truncated: budget.truncated(),
positions_degraded: ctx.positions_degraded(),
})
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use std::time::Duration;
use std::{assert_matches, fs};
use tempfile::TempDir;
use tokio::io::BufReader;
use tokio::sync::Mutex;
use tokio::time::timeout;
use url::Url;
use super::*;
use crate::bridge::translator::dto::{Position2D, Range};
use crate::bridge::translator::testing::*;
use crate::bridge::{NotificationCache, WorkspaceRoots};
use crate::config::ServerId;
use crate::error::Error;
use crate::test_lsp::client_path;
fn call_hierarchy_item_json(uri: &str) -> serde_json::Value {
serde_json::to_value(HierarchyItem {
name: "queried_fn".to_string(),
kind: 12,
detail: None,
uri: uri.to_string(),
range: Range {
start: Position2D {
line: 1,
character: 1,
},
end: Position2D {
line: 1,
character: 4,
},
},
selection_range: Range {
start: Position2D {
line: 1,
character: 1,
},
end: Position2D {
line: 1,
character: 4,
},
},
data: None,
out_of_workspace: false,
})
.unwrap()
}
#[tokio::test(start_paused = true)]
async fn test_handle_incoming_calls_returns_workspace_indexing_error_when_loading() {
let dir = TempDir::new().unwrap();
let server_id = ServerId::from_static("rust");
let caps = lsp_types::ServerCapabilities {
call_hierarchy_provider: Some(lsp_types::CallHierarchyProvider::Bool(true)),
..Default::default()
};
let (translator, _server) = translator_with_capabilities(&dir, &server_id, caps);
let cache = Arc::new(Mutex::new(NotificationCache::new()));
cache.lock().await.observe_indexing_signal(
&server_id,
"experimental/serverStatus",
Some(&serde_json::json!({"quiescent": false})),
);
let translator = translator.with_notification_cache(cache);
let path = dir.path().join("queried.rs");
fs::write(&path, "fn queried() {}").unwrap();
let uri = Url::from_file_path(&path).unwrap().to_string();
let err = translator
.handle_incoming_calls(checked_hierarchy_item(call_hierarchy_item_json(&uri)))
.await
.unwrap_err();
assert_matches!(
err,
Error::WorkspaceIndexing { server_id: id, .. } if id == server_id
);
}
#[tokio::test(start_paused = true)]
async fn test_handle_outgoing_calls_returns_workspace_indexing_error_when_loading() {
let dir = TempDir::new().unwrap();
let server_id = ServerId::from_static("rust");
let caps = lsp_types::ServerCapabilities {
call_hierarchy_provider: Some(lsp_types::CallHierarchyProvider::Bool(true)),
..Default::default()
};
let (translator, _server) = translator_with_capabilities(&dir, &server_id, caps);
let cache = Arc::new(Mutex::new(NotificationCache::new()));
cache.lock().await.observe_indexing_signal(
&server_id,
"experimental/serverStatus",
Some(&serde_json::json!({"quiescent": false})),
);
let translator = translator.with_notification_cache(cache);
let path = dir.path().join("queried.rs");
fs::write(&path, "fn queried() {}").unwrap();
let uri = Url::from_file_path(&path).unwrap().to_string();
let err = translator
.handle_outgoing_calls(checked_hierarchy_item(call_hierarchy_item_json(&uri)))
.await
.unwrap_err();
assert_matches!(
err,
Error::WorkspaceIndexing { server_id: id, .. } if id == server_id
);
}
#[tokio::test]
async fn test_handle_incoming_calls_with_nonexistent_file_uri_returns_file_io_not_invalid_uri()
{
let mut translator = Translator::new();
#[cfg(windows)]
translator.set_workspace_roots(
WorkspaceRoots::from_paths(&[std::path::PathBuf::from(r"C:\")]).unwrap(),
);
#[cfg(not(windows))]
translator.set_workspace_roots(
WorkspaceRoots::from_paths(&[std::path::PathBuf::from("/")]).unwrap(),
);
#[cfg(windows)]
let uri = "file:///C:/this/path/does/not/exist/anywhere.rs";
#[cfg(not(windows))]
let uri = "file:///this/path/does/not/exist/anywhere.rs";
let item = serde_json::json!({
"name": "foo",
"kind": 12,
"uri": uri,
"range": {
"start": {"line": 1, "character": 1},
"end": {"line": 1, "character": 1}
},
"selectionRange": {
"start": {"line": 1, "character": 1},
"end": {"line": 1, "character": 1}
}
});
let result = translator
.handle_incoming_calls(checked_hierarchy_item(item))
.await;
assert_matches!(
result,
Err(Error::FileIo { .. }),
"expected Error::FileIo from the canonicalize() failure now that Uri construction \
cannot itself reject a malformed uri, got {result:?}"
);
}
#[tokio::test]
async fn test_handle_incoming_calls_bounds_the_echo_of_a_non_file_uri() {
let translator = Translator::new();
let uri = format!("x\n{}", "Z".repeat(1 << 20));
let item = serde_json::json!({
"name": "foo",
"kind": 12,
"uri": uri,
"range": {
"start": {"line": 1, "character": 1},
"end": {"line": 1, "character": 1}
},
"selectionRange": {
"start": {"line": 1, "character": 1},
"end": {"line": 1, "character": 1}
}
});
let result = translator
.handle_incoming_calls(checked_hierarchy_item(item))
.await;
let Err(Error::InvalidToolParams(message)) = result else {
panic!("expected InvalidToolParams, got {result:?}");
};
assert!(
message.len() <= 2 * crate::util::MAX_ECHOED_PATH_BYTES,
"{}",
message.len()
);
assert!(
message.ends_with(crate::util::TRUNCATION_MARKER),
"{message:.80}"
);
assert!(!message.contains('\n'));
}
#[tokio::test]
async fn test_handle_outgoing_calls_with_nonexistent_file_uri_returns_file_io_not_invalid_uri()
{
let mut translator = Translator::new();
#[cfg(windows)]
translator.set_workspace_roots(
WorkspaceRoots::from_paths(&[std::path::PathBuf::from(r"C:\")]).unwrap(),
);
#[cfg(not(windows))]
translator.set_workspace_roots(
WorkspaceRoots::from_paths(&[std::path::PathBuf::from("/")]).unwrap(),
);
#[cfg(windows)]
let uri = "file:///C:/this/path/does/not/exist/anywhere.rs";
#[cfg(not(windows))]
let uri = "file:///this/path/does/not/exist/anywhere.rs";
let item = serde_json::json!({
"name": "foo",
"kind": 12,
"uri": uri,
"range": {
"start": {"line": 1, "character": 1},
"end": {"line": 1, "character": 1}
},
"selectionRange": {
"start": {"line": 1, "character": 1},
"end": {"line": 1, "character": 1}
}
});
let result = translator
.handle_outgoing_calls(checked_hierarchy_item(item))
.await;
assert_matches!(
result,
Err(Error::FileIo { .. }),
"expected Error::FileIo from the canonicalize() failure now that Uri construction \
cannot itself reject a malformed uri, got {result:?}"
);
}
#[tokio::test]
async fn test_convert_call_hierarchy_item_kind_is_numeric() {
let item = lsp_types::CallHierarchyItem {
name: "my_fn".to_string(),
kind: lsp_types::SymbolKind::Function,
tags: None,
detail: None,
uri: lsp_types::Uri::from("file:///tmp/test.rs"),
range: lsp_types::Range {
start: lsp_types::Position {
line: 0,
character: 0,
},
end: lsp_types::Position {
line: 0,
character: 5,
},
},
selection_range: lsp_types::Range {
start: lsp_types::Position {
line: 0,
character: 0,
},
end: lsp_types::Position {
line: 0,
character: 5,
},
},
data: None,
};
let result = convert_call_hierarchy_item(item, &test_ctx()).await;
assert_eq!(result.kind, 12u32);
assert_eq!(result.name, "my_fn");
}
#[tokio::test]
async fn test_handle_incoming_calls_round_trips_non_function_kind() {
let dir = TempDir::new().unwrap();
let server_id = ServerId::from_static("rust");
let caps = lsp_types::ServerCapabilities {
call_hierarchy_provider: Some(lsp_types::CallHierarchyProvider::Bool(true)),
..Default::default()
};
let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
let queried_path = dir.path().join("queried.rs");
fs::write(&queried_path, "fn queried() {}").unwrap();
let queried_uri = Url::from_file_path(&queried_path).unwrap().to_string();
let item = HierarchyItem {
name: "queried_method".to_string(),
kind: 6, detail: None,
uri: queried_uri,
range: Range {
start: Position2D {
line: 1,
character: 1,
},
end: Position2D {
line: 1,
character: 4,
},
},
selection_range: Range {
start: Position2D {
line: 1,
character: 1,
},
end: Position2D {
line: 1,
character: 4,
},
},
data: None,
out_of_workspace: false,
};
let translator = Arc::new(translator);
let handle = {
let translator = Arc::clone(&translator);
let item = serde_json::to_value(item).unwrap();
tokio::spawn(async move {
translator
.handle_incoming_calls(checked_hierarchy_item(item))
.await
})
};
let mut wire = BufReader::new(&mut server.write_stdout);
let opened = read_framed_message(&mut wire).await;
assert_eq!(opened["method"], "textDocument/didOpen");
let request = read_framed_message(&mut wire).await;
assert_eq!(request["method"], "callHierarchy/incomingCalls");
assert_eq!(
request["params"]["item"]["kind"], 6,
"the reverse u32 -> SymbolKind conversion must round-trip a non-Function kind \
exactly, not fall back to Function (12)"
);
write_response(
&mut server.read_half_stdin,
&request["id"],
serde_json::json!([]),
)
.await;
let result = timeout(Duration::from_secs(2), handle)
.await
.expect("handler call should not hang")
.unwrap()
.unwrap();
assert!(result.calls.is_empty());
}
#[tokio::test]
async fn test_handle_incoming_calls_from_ranges_convert_against_callers_own_uri() {
let dir = TempDir::new().unwrap();
let server_id = ServerId::from_static("rust");
let caps = lsp_types::ServerCapabilities {
call_hierarchy_provider: Some(lsp_types::CallHierarchyProvider::Bool(true)),
..Default::default()
};
let (translator, mut server) = translator_with_capabilities_and_encoding(
&dir,
&server_id,
caps,
lsp_types::PositionEncodingKind::UTF8,
);
let queried_path = canonical_dir(&dir).join("queried.rs");
fs::write(&queried_path, "abc").unwrap();
let queried_uri = Url::from_file_path(&queried_path).unwrap().to_string();
let caller_path = canonical_dir(&dir).join("caller.rs");
fs::write(&caller_path, "aöb").unwrap();
let caller_uri = Url::from_file_path(&caller_path).unwrap().to_string();
let item = HierarchyItem {
name: "queried_fn".to_string(),
kind: 12,
detail: None,
uri: queried_uri,
range: Range {
start: Position2D {
line: 1,
character: 1,
},
end: Position2D {
line: 1,
character: 4,
},
},
selection_range: Range {
start: Position2D {
line: 1,
character: 1,
},
end: Position2D {
line: 1,
character: 4,
},
},
data: None,
out_of_workspace: false,
};
let translator = Arc::new(translator);
let handle = {
let translator = Arc::clone(&translator);
let item = checked_hierarchy_item(serde_json::to_value(item).unwrap());
tokio::spawn(async move { translator.handle_incoming_calls(item).await })
};
let mut wire = BufReader::new(&mut server.write_stdout);
let opened = read_framed_message(&mut wire).await;
assert_eq!(opened["method"], "textDocument/didOpen");
let request = read_framed_message(&mut wire).await;
assert_eq!(request["method"], "callHierarchy/incomingCalls");
write_response(
&mut server.read_half_stdin,
&request["id"],
serde_json::json!([{
"from": {
"name": "caller_fn",
"kind": 12,
"uri": caller_uri,
"range": {
"start": {"line": 0, "character": 0},
"end": {"line": 0, "character": 1}
},
"selectionRange": {
"start": {"line": 0, "character": 0},
"end": {"line": 0, "character": 1}
}
},
"fromRanges": [{
"start": {"line": 0, "character": 0},
"end": {"line": 0, "character": 3}
}]
}]),
)
.await;
let result = timeout(Duration::from_secs(2), handle)
.await
.expect("handler call should not hang")
.unwrap()
.unwrap();
assert_eq!(result.calls.len(), 1);
let from_range = &result.calls[0].from_ranges[0];
assert_eq!(
from_range.end.character, 3,
"fromRanges must convert against the caller's own file (\"aöb\"), not the queried \
item's (\"abc\") -- a byte offset of 3 is UTF-16 column 3 in the former, 4 in the \
latter"
);
assert!(
!result.calls[0].from.out_of_workspace,
"an in-workspace caller must not be marked out_of_workspace"
);
}
#[tokio::test]
async fn test_handle_outgoing_calls_from_ranges_convert_against_queried_uri() {
let dir = TempDir::new().unwrap();
let server_id = ServerId::from_static("rust");
let caps = lsp_types::ServerCapabilities {
call_hierarchy_provider: Some(lsp_types::CallHierarchyProvider::Bool(true)),
..Default::default()
};
let (translator, mut server) = translator_with_capabilities_and_encoding(
&dir,
&server_id,
caps,
lsp_types::PositionEncodingKind::UTF8,
);
let queried_path = dir.path().join("queried.rs");
fs::write(&queried_path, "aöb").unwrap();
let queried_uri = Url::from_file_path(&queried_path).unwrap().to_string();
let callee_path = dir.path().join("callee.rs");
fs::write(&callee_path, "abc").unwrap();
let callee_uri = Url::from_file_path(&callee_path).unwrap().to_string();
let item = HierarchyItem {
name: "queried_fn".to_string(),
kind: 12,
detail: None,
uri: queried_uri,
range: Range {
start: Position2D {
line: 1,
character: 1,
},
end: Position2D {
line: 1,
character: 4,
},
},
selection_range: Range {
start: Position2D {
line: 1,
character: 1,
},
end: Position2D {
line: 1,
character: 4,
},
},
data: None,
out_of_workspace: false,
};
let translator = Arc::new(translator);
let handle = {
let translator = Arc::clone(&translator);
let item = serde_json::to_value(item).unwrap();
tokio::spawn(async move {
translator
.handle_outgoing_calls(checked_hierarchy_item(item))
.await
})
};
let mut wire = BufReader::new(&mut server.write_stdout);
let opened = read_framed_message(&mut wire).await;
assert_eq!(opened["method"], "textDocument/didOpen");
let request = read_framed_message(&mut wire).await;
assert_eq!(request["method"], "callHierarchy/outgoingCalls");
write_response(
&mut server.read_half_stdin,
&request["id"],
serde_json::json!([{
"to": {
"name": "callee_fn",
"kind": 12,
"uri": callee_uri,
"range": {
"start": {"line": 0, "character": 0},
"end": {"line": 0, "character": 1}
},
"selectionRange": {
"start": {"line": 0, "character": 0},
"end": {"line": 0, "character": 1}
}
},
"fromRanges": [{
"start": {"line": 0, "character": 0},
"end": {"line": 0, "character": 3}
}]
}]),
)
.await;
let result = timeout(Duration::from_secs(2), handle)
.await
.expect("handler call should not hang")
.unwrap()
.unwrap();
assert_eq!(result.calls.len(), 1);
let from_range = &result.calls[0].from_ranges[0];
assert_eq!(
from_range.end.character, 3,
"fromRanges must convert against the queried item's own file (\"aöb\"), not the \
callee's (\"abc\") -- a byte offset of 3 is UTF-16 column 3 in the former, 4 in \
the latter"
);
}
#[tokio::test]
async fn test_handle_incoming_calls_does_not_filter_out_of_workspace_caller() {
let dir = TempDir::new().unwrap();
let server_id = ServerId::from_static("rust");
let caps = lsp_types::ServerCapabilities {
call_hierarchy_provider: Some(lsp_types::CallHierarchyProvider::Bool(true)),
..Default::default()
};
let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
let queried_path = dir.path().join("queried.rs");
fs::write(&queried_path, "fn queried() {}").unwrap();
let queried_uri = Url::from_file_path(&queried_path).unwrap().to_string();
let outside_uri = "file:///outside/workspace/stdlib.rs";
let item = HierarchyItem {
name: "queried_fn".to_string(),
kind: 12,
detail: None,
uri: queried_uri,
range: Range {
start: Position2D {
line: 1,
character: 1,
},
end: Position2D {
line: 1,
character: 4,
},
},
selection_range: Range {
start: Position2D {
line: 1,
character: 1,
},
end: Position2D {
line: 1,
character: 4,
},
},
data: None,
out_of_workspace: false,
};
let translator = Arc::new(translator);
let handle = {
let translator = Arc::clone(&translator);
let item = serde_json::to_value(item).unwrap();
tokio::spawn(async move {
translator
.handle_incoming_calls(checked_hierarchy_item(item))
.await
})
};
let mut wire = BufReader::new(&mut server.write_stdout);
let opened = read_framed_message(&mut wire).await;
assert_eq!(opened["method"], "textDocument/didOpen");
let request = read_framed_message(&mut wire).await;
assert_eq!(request["method"], "callHierarchy/incomingCalls");
write_response(
&mut server.read_half_stdin,
&request["id"],
serde_json::json!([{
"from": {
"name": "caller_fn",
"kind": 12,
"uri": outside_uri,
"range": {
"start": {"line": 0, "character": 0},
"end": {"line": 0, "character": 1}
},
"selectionRange": {
"start": {"line": 0, "character": 0},
"end": {"line": 0, "character": 1}
}
},
"fromRanges": [{
"start": {"line": 0, "character": 0},
"end": {"line": 0, "character": 1}
}]
}]),
)
.await;
let result = timeout(Duration::from_secs(2), handle)
.await
.expect("handler call should not hang")
.unwrap()
.unwrap();
assert_eq!(
result.calls.len(),
1,
"an out-of-workspace caller must be returned, not dropped"
);
assert_eq!(result.calls[0].from.uri, outside_uri);
assert!(
result.calls[0].from.out_of_workspace,
"an out-of-workspace caller must be marked out_of_workspace"
);
}
#[tokio::test]
async fn test_handle_outgoing_calls_does_not_filter_out_of_workspace_callee() {
let dir = TempDir::new().unwrap();
let server_id = ServerId::from_static("rust");
let caps = lsp_types::ServerCapabilities {
call_hierarchy_provider: Some(lsp_types::CallHierarchyProvider::Bool(true)),
..Default::default()
};
let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
let queried_path = dir.path().join("queried.rs");
fs::write(&queried_path, "fn queried() {}").unwrap();
let queried_uri = Url::from_file_path(&queried_path).unwrap().to_string();
let outside_uri = "file:///outside/workspace/stdlib.rs";
let item = HierarchyItem {
name: "queried_fn".to_string(),
kind: 12,
detail: None,
uri: queried_uri,
range: Range {
start: Position2D {
line: 1,
character: 1,
},
end: Position2D {
line: 1,
character: 4,
},
},
selection_range: Range {
start: Position2D {
line: 1,
character: 1,
},
end: Position2D {
line: 1,
character: 4,
},
},
data: None,
out_of_workspace: false,
};
let translator = Arc::new(translator);
let handle = {
let translator = Arc::clone(&translator);
let item = serde_json::to_value(item).unwrap();
tokio::spawn(async move {
translator
.handle_outgoing_calls(checked_hierarchy_item(item))
.await
})
};
let mut wire = BufReader::new(&mut server.write_stdout);
let opened = read_framed_message(&mut wire).await;
assert_eq!(opened["method"], "textDocument/didOpen");
let request = read_framed_message(&mut wire).await;
assert_eq!(request["method"], "callHierarchy/outgoingCalls");
write_response(
&mut server.read_half_stdin,
&request["id"],
serde_json::json!([{
"to": {
"name": "callee_fn",
"kind": 12,
"uri": outside_uri,
"range": {
"start": {"line": 0, "character": 0},
"end": {"line": 0, "character": 1}
},
"selectionRange": {
"start": {"line": 0, "character": 0},
"end": {"line": 0, "character": 1}
}
},
"fromRanges": [{
"start": {"line": 0, "character": 0},
"end": {"line": 0, "character": 1}
}]
}]),
)
.await;
let result = timeout(Duration::from_secs(2), handle)
.await
.expect("handler call should not hang")
.unwrap()
.unwrap();
assert_eq!(
result.calls.len(),
1,
"an out-of-workspace callee must be returned, not dropped"
);
assert_eq!(result.calls[0].to.uri, outside_uri);
assert!(
result.calls[0].to.out_of_workspace,
"an out-of-workspace callee must be marked out_of_workspace"
);
}
#[tokio::test]
async fn test_prepare_then_incoming_calls_round_trip_percent_encoded_path() {
let dir = TempDir::new().unwrap();
let server_id = ServerId::from_static("rust");
let caps = lsp_types::ServerCapabilities {
call_hierarchy_provider: Some(lsp_types::CallHierarchyProvider::Bool(true)),
..Default::default()
};
let (translator, mut server) = translator_with_capabilities_and_encoding(
&dir,
&server_id,
caps,
lsp_types::PositionEncodingKind::UTF8,
);
let file_path = dir.path().join("my file café.rs");
fs::write(&file_path, "fn foo() {}").unwrap();
let file_uri = Url::from_file_path(&file_path).unwrap().to_string();
assert!(
file_uri.contains("%20"),
"test fixture must exercise percent-encoding"
);
let translator = Arc::new(translator);
let mut wire = BufReader::new(&mut server.write_stdout);
let prepare_handle = {
let translator = Arc::clone(&translator);
let path = file_path.to_string_lossy().into_owned();
tokio::spawn(async move {
translator
.handle_call_hierarchy_prepare(client_path(path), pos(1, 1))
.await
})
};
let opened = read_framed_message(&mut wire).await;
assert_eq!(opened["method"], "textDocument/didOpen");
let request = read_framed_message(&mut wire).await;
assert_eq!(request["method"], "textDocument/prepareCallHierarchy");
write_response(
&mut server.read_half_stdin,
&request["id"],
serde_json::json!([{
"name": "foo",
"kind": 12,
"uri": file_uri,
"range": {
"start": {"line": 0, "character": 0},
"end": {"line": 0, "character": 11}
},
"selectionRange": {
"start": {"line": 0, "character": 3},
"end": {"line": 0, "character": 6}
}
}]),
)
.await;
let prepare_result = timeout(Duration::from_secs(2), prepare_handle)
.await
.expect("prepare should not hang")
.unwrap()
.unwrap()
.result;
assert_eq!(prepare_result.items.len(), 1);
let item = prepare_result.items[0].clone();
assert!(item.uri.contains("%20"));
let incoming_handle = {
let translator = Arc::clone(&translator);
let item = serde_json::to_value(item).unwrap();
tokio::spawn(async move {
translator
.handle_incoming_calls(checked_hierarchy_item(item))
.await
})
};
let request = read_framed_message(&mut wire).await;
assert_eq!(request["method"], "callHierarchy/incomingCalls");
write_response(
&mut server.read_half_stdin,
&request["id"],
serde_json::json!([]),
)
.await;
let incoming_result = timeout(Duration::from_secs(2), incoming_handle)
.await
.expect("incoming calls should not hang")
.unwrap();
assert!(
incoming_result.is_ok(),
"expected success resolving the percent-encoded path, got {incoming_result:?}"
);
}
#[derive(Clone, Copy)]
enum Direction {
Incoming,
Outgoing,
}
impl Direction {
const fn lsp_method(self) -> &'static str {
match self {
Self::Incoming => "callHierarchy/incomingCalls",
Self::Outgoing => "callHierarchy/outgoingCalls",
}
}
const fn item_field(self) -> &'static str {
match self {
Self::Incoming => "from",
Self::Outgoing => "to",
}
}
}
async fn calls_with_response(
direction: Direction,
response: impl FnOnce(&str) -> serde_json::Value,
) -> serde_json::Value {
let dir = TempDir::new().unwrap();
let server_id = ServerId::from_static("rust");
let caps = lsp_types::ServerCapabilities {
call_hierarchy_provider: Some(lsp_types::CallHierarchyProvider::Bool(true)),
..Default::default()
};
let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
let queried_path = dir.path().join("queried.rs");
fs::write(&queried_path, "fn queried() {}").unwrap();
let queried_uri = Url::from_file_path(&queried_path).unwrap().to_string();
let item = call_hierarchy_item_json(&queried_uri);
let translator = Arc::new(translator);
let handle = {
let translator = Arc::clone(&translator);
tokio::spawn(async move {
match direction {
Direction::Incoming => serde_json::to_value(
translator
.handle_incoming_calls(checked_hierarchy_item(item))
.await
.unwrap(),
),
Direction::Outgoing => serde_json::to_value(
translator
.handle_outgoing_calls(checked_hierarchy_item(item))
.await
.unwrap(),
),
}
.unwrap()
})
};
let mut wire = BufReader::new(&mut server.write_stdout);
let opened = read_framed_message(&mut wire).await;
assert_eq!(opened["method"], "textDocument/didOpen");
let request = read_framed_message(&mut wire).await;
assert_eq!(request["method"], direction.lsp_method());
write_response(
&mut server.read_half_stdin,
&request["id"],
response(&queried_uri),
)
.await;
timeout(Duration::from_secs(30), handle)
.await
.expect("handler call should not hang")
.unwrap()
}
fn hierarchy_call_json(uri: &str, direction: Direction, ranges: usize) -> serde_json::Value {
let range = serde_json::json!({
"start": {"line": 0, "character": 0},
"end": {"line": 0, "character": 1}
});
serde_json::json!({
direction.item_field(): {
"name": "f", "kind": 12, "uri": uri,
"range": range, "selectionRange": range
},
"fromRanges": vec![range; ranges]
})
}
fn range_count(call: &serde_json::Value) -> usize {
call["from_ranges"].as_array().unwrap().len()
}
#[tokio::test]
async fn test_handle_incoming_calls_drops_call_whole_when_ranges_exceed_budget() {
use crate::bridge::translator::navigation::MAX_NORMALIZED_LOCATIONS;
let result = calls_with_response(Direction::Incoming, |uri| {
serde_json::json!([hierarchy_call_json(
uri,
Direction::Incoming,
MAX_NORMALIZED_LOCATIONS
)])
})
.await;
assert_eq!(result["truncated"], true);
assert_eq!(result["calls"].as_array().unwrap().len(), 0);
}
#[tokio::test]
async fn test_handle_incoming_calls_never_returns_call_with_ranges_cut() {
use crate::bridge::translator::navigation::MAX_NORMALIZED_LOCATIONS;
let ranges_per_call = MAX_NORMALIZED_LOCATIONS / 2;
let result = calls_with_response(Direction::Incoming, |uri| {
let call = hierarchy_call_json(uri, Direction::Incoming, ranges_per_call);
serde_json::Value::Array(vec![call; 3])
})
.await;
assert_eq!(result["truncated"], true);
let calls = result["calls"].as_array().unwrap();
assert_eq!(calls.len(), 1);
assert_eq!(range_count(&calls[0]), ranges_per_call);
}
#[tokio::test]
async fn test_handle_incoming_calls_exactly_at_cap_is_not_truncated() {
use crate::bridge::translator::navigation::MAX_NORMALIZED_LOCATIONS;
let result = calls_with_response(Direction::Incoming, |uri| {
serde_json::json!([hierarchy_call_json(
uri,
Direction::Incoming,
MAX_NORMALIZED_LOCATIONS - 1
)])
})
.await;
assert!(result.get("truncated").is_none());
assert_eq!(
range_count(&result["calls"][0]),
MAX_NORMALIZED_LOCATIONS - 1
);
}
#[tokio::test]
async fn test_handle_outgoing_calls_caps_item_count_and_reports_truncation() {
use crate::bridge::translator::navigation::MAX_NORMALIZED_LOCATIONS;
let result = calls_with_response(Direction::Outgoing, |uri| {
let call = hierarchy_call_json(uri, Direction::Outgoing, 0);
serde_json::Value::Array(vec![call; MAX_NORMALIZED_LOCATIONS + 500])
})
.await;
assert_eq!(result["truncated"], true);
assert_eq!(
result["calls"].as_array().unwrap().len(),
MAX_NORMALIZED_LOCATIONS
);
}
#[tokio::test]
async fn test_handle_incoming_calls_within_cap_is_not_truncated() {
let result = calls_with_response(Direction::Incoming, |uri| {
serde_json::json!([hierarchy_call_json(uri, Direction::Incoming, 0)])
})
.await;
assert!(result.get("truncated").is_none());
}
#[tokio::test]
async fn test_handle_call_hierarchy_prepare_caps_items_and_reports_truncation() {
use crate::bridge::translator::navigation::MAX_NORMALIZED_LOCATIONS;
for (count, truncated) in [
(MAX_NORMALIZED_LOCATIONS + 10, true),
(MAX_NORMALIZED_LOCATIONS, false),
] {
let dir = TempDir::new().unwrap();
let server_id = ServerId::from_static("rust");
let caps = lsp_types::ServerCapabilities {
call_hierarchy_provider: Some(lsp_types::CallHierarchyProvider::Bool(true)),
..Default::default()
};
let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
let path = dir.path().join("queried.rs");
fs::write(&path, "fn queried() {}").unwrap();
let uri = Url::from_file_path(&path).unwrap().to_string();
let translator = Arc::new(translator);
let handle = {
let translator = Arc::clone(&translator);
let path = path.to_string_lossy().into_owned();
tokio::spawn(async move {
translator
.handle_call_hierarchy_prepare(client_path(path), pos(1, 1))
.await
})
};
let mut wire = BufReader::new(&mut server.write_stdout);
let opened = read_framed_message(&mut wire).await;
assert_eq!(opened["method"], "textDocument/didOpen");
let request = read_framed_message(&mut wire).await;
assert_eq!(request["method"], "textDocument/prepareCallHierarchy");
let range = serde_json::json!({
"start": {"line": 0, "character": 0},
"end": {"line": 0, "character": 1}
});
let item = serde_json::json!({
"name": "f", "kind": 12, "uri": uri,
"range": range, "selectionRange": range
});
write_response(
&mut server.read_half_stdin,
&request["id"],
serde_json::Value::Array(vec![item; count]),
)
.await;
let result = timeout(Duration::from_secs(30), handle)
.await
.expect("handler call should not hang")
.unwrap()
.unwrap()
.result;
assert_eq!(result.items.len(), MAX_NORMALIZED_LOCATIONS);
assert_eq!(result.truncated, truncated);
let wire = serde_json::to_value(&result).unwrap();
assert_eq!(wire.get("truncated").is_some(), truncated);
}
}
}