1use std::{ops::Range, sync::Arc};
2
3use gpui::{App, AppContext, Entity, FontWeight, HighlightStyle, SharedString};
4use language::LanguageRegistry;
5use markdown::Markdown;
6use rpc::proto::{self, documentation};
7
8#[derive(Debug)]
9pub struct SignatureHelp {
10 pub active_signature: usize,
11 pub signatures: Vec<SignatureHelpData>,
12 pub(super) original_data: lsp::SignatureHelp,
13}
14
15#[derive(Debug, Clone)]
16pub struct SignatureHelpData {
17 pub label: SharedString,
18 pub documentation: Option<Entity<Markdown>>,
19 pub highlights: Vec<(Range<usize>, HighlightStyle)>,
20 pub active_parameter: Option<usize>,
21 pub parameters: Vec<ParameterInfo>,
22}
23
24#[derive(Debug, Clone)]
25pub struct ParameterInfo {
26 pub label_range: Option<Range<usize>>,
27 pub documentation: Option<Entity<Markdown>>,
28}
29
30impl SignatureHelp {
31 pub fn new(
32 help: lsp::SignatureHelp,
33 language_registry: Option<Arc<LanguageRegistry>>,
34 cx: &mut App,
35 ) -> Option<Self> {
36 if help.signatures.is_empty() {
37 return None;
38 }
39 let active_signature = help.active_signature.unwrap_or(0) as usize;
40 let mut signatures = Vec::<SignatureHelpData>::with_capacity(help.signatures.capacity());
41 for signature in &help.signatures {
42 let active_parameter = signature
43 .active_parameter
44 .unwrap_or_else(|| help.active_parameter.unwrap_or(0))
45 as usize;
46 let mut highlights = Vec::new();
47 let mut parameter_infos = Vec::new();
48
49 if let Some(parameters) = &signature.parameters {
50 for (index, parameter) in parameters.iter().enumerate() {
51 let label_range = match ¶meter.label {
52 lsp::ParameterLabel::LabelOffsets(parameter_label_offsets) => {
53 let range = *parameter_label_offsets.get(0)? as usize
54 ..*parameter_label_offsets.get(1)? as usize;
55 if index == active_parameter {
56 highlights.push((
57 range.clone(),
58 HighlightStyle {
59 font_weight: Some(FontWeight::EXTRA_BOLD),
60 ..HighlightStyle::default()
61 },
62 ));
63 }
64 Some(range)
65 }
66 lsp::ParameterLabel::Simple(parameter_label) => {
67 if let Some(start) = signature.label.find(parameter_label) {
68 let range = start..start + parameter_label.len();
69 if index == active_parameter {
70 highlights.push((
71 range.clone(),
72 HighlightStyle {
73 font_weight: Some(FontWeight::EXTRA_BOLD),
74 ..HighlightStyle::default()
75 },
76 ));
77 }
78 Some(range)
79 } else {
80 None
81 }
82 }
83 };
84
85 let documentation = parameter
86 .documentation
87 .as_ref()
88 .map(|doc| documentation_to_markdown(doc, language_registry.clone(), cx));
89
90 parameter_infos.push(ParameterInfo {
91 label_range,
92 documentation,
93 });
94 }
95 }
96
97 let label = SharedString::from(signature.label.clone());
98 let documentation = signature
99 .documentation
100 .as_ref()
101 .map(|doc| documentation_to_markdown(doc, language_registry.clone(), cx));
102
103 signatures.push(SignatureHelpData {
104 label,
105 documentation,
106 highlights,
107 active_parameter: Some(active_parameter),
108 parameters: parameter_infos,
109 });
110 }
111 Some(Self {
112 signatures,
113 active_signature,
114 original_data: help,
115 })
116 }
117}
118
119fn documentation_to_markdown(
120 documentation: &lsp::Documentation,
121 language_registry: Option<Arc<LanguageRegistry>>,
122 cx: &mut App,
123) -> Entity<Markdown> {
124 match documentation {
125 lsp::Documentation::String(string) => {
126 cx.new(|cx| Markdown::new_text(SharedString::from(string), cx))
127 }
128 lsp::Documentation::MarkupContent(markup) => match markup.kind {
129 lsp::MarkupKind::PlainText => {
130 cx.new(|cx| Markdown::new_text(SharedString::from(&markup.value), cx))
131 }
132 lsp::MarkupKind::Markdown => cx.new(|cx| {
133 Markdown::new(
134 SharedString::from(&markup.value),
135 language_registry,
136 None,
137 cx,
138 )
139 }),
140 },
141 }
142}
143
144pub fn lsp_to_proto_signature(lsp_help: lsp::SignatureHelp) -> proto::SignatureHelp {
145 proto::SignatureHelp {
146 signatures: lsp_help
147 .signatures
148 .into_iter()
149 .map(|signature| proto::SignatureInformation {
150 label: signature.label,
151 documentation: signature.documentation.map(lsp_to_proto_documentation),
152 parameters: signature
153 .parameters
154 .unwrap_or_default()
155 .into_iter()
156 .map(|parameter_info| proto::ParameterInformation {
157 label: Some(match parameter_info.label {
158 lsp::ParameterLabel::Simple(label) => {
159 proto::parameter_information::Label::Simple(label)
160 }
161 lsp::ParameterLabel::LabelOffsets(offsets) => {
162 proto::parameter_information::Label::LabelOffsets(
163 proto::LabelOffsets {
164 start: offsets[0],
165 end: offsets[1],
166 },
167 )
168 }
169 }),
170 documentation: parameter_info.documentation.map(lsp_to_proto_documentation),
171 })
172 .collect(),
173 active_parameter: signature.active_parameter,
174 })
175 .collect(),
176 active_signature: lsp_help.active_signature,
177 active_parameter: lsp_help.active_parameter,
178 }
179}
180
181fn lsp_to_proto_documentation(documentation: lsp::Documentation) -> proto::Documentation {
182 proto::Documentation {
183 content: Some(match documentation {
184 lsp::Documentation::String(string) => proto::documentation::Content::Value(string),
185 lsp::Documentation::MarkupContent(content) => {
186 proto::documentation::Content::MarkupContent(proto::MarkupContent {
187 is_markdown: matches!(content.kind, lsp::MarkupKind::Markdown),
188 value: content.value,
189 })
190 }
191 }),
192 }
193}
194
195pub fn proto_to_lsp_signature(proto_help: proto::SignatureHelp) -> lsp::SignatureHelp {
196 lsp::SignatureHelp {
197 signatures: proto_help
198 .signatures
199 .into_iter()
200 .map(|signature| lsp::SignatureInformation {
201 label: signature.label,
202 documentation: signature.documentation.and_then(proto_to_lsp_documentation),
203 parameters: Some(
204 signature
205 .parameters
206 .into_iter()
207 .filter_map(|parameter_info| {
208 Some(lsp::ParameterInformation {
209 label: match parameter_info.label? {
210 proto::parameter_information::Label::Simple(string) => {
211 lsp::ParameterLabel::Simple(string)
212 }
213 proto::parameter_information::Label::LabelOffsets(offsets) => {
214 lsp::ParameterLabel::LabelOffsets([
215 offsets.start,
216 offsets.end,
217 ])
218 }
219 },
220 documentation: parameter_info
221 .documentation
222 .and_then(proto_to_lsp_documentation),
223 })
224 })
225 .collect(),
226 ),
227 active_parameter: signature.active_parameter,
228 })
229 .collect(),
230 active_signature: proto_help.active_signature,
231 active_parameter: proto_help.active_parameter,
232 }
233}
234
235fn proto_to_lsp_documentation(documentation: proto::Documentation) -> Option<lsp::Documentation> {
236 {
237 Some(match documentation.content? {
238 documentation::Content::Value(string) => lsp::Documentation::String(string),
239 documentation::Content::MarkupContent(markup) => {
240 lsp::Documentation::MarkupContent(if markup.is_markdown {
241 lsp::MarkupContent {
242 kind: lsp::MarkupKind::Markdown,
243 value: markup.value,
244 }
245 } else {
246 lsp::MarkupContent {
247 kind: lsp::MarkupKind::PlainText,
248 value: markup.value,
249 }
250 })
251 }
252 })
253 }
254}
255
256#[cfg(test)]
257mod tests {
258 use gpui::{FontWeight, HighlightStyle, SharedString, TestAppContext};
259 use lsp::{Documentation, MarkupContent, MarkupKind};
260
261 use crate::lsp_command::signature_help::SignatureHelp;
262
263 fn current_parameter() -> HighlightStyle {
264 HighlightStyle {
265 font_weight: Some(FontWeight::EXTRA_BOLD),
266 ..Default::default()
267 }
268 }
269
270 #[gpui::test]
271 fn test_create_signature_help_markdown_string_1(cx: &mut TestAppContext) {
272 let signature_help = lsp::SignatureHelp {
273 signatures: vec![lsp::SignatureInformation {
274 label: "fn test(foo: u8, bar: &str)".to_string(),
275 documentation: Some(Documentation::String(
276 "This is a test documentation".to_string(),
277 )),
278 parameters: Some(vec![
279 lsp::ParameterInformation {
280 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
281 documentation: None,
282 },
283 lsp::ParameterInformation {
284 label: lsp::ParameterLabel::Simple("bar: &str".to_string()),
285 documentation: None,
286 },
287 ]),
288 active_parameter: None,
289 }],
290 active_signature: Some(0),
291 active_parameter: Some(0),
292 };
293 let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, cx));
294 assert!(maybe_markdown.is_some());
295
296 let markdown = maybe_markdown.unwrap();
297 let signature = markdown.signatures[markdown.active_signature].clone();
298 let markdown = (signature.label, signature.highlights);
299 assert_eq!(
300 markdown,
301 (
302 SharedString::new("fn test(foo: u8, bar: &str)"),
303 vec![(8..15, current_parameter())]
304 )
305 );
306 assert_eq!(
307 signature
308 .documentation
309 .unwrap()
310 .update(cx, |documentation, _| documentation.source().to_owned()),
311 "This is a test documentation",
312 )
313 }
314
315 #[gpui::test]
316 fn test_create_signature_help_markdown_string_2(cx: &mut TestAppContext) {
317 let signature_help = lsp::SignatureHelp {
318 signatures: vec![lsp::SignatureInformation {
319 label: "fn test(foo: u8, bar: &str)".to_string(),
320 documentation: Some(Documentation::MarkupContent(MarkupContent {
321 kind: MarkupKind::Markdown,
322 value: "This is a test documentation".to_string(),
323 })),
324 parameters: Some(vec![
325 lsp::ParameterInformation {
326 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
327 documentation: None,
328 },
329 lsp::ParameterInformation {
330 label: lsp::ParameterLabel::Simple("bar: &str".to_string()),
331 documentation: None,
332 },
333 ]),
334 active_parameter: None,
335 }],
336 active_signature: Some(0),
337 active_parameter: Some(1),
338 };
339 let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, cx));
340 assert!(maybe_markdown.is_some());
341
342 let markdown = maybe_markdown.unwrap();
343 let signature = markdown.signatures[markdown.active_signature].clone();
344 let markdown = (signature.label, signature.highlights);
345 assert_eq!(
346 markdown,
347 (
348 SharedString::new("fn test(foo: u8, bar: &str)"),
349 vec![(17..26, current_parameter())]
350 )
351 );
352 assert_eq!(
353 signature
354 .documentation
355 .unwrap()
356 .update(cx, |documentation, _| documentation.source().to_owned()),
357 "This is a test documentation",
358 )
359 }
360
361 #[gpui::test]
362 fn test_create_signature_help_markdown_string_3(cx: &mut TestAppContext) {
363 let signature_help = lsp::SignatureHelp {
364 signatures: vec![
365 lsp::SignatureInformation {
366 label: "fn test1(foo: u8, bar: &str)".to_string(),
367 documentation: None,
368 parameters: Some(vec![
369 lsp::ParameterInformation {
370 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
371 documentation: None,
372 },
373 lsp::ParameterInformation {
374 label: lsp::ParameterLabel::Simple("bar: &str".to_string()),
375 documentation: None,
376 },
377 ]),
378 active_parameter: None,
379 },
380 lsp::SignatureInformation {
381 label: "fn test2(hoge: String, fuga: bool)".to_string(),
382 documentation: None,
383 parameters: Some(vec![
384 lsp::ParameterInformation {
385 label: lsp::ParameterLabel::Simple("hoge: String".to_string()),
386 documentation: None,
387 },
388 lsp::ParameterInformation {
389 label: lsp::ParameterLabel::Simple("fuga: bool".to_string()),
390 documentation: None,
391 },
392 ]),
393 active_parameter: None,
394 },
395 ],
396 active_signature: Some(0),
397 active_parameter: Some(0),
398 };
399 let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, cx));
400 assert!(maybe_markdown.is_some());
401
402 let markdown = maybe_markdown.unwrap();
403 let signature = markdown.signatures[markdown.active_signature].clone();
404 let markdown = (signature.label, signature.highlights);
405 assert_eq!(
406 markdown,
407 (
408 SharedString::new("fn test1(foo: u8, bar: &str)"),
409 vec![(9..16, current_parameter())]
410 )
411 );
412 }
413
414 #[gpui::test]
415 fn test_create_signature_help_markdown_string_4(cx: &mut TestAppContext) {
416 let signature_help = lsp::SignatureHelp {
417 signatures: vec![
418 lsp::SignatureInformation {
419 label: "fn test1(foo: u8, bar: &str)".to_string(),
420 documentation: None,
421 parameters: Some(vec![
422 lsp::ParameterInformation {
423 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
424 documentation: None,
425 },
426 lsp::ParameterInformation {
427 label: lsp::ParameterLabel::Simple("bar: &str".to_string()),
428 documentation: None,
429 },
430 ]),
431 active_parameter: None,
432 },
433 lsp::SignatureInformation {
434 label: "fn test2(hoge: String, fuga: bool)".to_string(),
435 documentation: None,
436 parameters: Some(vec![
437 lsp::ParameterInformation {
438 label: lsp::ParameterLabel::Simple("hoge: String".to_string()),
439 documentation: None,
440 },
441 lsp::ParameterInformation {
442 label: lsp::ParameterLabel::Simple("fuga: bool".to_string()),
443 documentation: None,
444 },
445 ]),
446 active_parameter: None,
447 },
448 ],
449 active_signature: Some(1),
450 active_parameter: Some(0),
451 };
452 let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, cx));
453 assert!(maybe_markdown.is_some());
454
455 let markdown = maybe_markdown.unwrap();
456 let signature = markdown.signatures[markdown.active_signature].clone();
457 let markdown = (signature.label, signature.highlights);
458 assert_eq!(
459 markdown,
460 (
461 SharedString::new("fn test2(hoge: String, fuga: bool)"),
462 vec![(9..21, current_parameter())]
463 )
464 );
465 }
466
467 #[gpui::test]
468 fn test_create_signature_help_markdown_string_5(cx: &mut TestAppContext) {
469 let signature_help = lsp::SignatureHelp {
470 signatures: vec![
471 lsp::SignatureInformation {
472 label: "fn test1(foo: u8, bar: &str)".to_string(),
473 documentation: None,
474 parameters: Some(vec![
475 lsp::ParameterInformation {
476 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
477 documentation: None,
478 },
479 lsp::ParameterInformation {
480 label: lsp::ParameterLabel::Simple("bar: &str".to_string()),
481 documentation: None,
482 },
483 ]),
484 active_parameter: None,
485 },
486 lsp::SignatureInformation {
487 label: "fn test2(hoge: String, fuga: bool)".to_string(),
488 documentation: None,
489 parameters: Some(vec![
490 lsp::ParameterInformation {
491 label: lsp::ParameterLabel::Simple("hoge: String".to_string()),
492 documentation: None,
493 },
494 lsp::ParameterInformation {
495 label: lsp::ParameterLabel::Simple("fuga: bool".to_string()),
496 documentation: None,
497 },
498 ]),
499 active_parameter: None,
500 },
501 ],
502 active_signature: Some(1),
503 active_parameter: Some(1),
504 };
505 let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, cx));
506 assert!(maybe_markdown.is_some());
507
508 let markdown = maybe_markdown.unwrap();
509 let signature = markdown.signatures[markdown.active_signature].clone();
510 let markdown = (signature.label, signature.highlights);
511 assert_eq!(
512 markdown,
513 (
514 SharedString::new("fn test2(hoge: String, fuga: bool)"),
515 vec![(23..33, current_parameter())]
516 )
517 );
518 }
519
520 #[gpui::test]
521 fn test_create_signature_help_markdown_string_6(cx: &mut TestAppContext) {
522 let signature_help = lsp::SignatureHelp {
523 signatures: vec![
524 lsp::SignatureInformation {
525 label: "fn test1(foo: u8, bar: &str)".to_string(),
526 documentation: None,
527 parameters: Some(vec![
528 lsp::ParameterInformation {
529 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
530 documentation: None,
531 },
532 lsp::ParameterInformation {
533 label: lsp::ParameterLabel::Simple("bar: &str".to_string()),
534 documentation: None,
535 },
536 ]),
537 active_parameter: None,
538 },
539 lsp::SignatureInformation {
540 label: "fn test2(hoge: String, fuga: bool)".to_string(),
541 documentation: None,
542 parameters: Some(vec![
543 lsp::ParameterInformation {
544 label: lsp::ParameterLabel::Simple("hoge: String".to_string()),
545 documentation: None,
546 },
547 lsp::ParameterInformation {
548 label: lsp::ParameterLabel::Simple("fuga: bool".to_string()),
549 documentation: None,
550 },
551 ]),
552 active_parameter: None,
553 },
554 ],
555 active_signature: Some(1),
556 active_parameter: None,
557 };
558 let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, cx));
559 assert!(maybe_markdown.is_some());
560
561 let markdown = maybe_markdown.unwrap();
562 let signature = markdown.signatures[markdown.active_signature].clone();
563 let markdown = (signature.label, signature.highlights);
564 assert_eq!(
565 markdown,
566 (
567 SharedString::new("fn test2(hoge: String, fuga: bool)"),
568 vec![(9..21, current_parameter())]
569 )
570 );
571 }
572
573 #[gpui::test]
574 fn test_create_signature_help_markdown_string_7(cx: &mut TestAppContext) {
575 let signature_help = lsp::SignatureHelp {
576 signatures: vec![
577 lsp::SignatureInformation {
578 label: "fn test1(foo: u8, bar: &str)".to_string(),
579 documentation: None,
580 parameters: Some(vec![
581 lsp::ParameterInformation {
582 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
583 documentation: None,
584 },
585 lsp::ParameterInformation {
586 label: lsp::ParameterLabel::Simple("bar: &str".to_string()),
587 documentation: None,
588 },
589 ]),
590 active_parameter: None,
591 },
592 lsp::SignatureInformation {
593 label: "fn test2(hoge: String, fuga: bool)".to_string(),
594 documentation: None,
595 parameters: Some(vec![
596 lsp::ParameterInformation {
597 label: lsp::ParameterLabel::Simple("hoge: String".to_string()),
598 documentation: None,
599 },
600 lsp::ParameterInformation {
601 label: lsp::ParameterLabel::Simple("fuga: bool".to_string()),
602 documentation: None,
603 },
604 ]),
605 active_parameter: None,
606 },
607 lsp::SignatureInformation {
608 label: "fn test3(one: usize, two: u32)".to_string(),
609 documentation: None,
610 parameters: Some(vec![
611 lsp::ParameterInformation {
612 label: lsp::ParameterLabel::Simple("one: usize".to_string()),
613 documentation: None,
614 },
615 lsp::ParameterInformation {
616 label: lsp::ParameterLabel::Simple("two: u32".to_string()),
617 documentation: None,
618 },
619 ]),
620 active_parameter: None,
621 },
622 ],
623 active_signature: Some(2),
624 active_parameter: Some(1),
625 };
626 let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, cx));
627 assert!(maybe_markdown.is_some());
628
629 let markdown = maybe_markdown.unwrap();
630 let signature = markdown.signatures[markdown.active_signature].clone();
631 let markdown = (signature.label, signature.highlights);
632 assert_eq!(
633 markdown,
634 (
635 SharedString::new("fn test3(one: usize, two: u32)"),
636 vec![(21..29, current_parameter())]
637 )
638 );
639 }
640
641 #[gpui::test]
642 fn test_create_signature_help_markdown_string_8(cx: &mut TestAppContext) {
643 let signature_help = lsp::SignatureHelp {
644 signatures: vec![],
645 active_signature: None,
646 active_parameter: None,
647 };
648 let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, cx));
649 assert!(maybe_markdown.is_none());
650 }
651
652 #[gpui::test]
653 fn test_create_signature_help_markdown_string_9(cx: &mut TestAppContext) {
654 let signature_help = lsp::SignatureHelp {
655 signatures: vec![lsp::SignatureInformation {
656 label: "fn test(foo: u8, bar: &str)".to_string(),
657 documentation: None,
658 parameters: Some(vec![
659 lsp::ParameterInformation {
660 label: lsp::ParameterLabel::LabelOffsets([8, 15]),
661 documentation: None,
662 },
663 lsp::ParameterInformation {
664 label: lsp::ParameterLabel::LabelOffsets([17, 26]),
665 documentation: None,
666 },
667 ]),
668 active_parameter: None,
669 }],
670 active_signature: Some(0),
671 active_parameter: Some(0),
672 };
673 let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, cx));
674 assert!(maybe_markdown.is_some());
675
676 let markdown = maybe_markdown.unwrap();
677 let signature = markdown.signatures[markdown.active_signature].clone();
678 let markdown = (signature.label, signature.highlights);
679 assert_eq!(
680 markdown,
681 (
682 SharedString::new("fn test(foo: u8, bar: &str)"),
683 vec![(8..15, current_parameter())]
684 )
685 );
686 }
687
688 #[gpui::test]
689 fn test_parameter_documentation(cx: &mut TestAppContext) {
690 let signature_help = lsp::SignatureHelp {
691 signatures: vec![lsp::SignatureInformation {
692 label: "fn test(foo: u8, bar: &str)".to_string(),
693 documentation: Some(Documentation::String(
694 "This is a test documentation".to_string(),
695 )),
696 parameters: Some(vec![
697 lsp::ParameterInformation {
698 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
699 documentation: Some(Documentation::String("The foo parameter".to_string())),
700 },
701 lsp::ParameterInformation {
702 label: lsp::ParameterLabel::Simple("bar: &str".to_string()),
703 documentation: Some(Documentation::String("The bar parameter".to_string())),
704 },
705 ]),
706 active_parameter: None,
707 }],
708 active_signature: Some(0),
709 active_parameter: Some(0),
710 };
711 let maybe_signature_help = cx.update(|cx| SignatureHelp::new(signature_help, None, cx));
712 assert!(maybe_signature_help.is_some());
713
714 let signature_help = maybe_signature_help.unwrap();
715 let signature = &signature_help.signatures[signature_help.active_signature];
716
717 // Check that parameter documentation is extracted
718 assert_eq!(signature.parameters.len(), 2);
719 assert_eq!(
720 signature.parameters[0]
721 .documentation
722 .as_ref()
723 .unwrap()
724 .update(cx, |documentation, _| documentation.source().to_owned()),
725 "The foo parameter",
726 );
727 assert_eq!(
728 signature.parameters[1]
729 .documentation
730 .as_ref()
731 .unwrap()
732 .update(cx, |documentation, _| documentation.source().to_owned()),
733 "The bar parameter",
734 );
735
736 // Check that the active parameter is correct
737 assert_eq!(signature.active_parameter, Some(0));
738 }
739}