signature_help.rs

  1use std::{ops::Range, sync::Arc};
  2
  3use gpui::{App, AppContext, Entity, FontWeight, HighlightStyle, SharedString};
  4use language::LanguageRegistry;
  5use lsp::LanguageServerId;
  6use markdown::Markdown;
  7use rpc::proto::{self, documentation};
  8use util::maybe;
  9
 10#[derive(Debug)]
 11pub struct SignatureHelp {
 12    pub active_signature: usize,
 13    pub signatures: Vec<SignatureHelpData>,
 14    pub(super) original_data: lsp::SignatureHelp,
 15}
 16
 17#[derive(Debug, Clone)]
 18pub struct SignatureHelpData {
 19    pub label: SharedString,
 20    pub documentation: Option<Entity<Markdown>>,
 21    pub highlights: Vec<(Range<usize>, HighlightStyle)>,
 22    pub active_parameter: Option<usize>,
 23    pub parameters: Vec<ParameterInfo>,
 24}
 25
 26#[derive(Debug, Clone)]
 27pub struct ParameterInfo {
 28    pub label_range: Option<Range<usize>>,
 29    pub documentation: Option<Entity<Markdown>>,
 30}
 31
 32impl SignatureHelp {
 33    pub fn new(
 34        help: lsp::SignatureHelp,
 35        language_registry: Option<Arc<LanguageRegistry>>,
 36        lang_server_id: Option<LanguageServerId>,
 37        cx: &mut App,
 38    ) -> Option<Self> {
 39        if help.signatures.is_empty() {
 40            return None;
 41        }
 42        let active_signature = help.active_signature.unwrap_or(0) as usize;
 43        let mut signatures = Vec::<SignatureHelpData>::with_capacity(help.signatures.capacity());
 44        for signature in &help.signatures {
 45            let label = SharedString::from(signature.label.clone());
 46            let active_parameter = signature
 47                .active_parameter
 48                .unwrap_or_else(|| help.active_parameter.unwrap_or(0))
 49                as usize;
 50            let mut highlights = Vec::new();
 51            let mut parameter_infos = Vec::new();
 52
 53            if let Some(parameters) = &signature.parameters {
 54                for (index, parameter) in parameters.iter().enumerate() {
 55                    let label_range = match &parameter.label {
 56                        &lsp::ParameterLabel::LabelOffsets([offset1, offset2]) => {
 57                            maybe!({
 58                                let offset1 = offset1 as usize;
 59                                let offset2 = offset2 as usize;
 60                                if offset1 < offset2 {
 61                                    let mut indices = label.char_indices().scan(
 62                                        0,
 63                                        |utf16_offset_acc, (offset, c)| {
 64                                            let utf16_offset = *utf16_offset_acc;
 65                                            *utf16_offset_acc += c.len_utf16();
 66                                            Some((utf16_offset, offset))
 67                                        },
 68                                    );
 69                                    let (_, offset1) = indices
 70                                        .find(|(utf16_offset, _)| *utf16_offset == offset1)?;
 71                                    let (_, offset2) = indices
 72                                        .find(|(utf16_offset, _)| *utf16_offset == offset2)?;
 73                                    Some(offset1..offset2)
 74                                } else {
 75                                    log::warn!(
 76                                        "language server {lang_server_id:?} produced invalid parameter label range: {offset1:?}..{offset2:?}",
 77                                    );
 78                                    None
 79                                }
 80                            })
 81                        }
 82                        lsp::ParameterLabel::Simple(parameter_label) => {
 83                            if let Some(start) = signature.label.find(parameter_label) {
 84                                Some(start..start + parameter_label.len())
 85                            } else {
 86                                None
 87                            }
 88                        }
 89                    };
 90
 91                    if let Some(label_range) = &label_range
 92                        && index == active_parameter
 93                    {
 94                        highlights.push((
 95                            label_range.clone(),
 96                            HighlightStyle {
 97                                font_weight: Some(FontWeight::EXTRA_BOLD),
 98                                ..HighlightStyle::default()
 99                            },
100                        ));
101                    }
102
103                    let documentation = parameter
104                        .documentation
105                        .as_ref()
106                        .map(|doc| documentation_to_markdown(doc, language_registry.clone(), cx));
107
108                    parameter_infos.push(ParameterInfo {
109                        label_range,
110                        documentation,
111                    });
112                }
113            }
114
115            let documentation = signature
116                .documentation
117                .as_ref()
118                .map(|doc| documentation_to_markdown(doc, language_registry.clone(), cx));
119
120            signatures.push(SignatureHelpData {
121                label,
122                documentation,
123                highlights,
124                active_parameter: Some(active_parameter),
125                parameters: parameter_infos,
126            });
127        }
128        Some(Self {
129            signatures,
130            active_signature,
131            original_data: help,
132        })
133    }
134}
135
136fn documentation_to_markdown(
137    documentation: &lsp::Documentation,
138    language_registry: Option<Arc<LanguageRegistry>>,
139    cx: &mut App,
140) -> Entity<Markdown> {
141    match documentation {
142        lsp::Documentation::String(string) => {
143            cx.new(|cx| Markdown::new_text(SharedString::from(string), cx))
144        }
145        lsp::Documentation::MarkupContent(markup) => match markup.kind {
146            lsp::MarkupKind::PlainText => {
147                cx.new(|cx| Markdown::new_text(SharedString::from(&markup.value), cx))
148            }
149            lsp::MarkupKind::Markdown => cx.new(|cx| {
150                Markdown::new(
151                    SharedString::from(&markup.value),
152                    language_registry,
153                    None,
154                    cx,
155                )
156            }),
157        },
158    }
159}
160
161pub fn lsp_to_proto_signature(lsp_help: lsp::SignatureHelp) -> proto::SignatureHelp {
162    proto::SignatureHelp {
163        signatures: lsp_help
164            .signatures
165            .into_iter()
166            .map(|signature| proto::SignatureInformation {
167                label: signature.label,
168                documentation: signature.documentation.map(lsp_to_proto_documentation),
169                parameters: signature
170                    .parameters
171                    .unwrap_or_default()
172                    .into_iter()
173                    .map(|parameter_info| proto::ParameterInformation {
174                        label: Some(match parameter_info.label {
175                            lsp::ParameterLabel::Simple(label) => {
176                                proto::parameter_information::Label::Simple(label)
177                            }
178                            lsp::ParameterLabel::LabelOffsets(offsets) => {
179                                proto::parameter_information::Label::LabelOffsets(
180                                    proto::LabelOffsets {
181                                        start: offsets[0],
182                                        end: offsets[1],
183                                    },
184                                )
185                            }
186                        }),
187                        documentation: parameter_info.documentation.map(lsp_to_proto_documentation),
188                    })
189                    .collect(),
190                active_parameter: signature.active_parameter,
191            })
192            .collect(),
193        active_signature: lsp_help.active_signature,
194        active_parameter: lsp_help.active_parameter,
195    }
196}
197
198fn lsp_to_proto_documentation(documentation: lsp::Documentation) -> proto::Documentation {
199    proto::Documentation {
200        content: Some(match documentation {
201            lsp::Documentation::String(string) => proto::documentation::Content::Value(string),
202            lsp::Documentation::MarkupContent(content) => {
203                proto::documentation::Content::MarkupContent(proto::MarkupContent {
204                    is_markdown: matches!(content.kind, lsp::MarkupKind::Markdown),
205                    value: content.value,
206                })
207            }
208        }),
209    }
210}
211
212pub fn proto_to_lsp_signature(proto_help: proto::SignatureHelp) -> lsp::SignatureHelp {
213    lsp::SignatureHelp {
214        signatures: proto_help
215            .signatures
216            .into_iter()
217            .map(|signature| lsp::SignatureInformation {
218                label: signature.label,
219                documentation: signature.documentation.and_then(proto_to_lsp_documentation),
220                parameters: Some(
221                    signature
222                        .parameters
223                        .into_iter()
224                        .filter_map(|parameter_info| {
225                            Some(lsp::ParameterInformation {
226                                label: match parameter_info.label? {
227                                    proto::parameter_information::Label::Simple(string) => {
228                                        lsp::ParameterLabel::Simple(string)
229                                    }
230                                    proto::parameter_information::Label::LabelOffsets(offsets) => {
231                                        lsp::ParameterLabel::LabelOffsets([
232                                            offsets.start,
233                                            offsets.end,
234                                        ])
235                                    }
236                                },
237                                documentation: parameter_info
238                                    .documentation
239                                    .and_then(proto_to_lsp_documentation),
240                            })
241                        })
242                        .collect(),
243                ),
244                active_parameter: signature.active_parameter,
245            })
246            .collect(),
247        active_signature: proto_help.active_signature,
248        active_parameter: proto_help.active_parameter,
249    }
250}
251
252fn proto_to_lsp_documentation(documentation: proto::Documentation) -> Option<lsp::Documentation> {
253    {
254        Some(match documentation.content? {
255            documentation::Content::Value(string) => lsp::Documentation::String(string),
256            documentation::Content::MarkupContent(markup) => {
257                lsp::Documentation::MarkupContent(if markup.is_markdown {
258                    lsp::MarkupContent {
259                        kind: lsp::MarkupKind::Markdown,
260                        value: markup.value,
261                    }
262                } else {
263                    lsp::MarkupContent {
264                        kind: lsp::MarkupKind::PlainText,
265                        value: markup.value,
266                    }
267                })
268            }
269        })
270    }
271}