signature_help.rs

  1use crate::actions::ShowSignatureHelp;
  2use crate::hover_popover::open_markdown_url;
  3use crate::{BufferOffset, Editor, EditorSettings, ToggleAutoSignatureHelp, hover_markdown_style};
  4use gpui::{
  5    App, Context, Entity, HighlightStyle, MouseButton, ScrollHandle, Size, StyledText, Task,
  6    TextStyle, Window, combine_highlights,
  7};
  8use language::BufferSnapshot;
  9use markdown::{Markdown, MarkdownElement};
 10use multi_buffer::{Anchor, MultiBufferOffset, ToOffset};
 11use settings::Settings;
 12use std::ops::Range;
 13use text::Rope;
 14use theme::ThemeSettings;
 15use ui::{
 16    ActiveTheme, AnyElement, ButtonCommon, ButtonStyle, Clickable, FluentBuilder, IconButton,
 17    IconButtonShape, IconName, IconSize, InteractiveElement, IntoElement, Label, LabelCommon,
 18    LabelSize, ParentElement, Pixels, SharedString, StatefulInteractiveElement, Styled, StyledExt,
 19    WithScrollbar, div, relative,
 20};
 21
 22// Language-specific settings may define quotes as "brackets", so filter them out separately.
 23const QUOTE_PAIRS: [(&str, &str); 3] = [("'", "'"), ("\"", "\""), ("`", "`")];
 24
 25#[derive(Debug, Clone, Copy, PartialEq)]
 26pub enum SignatureHelpHiddenBy {
 27    AutoClose,
 28    Escape,
 29    Selection,
 30}
 31
 32impl Editor {
 33    pub fn toggle_auto_signature_help_menu(
 34        &mut self,
 35        _: &ToggleAutoSignatureHelp,
 36        window: &mut Window,
 37        cx: &mut Context<Self>,
 38    ) {
 39        self.auto_signature_help = self
 40            .auto_signature_help
 41            .map(|auto_signature_help| !auto_signature_help)
 42            .or_else(|| Some(!EditorSettings::get_global(cx).auto_signature_help));
 43        match self.auto_signature_help {
 44            Some(true) => {
 45                self.show_signature_help(&ShowSignatureHelp, window, cx);
 46            }
 47            Some(false) => {
 48                self.hide_signature_help(cx, SignatureHelpHiddenBy::AutoClose);
 49            }
 50            None => {}
 51        }
 52    }
 53
 54    pub(super) fn hide_signature_help(
 55        &mut self,
 56        cx: &mut Context<Self>,
 57        signature_help_hidden_by: SignatureHelpHiddenBy,
 58    ) -> bool {
 59        if self.signature_help_state.is_shown() {
 60            self.signature_help_state.task = None;
 61            self.signature_help_state.hide(signature_help_hidden_by);
 62            cx.notify();
 63            true
 64        } else {
 65            false
 66        }
 67    }
 68
 69    pub fn auto_signature_help_enabled(&self, cx: &App) -> bool {
 70        if let Some(auto_signature_help) = self.auto_signature_help {
 71            auto_signature_help
 72        } else {
 73            EditorSettings::get_global(cx).auto_signature_help
 74        }
 75    }
 76
 77    pub(super) fn should_open_signature_help_automatically(
 78        &mut self,
 79        old_cursor_position: &Anchor,
 80        cx: &mut Context<Self>,
 81    ) -> bool {
 82        if !(self.signature_help_state.is_shown() || self.auto_signature_help_enabled(cx)) {
 83            return false;
 84        }
 85        let newest_selection = self
 86            .selections
 87            .newest::<MultiBufferOffset>(&self.display_snapshot(cx));
 88        let head = newest_selection.head();
 89
 90        if !newest_selection.is_empty() && head != newest_selection.tail() {
 91            self.signature_help_state
 92                .hide(SignatureHelpHiddenBy::Selection);
 93            return false;
 94        }
 95
 96        let buffer_snapshot = self.buffer().read(cx).snapshot(cx);
 97        let bracket_range = |position: MultiBufferOffset| {
 98            let range = match (position, position + 1usize) {
 99                (MultiBufferOffset(0), b) if b <= buffer_snapshot.len() => MultiBufferOffset(0)..b,
100                (MultiBufferOffset(0), b) => MultiBufferOffset(0)..b - 1,
101                (a, b) if b <= buffer_snapshot.len() => a - 1..b,
102                (a, b) => a - 1..b - 1,
103            };
104            let start = buffer_snapshot.clip_offset(range.start, text::Bias::Left);
105            let end = buffer_snapshot.clip_offset(range.end, text::Bias::Right);
106            start..end
107        };
108        let not_quote_like_brackets =
109            |buffer: &BufferSnapshot, start: Range<BufferOffset>, end: Range<BufferOffset>| {
110                let text_start = buffer.text_for_range(start).collect::<String>();
111                let text_end = buffer.text_for_range(end).collect::<String>();
112                QUOTE_PAIRS
113                    .into_iter()
114                    .all(|(start, end)| text_start != start && text_end != end)
115            };
116
117        let previous_position = old_cursor_position.to_offset(&buffer_snapshot);
118        let previous_brackets_range = bracket_range(previous_position);
119        let previous_brackets_surround = buffer_snapshot
120            .innermost_enclosing_bracket_ranges(
121                previous_brackets_range,
122                Some(&not_quote_like_brackets),
123            )
124            .filter(|(start_bracket_range, end_bracket_range)| {
125                start_bracket_range.start != previous_position
126                    && end_bracket_range.end != previous_position
127            });
128        let current_brackets_range = bracket_range(head);
129        let current_brackets_surround = buffer_snapshot
130            .innermost_enclosing_bracket_ranges(
131                current_brackets_range,
132                Some(&not_quote_like_brackets),
133            )
134            .filter(|(start_bracket_range, end_bracket_range)| {
135                start_bracket_range.start != head && end_bracket_range.end != head
136            });
137
138        match (previous_brackets_surround, current_brackets_surround) {
139            (None, None) => {
140                self.signature_help_state
141                    .hide(SignatureHelpHiddenBy::AutoClose);
142                false
143            }
144            (Some(_), None) => {
145                self.signature_help_state
146                    .hide(SignatureHelpHiddenBy::AutoClose);
147                false
148            }
149            (None, Some(_)) => true,
150            (Some(previous), Some(current)) => {
151                let condition = self.signature_help_state.hidden_by_selection()
152                    || previous != current
153                    || (previous == current && self.signature_help_state.is_shown());
154                if !condition {
155                    self.signature_help_state
156                        .hide(SignatureHelpHiddenBy::AutoClose);
157                }
158                condition
159            }
160        }
161    }
162
163    pub fn show_signature_help(
164        &mut self,
165        _: &ShowSignatureHelp,
166        window: &mut Window,
167        cx: &mut Context<Self>,
168    ) {
169        if self.pending_rename.is_some() || self.has_visible_completions_menu() {
170            return;
171        }
172
173        let position = self.selections.newest_anchor().head();
174        let Some((buffer, buffer_position)) =
175            self.buffer.read(cx).text_anchor_for_position(position, cx)
176        else {
177            return;
178        };
179        let Some(lsp_store) = self.project().map(|p| p.read(cx).lsp_store()) else {
180            return;
181        };
182        let task = lsp_store.update(cx, |lsp_store, cx| {
183            lsp_store.signature_help(&buffer, buffer_position, cx)
184        });
185        let language = self.language_at(position, cx);
186
187        self.signature_help_state
188            .set_task(cx.spawn_in(window, async move |editor, cx| {
189                let signature_help = task.await;
190                editor
191                    .update(cx, |editor, cx| {
192                        let Some(mut signature_help) =
193                            signature_help.unwrap_or_default().into_iter().next()
194                        else {
195                            editor
196                                .signature_help_state
197                                .hide(SignatureHelpHiddenBy::AutoClose);
198                            return;
199                        };
200
201                        if let Some(language) = language {
202                            for signature in &mut signature_help.signatures {
203                                let text = Rope::from(signature.label.as_ref());
204                                let highlights = language
205                                    .highlight_text(&text, 0..signature.label.len())
206                                    .into_iter()
207                                    .flat_map(|(range, highlight_id)| {
208                                        Some((range, highlight_id.style(cx.theme().syntax())?))
209                                    });
210                                signature.highlights =
211                                    combine_highlights(signature.highlights.clone(), highlights)
212                                        .collect();
213                            }
214                        }
215                        let settings = ThemeSettings::get_global(cx);
216                        let style = TextStyle {
217                            color: cx.theme().colors().text,
218                            font_family: settings.buffer_font.family.clone(),
219                            font_fallbacks: settings.buffer_font.fallbacks.clone(),
220                            font_size: settings.buffer_font_size(cx).into(),
221                            font_weight: settings.buffer_font.weight,
222                            line_height: relative(settings.buffer_line_height.value()),
223                            ..TextStyle::default()
224                        };
225                        let scroll_handle = ScrollHandle::new();
226                        let signatures = signature_help
227                            .signatures
228                            .into_iter()
229                            .map(|s| SignatureHelp {
230                                label: s.label,
231                                documentation: s.documentation,
232                                highlights: s.highlights,
233                                active_parameter: s.active_parameter,
234                                parameter_documentation: s
235                                    .active_parameter
236                                    .and_then(|idx| s.parameters.get(idx))
237                                    .and_then(|param| param.documentation.clone()),
238                            })
239                            .collect::<Vec<_>>();
240
241                        if signatures.is_empty() {
242                            editor
243                                .signature_help_state
244                                .hide(SignatureHelpHiddenBy::AutoClose);
245                            return;
246                        }
247
248                        let current_signature = signature_help
249                            .active_signature
250                            .min(signatures.len().saturating_sub(1));
251
252                        let signature_help_popover = SignatureHelpPopover {
253                            style,
254                            signatures,
255                            current_signature,
256                            scroll_handle,
257                        };
258                        editor
259                            .signature_help_state
260                            .set_popover(signature_help_popover);
261                        cx.notify();
262                    })
263                    .ok();
264            }));
265    }
266}
267
268#[derive(Default, Debug)]
269pub struct SignatureHelpState {
270    task: Option<Task<()>>,
271    popover: Option<SignatureHelpPopover>,
272    hidden_by: Option<SignatureHelpHiddenBy>,
273}
274
275impl SignatureHelpState {
276    fn set_task(&mut self, task: Task<()>) {
277        self.task = Some(task);
278        self.hidden_by = None;
279    }
280
281    #[cfg(test)]
282    pub fn popover(&self) -> Option<&SignatureHelpPopover> {
283        self.popover.as_ref()
284    }
285
286    pub fn popover_mut(&mut self) -> Option<&mut SignatureHelpPopover> {
287        self.popover.as_mut()
288    }
289
290    fn set_popover(&mut self, popover: SignatureHelpPopover) {
291        self.popover = Some(popover);
292        self.hidden_by = None;
293    }
294
295    fn hide(&mut self, hidden_by: SignatureHelpHiddenBy) {
296        if self.hidden_by.is_none() {
297            self.popover = None;
298            self.hidden_by = Some(hidden_by);
299        }
300    }
301
302    fn hidden_by_selection(&self) -> bool {
303        self.hidden_by == Some(SignatureHelpHiddenBy::Selection)
304    }
305
306    pub fn is_shown(&self) -> bool {
307        self.popover.is_some()
308    }
309
310    pub fn has_multiple_signatures(&self) -> bool {
311        self.popover
312            .as_ref()
313            .is_some_and(|popover| popover.signatures.len() > 1)
314    }
315}
316
317#[cfg(test)]
318impl SignatureHelpState {
319    pub fn task(&self) -> Option<&Task<()>> {
320        self.task.as_ref()
321    }
322}
323
324#[derive(Clone, Debug, PartialEq)]
325pub struct SignatureHelp {
326    pub(crate) label: SharedString,
327    documentation: Option<Entity<Markdown>>,
328    highlights: Vec<(Range<usize>, HighlightStyle)>,
329    active_parameter: Option<usize>,
330    parameter_documentation: Option<Entity<Markdown>>,
331}
332
333#[derive(Clone, Debug)]
334pub struct SignatureHelpPopover {
335    pub style: TextStyle,
336    pub signatures: Vec<SignatureHelp>,
337    pub current_signature: usize,
338    scroll_handle: ScrollHandle,
339}
340
341impl SignatureHelpPopover {
342    pub fn render(
343        &mut self,
344        max_size: Size<Pixels>,
345        window: &mut Window,
346        cx: &mut Context<Editor>,
347    ) -> AnyElement {
348        let Some(signature) = self.signatures.get(self.current_signature) else {
349            return div().into_any_element();
350        };
351
352        let main_content = div()
353            .occlude()
354            .p_2()
355            .child(
356                div()
357                    .id("signature_help_container")
358                    .overflow_y_scroll()
359                    .max_w(max_size.width)
360                    .max_h(max_size.height)
361                    .track_scroll(&self.scroll_handle)
362                    .child(
363                        StyledText::new(signature.label.clone()).with_default_highlights(
364                            &self.style,
365                            signature.highlights.iter().cloned(),
366                        ),
367                    )
368                    .when_some(
369                        signature.parameter_documentation.clone(),
370                        |this, param_doc| {
371                            this.child(div().h_px().bg(cx.theme().colors().border_variant).my_1())
372                                .child(
373                                    MarkdownElement::new(
374                                        param_doc,
375                                        hover_markdown_style(window, cx),
376                                    )
377                                    .code_block_renderer(markdown::CodeBlockRenderer::Default {
378                                        copy_button: false,
379                                        border: false,
380                                        copy_button_on_hover: false,
381                                    })
382                                    .on_url_click(open_markdown_url),
383                                )
384                        },
385                    )
386                    .when_some(signature.documentation.clone(), |this, description| {
387                        this.child(div().h_px().bg(cx.theme().colors().border_variant).my_1())
388                            .child(
389                                MarkdownElement::new(description, hover_markdown_style(window, cx))
390                                    .code_block_renderer(markdown::CodeBlockRenderer::Default {
391                                        copy_button: false,
392                                        border: false,
393                                        copy_button_on_hover: false,
394                                    })
395                                    .on_url_click(open_markdown_url),
396                            )
397                    }),
398            )
399            .vertical_scrollbar_for(&self.scroll_handle, window, cx);
400
401        let controls = if self.signatures.len() > 1 {
402            let prev_button = IconButton::new("signature_help_prev", IconName::ChevronUp)
403                .shape(IconButtonShape::Square)
404                .style(ButtonStyle::Subtle)
405                .icon_size(IconSize::Small)
406                .tooltip(move |_window, cx| {
407                    ui::Tooltip::for_action("Previous Signature", &crate::SignatureHelpPrevious, cx)
408                })
409                .on_click(cx.listener(|editor, _, window, cx| {
410                    editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
411                }));
412
413            let next_button = IconButton::new("signature_help_next", IconName::ChevronDown)
414                .shape(IconButtonShape::Square)
415                .style(ButtonStyle::Subtle)
416                .icon_size(IconSize::Small)
417                .tooltip(move |_window, cx| {
418                    ui::Tooltip::for_action("Next Signature", &crate::SignatureHelpNext, cx)
419                })
420                .on_click(cx.listener(|editor, _, window, cx| {
421                    editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
422                }));
423
424            let page = Label::new(format!(
425                "{}/{}",
426                self.current_signature + 1,
427                self.signatures.len()
428            ))
429            .size(LabelSize::Small);
430
431            Some(
432                div()
433                    .flex()
434                    .flex_col()
435                    .items_center()
436                    .gap_0p5()
437                    .px_0p5()
438                    .py_0p5()
439                    .children([
440                        prev_button.into_any_element(),
441                        div().child(page).into_any_element(),
442                        next_button.into_any_element(),
443                    ])
444                    .into_any_element(),
445            )
446        } else {
447            None
448        };
449        div()
450            .elevation_2(cx)
451            .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
452            .on_mouse_move(|_, _, cx| cx.stop_propagation())
453            .flex()
454            .flex_row()
455            .when_some(controls, |this, controls| {
456                this.children(vec![
457                    div().flex().items_end().child(controls),
458                    div().w_px().bg(cx.theme().colors().border_variant),
459                ])
460            })
461            .child(main_content)
462            .into_any_element()
463    }
464}