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