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