signature_help.rs

  1use std::{ops::Range, sync::Arc};
  2
  3use gpui::FontWeight;
  4use language::{
  5    markdown::{MarkdownHighlight, MarkdownHighlightStyle},
  6    Language,
  7};
  8use rpc::proto::{self, documentation};
  9
 10pub const SIGNATURE_HELP_HIGHLIGHT_CURRENT: MarkdownHighlight =
 11    MarkdownHighlight::Style(MarkdownHighlightStyle {
 12        italic: false,
 13        underline: false,
 14        strikethrough: false,
 15        weight: FontWeight::EXTRA_BOLD,
 16    });
 17
 18pub const SIGNATURE_HELP_HIGHLIGHT_OVERLOAD: MarkdownHighlight =
 19    MarkdownHighlight::Style(MarkdownHighlightStyle {
 20        italic: true,
 21        underline: false,
 22        strikethrough: false,
 23        weight: FontWeight::NORMAL,
 24    });
 25
 26#[derive(Debug)]
 27pub struct SignatureHelp {
 28    pub markdown: String,
 29    pub highlights: Vec<(Range<usize>, MarkdownHighlight)>,
 30    pub(super) original_data: lsp::SignatureHelp,
 31}
 32
 33impl SignatureHelp {
 34    pub fn new(help: lsp::SignatureHelp, language: Option<Arc<Language>>) -> Option<Self> {
 35        let function_options_count = help.signatures.len();
 36
 37        let signature_information = help
 38            .active_signature
 39            .and_then(|active_signature| help.signatures.get(active_signature as usize))
 40            .or_else(|| help.signatures.first())?;
 41
 42        let str_for_join = ", ";
 43        let parameter_length = signature_information
 44            .parameters
 45            .as_ref()
 46            .map_or(0, |parameters| parameters.len());
 47        let mut highlight_start = 0;
 48        let (markdown, mut highlights): (Vec<_>, Vec<_>) = signature_information
 49            .parameters
 50            .as_ref()?
 51            .iter()
 52            .enumerate()
 53            .map(|(i, parameter_information)| {
 54                let label = match parameter_information.label.clone() {
 55                    lsp::ParameterLabel::Simple(string) => string,
 56                    lsp::ParameterLabel::LabelOffsets(offset) => signature_information
 57                        .label
 58                        .chars()
 59                        .skip(offset[0] as usize)
 60                        .take((offset[1] - offset[0]) as usize)
 61                        .collect::<String>(),
 62                };
 63                let label_length = label.len();
 64
 65                let highlights = help.active_parameter.and_then(|active_parameter| {
 66                    if i == active_parameter as usize {
 67                        Some((
 68                            highlight_start..(highlight_start + label_length),
 69                            SIGNATURE_HELP_HIGHLIGHT_CURRENT,
 70                        ))
 71                    } else {
 72                        None
 73                    }
 74                });
 75
 76                if i != parameter_length {
 77                    highlight_start += label_length + str_for_join.len();
 78                }
 79
 80                (label, highlights)
 81            })
 82            .unzip();
 83
 84        if markdown.is_empty() {
 85            None
 86        } else {
 87            let markdown = markdown.join(str_for_join);
 88            let language_name = language
 89                .map(|n| n.name().0.to_lowercase())
 90                .unwrap_or_default();
 91
 92            let markdown = if function_options_count >= 2 {
 93                let suffix = format!("(+{} overload)", function_options_count - 1);
 94                let highlight_start = markdown.len() + 1;
 95                highlights.push(Some((
 96                    highlight_start..(highlight_start + suffix.len()),
 97                    SIGNATURE_HELP_HIGHLIGHT_OVERLOAD,
 98                )));
 99                format!("```{language_name}\n{markdown} {suffix}")
100            } else {
101                format!("```{language_name}\n{markdown}")
102            };
103
104            Some(Self {
105                markdown,
106                highlights: highlights.into_iter().flatten().collect(),
107                original_data: help,
108            })
109        }
110    }
111}
112
113pub fn lsp_to_proto_signature(lsp_help: lsp::SignatureHelp) -> proto::SignatureHelp {
114    proto::SignatureHelp {
115        signatures: lsp_help
116            .signatures
117            .into_iter()
118            .map(|signature| proto::SignatureInformation {
119                label: signature.label,
120                documentation: signature.documentation.map(lsp_to_proto_documentation),
121                parameters: signature
122                    .parameters
123                    .unwrap_or_default()
124                    .into_iter()
125                    .map(|parameter_info| proto::ParameterInformation {
126                        label: Some(match parameter_info.label {
127                            lsp::ParameterLabel::Simple(label) => {
128                                proto::parameter_information::Label::Simple(label)
129                            }
130                            lsp::ParameterLabel::LabelOffsets(offsets) => {
131                                proto::parameter_information::Label::LabelOffsets(
132                                    proto::LabelOffsets {
133                                        start: offsets[0],
134                                        end: offsets[1],
135                                    },
136                                )
137                            }
138                        }),
139                        documentation: parameter_info.documentation.map(lsp_to_proto_documentation),
140                    })
141                    .collect(),
142                active_parameter: signature.active_parameter,
143            })
144            .collect(),
145        active_signature: lsp_help.active_signature,
146        active_parameter: lsp_help.active_parameter,
147    }
148}
149
150fn lsp_to_proto_documentation(documentation: lsp::Documentation) -> proto::Documentation {
151    proto::Documentation {
152        content: Some(match documentation {
153            lsp::Documentation::String(string) => proto::documentation::Content::Value(string),
154            lsp::Documentation::MarkupContent(content) => {
155                proto::documentation::Content::MarkupContent(proto::MarkupContent {
156                    is_markdown: matches!(content.kind, lsp::MarkupKind::Markdown),
157                    value: content.value,
158                })
159            }
160        }),
161    }
162}
163
164pub fn proto_to_lsp_signature(proto_help: proto::SignatureHelp) -> lsp::SignatureHelp {
165    lsp::SignatureHelp {
166        signatures: proto_help
167            .signatures
168            .into_iter()
169            .map(|signature| lsp::SignatureInformation {
170                label: signature.label,
171                documentation: signature.documentation.and_then(proto_to_lsp_documentation),
172                parameters: Some(
173                    signature
174                        .parameters
175                        .into_iter()
176                        .filter_map(|parameter_info| {
177                            Some(lsp::ParameterInformation {
178                                label: match parameter_info.label? {
179                                    proto::parameter_information::Label::Simple(string) => {
180                                        lsp::ParameterLabel::Simple(string)
181                                    }
182                                    proto::parameter_information::Label::LabelOffsets(offsets) => {
183                                        lsp::ParameterLabel::LabelOffsets([
184                                            offsets.start,
185                                            offsets.end,
186                                        ])
187                                    }
188                                },
189                                documentation: parameter_info
190                                    .documentation
191                                    .and_then(proto_to_lsp_documentation),
192                            })
193                        })
194                        .collect(),
195                ),
196                active_parameter: signature.active_parameter,
197            })
198            .collect(),
199        active_signature: proto_help.active_signature,
200        active_parameter: proto_help.active_parameter,
201    }
202}
203
204fn proto_to_lsp_documentation(documentation: proto::Documentation) -> Option<lsp::Documentation> {
205    {
206        Some(match documentation.content? {
207            documentation::Content::Value(string) => lsp::Documentation::String(string),
208            documentation::Content::MarkupContent(markup) => {
209                lsp::Documentation::MarkupContent(if markup.is_markdown {
210                    lsp::MarkupContent {
211                        kind: lsp::MarkupKind::Markdown,
212                        value: markup.value,
213                    }
214                } else {
215                    lsp::MarkupContent {
216                        kind: lsp::MarkupKind::PlainText,
217                        value: markup.value,
218                    }
219                })
220            }
221        })
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use crate::lsp_command::signature_help::{
228        SignatureHelp, SIGNATURE_HELP_HIGHLIGHT_CURRENT, SIGNATURE_HELP_HIGHLIGHT_OVERLOAD,
229    };
230
231    #[test]
232    fn test_create_signature_help_markdown_string_1() {
233        let signature_help = lsp::SignatureHelp {
234            signatures: vec![lsp::SignatureInformation {
235                label: "fn test(foo: u8, bar: &str)".to_string(),
236                documentation: None,
237                parameters: Some(vec![
238                    lsp::ParameterInformation {
239                        label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
240                        documentation: None,
241                    },
242                    lsp::ParameterInformation {
243                        label: lsp::ParameterLabel::Simple("bar: &str".to_string()),
244                        documentation: None,
245                    },
246                ]),
247                active_parameter: None,
248            }],
249            active_signature: Some(0),
250            active_parameter: Some(0),
251        };
252        let maybe_markdown = SignatureHelp::new(signature_help, None);
253        assert!(maybe_markdown.is_some());
254
255        let markdown = maybe_markdown.unwrap();
256        let markdown = (markdown.markdown, markdown.highlights);
257        assert_eq!(
258            markdown,
259            (
260                "```\nfoo: u8, bar: &str".to_string(),
261                vec![(0..7, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]
262            )
263        );
264    }
265
266    #[test]
267    fn test_create_signature_help_markdown_string_2() {
268        let signature_help = lsp::SignatureHelp {
269            signatures: vec![lsp::SignatureInformation {
270                label: "fn test(foo: u8, bar: &str)".to_string(),
271                documentation: None,
272                parameters: Some(vec![
273                    lsp::ParameterInformation {
274                        label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
275                        documentation: None,
276                    },
277                    lsp::ParameterInformation {
278                        label: lsp::ParameterLabel::Simple("bar: &str".to_string()),
279                        documentation: None,
280                    },
281                ]),
282                active_parameter: None,
283            }],
284            active_signature: Some(0),
285            active_parameter: Some(1),
286        };
287        let maybe_markdown = SignatureHelp::new(signature_help, None);
288        assert!(maybe_markdown.is_some());
289
290        let markdown = maybe_markdown.unwrap();
291        let markdown = (markdown.markdown, markdown.highlights);
292        assert_eq!(
293            markdown,
294            (
295                "```\nfoo: u8, bar: &str".to_string(),
296                vec![(9..18, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]
297            )
298        );
299    }
300
301    #[test]
302    fn test_create_signature_help_markdown_string_3() {
303        let signature_help = lsp::SignatureHelp {
304            signatures: vec![
305                lsp::SignatureInformation {
306                    label: "fn test1(foo: u8, bar: &str)".to_string(),
307                    documentation: None,
308                    parameters: Some(vec![
309                        lsp::ParameterInformation {
310                            label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
311                            documentation: None,
312                        },
313                        lsp::ParameterInformation {
314                            label: lsp::ParameterLabel::Simple("bar: &str".to_string()),
315                            documentation: None,
316                        },
317                    ]),
318                    active_parameter: None,
319                },
320                lsp::SignatureInformation {
321                    label: "fn test2(hoge: String, fuga: bool)".to_string(),
322                    documentation: None,
323                    parameters: Some(vec![
324                        lsp::ParameterInformation {
325                            label: lsp::ParameterLabel::Simple("hoge: String".to_string()),
326                            documentation: None,
327                        },
328                        lsp::ParameterInformation {
329                            label: lsp::ParameterLabel::Simple("fuga: bool".to_string()),
330                            documentation: None,
331                        },
332                    ]),
333                    active_parameter: None,
334                },
335            ],
336            active_signature: Some(0),
337            active_parameter: Some(0),
338        };
339        let maybe_markdown = SignatureHelp::new(signature_help, None);
340        assert!(maybe_markdown.is_some());
341
342        let markdown = maybe_markdown.unwrap();
343        let markdown = (markdown.markdown, markdown.highlights);
344        assert_eq!(
345            markdown,
346            (
347                "```\nfoo: u8, bar: &str (+1 overload)".to_string(),
348                vec![
349                    (0..7, SIGNATURE_HELP_HIGHLIGHT_CURRENT),
350                    (19..32, SIGNATURE_HELP_HIGHLIGHT_OVERLOAD)
351                ]
352            )
353        );
354    }
355
356    #[test]
357    fn test_create_signature_help_markdown_string_4() {
358        let signature_help = lsp::SignatureHelp {
359            signatures: vec![
360                lsp::SignatureInformation {
361                    label: "fn test1(foo: u8, bar: &str)".to_string(),
362                    documentation: None,
363                    parameters: Some(vec![
364                        lsp::ParameterInformation {
365                            label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
366                            documentation: None,
367                        },
368                        lsp::ParameterInformation {
369                            label: lsp::ParameterLabel::Simple("bar: &str".to_string()),
370                            documentation: None,
371                        },
372                    ]),
373                    active_parameter: None,
374                },
375                lsp::SignatureInformation {
376                    label: "fn test2(hoge: String, fuga: bool)".to_string(),
377                    documentation: None,
378                    parameters: Some(vec![
379                        lsp::ParameterInformation {
380                            label: lsp::ParameterLabel::Simple("hoge: String".to_string()),
381                            documentation: None,
382                        },
383                        lsp::ParameterInformation {
384                            label: lsp::ParameterLabel::Simple("fuga: bool".to_string()),
385                            documentation: None,
386                        },
387                    ]),
388                    active_parameter: None,
389                },
390            ],
391            active_signature: Some(1),
392            active_parameter: Some(0),
393        };
394        let maybe_markdown = SignatureHelp::new(signature_help, None);
395        assert!(maybe_markdown.is_some());
396
397        let markdown = maybe_markdown.unwrap();
398        let markdown = (markdown.markdown, markdown.highlights);
399        assert_eq!(
400            markdown,
401            (
402                "```\nhoge: String, fuga: bool (+1 overload)".to_string(),
403                vec![
404                    (0..12, SIGNATURE_HELP_HIGHLIGHT_CURRENT),
405                    (25..38, SIGNATURE_HELP_HIGHLIGHT_OVERLOAD)
406                ]
407            )
408        );
409    }
410
411    #[test]
412    fn test_create_signature_help_markdown_string_5() {
413        let signature_help = lsp::SignatureHelp {
414            signatures: vec![
415                lsp::SignatureInformation {
416                    label: "fn test1(foo: u8, bar: &str)".to_string(),
417                    documentation: None,
418                    parameters: Some(vec![
419                        lsp::ParameterInformation {
420                            label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
421                            documentation: None,
422                        },
423                        lsp::ParameterInformation {
424                            label: lsp::ParameterLabel::Simple("bar: &str".to_string()),
425                            documentation: None,
426                        },
427                    ]),
428                    active_parameter: None,
429                },
430                lsp::SignatureInformation {
431                    label: "fn test2(hoge: String, fuga: bool)".to_string(),
432                    documentation: None,
433                    parameters: Some(vec![
434                        lsp::ParameterInformation {
435                            label: lsp::ParameterLabel::Simple("hoge: String".to_string()),
436                            documentation: None,
437                        },
438                        lsp::ParameterInformation {
439                            label: lsp::ParameterLabel::Simple("fuga: bool".to_string()),
440                            documentation: None,
441                        },
442                    ]),
443                    active_parameter: None,
444                },
445            ],
446            active_signature: Some(1),
447            active_parameter: Some(1),
448        };
449        let maybe_markdown = SignatureHelp::new(signature_help, None);
450        assert!(maybe_markdown.is_some());
451
452        let markdown = maybe_markdown.unwrap();
453        let markdown = (markdown.markdown, markdown.highlights);
454        assert_eq!(
455            markdown,
456            (
457                "```\nhoge: String, fuga: bool (+1 overload)".to_string(),
458                vec![
459                    (14..24, SIGNATURE_HELP_HIGHLIGHT_CURRENT),
460                    (25..38, SIGNATURE_HELP_HIGHLIGHT_OVERLOAD)
461                ]
462            )
463        );
464    }
465
466    #[test]
467    fn test_create_signature_help_markdown_string_6() {
468        let signature_help = lsp::SignatureHelp {
469            signatures: vec![
470                lsp::SignatureInformation {
471                    label: "fn test1(foo: u8, bar: &str)".to_string(),
472                    documentation: None,
473                    parameters: Some(vec![
474                        lsp::ParameterInformation {
475                            label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
476                            documentation: None,
477                        },
478                        lsp::ParameterInformation {
479                            label: lsp::ParameterLabel::Simple("bar: &str".to_string()),
480                            documentation: None,
481                        },
482                    ]),
483                    active_parameter: None,
484                },
485                lsp::SignatureInformation {
486                    label: "fn test2(hoge: String, fuga: bool)".to_string(),
487                    documentation: None,
488                    parameters: Some(vec![
489                        lsp::ParameterInformation {
490                            label: lsp::ParameterLabel::Simple("hoge: String".to_string()),
491                            documentation: None,
492                        },
493                        lsp::ParameterInformation {
494                            label: lsp::ParameterLabel::Simple("fuga: bool".to_string()),
495                            documentation: None,
496                        },
497                    ]),
498                    active_parameter: None,
499                },
500            ],
501            active_signature: Some(1),
502            active_parameter: None,
503        };
504        let maybe_markdown = SignatureHelp::new(signature_help, None);
505        assert!(maybe_markdown.is_some());
506
507        let markdown = maybe_markdown.unwrap();
508        let markdown = (markdown.markdown, markdown.highlights);
509        assert_eq!(
510            markdown,
511            (
512                "```\nhoge: String, fuga: bool (+1 overload)".to_string(),
513                vec![(25..38, SIGNATURE_HELP_HIGHLIGHT_OVERLOAD)]
514            )
515        );
516    }
517
518    #[test]
519    fn test_create_signature_help_markdown_string_7() {
520        let signature_help = lsp::SignatureHelp {
521            signatures: vec![
522                lsp::SignatureInformation {
523                    label: "fn test1(foo: u8, bar: &str)".to_string(),
524                    documentation: None,
525                    parameters: Some(vec![
526                        lsp::ParameterInformation {
527                            label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
528                            documentation: None,
529                        },
530                        lsp::ParameterInformation {
531                            label: lsp::ParameterLabel::Simple("bar: &str".to_string()),
532                            documentation: None,
533                        },
534                    ]),
535                    active_parameter: None,
536                },
537                lsp::SignatureInformation {
538                    label: "fn test2(hoge: String, fuga: bool)".to_string(),
539                    documentation: None,
540                    parameters: Some(vec![
541                        lsp::ParameterInformation {
542                            label: lsp::ParameterLabel::Simple("hoge: String".to_string()),
543                            documentation: None,
544                        },
545                        lsp::ParameterInformation {
546                            label: lsp::ParameterLabel::Simple("fuga: bool".to_string()),
547                            documentation: None,
548                        },
549                    ]),
550                    active_parameter: None,
551                },
552                lsp::SignatureInformation {
553                    label: "fn test3(one: usize, two: u32)".to_string(),
554                    documentation: None,
555                    parameters: Some(vec![
556                        lsp::ParameterInformation {
557                            label: lsp::ParameterLabel::Simple("one: usize".to_string()),
558                            documentation: None,
559                        },
560                        lsp::ParameterInformation {
561                            label: lsp::ParameterLabel::Simple("two: u32".to_string()),
562                            documentation: None,
563                        },
564                    ]),
565                    active_parameter: None,
566                },
567            ],
568            active_signature: Some(2),
569            active_parameter: Some(1),
570        };
571        let maybe_markdown = SignatureHelp::new(signature_help, None);
572        assert!(maybe_markdown.is_some());
573
574        let markdown = maybe_markdown.unwrap();
575        let markdown = (markdown.markdown, markdown.highlights);
576        assert_eq!(
577            markdown,
578            (
579                "```\none: usize, two: u32 (+2 overload)".to_string(),
580                vec![
581                    (12..20, SIGNATURE_HELP_HIGHLIGHT_CURRENT),
582                    (21..34, SIGNATURE_HELP_HIGHLIGHT_OVERLOAD)
583                ]
584            )
585        );
586    }
587
588    #[test]
589    fn test_create_signature_help_markdown_string_8() {
590        let signature_help = lsp::SignatureHelp {
591            signatures: vec![],
592            active_signature: None,
593            active_parameter: None,
594        };
595        let maybe_markdown = SignatureHelp::new(signature_help, None);
596        assert!(maybe_markdown.is_none());
597    }
598
599    #[test]
600    fn test_create_signature_help_markdown_string_9() {
601        let signature_help = lsp::SignatureHelp {
602            signatures: vec![lsp::SignatureInformation {
603                label: "fn test(foo: u8, bar: &str)".to_string(),
604                documentation: None,
605                parameters: Some(vec![
606                    lsp::ParameterInformation {
607                        label: lsp::ParameterLabel::LabelOffsets([8, 15]),
608                        documentation: None,
609                    },
610                    lsp::ParameterInformation {
611                        label: lsp::ParameterLabel::LabelOffsets([17, 26]),
612                        documentation: None,
613                    },
614                ]),
615                active_parameter: None,
616            }],
617            active_signature: Some(0),
618            active_parameter: Some(0),
619        };
620        let maybe_markdown = SignatureHelp::new(signature_help, None);
621        assert!(maybe_markdown.is_some());
622
623        let markdown = maybe_markdown.unwrap();
624        let markdown = (markdown.markdown, markdown.highlights);
625        assert_eq!(
626            markdown,
627            (
628                "```\nfoo: u8, bar: &str".to_string(),
629                vec![(0..7, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]
630            )
631        );
632    }
633}