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().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
121                    .documentation
122                    .map(|documentation| lsp_to_proto_documentation(documentation)),
123                parameters: signature
124                    .parameters
125                    .unwrap_or_default()
126                    .into_iter()
127                    .map(|parameter_info| proto::ParameterInformation {
128                        label: Some(match parameter_info.label {
129                            lsp::ParameterLabel::Simple(label) => {
130                                proto::parameter_information::Label::Simple(label)
131                            }
132                            lsp::ParameterLabel::LabelOffsets(offsets) => {
133                                proto::parameter_information::Label::LabelOffsets(
134                                    proto::LabelOffsets {
135                                        start: offsets[0],
136                                        end: offsets[1],
137                                    },
138                                )
139                            }
140                        }),
141                        documentation: parameter_info.documentation.map(lsp_to_proto_documentation),
142                    })
143                    .collect(),
144                active_parameter: signature.active_parameter,
145            })
146            .collect(),
147        active_signature: lsp_help.active_signature,
148        active_parameter: lsp_help.active_parameter,
149    }
150}
151
152fn lsp_to_proto_documentation(documentation: lsp::Documentation) -> proto::Documentation {
153    proto::Documentation {
154        content: Some(match documentation {
155            lsp::Documentation::String(string) => proto::documentation::Content::Value(string),
156            lsp::Documentation::MarkupContent(content) => {
157                proto::documentation::Content::MarkupContent(proto::MarkupContent {
158                    is_markdown: matches!(content.kind, lsp::MarkupKind::Markdown),
159                    value: content.value,
160                })
161            }
162        }),
163    }
164}
165
166pub fn proto_to_lsp_signature(proto_help: proto::SignatureHelp) -> lsp::SignatureHelp {
167    lsp::SignatureHelp {
168        signatures: proto_help
169            .signatures
170            .into_iter()
171            .map(|signature| lsp::SignatureInformation {
172                label: signature.label,
173                documentation: signature.documentation.and_then(proto_to_lsp_documentation),
174                parameters: Some(
175                    signature
176                        .parameters
177                        .into_iter()
178                        .filter_map(|parameter_info| {
179                            Some(lsp::ParameterInformation {
180                                label: match parameter_info.label? {
181                                    proto::parameter_information::Label::Simple(string) => {
182                                        lsp::ParameterLabel::Simple(string)
183                                    }
184                                    proto::parameter_information::Label::LabelOffsets(offsets) => {
185                                        lsp::ParameterLabel::LabelOffsets([
186                                            offsets.start,
187                                            offsets.end,
188                                        ])
189                                    }
190                                },
191                                documentation: parameter_info
192                                    .documentation
193                                    .and_then(proto_to_lsp_documentation),
194                            })
195                        })
196                        .collect(),
197                ),
198                active_parameter: signature.active_parameter,
199            })
200            .collect(),
201        active_signature: proto_help.active_signature,
202        active_parameter: proto_help.active_parameter,
203    }
204}
205
206fn proto_to_lsp_documentation(documentation: proto::Documentation) -> Option<lsp::Documentation> {
207    let documentation = {
208        Some(match documentation.content? {
209            documentation::Content::Value(string) => lsp::Documentation::String(string),
210            documentation::Content::MarkupContent(markup) => {
211                lsp::Documentation::MarkupContent(if markup.is_markdown {
212                    lsp::MarkupContent {
213                        kind: lsp::MarkupKind::Markdown,
214                        value: markup.value,
215                    }
216                } else {
217                    lsp::MarkupContent {
218                        kind: lsp::MarkupKind::PlainText,
219                        value: markup.value,
220                    }
221                })
222            }
223        })
224    };
225    documentation
226}
227
228#[cfg(test)]
229mod tests {
230    use crate::lsp_command::signature_help::{
231        SignatureHelp, SIGNATURE_HELP_HIGHLIGHT_CURRENT, SIGNATURE_HELP_HIGHLIGHT_OVERLOAD,
232    };
233
234    #[test]
235    fn test_create_signature_help_markdown_string_1() {
236        let signature_help = lsp::SignatureHelp {
237            signatures: vec![lsp::SignatureInformation {
238                label: "fn test(foo: u8, bar: &str)".to_string(),
239                documentation: None,
240                parameters: Some(vec![
241                    lsp::ParameterInformation {
242                        label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
243                        documentation: None,
244                    },
245                    lsp::ParameterInformation {
246                        label: lsp::ParameterLabel::Simple("bar: &str".to_string()),
247                        documentation: None,
248                    },
249                ]),
250                active_parameter: None,
251            }],
252            active_signature: Some(0),
253            active_parameter: Some(0),
254        };
255        let maybe_markdown = SignatureHelp::new(signature_help, None);
256        assert!(maybe_markdown.is_some());
257
258        let markdown = maybe_markdown.unwrap();
259        let markdown = (markdown.markdown, markdown.highlights);
260        assert_eq!(
261            markdown,
262            (
263                "```\nfoo: u8, bar: &str".to_string(),
264                vec![(0..7, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]
265            )
266        );
267    }
268
269    #[test]
270    fn test_create_signature_help_markdown_string_2() {
271        let signature_help = lsp::SignatureHelp {
272            signatures: vec![lsp::SignatureInformation {
273                label: "fn test(foo: u8, bar: &str)".to_string(),
274                documentation: None,
275                parameters: Some(vec![
276                    lsp::ParameterInformation {
277                        label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
278                        documentation: None,
279                    },
280                    lsp::ParameterInformation {
281                        label: lsp::ParameterLabel::Simple("bar: &str".to_string()),
282                        documentation: None,
283                    },
284                ]),
285                active_parameter: None,
286            }],
287            active_signature: Some(0),
288            active_parameter: Some(1),
289        };
290        let maybe_markdown = SignatureHelp::new(signature_help, None);
291        assert!(maybe_markdown.is_some());
292
293        let markdown = maybe_markdown.unwrap();
294        let markdown = (markdown.markdown, markdown.highlights);
295        assert_eq!(
296            markdown,
297            (
298                "```\nfoo: u8, bar: &str".to_string(),
299                vec![(9..18, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]
300            )
301        );
302    }
303
304    #[test]
305    fn test_create_signature_help_markdown_string_3() {
306        let signature_help = lsp::SignatureHelp {
307            signatures: vec![
308                lsp::SignatureInformation {
309                    label: "fn test1(foo: u8, bar: &str)".to_string(),
310                    documentation: None,
311                    parameters: Some(vec![
312                        lsp::ParameterInformation {
313                            label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
314                            documentation: None,
315                        },
316                        lsp::ParameterInformation {
317                            label: lsp::ParameterLabel::Simple("bar: &str".to_string()),
318                            documentation: None,
319                        },
320                    ]),
321                    active_parameter: None,
322                },
323                lsp::SignatureInformation {
324                    label: "fn test2(hoge: String, fuga: bool)".to_string(),
325                    documentation: None,
326                    parameters: Some(vec![
327                        lsp::ParameterInformation {
328                            label: lsp::ParameterLabel::Simple("hoge: String".to_string()),
329                            documentation: None,
330                        },
331                        lsp::ParameterInformation {
332                            label: lsp::ParameterLabel::Simple("fuga: bool".to_string()),
333                            documentation: None,
334                        },
335                    ]),
336                    active_parameter: None,
337                },
338            ],
339            active_signature: Some(0),
340            active_parameter: Some(0),
341        };
342        let maybe_markdown = SignatureHelp::new(signature_help, None);
343        assert!(maybe_markdown.is_some());
344
345        let markdown = maybe_markdown.unwrap();
346        let markdown = (markdown.markdown, markdown.highlights);
347        assert_eq!(
348            markdown,
349            (
350                "```\nfoo: u8, bar: &str (+1 overload)".to_string(),
351                vec![
352                    (0..7, SIGNATURE_HELP_HIGHLIGHT_CURRENT),
353                    (19..32, SIGNATURE_HELP_HIGHLIGHT_OVERLOAD)
354                ]
355            )
356        );
357    }
358
359    #[test]
360    fn test_create_signature_help_markdown_string_4() {
361        let signature_help = lsp::SignatureHelp {
362            signatures: vec![
363                lsp::SignatureInformation {
364                    label: "fn test1(foo: u8, bar: &str)".to_string(),
365                    documentation: None,
366                    parameters: Some(vec![
367                        lsp::ParameterInformation {
368                            label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
369                            documentation: None,
370                        },
371                        lsp::ParameterInformation {
372                            label: lsp::ParameterLabel::Simple("bar: &str".to_string()),
373                            documentation: None,
374                        },
375                    ]),
376                    active_parameter: None,
377                },
378                lsp::SignatureInformation {
379                    label: "fn test2(hoge: String, fuga: bool)".to_string(),
380                    documentation: None,
381                    parameters: Some(vec![
382                        lsp::ParameterInformation {
383                            label: lsp::ParameterLabel::Simple("hoge: String".to_string()),
384                            documentation: None,
385                        },
386                        lsp::ParameterInformation {
387                            label: lsp::ParameterLabel::Simple("fuga: bool".to_string()),
388                            documentation: None,
389                        },
390                    ]),
391                    active_parameter: None,
392                },
393            ],
394            active_signature: Some(1),
395            active_parameter: Some(0),
396        };
397        let maybe_markdown = SignatureHelp::new(signature_help, None);
398        assert!(maybe_markdown.is_some());
399
400        let markdown = maybe_markdown.unwrap();
401        let markdown = (markdown.markdown, markdown.highlights);
402        assert_eq!(
403            markdown,
404            (
405                "```\nhoge: String, fuga: bool (+1 overload)".to_string(),
406                vec![
407                    (0..12, SIGNATURE_HELP_HIGHLIGHT_CURRENT),
408                    (25..38, SIGNATURE_HELP_HIGHLIGHT_OVERLOAD)
409                ]
410            )
411        );
412    }
413
414    #[test]
415    fn test_create_signature_help_markdown_string_5() {
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(1),
451        };
452        let maybe_markdown = SignatureHelp::new(signature_help, None);
453        assert!(maybe_markdown.is_some());
454
455        let markdown = maybe_markdown.unwrap();
456        let markdown = (markdown.markdown, markdown.highlights);
457        assert_eq!(
458            markdown,
459            (
460                "```\nhoge: String, fuga: bool (+1 overload)".to_string(),
461                vec![
462                    (14..24, SIGNATURE_HELP_HIGHLIGHT_CURRENT),
463                    (25..38, SIGNATURE_HELP_HIGHLIGHT_OVERLOAD)
464                ]
465            )
466        );
467    }
468
469    #[test]
470    fn test_create_signature_help_markdown_string_6() {
471        let signature_help = lsp::SignatureHelp {
472            signatures: vec![
473                lsp::SignatureInformation {
474                    label: "fn test1(foo: u8, bar: &str)".to_string(),
475                    documentation: None,
476                    parameters: Some(vec![
477                        lsp::ParameterInformation {
478                            label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
479                            documentation: None,
480                        },
481                        lsp::ParameterInformation {
482                            label: lsp::ParameterLabel::Simple("bar: &str".to_string()),
483                            documentation: None,
484                        },
485                    ]),
486                    active_parameter: None,
487                },
488                lsp::SignatureInformation {
489                    label: "fn test2(hoge: String, fuga: bool)".to_string(),
490                    documentation: None,
491                    parameters: Some(vec![
492                        lsp::ParameterInformation {
493                            label: lsp::ParameterLabel::Simple("hoge: String".to_string()),
494                            documentation: None,
495                        },
496                        lsp::ParameterInformation {
497                            label: lsp::ParameterLabel::Simple("fuga: bool".to_string()),
498                            documentation: None,
499                        },
500                    ]),
501                    active_parameter: None,
502                },
503            ],
504            active_signature: Some(1),
505            active_parameter: None,
506        };
507        let maybe_markdown = SignatureHelp::new(signature_help, None);
508        assert!(maybe_markdown.is_some());
509
510        let markdown = maybe_markdown.unwrap();
511        let markdown = (markdown.markdown, markdown.highlights);
512        assert_eq!(
513            markdown,
514            (
515                "```\nhoge: String, fuga: bool (+1 overload)".to_string(),
516                vec![(25..38, SIGNATURE_HELP_HIGHLIGHT_OVERLOAD)]
517            )
518        );
519    }
520
521    #[test]
522    fn test_create_signature_help_markdown_string_7() {
523        let signature_help = lsp::SignatureHelp {
524            signatures: vec![
525                lsp::SignatureInformation {
526                    label: "fn test1(foo: u8, bar: &str)".to_string(),
527                    documentation: None,
528                    parameters: Some(vec![
529                        lsp::ParameterInformation {
530                            label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
531                            documentation: None,
532                        },
533                        lsp::ParameterInformation {
534                            label: lsp::ParameterLabel::Simple("bar: &str".to_string()),
535                            documentation: None,
536                        },
537                    ]),
538                    active_parameter: None,
539                },
540                lsp::SignatureInformation {
541                    label: "fn test2(hoge: String, fuga: bool)".to_string(),
542                    documentation: None,
543                    parameters: Some(vec![
544                        lsp::ParameterInformation {
545                            label: lsp::ParameterLabel::Simple("hoge: String".to_string()),
546                            documentation: None,
547                        },
548                        lsp::ParameterInformation {
549                            label: lsp::ParameterLabel::Simple("fuga: bool".to_string()),
550                            documentation: None,
551                        },
552                    ]),
553                    active_parameter: None,
554                },
555                lsp::SignatureInformation {
556                    label: "fn test3(one: usize, two: u32)".to_string(),
557                    documentation: None,
558                    parameters: Some(vec![
559                        lsp::ParameterInformation {
560                            label: lsp::ParameterLabel::Simple("one: usize".to_string()),
561                            documentation: None,
562                        },
563                        lsp::ParameterInformation {
564                            label: lsp::ParameterLabel::Simple("two: u32".to_string()),
565                            documentation: None,
566                        },
567                    ]),
568                    active_parameter: None,
569                },
570            ],
571            active_signature: Some(2),
572            active_parameter: Some(1),
573        };
574        let maybe_markdown = SignatureHelp::new(signature_help, None);
575        assert!(maybe_markdown.is_some());
576
577        let markdown = maybe_markdown.unwrap();
578        let markdown = (markdown.markdown, markdown.highlights);
579        assert_eq!(
580            markdown,
581            (
582                "```\none: usize, two: u32 (+2 overload)".to_string(),
583                vec![
584                    (12..20, SIGNATURE_HELP_HIGHLIGHT_CURRENT),
585                    (21..34, SIGNATURE_HELP_HIGHLIGHT_OVERLOAD)
586                ]
587            )
588        );
589    }
590
591    #[test]
592    fn test_create_signature_help_markdown_string_8() {
593        let signature_help = lsp::SignatureHelp {
594            signatures: vec![],
595            active_signature: None,
596            active_parameter: None,
597        };
598        let maybe_markdown = SignatureHelp::new(signature_help, None);
599        assert!(maybe_markdown.is_none());
600    }
601
602    #[test]
603    fn test_create_signature_help_markdown_string_9() {
604        let signature_help = lsp::SignatureHelp {
605            signatures: vec![lsp::SignatureInformation {
606                label: "fn test(foo: u8, bar: &str)".to_string(),
607                documentation: None,
608                parameters: Some(vec![
609                    lsp::ParameterInformation {
610                        label: lsp::ParameterLabel::LabelOffsets([8, 15]),
611                        documentation: None,
612                    },
613                    lsp::ParameterInformation {
614                        label: lsp::ParameterLabel::LabelOffsets([17, 26]),
615                        documentation: None,
616                    },
617                ]),
618                active_parameter: None,
619            }],
620            active_signature: Some(0),
621            active_parameter: Some(0),
622        };
623        let maybe_markdown = SignatureHelp::new(signature_help, None);
624        assert!(maybe_markdown.is_some());
625
626        let markdown = maybe_markdown.unwrap();
627        let markdown = (markdown.markdown, markdown.highlights);
628        assert_eq!(
629            markdown,
630            (
631                "```\nfoo: u8, bar: &str".to_string(),
632                vec![(0..7, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]
633            )
634        );
635    }
636}