1use lsp_types::CompletionTriggerKind;
4
5use super::Translator;
6use super::dto::{
7 Completion, CompletionTrigger, CompletionsResult, InlayHintEntry, InlayHintsResult, Position,
8 PositionRange, SignatureHelpResult, SignatureInfo, SignatureParameter, lsp_kind_to_u32,
9};
10use super::navigation::ItemBudget;
11use super::positioned::Positioned;
12use super::routing::{Capability, DisclosedCapability, IndexingGate};
13use crate::bridge::encoding::{LabelOffsets, PositionEncoding};
14use crate::bridge::{ClientPath, Indexed};
15use crate::error::Result;
16
17fn extract_documentation(doc: lsp_types::Documentation) -> String {
19 match doc {
20 lsp_types::Documentation::String(s) => s,
21 lsp_types::Documentation::MarkupContent(m) => m.value,
22 }
23}
24
25const MAX_SIGNATURE_PARAMETERS: usize = 256;
27
28const RESOLVED_LABEL_BUDGET_MULTIPLE: usize = 4;
32
33fn signature_parameters(
43 params: Vec<lsp_types::ParameterInformation>,
44 signature_label: &str,
45 encoding: PositionEncoding,
46) -> Vec<SignatureParameter> {
47 if params.len() > MAX_SIGNATURE_PARAMETERS {
48 tracing::warn!(
49 reported = params.len(),
50 cap = MAX_SIGNATURE_PARAMETERS,
51 "signature parameter count exceeds the cap; truncating"
52 );
53 }
54 let offsets = LabelOffsets::new(
55 signature_label,
56 params
57 .iter()
58 .take(MAX_SIGNATURE_PARAMETERS)
59 .filter_map(|p| match p.label {
60 lsp_types::ParameterInformationLabel::Tuple((start, end)) => {
61 Some(<[u32; 2]>::from((start, end)))
62 }
63 lsp_types::ParameterInformationLabel::String(_) => None,
64 })
65 .flatten(),
66 encoding,
67 );
68 let mut resolved_budget = signature_label
69 .len()
70 .saturating_mul(RESOLVED_LABEL_BUDGET_MULTIPLE);
71 params
72 .into_iter()
73 .take(MAX_SIGNATURE_PARAMETERS)
74 .map(|param| {
75 let label = match param.label {
76 lsp_types::ParameterInformationLabel::String(s) => Some(s),
77 lsp_types::ParameterInformationLabel::Tuple((start, end)) => {
78 let resolved = offsets
79 .substring(start, end)
80 .filter(|text| text.len() <= resolved_budget);
81 if let Some(text) = resolved {
82 resolved_budget = resolved_budget.saturating_sub(text.len());
83 } else {
84 tracing::warn!(
85 start,
86 end,
87 encoding = encoding.to_lsp(),
88 "signature parameter label offsets do not resolve within the \
89 signature label; omitting the label"
90 );
91 }
92 resolved.map(str::to_string)
93 }
94 };
95 SignatureParameter {
96 label,
97 documentation: param.documentation.map(extract_documentation),
98 }
99 })
100 .collect()
101}
102
103impl Translator {
104 pub async fn handle_completions(
112 &self,
113 file_path: ClientPath,
114 position: Position,
115 trigger: Option<CompletionTrigger>,
116 ) -> Result<CompletionsResult> {
117 let context = trigger.map(|trigger_char| lsp_types::CompletionContext {
118 trigger_kind: CompletionTriggerKind::TriggerCharacter,
119 trigger_character: Some(trigger_char.into_string()),
120 });
121
122 let Positioned {
123 result: response,
124 ctx,
125 doc: _doc,
126 } = self
127 .position_request::<lsp_types::CompletionRequest>(
128 &file_path,
129 position,
130 IndexingGate::Required(Capability::Completions),
131 context,
132 )
133 .await?;
134
135 let items = match response {
136 Some(lsp_types::CompletionResponse::CompletionItemList(items)) => items,
137 Some(lsp_types::CompletionResponse::CompletionList(list)) => list.items,
138 None => vec![],
139 };
140
141 let result = CompletionsResult {
142 items: items
143 .into_iter()
144 .map(|item| Completion {
145 label: item.label,
146 kind: item.kind.map(lsp_kind_to_u32),
147 detail: item.detail,
148 documentation: item.documentation.map(extract_documentation),
149 })
150 .collect(),
151 positions_degraded: ctx.positions_degraded(),
152 };
153
154 Ok(result)
155 }
156
157 pub async fn handle_signature_help(
167 &self,
168 file_path: ClientPath,
169 position: Position,
170 ) -> Result<Indexed<SignatureHelpResult>> {
171 let Positioned {
172 result:
173 Indexed {
174 result: response,
175 indexing,
176 },
177 ctx,
178 doc: _doc,
179 } = self
180 .disclosed_position_request::<lsp_types::SignatureHelpRequest>(
181 &file_path,
182 position,
183 DisclosedCapability::SignatureHelp,
184 (),
185 )
186 .await?;
187
188 let result = match response {
189 Some(sig_help) => SignatureHelpResult {
190 signatures: sig_help
191 .signatures
192 .into_iter()
193 .map(|sig| SignatureInfo {
194 parameters: signature_parameters(
195 sig.parameters.unwrap_or_default(),
196 &sig.label,
197 ctx.encoding,
198 ),
199 documentation: sig.documentation.map(extract_documentation),
200 label: sig.label,
201 })
202 .collect(),
203 active_signature: sig_help.active_signature,
204 active_parameter: sig_help.active_parameter.and_then(|ap| match ap {
205 lsp_types::ActiveParameter::Int(n) => Some(n),
206 lsp_types::ActiveParameter::Null => None,
207 }),
208 positions_degraded: ctx.positions_degraded(),
209 },
210 None => SignatureHelpResult {
211 signatures: vec![],
212 active_signature: None,
213 active_parameter: None,
214 positions_degraded: ctx.positions_degraded(),
215 },
216 };
217
218 Ok(Indexed::new(result, indexing))
219 }
220
221 pub async fn handle_inlay_hints(
231 &self,
232 file_path: ClientPath,
233 range: PositionRange,
234 ) -> Result<Indexed<InlayHintsResult>> {
235 let Positioned {
236 result:
237 Indexed {
238 result: response,
239 indexing,
240 },
241 ctx,
242 doc,
243 } = self
244 .disclosed_range_request::<lsp_types::InlayHintRequest>(
245 &file_path,
246 DisclosedCapability::InlayHints,
247 range,
248 (),
249 )
250 .await?;
251 let uri = doc.uri();
252
253 let mut budget = ItemBudget::new();
254 let lsp_hints = budget.admit(response.unwrap_or_default());
255 let mut hints = Vec::with_capacity(lsp_hints.len());
256 for hint in lsp_hints {
257 let position = ctx.to_mcp(uri, hint.position).await;
258 let label = match hint.label {
259 lsp_types::Label::String(s) => s,
260 lsp_types::Label::InlayHintLabelPartList(parts) => {
261 parts.into_iter().map(|p| p.value).collect::<String>()
262 }
263 };
264 let tooltip = hint.tooltip.map(|t| match t {
265 lsp_types::Tooltip::String(s) => s,
266 lsp_types::Tooltip::MarkupContent(m) => m.value,
267 });
268 hints.push(InlayHintEntry {
269 position,
270 label,
271 kind: hint.kind.map(lsp_kind_to_u32),
272 padding_left: hint.padding_left,
273 padding_right: hint.padding_right,
274 tooltip,
275 });
276 }
277
278 Ok(Indexed::new(
279 InlayHintsResult {
280 hints,
281 truncated: budget.truncated(),
282 positions_degraded: ctx.positions_degraded(),
283 },
284 indexing,
285 ))
286 }
287}
288
289#[cfg(test)]
290mod tests {
291 use std::{assert_matches, fs};
292
293 use super::*;
294 use crate::bridge::IndexingSignal;
295 use crate::bridge::translator::testing::*;
296 use crate::config::ServerId;
297 use crate::error::Error;
298 use crate::test_lsp::client_path;
299
300 #[tokio::test(start_paused = true)]
304 async fn test_handle_completions_returns_workspace_indexing_error_when_loading() {
305 use std::sync::Arc;
306
307 use tempfile::TempDir;
308 use tokio::sync::Mutex;
309
310 use crate::bridge::NotificationCache;
311 use crate::config::ServerId;
312
313 let dir = TempDir::new().unwrap();
314 let server_id = ServerId::from_static("rust");
315 let caps = lsp_types::ServerCapabilities {
316 completion_provider: Some(lsp_types::CompletionOptions::default()),
317 ..Default::default()
318 };
319 let (translator, _server) = translator_with_capabilities(&dir, &server_id, caps);
320
321 let cache = Arc::new(Mutex::new(NotificationCache::new()));
322 cache.lock().await.observe_indexing_signal(
323 &server_id,
324 "experimental/serverStatus",
325 Some(&serde_json::json!({"quiescent": false})),
326 );
327 let translator = translator.with_notification_cache(cache);
328
329 let path = dir.path().join("main.rs");
330 fs::write(&path, "fn main() {}").unwrap();
331
332 let err = translator
333 .handle_completions(
334 client_path(path.to_string_lossy().into_owned()),
335 Position::at(1, 1),
336 None,
337 )
338 .await
339 .unwrap_err();
340
341 assert_matches!(
342 err,
343 Error::WorkspaceIndexing { server_id: id, .. } if id == server_id
344 );
345 }
346
347 #[tokio::test]
350 async fn test_handle_completions_dispatches_when_indexing_ready() {
351 use std::sync::Arc;
352
353 use tempfile::TempDir;
354 use tokio::io::BufReader;
355 use tokio::sync::Mutex;
356
357 use crate::bridge::NotificationCache;
358 use crate::config::ServerId;
359
360 let dir = TempDir::new().unwrap();
361 let server_id = ServerId::from_static("rust");
362 let caps = lsp_types::ServerCapabilities {
363 completion_provider: Some(lsp_types::CompletionOptions::default()),
364 ..Default::default()
365 };
366 let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
367
368 let cache = Arc::new(Mutex::new(NotificationCache::new()));
369 cache.lock().await.observe_indexing_signal(
370 &server_id,
371 "experimental/serverStatus",
372 Some(&serde_json::json!({"quiescent": true})),
373 );
374 let translator = Arc::new(translator.with_notification_cache(cache));
375
376 let path = dir.path().join("main.rs");
377 fs::write(&path, "fn main() {}").unwrap();
378
379 let handle = {
380 let translator = Arc::clone(&translator);
381 let path = path.to_string_lossy().to_string();
382 tokio::spawn(async move {
383 translator
384 .handle_completions(client_path(path), Position::at(1, 1), None)
385 .await
386 })
387 };
388
389 let mut wire = BufReader::new(&mut server.write_stdout);
390 let opened = read_framed_message(&mut wire).await;
391 assert_eq!(opened["method"], "textDocument/didOpen");
392 let request = read_framed_message(&mut wire).await;
393 assert_eq!(request["method"], "textDocument/completion");
394
395 write_response(
396 &mut server.read_half_stdin,
397 &request["id"],
398 serde_json::json!([]),
399 )
400 .await;
401
402 let result = handle.await.unwrap().unwrap();
403 assert!(result.items.is_empty());
404 }
405
406 #[tokio::test]
411 async fn test_handle_inlay_hints_preserves_custom_kind_above_u8_range() {
412 use std::sync::Arc;
413
414 use tempfile::TempDir;
415 use tokio::io::BufReader;
416
417 use crate::bridge::translator::testing::pos;
418 use crate::config::ServerId;
419
420 let dir = TempDir::new().unwrap();
421 let server_id = ServerId::from_static("rust");
422 let caps = lsp_types::ServerCapabilities {
423 inlay_hint_provider: Some(lsp_types::InlayHintProvider::Bool(true)),
424 ..Default::default()
425 };
426 let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
427
428 let path = dir.path().join("main.rs");
429 fs::write(&path, "fn main() {}\n").unwrap();
430
431 let translator = Arc::new(translator);
432 let handle = {
433 let translator = Arc::clone(&translator);
434 let path = path.to_string_lossy().to_string();
435 tokio::spawn(async move {
436 translator
437 .handle_inlay_hints(client_path(path), span(pos(1, 1), pos(1, 13)))
438 .await
439 })
440 };
441
442 let mut wire = BufReader::new(&mut server.write_stdout);
443 let opened = read_framed_message(&mut wire).await;
444 assert_eq!(opened["method"], "textDocument/didOpen");
445 let request = read_framed_message(&mut wire).await;
446 assert_eq!(request["method"], "textDocument/inlayHint");
447
448 write_response(
449 &mut server.read_half_stdin,
450 &request["id"],
451 serde_json::json!([{
452 "position": {"line": 0, "character": 5},
453 "label": "custom",
454 "kind": 300,
455 }]),
456 )
457 .await;
458
459 let result = handle.await.unwrap().unwrap().result;
460 assert_eq!(result.hints.len(), 1);
461 assert_eq!(
462 result.hints[0].kind,
463 Some(300u32),
464 "a custom InlayHintKind above u8::MAX must not be truncated or dropped"
465 );
466 }
467
468 async fn inlay_hints_with_response(count: usize) -> InlayHintsResult {
469 use std::sync::Arc;
470
471 use tempfile::TempDir;
472 use tokio::io::BufReader;
473
474 use crate::bridge::translator::testing::pos;
475 use crate::config::ServerId;
476
477 let dir = TempDir::new().unwrap();
478 let server_id = ServerId::from_static("rust");
479 let caps = lsp_types::ServerCapabilities {
480 inlay_hint_provider: Some(lsp_types::InlayHintProvider::Bool(true)),
481 ..Default::default()
482 };
483 let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
484
485 let path = dir.path().join("main.rs");
486 fs::write(&path, "fn main() {}\n").unwrap();
487
488 let translator = Arc::new(translator);
489 let handle = {
490 let translator = Arc::clone(&translator);
491 let path = path.to_string_lossy().to_string();
492 tokio::spawn(async move {
493 translator
494 .handle_inlay_hints(client_path(path), span(pos(1, 1), pos(1, 13)))
495 .await
496 })
497 };
498
499 let mut wire = BufReader::new(&mut server.write_stdout);
500 let opened = read_framed_message(&mut wire).await;
501 assert_eq!(opened["method"], "textDocument/didOpen");
502 let request = read_framed_message(&mut wire).await;
503 assert_eq!(request["method"], "textDocument/inlayHint");
504
505 let hints: Vec<_> = (0..count)
506 .map(|_| serde_json::json!({"position": {"line": 0, "character": 5}, "label": "h"}))
507 .collect();
508 write_response(
509 &mut server.read_half_stdin,
510 &request["id"],
511 serde_json::Value::Array(hints),
512 )
513 .await;
514
515 handle.await.unwrap().unwrap().result
516 }
517
518 #[tokio::test]
521 async fn test_handle_inlay_hints_caps_item_count_and_reports_truncation() {
522 use crate::bridge::translator::navigation::MAX_NORMALIZED_LOCATIONS;
523
524 let result = inlay_hints_with_response(MAX_NORMALIZED_LOCATIONS + 500).await;
525 assert_eq!(result.hints.len(), MAX_NORMALIZED_LOCATIONS);
526 assert!(result.truncated);
527 }
528
529 #[tokio::test]
531 async fn test_handle_inlay_hints_at_cap_is_not_truncated() {
532 use crate::bridge::translator::navigation::MAX_NORMALIZED_LOCATIONS;
533
534 let result = inlay_hints_with_response(MAX_NORMALIZED_LOCATIONS).await;
535 assert_eq!(result.hints.len(), MAX_NORMALIZED_LOCATIONS);
536 assert!(!result.truncated);
537 }
538
539 async fn utf8_degradation(
540 content: &str,
541 line: u32,
542 character: u32,
543 method: &str,
544 ) -> serde_json::Value {
545 use std::sync::Arc;
546
547 use tempfile::TempDir;
548 use tokio::io::BufReader;
549
550 use crate::config::ServerId;
551
552 let dir = TempDir::new().unwrap();
553 let server_id = ServerId::from_static("rust");
554 let caps = lsp_types::ServerCapabilities {
555 completion_provider: Some(lsp_types::CompletionOptions::default()),
556 signature_help_provider: Some(lsp_types::SignatureHelpOptions::default()),
557 ..Default::default()
558 };
559 let (translator, mut server) = translator_with_capabilities_and_encoding(
560 &dir,
561 &server_id,
562 caps,
563 lsp_types::PositionEncodingKind::UTF8,
564 );
565 let path = dir.path().join("main.rs");
566 fs::write(&path, content).unwrap();
567
568 let translator = Arc::new(translator);
569 let handle = {
570 let translator = Arc::clone(&translator);
571 let path = path.to_string_lossy().to_string();
572 let method = method.to_string();
573 tokio::spawn(async move {
574 if method == "textDocument/completion" {
575 translator
576 .handle_completions(client_path(path), pos(line, character), None)
577 .await
578 .map(|r| serde_json::to_value(r).unwrap())
579 } else {
580 translator
581 .handle_signature_help(client_path(path), pos(line, character))
582 .await
583 .map(|r| serde_json::to_value(r).unwrap())
584 }
585 })
586 };
587
588 let mut wire = BufReader::new(&mut server.write_stdout);
589 let opened = read_framed_message(&mut wire).await;
590 assert_eq!(opened["method"], "textDocument/didOpen");
591 let request = read_framed_message(&mut wire).await;
592 assert_eq!(request["method"], method);
593 let response = if method == "textDocument/completion" {
594 serde_json::json!([])
595 } else {
596 serde_json::Value::Null
597 };
598 write_response(&mut server.read_half_stdin, &request["id"], response).await;
599
600 handle.await.unwrap().unwrap()
601 }
602
603 #[tokio::test]
606 async fn test_handle_completions_trailing_empty_line_is_not_degraded() {
607 let wire = utf8_degradation("fn main() {}\n", 2, 1, "textDocument/completion").await;
608 assert!(wire.get("positions_degraded").is_none());
609 }
610
611 #[tokio::test]
614 async fn test_handle_completions_and_signature_help_line_past_eof_are_rejected() {
615 use tempfile::TempDir;
616
617 use crate::config::ServerId;
618
619 let dir = TempDir::new().unwrap();
620 let caps = lsp_types::ServerCapabilities {
621 completion_provider: Some(lsp_types::CompletionOptions::default()),
622 signature_help_provider: Some(lsp_types::SignatureHelpOptions::default()),
623 ..Default::default()
624 };
625 let (translator, _server) = translator_with_capabilities_and_encoding(
626 &dir,
627 &ServerId::from_static("rust"),
628 caps,
629 lsp_types::PositionEncodingKind::UTF8,
630 );
631 let path = dir.path().join("main.rs");
632 fs::write(&path, "fn main() {}").unwrap();
633 let file = || client_path(path.to_string_lossy().into_owned());
634
635 let completions = translator.handle_completions(file(), pos(5, 3), None).await;
636 assert_matches!(completions, Err(Error::PositionBeyondDocument { .. }));
637 let signature = translator.handle_signature_help(file(), pos(5, 3)).await;
638 assert_matches!(signature, Err(Error::PositionBeyondDocument { .. }));
639 }
640
641 #[tokio::test]
642 async fn test_handle_signature_help_in_range_position_is_not_degraded() {
643 let wire = utf8_degradation("fn main() {}", 1, 4, "textDocument/signatureHelp").await;
644 assert!(wire.get("positions_degraded").is_none());
645 }
646
647 fn tuple_param(start: u32, end: u32) -> lsp_types::ParameterInformation {
648 lsp_types::ParameterInformation {
649 label: lsp_types::ParameterInformationLabel::Tuple((start, end)),
650 documentation: None,
651 }
652 }
653
654 fn one_param(
655 param: lsp_types::ParameterInformation,
656 label: &str,
657 encoding: PositionEncoding,
658 ) -> SignatureParameter {
659 signature_parameters(vec![param], label, encoding)
660 .pop()
661 .unwrap()
662 }
663
664 #[test]
667 fn test_signature_parameter_tuple_label_resolves_per_encoding() {
668 let label = "é𝄞(a: u8)";
669 for (encoding, start, end) in [
670 (PositionEncoding::Utf8, 7, 12),
671 (PositionEncoding::Utf16, 4, 9),
672 (PositionEncoding::Utf32, 3, 8),
673 ] {
674 let param = one_param(tuple_param(start, end), label, encoding);
675 assert_eq!(param.label.as_deref(), Some("a: u8"), "{encoding:?}");
676 }
677 }
678
679 #[test]
682 fn test_signature_parameter_invalid_tuple_offsets_yield_no_label() {
683 let label = "é𝄞(a: u8)";
684 for (encoding, start, end) in [
685 (PositionEncoding::Utf16, 0, 99),
686 (PositionEncoding::Utf16, 9, 4),
687 (PositionEncoding::Utf16, 2, 4),
688 (PositionEncoding::Utf8, 1, 3),
689 (PositionEncoding::Utf32, 3, 99),
690 ] {
691 let param = one_param(tuple_param(start, end), label, encoding);
692 assert_eq!(param.label, None, "{encoding:?} {start}..{end}");
693 }
694 }
695
696 #[test]
697 fn test_signature_parameter_string_label_passes_through() {
698 let param = one_param(
699 lsp_types::ParameterInformation {
700 label: lsp_types::ParameterInformationLabel::String("a: u8".to_string()),
701 documentation: None,
702 },
703 "f(a: u8)",
704 PositionEncoding::Utf16,
705 );
706 assert_eq!(param.label.as_deref(), Some("a: u8"));
707 }
708
709 #[test]
710 fn test_signature_parameter_unresolved_label_is_omitted_when_serialized() {
711 let param = one_param(tuple_param(5, 1), "abcdef", PositionEncoding::Utf16);
712 let wire = serde_json::to_value(param).unwrap();
713 assert!(wire.get("label").is_none());
714 }
715
716 #[test]
719 fn test_signature_parameters_whole_label_pairs_are_bounded() {
720 let label = "x".repeat(1000);
721 let params = vec![tuple_param(0, 1000); MAX_SIGNATURE_PARAMETERS * 4];
722 let out = signature_parameters(params, &label, PositionEncoding::Utf16);
723 assert_eq!(out.len(), MAX_SIGNATURE_PARAMETERS);
724 let resolved: usize = out
725 .iter()
726 .filter_map(|p| p.label.as_ref())
727 .map(String::len)
728 .sum();
729 assert!(
730 resolved <= label.len() * RESOLVED_LABEL_BUDGET_MULTIPLE,
731 "resolved {resolved} bytes"
732 );
733 assert_eq!(out[0].label.as_deref(), Some(label.as_str()));
734 assert!(out[RESOLVED_LABEL_BUDGET_MULTIPLE].label.is_none());
735 }
736
737 #[test]
739 fn test_signature_parameters_overlapping_ranges_resolve() {
740 let label = "f(a, b, ...rest)";
741 let params = vec![
742 tuple_param(2, 3),
743 tuple_param(2, 3),
744 tuple_param(2, 15),
745 tuple_param(8, 15),
746 ];
747 let out = signature_parameters(params, label, PositionEncoding::Utf16);
748 let labels: Vec<_> = out.iter().map(|p| p.label.as_deref()).collect();
749 assert_eq!(
750 labels,
751 [Some("a"), Some("a"), Some("a, b, ...rest"), Some("...rest")]
752 );
753 }
754
755 async fn indexing_signal_of<T, Fut>(
758 caps: lsp_types::ServerCapabilities,
759 method: &str,
760 quiescent: Option<bool>,
761 call: impl FnOnce(std::sync::Arc<Translator>, String) -> Fut + Send + 'static,
762 ) -> IndexingSignal
763 where
764 T: Send + 'static,
765 Fut: Future<Output = Result<Indexed<T>>> + Send + 'static,
766 {
767 use std::sync::Arc;
768
769 use tempfile::TempDir;
770 use tokio::io::BufReader;
771 use tokio::sync::Mutex;
772
773 use crate::bridge::NotificationCache;
774
775 let dir = TempDir::new().unwrap();
776 let server_id = ServerId::from_static("rust");
777 let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
778
779 let cache = Arc::new(Mutex::new(NotificationCache::new()));
780 if let Some(quiescent) = quiescent {
781 cache.lock().await.observe_indexing_signal(
782 &server_id,
783 "experimental/serverStatus",
784 Some(&serde_json::json!({"quiescent": quiescent})),
785 );
786 }
787 let translator = Arc::new(translator.with_notification_cache(cache));
788
789 let path = dir.path().join("main.rs");
790 fs::write(&path, "fn main() {}\n").unwrap();
791 let path = path.to_string_lossy().into_owned();
792 let handle = tokio::spawn(call(Arc::clone(&translator), path));
793
794 let mut wire = BufReader::new(&mut server.write_stdout);
795 let opened = read_framed_message(&mut wire).await;
796 assert_eq!(opened["method"], "textDocument/didOpen");
797 let request = read_framed_message(&mut wire).await;
798 assert_eq!(request["method"], method);
799 write_response(
800 &mut server.read_half_stdin,
801 &request["id"],
802 serde_json::Value::Null,
803 )
804 .await;
805
806 handle.await.unwrap().unwrap().indexing
807 }
808
809 #[tokio::test]
813 async fn test_ungated_tools_report_indexing_only_while_loading() {
814 use crate::bridge::translator::testing::{pos, span};
815
816 let loading = IndexingSignal {
817 indexing_in_progress: true,
818 };
819 for (quiescent, expected) in [
820 (Some(false), loading),
821 (Some(true), IndexingSignal::default()),
822 (None, IndexingSignal::default()),
823 ] {
824 let call_hierarchy = lsp_types::ServerCapabilities {
825 call_hierarchy_provider: Some(lsp_types::CallHierarchyProvider::Bool(true)),
826 ..Default::default()
827 };
828 assert_eq!(
829 indexing_signal_of(
830 call_hierarchy,
831 "textDocument/prepareCallHierarchy",
832 quiescent,
833 |t, path| async move {
834 t.handle_call_hierarchy_prepare(client_path(path), pos(1, 1))
835 .await
836 },
837 )
838 .await,
839 expected,
840 "prepare_call_hierarchy {quiescent:?}"
841 );
842
843 let type_hierarchy = lsp_types::ServerCapabilities {
844 type_hierarchy_provider: Some(lsp_types::TypeHierarchyProvider::Bool(true)),
845 ..Default::default()
846 };
847 assert_eq!(
848 indexing_signal_of(
849 type_hierarchy,
850 "textDocument/prepareTypeHierarchy",
851 quiescent,
852 |t, path| async move {
853 t.handle_type_hierarchy_prepare(client_path(path), pos(1, 1))
854 .await
855 },
856 )
857 .await,
858 expected,
859 "prepare_type_hierarchy {quiescent:?}"
860 );
861
862 let signature_help = lsp_types::ServerCapabilities {
863 signature_help_provider: Some(lsp_types::SignatureHelpOptions::default()),
864 ..Default::default()
865 };
866 assert_eq!(
867 indexing_signal_of(
868 signature_help,
869 "textDocument/signatureHelp",
870 quiescent,
871 |t, path| async move {
872 t.handle_signature_help(client_path(path), pos(1, 1)).await
873 },
874 )
875 .await,
876 expected,
877 "get_signature_help {quiescent:?}"
878 );
879
880 let inlay_hints = lsp_types::ServerCapabilities {
881 inlay_hint_provider: Some(lsp_types::InlayHintProvider::Bool(true)),
882 ..Default::default()
883 };
884 assert_eq!(
885 indexing_signal_of(
886 inlay_hints,
887 "textDocument/inlayHint",
888 quiescent,
889 |t, path| async move {
890 t.handle_inlay_hints(client_path(path), span(pos(1, 1), pos(1, 13)))
891 .await
892 },
893 )
894 .await,
895 expected,
896 "get_inlay_hints {quiescent:?}"
897 );
898 }
899 }
900}