use crate::actions::ShowSignatureHelp; use crate::hover_popover::open_markdown_url; use crate::{Editor, EditorSettings, ToggleAutoSignatureHelp, hover_markdown_style}; use gpui::{ App, Context, Div, Entity, HighlightStyle, MouseButton, ScrollHandle, Size, Stateful, StyledText, Task, TextStyle, Window, combine_highlights, }; use language::BufferSnapshot; use markdown::{Markdown, MarkdownElement}; use multi_buffer::{Anchor, ToOffset}; use settings::Settings; use std::ops::Range; use text::Rope; use theme::ThemeSettings; use ui::{ ActiveTheme, AnyElement, ButtonCommon, ButtonStyle, Clickable, FluentBuilder, IconButton, IconButtonShape, IconName, IconSize, InteractiveElement, IntoElement, Label, LabelCommon, LabelSize, ParentElement, Pixels, Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement, Styled, StyledExt, div, px, relative, }; // Language-specific settings may define quotes as "brackets", so filter them out separately. const QUOTE_PAIRS: [(&str, &str); 3] = [("'", "'"), ("\"", "\""), ("`", "`")]; #[derive(Debug, Clone, Copy, PartialEq)] pub enum SignatureHelpHiddenBy { AutoClose, Escape, Selection, } impl Editor { pub fn toggle_auto_signature_help_menu( &mut self, _: &ToggleAutoSignatureHelp, window: &mut Window, cx: &mut Context, ) { self.auto_signature_help = self .auto_signature_help .map(|auto_signature_help| !auto_signature_help) .or_else(|| Some(!EditorSettings::get_global(cx).auto_signature_help)); match self.auto_signature_help { Some(true) => { self.show_signature_help(&ShowSignatureHelp, window, cx); } Some(false) => { self.hide_signature_help(cx, SignatureHelpHiddenBy::AutoClose); } None => {} } } pub(super) fn hide_signature_help( &mut self, cx: &mut Context, signature_help_hidden_by: SignatureHelpHiddenBy, ) -> bool { if self.signature_help_state.is_shown() { self.signature_help_state.task = None; self.signature_help_state.hide(signature_help_hidden_by); cx.notify(); true } else { false } } pub fn auto_signature_help_enabled(&self, cx: &App) -> bool { if let Some(auto_signature_help) = self.auto_signature_help { auto_signature_help } else { EditorSettings::get_global(cx).auto_signature_help } } pub(super) fn should_open_signature_help_automatically( &mut self, old_cursor_position: &Anchor, cx: &mut Context, ) -> bool { if !(self.signature_help_state.is_shown() || self.auto_signature_help_enabled(cx)) { return false; } let newest_selection = self.selections.newest::(cx); let head = newest_selection.head(); if !newest_selection.is_empty() && head != newest_selection.tail() { self.signature_help_state .hide(SignatureHelpHiddenBy::Selection); return false; } let buffer_snapshot = self.buffer().read(cx).snapshot(cx); let bracket_range = |position: usize| match (position, position + 1) { (0, b) if b <= buffer_snapshot.len() => 0..b, (0, b) => 0..b - 1, (a, b) if b <= buffer_snapshot.len() => a - 1..b, (a, b) => a - 1..b - 1, }; let not_quote_like_brackets = |buffer: &BufferSnapshot, start: Range, end: Range| { let text_start = buffer.text_for_range(start).collect::(); let text_end = buffer.text_for_range(end).collect::(); QUOTE_PAIRS .into_iter() .all(|(start, end)| text_start != start && text_end != end) }; let previous_position = old_cursor_position.to_offset(&buffer_snapshot); let previous_brackets_range = bracket_range(previous_position); let previous_brackets_surround = buffer_snapshot .innermost_enclosing_bracket_ranges( previous_brackets_range, Some(¬_quote_like_brackets), ) .filter(|(start_bracket_range, end_bracket_range)| { start_bracket_range.start != previous_position && end_bracket_range.end != previous_position }); let current_brackets_range = bracket_range(head); let current_brackets_surround = buffer_snapshot .innermost_enclosing_bracket_ranges( current_brackets_range, Some(¬_quote_like_brackets), ) .filter(|(start_bracket_range, end_bracket_range)| { start_bracket_range.start != head && end_bracket_range.end != head }); match (previous_brackets_surround, current_brackets_surround) { (None, None) => { self.signature_help_state .hide(SignatureHelpHiddenBy::AutoClose); false } (Some(_), None) => { self.signature_help_state .hide(SignatureHelpHiddenBy::AutoClose); false } (None, Some(_)) => true, (Some(previous), Some(current)) => { let condition = self.signature_help_state.hidden_by_selection() || previous != current || (previous == current && self.signature_help_state.is_shown()); if !condition { self.signature_help_state .hide(SignatureHelpHiddenBy::AutoClose); } condition } } } pub fn show_signature_help( &mut self, _: &ShowSignatureHelp, window: &mut Window, cx: &mut Context, ) { if self.pending_rename.is_some() || self.has_visible_completions_menu() { return; } let position = self.selections.newest_anchor().head(); let Some((buffer, buffer_position)) = self.buffer.read(cx).text_anchor_for_position(position, cx) else { return; }; let Some(lsp_store) = self.project.as_ref().map(|p| p.read(cx).lsp_store()) else { return; }; let task = lsp_store.update(cx, |lsp_store, cx| { lsp_store.signature_help(&buffer, buffer_position, cx) }); let language = self.language_at(position, cx); self.signature_help_state .set_task(cx.spawn_in(window, async move |editor, cx| { let signature_help = task.await; editor .update(cx, |editor, cx| { let Some(mut signature_help) = signature_help.into_iter().next() else { editor .signature_help_state .hide(SignatureHelpHiddenBy::AutoClose); return; }; if let Some(language) = language { for signature in &mut signature_help.signatures { let text = Rope::from(signature.label.to_string()); let highlights = language .highlight_text(&text, 0..signature.label.len()) .into_iter() .flat_map(|(range, highlight_id)| { Some((range, highlight_id.style(&cx.theme().syntax())?)) }); signature.highlights = combine_highlights(signature.highlights.clone(), highlights) .collect(); } } let settings = ThemeSettings::get_global(cx); let style = TextStyle { color: cx.theme().colors().text, font_family: settings.buffer_font.family.clone(), font_fallbacks: settings.buffer_font.fallbacks.clone(), font_size: settings.buffer_font_size(cx).into(), font_weight: settings.buffer_font.weight, line_height: relative(settings.buffer_line_height.value()), ..TextStyle::default() }; let scroll_handle = ScrollHandle::new(); let signatures = signature_help .signatures .into_iter() .map(|s| SignatureHelp { label: s.label, documentation: s.documentation, highlights: s.highlights, active_parameter: s.active_parameter, parameter_documentation: s .active_parameter .and_then(|idx| s.parameters.get(idx)) .and_then(|param| param.documentation.clone()), }) .collect::>(); if signatures.is_empty() { editor .signature_help_state .hide(SignatureHelpHiddenBy::AutoClose); return; } let current_signature = signature_help .active_signature .min(signatures.len().saturating_sub(1)); let signature_help_popover = SignatureHelpPopover { scrollbar_state: ScrollbarState::new(scroll_handle.clone()), style, signatures, current_signature, scroll_handle, }; editor .signature_help_state .set_popover(signature_help_popover); cx.notify(); }) .ok(); })); } } #[derive(Default, Debug)] pub struct SignatureHelpState { task: Option>, popover: Option, hidden_by: Option, } impl SignatureHelpState { fn set_task(&mut self, task: Task<()>) { self.task = Some(task); self.hidden_by = None; } #[cfg(test)] pub fn popover(&self) -> Option<&SignatureHelpPopover> { self.popover.as_ref() } pub fn popover_mut(&mut self) -> Option<&mut SignatureHelpPopover> { self.popover.as_mut() } fn set_popover(&mut self, popover: SignatureHelpPopover) { self.popover = Some(popover); self.hidden_by = None; } fn hide(&mut self, hidden_by: SignatureHelpHiddenBy) { if self.hidden_by.is_none() { self.popover = None; self.hidden_by = Some(hidden_by); } } fn hidden_by_selection(&self) -> bool { self.hidden_by == Some(SignatureHelpHiddenBy::Selection) } pub fn is_shown(&self) -> bool { self.popover.is_some() } pub fn has_multiple_signatures(&self) -> bool { self.popover .as_ref() .is_some_and(|popover| popover.signatures.len() > 1) } } #[cfg(test)] impl SignatureHelpState { pub fn task(&self) -> Option<&Task<()>> { self.task.as_ref() } } #[derive(Clone, Debug, PartialEq)] pub struct SignatureHelp { pub(crate) label: SharedString, documentation: Option>, highlights: Vec<(Range, HighlightStyle)>, active_parameter: Option, parameter_documentation: Option>, } #[derive(Clone, Debug)] pub struct SignatureHelpPopover { pub style: TextStyle, pub signatures: Vec, pub current_signature: usize, scroll_handle: ScrollHandle, scrollbar_state: ScrollbarState, } impl SignatureHelpPopover { pub fn render( &mut self, max_size: Size, window: &mut Window, cx: &mut Context, ) -> AnyElement { let Some(signature) = self.signatures.get(self.current_signature) else { return div().into_any_element(); }; let main_content = div() .occlude() .p_2() .child( div() .id("signature_help_container") .overflow_y_scroll() .max_w(max_size.width) .max_h(max_size.height) .track_scroll(&self.scroll_handle) .child( StyledText::new(signature.label.clone()).with_default_highlights( &self.style, signature.highlights.iter().cloned(), ), ) .when_some( signature.parameter_documentation.clone(), |this, param_doc| { this.child(div().h_px().bg(cx.theme().colors().border_variant).my_1()) .child( MarkdownElement::new( param_doc, hover_markdown_style(window, cx), ) .code_block_renderer(markdown::CodeBlockRenderer::Default { copy_button: false, border: false, copy_button_on_hover: false, }) .on_url_click(open_markdown_url), ) }, ) .when_some(signature.documentation.clone(), |this, description| { this.child(div().h_px().bg(cx.theme().colors().border_variant).my_1()) .child( MarkdownElement::new(description, hover_markdown_style(window, cx)) .code_block_renderer(markdown::CodeBlockRenderer::Default { copy_button: false, border: false, copy_button_on_hover: false, }) .on_url_click(open_markdown_url), ) }), ) .child(self.render_vertical_scrollbar(cx)); let controls = if self.signatures.len() > 1 { let prev_button = IconButton::new("signature_help_prev", IconName::ChevronUp) .shape(IconButtonShape::Square) .style(ButtonStyle::Subtle) .icon_size(IconSize::Small) .tooltip(move |window, cx| { ui::Tooltip::for_action( "Previous Signature", &crate::SignatureHelpPrevious, window, cx, ) }) .on_click(cx.listener(|editor, _, window, cx| { editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx); })); let next_button = IconButton::new("signature_help_next", IconName::ChevronDown) .shape(IconButtonShape::Square) .style(ButtonStyle::Subtle) .icon_size(IconSize::Small) .tooltip(move |window, cx| { ui::Tooltip::for_action("Next Signature", &crate::SignatureHelpNext, window, cx) }) .on_click(cx.listener(|editor, _, window, cx| { editor.signature_help_next(&crate::SignatureHelpNext, window, cx); })); let page = Label::new(format!( "{}/{}", self.current_signature + 1, self.signatures.len() )) .size(LabelSize::Small); Some( div() .flex() .flex_col() .items_center() .gap_0p5() .px_0p5() .py_0p5() .children([ prev_button.into_any_element(), div().child(page).into_any_element(), next_button.into_any_element(), ]) .into_any_element(), ) } else { None }; div() .elevation_2(cx) .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) .on_mouse_move(|_, _, cx| cx.stop_propagation()) .flex() .flex_row() .when_some(controls, |this, controls| { this.children(vec![ div().flex().items_end().child(controls), div().w_px().bg(cx.theme().colors().border_variant), ]) }) .child(main_content) .into_any_element() } fn render_vertical_scrollbar(&self, cx: &mut Context) -> Stateful
{ div() .occlude() .id("signature_help_scrollbar") .on_mouse_move(cx.listener(|_, _, _, cx| { cx.notify(); cx.stop_propagation() })) .on_hover(|_, _, cx| cx.stop_propagation()) .on_any_mouse_down(|_, _, cx| cx.stop_propagation()) .on_mouse_up(MouseButton::Left, |_, _, cx| cx.stop_propagation()) .on_scroll_wheel(cx.listener(|_, _, _, cx| cx.notify())) .h_full() .absolute() .right_1() .top_1() .bottom_1() .w(px(12.)) .cursor_default() .children(Scrollbar::vertical(self.scrollbar_state.clone())) } }