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