1mod popover;
2mod state;
3
4use crate::actions::ShowSignatureHelp;
5use crate::{Editor, EditorSettings, ToggleAutoSignatureHelp};
6use gpui::{AppContext, ViewContext};
7use language::markdown::parse_markdown;
8use multi_buffer::{Anchor, ToOffset};
9use settings::Settings;
10use std::ops::Range;
11
12pub use popover::SignatureHelpPopover;
13pub use state::SignatureHelpState;
14
15// Language-specific settings may define quotes as "brackets", so filter them out separately.
16const QUOTE_PAIRS: [(&str, &str); 3] = [("'", "'"), ("\"", "\""), ("`", "`")];
17
18#[derive(Debug, Clone, Copy, PartialEq)]
19pub enum SignatureHelpHiddenBy {
20 AutoClose,
21 Escape,
22 Selection,
23}
24
25impl Editor {
26 pub fn toggle_auto_signature_help_menu(
27 &mut self,
28 _: &ToggleAutoSignatureHelp,
29 cx: &mut ViewContext<Self>,
30 ) {
31 self.auto_signature_help = self
32 .auto_signature_help
33 .map(|auto_signature_help| !auto_signature_help)
34 .or_else(|| Some(!EditorSettings::get_global(cx).auto_signature_help));
35 match self.auto_signature_help {
36 Some(auto_signature_help) if auto_signature_help => {
37 self.show_signature_help(&ShowSignatureHelp, cx);
38 }
39 Some(_) => {
40 self.hide_signature_help(cx, SignatureHelpHiddenBy::AutoClose);
41 }
42 None => {}
43 }
44 cx.notify();
45 }
46
47 pub(super) fn hide_signature_help(
48 &mut self,
49 cx: &mut ViewContext<Self>,
50 signature_help_hidden_by: SignatureHelpHiddenBy,
51 ) -> bool {
52 if self.signature_help_state.is_shown() {
53 self.signature_help_state.kill_task();
54 self.signature_help_state.hide(signature_help_hidden_by);
55 cx.notify();
56 true
57 } else {
58 false
59 }
60 }
61
62 pub fn auto_signature_help_enabled(&self, cx: &AppContext) -> bool {
63 if let Some(auto_signature_help) = self.auto_signature_help {
64 auto_signature_help
65 } else {
66 EditorSettings::get_global(cx).auto_signature_help
67 }
68 }
69
70 pub(super) fn should_open_signature_help_automatically(
71 &mut self,
72 old_cursor_position: &Anchor,
73 backspace_pressed: bool,
74 cx: &mut ViewContext<Self>,
75 ) -> bool {
76 if !(self.signature_help_state.is_shown() || self.auto_signature_help_enabled(cx)) {
77 return false;
78 }
79 let newest_selection = self.selections.newest::<usize>(cx);
80 let head = newest_selection.head();
81
82 // There are two cases where the head and tail of a selection are different: selecting multiple ranges and using backspace.
83 // If we donβt exclude the backspace case, signature_help will blink every time backspace is pressed, so we need to prevent this.
84 if !newest_selection.is_empty() && !backspace_pressed && head != newest_selection.tail() {
85 self.signature_help_state
86 .hide(SignatureHelpHiddenBy::Selection);
87 return false;
88 }
89
90 let buffer_snapshot = self.buffer().read(cx).snapshot(cx);
91 let bracket_range = |position: usize| match (position, position + 1) {
92 (0, b) if b <= buffer_snapshot.len() => 0..b,
93 (0, b) => 0..b - 1,
94 (a, b) if b <= buffer_snapshot.len() => a - 1..b,
95 (a, b) => a - 1..b - 1,
96 };
97 let not_quote_like_brackets = |start: Range<usize>, end: Range<usize>| {
98 let text = buffer_snapshot.text();
99 let (text_start, text_end) = (text.get(start), text.get(end));
100 QUOTE_PAIRS
101 .into_iter()
102 .all(|(start, end)| text_start != Some(start) && text_end != Some(end))
103 };
104
105 let previous_position = old_cursor_position.to_offset(&buffer_snapshot);
106 let previous_brackets_range = bracket_range(previous_position);
107 let previous_brackets_surround = buffer_snapshot
108 .innermost_enclosing_bracket_ranges(
109 previous_brackets_range,
110 Some(¬_quote_like_brackets),
111 )
112 .filter(|(start_bracket_range, end_bracket_range)| {
113 start_bracket_range.start != previous_position
114 && end_bracket_range.end != previous_position
115 });
116 let current_brackets_range = bracket_range(head);
117 let current_brackets_surround = buffer_snapshot
118 .innermost_enclosing_bracket_ranges(
119 current_brackets_range,
120 Some(¬_quote_like_brackets),
121 )
122 .filter(|(start_bracket_range, end_bracket_range)| {
123 start_bracket_range.start != head && end_bracket_range.end != head
124 });
125
126 match (previous_brackets_surround, current_brackets_surround) {
127 (None, None) => {
128 self.signature_help_state
129 .hide(SignatureHelpHiddenBy::AutoClose);
130 false
131 }
132 (Some(_), None) => {
133 self.signature_help_state
134 .hide(SignatureHelpHiddenBy::AutoClose);
135 false
136 }
137 (None, Some(_)) => true,
138 (Some(previous), Some(current)) => {
139 let condition = self.signature_help_state.hidden_by_selection()
140 || previous != current
141 || (previous == current && self.signature_help_state.is_shown());
142 if !condition {
143 self.signature_help_state
144 .hide(SignatureHelpHiddenBy::AutoClose);
145 }
146 condition
147 }
148 }
149 }
150
151 pub fn show_signature_help(&mut self, _: &ShowSignatureHelp, cx: &mut ViewContext<Self>) {
152 if self.pending_rename.is_some() || self.has_active_completions_menu() {
153 return;
154 }
155
156 let position = self.selections.newest_anchor().head();
157 let Some((buffer, buffer_position)) =
158 self.buffer.read(cx).text_anchor_for_position(position, cx)
159 else {
160 return;
161 };
162
163 self.signature_help_state
164 .set_task(cx.spawn(move |editor, mut cx| async move {
165 let signature_help = editor
166 .update(&mut cx, |editor, cx| {
167 let language = editor.language_at(position, cx);
168 let project = editor.project.clone()?;
169 let (markdown, language_registry) = {
170 project.update(cx, |project, cx| {
171 let language_registry = project.languages().clone();
172 (
173 project.signature_help(&buffer, buffer_position, cx),
174 language_registry,
175 )
176 })
177 };
178 Some((markdown, language_registry, language))
179 })
180 .ok()
181 .flatten();
182 let signature_help_popover = if let Some((
183 signature_help_task,
184 language_registry,
185 language,
186 )) = signature_help
187 {
188 // TODO allow multiple signature helps inside the same popover
189 if let Some(mut signature_help) = signature_help_task.await.into_iter().next() {
190 let mut parsed_content = parse_markdown(
191 signature_help.markdown.as_str(),
192 &language_registry,
193 language,
194 )
195 .await;
196 parsed_content
197 .highlights
198 .append(&mut signature_help.highlights);
199 Some(SignatureHelpPopover { parsed_content })
200 } else {
201 None
202 }
203 } else {
204 None
205 };
206 editor
207 .update(&mut cx, |editor, cx| {
208 let previous_popover = editor.signature_help_state.popover();
209 if previous_popover != signature_help_popover.as_ref() {
210 if let Some(signature_help_popover) = signature_help_popover {
211 editor
212 .signature_help_state
213 .set_popover(signature_help_popover);
214 } else {
215 editor
216 .signature_help_state
217 .hide(SignatureHelpHiddenBy::AutoClose);
218 }
219 cx.notify();
220 }
221 })
222 .ok();
223 }));
224 }
225}