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_features: settings.buffer_font.features.clone(),
252                            font_size: settings.buffer_font_size(cx).into(),
253                            font_weight: settings.buffer_font.weight,
254                            line_height: relative(settings.buffer_line_height.value()),
255                            ..TextStyle::default()
256                        };
257                        let scroll_handle = ScrollHandle::new();
258                        let signatures = signature_help
259                            .signatures
260                            .into_iter()
261                            .map(|s| SignatureHelp {
262                                label: s.label,
263                                documentation: s.documentation,
264                                highlights: s.highlights,
265                                active_parameter: s.active_parameter,
266                                parameter_documentation: s
267                                    .active_parameter
268                                    .and_then(|idx| s.parameters.get(idx))
269                                    .and_then(|param| param.documentation.clone()),
270                            })
271                            .collect::<Vec<_>>();
272
273                        if signatures.is_empty() {
274                            editor
275                                .signature_help_state
276                                .hide(SignatureHelpHiddenBy::AutoClose);
277                            return;
278                        }
279
280                        let current_signature = signature_help
281                            .active_signature
282                            .min(signatures.len().saturating_sub(1));
283
284                        let signature_help_popover = SignatureHelpPopover {
285                            style,
286                            signatures,
287                            current_signature,
288                            scroll_handle,
289                        };
290                        editor
291                            .signature_help_state
292                            .set_popover(signature_help_popover);
293                        cx.notify();
294                    })
295                    .ok();
296            }));
297    }
298}
299
300#[derive(Default, Debug)]
301pub struct SignatureHelpState {
302    task: Option<Task<()>>,
303    popover: Option<SignatureHelpPopover>,
304    hidden_by: Option<SignatureHelpHiddenBy>,
305}
306
307impl SignatureHelpState {
308    fn set_task(&mut self, task: Task<()>) {
309        self.task = Some(task);
310        self.hidden_by = None;
311    }
312
313    #[cfg(test)]
314    pub fn popover(&self) -> Option<&SignatureHelpPopover> {
315        self.popover.as_ref()
316    }
317
318    pub fn popover_mut(&mut self) -> Option<&mut SignatureHelpPopover> {
319        self.popover.as_mut()
320    }
321
322    fn set_popover(&mut self, popover: SignatureHelpPopover) {
323        self.popover = Some(popover);
324        self.hidden_by = None;
325    }
326
327    fn hide(&mut self, hidden_by: SignatureHelpHiddenBy) {
328        if self.hidden_by.is_none() {
329            self.popover = None;
330            self.hidden_by = Some(hidden_by);
331        }
332    }
333
334    fn hidden_by_selection(&self) -> bool {
335        self.hidden_by == Some(SignatureHelpHiddenBy::Selection)
336    }
337
338    pub fn is_shown(&self) -> bool {
339        self.popover.is_some()
340    }
341
342    pub fn has_multiple_signatures(&self) -> bool {
343        self.popover
344            .as_ref()
345            .is_some_and(|popover| popover.signatures.len() > 1)
346    }
347}
348
349#[cfg(test)]
350impl SignatureHelpState {
351    pub fn task(&self) -> Option<&Task<()>> {
352        self.task.as_ref()
353    }
354}
355
356#[derive(Clone, Debug, PartialEq)]
357pub struct SignatureHelp {
358    pub(crate) label: SharedString,
359    documentation: Option<Entity<Markdown>>,
360    highlights: Vec<(Range<usize>, HighlightStyle)>,
361    active_parameter: Option<usize>,
362    parameter_documentation: Option<Entity<Markdown>>,
363}
364
365#[derive(Clone, Debug)]
366pub struct SignatureHelpPopover {
367    pub style: TextStyle,
368    pub signatures: Vec<SignatureHelp>,
369    pub current_signature: usize,
370    scroll_handle: ScrollHandle,
371}
372
373impl SignatureHelpPopover {
374    pub fn render(
375        &mut self,
376        max_size: Size<Pixels>,
377        window: &mut Window,
378        cx: &mut Context<Editor>,
379    ) -> AnyElement {
380        let Some(signature) = self.signatures.get(self.current_signature) else {
381            return div().into_any_element();
382        };
383
384        let main_content = div()
385            .occlude()
386            .p_2()
387            .child(
388                div()
389                    .id("signature_help_container")
390                    .overflow_y_scroll()
391                    .max_w(max_size.width)
392                    .max_h(max_size.height)
393                    .track_scroll(&self.scroll_handle)
394                    .child(
395                        StyledText::new(signature.label.clone()).with_default_highlights(
396                            &self.style,
397                            signature.highlights.iter().cloned(),
398                        ),
399                    )
400                    .when_some(
401                        signature.parameter_documentation.clone(),
402                        |this, param_doc| {
403                            this.child(div().h_px().bg(cx.theme().colors().border_variant).my_1())
404                                .child(
405                                    MarkdownElement::new(
406                                        param_doc,
407                                        hover_markdown_style(window, cx),
408                                    )
409                                    .code_block_renderer(markdown::CodeBlockRenderer::Default {
410                                        copy_button: false,
411                                        border: false,
412                                        copy_button_on_hover: false,
413                                    })
414                                    .on_url_click(open_markdown_url),
415                                )
416                        },
417                    )
418                    .when_some(signature.documentation.clone(), |this, description| {
419                        this.child(div().h_px().bg(cx.theme().colors().border_variant).my_1())
420                            .child(
421                                MarkdownElement::new(description, hover_markdown_style(window, cx))
422                                    .code_block_renderer(markdown::CodeBlockRenderer::Default {
423                                        copy_button: false,
424                                        border: false,
425                                        copy_button_on_hover: false,
426                                    })
427                                    .on_url_click(open_markdown_url),
428                            )
429                    }),
430            )
431            .vertical_scrollbar_for(&self.scroll_handle, window, cx);
432
433        let controls = if self.signatures.len() > 1 {
434            let prev_button = IconButton::new("signature_help_prev", IconName::ChevronUp)
435                .shape(IconButtonShape::Square)
436                .style(ButtonStyle::Subtle)
437                .icon_size(IconSize::Small)
438                .tooltip(move |_window, cx| {
439                    ui::Tooltip::for_action("Previous Signature", &crate::SignatureHelpPrevious, cx)
440                })
441                .on_click(cx.listener(|editor, _, window, cx| {
442                    editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
443                }));
444
445            let next_button = IconButton::new("signature_help_next", IconName::ChevronDown)
446                .shape(IconButtonShape::Square)
447                .style(ButtonStyle::Subtle)
448                .icon_size(IconSize::Small)
449                .tooltip(move |_window, cx| {
450                    ui::Tooltip::for_action("Next Signature", &crate::SignatureHelpNext, cx)
451                })
452                .on_click(cx.listener(|editor, _, window, cx| {
453                    editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
454                }));
455
456            let page = Label::new(format!(
457                "{}/{}",
458                self.current_signature + 1,
459                self.signatures.len()
460            ))
461            .size(LabelSize::Small);
462
463            Some(
464                div()
465                    .flex()
466                    .flex_col()
467                    .items_center()
468                    .gap_0p5()
469                    .px_0p5()
470                    .py_0p5()
471                    .children([
472                        prev_button.into_any_element(),
473                        div().child(page).into_any_element(),
474                        next_button.into_any_element(),
475                    ])
476                    .into_any_element(),
477            )
478        } else {
479            None
480        };
481        div()
482            .elevation_2(cx)
483            .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
484            .on_mouse_move(|_, _, cx| cx.stop_propagation())
485            .flex()
486            .flex_row()
487            .when_some(controls, |this, controls| {
488                this.children(vec![
489                    div().flex().items_end().child(controls),
490                    div().w_px().bg(cx.theme().colors().border_variant),
491                ])
492            })
493            .child(main_content)
494            .into_any_element()
495    }
496}