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        if self.pending_rename.is_some() || self.has_visible_completions_menu() {
171            return;
172        }
173
174        // If there's an already running signature
175        // help task, this will drop it.
176        self.signature_help_state.task = None;
177
178        let position = self.selections.newest_anchor().head();
179        let Some((buffer, buffer_position)) =
180            self.buffer.read(cx).text_anchor_for_position(position, cx)
181        else {
182            return;
183        };
184        let Some(lsp_store) = self.project().map(|p| p.read(cx).lsp_store()) else {
185            return;
186        };
187        let lsp_task = lsp_store.update(cx, |lsp_store, cx| {
188            lsp_store.signature_help(&buffer, buffer_position, cx)
189        });
190        let language = self.language_at(position, cx);
191
192        let signature_help_delay_ms = EditorSettings::get_global(cx).hover_popover_delay.0;
193
194        self.signature_help_state
195            .set_task(cx.spawn_in(window, async move |editor, cx| {
196                if signature_help_delay_ms > 0 {
197                    cx.background_executor()
198                        .timer(Duration::from_millis(signature_help_delay_ms))
199                        .await;
200                }
201
202                let signature_help = lsp_task.await;
203
204                editor
205                    .update(cx, |editor, cx| {
206                        let Some(mut signature_help) =
207                            signature_help.unwrap_or_default().into_iter().next()
208                        else {
209                            editor
210                                .signature_help_state
211                                .hide(SignatureHelpHiddenBy::AutoClose);
212                            return;
213                        };
214
215                        if let Some(language) = language {
216                            for signature in &mut signature_help.signatures {
217                                let text = Rope::from(signature.label.as_ref());
218                                let highlights = language
219                                    .highlight_text(&text, 0..signature.label.len())
220                                    .into_iter()
221                                    .flat_map(|(range, highlight_id)| {
222                                        Some((range, highlight_id.style(cx.theme().syntax())?))
223                                    });
224                                signature.highlights =
225                                    combine_highlights(signature.highlights.clone(), highlights)
226                                        .collect();
227                            }
228                        }
229                        let settings = ThemeSettings::get_global(cx);
230                        let style = TextStyle {
231                            color: cx.theme().colors().text,
232                            font_family: settings.buffer_font.family.clone(),
233                            font_fallbacks: settings.buffer_font.fallbacks.clone(),
234                            font_size: settings.buffer_font_size(cx).into(),
235                            font_weight: settings.buffer_font.weight,
236                            line_height: relative(settings.buffer_line_height.value()),
237                            ..TextStyle::default()
238                        };
239                        let scroll_handle = ScrollHandle::new();
240                        let signatures = signature_help
241                            .signatures
242                            .into_iter()
243                            .map(|s| SignatureHelp {
244                                label: s.label,
245                                documentation: s.documentation,
246                                highlights: s.highlights,
247                                active_parameter: s.active_parameter,
248                                parameter_documentation: s
249                                    .active_parameter
250                                    .and_then(|idx| s.parameters.get(idx))
251                                    .and_then(|param| param.documentation.clone()),
252                            })
253                            .collect::<Vec<_>>();
254
255                        if signatures.is_empty() {
256                            editor
257                                .signature_help_state
258                                .hide(SignatureHelpHiddenBy::AutoClose);
259                            return;
260                        }
261
262                        let current_signature = signature_help
263                            .active_signature
264                            .min(signatures.len().saturating_sub(1));
265
266                        let signature_help_popover = SignatureHelpPopover {
267                            style,
268                            signatures,
269                            current_signature,
270                            scroll_handle,
271                        };
272                        editor
273                            .signature_help_state
274                            .set_popover(signature_help_popover);
275                        cx.notify();
276                    })
277                    .ok();
278            }));
279    }
280}
281
282#[derive(Default, Debug)]
283pub struct SignatureHelpState {
284    task: Option<Task<()>>,
285    popover: Option<SignatureHelpPopover>,
286    hidden_by: Option<SignatureHelpHiddenBy>,
287}
288
289impl SignatureHelpState {
290    fn set_task(&mut self, task: Task<()>) {
291        self.task = Some(task);
292        self.hidden_by = None;
293    }
294
295    #[cfg(test)]
296    pub fn popover(&self) -> Option<&SignatureHelpPopover> {
297        self.popover.as_ref()
298    }
299
300    pub fn popover_mut(&mut self) -> Option<&mut SignatureHelpPopover> {
301        self.popover.as_mut()
302    }
303
304    fn set_popover(&mut self, popover: SignatureHelpPopover) {
305        self.popover = Some(popover);
306        self.hidden_by = None;
307    }
308
309    fn hide(&mut self, hidden_by: SignatureHelpHiddenBy) {
310        if self.hidden_by.is_none() {
311            self.popover = None;
312            self.hidden_by = Some(hidden_by);
313        }
314    }
315
316    fn hidden_by_selection(&self) -> bool {
317        self.hidden_by == Some(SignatureHelpHiddenBy::Selection)
318    }
319
320    pub fn is_shown(&self) -> bool {
321        self.popover.is_some()
322    }
323
324    pub fn has_multiple_signatures(&self) -> bool {
325        self.popover
326            .as_ref()
327            .is_some_and(|popover| popover.signatures.len() > 1)
328    }
329}
330
331#[cfg(test)]
332impl SignatureHelpState {
333    pub fn task(&self) -> Option<&Task<()>> {
334        self.task.as_ref()
335    }
336}
337
338#[derive(Clone, Debug, PartialEq)]
339pub struct SignatureHelp {
340    pub(crate) label: SharedString,
341    documentation: Option<Entity<Markdown>>,
342    highlights: Vec<(Range<usize>, HighlightStyle)>,
343    active_parameter: Option<usize>,
344    parameter_documentation: Option<Entity<Markdown>>,
345}
346
347#[derive(Clone, Debug)]
348pub struct SignatureHelpPopover {
349    pub style: TextStyle,
350    pub signatures: Vec<SignatureHelp>,
351    pub current_signature: usize,
352    scroll_handle: ScrollHandle,
353}
354
355impl SignatureHelpPopover {
356    pub fn render(
357        &mut self,
358        max_size: Size<Pixels>,
359        window: &mut Window,
360        cx: &mut Context<Editor>,
361    ) -> AnyElement {
362        let Some(signature) = self.signatures.get(self.current_signature) else {
363            return div().into_any_element();
364        };
365
366        let main_content = div()
367            .occlude()
368            .p_2()
369            .child(
370                div()
371                    .id("signature_help_container")
372                    .overflow_y_scroll()
373                    .max_w(max_size.width)
374                    .max_h(max_size.height)
375                    .track_scroll(&self.scroll_handle)
376                    .child(
377                        StyledText::new(signature.label.clone()).with_default_highlights(
378                            &self.style,
379                            signature.highlights.iter().cloned(),
380                        ),
381                    )
382                    .when_some(
383                        signature.parameter_documentation.clone(),
384                        |this, param_doc| {
385                            this.child(div().h_px().bg(cx.theme().colors().border_variant).my_1())
386                                .child(
387                                    MarkdownElement::new(
388                                        param_doc,
389                                        hover_markdown_style(window, cx),
390                                    )
391                                    .code_block_renderer(markdown::CodeBlockRenderer::Default {
392                                        copy_button: false,
393                                        border: false,
394                                        copy_button_on_hover: false,
395                                    })
396                                    .on_url_click(open_markdown_url),
397                                )
398                        },
399                    )
400                    .when_some(signature.documentation.clone(), |this, description| {
401                        this.child(div().h_px().bg(cx.theme().colors().border_variant).my_1())
402                            .child(
403                                MarkdownElement::new(description, hover_markdown_style(window, cx))
404                                    .code_block_renderer(markdown::CodeBlockRenderer::Default {
405                                        copy_button: false,
406                                        border: false,
407                                        copy_button_on_hover: false,
408                                    })
409                                    .on_url_click(open_markdown_url),
410                            )
411                    }),
412            )
413            .vertical_scrollbar_for(&self.scroll_handle, window, cx);
414
415        let controls = if self.signatures.len() > 1 {
416            let prev_button = IconButton::new("signature_help_prev", IconName::ChevronUp)
417                .shape(IconButtonShape::Square)
418                .style(ButtonStyle::Subtle)
419                .icon_size(IconSize::Small)
420                .tooltip(move |_window, cx| {
421                    ui::Tooltip::for_action("Previous Signature", &crate::SignatureHelpPrevious, cx)
422                })
423                .on_click(cx.listener(|editor, _, window, cx| {
424                    editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
425                }));
426
427            let next_button = IconButton::new("signature_help_next", IconName::ChevronDown)
428                .shape(IconButtonShape::Square)
429                .style(ButtonStyle::Subtle)
430                .icon_size(IconSize::Small)
431                .tooltip(move |_window, cx| {
432                    ui::Tooltip::for_action("Next Signature", &crate::SignatureHelpNext, cx)
433                })
434                .on_click(cx.listener(|editor, _, window, cx| {
435                    editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
436                }));
437
438            let page = Label::new(format!(
439                "{}/{}",
440                self.current_signature + 1,
441                self.signatures.len()
442            ))
443            .size(LabelSize::Small);
444
445            Some(
446                div()
447                    .flex()
448                    .flex_col()
449                    .items_center()
450                    .gap_0p5()
451                    .px_0p5()
452                    .py_0p5()
453                    .children([
454                        prev_button.into_any_element(),
455                        div().child(page).into_any_element(),
456                        next_button.into_any_element(),
457                    ])
458                    .into_any_element(),
459            )
460        } else {
461            None
462        };
463        div()
464            .elevation_2(cx)
465            .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
466            .on_mouse_move(|_, _, cx| cx.stop_propagation())
467            .flex()
468            .flex_row()
469            .when_some(controls, |this, controls| {
470                this.children(vec![
471                    div().flex().items_end().child(controls),
472                    div().w_px().bg(cx.theme().colors().border_variant),
473                ])
474            })
475            .child(main_content)
476            .into_any_element()
477    }
478}