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 ¶meter.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}