signature_help.rs

  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(&not_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(&not_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}