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, 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>(&file_path, position, ())
181 .await?;
182
183 let result = match response {
184 Some(sig_help) => SignatureHelpResult {
185 signatures: sig_help
186 .signatures
187 .into_iter()
188 .map(|sig| SignatureInfo {
189 parameters: signature_parameters(
190 sig.parameters.unwrap_or_default(),
191 &sig.label,
192 ctx.encoding,
193 ),
194 documentation: sig.documentation.map(extract_documentation),
195 label: sig.label,
196 })
197 .collect(),
198 active_signature: sig_help.active_signature,
199 active_parameter: sig_help.active_parameter.and_then(|ap| match ap {
200 lsp_types::ActiveParameter::Int(n) => Some(n),
201 lsp_types::ActiveParameter::Null => None,
202 }),
203 positions_degraded: ctx.positions_degraded(),
204 },
205 None => SignatureHelpResult {
206 signatures: vec![],
207 active_signature: None,
208 active_parameter: None,
209 positions_degraded: ctx.positions_degraded(),
210 },
211 };
212
213 Ok(Indexed::new(result, indexing))
214 }
215
216 pub async fn handle_inlay_hints(
226 &self,
227 file_path: ClientPath,
228 range: PositionRange,
229 ) -> Result<Indexed<InlayHintsResult>> {
230 let Positioned {
231 result:
232 Indexed {
233 result: response,
234 indexing,
235 },
236 ctx,
237 doc,
238 } = self
239 .disclosed_range_request::<lsp_types::InlayHintRequest>(&file_path, range, ())
240 .await?;
241 let uri = doc.uri();
242
243 let mut budget = ItemBudget::new();
244 let lsp_hints = budget.admit(response.unwrap_or_default());
245 let mut hints = Vec::with_capacity(lsp_hints.len());
246 for hint in lsp_hints {
247 let position = ctx.to_mcp(uri, hint.position).await;
248 let label = match hint.label {
249 lsp_types::Label::String(s) => s,
250 lsp_types::Label::InlayHintLabelPartList(parts) => {
251 parts.into_iter().map(|p| p.value).collect::<String>()
252 }
253 };
254 let tooltip = hint.tooltip.map(|t| match t {
255 lsp_types::Tooltip::String(s) => s,
256 lsp_types::Tooltip::MarkupContent(m) => m.value,
257 });
258 hints.push(InlayHintEntry {
259 position,
260 label,
261 kind: hint.kind.map(lsp_kind_to_u32),
262 padding_left: hint.padding_left,
263 padding_right: hint.padding_right,
264 tooltip,
265 });
266 }
267
268 Ok(Indexed::new(
269 InlayHintsResult {
270 hints,
271 truncated: budget.truncated(),
272 positions_degraded: ctx.positions_degraded(),
273 },
274 indexing,
275 ))
276 }
277}
278
279#[cfg(test)]
280mod tests {
281 use std::{assert_matches, fs};
282
283 use super::*;
284 use crate::bridge::IndexingSignal;
285 use crate::bridge::translator::testing::*;
286 use crate::config::ServerId;
287 use crate::error::Error;
288 use crate::test_lsp::client_path;
289
290 #[tokio::test(start_paused = true)]
294 async fn test_handle_completions_returns_workspace_indexing_error_when_loading() {
295 use std::sync::Arc;
296
297 use tempfile::TempDir;
298 use tokio::sync::Mutex;
299
300 use crate::bridge::NotificationCache;
301 use crate::config::ServerId;
302
303 let dir = TempDir::new().unwrap();
304 let server_id = ServerId::from_static("rust");
305 let caps = lsp_types::ServerCapabilities {
306 completion_provider: Some(lsp_types::CompletionOptions::default()),
307 ..Default::default()
308 };
309 let (translator, _server) = translator_with_capabilities(&dir, &server_id, caps);
310
311 let cache = Arc::new(Mutex::new(NotificationCache::new()));
312 cache.lock().await.observe_indexing_signal(
313 &server_id,
314 "experimental/serverStatus",
315 Some(&serde_json::json!({"quiescent": false})),
316 );
317 let translator = translator.with_notification_cache(cache);
318
319 let path = dir.path().join("main.rs");
320 fs::write(&path, "fn main() {}").unwrap();
321
322 let err = translator
323 .handle_completions(
324 client_path(path.to_string_lossy().into_owned()),
325 Position::at(1, 1),
326 None,
327 )
328 .await
329 .unwrap_err();
330
331 assert_matches!(
332 err,
333 Error::WorkspaceIndexing { server_id: id, .. } if id == server_id
334 );
335 }
336
337 #[tokio::test]
340 async fn test_handle_completions_dispatches_when_indexing_ready() {
341 use std::sync::Arc;
342
343 use tempfile::TempDir;
344 use tokio::io::BufReader;
345 use tokio::sync::Mutex;
346
347 use crate::bridge::NotificationCache;
348 use crate::config::ServerId;
349
350 let dir = TempDir::new().unwrap();
351 let server_id = ServerId::from_static("rust");
352 let caps = lsp_types::ServerCapabilities {
353 completion_provider: Some(lsp_types::CompletionOptions::default()),
354 ..Default::default()
355 };
356 let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
357
358 let cache = Arc::new(Mutex::new(NotificationCache::new()));
359 cache.lock().await.observe_indexing_signal(
360 &server_id,
361 "experimental/serverStatus",
362 Some(&serde_json::json!({"quiescent": true})),
363 );
364 let translator = Arc::new(translator.with_notification_cache(cache));
365
366 let path = dir.path().join("main.rs");
367 fs::write(&path, "fn main() {}").unwrap();
368
369 let handle = {
370 let translator = Arc::clone(&translator);
371 let path = path.to_string_lossy().to_string();
372 tokio::spawn(async move {
373 translator
374 .handle_completions(client_path(path), Position::at(1, 1), None)
375 .await
376 })
377 };
378
379 let mut wire = BufReader::new(&mut server.write_stdout);
380 let opened = read_framed_message(&mut wire).await;
381 assert_eq!(opened["method"], "textDocument/didOpen");
382 let request = read_framed_message(&mut wire).await;
383 assert_eq!(request["method"], "textDocument/completion");
384
385 write_response(
386 &mut server.read_half_stdin,
387 &request["id"],
388 serde_json::json!([]),
389 )
390 .await;
391
392 let result = handle.await.unwrap().unwrap();
393 assert!(result.items.is_empty());
394 }
395
396 #[tokio::test]
401 async fn test_handle_inlay_hints_preserves_custom_kind_above_u8_range() {
402 use std::sync::Arc;
403
404 use tempfile::TempDir;
405 use tokio::io::BufReader;
406
407 use crate::bridge::translator::testing::pos;
408 use crate::config::ServerId;
409
410 let dir = TempDir::new().unwrap();
411 let server_id = ServerId::from_static("rust");
412 let caps = lsp_types::ServerCapabilities {
413 inlay_hint_provider: Some(lsp_types::InlayHintProvider::Bool(true)),
414 ..Default::default()
415 };
416 let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
417
418 let path = dir.path().join("main.rs");
419 fs::write(&path, "fn main() {}\n").unwrap();
420
421 let translator = Arc::new(translator);
422 let handle = {
423 let translator = Arc::clone(&translator);
424 let path = path.to_string_lossy().to_string();
425 tokio::spawn(async move {
426 translator
427 .handle_inlay_hints(client_path(path), span(pos(1, 1), pos(1, 13)))
428 .await
429 })
430 };
431
432 let mut wire = BufReader::new(&mut server.write_stdout);
433 let opened = read_framed_message(&mut wire).await;
434 assert_eq!(opened["method"], "textDocument/didOpen");
435 let request = read_framed_message(&mut wire).await;
436 assert_eq!(request["method"], "textDocument/inlayHint");
437
438 write_response(
439 &mut server.read_half_stdin,
440 &request["id"],
441 serde_json::json!([{
442 "position": {"line": 0, "character": 5},
443 "label": "custom",
444 "kind": 300,
445 }]),
446 )
447 .await;
448
449 let result = handle.await.unwrap().unwrap().result;
450 assert_eq!(result.hints.len(), 1);
451 assert_eq!(
452 result.hints[0].kind,
453 Some(300u32),
454 "a custom InlayHintKind above u8::MAX must not be truncated or dropped"
455 );
456 }
457
458 async fn inlay_hints_with_response(count: usize) -> InlayHintsResult {
459 use std::sync::Arc;
460
461 use tempfile::TempDir;
462 use tokio::io::BufReader;
463
464 use crate::bridge::translator::testing::pos;
465 use crate::config::ServerId;
466
467 let dir = TempDir::new().unwrap();
468 let server_id = ServerId::from_static("rust");
469 let caps = lsp_types::ServerCapabilities {
470 inlay_hint_provider: Some(lsp_types::InlayHintProvider::Bool(true)),
471 ..Default::default()
472 };
473 let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
474
475 let path = dir.path().join("main.rs");
476 fs::write(&path, "fn main() {}\n").unwrap();
477
478 let translator = Arc::new(translator);
479 let handle = {
480 let translator = Arc::clone(&translator);
481 let path = path.to_string_lossy().to_string();
482 tokio::spawn(async move {
483 translator
484 .handle_inlay_hints(client_path(path), span(pos(1, 1), pos(1, 13)))
485 .await
486 })
487 };
488
489 let mut wire = BufReader::new(&mut server.write_stdout);
490 let opened = read_framed_message(&mut wire).await;
491 assert_eq!(opened["method"], "textDocument/didOpen");
492 let request = read_framed_message(&mut wire).await;
493 assert_eq!(request["method"], "textDocument/inlayHint");
494
495 let hints: Vec<_> = (0..count)
496 .map(|_| serde_json::json!({"position": {"line": 0, "character": 5}, "label": "h"}))
497 .collect();
498 write_response(
499 &mut server.read_half_stdin,
500 &request["id"],
501 serde_json::Value::Array(hints),
502 )
503 .await;
504
505 handle.await.unwrap().unwrap().result
506 }
507
508 #[tokio::test]
511 async fn test_handle_inlay_hints_caps_item_count_and_reports_truncation() {
512 use crate::bridge::translator::navigation::MAX_NORMALIZED_LOCATIONS;
513
514 let result = inlay_hints_with_response(MAX_NORMALIZED_LOCATIONS + 500).await;
515 assert_eq!(result.hints.len(), MAX_NORMALIZED_LOCATIONS);
516 assert!(result.truncated);
517 }
518
519 #[tokio::test]
521 async fn test_handle_inlay_hints_at_cap_is_not_truncated() {
522 use crate::bridge::translator::navigation::MAX_NORMALIZED_LOCATIONS;
523
524 let result = inlay_hints_with_response(MAX_NORMALIZED_LOCATIONS).await;
525 assert_eq!(result.hints.len(), MAX_NORMALIZED_LOCATIONS);
526 assert!(!result.truncated);
527 }
528
529 async fn utf8_degradation(
530 content: &str,
531 line: u32,
532 character: u32,
533 method: &str,
534 ) -> serde_json::Value {
535 use std::sync::Arc;
536
537 use tempfile::TempDir;
538 use tokio::io::BufReader;
539
540 use crate::config::ServerId;
541
542 let dir = TempDir::new().unwrap();
543 let server_id = ServerId::from_static("rust");
544 let caps = lsp_types::ServerCapabilities {
545 completion_provider: Some(lsp_types::CompletionOptions::default()),
546 signature_help_provider: Some(lsp_types::SignatureHelpOptions::default()),
547 ..Default::default()
548 };
549 let (translator, mut server) = translator_with_capabilities_and_encoding(
550 &dir,
551 &server_id,
552 caps,
553 lsp_types::PositionEncodingKind::UTF8,
554 );
555 let path = dir.path().join("main.rs");
556 fs::write(&path, content).unwrap();
557
558 let translator = Arc::new(translator);
559 let handle = {
560 let translator = Arc::clone(&translator);
561 let path = path.to_string_lossy().to_string();
562 let method = method.to_string();
563 tokio::spawn(async move {
564 if method == "textDocument/completion" {
565 translator
566 .handle_completions(client_path(path), pos(line, character), None)
567 .await
568 .map(|r| serde_json::to_value(r).unwrap())
569 } else {
570 translator
571 .handle_signature_help(client_path(path), pos(line, character))
572 .await
573 .map(|r| serde_json::to_value(r).unwrap())
574 }
575 })
576 };
577
578 let mut wire = BufReader::new(&mut server.write_stdout);
579 let opened = read_framed_message(&mut wire).await;
580 assert_eq!(opened["method"], "textDocument/didOpen");
581 let request = read_framed_message(&mut wire).await;
582 assert_eq!(request["method"], method);
583 let response = if method == "textDocument/completion" {
584 serde_json::json!([])
585 } else {
586 serde_json::Value::Null
587 };
588 write_response(&mut server.read_half_stdin, &request["id"], response).await;
589
590 handle.await.unwrap().unwrap()
591 }
592
593 #[tokio::test]
596 async fn test_handle_completions_trailing_empty_line_is_not_degraded() {
597 let wire = utf8_degradation("fn main() {}\n", 2, 1, "textDocument/completion").await;
598 assert!(wire.get("positions_degraded").is_none());
599 }
600
601 #[tokio::test]
604 async fn test_handle_completions_and_signature_help_line_past_eof_are_rejected() {
605 use tempfile::TempDir;
606
607 use crate::config::ServerId;
608
609 let dir = TempDir::new().unwrap();
610 let caps = lsp_types::ServerCapabilities {
611 completion_provider: Some(lsp_types::CompletionOptions::default()),
612 signature_help_provider: Some(lsp_types::SignatureHelpOptions::default()),
613 ..Default::default()
614 };
615 let (translator, _server) = translator_with_capabilities_and_encoding(
616 &dir,
617 &ServerId::from_static("rust"),
618 caps,
619 lsp_types::PositionEncodingKind::UTF8,
620 );
621 let path = dir.path().join("main.rs");
622 fs::write(&path, "fn main() {}").unwrap();
623 let file = || client_path(path.to_string_lossy().into_owned());
624
625 let completions = translator.handle_completions(file(), pos(5, 3), None).await;
626 assert_matches!(completions, Err(Error::PositionBeyondDocument { .. }));
627 let signature = translator.handle_signature_help(file(), pos(5, 3)).await;
628 assert_matches!(signature, Err(Error::PositionBeyondDocument { .. }));
629 }
630
631 #[tokio::test]
632 async fn test_handle_signature_help_in_range_position_is_not_degraded() {
633 let wire = utf8_degradation("fn main() {}", 1, 4, "textDocument/signatureHelp").await;
634 assert!(wire.get("positions_degraded").is_none());
635 }
636
637 fn tuple_param(start: u32, end: u32) -> lsp_types::ParameterInformation {
638 lsp_types::ParameterInformation {
639 label: lsp_types::ParameterInformationLabel::Tuple((start, end)),
640 documentation: None,
641 }
642 }
643
644 fn one_param(
645 param: lsp_types::ParameterInformation,
646 label: &str,
647 encoding: PositionEncoding,
648 ) -> SignatureParameter {
649 signature_parameters(vec![param], label, encoding)
650 .pop()
651 .unwrap()
652 }
653
654 #[test]
657 fn test_signature_parameter_tuple_label_resolves_per_encoding() {
658 let label = "é𝄞(a: u8)";
659 for (encoding, start, end) in [
660 (PositionEncoding::Utf8, 7, 12),
661 (PositionEncoding::Utf16, 4, 9),
662 (PositionEncoding::Utf32, 3, 8),
663 ] {
664 let param = one_param(tuple_param(start, end), label, encoding);
665 assert_eq!(param.label.as_deref(), Some("a: u8"), "{encoding:?}");
666 }
667 }
668
669 #[test]
672 fn test_signature_parameter_invalid_tuple_offsets_yield_no_label() {
673 let label = "é𝄞(a: u8)";
674 for (encoding, start, end) in [
675 (PositionEncoding::Utf16, 0, 99),
676 (PositionEncoding::Utf16, 9, 4),
677 (PositionEncoding::Utf16, 2, 4),
678 (PositionEncoding::Utf8, 1, 3),
679 (PositionEncoding::Utf32, 3, 99),
680 ] {
681 let param = one_param(tuple_param(start, end), label, encoding);
682 assert_eq!(param.label, None, "{encoding:?} {start}..{end}");
683 }
684 }
685
686 #[test]
687 fn test_signature_parameter_string_label_passes_through() {
688 let param = one_param(
689 lsp_types::ParameterInformation {
690 label: lsp_types::ParameterInformationLabel::String("a: u8".to_string()),
691 documentation: None,
692 },
693 "f(a: u8)",
694 PositionEncoding::Utf16,
695 );
696 assert_eq!(param.label.as_deref(), Some("a: u8"));
697 }
698
699 #[test]
700 fn test_signature_parameter_unresolved_label_is_omitted_when_serialized() {
701 let param = one_param(tuple_param(5, 1), "abcdef", PositionEncoding::Utf16);
702 let wire = serde_json::to_value(param).unwrap();
703 assert!(wire.get("label").is_none());
704 }
705
706 #[test]
709 fn test_signature_parameters_whole_label_pairs_are_bounded() {
710 let label = "x".repeat(1000);
711 let params = vec![tuple_param(0, 1000); MAX_SIGNATURE_PARAMETERS * 4];
712 let out = signature_parameters(params, &label, PositionEncoding::Utf16);
713 assert_eq!(out.len(), MAX_SIGNATURE_PARAMETERS);
714 let resolved: usize = out
715 .iter()
716 .filter_map(|p| p.label.as_ref())
717 .map(String::len)
718 .sum();
719 assert!(
720 resolved <= label.len() * RESOLVED_LABEL_BUDGET_MULTIPLE,
721 "resolved {resolved} bytes"
722 );
723 assert_eq!(out[0].label.as_deref(), Some(label.as_str()));
724 assert!(out[RESOLVED_LABEL_BUDGET_MULTIPLE].label.is_none());
725 }
726
727 #[test]
729 fn test_signature_parameters_overlapping_ranges_resolve() {
730 let label = "f(a, b, ...rest)";
731 let params = vec![
732 tuple_param(2, 3),
733 tuple_param(2, 3),
734 tuple_param(2, 15),
735 tuple_param(8, 15),
736 ];
737 let out = signature_parameters(params, label, PositionEncoding::Utf16);
738 let labels: Vec<_> = out.iter().map(|p| p.label.as_deref()).collect();
739 assert_eq!(
740 labels,
741 [Some("a"), Some("a"), Some("a, b, ...rest"), Some("...rest")]
742 );
743 }
744
745 async fn indexing_signal_of<T, Fut>(
748 caps: lsp_types::ServerCapabilities,
749 method: &str,
750 quiescent: Option<bool>,
751 call: impl FnOnce(std::sync::Arc<Translator>, String) -> Fut + Send + 'static,
752 ) -> IndexingSignal
753 where
754 T: Send + 'static,
755 Fut: Future<Output = Result<Indexed<T>>> + Send + 'static,
756 {
757 use std::sync::Arc;
758
759 use tempfile::TempDir;
760 use tokio::io::BufReader;
761 use tokio::sync::Mutex;
762
763 use crate::bridge::NotificationCache;
764
765 let dir = TempDir::new().unwrap();
766 let server_id = ServerId::from_static("rust");
767 let (translator, mut server) = translator_with_capabilities(&dir, &server_id, caps);
768
769 let cache = Arc::new(Mutex::new(NotificationCache::new()));
770 if let Some(quiescent) = quiescent {
771 cache.lock().await.observe_indexing_signal(
772 &server_id,
773 "experimental/serverStatus",
774 Some(&serde_json::json!({"quiescent": quiescent})),
775 );
776 }
777 let translator = Arc::new(translator.with_notification_cache(cache));
778
779 let path = dir.path().join("main.rs");
780 fs::write(&path, "fn main() {}\n").unwrap();
781 let path = path.to_string_lossy().into_owned();
782 let handle = tokio::spawn(call(Arc::clone(&translator), path));
783
784 let mut wire = BufReader::new(&mut server.write_stdout);
785 let opened = read_framed_message(&mut wire).await;
786 assert_eq!(opened["method"], "textDocument/didOpen");
787 let request = read_framed_message(&mut wire).await;
788 assert_eq!(request["method"], method);
789 write_response(
790 &mut server.read_half_stdin,
791 &request["id"],
792 serde_json::Value::Null,
793 )
794 .await;
795
796 handle.await.unwrap().unwrap().indexing
797 }
798
799 #[tokio::test]
803 async fn test_ungated_tools_report_indexing_only_while_loading() {
804 use crate::bridge::translator::testing::{pos, span};
805
806 let loading = IndexingSignal {
807 indexing_in_progress: true,
808 };
809 for (quiescent, expected) in [
810 (Some(false), loading),
811 (Some(true), IndexingSignal::default()),
812 (None, IndexingSignal::default()),
813 ] {
814 let call_hierarchy = lsp_types::ServerCapabilities {
815 call_hierarchy_provider: Some(lsp_types::CallHierarchyProvider::Bool(true)),
816 ..Default::default()
817 };
818 assert_eq!(
819 indexing_signal_of(
820 call_hierarchy,
821 "textDocument/prepareCallHierarchy",
822 quiescent,
823 |t, path| async move {
824 t.handle_call_hierarchy_prepare(client_path(path), pos(1, 1))
825 .await
826 },
827 )
828 .await,
829 expected,
830 "prepare_call_hierarchy {quiescent:?}"
831 );
832
833 let type_hierarchy = lsp_types::ServerCapabilities {
834 type_hierarchy_provider: Some(lsp_types::TypeHierarchyProvider::Bool(true)),
835 ..Default::default()
836 };
837 assert_eq!(
838 indexing_signal_of(
839 type_hierarchy,
840 "textDocument/prepareTypeHierarchy",
841 quiescent,
842 |t, path| async move {
843 t.handle_type_hierarchy_prepare(client_path(path), pos(1, 1))
844 .await
845 },
846 )
847 .await,
848 expected,
849 "prepare_type_hierarchy {quiescent:?}"
850 );
851
852 let signature_help = lsp_types::ServerCapabilities {
853 signature_help_provider: Some(lsp_types::SignatureHelpOptions::default()),
854 ..Default::default()
855 };
856 assert_eq!(
857 indexing_signal_of(
858 signature_help,
859 "textDocument/signatureHelp",
860 quiescent,
861 |t, path| async move {
862 t.handle_signature_help(client_path(path), pos(1, 1)).await
863 },
864 )
865 .await,
866 expected,
867 "get_signature_help {quiescent:?}"
868 );
869
870 let inlay_hints = lsp_types::ServerCapabilities {
871 inlay_hint_provider: Some(lsp_types::InlayHintProvider::Bool(true)),
872 ..Default::default()
873 };
874 assert_eq!(
875 indexing_signal_of(
876 inlay_hints,
877 "textDocument/inlayHint",
878 quiescent,
879 |t, path| async move {
880 t.handle_inlay_hints(client_path(path), span(pos(1, 1), pos(1, 13)))
881 .await
882 },
883 )
884 .await,
885 expected,
886 "get_inlay_hints {quiescent:?}"
887 );
888 }
889 }
890}