1use crate::actions::ShowSignatureHelp;
2use crate::hover_popover::open_markdown_url;
3use crate::{Editor, EditorSettings, ToggleAutoSignatureHelp, hover_markdown_style};
4use gpui::{
5 App, Context, Div, Entity, HighlightStyle, MouseButton, ScrollHandle, Size, Stateful,
6 StyledText, Task, TextStyle, Window, combine_highlights,
7};
8use language::BufferSnapshot;
9use markdown::{Markdown, MarkdownElement};
10use multi_buffer::{Anchor, ToOffset};
11use settings::Settings;
12use std::ops::Range;
13use text::Rope;
14use theme::ThemeSettings;
15use ui::{
16 ActiveTheme, AnyElement, ButtonCommon, ButtonStyle, Clickable, FluentBuilder, IconButton,
17 IconButtonShape, IconName, IconSize, InteractiveElement, IntoElement, Label, LabelCommon,
18 LabelSize, ParentElement, Pixels, Scrollbar, ScrollbarState, SharedString,
19 StatefulInteractiveElement, Styled, StyledExt, div, px, relative,
20};
21
22// Language-specific settings may define quotes as "brackets", so filter them out separately.
23const QUOTE_PAIRS: [(&str, &str); 3] = [("'", "'"), ("\"", "\""), ("`", "`")];
24
25#[derive(Debug, Clone, Copy, PartialEq)]
26pub enum SignatureHelpHiddenBy {
27 AutoClose,
28 Escape,
29 Selection,
30}
31
32impl Editor {
33 pub fn toggle_auto_signature_help_menu(
34 &mut self,
35 _: &ToggleAutoSignatureHelp,
36 window: &mut Window,
37 cx: &mut Context<Self>,
38 ) {
39 self.auto_signature_help = self
40 .auto_signature_help
41 .map(|auto_signature_help| !auto_signature_help)
42 .or_else(|| Some(!EditorSettings::get_global(cx).auto_signature_help));
43 match self.auto_signature_help {
44 Some(true) => {
45 self.show_signature_help(&ShowSignatureHelp, window, cx);
46 }
47 Some(false) => {
48 self.hide_signature_help(cx, SignatureHelpHiddenBy::AutoClose);
49 }
50 None => {}
51 }
52 }
53
54 pub(super) fn hide_signature_help(
55 &mut self,
56 cx: &mut Context<Self>,
57 signature_help_hidden_by: SignatureHelpHiddenBy,
58 ) -> bool {
59 if self.signature_help_state.is_shown() {
60 self.signature_help_state.task = None;
61 self.signature_help_state.hide(signature_help_hidden_by);
62 cx.notify();
63 true
64 } else {
65 false
66 }
67 }
68
69 pub fn auto_signature_help_enabled(&self, cx: &App) -> bool {
70 if let Some(auto_signature_help) = self.auto_signature_help {
71 auto_signature_help
72 } else {
73 EditorSettings::get_global(cx).auto_signature_help
74 }
75 }
76
77 pub(super) fn should_open_signature_help_automatically(
78 &mut self,
79 old_cursor_position: &Anchor,
80 cx: &mut Context<Self>,
81 ) -> bool {
82 if !(self.signature_help_state.is_shown() || self.auto_signature_help_enabled(cx)) {
83 return false;
84 }
85 let newest_selection = self.selections.newest::<usize>(cx);
86 let head = newest_selection.head();
87
88 if !newest_selection.is_empty() && head != newest_selection.tail() {
89 self.signature_help_state
90 .hide(SignatureHelpHiddenBy::Selection);
91 return false;
92 }
93
94 let buffer_snapshot = self.buffer().read(cx).snapshot(cx);
95 let bracket_range = |position: usize| match (position, position + 1) {
96 (0, b) if b <= buffer_snapshot.len() => 0..b,
97 (0, b) => 0..b - 1,
98 (a, b) if b <= buffer_snapshot.len() => a - 1..b,
99 (a, b) => a - 1..b - 1,
100 };
101 let not_quote_like_brackets =
102 |buffer: &BufferSnapshot, start: Range<usize>, end: Range<usize>| {
103 let text_start = buffer.text_for_range(start).collect::<String>();
104 let text_end = buffer.text_for_range(end).collect::<String>();
105 QUOTE_PAIRS
106 .into_iter()
107 .all(|(start, end)| text_start != start && text_end != end)
108 };
109
110 let previous_position = old_cursor_position.to_offset(&buffer_snapshot);
111 let previous_brackets_range = bracket_range(previous_position);
112 let previous_brackets_surround = buffer_snapshot
113 .innermost_enclosing_bracket_ranges(
114 previous_brackets_range,
115 Some(¬_quote_like_brackets),
116 )
117 .filter(|(start_bracket_range, end_bracket_range)| {
118 start_bracket_range.start != previous_position
119 && end_bracket_range.end != previous_position
120 });
121 let current_brackets_range = bracket_range(head);
122 let current_brackets_surround = buffer_snapshot
123 .innermost_enclosing_bracket_ranges(
124 current_brackets_range,
125 Some(¬_quote_like_brackets),
126 )
127 .filter(|(start_bracket_range, end_bracket_range)| {
128 start_bracket_range.start != head && end_bracket_range.end != head
129 });
130
131 match (previous_brackets_surround, current_brackets_surround) {
132 (None, None) => {
133 self.signature_help_state
134 .hide(SignatureHelpHiddenBy::AutoClose);
135 false
136 }
137 (Some(_), None) => {
138 self.signature_help_state
139 .hide(SignatureHelpHiddenBy::AutoClose);
140 false
141 }
142 (None, Some(_)) => true,
143 (Some(previous), Some(current)) => {
144 let condition = self.signature_help_state.hidden_by_selection()
145 || previous != current
146 || (previous == current && self.signature_help_state.is_shown());
147 if !condition {
148 self.signature_help_state
149 .hide(SignatureHelpHiddenBy::AutoClose);
150 }
151 condition
152 }
153 }
154 }
155
156 pub fn show_signature_help(
157 &mut self,
158 _: &ShowSignatureHelp,
159 window: &mut Window,
160 cx: &mut Context<Self>,
161 ) {
162 if self.pending_rename.is_some() || self.has_visible_completions_menu() {
163 return;
164 }
165
166 let position = self.selections.newest_anchor().head();
167 let Some((buffer, buffer_position)) =
168 self.buffer.read(cx).text_anchor_for_position(position, cx)
169 else {
170 return;
171 };
172 let Some(lsp_store) = self.project().map(|p| p.read(cx).lsp_store()) else {
173 return;
174 };
175 let task = lsp_store.update(cx, |lsp_store, cx| {
176 lsp_store.signature_help(&buffer, buffer_position, cx)
177 });
178 let language = self.language_at(position, cx);
179
180 self.signature_help_state
181 .set_task(cx.spawn_in(window, async move |editor, cx| {
182 let signature_help = task.await;
183 editor
184 .update(cx, |editor, cx| {
185 let Some(mut signature_help) = signature_help.into_iter().next() else {
186 editor
187 .signature_help_state
188 .hide(SignatureHelpHiddenBy::AutoClose);
189 return;
190 };
191
192 if let Some(language) = language {
193 for signature in &mut signature_help.signatures {
194 let text = Rope::from(signature.label.as_ref());
195 let highlights = language
196 .highlight_text(&text, 0..signature.label.len())
197 .into_iter()
198 .flat_map(|(range, highlight_id)| {
199 Some((range, highlight_id.style(cx.theme().syntax())?))
200 });
201 signature.highlights =
202 combine_highlights(signature.highlights.clone(), highlights)
203 .collect();
204 }
205 }
206 let settings = ThemeSettings::get_global(cx);
207 let style = TextStyle {
208 color: cx.theme().colors().text,
209 font_family: settings.buffer_font.family.clone(),
210 font_fallbacks: settings.buffer_font.fallbacks.clone(),
211 font_size: settings.buffer_font_size(cx).into(),
212 font_weight: settings.buffer_font.weight,
213 line_height: relative(settings.buffer_line_height.value()),
214 ..TextStyle::default()
215 };
216 let scroll_handle = ScrollHandle::new();
217 let signatures = signature_help
218 .signatures
219 .into_iter()
220 .map(|s| SignatureHelp {
221 label: s.label,
222 documentation: s.documentation,
223 highlights: s.highlights,
224 active_parameter: s.active_parameter,
225 parameter_documentation: s
226 .active_parameter
227 .and_then(|idx| s.parameters.get(idx))
228 .and_then(|param| param.documentation.clone()),
229 })
230 .collect::<Vec<_>>();
231
232 if signatures.is_empty() {
233 editor
234 .signature_help_state
235 .hide(SignatureHelpHiddenBy::AutoClose);
236 return;
237 }
238
239 let current_signature = signature_help
240 .active_signature
241 .min(signatures.len().saturating_sub(1));
242
243 let signature_help_popover = SignatureHelpPopover {
244 scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
245 style,
246 signatures,
247 current_signature,
248 scroll_handle,
249 };
250 editor
251 .signature_help_state
252 .set_popover(signature_help_popover);
253 cx.notify();
254 })
255 .ok();
256 }));
257 }
258}
259
260#[derive(Default, Debug)]
261pub struct SignatureHelpState {
262 task: Option<Task<()>>,
263 popover: Option<SignatureHelpPopover>,
264 hidden_by: Option<SignatureHelpHiddenBy>,
265}
266
267impl SignatureHelpState {
268 fn set_task(&mut self, task: Task<()>) {
269 self.task = Some(task);
270 self.hidden_by = None;
271 }
272
273 #[cfg(test)]
274 pub fn popover(&self) -> Option<&SignatureHelpPopover> {
275 self.popover.as_ref()
276 }
277
278 pub fn popover_mut(&mut self) -> Option<&mut SignatureHelpPopover> {
279 self.popover.as_mut()
280 }
281
282 fn set_popover(&mut self, popover: SignatureHelpPopover) {
283 self.popover = Some(popover);
284 self.hidden_by = None;
285 }
286
287 fn hide(&mut self, hidden_by: SignatureHelpHiddenBy) {
288 if self.hidden_by.is_none() {
289 self.popover = None;
290 self.hidden_by = Some(hidden_by);
291 }
292 }
293
294 fn hidden_by_selection(&self) -> bool {
295 self.hidden_by == Some(SignatureHelpHiddenBy::Selection)
296 }
297
298 pub fn is_shown(&self) -> bool {
299 self.popover.is_some()
300 }
301
302 pub fn has_multiple_signatures(&self) -> bool {
303 self.popover
304 .as_ref()
305 .is_some_and(|popover| popover.signatures.len() > 1)
306 }
307}
308
309#[cfg(test)]
310impl SignatureHelpState {
311 pub fn task(&self) -> Option<&Task<()>> {
312 self.task.as_ref()
313 }
314}
315
316#[derive(Clone, Debug, PartialEq)]
317pub struct SignatureHelp {
318 pub(crate) label: SharedString,
319 documentation: Option<Entity<Markdown>>,
320 highlights: Vec<(Range<usize>, HighlightStyle)>,
321 active_parameter: Option<usize>,
322 parameter_documentation: Option<Entity<Markdown>>,
323}
324
325#[derive(Clone, Debug)]
326pub struct SignatureHelpPopover {
327 pub style: TextStyle,
328 pub signatures: Vec<SignatureHelp>,
329 pub current_signature: usize,
330 scroll_handle: ScrollHandle,
331 scrollbar_state: ScrollbarState,
332}
333
334impl SignatureHelpPopover {
335 pub fn render(
336 &mut self,
337 max_size: Size<Pixels>,
338 window: &mut Window,
339 cx: &mut Context<Editor>,
340 ) -> AnyElement {
341 let Some(signature) = self.signatures.get(self.current_signature) else {
342 return div().into_any_element();
343 };
344
345 let main_content = div()
346 .occlude()
347 .p_2()
348 .child(
349 div()
350 .id("signature_help_container")
351 .overflow_y_scroll()
352 .max_w(max_size.width)
353 .max_h(max_size.height)
354 .track_scroll(&self.scroll_handle)
355 .child(
356 StyledText::new(signature.label.clone()).with_default_highlights(
357 &self.style,
358 signature.highlights.iter().cloned(),
359 ),
360 )
361 .when_some(
362 signature.parameter_documentation.clone(),
363 |this, param_doc| {
364 this.child(div().h_px().bg(cx.theme().colors().border_variant).my_1())
365 .child(
366 MarkdownElement::new(
367 param_doc,
368 hover_markdown_style(window, cx),
369 )
370 .code_block_renderer(markdown::CodeBlockRenderer::Default {
371 copy_button: false,
372 border: false,
373 copy_button_on_hover: false,
374 })
375 .on_url_click(open_markdown_url),
376 )
377 },
378 )
379 .when_some(signature.documentation.clone(), |this, description| {
380 this.child(div().h_px().bg(cx.theme().colors().border_variant).my_1())
381 .child(
382 MarkdownElement::new(description, hover_markdown_style(window, cx))
383 .code_block_renderer(markdown::CodeBlockRenderer::Default {
384 copy_button: false,
385 border: false,
386 copy_button_on_hover: false,
387 })
388 .on_url_click(open_markdown_url),
389 )
390 }),
391 )
392 .child(self.render_vertical_scrollbar(cx));
393 let controls = if self.signatures.len() > 1 {
394 let prev_button = IconButton::new("signature_help_prev", IconName::ChevronUp)
395 .shape(IconButtonShape::Square)
396 .style(ButtonStyle::Subtle)
397 .icon_size(IconSize::Small)
398 .tooltip(move |window, cx| {
399 ui::Tooltip::for_action(
400 "Previous Signature",
401 &crate::SignatureHelpPrevious,
402 window,
403 cx,
404 )
405 })
406 .on_click(cx.listener(|editor, _, window, cx| {
407 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
408 }));
409
410 let next_button = IconButton::new("signature_help_next", IconName::ChevronDown)
411 .shape(IconButtonShape::Square)
412 .style(ButtonStyle::Subtle)
413 .icon_size(IconSize::Small)
414 .tooltip(move |window, cx| {
415 ui::Tooltip::for_action("Next Signature", &crate::SignatureHelpNext, window, cx)
416 })
417 .on_click(cx.listener(|editor, _, window, cx| {
418 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
419 }));
420
421 let page = Label::new(format!(
422 "{}/{}",
423 self.current_signature + 1,
424 self.signatures.len()
425 ))
426 .size(LabelSize::Small);
427
428 Some(
429 div()
430 .flex()
431 .flex_col()
432 .items_center()
433 .gap_0p5()
434 .px_0p5()
435 .py_0p5()
436 .children([
437 prev_button.into_any_element(),
438 div().child(page).into_any_element(),
439 next_button.into_any_element(),
440 ])
441 .into_any_element(),
442 )
443 } else {
444 None
445 };
446 div()
447 .elevation_2(cx)
448 .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
449 .on_mouse_move(|_, _, cx| cx.stop_propagation())
450 .flex()
451 .flex_row()
452 .when_some(controls, |this, controls| {
453 this.children(vec![
454 div().flex().items_end().child(controls),
455 div().w_px().bg(cx.theme().colors().border_variant),
456 ])
457 })
458 .child(main_content)
459 .into_any_element()
460 }
461
462 fn render_vertical_scrollbar(&self, cx: &mut Context<Editor>) -> Stateful<Div> {
463 div()
464 .occlude()
465 .id("signature_help_scrollbar")
466 .on_mouse_move(cx.listener(|_, _, _, cx| {
467 cx.notify();
468 cx.stop_propagation()
469 }))
470 .on_hover(|_, _, cx| cx.stop_propagation())
471 .on_any_mouse_down(|_, _, cx| cx.stop_propagation())
472 .on_mouse_up(MouseButton::Left, |_, _, cx| cx.stop_propagation())
473 .on_scroll_wheel(cx.listener(|_, _, _, cx| cx.notify()))
474 .h_full()
475 .absolute()
476 .right_1()
477 .top_1()
478 .bottom_1()
479 .w(px(12.))
480 .cursor_default()
481 .children(Scrollbar::vertical(self.scrollbar_state.clone()))
482 }
483}