use std::time::Duration;
use lsp_types::ReferenceContext;
use tokio::time::Instant;
use super::Translator;
use super::dto::{
HoverResult, IndexCoverage, Location, LocationsResult, Position, PositionDegradation,
ReferencesResult,
};
use super::enclosing::{Contextualized, ResultContext};
use super::encoding_ctx::EncodingCtx;
use super::positioned::{FromPosition, GotoRequest, Positioned};
use super::routing::{Capability, IndexingGate};
use crate::bridge::indexing::{
DEFAULT_INDEXING_READY_TIMEOUT_SECS, INDEXING_STALENESS_BOUND, PROGRESS_LATCH_IDLE,
PROGRESS_SETTLE,
};
use crate::bridge::{ClientPath, IndexingState};
use crate::config::ServerId;
use crate::error::{Error, Result};
pub(super) const INDEXING_READY_TIMEOUT: Duration =
Duration::from_secs(DEFAULT_INDEXING_READY_TIMEOUT_SECS);
const INDEXING_POLL_INTERVAL: Duration = Duration::from_millis(100);
const _: () = assert!(
INDEXING_STALENESS_BOUND.as_nanos() > INDEXING_READY_TIMEOUT.as_nanos(),
"INDEXING_STALENESS_BOUND must be greater than INDEXING_READY_TIMEOUT"
);
const _: () = assert!(
PROGRESS_SETTLE.as_nanos() < PROGRESS_LATCH_IDLE.as_nanos(),
"PROGRESS_SETTLE must be less than PROGRESS_LATCH_IDLE"
);
const _: () = assert!(
PROGRESS_SETTLE.as_nanos() < INDEXING_READY_TIMEOUT.as_nanos(),
"PROGRESS_SETTLE must be less than INDEXING_READY_TIMEOUT"
);
fn definition_to_locations(definition: lsp_types::Definition) -> Vec<lsp_types::Location> {
match definition {
lsp_types::Definition::Location(loc) => vec![loc],
lsp_types::Definition::LocationList(locs) => locs,
}
}
fn definition_link_to_location(link: lsp_types::DefinitionLink) -> lsp_types::Location {
lsp_types::Location {
uri: link.target_uri,
range: link.target_selection_range,
}
}
pub(super) const MAX_NORMALIZED_LOCATIONS: usize = 10_000;
#[derive(Debug)]
pub(super) struct ItemBudget {
remaining: usize,
cap: usize,
truncated: bool,
}
impl ItemBudget {
pub(super) const fn new() -> Self {
Self::with_cap(MAX_NORMALIZED_LOCATIONS)
}
pub(super) const fn with_cap(cap: usize) -> Self {
Self {
remaining: cap,
cap,
truncated: false,
}
}
pub(super) fn admit<T>(&mut self, mut items: Vec<T>) -> Vec<T> {
if items.len() > self.remaining {
self.record_drop(items.len());
items.truncate(self.remaining);
}
self.remaining = self.remaining.saturating_sub(items.len());
items
}
pub(super) fn admit_iter<T>(&mut self, items: impl IntoIterator<Item = T>) -> Vec<T> {
let mut items = items.into_iter();
let kept: Vec<T> = items.by_ref().take(self.remaining).collect();
if items.next().is_some() {
self.record_drop(kept.len().saturating_add(1));
}
self.remaining = self.remaining.saturating_sub(kept.len());
kept
}
pub(super) fn admit_whole<T>(&mut self, items: Vec<T>) -> Option<Vec<T>> {
self.spend_whole(items.len()).then_some(items)
}
pub(super) fn spend_whole(&mut self, count: usize) -> bool {
if count > self.remaining {
self.record_drop(count);
return false;
}
self.remaining = self.remaining.saturating_sub(count);
true
}
fn record_drop(&mut self, reported: usize) {
if !self.truncated {
tracing::warn!(
reported,
cap = self.cap,
"LSP response item count exceeds the item cap; truncating"
);
}
self.truncated = true;
}
pub(super) const fn truncated(&self) -> bool {
self.truncated
}
}
async fn lsp_locations_to_mcp(
locs: Vec<lsp_types::Location>,
ctx: &EncodingCtx,
) -> NormalizedLocations {
let mut budget = ItemBudget::new();
let locs = budget.admit(locs);
let truncated = budget.truncated();
let mut locations = Vec::with_capacity(locs.len());
for loc in locs {
locations.push(Location {
uri: loc.uri.to_string(),
range: ctx.normalize_range(&loc.uri, loc.range).await,
out_of_workspace: ctx.is_out_of_workspace(&loc.uri),
});
}
NormalizedLocations {
locations,
truncated,
positions_degraded: ctx.positions_degraded(),
}
}
struct NormalizedLocations {
locations: Vec<Location>,
truncated: bool,
positions_degraded: Option<PositionDegradation>,
}
enum GotoKind {
Locations(Vec<lsp_types::Location>),
Links(Vec<lsp_types::LocationLink>),
}
trait GotoResponse {
fn into_kind(self) -> GotoKind;
}
impl GotoResponse for lsp_types::DefinitionResponse {
fn into_kind(self) -> GotoKind {
match self {
Self::Definition(def) => GotoKind::Locations(definition_to_locations(def)),
Self::DefinitionLinkList(links) => GotoKind::Links(links),
}
}
}
impl GotoResponse for lsp_types::ImplementationResponse {
fn into_kind(self) -> GotoKind {
match self {
Self::Definition(def) => GotoKind::Locations(definition_to_locations(def)),
Self::DefinitionLinkList(links) => GotoKind::Links(links),
}
}
}
impl GotoResponse for lsp_types::TypeDefinitionResponse {
fn into_kind(self) -> GotoKind {
match self {
Self::Definition(def) => GotoKind::Locations(definition_to_locations(def)),
Self::DefinitionLinkList(links) => GotoKind::Links(links),
}
}
}
impl GotoResponse for lsp_types::DeclarationResponse {
fn into_kind(self) -> GotoKind {
match self {
Self::Declaration(lsp_types::Declaration::Location(loc)) => {
GotoKind::Locations(vec![loc])
}
Self::Declaration(lsp_types::Declaration::LocationList(locs)) => {
GotoKind::Locations(locs)
}
Self::DeclarationLinkList(links) => GotoKind::Links(links),
}
}
}
async fn goto_response_to_locations<R: GotoResponse>(
response: Option<R>,
ctx: &EncodingCtx,
) -> NormalizedLocations {
let lsp_locs = match response.map(GotoResponse::into_kind) {
Some(GotoKind::Locations(locs)) => locs,
Some(GotoKind::Links(links)) => {
links.into_iter().map(definition_link_to_location).collect()
}
None => vec![],
};
lsp_locations_to_mcp(lsp_locs, ctx).await
}
#[allow(deprecated, reason = "LSP servers still send this deprecated field")]
fn extract_hover_contents(contents: lsp_types::Contents) -> String {
match contents {
lsp_types::Contents::MarkedString(marked_string) => marked_string_to_string(marked_string),
lsp_types::Contents::MarkedStringList(marked_strings) => marked_strings
.into_iter()
.map(marked_string_to_string)
.collect::<Vec<_>>()
.join("\n\n"),
lsp_types::Contents::MarkupContent(markup) => markup.value,
}
}
#[allow(deprecated, reason = "LSP servers still send this deprecated field")]
fn marked_string_to_string(marked: lsp_types::MarkedString) -> String {
match marked {
lsp_types::MarkedString::String(s) => s,
lsp_types::MarkedString::MarkedStringWithLanguage(ls) => {
format!("```{}\n{}\n```", ls.language, ls.value)
}
}
}
impl Translator {
pub(super) async fn wait_for_indexing_ready(&self, server_id: &ServerId) -> Result<()> {
self.wait_for_indexing_ready_with(
server_id,
self.indexing_ready_timeout,
INDEXING_POLL_INTERVAL,
)
.await
}
async fn wait_for_indexing_ready_with(
&self,
server_id: &ServerId,
timeout: Duration,
poll_interval: Duration,
) -> Result<()> {
let Some(cache) = self.notification_cache.as_ref() else {
return Ok(());
};
let start = Instant::now();
#[allow(
clippy::arithmetic_side_effects,
reason = "production passes the fixed INDEXING_READY_TIMEOUT"
)]
let deadline = start + timeout;
loop {
let state = cache.lock().await.indexing_state(server_id);
if state != IndexingState::Loading {
return Ok(());
}
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
return Err(Error::WorkspaceIndexing {
server_id: server_id.clone(),
elapsed_secs: start.elapsed().as_secs(),
});
}
tokio::time::sleep(poll_interval.min(remaining)).await;
}
}
pub async fn handle_hover(
&self,
file_path: ClientPath,
position: Position,
) -> Result<HoverResult> {
let Positioned {
result: response,
ctx,
doc,
} = self
.position_request::<lsp_types::HoverRequest>(
&file_path,
position,
IndexingGate::Required(Capability::Hover),
(),
)
.await?;
let response_uri = doc.uri();
let result = match response {
Some(hover) => {
let contents = extract_hover_contents(hover.contents);
let range = match hover.range {
Some(r) => Some(ctx.normalize_range(response_uri, r).await),
None => None,
};
HoverResult {
contents,
range,
positions_degraded: ctx.positions_degraded(),
}
}
None => HoverResult {
contents: "No hover information available".to_string(),
range: None,
positions_degraded: ctx.positions_degraded(),
},
};
Ok(result)
}
async fn contextualize_into(
&self,
normalized: NormalizedLocations,
context: ResultContext,
) -> LocationsResult {
let truncated = normalized.truncated;
let Contextualized {
items: locations,
enrichment,
positions_degraded,
} = self
.contextualize_locations(normalized.locations, context, normalized.positions_degraded)
.await;
LocationsResult {
locations,
truncated,
positions_degraded,
enrichment,
}
}
async fn handle_goto<R, T>(
&self,
file_path: &ClientPath,
position: Position,
) -> Result<NormalizedLocations>
where
R: GotoRequest + lsp_types::Request<Result = Option<T>>,
R::Params: FromPosition<Extra = ()>,
T: GotoResponse,
{
let Positioned {
result: response,
ctx,
doc: _doc,
} = self
.position_request::<R>(
file_path,
position,
IndexingGate::Required(R::CAPABILITY),
(),
)
.await?;
Ok(goto_response_to_locations(response, &ctx).await)
}
pub async fn handle_definition(
&self,
file_path: ClientPath,
position: Position,
context: ResultContext,
) -> Result<LocationsResult> {
let normalized = self
.handle_goto::<lsp_types::DefinitionRequest, _>(&file_path, position)
.await?;
Ok(self.contextualize_into(normalized, context).await)
}
pub async fn handle_references(
&self,
file_path: ClientPath,
position: Position,
include_declaration: bool,
context: ResultContext,
) -> Result<ReferencesResult> {
let Positioned {
result: response,
ctx,
doc,
} = self
.position_request::<lsp_types::ReferencesRequest>(
&file_path,
position,
IndexingGate::Required(Capability::References),
ReferenceContext {
include_declaration,
},
)
.await?;
let normalized = lsp_locations_to_mcp(response.unwrap_or_default(), &ctx).await;
let result = self.contextualize_into(normalized, context).await;
let index_coverage = result
.locations
.is_empty()
.then(|| self.index_coverage(doc.server_id()));
Ok(ReferencesResult {
result,
index_coverage,
})
}
pub(super) fn index_coverage(&self, server: &ServerId) -> IndexCoverage {
let tracker = &self.document_tracker;
IndexCoverage {
synced_documents: u32::try_from(tracker.synced_document_count(server))
.unwrap_or(u32::MAX),
server_restarted: tracker.sync_history_forgotten(server),
}
}
pub async fn handle_implementation(
&self,
file_path: ClientPath,
position: Position,
context: ResultContext,
) -> Result<LocationsResult> {
let normalized = self
.handle_goto::<lsp_types::ImplementationRequest, _>(&file_path, position)
.await?;
Ok(self.contextualize_into(normalized, context).await)
}
pub async fn handle_type_definition(
&self,
file_path: ClientPath,
position: Position,
context: ResultContext,
) -> Result<LocationsResult> {
let normalized = self
.handle_goto::<lsp_types::TypeDefinitionRequest, _>(&file_path, position)
.await?;
Ok(self.contextualize_into(normalized, context).await)
}
pub async fn handle_declaration(
&self,
file_path: ClientPath,
position: Position,
context: ResultContext,
) -> Result<LocationsResult> {
let normalized = self
.handle_goto::<lsp_types::DeclarationRequest, _>(&file_path, position)
.await?;
Ok(self.contextualize_into(normalized, context).await)
}
}
#[cfg(test)]
#[allow(deprecated, reason = "LSP servers still send this deprecated field")]
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::encoding::PositionEncoding;
use crate::bridge::translator::testing::*;
use crate::bridge::{NotificationCache, path_to_uri};
use crate::config::{IndexingReadyTimeoutSecs, ServerId};
use crate::test_lsp::client_path;
use crate::util::lock_std;
#[tokio::test]
async fn test_wait_for_indexing_ready_without_cache_is_noop() {
let translator = Translator::new();
let server_id = ServerId::from_static("rust");
translator
.wait_for_indexing_ready(&server_id)
.await
.unwrap();
}
#[tokio::test]
async fn test_wait_for_indexing_ready_unknown_state_is_noop() {
let translator = Translator::new()
.with_notification_cache(Arc::new(Mutex::new(NotificationCache::new())));
let server_id = ServerId::from_static("rust");
translator
.wait_for_indexing_ready(&server_id)
.await
.unwrap();
}
#[tokio::test]
async fn test_wait_for_indexing_ready_ready_state_is_noop() {
let cache = Arc::new(Mutex::new(NotificationCache::new()));
let server_id = ServerId::from_static("rust");
cache.lock().await.observe_indexing_signal(
&server_id,
"experimental/serverStatus",
Some(&serde_json::json!({"quiescent": true})),
);
let translator = Translator::new().with_notification_cache(cache);
translator
.wait_for_indexing_ready(&server_id)
.await
.unwrap();
}
#[tokio::test(start_paused = true)]
async fn test_wait_for_indexing_ready_uses_configured_timeout_override() {
let cache = Arc::new(Mutex::new(NotificationCache::new()));
let server_id = ServerId::from_static("rust");
cache.lock().await.observe_indexing_signal(
&server_id,
"experimental/serverStatus",
Some(&serde_json::json!({"quiescent": false})),
);
let translator = Translator::new()
.with_notification_cache(cache)
.with_indexing_ready_timeout(IndexingReadyTimeoutSecs::new(5).unwrap());
let start = Instant::now();
let err = translator
.wait_for_indexing_ready(&server_id)
.await
.unwrap_err();
assert_matches!(err, Error::WorkspaceIndexing { elapsed_secs, .. } if elapsed_secs == 5);
assert_eq!(start.elapsed(), Duration::from_secs(5));
}
#[tokio::test]
async fn test_wait_for_indexing_ready_loading_times_out() {
let cache = Arc::new(Mutex::new(NotificationCache::new()));
let server_id = ServerId::from_static("rust");
cache.lock().await.observe_indexing_signal(
&server_id,
"experimental/serverStatus",
Some(&serde_json::json!({"quiescent": false})),
);
let translator = Translator::new().with_notification_cache(cache);
let err = translator
.wait_for_indexing_ready_with(
&server_id,
Duration::from_millis(50),
Duration::from_millis(10),
)
.await
.unwrap_err();
assert_matches!(
err,
Error::WorkspaceIndexing { server_id: id, .. } if id == ServerId::from_static("rust")
);
}
#[tokio::test]
async fn test_wait_for_indexing_ready_returns_ok_once_signaled_ready() {
let cache = Arc::new(Mutex::new(NotificationCache::new()));
let server_id = ServerId::from_static("rust");
cache.lock().await.observe_indexing_signal(
&server_id,
"experimental/serverStatus",
Some(&serde_json::json!({"quiescent": false})),
);
let translator = Translator::new().with_notification_cache(Arc::clone(&cache));
let waiter = {
let server_id = server_id.clone();
tokio::spawn(async move {
translator
.wait_for_indexing_ready_with(
&server_id,
Duration::from_secs(5),
Duration::from_millis(10),
)
.await
})
};
tokio::time::sleep(Duration::from_millis(30)).await;
cache.lock().await.observe_indexing_signal(
&server_id,
"experimental/serverStatus",
Some(&serde_json::json!({"quiescent": true})),
);
timeout(Duration::from_secs(1), waiter)
.await
.expect("waiter task timed out")
.expect("waiter task panicked")
.expect("expected Ok once quiescent");
}
#[tokio::test(start_paused = true)]
async fn test_handle_hover_returns_workspace_indexing_error_when_loading() {
let dir = TempDir::new().unwrap();
let server_id = ServerId::from_static("rust");
let caps = lsp_types::ServerCapabilities {
hover_provider: Some(lsp_types::HoverProvider::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("main.rs");
fs::write(&path, "fn main() {}").unwrap();
let err = translator
.handle_hover(client_path(path.to_string_lossy().into_owned()), pos(1, 1))
.await
.unwrap_err();
assert_matches!(
err,
Error::WorkspaceIndexing { server_id: id, elapsed_secs: 30 } if id == server_id
);
}
#[tokio::test]
async fn test_handle_hover_dispatches_when_indexing_ready() {
let dir = TempDir::new().unwrap();
let server_id = ServerId::from_static("rust");
let caps = lsp_types::ServerCapabilities {
hover_provider: Some(lsp_types::HoverProvider::Bool(true)),
..Default::default()
};
let (translator, mut 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": true})),
);
let translator = Arc::new(translator.with_notification_cache(cache));
let path = dir.path().join("main.rs");
fs::write(&path, "fn main() {}").unwrap();
let handle = {
let translator = Arc::clone(&translator);
let path = path.to_string_lossy().to_string();
tokio::spawn(async move { translator.handle_hover(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/hover");
write_response(
&mut server.read_half_stdin,
&request["id"],
serde_json::json!({
"contents": {"kind": "markdown", "value": "hover text"}
}),
)
.await;
let result = handle.await.unwrap().unwrap();
assert_eq!(result.contents, "hover text");
}
#[tokio::test(start_paused = true)]
async fn test_handle_hover_timeout_keeps_document_open_for_next_request() {
let dir = TempDir::new().unwrap();
let server_id = ServerId::from_static("rust");
let caps = lsp_types::ServerCapabilities {
hover_provider: Some(lsp_types::HoverProvider::Bool(true)),
..Default::default()
};
let (translator, mut 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": true})),
);
let translator = Arc::new(translator.with_notification_cache(cache));
let path = dir.path().join("main.rs");
fs::write(&path, "fn main() {}").unwrap();
let path = path.to_string_lossy().to_string();
let spawn_hover = || {
let translator = Arc::clone(&translator);
let path = path.clone();
tokio::spawn(async move { translator.handle_hover(client_path(path), pos(1, 1)).await })
};
let mut wire = BufReader::new(&mut server.write_stdout);
let first = spawn_hover();
let opened = read_framed_message(&mut wire).await;
assert_eq!(opened["method"], "textDocument/didOpen");
let first_request = read_framed_message(&mut wire).await;
assert_eq!(first_request["method"], "textDocument/hover");
let timeout_secs = crate::config::LspServerConfig::rust_analyzer().request_timeout_seconds;
tokio::time::advance(Duration::from_secs(timeout_secs.get() + 1)).await;
assert_matches!(first.await.unwrap().unwrap_err(), Error::Timeout(_));
let cancel = read_framed_message(&mut wire).await;
assert_eq!(cancel["method"], "$/cancelRequest");
assert_eq!(cancel["params"]["id"], first_request["id"]);
let second = spawn_hover();
let second_request = read_framed_message(&mut wire).await;
assert_eq!(
second_request["method"], "textDocument/hover",
"a timed-out hover must not cause the document to be re-opened"
);
write_response(
&mut server.read_half_stdin,
&second_request["id"],
serde_json::json!({"contents": {"kind": "markdown", "value": "after timeout"}}),
)
.await;
assert_eq!(second.await.unwrap().unwrap().contents, "after timeout");
}
#[tokio::test(start_paused = true)]
async fn test_handle_definition_returns_workspace_indexing_error_when_loading() {
let dir = TempDir::new().unwrap();
let server_id = ServerId::from_static("rust");
let caps = lsp_types::ServerCapabilities {
definition_provider: Some(lsp_types::DefinitionProvider::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("main.rs");
fs::write(&path, "fn main() {}").unwrap();
let err = translator
.handle_definition(
client_path(path.to_string_lossy().into_owned()),
pos(1, 1),
ResultContext::None,
)
.await
.unwrap_err();
assert_matches!(
err,
Error::WorkspaceIndexing { server_id: id, .. } if id == server_id
);
}
#[tokio::test]
async fn test_handle_definition_dispatches_when_indexing_ready() {
let dir = TempDir::new().unwrap();
let server_id = ServerId::from_static("rust");
let caps = lsp_types::ServerCapabilities {
definition_provider: Some(lsp_types::DefinitionProvider::Bool(true)),
..Default::default()
};
let (translator, mut 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": true})),
);
let translator = Arc::new(translator.with_notification_cache(cache));
let path = dir.path().join("main.rs");
fs::write(&path, "fn main() {}").unwrap();
let handle = {
let translator = Arc::clone(&translator);
let path = path.to_string_lossy().to_string();
tokio::spawn(async move {
translator
.handle_definition(client_path(path), pos(1, 1), ResultContext::None)
.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/definition");
write_response(
&mut server.read_half_stdin,
&request["id"],
serde_json::Value::Null,
)
.await;
let result = handle.await.unwrap().unwrap();
assert!(result.locations.is_empty());
}
#[tokio::test(start_paused = true)]
async fn test_handle_references_returns_workspace_indexing_error_when_loading() {
let dir = TempDir::new().unwrap();
let server_id = ServerId::from_static("rust");
let caps = lsp_types::ServerCapabilities {
references_provider: Some(lsp_types::ReferencesProvider::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("main.rs");
fs::write(&path, "fn main() {}").unwrap();
let err = translator
.handle_references(
client_path(path.to_string_lossy().into_owned()),
pos(1, 1),
true,
ResultContext::None,
)
.await
.unwrap_err();
assert_matches!(
err,
Error::WorkspaceIndexing { server_id: id, .. } if id == server_id
);
}
#[tokio::test]
async fn test_handle_references_dispatches_when_indexing_ready() {
let dir = TempDir::new().unwrap();
let server_id = ServerId::from_static("rust");
let caps = lsp_types::ServerCapabilities {
references_provider: Some(lsp_types::ReferencesProvider::Bool(true)),
..Default::default()
};
let (translator, mut 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": true})),
);
let translator = Arc::new(translator.with_notification_cache(cache));
let path = dir.path().join("main.rs");
fs::write(&path, "fn main() {}").unwrap();
let handle = {
let translator = Arc::clone(&translator);
let path = path.to_string_lossy().to_string();
tokio::spawn(async move {
translator
.handle_references(client_path(path), pos(1, 1), true, ResultContext::None)
.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/references");
write_response(
&mut server.read_half_stdin,
&request["id"],
serde_json::Value::Null,
)
.await;
let result = handle.await.unwrap().unwrap();
assert!(result.result.locations.is_empty());
assert_eq!(
result.index_coverage,
Some(IndexCoverage {
synced_documents: 1,
server_restarted: false,
})
);
let json = serde_json::to_value(&result).unwrap();
assert_eq!(
json["index_coverage"],
serde_json::json!({"synced_documents": 1})
);
translator.document_tracker.forget_server(&server_id);
assert_eq!(
translator.index_coverage(&server_id),
IndexCoverage {
synced_documents: 0,
server_restarted: true,
}
);
}
#[tokio::test(start_paused = true)]
async fn test_handle_implementation_returns_workspace_indexing_error_when_loading() {
let dir = TempDir::new().unwrap();
let server_id = ServerId::from_static("rust");
let caps = lsp_types::ServerCapabilities {
implementation_provider: Some(lsp_types::ImplementationProvider::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("main.rs");
fs::write(&path, "fn main() {}").unwrap();
let err = translator
.handle_implementation(
client_path(path.to_string_lossy().into_owned()),
pos(1, 1),
ResultContext::None,
)
.await
.unwrap_err();
assert_matches!(
err,
Error::WorkspaceIndexing { server_id: id, .. } if id == server_id
);
}
#[tokio::test(start_paused = true)]
async fn test_handle_type_definition_returns_workspace_indexing_error_when_loading() {
let dir = TempDir::new().unwrap();
let server_id = ServerId::from_static("rust");
let caps = lsp_types::ServerCapabilities {
type_definition_provider: Some(lsp_types::TypeDefinitionProvider::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("main.rs");
fs::write(&path, "fn main() {}").unwrap();
let err = translator
.handle_type_definition(
client_path(path.to_string_lossy().into_owned()),
pos(1, 1),
ResultContext::None,
)
.await
.unwrap_err();
assert_matches!(
err,
Error::WorkspaceIndexing { server_id: id, .. } if id == server_id
);
}
#[tokio::test]
async fn test_wait_for_indexing_ready_timeout_does_not_mutate_shared_state() {
let cache = Arc::new(Mutex::new(NotificationCache::new()));
let server_id = ServerId::from_static("rust");
cache.lock().await.observe_indexing_signal(
&server_id,
"experimental/serverStatus",
Some(&serde_json::json!({"quiescent": false})),
);
let translator = Translator::new().with_notification_cache(Arc::clone(&cache));
translator
.wait_for_indexing_ready_with(
&server_id,
Duration::from_millis(30),
Duration::from_millis(10),
)
.await
.unwrap_err();
assert_eq!(
cache.lock().await.indexing_state(&server_id),
IndexingState::Loading,
"a timed-out wait must not touch the shared entry -- it is still fresh, so it must \
still read as Loading for any other caller"
);
}
#[tokio::test]
async fn test_wait_for_indexing_ready_one_callers_timeout_does_not_release_another() {
let cache = Arc::new(Mutex::new(NotificationCache::new()));
let server_id = ServerId::from_static("rust");
cache.lock().await.observe_indexing_signal(
&server_id,
"experimental/serverStatus",
Some(&serde_json::json!({"quiescent": false})),
);
let translator = Arc::new(Translator::new().with_notification_cache(Arc::clone(&cache)));
let short = {
let translator = Arc::clone(&translator);
let server_id = server_id.clone();
tokio::spawn(async move {
translator
.wait_for_indexing_ready_with(
&server_id,
Duration::from_millis(80),
Duration::from_millis(10),
)
.await
})
};
let long = {
let translator = Arc::clone(&translator);
let server_id = server_id.clone();
tokio::spawn(async move {
translator
.wait_for_indexing_ready_with(
&server_id,
Duration::from_secs(30),
Duration::from_millis(10),
)
.await
})
};
let short_result = short.await.unwrap();
assert_matches!(
short_result,
Err(Error::WorkspaceIndexing { .. }),
"the short-timeout waiter must time out on its own schedule, got {short_result:?}"
);
tokio::time::sleep(Duration::from_millis(150)).await;
assert!(
!long.is_finished(),
"a concurrent caller's short timeout must never resolve another caller's \
independent wait early"
);
long.abort();
}
#[test]
fn test_extract_hover_contents_string() {
let marked_string = lsp_types::MarkedString::String("Test hover".to_string());
let contents = lsp_types::Contents::MarkedString(marked_string);
let result = extract_hover_contents(contents);
assert_eq!(result, "Test hover");
}
#[test]
fn test_extract_hover_contents_language_string() {
let marked_string = lsp_types::MarkedString::MarkedStringWithLanguage(
lsp_types::MarkedStringWithLanguage {
language: "rust".to_string(),
value: "fn main() {}".to_string(),
},
);
let contents = lsp_types::Contents::MarkedString(marked_string);
let result = extract_hover_contents(contents);
assert_eq!(result, "```rust\nfn main() {}\n```");
}
#[test]
fn test_extract_hover_contents_markup() {
let markup = lsp_types::MarkupContent {
kind: lsp_types::MarkupKind::Markdown,
value: "# Documentation".to_string(),
};
let contents = lsp_types::Contents::MarkupContent(markup);
let result = extract_hover_contents(contents);
assert_eq!(result, "# Documentation");
}
#[tokio::test]
async fn test_handle_definition_flattens_single_location() {
let dir = TempDir::new().unwrap();
let server_id = ServerId::from_static("rust");
let caps = lsp_types::ServerCapabilities {
definition_provider: Some(lsp_types::DefinitionProvider::Bool(true)),
..Default::default()
};
let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
let path = canonical_dir(&dir).join("main.rs");
fs::write(&path, "fn main() {}").unwrap();
let target_path = canonical_dir(&dir).join("target.rs");
fs::write(&target_path, "fn target() {}").unwrap();
let target_uri = Url::from_file_path(&target_path).unwrap().to_string();
let translator = Arc::new(translator);
let handle = {
let translator = Arc::clone(&translator);
let path = path.to_string_lossy().to_string();
tokio::spawn(async move {
translator
.handle_definition(client_path(path), Position::at(1, 1), ResultContext::None)
.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/definition");
write_response(
&mut server.read_half_stdin,
&request["id"],
serde_json::json!({
"uri": target_uri,
"range": {
"start": {"line": 0, "character": 0},
"end": {"line": 0, "character": 6}
}
}),
)
.await;
let result = timeout(Duration::from_secs(2), handle)
.await
.expect("handle_definition should not hang")
.unwrap()
.unwrap();
assert_eq!(result.locations.len(), 1);
assert_eq!(result.locations[0].uri, target_uri);
assert!(
!result.locations[0].out_of_workspace,
"a definition location inside the workspace root must not be marked out_of_workspace"
);
}
#[tokio::test]
async fn test_handle_definition_does_not_filter_out_of_workspace_location() {
let dir = TempDir::new().unwrap();
let server_id = ServerId::from_static("rust");
let caps = lsp_types::ServerCapabilities {
definition_provider: Some(lsp_types::DefinitionProvider::Bool(true)),
..Default::default()
};
let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
let path = dir.path().join("main.rs");
fs::write(&path, "fn main() {}").unwrap();
let outside_uri = "file:///outside/workspace/stdlib.rs";
let translator = Arc::new(translator);
let handle = {
let translator = Arc::clone(&translator);
let path = path.to_string_lossy().to_string();
tokio::spawn(async move {
translator
.handle_definition(client_path(path), Position::at(1, 1), ResultContext::None)
.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/definition");
write_response(
&mut server.read_half_stdin,
&request["id"],
serde_json::json!({
"uri": outside_uri,
"range": {
"start": {"line": 0, "character": 0},
"end": {"line": 0, "character": 6}
}
}),
)
.await;
let result = timeout(Duration::from_secs(2), handle)
.await
.expect("handle_definition should not hang")
.unwrap()
.unwrap();
assert_eq!(
result.locations.len(),
1,
"an out-of-workspace definition location (e.g. stdlib/a dependency) must be \
returned, not dropped"
);
assert_eq!(result.locations[0].uri, outside_uri);
assert!(
result.locations[0].out_of_workspace,
"a definition location outside every workspace root must be marked out_of_workspace"
);
}
#[tokio::test]
async fn test_handle_references_does_not_filter_out_of_workspace_location() {
let dir = TempDir::new().unwrap();
let server_id = ServerId::from_static("rust");
let caps = lsp_types::ServerCapabilities {
references_provider: Some(lsp_types::ReferencesProvider::Bool(true)),
..Default::default()
};
let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
let path = canonical_dir(&dir).join("main.rs");
fs::write(&path, "fn main() {}").unwrap();
let inside_path = canonical_dir(&dir).join("inside.rs");
fs::write(&inside_path, "fn used() {}").unwrap();
let inside_uri = Url::from_file_path(&inside_path).unwrap().to_string();
let outside_uri = "file:///outside/workspace/stdlib.rs";
let translator = Arc::new(translator);
let handle = {
let translator = Arc::clone(&translator);
let path = path.to_string_lossy().to_string();
tokio::spawn(async move {
translator
.handle_references(client_path(path), pos(1, 1), true, ResultContext::None)
.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/references");
write_response(
&mut server.read_half_stdin,
&request["id"],
serde_json::json!([
{
"uri": inside_uri,
"range": {
"start": {"line": 0, "character": 0},
"end": {"line": 0, "character": 4}
}
},
{
"uri": outside_uri,
"range": {
"start": {"line": 0, "character": 0},
"end": {"line": 0, "character": 4}
}
}
]),
)
.await;
let result = timeout(Duration::from_secs(2), handle)
.await
.expect("handle_references should not hang")
.unwrap()
.unwrap();
assert!(result.index_coverage.is_none());
assert!(
serde_json::to_value(&result)
.unwrap()
.get("index_coverage")
.is_none()
);
let result = result.result;
assert_eq!(
result.locations.len(),
2,
"both the in-workspace and out-of-workspace locations must survive"
);
assert!(result.locations.iter().any(|l| l.uri == inside_uri));
assert!(result.locations.iter().any(|l| l.uri == outside_uri));
assert!(
!result
.locations
.iter()
.find(|l| l.uri == inside_uri)
.unwrap()
.out_of_workspace,
"an in-workspace reference location must not be marked out_of_workspace"
);
assert!(
result
.locations
.iter()
.find(|l| l.uri == outside_uri)
.unwrap()
.out_of_workspace,
"an out-of-workspace reference location must be marked out_of_workspace"
);
}
#[tokio::test]
async fn test_handle_references_sets_truncated_flag_past_cap() {
let dir = TempDir::new().unwrap();
let server_id = ServerId::from_static("rust");
let caps = lsp_types::ServerCapabilities {
references_provider: Some(lsp_types::ReferencesProvider::Bool(true)),
..Default::default()
};
let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
let path = dir.path().join("main.rs");
fs::write(&path, "fn main() {}").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().to_string();
tokio::spawn(async move {
translator
.handle_references(client_path(path), pos(1, 1), true, ResultContext::None)
.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/references");
let locations: Vec<serde_json::Value> = (0..MAX_NORMALIZED_LOCATIONS + 500)
.map(|_| {
serde_json::json!({
"uri": uri,
"range": {
"start": {"line": 0, "character": 0},
"end": {"line": 0, "character": 4}
}
})
})
.collect();
write_response(
&mut server.read_half_stdin,
&request["id"],
serde_json::json!(locations),
)
.await;
let result = timeout(Duration::from_secs(5), handle)
.await
.expect("handle_references should not hang")
.unwrap()
.unwrap()
.result;
assert_eq!(result.locations.len(), MAX_NORMALIZED_LOCATIONS);
assert!(
result.truncated,
"a references response past MAX_NORMALIZED_LOCATIONS must set truncated: true"
);
}
#[tokio::test]
async fn test_handle_implementation_flattens_location_list() {
let dir = TempDir::new().unwrap();
let server_id = ServerId::from_static("rust");
let caps = lsp_types::ServerCapabilities {
implementation_provider: Some(lsp_types::ImplementationProvider::Bool(true)),
..Default::default()
};
let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
let path = dir.path().join("main.rs");
fs::write(&path, "fn main() {}").unwrap();
let first_impl_path = dir.path().join("impl_a.rs");
fs::write(&first_impl_path, "struct A;").unwrap();
let first_impl_uri = Url::from_file_path(&first_impl_path).unwrap().to_string();
let second_impl_path = dir.path().join("impl_b.rs");
fs::write(&second_impl_path, "struct B;").unwrap();
let second_impl_uri = Url::from_file_path(&second_impl_path).unwrap().to_string();
let translator = Arc::new(translator);
let handle = {
let translator = Arc::clone(&translator);
let path = path.to_string_lossy().to_string();
tokio::spawn(async move {
translator
.handle_implementation(
client_path(path),
Position::at(1, 1),
ResultContext::None,
)
.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/implementation");
write_response(
&mut server.read_half_stdin,
&request["id"],
serde_json::json!([
{
"uri": first_impl_uri,
"range": {
"start": {"line": 0, "character": 0},
"end": {"line": 0, "character": 9}
}
},
{
"uri": second_impl_uri,
"range": {
"start": {"line": 0, "character": 0},
"end": {"line": 0, "character": 9}
}
}
]),
)
.await;
let result = timeout(Duration::from_secs(2), handle)
.await
.expect("handle_implementation should not hang")
.unwrap()
.unwrap();
assert_eq!(result.locations.len(), 2);
assert_eq!(result.locations[0].uri, first_impl_uri);
assert_eq!(result.locations[1].uri, second_impl_uri);
}
#[tokio::test]
async fn test_handle_declaration_flattens_location_list() {
let dir = TempDir::new().unwrap();
let server_id = ServerId::from_static("rust");
let caps = lsp_types::ServerCapabilities {
declaration_provider: Some(lsp_types::DeclarationProvider::Bool(true)),
..Default::default()
};
let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
let path = dir.path().join("main.rs");
fs::write(&path, "fn main() {}").unwrap();
let decl_path = dir.path().join("decl.rs");
fs::write(&decl_path, "fn decl();").unwrap();
let decl_uri = Url::from_file_path(&decl_path).unwrap().to_string();
let translator = Arc::new(translator);
let handle = {
let translator = Arc::clone(&translator);
let path = client_path(&path);
tokio::spawn(async move {
translator
.handle_declaration(path, pos(1, 1), ResultContext::None)
.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/declaration");
write_response(
&mut server.read_half_stdin,
&request["id"],
serde_json::json!([{
"uri": decl_uri,
"range": {
"start": {"line": 0, "character": 3},
"end": {"line": 0, "character": 7}
}
}]),
)
.await;
let result = timeout(Duration::from_secs(2), handle)
.await
.expect("handle_declaration should not hang")
.unwrap()
.unwrap();
assert_eq!(result.locations.len(), 1);
assert_eq!(result.locations[0].uri, decl_uri);
}
#[tokio::test]
async fn test_handle_declaration_flattens_link_list() {
let dir = TempDir::new().unwrap();
let server_id = ServerId::from_static("rust");
let caps = lsp_types::ServerCapabilities {
declaration_provider: Some(lsp_types::DeclarationProvider::Bool(true)),
..Default::default()
};
let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
let path = dir.path().join("main.rs");
fs::write(&path, "fn main() {}").unwrap();
let target_path = dir.path().join("target.h");
fs::write(&target_path, "struct TargetType;").unwrap();
let target_uri = Url::from_file_path(&target_path).unwrap().to_string();
let translator = Arc::new(translator);
let handle = {
let translator = Arc::clone(&translator);
let path = client_path(&path);
tokio::spawn(async move {
translator
.handle_declaration(path, pos(1, 1), ResultContext::None)
.await
})
};
let mut wire = BufReader::new(&mut server.write_stdout);
let _opened = read_framed_message(&mut wire).await;
let request = read_framed_message(&mut wire).await;
assert_eq!(request["method"], "textDocument/declaration");
write_response(
&mut server.read_half_stdin,
&request["id"],
serde_json::json!([{
"targetUri": target_uri,
"targetRange": {
"start": {"line": 0, "character": 0},
"end": {"line": 0, "character": 18}
},
"targetSelectionRange": {
"start": {"line": 0, "character": 7},
"end": {"line": 0, "character": 17}
}
}]),
)
.await;
let result = timeout(Duration::from_secs(2), handle)
.await
.expect("handle_declaration should not hang")
.unwrap()
.unwrap();
assert_eq!(result.locations.len(), 1);
assert_eq!(result.locations[0].range.start.character, 8);
}
#[tokio::test]
async fn test_handle_declaration_null_response_is_empty() {
let dir = TempDir::new().unwrap();
let server_id = ServerId::from_static("rust");
let caps = lsp_types::ServerCapabilities {
declaration_provider: Some(lsp_types::DeclarationProvider::Bool(true)),
..Default::default()
};
let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
let path = dir.path().join("main.rs");
fs::write(&path, "fn main() {}").unwrap();
let translator = Arc::new(translator);
let handle = {
let translator = Arc::clone(&translator);
let path = client_path(&path);
tokio::spawn(async move {
translator
.handle_declaration(path, pos(1, 1), ResultContext::None)
.await
})
};
let mut wire = BufReader::new(&mut server.write_stdout);
let _opened = read_framed_message(&mut wire).await;
let request = read_framed_message(&mut wire).await;
write_response(
&mut server.read_half_stdin,
&request["id"],
serde_json::Value::Null,
)
.await;
let result = timeout(Duration::from_secs(2), handle)
.await
.expect("handle_declaration should not hang")
.unwrap()
.unwrap();
assert!(result.locations.is_empty());
}
#[tokio::test(start_paused = true)]
async fn test_handle_declaration_returns_workspace_indexing_error_when_loading() {
let dir = TempDir::new().unwrap();
let server_id = ServerId::from_static("rust");
let caps = lsp_types::ServerCapabilities {
declaration_provider: Some(lsp_types::DeclarationProvider::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("main.rs");
fs::write(&path, "fn main() {}").unwrap();
let err = translator
.handle_declaration(client_path(&path), pos(1, 1), ResultContext::None)
.await
.unwrap_err();
assert_matches!(
err,
Error::WorkspaceIndexing { server_id: id, .. } if id == server_id
);
}
#[tokio::test]
async fn test_handle_type_definition_flattens_definition_link_list() {
let dir = TempDir::new().unwrap();
let server_id = ServerId::from_static("rust");
let caps = lsp_types::ServerCapabilities {
type_definition_provider: Some(lsp_types::TypeDefinitionProvider::Bool(true)),
..Default::default()
};
let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
let path = dir.path().join("main.rs");
fs::write(&path, "fn main() {}").unwrap();
let target_path = dir.path().join("target_type.rs");
fs::write(&target_path, "struct TargetType;").unwrap();
let target_uri = Url::from_file_path(&target_path).unwrap().to_string();
let translator = Arc::new(translator);
let handle = {
let translator = Arc::clone(&translator);
let path = path.to_string_lossy().to_string();
tokio::spawn(async move {
translator
.handle_type_definition(
client_path(path),
Position::at(1, 1),
ResultContext::None,
)
.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/typeDefinition");
write_response(
&mut server.read_half_stdin,
&request["id"],
serde_json::json!([{
"targetUri": target_uri,
"targetRange": {
"start": {"line": 0, "character": 0},
"end": {"line": 0, "character": 18}
},
"targetSelectionRange": {
"start": {"line": 0, "character": 7},
"end": {"line": 0, "character": 17}
}
}]),
)
.await;
let result = timeout(Duration::from_secs(2), handle)
.await
.expect("handle_type_definition should not hang")
.unwrap()
.unwrap();
assert_eq!(result.locations.len(), 1);
assert_eq!(result.locations[0].uri, target_uri);
assert_eq!(result.locations[0].range.start.character, 8);
}
#[tokio::test]
async fn test_lsp_locations_to_mcp_reads_disk_once_per_distinct_file_line() {
let dir = TempDir::new().unwrap();
let mut uris = Vec::new();
for i in 0..3 {
let path = dir.path().join(format!("file{i}.rs"));
fs::write(&path, "hello").unwrap();
uris.push(path_to_uri(&path).unwrap());
}
let ctx = test_ctx_with(PositionEncoding::Utf8);
let locs: Vec<lsp_types::Location> = (0..300)
.map(|i| lsp_types::Location {
uri: uris[i % 3].clone(),
range: lsp_types::Range {
start: lsp_types::Position {
line: 0,
character: 0,
},
end: lsp_types::Position {
line: 0,
character: 3,
},
},
})
.collect();
let result = lsp_locations_to_mcp(locs, &ctx).await;
assert_eq!(result.locations.len(), 300);
assert!(!result.truncated);
assert!(
result.locations.iter().all(|l| l.range.end.character == 4),
"MCP columns are 1-based, so LSP byte offset 3 in all-ASCII \"hello\" must convert \
to 4"
);
assert_eq!(
lock_std(&ctx.line_cache).entries.len(),
3,
"300 locations across 3 distinct files must populate the cache with exactly 3 \
entries (one disk read per distinct file/line), not one per location"
);
}
#[tokio::test]
async fn test_lsp_locations_to_mcp_truncates_to_max_normalized_locations() {
let ctx = test_ctx();
let uri = test_uri();
let locs: Vec<lsp_types::Location> = (0..MAX_NORMALIZED_LOCATIONS + 500)
.map(|_| lsp_types::Location {
uri: uri.clone(),
range: lsp_types::Range {
start: lsp_types::Position {
line: 0,
character: 0,
},
end: lsp_types::Position {
line: 0,
character: 1,
},
},
})
.collect();
let result = lsp_locations_to_mcp(locs, &ctx).await;
assert_eq!(result.locations.len(), MAX_NORMALIZED_LOCATIONS);
assert!(
result.truncated,
"a response naming more than MAX_NORMALIZED_LOCATIONS must report truncated: true"
);
}
#[test]
fn test_item_budget_admit_spends_shared_allowance() {
let mut budget = ItemBudget::new();
let first = budget.admit(vec![0u8; MAX_NORMALIZED_LOCATIONS - 1]);
assert_eq!(first.len(), MAX_NORMALIZED_LOCATIONS - 1);
assert!(!budget.truncated());
let second = budget.admit(vec![0u8; 5]);
assert_eq!(second.len(), 1);
assert!(budget.truncated());
assert_eq!(budget.admit(vec![0u8; 3]).len(), 0);
}
#[test]
fn test_item_budget_admit_whole_never_splits_a_list() {
let mut budget = ItemBudget::new();
assert!(
budget
.admit_whole(vec![0u8; MAX_NORMALIZED_LOCATIONS - 2])
.is_some()
);
assert!(budget.admit_whole(vec![0u8; 3]).is_none());
assert!(budget.truncated());
assert!(budget.admit_whole(vec![0u8; 2]).is_some());
}
#[test]
fn test_item_budget_with_cap_admits_a_lazy_sequence_up_to_the_cap() {
let mut budget = ItemBudget::with_cap(3);
assert_eq!(budget.admit_iter(0..3), [0, 1, 2]);
assert!(!budget.truncated());
let mut budget = ItemBudget::with_cap(3);
assert_eq!(budget.admit_iter(0..), [0, 1, 2]);
assert!(budget.truncated());
assert_eq!(budget.admit_iter(0..2).len(), 0);
}
#[test]
fn test_item_budget_exactly_at_cap_is_not_truncated() {
let mut budget = ItemBudget::new();
let items = budget.admit(vec![0u8; MAX_NORMALIZED_LOCATIONS]);
assert_eq!(items.len(), MAX_NORMALIZED_LOCATIONS);
assert!(!budget.truncated());
}
}