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(¬_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(¬_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}