signature_help.rs

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