1use std::time::Duration;
5
6use lsp_types::ReferenceContext;
7use tokio::time::Instant;
8
9use super::Translator;
10use super::dto::{
11 DefinitionResult, HoverResult, Location, LocationsResult, Position, PositionDegradation,
12 ReferencesResult,
13};
14use super::enclosing::{Contextualized, ResultContext};
15use super::encoding_ctx::EncodingCtx;
16use super::positioned::{FromPosition, Positioned};
17use super::routing::{Capability, IndexingGate};
18use crate::bridge::indexing::{
19 DEFAULT_INDEXING_READY_TIMEOUT_SECS, INDEXING_STALENESS_BOUND, PROGRESS_LATCH_IDLE,
20 PROGRESS_SETTLE,
21};
22use crate::bridge::{ClientPath, IndexingState};
23use crate::config::ServerId;
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
102#[derive(Debug)]
110pub(super) struct ItemBudget {
111 remaining: usize,
112 cap: usize,
113 truncated: bool,
114}
115
116impl ItemBudget {
117 pub(super) const fn new() -> Self {
119 Self::with_cap(MAX_NORMALIZED_LOCATIONS)
120 }
121
122 pub(super) const fn with_cap(cap: usize) -> Self {
124 Self {
125 remaining: cap,
126 cap,
127 truncated: false,
128 }
129 }
130
131 pub(super) fn admit<T>(&mut self, mut items: Vec<T>) -> Vec<T> {
135 if items.len() > self.remaining {
136 self.record_drop(items.len());
137 items.truncate(self.remaining);
138 }
139 self.remaining = self.remaining.saturating_sub(items.len());
140 items
141 }
142
143 pub(super) fn admit_iter<T>(&mut self, items: impl IntoIterator<Item = T>) -> Vec<T> {
147 let mut items = items.into_iter();
148 let kept: Vec<T> = items.by_ref().take(self.remaining).collect();
149 if items.next().is_some() {
150 self.record_drop(kept.len().saturating_add(1));
151 }
152 self.remaining = self.remaining.saturating_sub(kept.len());
153 kept
154 }
155
156 pub(super) fn admit_whole<T>(&mut self, items: Vec<T>) -> Option<Vec<T>> {
160 self.spend_whole(items.len()).then_some(items)
161 }
162
163 pub(super) fn spend_whole(&mut self, count: usize) -> bool {
167 if count > self.remaining {
168 self.record_drop(count);
169 return false;
170 }
171 self.remaining = self.remaining.saturating_sub(count);
172 true
173 }
174
175 fn record_drop(&mut self, reported: usize) {
176 if !self.truncated {
177 tracing::warn!(
178 reported,
179 cap = self.cap,
180 "LSP response item count exceeds the item cap; truncating"
181 );
182 }
183 self.truncated = true;
184 }
185
186 pub(super) const fn truncated(&self) -> bool {
189 self.truncated
190 }
191}
192
193async fn lsp_locations_to_mcp(
210 locs: Vec<lsp_types::Location>,
211 ctx: &EncodingCtx,
212) -> NormalizedLocations {
213 let mut budget = ItemBudget::new();
214 let locs = budget.admit(locs);
215 let truncated = budget.truncated();
216 let mut locations = Vec::with_capacity(locs.len());
217 for loc in locs {
218 locations.push(Location {
219 uri: loc.uri.to_string(),
220 range: ctx.normalize_range(&loc.uri, loc.range).await,
221 out_of_workspace: ctx.is_out_of_workspace(&loc.uri),
222 });
223 }
224 NormalizedLocations {
225 locations,
226 truncated,
227 positions_degraded: ctx.positions_degraded(),
228 }
229}
230
231struct NormalizedLocations {
241 locations: Vec<Location>,
242 truncated: bool,
243 positions_degraded: Option<PositionDegradation>,
244}
245
246enum GotoKind {
251 Locations(Vec<lsp_types::Location>),
253 Links(Vec<lsp_types::LocationLink>),
255}
256
257trait GotoResponse {
261 fn into_kind(self) -> GotoKind;
264}
265
266impl GotoResponse for lsp_types::DefinitionResponse {
267 fn into_kind(self) -> GotoKind {
268 match self {
269 Self::Definition(def) => GotoKind::Locations(definition_to_locations(def)),
270 Self::DefinitionLinkList(links) => GotoKind::Links(links),
271 }
272 }
273}
274
275impl GotoResponse for lsp_types::ImplementationResponse {
276 fn into_kind(self) -> GotoKind {
277 match self {
278 Self::Definition(def) => GotoKind::Locations(definition_to_locations(def)),
279 Self::DefinitionLinkList(links) => GotoKind::Links(links),
280 }
281 }
282}
283
284impl GotoResponse for lsp_types::TypeDefinitionResponse {
285 fn into_kind(self) -> GotoKind {
286 match self {
287 Self::Definition(def) => GotoKind::Locations(definition_to_locations(def)),
288 Self::DefinitionLinkList(links) => GotoKind::Links(links),
289 }
290 }
291}
292
293impl GotoResponse for lsp_types::DeclarationResponse {
294 fn into_kind(self) -> GotoKind {
295 match self {
296 Self::Declaration(lsp_types::Declaration::Location(loc)) => {
297 GotoKind::Locations(vec![loc])
298 }
299 Self::Declaration(lsp_types::Declaration::LocationList(locs)) => {
300 GotoKind::Locations(locs)
301 }
302 Self::DeclarationLinkList(links) => GotoKind::Links(links),
303 }
304 }
305}
306
307async fn goto_response_to_locations<R: GotoResponse>(
312 response: Option<R>,
313 ctx: &EncodingCtx,
314) -> NormalizedLocations {
315 let lsp_locs = match response.map(GotoResponse::into_kind) {
316 Some(GotoKind::Locations(locs)) => locs,
317 Some(GotoKind::Links(links)) => {
318 links.into_iter().map(definition_link_to_location).collect()
319 }
320 None => vec![],
321 };
322 lsp_locations_to_mcp(lsp_locs, ctx).await
323}
324
325#[allow(deprecated, reason = "LSP servers still send this deprecated field")]
333fn extract_hover_contents(contents: lsp_types::Contents) -> String {
334 match contents {
335 lsp_types::Contents::MarkedString(marked_string) => marked_string_to_string(marked_string),
336 lsp_types::Contents::MarkedStringList(marked_strings) => marked_strings
337 .into_iter()
338 .map(marked_string_to_string)
339 .collect::<Vec<_>>()
340 .join("\n\n"),
341 lsp_types::Contents::MarkupContent(markup) => markup.value,
342 }
343}
344
345#[allow(deprecated, reason = "LSP servers still send this deprecated field")]
347fn marked_string_to_string(marked: lsp_types::MarkedString) -> String {
348 match marked {
349 lsp_types::MarkedString::String(s) => s,
350 lsp_types::MarkedString::MarkedStringWithLanguage(ls) => {
351 format!("```{}\n{}\n```", ls.language, ls.value)
352 }
353 }
354}
355
356impl Translator {
357 pub(super) async fn wait_for_indexing_ready(&self, server_id: &ServerId) -> Result<()> {
381 self.wait_for_indexing_ready_with(
382 server_id,
383 self.indexing_ready_timeout,
384 INDEXING_POLL_INTERVAL,
385 )
386 .await
387 }
388
389 async fn wait_for_indexing_ready_with(
404 &self,
405 server_id: &ServerId,
406 timeout: Duration,
407 poll_interval: Duration,
408 ) -> Result<()> {
409 let Some(cache) = self.notification_cache.as_ref() else {
410 return Ok(());
411 };
412
413 let start = Instant::now();
414 #[allow(
415 clippy::arithmetic_side_effects,
416 reason = "production passes the fixed INDEXING_READY_TIMEOUT"
417 )]
418 let deadline = start + timeout;
419 loop {
420 let state = cache.lock().await.indexing_state(server_id);
421 if state != IndexingState::Loading {
422 return Ok(());
423 }
424 let remaining = deadline.saturating_duration_since(Instant::now());
425 if remaining.is_zero() {
426 return Err(Error::WorkspaceIndexing {
427 server_id: server_id.clone(),
428 elapsed_secs: start.elapsed().as_secs(),
429 });
430 }
431 tokio::time::sleep(poll_interval.min(remaining)).await;
432 }
433 }
434
435 pub async fn handle_hover(
444 &self,
445 file_path: ClientPath,
446 position: Position,
447 ) -> Result<HoverResult> {
448 let Positioned {
449 result: response,
450 ctx,
451 doc,
452 } = self
453 .position_request::<lsp_types::HoverRequest>(
454 &file_path,
455 position,
456 IndexingGate::Required(Capability::Hover),
457 (),
458 )
459 .await?;
460 let response_uri = doc.uri();
461
462 let result = match response {
463 Some(hover) => {
464 let contents = extract_hover_contents(hover.contents);
465 let range = match hover.range {
466 Some(r) => Some(ctx.normalize_range(response_uri, r).await),
467 None => None,
468 };
469 HoverResult {
470 contents,
471 range,
472 positions_degraded: ctx.positions_degraded(),
473 }
474 }
475 None => HoverResult {
476 contents: "No hover information available".to_string(),
477 range: None,
478 positions_degraded: ctx.positions_degraded(),
479 },
480 };
481
482 Ok(result)
483 }
484
485 async fn contextualize_into(
486 &self,
487 normalized: NormalizedLocations,
488 context: ResultContext,
489 ) -> LocationsResult {
490 let truncated = normalized.truncated;
491 let Contextualized {
492 items: locations,
493 enrichment,
494 positions_degraded,
495 } = self
496 .contextualize_locations(normalized.locations, context, normalized.positions_degraded)
497 .await;
498 LocationsResult {
499 locations,
500 truncated,
501 positions_degraded,
502 enrichment,
503 }
504 }
505
506 async fn handle_goto<R, T>(
520 &self,
521 file_path: &ClientPath,
522 position: Position,
523 capability: Capability,
524 ) -> Result<NormalizedLocations>
525 where
526 R: lsp_types::Request<Result = Option<T>>,
527 R::Params: FromPosition<Extra = ()>,
528 T: GotoResponse,
529 {
530 let Positioned {
531 result: response,
532 ctx,
533 doc: _doc,
534 } = self
535 .position_request::<R>(file_path, position, IndexingGate::Required(capability), ())
536 .await?;
537
538 Ok(goto_response_to_locations(response, &ctx).await)
539 }
540
541 pub async fn handle_definition(
550 &self,
551 file_path: ClientPath,
552 position: Position,
553 context: ResultContext,
554 ) -> Result<DefinitionResult> {
555 let normalized = self
556 .handle_goto::<lsp_types::DefinitionRequest, _>(
557 &file_path,
558 position,
559 Capability::Definition,
560 )
561 .await?;
562 Ok(self.contextualize_into(normalized, context).await)
563 }
564
565 pub async fn handle_references(
574 &self,
575 file_path: ClientPath,
576 position: Position,
577 include_declaration: bool,
578 context: ResultContext,
579 ) -> Result<ReferencesResult> {
580 let Positioned {
581 result: response,
582 ctx,
583 doc: _doc,
584 } = self
585 .position_request::<lsp_types::ReferencesRequest>(
586 &file_path,
587 position,
588 IndexingGate::Required(Capability::References),
589 ReferenceContext {
590 include_declaration,
591 },
592 )
593 .await?;
594
595 let normalized = lsp_locations_to_mcp(response.unwrap_or_default(), &ctx).await;
596 Ok(self.contextualize_into(normalized, context).await)
597 }
598
599 pub async fn handle_implementation(
610 &self,
611 file_path: ClientPath,
612 position: Position,
613 context: ResultContext,
614 ) -> Result<LocationsResult> {
615 let normalized = self
616 .handle_goto::<lsp_types::ImplementationRequest, _>(
617 &file_path,
618 position,
619 Capability::Implementation,
620 )
621 .await?;
622 Ok(self.contextualize_into(normalized, context).await)
623 }
624
625 pub async fn handle_type_definition(
637 &self,
638 file_path: ClientPath,
639 position: Position,
640 context: ResultContext,
641 ) -> Result<LocationsResult> {
642 let normalized = self
643 .handle_goto::<lsp_types::TypeDefinitionRequest, _>(
644 &file_path,
645 position,
646 Capability::TypeDefinition,
647 )
648 .await?;
649 Ok(self.contextualize_into(normalized, context).await)
650 }
651
652 pub async fn handle_declaration(
665 &self,
666 file_path: ClientPath,
667 position: Position,
668 context: ResultContext,
669 ) -> Result<LocationsResult> {
670 let normalized = self
671 .handle_goto::<lsp_types::DeclarationRequest, _>(
672 &file_path,
673 position,
674 Capability::Declaration,
675 )
676 .await?;
677 Ok(self.contextualize_into(normalized, context).await)
678 }
679}
680
681#[cfg(test)]
682#[allow(deprecated, reason = "LSP servers still send this deprecated field")]
683mod tests {
684 use std::sync::Arc;
685 use std::time::Duration;
686 use std::{assert_matches, fs};
687
688 use tempfile::TempDir;
689 use tokio::io::BufReader;
690 use tokio::sync::Mutex;
691 use tokio::time::timeout;
692 use url::Url;
693
694 use super::*;
695 use crate::bridge::encoding::PositionEncoding;
696 use crate::bridge::translator::testing::*;
697 use crate::bridge::{NotificationCache, path_to_uri};
698 use crate::config::{IndexingReadyTimeoutSecs, ServerId};
699 use crate::test_lsp::client_path;
700 use crate::util::lock_std;
701
702 #[tokio::test]
707 async fn test_wait_for_indexing_ready_without_cache_is_noop() {
708 let translator = Translator::new();
710 let server_id = ServerId::from_static("rust");
711
712 translator
713 .wait_for_indexing_ready(&server_id)
714 .await
715 .unwrap();
716 }
717
718 #[tokio::test]
719 async fn test_wait_for_indexing_ready_unknown_state_is_noop() {
720 let translator = Translator::new()
721 .with_notification_cache(Arc::new(Mutex::new(NotificationCache::new())));
722 let server_id = ServerId::from_static("rust");
723
724 translator
725 .wait_for_indexing_ready(&server_id)
726 .await
727 .unwrap();
728 }
729
730 #[tokio::test]
731 async fn test_wait_for_indexing_ready_ready_state_is_noop() {
732 let cache = Arc::new(Mutex::new(NotificationCache::new()));
733 let server_id = ServerId::from_static("rust");
734 cache.lock().await.observe_indexing_signal(
735 &server_id,
736 "experimental/serverStatus",
737 Some(&serde_json::json!({"quiescent": true})),
738 );
739 let translator = Translator::new().with_notification_cache(cache);
740
741 translator
742 .wait_for_indexing_ready(&server_id)
743 .await
744 .unwrap();
745 }
746
747 #[tokio::test(start_paused = true)]
753 async fn test_wait_for_indexing_ready_uses_configured_timeout_override() {
754 let cache = Arc::new(Mutex::new(NotificationCache::new()));
755 let server_id = ServerId::from_static("rust");
756 cache.lock().await.observe_indexing_signal(
757 &server_id,
758 "experimental/serverStatus",
759 Some(&serde_json::json!({"quiescent": false})),
760 );
761 let translator = Translator::new()
762 .with_notification_cache(cache)
763 .with_indexing_ready_timeout(IndexingReadyTimeoutSecs::new(5).unwrap());
764
765 let start = Instant::now();
766 let err = translator
767 .wait_for_indexing_ready(&server_id)
768 .await
769 .unwrap_err();
770
771 assert_matches!(err, Error::WorkspaceIndexing { elapsed_secs, .. } if elapsed_secs == 5);
772 assert_eq!(start.elapsed(), Duration::from_secs(5));
773 }
774
775 #[tokio::test]
776 async fn test_wait_for_indexing_ready_loading_times_out() {
777 let cache = Arc::new(Mutex::new(NotificationCache::new()));
778 let server_id = ServerId::from_static("rust");
779 cache.lock().await.observe_indexing_signal(
780 &server_id,
781 "experimental/serverStatus",
782 Some(&serde_json::json!({"quiescent": false})),
783 );
784 let translator = Translator::new().with_notification_cache(cache);
785
786 let err = translator
787 .wait_for_indexing_ready_with(
788 &server_id,
789 Duration::from_millis(50),
790 Duration::from_millis(10),
791 )
792 .await
793 .unwrap_err();
794
795 assert_matches!(
796 err,
797 Error::WorkspaceIndexing { server_id: id, .. } if id == ServerId::from_static("rust")
798 );
799 }
800
801 #[tokio::test]
802 async fn test_wait_for_indexing_ready_returns_ok_once_signaled_ready() {
803 let cache = Arc::new(Mutex::new(NotificationCache::new()));
804 let server_id = ServerId::from_static("rust");
805 cache.lock().await.observe_indexing_signal(
806 &server_id,
807 "experimental/serverStatus",
808 Some(&serde_json::json!({"quiescent": false})),
809 );
810 let translator = Translator::new().with_notification_cache(Arc::clone(&cache));
811
812 let waiter = {
813 let server_id = server_id.clone();
814 tokio::spawn(async move {
815 translator
816 .wait_for_indexing_ready_with(
817 &server_id,
818 Duration::from_secs(5),
819 Duration::from_millis(10),
820 )
821 .await
822 })
823 };
824
825 tokio::time::sleep(Duration::from_millis(30)).await;
826 cache.lock().await.observe_indexing_signal(
827 &server_id,
828 "experimental/serverStatus",
829 Some(&serde_json::json!({"quiescent": true})),
830 );
831
832 timeout(Duration::from_secs(1), waiter)
833 .await
834 .expect("waiter task timed out")
835 .expect("waiter task panicked")
836 .expect("expected Ok once quiescent");
837 }
838
839 #[tokio::test(start_paused = true)]
845 async fn test_handle_hover_returns_workspace_indexing_error_when_loading() {
846 let dir = TempDir::new().unwrap();
847 let server_id = ServerId::from_static("rust");
848 let caps = lsp_types::ServerCapabilities {
849 hover_provider: Some(lsp_types::HoverProvider::Bool(true)),
850 ..Default::default()
851 };
852 let (translator, _server) = translator_with_capabilities(&dir, &server_id, caps);
853
854 let cache = Arc::new(Mutex::new(NotificationCache::new()));
855 cache.lock().await.observe_indexing_signal(
856 &server_id,
857 "experimental/serverStatus",
858 Some(&serde_json::json!({"quiescent": false})),
859 );
860 let translator = translator.with_notification_cache(cache);
861
862 let path = dir.path().join("main.rs");
863 fs::write(&path, "fn main() {}").unwrap();
864
865 let err = translator
866 .handle_hover(client_path(path.to_string_lossy().into_owned()), pos(1, 1))
867 .await
868 .unwrap_err();
869
870 assert_matches!(
871 err,
872 Error::WorkspaceIndexing { server_id: id, elapsed_secs: 30 } if id == server_id
873 );
874 }
875
876 #[tokio::test]
879 async fn test_handle_hover_dispatches_when_indexing_ready() {
880 let dir = TempDir::new().unwrap();
881 let server_id = ServerId::from_static("rust");
882 let caps = lsp_types::ServerCapabilities {
883 hover_provider: Some(lsp_types::HoverProvider::Bool(true)),
884 ..Default::default()
885 };
886 let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
887
888 let cache = Arc::new(Mutex::new(NotificationCache::new()));
889 cache.lock().await.observe_indexing_signal(
890 &server_id,
891 "experimental/serverStatus",
892 Some(&serde_json::json!({"quiescent": true})),
893 );
894 let translator = Arc::new(translator.with_notification_cache(cache));
895
896 let path = dir.path().join("main.rs");
897 fs::write(&path, "fn main() {}").unwrap();
898
899 let handle = {
900 let translator = Arc::clone(&translator);
901 let path = path.to_string_lossy().to_string();
902 tokio::spawn(async move { translator.handle_hover(client_path(path), pos(1, 1)).await })
903 };
904
905 let mut wire = BufReader::new(&mut server.write_stdout);
906 let opened = read_framed_message(&mut wire).await;
907 assert_eq!(opened["method"], "textDocument/didOpen");
908 let request = read_framed_message(&mut wire).await;
909 assert_eq!(request["method"], "textDocument/hover");
910
911 write_response(
912 &mut server.read_half_stdin,
913 &request["id"],
914 serde_json::json!({
915 "contents": {"kind": "markdown", "value": "hover text"}
916 }),
917 )
918 .await;
919
920 let result = handle.await.unwrap().unwrap();
921 assert_eq!(result.contents, "hover text");
922 }
923
924 #[tokio::test(start_paused = true)]
928 async fn test_handle_hover_timeout_keeps_document_open_for_next_request() {
929 let dir = TempDir::new().unwrap();
930 let server_id = ServerId::from_static("rust");
931 let caps = lsp_types::ServerCapabilities {
932 hover_provider: Some(lsp_types::HoverProvider::Bool(true)),
933 ..Default::default()
934 };
935 let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
936
937 let cache = Arc::new(Mutex::new(NotificationCache::new()));
938 cache.lock().await.observe_indexing_signal(
939 &server_id,
940 "experimental/serverStatus",
941 Some(&serde_json::json!({"quiescent": true})),
942 );
943 let translator = Arc::new(translator.with_notification_cache(cache));
944
945 let path = dir.path().join("main.rs");
946 fs::write(&path, "fn main() {}").unwrap();
947 let path = path.to_string_lossy().to_string();
948
949 let spawn_hover = || {
950 let translator = Arc::clone(&translator);
951 let path = path.clone();
952 tokio::spawn(async move { translator.handle_hover(client_path(path), pos(1, 1)).await })
953 };
954
955 let mut wire = BufReader::new(&mut server.write_stdout);
956
957 let first = spawn_hover();
958 let opened = read_framed_message(&mut wire).await;
959 assert_eq!(opened["method"], "textDocument/didOpen");
960 let first_request = read_framed_message(&mut wire).await;
961 assert_eq!(first_request["method"], "textDocument/hover");
962
963 let timeout_secs = crate::config::LspServerConfig::rust_analyzer().request_timeout_seconds;
964 tokio::time::advance(Duration::from_secs(timeout_secs.get() + 1)).await;
965 assert_matches!(first.await.unwrap().unwrap_err(), Error::Timeout(_));
966
967 let second = spawn_hover();
968 let second_request = read_framed_message(&mut wire).await;
969 assert_eq!(
970 second_request["method"], "textDocument/hover",
971 "a timed-out hover must not cause the document to be re-opened"
972 );
973 write_response(
974 &mut server.read_half_stdin,
975 &second_request["id"],
976 serde_json::json!({"contents": {"kind": "markdown", "value": "after timeout"}}),
977 )
978 .await;
979
980 assert_eq!(second.await.unwrap().unwrap().contents, "after timeout");
981 }
982
983 #[tokio::test(start_paused = true)]
987 async fn test_handle_definition_returns_workspace_indexing_error_when_loading() {
988 let dir = TempDir::new().unwrap();
989 let server_id = ServerId::from_static("rust");
990 let caps = lsp_types::ServerCapabilities {
991 definition_provider: Some(lsp_types::DefinitionProvider::Bool(true)),
992 ..Default::default()
993 };
994 let (translator, _server) = translator_with_capabilities(&dir, &server_id, caps);
995
996 let cache = Arc::new(Mutex::new(NotificationCache::new()));
997 cache.lock().await.observe_indexing_signal(
998 &server_id,
999 "experimental/serverStatus",
1000 Some(&serde_json::json!({"quiescent": false})),
1001 );
1002 let translator = translator.with_notification_cache(cache);
1003
1004 let path = dir.path().join("main.rs");
1005 fs::write(&path, "fn main() {}").unwrap();
1006
1007 let err = translator
1008 .handle_definition(
1009 client_path(path.to_string_lossy().into_owned()),
1010 pos(1, 1),
1011 ResultContext::None,
1012 )
1013 .await
1014 .unwrap_err();
1015
1016 assert_matches!(
1017 err,
1018 Error::WorkspaceIndexing { server_id: id, .. } if id == server_id
1019 );
1020 }
1021
1022 #[tokio::test]
1025 async fn test_handle_definition_dispatches_when_indexing_ready() {
1026 let dir = TempDir::new().unwrap();
1027 let server_id = ServerId::from_static("rust");
1028 let caps = lsp_types::ServerCapabilities {
1029 definition_provider: Some(lsp_types::DefinitionProvider::Bool(true)),
1030 ..Default::default()
1031 };
1032 let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
1033
1034 let cache = Arc::new(Mutex::new(NotificationCache::new()));
1035 cache.lock().await.observe_indexing_signal(
1036 &server_id,
1037 "experimental/serverStatus",
1038 Some(&serde_json::json!({"quiescent": true})),
1039 );
1040 let translator = Arc::new(translator.with_notification_cache(cache));
1041
1042 let path = dir.path().join("main.rs");
1043 fs::write(&path, "fn main() {}").unwrap();
1044
1045 let handle = {
1046 let translator = Arc::clone(&translator);
1047 let path = path.to_string_lossy().to_string();
1048 tokio::spawn(async move {
1049 translator
1050 .handle_definition(client_path(path), pos(1, 1), ResultContext::None)
1051 .await
1052 })
1053 };
1054
1055 let mut wire = BufReader::new(&mut server.write_stdout);
1056 let opened = read_framed_message(&mut wire).await;
1057 assert_eq!(opened["method"], "textDocument/didOpen");
1058 let request = read_framed_message(&mut wire).await;
1059 assert_eq!(request["method"], "textDocument/definition");
1060
1061 write_response(
1062 &mut server.read_half_stdin,
1063 &request["id"],
1064 serde_json::Value::Null,
1065 )
1066 .await;
1067
1068 let result = handle.await.unwrap().unwrap();
1069 assert!(result.locations.is_empty());
1070 }
1071
1072 #[tokio::test(start_paused = true)]
1076 async fn test_handle_references_returns_workspace_indexing_error_when_loading() {
1077 let dir = TempDir::new().unwrap();
1078 let server_id = ServerId::from_static("rust");
1079 let caps = lsp_types::ServerCapabilities {
1080 references_provider: Some(lsp_types::ReferencesProvider::Bool(true)),
1081 ..Default::default()
1082 };
1083 let (translator, _server) = translator_with_capabilities(&dir, &server_id, caps);
1084
1085 let cache = Arc::new(Mutex::new(NotificationCache::new()));
1086 cache.lock().await.observe_indexing_signal(
1087 &server_id,
1088 "experimental/serverStatus",
1089 Some(&serde_json::json!({"quiescent": false})),
1090 );
1091 let translator = translator.with_notification_cache(cache);
1092
1093 let path = dir.path().join("main.rs");
1094 fs::write(&path, "fn main() {}").unwrap();
1095
1096 let err = translator
1097 .handle_references(
1098 client_path(path.to_string_lossy().into_owned()),
1099 pos(1, 1),
1100 true,
1101 ResultContext::None,
1102 )
1103 .await
1104 .unwrap_err();
1105
1106 assert_matches!(
1107 err,
1108 Error::WorkspaceIndexing { server_id: id, .. } if id == server_id
1109 );
1110 }
1111
1112 #[tokio::test]
1115 async fn test_handle_references_dispatches_when_indexing_ready() {
1116 let dir = TempDir::new().unwrap();
1117 let server_id = ServerId::from_static("rust");
1118 let caps = lsp_types::ServerCapabilities {
1119 references_provider: Some(lsp_types::ReferencesProvider::Bool(true)),
1120 ..Default::default()
1121 };
1122 let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
1123
1124 let cache = Arc::new(Mutex::new(NotificationCache::new()));
1125 cache.lock().await.observe_indexing_signal(
1126 &server_id,
1127 "experimental/serverStatus",
1128 Some(&serde_json::json!({"quiescent": true})),
1129 );
1130 let translator = Arc::new(translator.with_notification_cache(cache));
1131
1132 let path = dir.path().join("main.rs");
1133 fs::write(&path, "fn main() {}").unwrap();
1134
1135 let handle = {
1136 let translator = Arc::clone(&translator);
1137 let path = path.to_string_lossy().to_string();
1138 tokio::spawn(async move {
1139 translator
1140 .handle_references(client_path(path), pos(1, 1), true, ResultContext::None)
1141 .await
1142 })
1143 };
1144
1145 let mut wire = BufReader::new(&mut server.write_stdout);
1146 let opened = read_framed_message(&mut wire).await;
1147 assert_eq!(opened["method"], "textDocument/didOpen");
1148 let request = read_framed_message(&mut wire).await;
1149 assert_eq!(request["method"], "textDocument/references");
1150
1151 write_response(
1152 &mut server.read_half_stdin,
1153 &request["id"],
1154 serde_json::Value::Null,
1155 )
1156 .await;
1157
1158 let result = handle.await.unwrap().unwrap();
1159 assert!(result.locations.is_empty());
1160 }
1161
1162 #[tokio::test(start_paused = true)]
1167 async fn test_handle_implementation_returns_workspace_indexing_error_when_loading() {
1168 let dir = TempDir::new().unwrap();
1169 let server_id = ServerId::from_static("rust");
1170 let caps = lsp_types::ServerCapabilities {
1171 implementation_provider: Some(lsp_types::ImplementationProvider::Bool(true)),
1172 ..Default::default()
1173 };
1174 let (translator, _server) = translator_with_capabilities(&dir, &server_id, caps);
1175
1176 let cache = Arc::new(Mutex::new(NotificationCache::new()));
1177 cache.lock().await.observe_indexing_signal(
1178 &server_id,
1179 "experimental/serverStatus",
1180 Some(&serde_json::json!({"quiescent": false})),
1181 );
1182 let translator = translator.with_notification_cache(cache);
1183
1184 let path = dir.path().join("main.rs");
1185 fs::write(&path, "fn main() {}").unwrap();
1186
1187 let err = translator
1188 .handle_implementation(
1189 client_path(path.to_string_lossy().into_owned()),
1190 pos(1, 1),
1191 ResultContext::None,
1192 )
1193 .await
1194 .unwrap_err();
1195
1196 assert_matches!(
1197 err,
1198 Error::WorkspaceIndexing { server_id: id, .. } if id == server_id
1199 );
1200 }
1201
1202 #[tokio::test(start_paused = true)]
1204 async fn test_handle_type_definition_returns_workspace_indexing_error_when_loading() {
1205 let dir = TempDir::new().unwrap();
1206 let server_id = ServerId::from_static("rust");
1207 let caps = lsp_types::ServerCapabilities {
1208 type_definition_provider: Some(lsp_types::TypeDefinitionProvider::Bool(true)),
1209 ..Default::default()
1210 };
1211 let (translator, _server) = translator_with_capabilities(&dir, &server_id, caps);
1212
1213 let cache = Arc::new(Mutex::new(NotificationCache::new()));
1214 cache.lock().await.observe_indexing_signal(
1215 &server_id,
1216 "experimental/serverStatus",
1217 Some(&serde_json::json!({"quiescent": false})),
1218 );
1219 let translator = translator.with_notification_cache(cache);
1220
1221 let path = dir.path().join("main.rs");
1222 fs::write(&path, "fn main() {}").unwrap();
1223
1224 let err = translator
1225 .handle_type_definition(
1226 client_path(path.to_string_lossy().into_owned()),
1227 pos(1, 1),
1228 ResultContext::None,
1229 )
1230 .await
1231 .unwrap_err();
1232
1233 assert_matches!(
1234 err,
1235 Error::WorkspaceIndexing { server_id: id, .. } if id == server_id
1236 );
1237 }
1238
1239 #[tokio::test]
1248 async fn test_wait_for_indexing_ready_timeout_does_not_mutate_shared_state() {
1249 let cache = Arc::new(Mutex::new(NotificationCache::new()));
1250 let server_id = ServerId::from_static("rust");
1251 cache.lock().await.observe_indexing_signal(
1252 &server_id,
1253 "experimental/serverStatus",
1254 Some(&serde_json::json!({"quiescent": false})),
1255 );
1256 let translator = Translator::new().with_notification_cache(Arc::clone(&cache));
1257
1258 translator
1259 .wait_for_indexing_ready_with(
1260 &server_id,
1261 Duration::from_millis(30),
1262 Duration::from_millis(10),
1263 )
1264 .await
1265 .unwrap_err();
1266
1267 assert_eq!(
1268 cache.lock().await.indexing_state(&server_id),
1269 IndexingState::Loading,
1270 "a timed-out wait must not touch the shared entry -- it is still fresh, so it must \
1271 still read as Loading for any other caller"
1272 );
1273 }
1274
1275 #[tokio::test]
1282 async fn test_wait_for_indexing_ready_one_callers_timeout_does_not_release_another() {
1283 let cache = Arc::new(Mutex::new(NotificationCache::new()));
1284 let server_id = ServerId::from_static("rust");
1285 cache.lock().await.observe_indexing_signal(
1286 &server_id,
1287 "experimental/serverStatus",
1288 Some(&serde_json::json!({"quiescent": false})),
1289 );
1290 let translator = Arc::new(Translator::new().with_notification_cache(Arc::clone(&cache)));
1291
1292 let short = {
1293 let translator = Arc::clone(&translator);
1294 let server_id = server_id.clone();
1295 tokio::spawn(async move {
1296 translator
1297 .wait_for_indexing_ready_with(
1298 &server_id,
1299 Duration::from_millis(80),
1300 Duration::from_millis(10),
1301 )
1302 .await
1303 })
1304 };
1305 let long = {
1306 let translator = Arc::clone(&translator);
1307 let server_id = server_id.clone();
1308 tokio::spawn(async move {
1309 translator
1310 .wait_for_indexing_ready_with(
1311 &server_id,
1312 Duration::from_secs(30),
1313 Duration::from_millis(10),
1314 )
1315 .await
1316 })
1317 };
1318
1319 let short_result = short.await.unwrap();
1320 assert_matches!(
1321 short_result,
1322 Err(Error::WorkspaceIndexing { .. }),
1323 "the short-timeout waiter must time out on its own schedule, got {short_result:?}"
1324 );
1325
1326 tokio::time::sleep(Duration::from_millis(150)).await;
1329 assert!(
1330 !long.is_finished(),
1331 "a concurrent caller's short timeout must never resolve another caller's \
1332 independent wait early"
1333 );
1334 long.abort();
1335 }
1336
1337 #[test]
1338 fn test_extract_hover_contents_string() {
1339 let marked_string = lsp_types::MarkedString::String("Test hover".to_string());
1340 let contents = lsp_types::Contents::MarkedString(marked_string);
1341 let result = extract_hover_contents(contents);
1342 assert_eq!(result, "Test hover");
1343 }
1344
1345 #[test]
1346 fn test_extract_hover_contents_language_string() {
1347 let marked_string = lsp_types::MarkedString::MarkedStringWithLanguage(
1348 lsp_types::MarkedStringWithLanguage {
1349 language: "rust".to_string(),
1350 value: "fn main() {}".to_string(),
1351 },
1352 );
1353 let contents = lsp_types::Contents::MarkedString(marked_string);
1354 let result = extract_hover_contents(contents);
1355 assert_eq!(result, "```rust\nfn main() {}\n```");
1356 }
1357
1358 #[test]
1359 fn test_extract_hover_contents_markup() {
1360 let markup = lsp_types::MarkupContent {
1361 kind: lsp_types::MarkupKind::Markdown,
1362 value: "# Documentation".to_string(),
1363 };
1364 let contents = lsp_types::Contents::MarkupContent(markup);
1365 let result = extract_hover_contents(contents);
1366 assert_eq!(result, "# Documentation");
1367 }
1368
1369 #[tokio::test]
1373 async fn test_handle_definition_flattens_single_location() {
1374 let dir = TempDir::new().unwrap();
1375 let server_id = ServerId::from_static("rust");
1376 let caps = lsp_types::ServerCapabilities {
1377 definition_provider: Some(lsp_types::DefinitionProvider::Bool(true)),
1378 ..Default::default()
1379 };
1380 let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
1381
1382 let path = canonical_dir(&dir).join("main.rs");
1383 fs::write(&path, "fn main() {}").unwrap();
1384 let target_path = canonical_dir(&dir).join("target.rs");
1385 fs::write(&target_path, "fn target() {}").unwrap();
1386 let target_uri = Url::from_file_path(&target_path).unwrap().to_string();
1387
1388 let translator = Arc::new(translator);
1389 let handle = {
1390 let translator = Arc::clone(&translator);
1391 let path = path.to_string_lossy().to_string();
1392 tokio::spawn(async move {
1393 translator
1394 .handle_definition(client_path(path), Position::at(1, 1), ResultContext::None)
1395 .await
1396 })
1397 };
1398
1399 let mut wire = BufReader::new(&mut server.write_stdout);
1400 let opened = read_framed_message(&mut wire).await;
1401 assert_eq!(opened["method"], "textDocument/didOpen");
1402 let request = read_framed_message(&mut wire).await;
1403 assert_eq!(request["method"], "textDocument/definition");
1404
1405 write_response(
1406 &mut server.read_half_stdin,
1407 &request["id"],
1408 serde_json::json!({
1409 "uri": target_uri,
1410 "range": {
1411 "start": {"line": 0, "character": 0},
1412 "end": {"line": 0, "character": 6}
1413 }
1414 }),
1415 )
1416 .await;
1417
1418 let result = timeout(Duration::from_secs(2), handle)
1419 .await
1420 .expect("handle_definition should not hang")
1421 .unwrap()
1422 .unwrap();
1423
1424 assert_eq!(result.locations.len(), 1);
1425 assert_eq!(result.locations[0].uri, target_uri);
1426 assert!(
1427 !result.locations[0].out_of_workspace,
1428 "a definition location inside the workspace root must not be marked out_of_workspace"
1429 );
1430 }
1431
1432 #[tokio::test]
1440 async fn test_handle_definition_does_not_filter_out_of_workspace_location() {
1441 let dir = TempDir::new().unwrap();
1442 let server_id = ServerId::from_static("rust");
1443 let caps = lsp_types::ServerCapabilities {
1444 definition_provider: Some(lsp_types::DefinitionProvider::Bool(true)),
1445 ..Default::default()
1446 };
1447 let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
1448
1449 let path = dir.path().join("main.rs");
1450 fs::write(&path, "fn main() {}").unwrap();
1451 let outside_uri = "file:///outside/workspace/stdlib.rs";
1452
1453 let translator = Arc::new(translator);
1454 let handle = {
1455 let translator = Arc::clone(&translator);
1456 let path = path.to_string_lossy().to_string();
1457 tokio::spawn(async move {
1458 translator
1459 .handle_definition(client_path(path), Position::at(1, 1), ResultContext::None)
1460 .await
1461 })
1462 };
1463
1464 let mut wire = BufReader::new(&mut server.write_stdout);
1465 let opened = read_framed_message(&mut wire).await;
1466 assert_eq!(opened["method"], "textDocument/didOpen");
1467 let request = read_framed_message(&mut wire).await;
1468 assert_eq!(request["method"], "textDocument/definition");
1469
1470 write_response(
1471 &mut server.read_half_stdin,
1472 &request["id"],
1473 serde_json::json!({
1474 "uri": outside_uri,
1475 "range": {
1476 "start": {"line": 0, "character": 0},
1477 "end": {"line": 0, "character": 6}
1478 }
1479 }),
1480 )
1481 .await;
1482
1483 let result = timeout(Duration::from_secs(2), handle)
1484 .await
1485 .expect("handle_definition should not hang")
1486 .unwrap()
1487 .unwrap();
1488
1489 assert_eq!(
1490 result.locations.len(),
1491 1,
1492 "an out-of-workspace definition location (e.g. stdlib/a dependency) must be \
1493 returned, not dropped"
1494 );
1495 assert_eq!(result.locations[0].uri, outside_uri);
1496 assert!(
1497 result.locations[0].out_of_workspace,
1498 "a definition location outside every workspace root must be marked out_of_workspace"
1499 );
1500 }
1501
1502 #[tokio::test]
1506 async fn test_handle_references_does_not_filter_out_of_workspace_location() {
1507 let dir = TempDir::new().unwrap();
1508 let server_id = ServerId::from_static("rust");
1509 let caps = lsp_types::ServerCapabilities {
1510 references_provider: Some(lsp_types::ReferencesProvider::Bool(true)),
1511 ..Default::default()
1512 };
1513 let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
1514
1515 let path = canonical_dir(&dir).join("main.rs");
1516 fs::write(&path, "fn main() {}").unwrap();
1517 let inside_path = canonical_dir(&dir).join("inside.rs");
1518 fs::write(&inside_path, "fn used() {}").unwrap();
1519 let inside_uri = Url::from_file_path(&inside_path).unwrap().to_string();
1520 let outside_uri = "file:///outside/workspace/stdlib.rs";
1521
1522 let translator = Arc::new(translator);
1523 let handle = {
1524 let translator = Arc::clone(&translator);
1525 let path = path.to_string_lossy().to_string();
1526 tokio::spawn(async move {
1527 translator
1528 .handle_references(client_path(path), pos(1, 1), true, ResultContext::None)
1529 .await
1530 })
1531 };
1532
1533 let mut wire = BufReader::new(&mut server.write_stdout);
1534 let opened = read_framed_message(&mut wire).await;
1535 assert_eq!(opened["method"], "textDocument/didOpen");
1536 let request = read_framed_message(&mut wire).await;
1537 assert_eq!(request["method"], "textDocument/references");
1538
1539 write_response(
1540 &mut server.read_half_stdin,
1541 &request["id"],
1542 serde_json::json!([
1543 {
1544 "uri": inside_uri,
1545 "range": {
1546 "start": {"line": 0, "character": 0},
1547 "end": {"line": 0, "character": 4}
1548 }
1549 },
1550 {
1551 "uri": outside_uri,
1552 "range": {
1553 "start": {"line": 0, "character": 0},
1554 "end": {"line": 0, "character": 4}
1555 }
1556 }
1557 ]),
1558 )
1559 .await;
1560
1561 let result = timeout(Duration::from_secs(2), handle)
1562 .await
1563 .expect("handle_references should not hang")
1564 .unwrap()
1565 .unwrap();
1566
1567 assert_eq!(
1568 result.locations.len(),
1569 2,
1570 "both the in-workspace and out-of-workspace locations must survive"
1571 );
1572 assert!(result.locations.iter().any(|l| l.uri == inside_uri));
1573 assert!(result.locations.iter().any(|l| l.uri == outside_uri));
1574 assert!(
1575 !result
1576 .locations
1577 .iter()
1578 .find(|l| l.uri == inside_uri)
1579 .unwrap()
1580 .out_of_workspace,
1581 "an in-workspace reference location must not be marked out_of_workspace"
1582 );
1583 assert!(
1584 result
1585 .locations
1586 .iter()
1587 .find(|l| l.uri == outside_uri)
1588 .unwrap()
1589 .out_of_workspace,
1590 "an out-of-workspace reference location must be marked out_of_workspace"
1591 );
1592 }
1593
1594 #[tokio::test]
1600 async fn test_handle_references_sets_truncated_flag_past_cap() {
1601 let dir = TempDir::new().unwrap();
1602 let server_id = ServerId::from_static("rust");
1603 let caps = lsp_types::ServerCapabilities {
1604 references_provider: Some(lsp_types::ReferencesProvider::Bool(true)),
1605 ..Default::default()
1606 };
1607 let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
1608
1609 let path = dir.path().join("main.rs");
1610 fs::write(&path, "fn main() {}").unwrap();
1611 let uri = Url::from_file_path(&path).unwrap().to_string();
1612
1613 let translator = Arc::new(translator);
1614 let handle = {
1615 let translator = Arc::clone(&translator);
1616 let path = path.to_string_lossy().to_string();
1617 tokio::spawn(async move {
1618 translator
1619 .handle_references(client_path(path), pos(1, 1), true, ResultContext::None)
1620 .await
1621 })
1622 };
1623
1624 let mut wire = BufReader::new(&mut server.write_stdout);
1625 let opened = read_framed_message(&mut wire).await;
1626 assert_eq!(opened["method"], "textDocument/didOpen");
1627 let request = read_framed_message(&mut wire).await;
1628 assert_eq!(request["method"], "textDocument/references");
1629
1630 let locations: Vec<serde_json::Value> = (0..MAX_NORMALIZED_LOCATIONS + 500)
1631 .map(|_| {
1632 serde_json::json!({
1633 "uri": uri,
1634 "range": {
1635 "start": {"line": 0, "character": 0},
1636 "end": {"line": 0, "character": 4}
1637 }
1638 })
1639 })
1640 .collect();
1641 write_response(
1642 &mut server.read_half_stdin,
1643 &request["id"],
1644 serde_json::json!(locations),
1645 )
1646 .await;
1647
1648 let result = timeout(Duration::from_secs(5), handle)
1649 .await
1650 .expect("handle_references should not hang")
1651 .unwrap()
1652 .unwrap();
1653
1654 assert_eq!(result.locations.len(), MAX_NORMALIZED_LOCATIONS);
1655 assert!(
1656 result.truncated,
1657 "a references response past MAX_NORMALIZED_LOCATIONS must set truncated: true"
1658 );
1659 }
1660
1661 #[tokio::test]
1665 async fn test_handle_implementation_flattens_location_list() {
1666 let dir = TempDir::new().unwrap();
1667 let server_id = ServerId::from_static("rust");
1668 let caps = lsp_types::ServerCapabilities {
1669 implementation_provider: Some(lsp_types::ImplementationProvider::Bool(true)),
1670 ..Default::default()
1671 };
1672 let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
1673
1674 let path = dir.path().join("main.rs");
1675 fs::write(&path, "fn main() {}").unwrap();
1676 let first_impl_path = dir.path().join("impl_a.rs");
1677 fs::write(&first_impl_path, "struct A;").unwrap();
1678 let first_impl_uri = Url::from_file_path(&first_impl_path).unwrap().to_string();
1679 let second_impl_path = dir.path().join("impl_b.rs");
1680 fs::write(&second_impl_path, "struct B;").unwrap();
1681 let second_impl_uri = Url::from_file_path(&second_impl_path).unwrap().to_string();
1682
1683 let translator = Arc::new(translator);
1684 let handle = {
1685 let translator = Arc::clone(&translator);
1686 let path = path.to_string_lossy().to_string();
1687 tokio::spawn(async move {
1688 translator
1689 .handle_implementation(
1690 client_path(path),
1691 Position::at(1, 1),
1692 ResultContext::None,
1693 )
1694 .await
1695 })
1696 };
1697
1698 let mut wire = BufReader::new(&mut server.write_stdout);
1699 let opened = read_framed_message(&mut wire).await;
1700 assert_eq!(opened["method"], "textDocument/didOpen");
1701 let request = read_framed_message(&mut wire).await;
1702 assert_eq!(request["method"], "textDocument/implementation");
1703
1704 write_response(
1705 &mut server.read_half_stdin,
1706 &request["id"],
1707 serde_json::json!([
1708 {
1709 "uri": first_impl_uri,
1710 "range": {
1711 "start": {"line": 0, "character": 0},
1712 "end": {"line": 0, "character": 9}
1713 }
1714 },
1715 {
1716 "uri": second_impl_uri,
1717 "range": {
1718 "start": {"line": 0, "character": 0},
1719 "end": {"line": 0, "character": 9}
1720 }
1721 }
1722 ]),
1723 )
1724 .await;
1725
1726 let result = timeout(Duration::from_secs(2), handle)
1727 .await
1728 .expect("handle_implementation should not hang")
1729 .unwrap()
1730 .unwrap();
1731
1732 assert_eq!(result.locations.len(), 2);
1733 assert_eq!(result.locations[0].uri, first_impl_uri);
1734 assert_eq!(result.locations[1].uri, second_impl_uri);
1735 }
1736
1737 #[tokio::test]
1740 async fn test_handle_declaration_flattens_location_list() {
1741 let dir = TempDir::new().unwrap();
1742 let server_id = ServerId::from_static("rust");
1743 let caps = lsp_types::ServerCapabilities {
1744 declaration_provider: Some(lsp_types::DeclarationProvider::Bool(true)),
1745 ..Default::default()
1746 };
1747 let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
1748
1749 let path = dir.path().join("main.rs");
1750 fs::write(&path, "fn main() {}").unwrap();
1751 let decl_path = dir.path().join("decl.rs");
1752 fs::write(&decl_path, "fn decl();").unwrap();
1753 let decl_uri = Url::from_file_path(&decl_path).unwrap().to_string();
1754
1755 let translator = Arc::new(translator);
1756 let handle = {
1757 let translator = Arc::clone(&translator);
1758 let path = client_path(&path);
1759 tokio::spawn(async move {
1760 translator
1761 .handle_declaration(path, pos(1, 1), ResultContext::None)
1762 .await
1763 })
1764 };
1765
1766 let mut wire = BufReader::new(&mut server.write_stdout);
1767 let opened = read_framed_message(&mut wire).await;
1768 assert_eq!(opened["method"], "textDocument/didOpen");
1769 let request = read_framed_message(&mut wire).await;
1770 assert_eq!(request["method"], "textDocument/declaration");
1771
1772 write_response(
1773 &mut server.read_half_stdin,
1774 &request["id"],
1775 serde_json::json!([{
1776 "uri": decl_uri,
1777 "range": {
1778 "start": {"line": 0, "character": 3},
1779 "end": {"line": 0, "character": 7}
1780 }
1781 }]),
1782 )
1783 .await;
1784
1785 let result = timeout(Duration::from_secs(2), handle)
1786 .await
1787 .expect("handle_declaration should not hang")
1788 .unwrap()
1789 .unwrap();
1790
1791 assert_eq!(result.locations.len(), 1);
1792 assert_eq!(result.locations[0].uri, decl_uri);
1793 }
1794
1795 #[tokio::test]
1797 async fn test_handle_declaration_flattens_link_list() {
1798 let dir = TempDir::new().unwrap();
1799 let server_id = ServerId::from_static("rust");
1800 let caps = lsp_types::ServerCapabilities {
1801 declaration_provider: Some(lsp_types::DeclarationProvider::Bool(true)),
1802 ..Default::default()
1803 };
1804 let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
1805
1806 let path = dir.path().join("main.rs");
1807 fs::write(&path, "fn main() {}").unwrap();
1808 let target_path = dir.path().join("target.h");
1809 fs::write(&target_path, "struct TargetType;").unwrap();
1810 let target_uri = Url::from_file_path(&target_path).unwrap().to_string();
1811
1812 let translator = Arc::new(translator);
1813 let handle = {
1814 let translator = Arc::clone(&translator);
1815 let path = client_path(&path);
1816 tokio::spawn(async move {
1817 translator
1818 .handle_declaration(path, pos(1, 1), ResultContext::None)
1819 .await
1820 })
1821 };
1822
1823 let mut wire = BufReader::new(&mut server.write_stdout);
1824 let _opened = read_framed_message(&mut wire).await;
1825 let request = read_framed_message(&mut wire).await;
1826 assert_eq!(request["method"], "textDocument/declaration");
1827
1828 write_response(
1829 &mut server.read_half_stdin,
1830 &request["id"],
1831 serde_json::json!([{
1832 "targetUri": target_uri,
1833 "targetRange": {
1834 "start": {"line": 0, "character": 0},
1835 "end": {"line": 0, "character": 18}
1836 },
1837 "targetSelectionRange": {
1838 "start": {"line": 0, "character": 7},
1839 "end": {"line": 0, "character": 17}
1840 }
1841 }]),
1842 )
1843 .await;
1844
1845 let result = timeout(Duration::from_secs(2), handle)
1846 .await
1847 .expect("handle_declaration should not hang")
1848 .unwrap()
1849 .unwrap();
1850
1851 assert_eq!(result.locations.len(), 1);
1852 assert_eq!(result.locations[0].range.start.character, 8);
1853 }
1854
1855 #[tokio::test]
1857 async fn test_handle_declaration_null_response_is_empty() {
1858 let dir = TempDir::new().unwrap();
1859 let server_id = ServerId::from_static("rust");
1860 let caps = lsp_types::ServerCapabilities {
1861 declaration_provider: Some(lsp_types::DeclarationProvider::Bool(true)),
1862 ..Default::default()
1863 };
1864 let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
1865
1866 let path = dir.path().join("main.rs");
1867 fs::write(&path, "fn main() {}").unwrap();
1868
1869 let translator = Arc::new(translator);
1870 let handle = {
1871 let translator = Arc::clone(&translator);
1872 let path = client_path(&path);
1873 tokio::spawn(async move {
1874 translator
1875 .handle_declaration(path, pos(1, 1), ResultContext::None)
1876 .await
1877 })
1878 };
1879
1880 let mut wire = BufReader::new(&mut server.write_stdout);
1881 let _opened = read_framed_message(&mut wire).await;
1882 let request = read_framed_message(&mut wire).await;
1883 write_response(
1884 &mut server.read_half_stdin,
1885 &request["id"],
1886 serde_json::Value::Null,
1887 )
1888 .await;
1889
1890 let result = timeout(Duration::from_secs(2), handle)
1891 .await
1892 .expect("handle_declaration should not hang")
1893 .unwrap()
1894 .unwrap();
1895 assert!(result.locations.is_empty());
1896 }
1897
1898 #[tokio::test(start_paused = true)]
1900 async fn test_handle_declaration_returns_workspace_indexing_error_when_loading() {
1901 let dir = TempDir::new().unwrap();
1902 let server_id = ServerId::from_static("rust");
1903 let caps = lsp_types::ServerCapabilities {
1904 declaration_provider: Some(lsp_types::DeclarationProvider::Bool(true)),
1905 ..Default::default()
1906 };
1907 let (translator, _server) = translator_with_capabilities(&dir, &server_id, caps);
1908
1909 let cache = Arc::new(Mutex::new(NotificationCache::new()));
1910 cache.lock().await.observe_indexing_signal(
1911 &server_id,
1912 "experimental/serverStatus",
1913 Some(&serde_json::json!({"quiescent": false})),
1914 );
1915 let translator = translator.with_notification_cache(cache);
1916
1917 let path = dir.path().join("main.rs");
1918 fs::write(&path, "fn main() {}").unwrap();
1919
1920 let err = translator
1921 .handle_declaration(client_path(&path), pos(1, 1), ResultContext::None)
1922 .await
1923 .unwrap_err();
1924
1925 assert_matches!(
1926 err,
1927 Error::WorkspaceIndexing { server_id: id, .. } if id == server_id
1928 );
1929 }
1930
1931 #[tokio::test]
1937 async fn test_handle_type_definition_flattens_definition_link_list() {
1938 let dir = TempDir::new().unwrap();
1939 let server_id = ServerId::from_static("rust");
1940 let caps = lsp_types::ServerCapabilities {
1941 type_definition_provider: Some(lsp_types::TypeDefinitionProvider::Bool(true)),
1942 ..Default::default()
1943 };
1944 let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
1945
1946 let path = dir.path().join("main.rs");
1947 fs::write(&path, "fn main() {}").unwrap();
1948 let target_path = dir.path().join("target_type.rs");
1949 fs::write(&target_path, "struct TargetType;").unwrap();
1950 let target_uri = Url::from_file_path(&target_path).unwrap().to_string();
1951
1952 let translator = Arc::new(translator);
1953 let handle = {
1954 let translator = Arc::clone(&translator);
1955 let path = path.to_string_lossy().to_string();
1956 tokio::spawn(async move {
1957 translator
1958 .handle_type_definition(
1959 client_path(path),
1960 Position::at(1, 1),
1961 ResultContext::None,
1962 )
1963 .await
1964 })
1965 };
1966
1967 let mut wire = BufReader::new(&mut server.write_stdout);
1968 let opened = read_framed_message(&mut wire).await;
1969 assert_eq!(opened["method"], "textDocument/didOpen");
1970 let request = read_framed_message(&mut wire).await;
1971 assert_eq!(request["method"], "textDocument/typeDefinition");
1972
1973 write_response(
1974 &mut server.read_half_stdin,
1975 &request["id"],
1976 serde_json::json!([{
1977 "targetUri": target_uri,
1978 "targetRange": {
1979 "start": {"line": 0, "character": 0},
1980 "end": {"line": 0, "character": 18}
1981 },
1982 "targetSelectionRange": {
1983 "start": {"line": 0, "character": 7},
1984 "end": {"line": 0, "character": 17}
1985 }
1986 }]),
1987 )
1988 .await;
1989
1990 let result = timeout(Duration::from_secs(2), handle)
1991 .await
1992 .expect("handle_type_definition should not hang")
1993 .unwrap()
1994 .unwrap();
1995
1996 assert_eq!(result.locations.len(), 1);
1997 assert_eq!(result.locations[0].uri, target_uri);
1998 assert_eq!(result.locations[0].range.start.character, 8);
1999 }
2000
2001 #[tokio::test]
2013 async fn test_lsp_locations_to_mcp_reads_disk_once_per_distinct_file_line() {
2014 let dir = TempDir::new().unwrap();
2015 let mut uris = Vec::new();
2016 for i in 0..3 {
2017 let path = dir.path().join(format!("file{i}.rs"));
2018 fs::write(&path, "hello").unwrap();
2019 uris.push(path_to_uri(&path).unwrap());
2020 }
2021
2022 let ctx = test_ctx_with(PositionEncoding::Utf8);
2023 let locs: Vec<lsp_types::Location> = (0..300)
2025 .map(|i| lsp_types::Location {
2026 uri: uris[i % 3].clone(),
2027 range: lsp_types::Range {
2028 start: lsp_types::Position {
2029 line: 0,
2030 character: 0,
2031 },
2032 end: lsp_types::Position {
2033 line: 0,
2034 character: 3,
2035 },
2036 },
2037 })
2038 .collect();
2039
2040 let result = lsp_locations_to_mcp(locs, &ctx).await;
2041
2042 assert_eq!(result.locations.len(), 300);
2043 assert!(!result.truncated);
2044 assert!(
2045 result.locations.iter().all(|l| l.range.end.character == 4),
2046 "MCP columns are 1-based, so LSP byte offset 3 in all-ASCII \"hello\" must convert \
2047 to 4"
2048 );
2049 assert_eq!(
2050 lock_std(&ctx.line_cache).entries.len(),
2051 3,
2052 "300 locations across 3 distinct files must populate the cache with exactly 3 \
2053 entries (one disk read per distinct file/line), not one per location"
2054 );
2055 }
2056
2057 #[tokio::test]
2064 async fn test_lsp_locations_to_mcp_truncates_to_max_normalized_locations() {
2065 let ctx = test_ctx();
2066 let uri = test_uri();
2067 let locs: Vec<lsp_types::Location> = (0..MAX_NORMALIZED_LOCATIONS + 500)
2068 .map(|_| lsp_types::Location {
2069 uri: uri.clone(),
2070 range: lsp_types::Range {
2071 start: lsp_types::Position {
2072 line: 0,
2073 character: 0,
2074 },
2075 end: lsp_types::Position {
2076 line: 0,
2077 character: 1,
2078 },
2079 },
2080 })
2081 .collect();
2082
2083 let result = lsp_locations_to_mcp(locs, &ctx).await;
2084
2085 assert_eq!(result.locations.len(), MAX_NORMALIZED_LOCATIONS);
2086 assert!(
2087 result.truncated,
2088 "a response naming more than MAX_NORMALIZED_LOCATIONS must report truncated: true"
2089 );
2090 }
2091
2092 #[test]
2093 fn test_item_budget_admit_spends_shared_allowance() {
2094 let mut budget = ItemBudget::new();
2095 let first = budget.admit(vec![0u8; MAX_NORMALIZED_LOCATIONS - 1]);
2096 assert_eq!(first.len(), MAX_NORMALIZED_LOCATIONS - 1);
2097 assert!(!budget.truncated());
2098
2099 let second = budget.admit(vec![0u8; 5]);
2100 assert_eq!(second.len(), 1);
2101 assert!(budget.truncated());
2102
2103 assert_eq!(budget.admit(vec![0u8; 3]).len(), 0);
2104 }
2105
2106 #[test]
2107 fn test_item_budget_admit_whole_never_splits_a_list() {
2108 let mut budget = ItemBudget::new();
2109 assert!(
2110 budget
2111 .admit_whole(vec![0u8; MAX_NORMALIZED_LOCATIONS - 2])
2112 .is_some()
2113 );
2114 assert!(budget.admit_whole(vec![0u8; 3]).is_none());
2115 assert!(budget.truncated());
2116 assert!(budget.admit_whole(vec![0u8; 2]).is_some());
2117 }
2118
2119 #[test]
2120 fn test_item_budget_with_cap_admits_a_lazy_sequence_up_to_the_cap() {
2121 let mut budget = ItemBudget::with_cap(3);
2122 assert_eq!(budget.admit_iter(0..3), [0, 1, 2]);
2123 assert!(!budget.truncated());
2124
2125 let mut budget = ItemBudget::with_cap(3);
2126 assert_eq!(budget.admit_iter(0..), [0, 1, 2]);
2127 assert!(budget.truncated());
2128 assert!(budget.admit_iter(0..2).is_empty());
2129 }
2130
2131 #[test]
2132 fn test_item_budget_exactly_at_cap_is_not_truncated() {
2133 let mut budget = ItemBudget::new();
2134 let items = budget.admit(vec![0u8; MAX_NORMALIZED_LOCATIONS]);
2135 assert_eq!(items.len(), MAX_NORMALIZED_LOCATIONS);
2136 assert!(!budget.truncated());
2137 }
2138}