1use std::time::Duration;
5
6use lsp_types::{
7 HoverParams as LspHoverParams, PartialResultParams, ReferenceContext, ReferenceParams,
8 TextDocumentIdentifier, TextDocumentPositionParams, WorkDoneProgressParams,
9};
10use tokio::time::Instant;
11
12use super::Translator;
13use super::dto::{
14 DefinitionResult, HoverResult, Location, LocationsResult, Position, ReferencesResult,
15};
16use super::encoding_ctx::EncodingCtx;
17use super::routing::{Capability, IndexingGate};
18use crate::bridge::IndexingState;
19use crate::bridge::indexing::{
20 DEFAULT_INDEXING_READY_TIMEOUT_SECS, INDEXING_STALENESS_BOUND, PROGRESS_LATCH_IDLE,
21 PROGRESS_SETTLE,
22};
23use crate::config::{ServerId, ToolKind};
24use crate::error::{Error, Result};
25
26pub(super) const INDEXING_READY_TIMEOUT: Duration =
40 Duration::from_secs(DEFAULT_INDEXING_READY_TIMEOUT_SECS);
41
42const INDEXING_POLL_INTERVAL: Duration = Duration::from_millis(100);
47
48const _: () = assert!(
53 INDEXING_STALENESS_BOUND.as_nanos() > INDEXING_READY_TIMEOUT.as_nanos(),
54 "INDEXING_STALENESS_BOUND must be greater than INDEXING_READY_TIMEOUT"
55);
56
57const _: () = assert!(
62 PROGRESS_SETTLE.as_nanos() < PROGRESS_LATCH_IDLE.as_nanos(),
63 "PROGRESS_SETTLE must be less than PROGRESS_LATCH_IDLE"
64);
65
66const _: () = assert!(
70 PROGRESS_SETTLE.as_nanos() < INDEXING_READY_TIMEOUT.as_nanos(),
71 "PROGRESS_SETTLE must be less than INDEXING_READY_TIMEOUT"
72);
73
74fn definition_to_locations(definition: lsp_types::Definition) -> Vec<lsp_types::Location> {
76 match definition {
77 lsp_types::Definition::Location(loc) => vec![loc],
78 lsp_types::Definition::LocationList(locs) => locs,
79 }
80}
81
82fn definition_link_to_location(link: lsp_types::DefinitionLink) -> lsp_types::Location {
84 lsp_types::Location {
85 uri: link.target_uri,
86 range: link.target_selection_range,
87 }
88}
89
90pub(super) const MAX_NORMALIZED_LOCATIONS: usize = 10_000;
101
102async fn lsp_locations_to_mcp(
119 mut locs: Vec<lsp_types::Location>,
120 ctx: &EncodingCtx,
121) -> NormalizedLocations {
122 let truncated = locs.len() > MAX_NORMALIZED_LOCATIONS;
123 if truncated {
124 tracing::warn!(
125 reported = locs.len(),
126 cap = MAX_NORMALIZED_LOCATIONS,
127 "LSP response location count exceeds MAX_NORMALIZED_LOCATIONS; truncating"
128 );
129 }
130 locs.truncate(MAX_NORMALIZED_LOCATIONS);
131 let mut locations = Vec::with_capacity(locs.len());
132 for loc in locs {
133 locations.push(Location {
134 uri: loc.uri.to_string(),
135 range: ctx.normalize_range(&loc.uri, loc.range).await,
136 out_of_workspace: ctx.is_out_of_workspace(&loc.uri),
137 });
138 }
139 NormalizedLocations {
140 locations,
141 truncated,
142 positions_degraded: ctx.positions_degraded(),
143 }
144}
145
146struct NormalizedLocations {
156 locations: Vec<Location>,
157 truncated: bool,
158 positions_degraded: bool,
159}
160
161enum GotoKind {
166 Definition(lsp_types::Definition),
168 DefinitionLinkList(Vec<lsp_types::DefinitionLink>),
170}
171
172trait GotoResponse {
176 fn into_kind(self) -> GotoKind;
179}
180
181impl GotoResponse for lsp_types::DefinitionResponse {
182 fn into_kind(self) -> GotoKind {
183 match self {
184 Self::Definition(def) => GotoKind::Definition(def),
185 Self::DefinitionLinkList(links) => GotoKind::DefinitionLinkList(links),
186 }
187 }
188}
189
190impl GotoResponse for lsp_types::ImplementationResponse {
191 fn into_kind(self) -> GotoKind {
192 match self {
193 Self::Definition(def) => GotoKind::Definition(def),
194 Self::DefinitionLinkList(links) => GotoKind::DefinitionLinkList(links),
195 }
196 }
197}
198
199impl GotoResponse for lsp_types::TypeDefinitionResponse {
200 fn into_kind(self) -> GotoKind {
201 match self {
202 Self::Definition(def) => GotoKind::Definition(def),
203 Self::DefinitionLinkList(links) => GotoKind::DefinitionLinkList(links),
204 }
205 }
206}
207
208async fn goto_response_to_locations<R: GotoResponse>(
212 response: Option<R>,
213 ctx: &EncodingCtx,
214) -> NormalizedLocations {
215 let lsp_locs = match response.map(GotoResponse::into_kind) {
216 Some(GotoKind::Definition(def)) => definition_to_locations(def),
217 Some(GotoKind::DefinitionLinkList(links)) => {
218 links.into_iter().map(definition_link_to_location).collect()
219 }
220 None => vec![],
221 };
222 lsp_locations_to_mcp(lsp_locs, ctx).await
223}
224
225trait GotoParams: Sized {
229 fn from_position(text_document_position_params: TextDocumentPositionParams) -> Self;
232}
233
234impl GotoParams for lsp_types::DefinitionParams {
235 fn from_position(text_document_position_params: TextDocumentPositionParams) -> Self {
236 Self {
237 text_document_position_params,
238 work_done_progress_params: WorkDoneProgressParams::default(),
239 partial_result_params: PartialResultParams::default(),
240 }
241 }
242}
243
244impl GotoParams for lsp_types::ImplementationParams {
245 fn from_position(text_document_position_params: TextDocumentPositionParams) -> Self {
246 Self {
247 text_document_position_params,
248 work_done_progress_params: WorkDoneProgressParams::default(),
249 partial_result_params: PartialResultParams::default(),
250 }
251 }
252}
253
254impl GotoParams for lsp_types::TypeDefinitionParams {
255 fn from_position(text_document_position_params: TextDocumentPositionParams) -> Self {
256 Self {
257 text_document_position_params,
258 work_done_progress_params: WorkDoneProgressParams::default(),
259 partial_result_params: PartialResultParams::default(),
260 }
261 }
262}
263
264#[allow(deprecated)]
272fn extract_hover_contents(contents: lsp_types::Contents) -> String {
273 match contents {
274 lsp_types::Contents::MarkedString(marked_string) => marked_string_to_string(marked_string),
275 lsp_types::Contents::MarkedStringList(marked_strings) => marked_strings
276 .into_iter()
277 .map(marked_string_to_string)
278 .collect::<Vec<_>>()
279 .join("\n\n"),
280 lsp_types::Contents::MarkupContent(markup) => markup.value,
281 }
282}
283
284#[allow(deprecated)]
286fn marked_string_to_string(marked: lsp_types::MarkedString) -> String {
287 match marked {
288 lsp_types::MarkedString::String(s) => s,
289 lsp_types::MarkedString::MarkedStringWithLanguage(ls) => {
290 format!("```{}\n{}\n```", ls.language, ls.value)
291 }
292 }
293}
294
295impl Translator {
296 pub(super) async fn wait_for_indexing_ready(&self, server_id: &ServerId) -> Result<()> {
320 self.wait_for_indexing_ready_with(
321 server_id,
322 self.indexing_ready_timeout,
323 INDEXING_POLL_INTERVAL,
324 )
325 .await
326 }
327
328 async fn wait_for_indexing_ready_with(
343 &self,
344 server_id: &ServerId,
345 timeout: Duration,
346 poll_interval: Duration,
347 ) -> Result<()> {
348 let Some(cache) = self.notification_cache.as_ref() else {
349 return Ok(());
350 };
351
352 let start = Instant::now();
353 let deadline = start + timeout;
354 loop {
355 let state = cache.lock().await.indexing_state(server_id);
356 if state != IndexingState::Loading {
357 return Ok(());
358 }
359 let remaining = deadline.saturating_duration_since(Instant::now());
360 if remaining.is_zero() {
361 return Err(Error::WorkspaceIndexing {
362 server_id: server_id.clone(),
363 elapsed_secs: start.elapsed().as_secs(),
364 });
365 }
366 tokio::time::sleep(poll_interval.min(remaining)).await;
367 }
368 }
369
370 pub async fn handle_hover(&self, file_path: String, position: Position) -> Result<HoverResult> {
379 let Position { line, character } = position;
380 let (server_id, client, uri) = self
381 .prepare_gated_document(
382 &file_path,
383 ToolKind::Hover,
384 Capability::Hover,
385 IndexingGate::Required,
386 )
387 .await?;
388 let ctx = self.encoding_ctx(&server_id);
389 let lsp_position = ctx.to_lsp(&uri, line, character).await;
390 let response_uri = uri.clone();
391
392 let params = LspHoverParams {
393 text_document_position_params: TextDocumentPositionParams {
394 text_document: TextDocumentIdentifier { uri },
395 position: lsp_position,
396 },
397 work_done_progress_params: WorkDoneProgressParams::default(),
398 };
399
400 let response = client
401 .request_typed::<lsp_types::HoverRequest>(params, client.request_timeout())
402 .await?;
403
404 let result = match response {
405 Some(hover) => {
406 let contents = extract_hover_contents(hover.contents);
407 let range = match hover.range {
408 Some(r) => Some(ctx.normalize_range(&response_uri, r).await),
409 None => None,
410 };
411 HoverResult {
412 contents,
413 range,
414 positions_degraded: ctx.positions_degraded(),
415 }
416 }
417 None => HoverResult {
418 contents: "No hover information available".to_string(),
419 range: None,
420 positions_degraded: ctx.positions_degraded(),
421 },
422 };
423
424 Ok(result)
425 }
426
427 async fn handle_goto<R, T>(
441 &self,
442 file_path: &str,
443 position: Position,
444 tool: ToolKind,
445 capability: Capability,
446 ) -> Result<NormalizedLocations>
447 where
448 R: lsp_types::Request<Result = Option<T>>,
449 R::Params: GotoParams,
450 T: GotoResponse,
451 {
452 let Position { line, character } = position;
453 let (server_id, client, uri) = self
454 .prepare_gated_document(file_path, tool, capability, IndexingGate::Required)
455 .await?;
456 let ctx = self.encoding_ctx(&server_id);
457 let lsp_position = ctx.to_lsp(&uri, line, character).await;
458
459 let params = R::Params::from_position(TextDocumentPositionParams {
460 text_document: TextDocumentIdentifier { uri },
461 position: lsp_position,
462 });
463
464 let response = client
465 .request_typed::<R>(params, client.request_timeout())
466 .await?;
467
468 Ok(goto_response_to_locations(response, &ctx).await)
469 }
470
471 pub async fn handle_definition(
480 &self,
481 file_path: String,
482 position: Position,
483 ) -> Result<DefinitionResult> {
484 let NormalizedLocations {
485 locations,
486 truncated,
487 positions_degraded,
488 } = self
489 .handle_goto::<lsp_types::DefinitionRequest, _>(
490 &file_path,
491 position,
492 ToolKind::Definition,
493 Capability::Definition,
494 )
495 .await?;
496
497 Ok(DefinitionResult {
498 locations,
499 truncated,
500 positions_degraded,
501 })
502 }
503
504 pub async fn handle_references(
513 &self,
514 file_path: String,
515 position: Position,
516 include_declaration: bool,
517 ) -> Result<ReferencesResult> {
518 let Position { line, character } = position;
519 let (server_id, client, uri) = self
520 .prepare_gated_document(
521 &file_path,
522 ToolKind::References,
523 Capability::References,
524 IndexingGate::Required,
525 )
526 .await?;
527 let ctx = self.encoding_ctx(&server_id);
528 let lsp_position = ctx.to_lsp(&uri, line, character).await;
529
530 let params = ReferenceParams {
531 text_document_position_params: TextDocumentPositionParams {
532 text_document: TextDocumentIdentifier { uri },
533 position: lsp_position,
534 },
535 work_done_progress_params: WorkDoneProgressParams::default(),
536 partial_result_params: PartialResultParams::default(),
537 context: ReferenceContext {
538 include_declaration,
539 },
540 };
541
542 let response = client
543 .request_typed::<lsp_types::ReferencesRequest>(params, client.request_timeout())
544 .await?;
545
546 let locations = response.unwrap_or_default();
547 let NormalizedLocations {
548 locations,
549 truncated,
550 positions_degraded,
551 } = lsp_locations_to_mcp(locations, &ctx).await;
552
553 Ok(ReferencesResult {
554 locations,
555 truncated,
556 positions_degraded,
557 })
558 }
559
560 pub async fn handle_implementation(
571 &self,
572 file_path: String,
573 position: Position,
574 ) -> Result<LocationsResult> {
575 let NormalizedLocations {
576 locations,
577 truncated,
578 positions_degraded,
579 } = self
580 .handle_goto::<lsp_types::ImplementationRequest, _>(
581 &file_path,
582 position,
583 ToolKind::Implementation,
584 Capability::Implementation,
585 )
586 .await?;
587
588 Ok(LocationsResult {
589 locations,
590 truncated,
591 positions_degraded,
592 })
593 }
594
595 pub async fn handle_type_definition(
607 &self,
608 file_path: String,
609 position: Position,
610 ) -> Result<LocationsResult> {
611 let NormalizedLocations {
612 locations,
613 truncated,
614 positions_degraded,
615 } = self
616 .handle_goto::<lsp_types::TypeDefinitionRequest, _>(
617 &file_path,
618 position,
619 ToolKind::TypeDefinition,
620 Capability::TypeDefinition,
621 )
622 .await?;
623
624 Ok(LocationsResult {
625 locations,
626 truncated,
627 positions_degraded,
628 })
629 }
630}
631
632#[cfg(test)]
633#[allow(clippy::unwrap_used, clippy::expect_used, deprecated)]
634mod tests {
635 use std::fs;
636 use std::sync::Arc;
637 use std::time::Duration;
638
639 use tempfile::TempDir;
640 use tokio::io::BufReader;
641 use tokio::sync::Mutex;
642 use tokio::time::timeout;
643 use url::Url;
644
645 use super::*;
646 use crate::bridge::encoding::PositionEncoding;
647 use crate::bridge::translator::testing::*;
648 use crate::bridge::{NotificationCache, lock_std, path_to_uri};
649 use crate::config::ServerId;
650
651 #[tokio::test]
656 async fn test_wait_for_indexing_ready_without_cache_is_noop() {
657 let translator = Translator::new();
659 let server_id = ServerId::from("rust");
660
661 translator
662 .wait_for_indexing_ready(&server_id)
663 .await
664 .unwrap();
665 }
666
667 #[tokio::test]
668 async fn test_wait_for_indexing_ready_unknown_state_is_noop() {
669 let translator = Translator::new()
670 .with_notification_cache(Arc::new(Mutex::new(NotificationCache::new())));
671 let server_id = ServerId::from("rust");
672
673 translator
674 .wait_for_indexing_ready(&server_id)
675 .await
676 .unwrap();
677 }
678
679 #[tokio::test]
680 async fn test_wait_for_indexing_ready_ready_state_is_noop() {
681 let cache = Arc::new(Mutex::new(NotificationCache::new()));
682 let server_id = ServerId::from("rust");
683 cache.lock().await.observe_indexing_signal(
684 &server_id,
685 "experimental/serverStatus",
686 Some(&serde_json::json!({"quiescent": true})),
687 );
688 let translator = Translator::new().with_notification_cache(cache);
689
690 translator
691 .wait_for_indexing_ready(&server_id)
692 .await
693 .unwrap();
694 }
695
696 #[tokio::test(start_paused = true)]
702 async fn test_wait_for_indexing_ready_uses_configured_timeout_override() {
703 let cache = Arc::new(Mutex::new(NotificationCache::new()));
704 let server_id = ServerId::from("rust");
705 cache.lock().await.observe_indexing_signal(
706 &server_id,
707 "experimental/serverStatus",
708 Some(&serde_json::json!({"quiescent": false})),
709 );
710 let translator = Translator::new()
711 .with_notification_cache(cache)
712 .with_indexing_ready_timeout(Duration::from_secs(5));
713
714 let start = Instant::now();
715 let err = translator
716 .wait_for_indexing_ready(&server_id)
717 .await
718 .unwrap_err();
719
720 assert!(matches!(err, Error::WorkspaceIndexing { elapsed_secs, .. } if elapsed_secs == 5));
721 assert_eq!(start.elapsed(), Duration::from_secs(5));
722 }
723
724 #[tokio::test]
725 async fn test_wait_for_indexing_ready_loading_times_out() {
726 let cache = Arc::new(Mutex::new(NotificationCache::new()));
727 let server_id = ServerId::from("rust");
728 cache.lock().await.observe_indexing_signal(
729 &server_id,
730 "experimental/serverStatus",
731 Some(&serde_json::json!({"quiescent": false})),
732 );
733 let translator = Translator::new().with_notification_cache(cache);
734
735 let err = translator
736 .wait_for_indexing_ready_with(
737 &server_id,
738 Duration::from_millis(50),
739 Duration::from_millis(10),
740 )
741 .await
742 .unwrap_err();
743
744 assert!(matches!(
745 err,
746 Error::WorkspaceIndexing { server_id: id, .. } if id == ServerId::from("rust")
747 ));
748 }
749
750 #[tokio::test]
751 async fn test_wait_for_indexing_ready_returns_ok_once_signaled_ready() {
752 let cache = Arc::new(Mutex::new(NotificationCache::new()));
753 let server_id = ServerId::from("rust");
754 cache.lock().await.observe_indexing_signal(
755 &server_id,
756 "experimental/serverStatus",
757 Some(&serde_json::json!({"quiescent": false})),
758 );
759 let translator = Translator::new().with_notification_cache(Arc::clone(&cache));
760
761 let waiter = {
762 let server_id = server_id.clone();
763 tokio::spawn(async move {
764 translator
765 .wait_for_indexing_ready_with(
766 &server_id,
767 Duration::from_secs(5),
768 Duration::from_millis(10),
769 )
770 .await
771 })
772 };
773
774 tokio::time::sleep(Duration::from_millis(30)).await;
775 cache.lock().await.observe_indexing_signal(
776 &server_id,
777 "experimental/serverStatus",
778 Some(&serde_json::json!({"quiescent": true})),
779 );
780
781 timeout(Duration::from_secs(1), waiter)
782 .await
783 .expect("waiter task timed out")
784 .expect("waiter task panicked")
785 .expect("expected Ok once quiescent");
786 }
787
788 #[tokio::test(start_paused = true)]
794 async fn test_handle_hover_returns_workspace_indexing_error_when_loading() {
795 let dir = TempDir::new().unwrap();
796 let server_id = ServerId::from("rust");
797 let caps = lsp_types::ServerCapabilities {
798 hover_provider: Some(lsp_types::HoverProvider::Bool(true)),
799 ..Default::default()
800 };
801 let (translator, _server) = translator_with_capabilities(&dir, &server_id, caps);
802
803 let cache = Arc::new(Mutex::new(NotificationCache::new()));
804 cache.lock().await.observe_indexing_signal(
805 &server_id,
806 "experimental/serverStatus",
807 Some(&serde_json::json!({"quiescent": false})),
808 );
809 let translator = translator.with_notification_cache(cache);
810
811 let path = dir.path().join("main.rs");
812 fs::write(&path, "fn main() {}").unwrap();
813
814 let err = translator
815 .handle_hover(path.to_string_lossy().to_string(), pos(1, 1))
816 .await
817 .unwrap_err();
818
819 assert!(matches!(
820 err,
821 Error::WorkspaceIndexing { server_id: id, elapsed_secs: 30 } if id == server_id
822 ));
823 }
824
825 #[tokio::test]
828 async fn test_handle_hover_dispatches_when_indexing_ready() {
829 let dir = TempDir::new().unwrap();
830 let server_id = ServerId::from("rust");
831 let caps = lsp_types::ServerCapabilities {
832 hover_provider: Some(lsp_types::HoverProvider::Bool(true)),
833 ..Default::default()
834 };
835 let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
836
837 let cache = Arc::new(Mutex::new(NotificationCache::new()));
838 cache.lock().await.observe_indexing_signal(
839 &server_id,
840 "experimental/serverStatus",
841 Some(&serde_json::json!({"quiescent": true})),
842 );
843 let translator = Arc::new(translator.with_notification_cache(cache));
844
845 let path = dir.path().join("main.rs");
846 fs::write(&path, "fn main() {}").unwrap();
847
848 let handle = {
849 let translator = Arc::clone(&translator);
850 let path = path.to_string_lossy().to_string();
851 tokio::spawn(async move { translator.handle_hover(path, pos(1, 1)).await })
852 };
853
854 let mut wire = BufReader::new(&mut server.write_stdout);
855 let opened = read_framed_message(&mut wire).await;
856 assert_eq!(opened["method"], "textDocument/didOpen");
857 let request = read_framed_message(&mut wire).await;
858 assert_eq!(request["method"], "textDocument/hover");
859
860 write_response(
861 &mut server.read_half_stdin,
862 &request["id"],
863 serde_json::json!({
864 "contents": {"kind": "markdown", "value": "hover text"}
865 }),
866 )
867 .await;
868
869 let result = handle.await.unwrap().unwrap();
870 assert_eq!(result.contents, "hover text");
871 }
872
873 #[tokio::test(start_paused = true)]
877 async fn test_handle_definition_returns_workspace_indexing_error_when_loading() {
878 let dir = TempDir::new().unwrap();
879 let server_id = ServerId::from("rust");
880 let caps = lsp_types::ServerCapabilities {
881 definition_provider: Some(lsp_types::DefinitionProvider::Bool(true)),
882 ..Default::default()
883 };
884 let (translator, _server) = translator_with_capabilities(&dir, &server_id, caps);
885
886 let cache = Arc::new(Mutex::new(NotificationCache::new()));
887 cache.lock().await.observe_indexing_signal(
888 &server_id,
889 "experimental/serverStatus",
890 Some(&serde_json::json!({"quiescent": false})),
891 );
892 let translator = translator.with_notification_cache(cache);
893
894 let path = dir.path().join("main.rs");
895 fs::write(&path, "fn main() {}").unwrap();
896
897 let err = translator
898 .handle_definition(path.to_string_lossy().to_string(), pos(1, 1))
899 .await
900 .unwrap_err();
901
902 assert!(matches!(
903 err,
904 Error::WorkspaceIndexing { server_id: id, .. } if id == server_id
905 ));
906 }
907
908 #[tokio::test]
911 async fn test_handle_definition_dispatches_when_indexing_ready() {
912 let dir = TempDir::new().unwrap();
913 let server_id = ServerId::from("rust");
914 let caps = lsp_types::ServerCapabilities {
915 definition_provider: Some(lsp_types::DefinitionProvider::Bool(true)),
916 ..Default::default()
917 };
918 let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
919
920 let cache = Arc::new(Mutex::new(NotificationCache::new()));
921 cache.lock().await.observe_indexing_signal(
922 &server_id,
923 "experimental/serverStatus",
924 Some(&serde_json::json!({"quiescent": true})),
925 );
926 let translator = Arc::new(translator.with_notification_cache(cache));
927
928 let path = dir.path().join("main.rs");
929 fs::write(&path, "fn main() {}").unwrap();
930
931 let handle = {
932 let translator = Arc::clone(&translator);
933 let path = path.to_string_lossy().to_string();
934 tokio::spawn(async move { translator.handle_definition(path, pos(1, 1)).await })
935 };
936
937 let mut wire = BufReader::new(&mut server.write_stdout);
938 let opened = read_framed_message(&mut wire).await;
939 assert_eq!(opened["method"], "textDocument/didOpen");
940 let request = read_framed_message(&mut wire).await;
941 assert_eq!(request["method"], "textDocument/definition");
942
943 write_response(
944 &mut server.read_half_stdin,
945 &request["id"],
946 serde_json::Value::Null,
947 )
948 .await;
949
950 let result = handle.await.unwrap().unwrap();
951 assert!(result.locations.is_empty());
952 }
953
954 #[tokio::test(start_paused = true)]
958 async fn test_handle_references_returns_workspace_indexing_error_when_loading() {
959 let dir = TempDir::new().unwrap();
960 let server_id = ServerId::from("rust");
961 let caps = lsp_types::ServerCapabilities {
962 references_provider: Some(lsp_types::ReferencesProvider::Bool(true)),
963 ..Default::default()
964 };
965 let (translator, _server) = translator_with_capabilities(&dir, &server_id, caps);
966
967 let cache = Arc::new(Mutex::new(NotificationCache::new()));
968 cache.lock().await.observe_indexing_signal(
969 &server_id,
970 "experimental/serverStatus",
971 Some(&serde_json::json!({"quiescent": false})),
972 );
973 let translator = translator.with_notification_cache(cache);
974
975 let path = dir.path().join("main.rs");
976 fs::write(&path, "fn main() {}").unwrap();
977
978 let err = translator
979 .handle_references(path.to_string_lossy().to_string(), pos(1, 1), true)
980 .await
981 .unwrap_err();
982
983 assert!(matches!(
984 err,
985 Error::WorkspaceIndexing { server_id: id, .. } if id == server_id
986 ));
987 }
988
989 #[tokio::test]
992 async fn test_handle_references_dispatches_when_indexing_ready() {
993 let dir = TempDir::new().unwrap();
994 let server_id = ServerId::from("rust");
995 let caps = lsp_types::ServerCapabilities {
996 references_provider: Some(lsp_types::ReferencesProvider::Bool(true)),
997 ..Default::default()
998 };
999 let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
1000
1001 let cache = Arc::new(Mutex::new(NotificationCache::new()));
1002 cache.lock().await.observe_indexing_signal(
1003 &server_id,
1004 "experimental/serverStatus",
1005 Some(&serde_json::json!({"quiescent": true})),
1006 );
1007 let translator = Arc::new(translator.with_notification_cache(cache));
1008
1009 let path = dir.path().join("main.rs");
1010 fs::write(&path, "fn main() {}").unwrap();
1011
1012 let handle = {
1013 let translator = Arc::clone(&translator);
1014 let path = path.to_string_lossy().to_string();
1015 tokio::spawn(async move { translator.handle_references(path, pos(1, 1), true).await })
1016 };
1017
1018 let mut wire = BufReader::new(&mut server.write_stdout);
1019 let opened = read_framed_message(&mut wire).await;
1020 assert_eq!(opened["method"], "textDocument/didOpen");
1021 let request = read_framed_message(&mut wire).await;
1022 assert_eq!(request["method"], "textDocument/references");
1023
1024 write_response(
1025 &mut server.read_half_stdin,
1026 &request["id"],
1027 serde_json::Value::Null,
1028 )
1029 .await;
1030
1031 let result = handle.await.unwrap().unwrap();
1032 assert!(result.locations.is_empty());
1033 }
1034
1035 #[tokio::test(start_paused = true)]
1040 async fn test_handle_implementation_returns_workspace_indexing_error_when_loading() {
1041 let dir = TempDir::new().unwrap();
1042 let server_id = ServerId::from("rust");
1043 let caps = lsp_types::ServerCapabilities {
1044 implementation_provider: Some(lsp_types::ImplementationProvider::Bool(true)),
1045 ..Default::default()
1046 };
1047 let (translator, _server) = translator_with_capabilities(&dir, &server_id, caps);
1048
1049 let cache = Arc::new(Mutex::new(NotificationCache::new()));
1050 cache.lock().await.observe_indexing_signal(
1051 &server_id,
1052 "experimental/serverStatus",
1053 Some(&serde_json::json!({"quiescent": false})),
1054 );
1055 let translator = translator.with_notification_cache(cache);
1056
1057 let path = dir.path().join("main.rs");
1058 fs::write(&path, "fn main() {}").unwrap();
1059
1060 let err = translator
1061 .handle_implementation(path.to_string_lossy().to_string(), pos(1, 1))
1062 .await
1063 .unwrap_err();
1064
1065 assert!(matches!(
1066 err,
1067 Error::WorkspaceIndexing { server_id: id, .. } if id == server_id
1068 ));
1069 }
1070
1071 #[tokio::test(start_paused = true)]
1073 async fn test_handle_type_definition_returns_workspace_indexing_error_when_loading() {
1074 let dir = TempDir::new().unwrap();
1075 let server_id = ServerId::from("rust");
1076 let caps = lsp_types::ServerCapabilities {
1077 type_definition_provider: Some(lsp_types::TypeDefinitionProvider::Bool(true)),
1078 ..Default::default()
1079 };
1080 let (translator, _server) = translator_with_capabilities(&dir, &server_id, caps);
1081
1082 let cache = Arc::new(Mutex::new(NotificationCache::new()));
1083 cache.lock().await.observe_indexing_signal(
1084 &server_id,
1085 "experimental/serverStatus",
1086 Some(&serde_json::json!({"quiescent": false})),
1087 );
1088 let translator = translator.with_notification_cache(cache);
1089
1090 let path = dir.path().join("main.rs");
1091 fs::write(&path, "fn main() {}").unwrap();
1092
1093 let err = translator
1094 .handle_type_definition(path.to_string_lossy().to_string(), pos(1, 1))
1095 .await
1096 .unwrap_err();
1097
1098 assert!(matches!(
1099 err,
1100 Error::WorkspaceIndexing { server_id: id, .. } if id == server_id
1101 ));
1102 }
1103
1104 #[tokio::test]
1113 async fn test_wait_for_indexing_ready_timeout_does_not_mutate_shared_state() {
1114 let cache = Arc::new(Mutex::new(NotificationCache::new()));
1115 let server_id = ServerId::from("rust");
1116 cache.lock().await.observe_indexing_signal(
1117 &server_id,
1118 "experimental/serverStatus",
1119 Some(&serde_json::json!({"quiescent": false})),
1120 );
1121 let translator = Translator::new().with_notification_cache(Arc::clone(&cache));
1122
1123 translator
1124 .wait_for_indexing_ready_with(
1125 &server_id,
1126 Duration::from_millis(30),
1127 Duration::from_millis(10),
1128 )
1129 .await
1130 .unwrap_err();
1131
1132 assert_eq!(
1133 cache.lock().await.indexing_state(&server_id),
1134 IndexingState::Loading,
1135 "a timed-out wait must not touch the shared entry -- it is still fresh, so it must \
1136 still read as Loading for any other caller"
1137 );
1138 }
1139
1140 #[tokio::test]
1147 async fn test_wait_for_indexing_ready_one_callers_timeout_does_not_release_another() {
1148 let cache = Arc::new(Mutex::new(NotificationCache::new()));
1149 let server_id = ServerId::from("rust");
1150 cache.lock().await.observe_indexing_signal(
1151 &server_id,
1152 "experimental/serverStatus",
1153 Some(&serde_json::json!({"quiescent": false})),
1154 );
1155 let translator = Arc::new(Translator::new().with_notification_cache(Arc::clone(&cache)));
1156
1157 let short = {
1158 let translator = Arc::clone(&translator);
1159 let server_id = server_id.clone();
1160 tokio::spawn(async move {
1161 translator
1162 .wait_for_indexing_ready_with(
1163 &server_id,
1164 Duration::from_millis(80),
1165 Duration::from_millis(10),
1166 )
1167 .await
1168 })
1169 };
1170 let long = {
1171 let translator = Arc::clone(&translator);
1172 let server_id = server_id.clone();
1173 tokio::spawn(async move {
1174 translator
1175 .wait_for_indexing_ready_with(
1176 &server_id,
1177 Duration::from_secs(30),
1178 Duration::from_millis(10),
1179 )
1180 .await
1181 })
1182 };
1183
1184 let short_result = short.await.unwrap();
1185 assert!(
1186 matches!(short_result, Err(Error::WorkspaceIndexing { .. })),
1187 "the short-timeout waiter must time out on its own schedule, got {short_result:?}"
1188 );
1189
1190 tokio::time::sleep(Duration::from_millis(150)).await;
1193 assert!(
1194 !long.is_finished(),
1195 "a concurrent caller's short timeout must never resolve another caller's \
1196 independent wait early"
1197 );
1198 long.abort();
1199 }
1200
1201 #[test]
1202 fn test_extract_hover_contents_string() {
1203 let marked_string = lsp_types::MarkedString::String("Test hover".to_string());
1204 let contents = lsp_types::Contents::MarkedString(marked_string);
1205 let result = extract_hover_contents(contents);
1206 assert_eq!(result, "Test hover");
1207 }
1208
1209 #[test]
1210 fn test_extract_hover_contents_language_string() {
1211 let marked_string = lsp_types::MarkedString::MarkedStringWithLanguage(
1212 lsp_types::MarkedStringWithLanguage {
1213 language: "rust".to_string(),
1214 value: "fn main() {}".to_string(),
1215 },
1216 );
1217 let contents = lsp_types::Contents::MarkedString(marked_string);
1218 let result = extract_hover_contents(contents);
1219 assert_eq!(result, "```rust\nfn main() {}\n```");
1220 }
1221
1222 #[test]
1223 fn test_extract_hover_contents_markup() {
1224 let markup = lsp_types::MarkupContent {
1225 kind: lsp_types::MarkupKind::Markdown,
1226 value: "# Documentation".to_string(),
1227 };
1228 let contents = lsp_types::Contents::MarkupContent(markup);
1229 let result = extract_hover_contents(contents);
1230 assert_eq!(result, "# Documentation");
1231 }
1232
1233 #[tokio::test]
1237 async fn test_handle_definition_flattens_single_location() {
1238 let dir = TempDir::new().unwrap();
1239 let server_id = ServerId::from("rust");
1240 let caps = lsp_types::ServerCapabilities {
1241 definition_provider: Some(lsp_types::DefinitionProvider::Bool(true)),
1242 ..Default::default()
1243 };
1244 let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
1245
1246 let path = dir.path().join("main.rs");
1247 fs::write(&path, "fn main() {}").unwrap();
1248 let target_path = dir.path().join("target.rs");
1249 fs::write(&target_path, "fn target() {}").unwrap();
1250 let target_uri = Url::from_file_path(&target_path).unwrap().to_string();
1251
1252 let translator = Arc::new(translator);
1253 let handle = {
1254 let translator = Arc::clone(&translator);
1255 let path = path.to_string_lossy().to_string();
1256 tokio::spawn(async move {
1257 translator
1258 .handle_definition(
1259 path,
1260 Position {
1261 line: 1,
1262 character: 1,
1263 },
1264 )
1265 .await
1266 })
1267 };
1268
1269 let mut wire = BufReader::new(&mut server.write_stdout);
1270 let opened = read_framed_message(&mut wire).await;
1271 assert_eq!(opened["method"], "textDocument/didOpen");
1272 let request = read_framed_message(&mut wire).await;
1273 assert_eq!(request["method"], "textDocument/definition");
1274
1275 write_response(
1276 &mut server.read_half_stdin,
1277 &request["id"],
1278 serde_json::json!({
1279 "uri": target_uri,
1280 "range": {
1281 "start": {"line": 0, "character": 0},
1282 "end": {"line": 0, "character": 6}
1283 }
1284 }),
1285 )
1286 .await;
1287
1288 let result = timeout(Duration::from_secs(2), handle)
1289 .await
1290 .expect("handle_definition should not hang")
1291 .unwrap()
1292 .unwrap();
1293
1294 assert_eq!(result.locations.len(), 1);
1295 assert_eq!(result.locations[0].uri, target_uri);
1296 assert!(
1297 !result.locations[0].out_of_workspace,
1298 "a definition location inside the workspace root must not be marked out_of_workspace"
1299 );
1300 }
1301
1302 #[tokio::test]
1310 async fn test_handle_definition_does_not_filter_out_of_workspace_location() {
1311 let dir = TempDir::new().unwrap();
1312 let server_id = ServerId::from("rust");
1313 let caps = lsp_types::ServerCapabilities {
1314 definition_provider: Some(lsp_types::DefinitionProvider::Bool(true)),
1315 ..Default::default()
1316 };
1317 let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
1318
1319 let path = dir.path().join("main.rs");
1320 fs::write(&path, "fn main() {}").unwrap();
1321 let outside_uri = "file:///outside/workspace/stdlib.rs";
1322
1323 let translator = Arc::new(translator);
1324 let handle = {
1325 let translator = Arc::clone(&translator);
1326 let path = path.to_string_lossy().to_string();
1327 tokio::spawn(async move {
1328 translator
1329 .handle_definition(
1330 path,
1331 Position {
1332 line: 1,
1333 character: 1,
1334 },
1335 )
1336 .await
1337 })
1338 };
1339
1340 let mut wire = BufReader::new(&mut server.write_stdout);
1341 let opened = read_framed_message(&mut wire).await;
1342 assert_eq!(opened["method"], "textDocument/didOpen");
1343 let request = read_framed_message(&mut wire).await;
1344 assert_eq!(request["method"], "textDocument/definition");
1345
1346 write_response(
1347 &mut server.read_half_stdin,
1348 &request["id"],
1349 serde_json::json!({
1350 "uri": outside_uri,
1351 "range": {
1352 "start": {"line": 0, "character": 0},
1353 "end": {"line": 0, "character": 6}
1354 }
1355 }),
1356 )
1357 .await;
1358
1359 let result = timeout(Duration::from_secs(2), handle)
1360 .await
1361 .expect("handle_definition should not hang")
1362 .unwrap()
1363 .unwrap();
1364
1365 assert_eq!(
1366 result.locations.len(),
1367 1,
1368 "an out-of-workspace definition location (e.g. stdlib/a dependency) must be \
1369 returned, not dropped"
1370 );
1371 assert_eq!(result.locations[0].uri, outside_uri);
1372 assert!(
1373 result.locations[0].out_of_workspace,
1374 "a definition location outside every workspace root must be marked out_of_workspace"
1375 );
1376 }
1377
1378 #[tokio::test]
1382 async fn test_handle_references_does_not_filter_out_of_workspace_location() {
1383 let dir = TempDir::new().unwrap();
1384 let server_id = ServerId::from("rust");
1385 let caps = lsp_types::ServerCapabilities {
1386 references_provider: Some(lsp_types::ReferencesProvider::Bool(true)),
1387 ..Default::default()
1388 };
1389 let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
1390
1391 let path = dir.path().join("main.rs");
1392 fs::write(&path, "fn main() {}").unwrap();
1393 let inside_path = dir.path().join("inside.rs");
1394 fs::write(&inside_path, "fn used() {}").unwrap();
1395 let inside_uri = Url::from_file_path(&inside_path).unwrap().to_string();
1396 let outside_uri = "file:///outside/workspace/stdlib.rs";
1397
1398 let translator = Arc::new(translator);
1399 let handle = {
1400 let translator = Arc::clone(&translator);
1401 let path = path.to_string_lossy().to_string();
1402 tokio::spawn(async move { translator.handle_references(path, pos(1, 1), true).await })
1403 };
1404
1405 let mut wire = BufReader::new(&mut server.write_stdout);
1406 let opened = read_framed_message(&mut wire).await;
1407 assert_eq!(opened["method"], "textDocument/didOpen");
1408 let request = read_framed_message(&mut wire).await;
1409 assert_eq!(request["method"], "textDocument/references");
1410
1411 write_response(
1412 &mut server.read_half_stdin,
1413 &request["id"],
1414 serde_json::json!([
1415 {
1416 "uri": inside_uri,
1417 "range": {
1418 "start": {"line": 0, "character": 0},
1419 "end": {"line": 0, "character": 4}
1420 }
1421 },
1422 {
1423 "uri": outside_uri,
1424 "range": {
1425 "start": {"line": 0, "character": 0},
1426 "end": {"line": 0, "character": 4}
1427 }
1428 }
1429 ]),
1430 )
1431 .await;
1432
1433 let result = timeout(Duration::from_secs(2), handle)
1434 .await
1435 .expect("handle_references should not hang")
1436 .unwrap()
1437 .unwrap();
1438
1439 assert_eq!(
1440 result.locations.len(),
1441 2,
1442 "both the in-workspace and out-of-workspace locations must survive"
1443 );
1444 assert!(result.locations.iter().any(|l| l.uri == inside_uri));
1445 assert!(result.locations.iter().any(|l| l.uri == outside_uri));
1446 assert!(
1447 !result
1448 .locations
1449 .iter()
1450 .find(|l| l.uri == inside_uri)
1451 .unwrap()
1452 .out_of_workspace,
1453 "an in-workspace reference location must not be marked out_of_workspace"
1454 );
1455 assert!(
1456 result
1457 .locations
1458 .iter()
1459 .find(|l| l.uri == outside_uri)
1460 .unwrap()
1461 .out_of_workspace,
1462 "an out-of-workspace reference location must be marked out_of_workspace"
1463 );
1464 }
1465
1466 #[tokio::test]
1472 async fn test_handle_references_sets_truncated_flag_past_cap() {
1473 let dir = TempDir::new().unwrap();
1474 let server_id = ServerId::from("rust");
1475 let caps = lsp_types::ServerCapabilities {
1476 references_provider: Some(lsp_types::ReferencesProvider::Bool(true)),
1477 ..Default::default()
1478 };
1479 let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
1480
1481 let path = dir.path().join("main.rs");
1482 fs::write(&path, "fn main() {}").unwrap();
1483 let uri = Url::from_file_path(&path).unwrap().to_string();
1484
1485 let translator = Arc::new(translator);
1486 let handle = {
1487 let translator = Arc::clone(&translator);
1488 let path = path.to_string_lossy().to_string();
1489 tokio::spawn(async move { translator.handle_references(path, pos(1, 1), true).await })
1490 };
1491
1492 let mut wire = BufReader::new(&mut server.write_stdout);
1493 let opened = read_framed_message(&mut wire).await;
1494 assert_eq!(opened["method"], "textDocument/didOpen");
1495 let request = read_framed_message(&mut wire).await;
1496 assert_eq!(request["method"], "textDocument/references");
1497
1498 let locations: Vec<serde_json::Value> = (0..MAX_NORMALIZED_LOCATIONS + 500)
1499 .map(|_| {
1500 serde_json::json!({
1501 "uri": uri,
1502 "range": {
1503 "start": {"line": 0, "character": 0},
1504 "end": {"line": 0, "character": 4}
1505 }
1506 })
1507 })
1508 .collect();
1509 write_response(
1510 &mut server.read_half_stdin,
1511 &request["id"],
1512 serde_json::json!(locations),
1513 )
1514 .await;
1515
1516 let result = timeout(Duration::from_secs(5), handle)
1517 .await
1518 .expect("handle_references should not hang")
1519 .unwrap()
1520 .unwrap();
1521
1522 assert_eq!(result.locations.len(), MAX_NORMALIZED_LOCATIONS);
1523 assert!(
1524 result.truncated,
1525 "a references response past MAX_NORMALIZED_LOCATIONS must set truncated: true"
1526 );
1527 }
1528
1529 #[tokio::test]
1533 async fn test_handle_implementation_flattens_location_list() {
1534 let dir = TempDir::new().unwrap();
1535 let server_id = ServerId::from("rust");
1536 let caps = lsp_types::ServerCapabilities {
1537 implementation_provider: Some(lsp_types::ImplementationProvider::Bool(true)),
1538 ..Default::default()
1539 };
1540 let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
1541
1542 let path = dir.path().join("main.rs");
1543 fs::write(&path, "fn main() {}").unwrap();
1544 let first_impl_path = dir.path().join("impl_a.rs");
1545 fs::write(&first_impl_path, "struct A;").unwrap();
1546 let first_impl_uri = Url::from_file_path(&first_impl_path).unwrap().to_string();
1547 let second_impl_path = dir.path().join("impl_b.rs");
1548 fs::write(&second_impl_path, "struct B;").unwrap();
1549 let second_impl_uri = Url::from_file_path(&second_impl_path).unwrap().to_string();
1550
1551 let translator = Arc::new(translator);
1552 let handle = {
1553 let translator = Arc::clone(&translator);
1554 let path = path.to_string_lossy().to_string();
1555 tokio::spawn(async move {
1556 translator
1557 .handle_implementation(
1558 path,
1559 Position {
1560 line: 1,
1561 character: 1,
1562 },
1563 )
1564 .await
1565 })
1566 };
1567
1568 let mut wire = BufReader::new(&mut server.write_stdout);
1569 let opened = read_framed_message(&mut wire).await;
1570 assert_eq!(opened["method"], "textDocument/didOpen");
1571 let request = read_framed_message(&mut wire).await;
1572 assert_eq!(request["method"], "textDocument/implementation");
1573
1574 write_response(
1575 &mut server.read_half_stdin,
1576 &request["id"],
1577 serde_json::json!([
1578 {
1579 "uri": first_impl_uri,
1580 "range": {
1581 "start": {"line": 0, "character": 0},
1582 "end": {"line": 0, "character": 9}
1583 }
1584 },
1585 {
1586 "uri": second_impl_uri,
1587 "range": {
1588 "start": {"line": 0, "character": 0},
1589 "end": {"line": 0, "character": 9}
1590 }
1591 }
1592 ]),
1593 )
1594 .await;
1595
1596 let result = timeout(Duration::from_secs(2), handle)
1597 .await
1598 .expect("handle_implementation should not hang")
1599 .unwrap()
1600 .unwrap();
1601
1602 assert_eq!(result.locations.len(), 2);
1603 assert_eq!(result.locations[0].uri, first_impl_uri);
1604 assert_eq!(result.locations[1].uri, second_impl_uri);
1605 }
1606
1607 #[tokio::test]
1613 async fn test_handle_type_definition_flattens_definition_link_list() {
1614 let dir = TempDir::new().unwrap();
1615 let server_id = ServerId::from("rust");
1616 let caps = lsp_types::ServerCapabilities {
1617 type_definition_provider: Some(lsp_types::TypeDefinitionProvider::Bool(true)),
1618 ..Default::default()
1619 };
1620 let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
1621
1622 let path = dir.path().join("main.rs");
1623 fs::write(&path, "fn main() {}").unwrap();
1624 let target_path = dir.path().join("target_type.rs");
1625 fs::write(&target_path, "struct TargetType;").unwrap();
1626 let target_uri = Url::from_file_path(&target_path).unwrap().to_string();
1627
1628 let translator = Arc::new(translator);
1629 let handle = {
1630 let translator = Arc::clone(&translator);
1631 let path = path.to_string_lossy().to_string();
1632 tokio::spawn(async move {
1633 translator
1634 .handle_type_definition(
1635 path,
1636 Position {
1637 line: 1,
1638 character: 1,
1639 },
1640 )
1641 .await
1642 })
1643 };
1644
1645 let mut wire = BufReader::new(&mut server.write_stdout);
1646 let opened = read_framed_message(&mut wire).await;
1647 assert_eq!(opened["method"], "textDocument/didOpen");
1648 let request = read_framed_message(&mut wire).await;
1649 assert_eq!(request["method"], "textDocument/typeDefinition");
1650
1651 write_response(
1652 &mut server.read_half_stdin,
1653 &request["id"],
1654 serde_json::json!([{
1655 "targetUri": target_uri,
1656 "targetRange": {
1657 "start": {"line": 0, "character": 0},
1658 "end": {"line": 0, "character": 18}
1659 },
1660 "targetSelectionRange": {
1661 "start": {"line": 0, "character": 7},
1662 "end": {"line": 0, "character": 17}
1663 }
1664 }]),
1665 )
1666 .await;
1667
1668 let result = timeout(Duration::from_secs(2), handle)
1669 .await
1670 .expect("handle_type_definition should not hang")
1671 .unwrap()
1672 .unwrap();
1673
1674 assert_eq!(result.locations.len(), 1);
1675 assert_eq!(result.locations[0].uri, target_uri);
1676 assert_eq!(result.locations[0].range.start.character, 8);
1677 }
1678
1679 #[tokio::test]
1691 async fn test_lsp_locations_to_mcp_reads_disk_once_per_distinct_file_line() {
1692 let dir = TempDir::new().unwrap();
1693 let mut uris = Vec::new();
1694 for i in 0..3 {
1695 let path = dir.path().join(format!("file{i}.rs"));
1696 fs::write(&path, "hello").unwrap();
1697 uris.push(path_to_uri(&path).unwrap());
1698 }
1699
1700 let ctx = test_ctx_with(PositionEncoding::Utf8);
1701 let locs: Vec<lsp_types::Location> = (0..300)
1703 .map(|i| lsp_types::Location {
1704 uri: uris[i % 3].clone(),
1705 range: lsp_types::Range {
1706 start: lsp_types::Position {
1707 line: 0,
1708 character: 0,
1709 },
1710 end: lsp_types::Position {
1711 line: 0,
1712 character: 3,
1713 },
1714 },
1715 })
1716 .collect();
1717
1718 let result = lsp_locations_to_mcp(locs, &ctx).await;
1719
1720 assert_eq!(result.locations.len(), 300);
1721 assert!(!result.truncated);
1722 assert!(
1723 result.locations.iter().all(|l| l.range.end.character == 4),
1724 "MCP columns are 1-based, so LSP byte offset 3 in all-ASCII \"hello\" must convert \
1725 to 4"
1726 );
1727 assert_eq!(
1728 lock_std(&ctx.line_cache).entries.len(),
1729 3,
1730 "300 locations across 3 distinct files must populate the cache with exactly 3 \
1731 entries (one disk read per distinct file/line), not one per location"
1732 );
1733 }
1734
1735 #[tokio::test]
1742 async fn test_lsp_locations_to_mcp_truncates_to_max_normalized_locations() {
1743 let ctx = test_ctx();
1744 let uri = test_uri();
1745 let locs: Vec<lsp_types::Location> = (0..MAX_NORMALIZED_LOCATIONS + 500)
1746 .map(|_| lsp_types::Location {
1747 uri: uri.clone(),
1748 range: lsp_types::Range {
1749 start: lsp_types::Position {
1750 line: 0,
1751 character: 0,
1752 },
1753 end: lsp_types::Position {
1754 line: 0,
1755 character: 1,
1756 },
1757 },
1758 })
1759 .collect();
1760
1761 let result = lsp_locations_to_mcp(locs, &ctx).await;
1762
1763 assert_eq!(result.locations.len(), MAX_NORMALIZED_LOCATIONS);
1764 assert!(
1765 result.truncated,
1766 "a response naming more than MAX_NORMALIZED_LOCATIONS must report truncated: true"
1767 );
1768 }
1769}