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        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        if !newest_selection.is_empty() && head != newest_selection.tail() {
 86            self.signature_help_state
 87                .hide(SignatureHelpHiddenBy::Selection);
 88            return false;
 89        }
 90
 91        let buffer_snapshot = self.buffer().read(cx).snapshot(cx);
 92        let bracket_range = |position: usize| match (position, position + 1) {
 93            (0, b) if b <= buffer_snapshot.len() => 0..b,
 94            (0, b) => 0..b - 1,
 95            (a, b) if b <= buffer_snapshot.len() => a - 1..b,
 96            (a, b) => a - 1..b - 1,
 97        };
 98        let not_quote_like_brackets =
 99            |buffer: &BufferSnapshot, start: Range<usize>, end: Range<usize>| {
100                let text_start = buffer.text_for_range(start).collect::<String>();
101                let text_end = buffer.text_for_range(end).collect::<String>();
102                QUOTE_PAIRS
103                    .into_iter()
104                    .all(|(start, end)| text_start != start && text_end != end)
105            };
106
107        let previous_position = old_cursor_position.to_offset(&buffer_snapshot);
108        let previous_brackets_range = bracket_range(previous_position);
109        let previous_brackets_surround = buffer_snapshot
110            .innermost_enclosing_bracket_ranges(
111                previous_brackets_range,
112                Some(&not_quote_like_brackets),
113            )
114            .filter(|(start_bracket_range, end_bracket_range)| {
115                start_bracket_range.start != previous_position
116                    && end_bracket_range.end != previous_position
117            });
118        let current_brackets_range = bracket_range(head);
119        let current_brackets_surround = buffer_snapshot
120            .innermost_enclosing_bracket_ranges(
121                current_brackets_range,
122                Some(&not_quote_like_brackets),
123            )
124            .filter(|(start_bracket_range, end_bracket_range)| {
125                start_bracket_range.start != head && end_bracket_range.end != head
126            });
127
128        match (previous_brackets_surround, current_brackets_surround) {
129            (None, None) => {
130                self.signature_help_state
131                    .hide(SignatureHelpHiddenBy::AutoClose);
132                false
133            }
134            (Some(_), None) => {
135                self.signature_help_state
136                    .hide(SignatureHelpHiddenBy::AutoClose);
137                false
138            }
139            (None, Some(_)) => true,
140            (Some(previous), Some(current)) => {
141                let condition = self.signature_help_state.hidden_by_selection()
142                    || previous != current
143                    || (previous == current && self.signature_help_state.is_shown());
144                if !condition {
145                    self.signature_help_state
146                        .hide(SignatureHelpHiddenBy::AutoClose);
147                }
148                condition
149            }
150        }
151    }
152
153    pub fn show_signature_help(
154        &mut self,
155        _: &ShowSignatureHelp,
156        window: &mut Window,
157        cx: &mut Context<Self>,
158    ) {
159        if self.pending_rename.is_some() || self.has_visible_completions_menu() {
160            return;
161        }
162
163        let position = self.selections.newest_anchor().head();
164        let Some((buffer, buffer_position)) =
165            self.buffer.read(cx).text_anchor_for_position(position, cx)
166        else {
167            return;
168        };
169        let Some(lsp_store) = self.project.as_ref().map(|p| p.read(cx).lsp_store()) else {
170            return;
171        };
172        let task = lsp_store.update(cx, |lsp_store, cx| {
173            lsp_store.signature_help(&buffer, buffer_position, cx)
174        });
175        let language = self.language_at(position, cx);
176
177        self.signature_help_state
178            .set_task(cx.spawn_in(window, async move |editor, cx| {
179                let signature_help = task.await;
180                editor
181                    .update(cx, |editor, cx| {
182                        let Some(mut signature_help) = signature_help.into_iter().next() else {
183                            editor
184                                .signature_help_state
185                                .hide(SignatureHelpHiddenBy::AutoClose);
186                            return;
187                        };
188
189                        if let Some(language) = language {
190                            let text = Rope::from(signature_help.label.clone());
191                            let highlights = language
192                                .highlight_text(&text, 0..signature_help.label.len())
193                                .into_iter()
194                                .flat_map(|(range, highlight_id)| {
195                                    Some((range, highlight_id.style(&cx.theme().syntax())?))
196                                });
197                            signature_help.highlights =
198                                combine_highlights(signature_help.highlights, highlights).collect()
199                        }
200                        let settings = ThemeSettings::get_global(cx);
201                        let text_style = TextStyle {
202                            color: cx.theme().colors().text,
203                            font_family: settings.buffer_font.family.clone(),
204                            font_fallbacks: settings.buffer_font.fallbacks.clone(),
205                            font_size: settings.buffer_font_size(cx).into(),
206                            font_weight: settings.buffer_font.weight,
207                            line_height: relative(settings.buffer_line_height.value()),
208                            ..Default::default()
209                        };
210
211                        let signature_help_popover = SignatureHelpPopover {
212                            label: signature_help.label.into(),
213                            highlights: signature_help.highlights,
214                            style: text_style,
215                        };
216                        editor
217                            .signature_help_state
218                            .set_popover(signature_help_popover);
219                        cx.notify();
220                    })
221                    .ok();
222            }));
223    }
224}
225
226#[derive(Default, Debug)]
227pub struct SignatureHelpState {
228    task: Option<Task<()>>,
229    popover: Option<SignatureHelpPopover>,
230    hidden_by: Option<SignatureHelpHiddenBy>,
231}
232
233impl SignatureHelpState {
234    pub fn set_task(&mut self, task: Task<()>) {
235        self.task = Some(task);
236        self.hidden_by = None;
237    }
238
239    pub fn kill_task(&mut self) {
240        self.task = None;
241    }
242
243    #[cfg(test)]
244    pub fn popover(&self) -> Option<&SignatureHelpPopover> {
245        self.popover.as_ref()
246    }
247
248    pub fn popover_mut(&mut self) -> Option<&mut SignatureHelpPopover> {
249        self.popover.as_mut()
250    }
251
252    pub fn set_popover(&mut self, popover: SignatureHelpPopover) {
253        self.popover = Some(popover);
254        self.hidden_by = None;
255    }
256
257    pub fn hide(&mut self, hidden_by: SignatureHelpHiddenBy) {
258        if self.hidden_by.is_none() {
259            self.popover = None;
260            self.hidden_by = Some(hidden_by);
261        }
262    }
263
264    pub fn hidden_by_selection(&self) -> bool {
265        self.hidden_by == Some(SignatureHelpHiddenBy::Selection)
266    }
267
268    pub fn is_shown(&self) -> bool {
269        self.popover.is_some()
270    }
271}
272
273#[cfg(test)]
274impl SignatureHelpState {
275    pub fn task(&self) -> Option<&Task<()>> {
276        self.task.as_ref()
277    }
278}
279
280#[derive(Clone, Debug, PartialEq)]
281pub struct SignatureHelpPopover {
282    pub label: SharedString,
283    pub style: TextStyle,
284    pub highlights: Vec<(Range<usize>, HighlightStyle)>,
285}
286
287impl SignatureHelpPopover {
288    pub fn render(&mut self, max_size: Size<Pixels>, cx: &mut Context<Editor>) -> AnyElement {
289        div()
290            .id("signature_help_popover")
291            .elevation_2(cx)
292            .overflow_y_scroll()
293            .max_w(max_size.width)
294            .max_h(max_size.height)
295            .on_mouse_move(|_, _, cx| cx.stop_propagation())
296            .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
297            .child(
298                div().px_2().py_0p5().child(
299                    StyledText::new(self.label.clone())
300                        .with_default_highlights(&self.style, self.highlights.iter().cloned()),
301                ),
302            )
303            .into_any_element()
304    }
305}