mod popover; mod state; use crate::actions::ShowSignatureHelp; use crate::{Editor, EditorSettings, ToggleAutoSignatureHelp}; use gpui::{AppContext, ViewContext}; use language::markdown::parse_markdown; use multi_buffer::{Anchor, ToOffset}; use settings::Settings; use std::ops::Range; pub use popover::SignatureHelpPopover; pub use state::SignatureHelpState; // 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, cx: &mut ViewContext, ) { 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(auto_signature_help) if auto_signature_help => { self.show_signature_help(&ShowSignatureHelp, cx); } Some(_) => { self.hide_signature_help(cx, SignatureHelpHiddenBy::AutoClose); } None => {} } cx.notify(); } pub(super) fn hide_signature_help( &mut self, cx: &mut ViewContext, signature_help_hidden_by: SignatureHelpHiddenBy, ) -> bool { if self.signature_help_state.is_shown() { self.signature_help_state.kill_task(); self.signature_help_state.hide(signature_help_hidden_by); cx.notify(); true } else { false } } pub fn auto_signature_help_enabled(&self, cx: &AppContext) -> 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, backspace_pressed: bool, cx: &mut ViewContext, ) -> 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(); // There are two cases where the head and tail of a selection are different: selecting multiple ranges and using backspace. // If we don’t exclude the backspace case, signature_help will blink every time backspace is pressed, so we need to prevent this. if !newest_selection.is_empty() && !backspace_pressed && 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 = |start: Range, end: Range| { let text = buffer_snapshot.text(); let (text_start, text_end) = (text.get(start), text.get(end)); QUOTE_PAIRS .into_iter() .all(|(start, end)| text_start != Some(start) && text_end != Some(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, cx: &mut ViewContext) { if self.pending_rename.is_some() || self.has_active_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; }; self.signature_help_state .set_task(cx.spawn(move |editor, mut cx| async move { let signature_help = editor .update(&mut cx, |editor, cx| { let language = editor.language_at(position, cx); let project = editor.project.clone()?; let (markdown, language_registry) = { project.update(cx, |project, cx| { let language_registry = project.languages().clone(); ( project.signature_help(&buffer, buffer_position, cx), language_registry, ) }) }; Some((markdown, language_registry, language)) }) .ok() .flatten(); let signature_help_popover = if let Some(( signature_help_task, language_registry, language, )) = signature_help { // TODO allow multiple signature helps inside the same popover if let Some(mut signature_help) = signature_help_task.await.into_iter().next() { let mut parsed_content = parse_markdown( signature_help.markdown.as_str(), &language_registry, language, ) .await; parsed_content .highlights .append(&mut signature_help.highlights); Some(SignatureHelpPopover { parsed_content }) } else { None } } else { None }; editor .update(&mut cx, |editor, cx| { let previous_popover = editor.signature_help_state.popover(); if previous_popover != signature_help_popover.as_ref() { if let Some(signature_help_popover) = signature_help_popover { editor .signature_help_state .set_popover(signature_help_popover); } else { editor .signature_help_state .hide(SignatureHelpHiddenBy::AutoClose); } cx.notify(); } }) .ok(); })); } }