1use crate::actions::ShowSignatureHelp;
2use crate::{Editor, EditorSettings, ToggleAutoSignatureHelp};
3use gpui::{
4 App, Context, HighlightStyle, MouseButton, Size, StyledText, Task, TextStyle, Window,
5 combine_highlights,
6};
7use language::BufferSnapshot;
8use multi_buffer::{Anchor, ToOffset};
9use settings::Settings;
10use std::ops::Range;
11use text::Rope;
12use theme::ThemeSettings;
13use ui::{
14 ActiveTheme, AnyElement, InteractiveElement, IntoElement, ParentElement, Pixels, SharedString,
15 StatefulInteractiveElement, Styled, StyledExt, div, relative,
16};
17
18// Language-specific settings may define quotes as "brackets", so filter them out separately.
19const QUOTE_PAIRS: [(&str, &str); 3] = [("'", "'"), ("\"", "\""), ("`", "`")];
20
21#[derive(Debug, Clone, Copy, PartialEq)]
22pub enum SignatureHelpHiddenBy {
23 AutoClose,
24 Escape,
25 Selection,
26}
27
28impl Editor {
29 pub fn toggle_auto_signature_help_menu(
30 &mut self,
31 _: &ToggleAutoSignatureHelp,
32 window: &mut Window,
33 cx: &mut Context<Self>,
34 ) {
35 self.auto_signature_help = self
36 .auto_signature_help
37 .map(|auto_signature_help| !auto_signature_help)
38 .or_else(|| Some(!EditorSettings::get_global(cx).auto_signature_help));
39 match self.auto_signature_help {
40 Some(auto_signature_help) if auto_signature_help => {
41 self.show_signature_help(&ShowSignatureHelp, window, cx);
42 }
43 Some(_) => {
44 self.hide_signature_help(cx, SignatureHelpHiddenBy::AutoClose);
45 }
46 None => {}
47 }
48 cx.notify();
49 }
50
51 pub(super) fn hide_signature_help(
52 &mut self,
53 cx: &mut Context<Self>,
54 signature_help_hidden_by: SignatureHelpHiddenBy,
55 ) -> bool {
56 if self.signature_help_state.is_shown() {
57 self.signature_help_state.kill_task();
58 self.signature_help_state.hide(signature_help_hidden_by);
59 cx.notify();
60 true
61 } else {
62 false
63 }
64 }
65
66 pub fn auto_signature_help_enabled(&self, cx: &App) -> bool {
67 if let Some(auto_signature_help) = self.auto_signature_help {
68 auto_signature_help
69 } else {
70 EditorSettings::get_global(cx).auto_signature_help
71 }
72 }
73
74 pub(super) fn should_open_signature_help_automatically(
75 &mut self,
76 old_cursor_position: &Anchor,
77 backspace_pressed: bool,
78
79 cx: &mut Context<Self>,
80 ) -> bool {
81 if !(self.signature_help_state.is_shown() || self.auto_signature_help_enabled(cx)) {
82 return false;
83 }
84 let newest_selection = self.selections.newest::<usize>(cx);
85 let head = newest_selection.head();
86
87 // There are two cases where the head and tail of a selection are different: selecting multiple ranges and using backspace.
88 // If we donβt exclude the backspace case, signature_help will blink every time backspace is pressed, so we need to prevent this.
89 if !newest_selection.is_empty() && !backspace_pressed && head != newest_selection.tail() {
90 self.signature_help_state
91 .hide(SignatureHelpHiddenBy::Selection);
92 return false;
93 }
94
95 let buffer_snapshot = self.buffer().read(cx).snapshot(cx);
96 let bracket_range = |position: usize| match (position, position + 1) {
97 (0, b) if b <= buffer_snapshot.len() => 0..b,
98 (0, b) => 0..b - 1,
99 (a, b) if b <= buffer_snapshot.len() => a - 1..b,
100 (a, b) => a - 1..b - 1,
101 };
102 let not_quote_like_brackets =
103 |buffer: &BufferSnapshot, start: Range<usize>, end: Range<usize>| {
104 let text_start = buffer.text_for_range(start).collect::<String>();
105 let text_end = buffer.text_for_range(end).collect::<String>();
106 QUOTE_PAIRS
107 .into_iter()
108 .all(|(start, end)| text_start != start && text_end != end)
109 };
110
111 let previous_position = old_cursor_position.to_offset(&buffer_snapshot);
112 let previous_brackets_range = bracket_range(previous_position);
113 let previous_brackets_surround = buffer_snapshot
114 .innermost_enclosing_bracket_ranges(
115 previous_brackets_range,
116 Some(¬_quote_like_brackets),
117 )
118 .filter(|(start_bracket_range, end_bracket_range)| {
119 start_bracket_range.start != previous_position
120 && end_bracket_range.end != previous_position
121 });
122 let current_brackets_range = bracket_range(head);
123 let current_brackets_surround = buffer_snapshot
124 .innermost_enclosing_bracket_ranges(
125 current_brackets_range,
126 Some(¬_quote_like_brackets),
127 )
128 .filter(|(start_bracket_range, end_bracket_range)| {
129 start_bracket_range.start != head && end_bracket_range.end != head
130 });
131
132 match (previous_brackets_surround, current_brackets_surround) {
133 (None, None) => {
134 self.signature_help_state
135 .hide(SignatureHelpHiddenBy::AutoClose);
136 false
137 }
138 (Some(_), None) => {
139 self.signature_help_state
140 .hide(SignatureHelpHiddenBy::AutoClose);
141 false
142 }
143 (None, Some(_)) => true,
144 (Some(previous), Some(current)) => {
145 let condition = self.signature_help_state.hidden_by_selection()
146 || previous != current
147 || (previous == current && self.signature_help_state.is_shown());
148 if !condition {
149 self.signature_help_state
150 .hide(SignatureHelpHiddenBy::AutoClose);
151 }
152 condition
153 }
154 }
155 }
156
157 pub fn show_signature_help(
158 &mut self,
159 _: &ShowSignatureHelp,
160 window: &mut Window,
161 cx: &mut Context<Self>,
162 ) {
163 if self.pending_rename.is_some() || self.has_visible_completions_menu() {
164 return;
165 }
166
167 let position = self.selections.newest_anchor().head();
168 let Some((buffer, buffer_position)) =
169 self.buffer.read(cx).text_anchor_for_position(position, cx)
170 else {
171 return;
172 };
173 let Some(lsp_store) = self.project.as_ref().map(|p| p.read(cx).lsp_store()) else {
174 return;
175 };
176 let task = lsp_store.update(cx, |lsp_store, cx| {
177 lsp_store.signature_help(&buffer, buffer_position, cx)
178 });
179 let language = self.language_at(position, cx);
180
181 self.signature_help_state
182 .set_task(cx.spawn_in(window, async move |editor, cx| {
183 let signature_help = task.await;
184 editor
185 .update(cx, |editor, cx| {
186 let Some(mut signature_help) = signature_help.into_iter().next() else {
187 editor
188 .signature_help_state
189 .hide(SignatureHelpHiddenBy::AutoClose);
190 return;
191 };
192
193 if let Some(language) = language {
194 let text = Rope::from(signature_help.label.clone());
195 let highlights = language
196 .highlight_text(&text, 0..signature_help.label.len())
197 .into_iter()
198 .flat_map(|(range, highlight_id)| {
199 Some((range, highlight_id.style(&cx.theme().syntax())?))
200 });
201 signature_help.highlights =
202 combine_highlights(signature_help.highlights, highlights).collect()
203 }
204 let settings = ThemeSettings::get_global(cx);
205 let text_style = TextStyle {
206 color: cx.theme().colors().text,
207 font_family: settings.buffer_font.family.clone(),
208 font_fallbacks: settings.buffer_font.fallbacks.clone(),
209 font_size: settings.buffer_font_size(cx).into(),
210 font_weight: settings.buffer_font.weight,
211 line_height: relative(settings.buffer_line_height.value()),
212 ..Default::default()
213 };
214
215 let signature_help_popover = SignatureHelpPopover {
216 label: signature_help.label.into(),
217 highlights: signature_help.highlights,
218 style: text_style,
219 };
220 editor
221 .signature_help_state
222 .set_popover(signature_help_popover);
223 cx.notify();
224 })
225 .ok();
226 }));
227 }
228}
229
230#[derive(Default, Debug)]
231pub struct SignatureHelpState {
232 task: Option<Task<()>>,
233 popover: Option<SignatureHelpPopover>,
234 hidden_by: Option<SignatureHelpHiddenBy>,
235 backspace_pressed: bool,
236}
237
238impl SignatureHelpState {
239 pub fn set_task(&mut self, task: Task<()>) {
240 self.task = Some(task);
241 self.hidden_by = None;
242 }
243
244 pub fn kill_task(&mut self) {
245 self.task = None;
246 }
247
248 #[cfg(test)]
249 pub fn popover(&self) -> Option<&SignatureHelpPopover> {
250 self.popover.as_ref()
251 }
252
253 pub fn popover_mut(&mut self) -> Option<&mut SignatureHelpPopover> {
254 self.popover.as_mut()
255 }
256
257 pub fn backspace_pressed(&self) -> bool {
258 self.backspace_pressed
259 }
260
261 pub fn set_backspace_pressed(&mut self, backspace_pressed: bool) {
262 self.backspace_pressed = backspace_pressed;
263 }
264
265 pub fn set_popover(&mut self, popover: SignatureHelpPopover) {
266 self.popover = Some(popover);
267 self.hidden_by = None;
268 }
269
270 pub fn hide(&mut self, hidden_by: SignatureHelpHiddenBy) {
271 if self.hidden_by.is_none() {
272 self.popover = None;
273 self.hidden_by = Some(hidden_by);
274 }
275 }
276
277 pub fn hidden_by_selection(&self) -> bool {
278 self.hidden_by == Some(SignatureHelpHiddenBy::Selection)
279 }
280
281 pub fn is_shown(&self) -> bool {
282 self.popover.is_some()
283 }
284}
285
286#[cfg(test)]
287impl SignatureHelpState {
288 pub fn task(&self) -> Option<&Task<()>> {
289 self.task.as_ref()
290 }
291}
292
293#[derive(Clone, Debug, PartialEq)]
294pub struct SignatureHelpPopover {
295 pub label: SharedString,
296 pub style: TextStyle,
297 pub highlights: Vec<(Range<usize>, HighlightStyle)>,
298}
299
300impl SignatureHelpPopover {
301 pub fn render(&mut self, max_size: Size<Pixels>, cx: &mut Context<Editor>) -> AnyElement {
302 div()
303 .id("signature_help_popover")
304 .elevation_2(cx)
305 .overflow_y_scroll()
306 .max_w(max_size.width)
307 .max_h(max_size.height)
308 .on_mouse_move(|_, _, cx| cx.stop_propagation())
309 .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
310 .child(
311 div().px_2().py_0p5().child(
312 StyledText::new(self.label.clone())
313 .with_default_highlights(&self.style, self.highlights.iter().cloned()),
314 ),
315 )
316 .into_any_element()
317 }
318}