1use std::collections::HashMap;
18use std::time::Duration;
19
20use lsp_types::{ProgressParams, ProgressToken};
21use schemars::JsonSchema;
22use serde::{Deserialize, Serialize};
23use tokio::time::Instant;
24
25use crate::config::ServerId;
26use crate::lsp::types::ProgressKind;
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum IndexingReset {
31 Forget,
34 AwaitReplacement {
40 within: Duration,
42 },
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, JsonSchema)]
63#[serde(rename_all = "snake_case")]
64pub enum IndexingState {
65 #[default]
68 Unknown,
69 Loading,
71 Ready,
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
97#[serde(rename_all = "snake_case")]
98pub enum IndexingPolicy {
99 #[default]
101 Auto,
102 Disabled,
104}
105
106impl IndexingPolicy {
107 #[expect(
116 clippy::trivially_copy_pass_by_ref,
117 reason = "serde's `skip_serializing_if` requires `fn(&T) -> bool`"
118 )]
119 #[must_use]
120 pub(crate) const fn is_auto(&self) -> bool {
121 matches!(self, Self::Auto)
122 }
123}
124
125#[derive(Debug, Clone, Copy, PartialEq, Eq)]
137enum IndexingSignalSource {
138 ServerStatus,
140 Progress,
142}
143
144#[derive(Debug, Clone)]
147enum TrackedIndexing {
148 AwaitingFirstSignal { since: Instant, within: Duration },
151 Signalled(Signalled),
153}
154
155#[derive(Debug, Clone, Copy, PartialEq, Eq)]
157enum StatusReading {
158 Loading,
160 Ready,
162}
163
164impl From<StatusReading> for IndexingState {
165 fn from(reading: StatusReading) -> Self {
166 match reading {
167 StatusReading::Loading => Self::Loading,
168 StatusReading::Ready => Self::Ready,
169 }
170 }
171}
172
173#[derive(Debug, Clone)]
185enum Signalled {
186 ServerStatus {
188 reading: StatusReading,
189 last_updated: Instant,
190 },
191 Progress(ProgressEntry),
193}
194
195#[derive(Debug, Clone)]
197struct ProgressEntry {
198 last_updated: Instant,
199 open: HashMap<ProgressToken, Instant>,
215 empty_since: Option<Instant>,
221 latched: bool,
228}
229
230impl ProgressEntry {
231 fn new() -> Self {
235 Self {
236 last_updated: Instant::now(),
237 open: HashMap::new(),
238 empty_since: None,
239 latched: false,
240 }
241 }
242}
243
244const SERVER_STATUS_METHOD: &str = "experimental/serverStatus";
247
248const QUIESCENT_FIELD: &str = "quiescent";
252
253pub const INDEXING_STALENESS_BOUND: Duration = Duration::from_mins(1);
263
264pub const DEFAULT_INDEXING_READY_TIMEOUT_SECS: u64 = 30;
275
276pub const PROGRESS_SETTLE: Duration = Duration::from_secs(3);
288
289pub const PROGRESS_LATCH_IDLE: Duration = Duration::from_secs(30);
303
304const PROGRESS_OPEN_CAP: usize = 64;
316
317const PROGRESS_TOKEN_MAX_LEN: usize = 256;
327
328#[derive(Debug, Default)]
331struct ServerIndexing {
332 policy: IndexingPolicy,
333 seen: Option<IndexingSignalSource>,
337 tracked: Option<TrackedIndexing>,
338}
339
340impl ServerIndexing {
341 fn is_disabled(&self) -> bool {
342 self.policy == IndexingPolicy::Disabled
343 }
344
345 fn log_wait_ended(&self, server_id: &ServerId) {
346 if matches!(
347 self.tracked,
348 Some(TrackedIndexing::AwaitingFirstSignal { .. })
349 ) {
350 tracing::debug!(%server_id, "indexing signal ends the pre-signal wait");
351 }
352 }
353}
354
355#[derive(Debug, Default)]
359pub struct IndexingTracker {
360 servers: HashMap<ServerId, ServerIndexing>,
361}
362
363impl IndexingTracker {
364 pub(crate) fn new() -> Self {
367 Self::default()
368 }
369
370 pub(crate) fn set_policy(&mut self, server_id: ServerId, policy: IndexingPolicy) {
374 self.servers.entry(server_id).or_default().policy = policy;
375 }
376
377 fn is_disabled(&self, server_id: &ServerId) -> bool {
378 self.servers
379 .get(server_id)
380 .is_some_and(ServerIndexing::is_disabled)
381 }
382
383 pub(crate) fn observe_server_status(
401 &mut self,
402 server_id: &ServerId,
403 method: &str,
404 params: Option<&serde_json::Value>,
405 ) {
406 if self.is_disabled(server_id) {
407 return;
408 }
409 if method != SERVER_STATUS_METHOD {
410 return;
411 }
412 let Some(quiescent) = params
413 .and_then(|p| p.get(QUIESCENT_FIELD))
414 .and_then(serde_json::Value::as_bool)
415 else {
416 return;
417 };
418
419 let server = self.servers.entry(server_id.clone()).or_default();
420 let sticky_ready = matches!(
421 server.tracked,
422 Some(TrackedIndexing::Signalled(Signalled::ServerStatus {
423 reading: StatusReading::Ready,
424 ..
425 }))
426 );
427 if sticky_ready {
428 return;
429 }
430
431 server.seen = Some(IndexingSignalSource::ServerStatus);
432 server.log_wait_ended(server_id);
433 server.tracked = Some(TrackedIndexing::Signalled(Signalled::ServerStatus {
434 reading: if quiescent {
435 StatusReading::Ready
436 } else {
437 StatusReading::Loading
438 },
439 last_updated: Instant::now(),
440 }));
441 }
442
443 pub(crate) fn observe_progress(&mut self, server_id: &ServerId, params: &ProgressParams) {
482 if self.is_disabled(server_id) {
483 return;
484 }
485 let Some(kind) = ProgressKind::from_value(¶ms.value) else {
486 return;
487 };
488 if let Some(server) = self.servers.get(server_id) {
489 match &server.tracked {
490 Some(TrackedIndexing::Signalled(Signalled::ServerStatus { .. })) => return,
491 Some(TrackedIndexing::AwaitingFirstSignal { .. })
493 if server.seen != Some(IndexingSignalSource::Progress) =>
494 {
495 return;
496 }
497 _ => {}
498 }
499 }
500 if let ProgressToken::String(token) = ¶ms.token
501 && token.len() > PROGRESS_TOKEN_MAX_LEN
502 {
503 return;
505 }
506
507 let server = self.servers.entry(server_id.clone()).or_default();
508 server.seen.get_or_insert(IndexingSignalSource::Progress);
509 server.log_wait_ended(server_id);
510 let tracked = server.tracked.get_or_insert_with(|| {
511 TrackedIndexing::Signalled(Signalled::Progress(ProgressEntry::new()))
512 });
513 if matches!(tracked, TrackedIndexing::AwaitingFirstSignal { .. }) {
514 *tracked = TrackedIndexing::Signalled(Signalled::Progress(ProgressEntry::new()));
515 }
516 let TrackedIndexing::Signalled(Signalled::Progress(entry)) = tracked else {
517 return;
518 };
519 if entry.latched {
520 return;
521 }
522
523 let now = Instant::now();
524 entry
526 .open
527 .retain(|_, began| began.elapsed() < INDEXING_STALENESS_BOUND);
528
529 match kind {
530 ProgressKind::Begin => {
531 if entry.open.is_empty()
532 && entry
533 .empty_since
534 .is_some_and(|since| since.elapsed() >= PROGRESS_LATCH_IDLE)
535 {
536 entry.latched = true;
537 entry.open.clear();
538 return;
539 }
540 if entry.open.len() < PROGRESS_OPEN_CAP {
541 entry.open.insert(params.token.clone(), now);
542 }
543 entry.empty_since = None;
545 }
546 ProgressKind::End => {
547 if entry.open.remove(¶ms.token).is_some() && entry.open.is_empty() {
548 entry.empty_since = Some(now);
549 }
550 }
551 }
552 entry.last_updated = now;
553 }
554
555 pub(crate) fn state(&self, server_id: &ServerId) -> IndexingState {
589 let Some(server) = self.servers.get(server_id) else {
590 return IndexingState::Unknown;
591 };
592 if server.is_disabled() {
593 return IndexingState::Unknown;
594 }
595 match &server.tracked {
596 None => IndexingState::Unknown,
597 Some(TrackedIndexing::AwaitingFirstSignal { since, within }) => {
598 if since.elapsed() < *within {
599 IndexingState::Loading
600 } else {
601 IndexingState::Unknown
602 }
603 }
604 Some(TrackedIndexing::Signalled(signalled)) => Self::signalled_state(signalled),
605 }
606 }
607
608 fn signalled_state(signalled: &Signalled) -> IndexingState {
609 match signalled {
610 Signalled::ServerStatus {
611 reading,
612 last_updated,
613 } => {
614 if *reading == StatusReading::Loading
615 && last_updated.elapsed() >= INDEXING_STALENESS_BOUND
616 {
617 IndexingState::Unknown
618 } else {
619 (*reading).into()
620 }
621 }
622 Signalled::Progress(entry) => Self::progress_state(entry),
623 }
624 }
625
626 fn progress_state(entry: &ProgressEntry) -> IndexingState {
627 if entry.latched {
628 return IndexingState::Ready;
629 }
630 if entry.last_updated.elapsed() >= INDEXING_STALENESS_BOUND {
631 return IndexingState::Unknown;
632 }
633 if entry.open.len() >= PROGRESS_OPEN_CAP {
634 return IndexingState::Unknown;
635 }
636 let has_live_open = entry
637 .open
638 .values()
639 .any(|began| began.elapsed() < INDEXING_STALENESS_BOUND);
640 if has_live_open {
641 return IndexingState::Loading;
642 }
643 if entry.open.is_empty() {
644 match entry.empty_since {
645 Some(since) if since.elapsed() < PROGRESS_SETTLE => IndexingState::Loading,
646 Some(_) => IndexingState::Ready,
647 None => IndexingState::Unknown,
649 }
650 } else {
651 IndexingState::Unknown
652 }
653 }
654
655 pub(crate) fn reset(&mut self, server_id: &ServerId, reset: IndexingReset) {
660 let Some(server) = self.servers.get_mut(server_id) else {
661 return;
662 };
663 server.tracked = None;
664 let IndexingReset::AwaitReplacement { within } = reset else {
665 return;
666 };
667 if server.is_disabled() || server.seen.is_none() {
668 return;
669 }
670 tracing::debug!(%server_id, ?within, "replacement awaits its first indexing signal");
671 server.tracked = Some(TrackedIndexing::AwaitingFirstSignal {
672 since: Instant::now(),
673 within,
674 });
675 }
676}
677
678#[cfg(test)]
679mod tests {
680 use super::*;
681
682 fn test_server() -> ServerId {
683 ServerId::from_static("test-server")
684 }
685
686 fn progress(kind: &str, token: i32) -> ProgressParams {
687 ProgressParams {
688 token: ProgressToken::Int(token),
689 value: serde_json::json!({ "kind": kind }),
690 }
691 }
692
693 #[test]
694 fn test_state_defaults_unknown() {
695 let tracker = IndexingTracker::new();
696 assert_eq!(tracker.state(&test_server()), IndexingState::Unknown);
697 }
698
699 #[test]
700 fn test_observe_server_status_quiescent_false_marks_loading() {
701 let mut tracker = IndexingTracker::new();
702 let server = test_server();
703 tracker.observe_server_status(
704 &server,
705 "experimental/serverStatus",
706 Some(&serde_json::json!({"quiescent": false})),
707 );
708 assert_eq!(tracker.state(&server), IndexingState::Loading);
709 }
710
711 #[test]
712 fn test_observe_server_status_quiescent_true_marks_ready() {
713 let mut tracker = IndexingTracker::new();
714 let server = test_server();
715 tracker.observe_server_status(
716 &server,
717 "experimental/serverStatus",
718 Some(&serde_json::json!({"quiescent": true})),
719 );
720 assert_eq!(tracker.state(&server), IndexingState::Ready);
721 }
722
723 #[test]
724 fn test_observe_server_status_ignores_unrecognized_method() {
725 let mut tracker = IndexingTracker::new();
726 let server = test_server();
727 tracker.observe_server_status(
728 &server,
729 "window/logMessage",
730 Some(&serde_json::json!({"quiescent": false})),
731 );
732 assert_eq!(tracker.state(&server), IndexingState::Unknown);
733 }
734
735 #[test]
740 fn test_malformed_server_status_does_not_mark_source_or_disable_progress() {
741 let mut tracker = IndexingTracker::new();
742 let server = test_server();
743 tracker.observe_server_status(&server, "experimental/serverStatus", None);
744 assert_eq!(tracker.state(&server), IndexingState::Unknown);
745
746 tracker.observe_progress(&server, &progress("begin", 1));
747 assert_eq!(
748 tracker.state(&server),
749 IndexingState::Loading,
750 "the progress path must still be live after a malformed serverStatus payload"
751 );
752 }
753
754 #[test]
755 fn test_observe_server_status_ready_is_sticky() {
756 let mut tracker = IndexingTracker::new();
757 let server = test_server();
758 tracker.observe_server_status(
759 &server,
760 "experimental/serverStatus",
761 Some(&serde_json::json!({"quiescent": true})),
762 );
763 tracker.observe_server_status(
764 &server,
765 "experimental/serverStatus",
766 Some(&serde_json::json!({"quiescent": false})),
767 );
768 assert_eq!(tracker.state(&server), IndexingState::Ready);
769 }
770
771 #[test]
772 fn test_observe_server_status_tracks_servers_independently() {
773 let mut tracker = IndexingTracker::new();
774 let rust = ServerId::from_static("rust");
775 let python = ServerId::from_static("python");
776 tracker.observe_server_status(
777 &rust,
778 "experimental/serverStatus",
779 Some(&serde_json::json!({"quiescent": false})),
780 );
781 assert_eq!(tracker.state(&rust), IndexingState::Loading);
782 assert_eq!(tracker.state(&python), IndexingState::Unknown);
783 }
784
785 #[tokio::test(start_paused = true)]
786 async fn test_state_treats_stale_server_status_loading_as_unknown() {
787 let mut tracker = IndexingTracker::new();
788 let server = test_server();
789 tracker.observe_server_status(
790 &server,
791 "experimental/serverStatus",
792 Some(&serde_json::json!({"quiescent": false})),
793 );
794 assert_eq!(tracker.state(&server), IndexingState::Loading);
795
796 tokio::time::advance(INDEXING_STALENESS_BOUND.saturating_sub(Duration::from_secs(1))).await;
797 assert_eq!(tracker.state(&server), IndexingState::Loading);
798
799 tokio::time::advance(Duration::from_secs(2)).await;
800 assert_eq!(tracker.state(&server), IndexingState::Unknown);
801 }
802
803 #[tokio::test(start_paused = true)]
804 async fn test_observe_server_status_refreshes_staleness_clock() {
805 let mut tracker = IndexingTracker::new();
806 let server = test_server();
807 tracker.observe_server_status(
808 &server,
809 "experimental/serverStatus",
810 Some(&serde_json::json!({"quiescent": false})),
811 );
812
813 tokio::time::advance(INDEXING_STALENESS_BOUND.saturating_sub(Duration::from_secs(1))).await;
814 tracker.observe_server_status(
815 &server,
816 "experimental/serverStatus",
817 Some(&serde_json::json!({"quiescent": false})),
818 );
819
820 tokio::time::advance(INDEXING_STALENESS_BOUND.saturating_sub(Duration::from_secs(1))).await;
821 assert_eq!(tracker.state(&server), IndexingState::Loading);
822 }
823
824 #[test]
828 fn test_first_begin_on_fresh_entry_yields_loading_never_ready() {
829 let mut tracker = IndexingTracker::new();
830 let server = test_server();
831 tracker.observe_progress(&server, &progress("begin", 1));
832 assert_eq!(tracker.state(&server), IndexingState::Loading);
833 }
834
835 #[tokio::test(start_paused = true)]
838 async fn test_unmatched_end_does_not_create_empty_since() {
839 let mut tracker = IndexingTracker::new();
840 let server = test_server();
841 tracker.observe_progress(&server, &progress("begin", 1));
843 tracker.observe_progress(&server, &progress("end", 1));
844
845 tokio::time::advance(PROGRESS_SETTLE + Duration::from_secs(1)).await;
846 assert_eq!(
847 tracker.state(&server),
848 IndexingState::Ready,
849 "settled after the real end"
850 );
851
852 tracker.observe_progress(&server, &progress("end", 99));
854 assert_eq!(
855 tracker.state(&server),
856 IndexingState::Ready,
857 "an unmatched end must not manufacture a fresh settle window"
858 );
859 }
860
861 #[tokio::test(start_paused = true)]
867 async fn test_end_as_first_ever_frame_reads_unknown_not_ready() {
868 let mut tracker = IndexingTracker::new();
869 let server = test_server();
870 tracker.observe_progress(&server, &progress("end", 1)); assert_eq!(
873 tracker.state(&server),
874 IndexingState::Unknown,
875 "an end with no observed prior begin must not read Ready immediately"
876 );
877
878 tokio::time::advance(PROGRESS_SETTLE + Duration::from_secs(1)).await;
879 assert_eq!(
880 tracker.state(&server),
881 IndexingState::Unknown,
882 "must still read Unknown once the settle window would have expired -- there was \
883 never a real empty_since to settle from"
884 );
885 }
886
887 #[tokio::test(start_paused = true)]
890 async fn test_begin_after_latch_idle_gap_latches_permanently() {
891 let mut tracker = IndexingTracker::new();
892 let server = test_server();
893 tracker.observe_progress(&server, &progress("begin", 1));
894 tracker.observe_progress(&server, &progress("end", 1));
895
896 tokio::time::advance(PROGRESS_LATCH_IDLE + Duration::from_secs(1)).await;
897 tracker.observe_progress(&server, &progress("begin", 2));
898 assert_eq!(
899 tracker.state(&server),
900 IndexingState::Ready,
901 "a begin after PROGRESS_LATCH_IDLE of quiet must latch, not re-gate"
902 );
903
904 tracker.observe_progress(&server, &progress("end", 2));
906 tracker.observe_progress(&server, &progress("begin", 3));
907 assert_eq!(tracker.state(&server), IndexingState::Ready);
908 }
909
910 #[tokio::test(start_paused = true)]
914 async fn test_begin_after_mid_gap_regates_and_does_not_latch() {
915 let mut tracker = IndexingTracker::new();
916 let server = test_server();
917 tracker.observe_progress(&server, &progress("begin", 1));
918 tracker.observe_progress(&server, &progress("end", 1));
919
920 tokio::time::advance(Duration::from_secs(10)).await; tracker.observe_progress(&server, &progress("begin", 2));
922 assert_eq!(
923 tracker.state(&server),
924 IndexingState::Loading,
925 "a mid-gap begin must re-gate the next phase, not latch it away"
926 );
927 }
928
929 #[tokio::test(start_paused = true)]
932 async fn test_phase_gap_shorter_than_settle_does_not_read_ready() {
933 let mut tracker = IndexingTracker::new();
934 let server = test_server();
935 tracker.observe_progress(&server, &progress("begin", 1));
936 tracker.observe_progress(&server, &progress("end", 1));
937
938 tokio::time::advance(PROGRESS_SETTLE.checked_sub(Duration::from_secs(1)).unwrap()).await;
939 assert_eq!(tracker.state(&server), IndexingState::Loading);
940 }
941
942 #[test]
943 fn test_open_non_empty_reads_loading() {
944 let mut tracker = IndexingTracker::new();
945 let server = test_server();
946 tracker.observe_progress(&server, &progress("begin", 1));
947 tracker.observe_progress(&server, &progress("begin", 2));
948 tracker.observe_progress(&server, &progress("end", 1));
949 assert_eq!(
950 tracker.state(&server),
951 IndexingState::Loading,
952 "token 2 is still open"
953 );
954 }
955
956 #[test]
961 fn test_observe_progress_tracks_servers_independently() {
962 let mut tracker = IndexingTracker::new();
963 let rust = ServerId::from_static("rust");
964 let go = ServerId::from_static("go");
965
966 tracker.observe_progress(&rust, &progress("begin", 1));
967
968 assert_eq!(
969 tracker.state(&rust),
970 IndexingState::Loading,
971 "rust's token is still open"
972 );
973 assert_eq!(
974 tracker.state(&go),
975 IndexingState::Unknown,
976 "go has received no progress signal of its own"
977 );
978 }
979
980 #[tokio::test(start_paused = true)]
981 async fn test_progress_last_updated_past_staleness_bound_reads_unknown() {
982 let mut tracker = IndexingTracker::new();
983 let server = test_server();
984 tracker.observe_progress(&server, &progress("begin", 1));
985
986 tokio::time::advance(INDEXING_STALENESS_BOUND + Duration::from_secs(1)).await;
987 assert_eq!(
988 tracker.state(&server),
989 IndexingState::Unknown,
990 "a single op with no phase boundary for over a minute must fail open"
991 );
992 }
993
994 #[tokio::test(start_paused = true)]
998 async fn test_multiphase_load_past_staleness_bound_with_boundaries_stays_loading() {
999 let mut tracker = IndexingTracker::new();
1000 let server = test_server();
1001 tracker.observe_progress(&server, &progress("begin", 1));
1002
1003 tokio::time::advance(Duration::from_secs(40)).await;
1004 tracker.observe_progress(&server, &progress("end", 1));
1005 tracker.observe_progress(&server, &progress("begin", 2));
1006
1007 tokio::time::advance(Duration::from_secs(40)).await;
1008 assert_eq!(tracker.state(&server), IndexingState::Loading);
1010 }
1011
1012 #[tokio::test(start_paused = true)]
1018 async fn test_lost_end_self_heals_after_staleness_bound() {
1019 let mut tracker = IndexingTracker::new();
1020 let server = test_server();
1021 tracker.observe_progress(&server, &progress("begin", 1)); tokio::time::advance(Duration::from_secs(30)).await;
1025 tracker.observe_progress(&server, &progress("begin", 2));
1026 tracker.observe_progress(&server, &progress("end", 2));
1027
1028 tokio::time::advance(Duration::from_secs(25)).await; tracker.observe_progress(&server, &progress("begin", 3));
1030 tracker.observe_progress(&server, &progress("end", 3));
1031
1032 assert_eq!(
1033 tracker.state(&server),
1034 IndexingState::Loading,
1035 "token 1 is still within its own staleness window at t=55s"
1036 );
1037
1038 tokio::time::advance(Duration::from_secs(6)).await; assert_eq!(
1041 tracker.state(&server),
1042 IndexingState::Unknown,
1043 "a token whose own begin exceeded INDEXING_STALENESS_BOUND must stop pinning \
1044 Loading even while unrelated later frames keep the entry-wide last_updated clock \
1045 fresh"
1046 );
1047 }
1048
1049 #[test]
1053 fn test_open_token_cap_forces_unknown() {
1054 let mut tracker = IndexingTracker::new();
1055 let server = test_server();
1056 for i in 0..(PROGRESS_OPEN_CAP - 1) {
1057 tracker.observe_progress(&server, &progress("begin", i32::try_from(i).unwrap()));
1058 }
1059 assert_eq!(
1060 tracker.state(&server),
1061 IndexingState::Loading,
1062 "PROGRESS_OPEN_CAP - 1 distinct open tokens must still read Loading"
1063 );
1064
1065 tracker.observe_progress(
1066 &server,
1067 &progress("begin", i32::try_from(PROGRESS_OPEN_CAP - 1).unwrap()),
1068 );
1069 assert_eq!(
1070 tracker.state(&server),
1071 IndexingState::Unknown,
1072 "reaching PROGRESS_OPEN_CAP must fail open instead of staying Loading forever"
1073 );
1074 }
1075
1076 #[test]
1080 fn test_oversized_string_token_is_rejected() {
1081 let mut tracker = IndexingTracker::new();
1082 let server = test_server();
1083 let oversized = ProgressParams {
1084 token: ProgressToken::String("x".repeat(PROGRESS_TOKEN_MAX_LEN + 1)),
1085 value: serde_json::json!({ "kind": "begin" }),
1086 };
1087 tracker.observe_progress(&server, &oversized);
1088 assert_eq!(
1089 tracker.state(&server),
1090 IndexingState::Unknown,
1091 "an oversized token must be dropped outright, not tracked"
1092 );
1093 }
1094
1095 #[test]
1096 fn test_malformed_progress_kind_ignored() {
1097 let mut tracker = IndexingTracker::new();
1098 let server = test_server();
1099 tracker.observe_progress(&server, &progress("report", 1));
1100 assert_eq!(tracker.state(&server), IndexingState::Unknown);
1101
1102 let missing_kind = ProgressParams {
1103 token: ProgressToken::Int(1),
1104 value: serde_json::json!({}),
1105 };
1106 tracker.observe_progress(&server, &missing_kind);
1107 assert_eq!(tracker.state(&server), IndexingState::Unknown);
1108 }
1109
1110 #[test]
1111 fn test_disabled_policy_pins_unknown() {
1112 let mut tracker = IndexingTracker::new();
1113 let server = test_server();
1114 tracker.set_policy(server.clone(), IndexingPolicy::Disabled);
1115
1116 tracker.observe_server_status(
1117 &server,
1118 "experimental/serverStatus",
1119 Some(&serde_json::json!({"quiescent": false})),
1120 );
1121 tracker.observe_progress(&server, &progress("begin", 1));
1122 assert_eq!(tracker.state(&server), IndexingState::Unknown);
1123 }
1124
1125 #[tokio::test(start_paused = true)]
1130 async fn test_progress_settle_then_quiescent_false_yields_loading() {
1131 let mut tracker = IndexingTracker::new();
1132 let server = test_server();
1133 tracker.observe_progress(&server, &progress("begin", 1));
1134 tracker.observe_progress(&server, &progress("end", 1));
1135 tokio::time::advance(PROGRESS_SETTLE + Duration::from_secs(1)).await;
1136 assert_eq!(tracker.state(&server), IndexingState::Ready);
1137
1138 tracker.observe_server_status(
1139 &server,
1140 "experimental/serverStatus",
1141 Some(&serde_json::json!({"quiescent": false})),
1142 );
1143 assert_eq!(tracker.state(&server), IndexingState::Loading);
1144 }
1145
1146 #[test]
1151 fn test_server_status_entry_ignores_later_progress_frames() {
1152 let mut tracker = IndexingTracker::new();
1153 let server = test_server();
1154 tracker.observe_server_status(
1155 &server,
1156 "experimental/serverStatus",
1157 Some(&serde_json::json!({"quiescent": true})),
1158 );
1159 tracker.observe_progress(&server, &progress("begin", 1));
1160 assert_eq!(tracker.state(&server), IndexingState::Ready);
1161 }
1162
1163 #[test]
1164 fn test_reset_clears_entry_but_not_policy() {
1165 let mut tracker = IndexingTracker::new();
1166 let server = test_server();
1167 tracker.set_policy(server.clone(), IndexingPolicy::Disabled);
1168 tracker.observe_progress(&server, &progress("begin", 1));
1169 tracker.reset(&server, IndexingReset::Forget);
1170 assert_eq!(
1171 tracker.state(&server),
1172 IndexingState::Unknown,
1173 "policy stays Disabled, so this reads Unknown regardless of the reset entry"
1174 );
1175
1176 let mut auto_tracker = IndexingTracker::new();
1177 auto_tracker.observe_progress(&server, &progress("begin", 1));
1178 auto_tracker.reset(&server, IndexingReset::Forget);
1179 assert_eq!(auto_tracker.state(&server), IndexingState::Unknown);
1180 }
1181
1182 const WITHIN: Duration = Duration::from_secs(30);
1183
1184 fn server_status(tracker: &mut IndexingTracker, server: &ServerId, quiescent: bool) {
1185 tracker.observe_server_status(
1186 server,
1187 "experimental/serverStatus",
1188 Some(&serde_json::json!({ "quiescent": quiescent })),
1189 );
1190 }
1191
1192 fn await_replacement(tracker: &mut IndexingTracker, server: &ServerId) {
1193 tracker.reset(server, IndexingReset::AwaitReplacement { within: WITHIN });
1194 }
1195
1196 #[test]
1197 fn test_await_replacement_without_history_stays_unknown() {
1198 let mut tracker = IndexingTracker::new();
1199 let server = test_server();
1200 await_replacement(&mut tracker, &server);
1201 assert_eq!(tracker.state(&server), IndexingState::Unknown);
1202 }
1203
1204 #[test]
1205 fn test_await_replacement_after_ready_reads_loading() {
1206 let mut tracker = IndexingTracker::new();
1207 let server = test_server();
1208 server_status(&mut tracker, &server, true);
1209 await_replacement(&mut tracker, &server);
1210 assert_eq!(tracker.state(&server), IndexingState::Loading);
1211 }
1212
1213 #[test]
1214 fn test_await_replacement_after_loading_reads_loading() {
1215 let mut tracker = IndexingTracker::new();
1216 let server = test_server();
1217 server_status(&mut tracker, &server, false);
1218 await_replacement(&mut tracker, &server);
1219 assert_eq!(tracker.state(&server), IndexingState::Loading);
1220 }
1221
1222 #[tokio::test(start_paused = true)]
1223 async fn test_await_replacement_ends_after_within() {
1224 let mut tracker = IndexingTracker::new();
1225 let server = test_server();
1226 server_status(&mut tracker, &server, true);
1227 await_replacement(&mut tracker, &server);
1228
1229 tokio::time::advance(WITHIN.saturating_sub(Duration::from_secs(1))).await;
1230 assert_eq!(tracker.state(&server), IndexingState::Loading);
1231 tokio::time::advance(Duration::from_secs(2)).await;
1232 assert_eq!(tracker.state(&server), IndexingState::Unknown);
1233 }
1234
1235 #[test]
1236 fn test_await_replacement_ready_signal_ends_wait_at_once() {
1237 let mut tracker = IndexingTracker::new();
1238 let server = test_server();
1239 server_status(&mut tracker, &server, true);
1240 await_replacement(&mut tracker, &server);
1241 server_status(&mut tracker, &server, true);
1242 assert_eq!(tracker.state(&server), IndexingState::Ready);
1243 }
1244
1245 #[test]
1246 fn test_await_replacement_loading_signal_is_a_real_loading_entry() {
1247 let mut tracker = IndexingTracker::new();
1248 let server = test_server();
1249 server_status(&mut tracker, &server, true);
1250 await_replacement(&mut tracker, &server);
1251 server_status(&mut tracker, &server, false);
1252 assert_eq!(tracker.state(&server), IndexingState::Loading);
1253 server_status(&mut tracker, &server, true);
1254 assert_eq!(tracker.state(&server), IndexingState::Ready);
1255 }
1256
1257 #[test]
1258 fn test_await_replacement_ignores_progress_of_a_server_status_server() {
1259 let mut tracker = IndexingTracker::new();
1260 let server = test_server();
1261 server_status(&mut tracker, &server, true);
1262 await_replacement(&mut tracker, &server);
1263 tracker.observe_progress(&server, &progress("begin", 1));
1264 tracker.observe_progress(&server, &progress("end", 1));
1265 assert_eq!(
1266 tracker.state(&server),
1267 IndexingState::Loading,
1268 "startup progress must not end the wait for the serverStatus signal"
1269 );
1270 }
1271
1272 #[test]
1273 fn test_await_replacement_progress_server_is_replaced_by_progress_entry() {
1274 let mut tracker = IndexingTracker::new();
1275 let server = test_server();
1276 tracker.observe_progress(&server, &progress("begin", 1));
1277 tracker.observe_progress(&server, &progress("end", 1));
1278 await_replacement(&mut tracker, &server);
1279 assert_eq!(tracker.state(&server), IndexingState::Loading);
1280
1281 tracker.observe_progress(&server, &progress("begin", 2));
1282 assert_eq!(tracker.state(&server), IndexingState::Loading);
1283 tracker.observe_progress(&server, &progress("end", 2));
1284 assert_eq!(
1285 tracker.state(&server),
1286 IndexingState::Loading,
1287 "settle window of the fresh progress entry"
1288 );
1289 }
1290
1291 #[test]
1292 fn test_await_replacement_twice_restarts_the_wait() {
1293 let mut tracker = IndexingTracker::new();
1294 let server = test_server();
1295 server_status(&mut tracker, &server, true);
1296 await_replacement(&mut tracker, &server);
1297 await_replacement(&mut tracker, &server);
1298 assert_eq!(tracker.state(&server), IndexingState::Loading);
1299 tracker.reset(&server, IndexingReset::Forget);
1300 assert_eq!(tracker.state(&server), IndexingState::Unknown);
1301 }
1302
1303 #[test]
1304 fn test_await_replacement_respects_disabled_policy() {
1305 let mut tracker = IndexingTracker::new();
1306 let server = test_server();
1307 tracker.set_policy(server.clone(), IndexingPolicy::Disabled);
1308 await_replacement(&mut tracker, &server);
1309 assert_eq!(tracker.state(&server), IndexingState::Unknown);
1310 }
1311
1312 #[test]
1313 fn test_await_replacement_tracks_servers_independently() {
1314 let mut tracker = IndexingTracker::new();
1315 let rust = ServerId::from_static("rust");
1316 let python = ServerId::from_static("python");
1317 server_status(&mut tracker, &rust, true);
1318 server_status(&mut tracker, &python, true);
1319 await_replacement(&mut tracker, &rust);
1320 assert_eq!(tracker.state(&rust), IndexingState::Loading);
1321 assert_eq!(tracker.state(&python), IndexingState::Ready);
1322 }
1323
1324 mod mock_server {
1329 use std::time::Duration;
1330
1331 use serde_json::json;
1332 use tokio::io::BufReader;
1333
1334 use super::*;
1335 use crate::lsp::LspNotification;
1336 use crate::test_lsp::{
1337 FakeServer, fake_lsp_client_with_lanes, read_framed_message,
1338 spawn_test_pump_over_lanes, write_notification, write_request,
1339 };
1340
1341 async fn assert_create_request_is_acknowledged(server: &mut FakeServer) {
1342 write_request(
1343 &mut server.read_half_stdin,
1344 &json!(7),
1345 "window/workDoneProgress/create",
1346 json!({ "token": "idx" }),
1347 )
1348 .await;
1349
1350 let reply = read_framed_message(&mut BufReader::new(&mut server.write_stdout)).await;
1351 assert_eq!(reply["id"], 7);
1352 assert_eq!(reply.get("result"), Some(&serde_json::Value::Null));
1353 assert!(reply.get("error").is_none(), "unexpected error: {reply}");
1354 }
1355
1356 async fn send_progress(server: &mut FakeServer, kind: &str) {
1357 write_notification(
1358 &mut server.read_half_stdin,
1359 "$/progress",
1360 json!({ "token": "idx", "value": { "kind": kind, "title": "Indexing" } }),
1361 )
1362 .await;
1363 }
1364
1365 fn kind_of(notification: &LspNotification) -> &str {
1366 let LspNotification::Progress(params) = notification else {
1367 panic!("expected a progress notification, got {notification:?}");
1368 };
1369 params.value["kind"].as_str().unwrap()
1370 }
1371
1372 async fn wait_for(
1373 cache: &tokio::sync::Mutex<crate::bridge::NotificationCache>,
1374 id: &ServerId,
1375 expected: IndexingState,
1376 ) {
1377 for _ in 0..200 {
1378 if cache.lock().await.indexing_state(id) == expected {
1379 return;
1380 }
1381 tokio::time::sleep(Duration::from_millis(1)).await;
1382 }
1383 panic!("indexing state never became {expected:?}");
1384 }
1385
1386 #[tokio::test]
1387 async fn test_report_frame_never_reaches_the_lifecycle_lane() {
1388 let (_client, mut server, mut lanes) = fake_lsp_client_with_lanes();
1389 assert_create_request_is_acknowledged(&mut server).await;
1390
1391 for kind in ["begin", "report", "end"] {
1392 send_progress(&mut server, kind).await;
1393 }
1394
1395 let first = tokio::time::timeout(Duration::from_secs(5), lanes.lifecycle_rx.recv())
1396 .await
1397 .unwrap()
1398 .unwrap();
1399 let second = tokio::time::timeout(Duration::from_secs(5), lanes.lifecycle_rx.recv())
1400 .await
1401 .unwrap()
1402 .unwrap();
1403 assert_eq!((kind_of(&first), kind_of(&second)), ("begin", "end"));
1404 assert!(lanes.lifecycle_rx.try_recv().is_err());
1405 }
1406
1407 #[tokio::test]
1408 async fn test_progress_sequence_moves_tracker_from_loading_to_ready() {
1409 let (_client, mut server, lanes) = fake_lsp_client_with_lanes();
1410 assert_create_request_is_acknowledged(&mut server).await;
1411 let (cache, _cancel) = spawn_test_pump_over_lanes(lanes);
1412 let id = ServerId::from_static("rust");
1413 tokio::time::pause();
1414
1415 send_progress(&mut server, "begin").await;
1416 wait_for(&cache, &id, IndexingState::Loading).await;
1417
1418 send_progress(&mut server, "report").await;
1419 send_progress(&mut server, "end").await;
1420 tokio::time::sleep(Duration::from_millis(10)).await;
1421 assert_eq!(
1422 cache.lock().await.indexing_state(&id),
1423 IndexingState::Loading,
1424 "still inside the settle window after `end`"
1425 );
1426
1427 tokio::time::advance(PROGRESS_SETTLE + Duration::from_secs(1)).await;
1428 assert_eq!(cache.lock().await.indexing_state(&id), IndexingState::Ready);
1429 }
1430 }
1431}