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 self.show_signature_help_impl(false, window, cx);
171 }
172
173 pub(super) fn show_signature_help_auto(&mut self, window: &mut Window, cx: &mut Context<Self>) {
174 self.show_signature_help_impl(true, window, cx);
175 }
176
177 fn show_signature_help_impl(
178 &mut self,
179 use_delay: bool,
180 window: &mut Window,
181 cx: &mut Context<Self>,
182 ) {
183 if self.pending_rename.is_some() || self.has_visible_completions_menu() {
184 return;
185 }
186
187 // If there's an already running signature
188 // help task, this will drop it.
189 self.signature_help_state.task = None;
190
191 let position = self.selections.newest_anchor().head();
192 let Some((buffer, buffer_position)) =
193 self.buffer.read(cx).text_anchor_for_position(position, cx)
194 else {
195 return;
196 };
197 let Some(lsp_store) = self.project().map(|p| p.read(cx).lsp_store()) else {
198 return;
199 };
200 let lsp_task = lsp_store.update(cx, |lsp_store, cx| {
201 lsp_store.signature_help(&buffer, buffer_position, cx)
202 });
203 let language = self.language_at(position, cx);
204
205 let signature_help_delay_ms = if use_delay {
206 EditorSettings::get_global(cx).hover_popover_delay.0
207 } else {
208 0
209 };
210
211 self.signature_help_state
212 .set_task(cx.spawn_in(window, async move |editor, cx| {
213 if signature_help_delay_ms > 0 {
214 cx.background_executor()
215 .timer(Duration::from_millis(signature_help_delay_ms))
216 .await;
217 }
218
219 let signature_help = lsp_task.await;
220
221 editor
222 .update(cx, |editor, cx| {
223 let Some(mut signature_help) =
224 signature_help.unwrap_or_default().into_iter().next()
225 else {
226 editor
227 .signature_help_state
228 .hide(SignatureHelpHiddenBy::AutoClose);
229 return;
230 };
231
232 if let Some(language) = language {
233 for signature in &mut signature_help.signatures {
234 let text = Rope::from(signature.label.as_ref());
235 let highlights = language
236 .highlight_text(&text, 0..signature.label.len())
237 .into_iter()
238 .flat_map(|(range, highlight_id)| {
239 Some((range, highlight_id.style(cx.theme().syntax())?))
240 });
241 signature.highlights =
242 combine_highlights(signature.highlights.clone(), highlights)
243 .collect();
244 }
245 }
246 let settings = ThemeSettings::get_global(cx);
247 let style = TextStyle {
248 color: cx.theme().colors().text,
249 font_family: settings.buffer_font.family.clone(),
250 font_fallbacks: settings.buffer_font.fallbacks.clone(),
251 font_features: settings.buffer_font.features.clone(),
252 font_size: settings.buffer_font_size(cx).into(),
253 font_weight: settings.buffer_font.weight,
254 line_height: relative(settings.buffer_line_height.value()),
255 ..TextStyle::default()
256 };
257 let scroll_handle = ScrollHandle::new();
258 let signatures = signature_help
259 .signatures
260 .into_iter()
261 .map(|s| SignatureHelp {
262 label: s.label,
263 documentation: s.documentation,
264 highlights: s.highlights,
265 active_parameter: s.active_parameter,
266 parameter_documentation: s
267 .active_parameter
268 .and_then(|idx| s.parameters.get(idx))
269 .and_then(|param| param.documentation.clone()),
270 })
271 .collect::<Vec<_>>();
272
273 if signatures.is_empty() {
274 editor
275 .signature_help_state
276 .hide(SignatureHelpHiddenBy::AutoClose);
277 return;
278 }
279
280 let current_signature = signature_help
281 .active_signature
282 .min(signatures.len().saturating_sub(1));
283
284 let signature_help_popover = SignatureHelpPopover {
285 style,
286 signatures,
287 current_signature,
288 scroll_handle,
289 };
290 editor
291 .signature_help_state
292 .set_popover(signature_help_popover);
293 cx.notify();
294 })
295 .ok();
296 }));
297 }
298}
299
300#[derive(Default, Debug)]
301pub struct SignatureHelpState {
302 task: Option<Task<()>>,
303 popover: Option<SignatureHelpPopover>,
304 hidden_by: Option<SignatureHelpHiddenBy>,
305}
306
307impl SignatureHelpState {
308 fn set_task(&mut self, task: Task<()>) {
309 self.task = Some(task);
310 self.hidden_by = None;
311 }
312
313 #[cfg(test)]
314 pub fn popover(&self) -> Option<&SignatureHelpPopover> {
315 self.popover.as_ref()
316 }
317
318 pub fn popover_mut(&mut self) -> Option<&mut SignatureHelpPopover> {
319 self.popover.as_mut()
320 }
321
322 fn set_popover(&mut self, popover: SignatureHelpPopover) {
323 self.popover = Some(popover);
324 self.hidden_by = None;
325 }
326
327 fn hide(&mut self, hidden_by: SignatureHelpHiddenBy) {
328 if self.hidden_by.is_none() {
329 self.popover = None;
330 self.hidden_by = Some(hidden_by);
331 }
332 }
333
334 fn hidden_by_selection(&self) -> bool {
335 self.hidden_by == Some(SignatureHelpHiddenBy::Selection)
336 }
337
338 pub fn is_shown(&self) -> bool {
339 self.popover.is_some()
340 }
341
342 pub fn has_multiple_signatures(&self) -> bool {
343 self.popover
344 .as_ref()
345 .is_some_and(|popover| popover.signatures.len() > 1)
346 }
347}
348
349#[cfg(test)]
350impl SignatureHelpState {
351 pub fn task(&self) -> Option<&Task<()>> {
352 self.task.as_ref()
353 }
354}
355
356#[derive(Clone, Debug, PartialEq)]
357pub struct SignatureHelp {
358 pub(crate) label: SharedString,
359 documentation: Option<Entity<Markdown>>,
360 highlights: Vec<(Range<usize>, HighlightStyle)>,
361 active_parameter: Option<usize>,
362 parameter_documentation: Option<Entity<Markdown>>,
363}
364
365#[derive(Clone, Debug)]
366pub struct SignatureHelpPopover {
367 pub style: TextStyle,
368 pub signatures: Vec<SignatureHelp>,
369 pub current_signature: usize,
370 scroll_handle: ScrollHandle,
371}
372
373impl SignatureHelpPopover {
374 pub fn render(
375 &mut self,
376 max_size: Size<Pixels>,
377 window: &mut Window,
378 cx: &mut Context<Editor>,
379 ) -> AnyElement {
380 let Some(signature) = self.signatures.get(self.current_signature) else {
381 return div().into_any_element();
382 };
383
384 let main_content = div()
385 .occlude()
386 .p_2()
387 .child(
388 div()
389 .id("signature_help_container")
390 .overflow_y_scroll()
391 .max_w(max_size.width)
392 .max_h(max_size.height)
393 .track_scroll(&self.scroll_handle)
394 .child(
395 StyledText::new(signature.label.clone()).with_default_highlights(
396 &self.style,
397 signature.highlights.iter().cloned(),
398 ),
399 )
400 .when_some(
401 signature.parameter_documentation.clone(),
402 |this, param_doc| {
403 this.child(div().h_px().bg(cx.theme().colors().border_variant).my_1())
404 .child(
405 MarkdownElement::new(
406 param_doc,
407 hover_markdown_style(window, cx),
408 )
409 .code_block_renderer(markdown::CodeBlockRenderer::Default {
410 copy_button: false,
411 border: false,
412 copy_button_on_hover: false,
413 })
414 .on_url_click(open_markdown_url),
415 )
416 },
417 )
418 .when_some(signature.documentation.clone(), |this, description| {
419 this.child(div().h_px().bg(cx.theme().colors().border_variant).my_1())
420 .child(
421 MarkdownElement::new(description, hover_markdown_style(window, cx))
422 .code_block_renderer(markdown::CodeBlockRenderer::Default {
423 copy_button: false,
424 border: false,
425 copy_button_on_hover: false,
426 })
427 .on_url_click(open_markdown_url),
428 )
429 }),
430 )
431 .vertical_scrollbar_for(&self.scroll_handle, window, cx);
432
433 let controls = if self.signatures.len() > 1 {
434 let prev_button = IconButton::new("signature_help_prev", IconName::ChevronUp)
435 .shape(IconButtonShape::Square)
436 .style(ButtonStyle::Subtle)
437 .icon_size(IconSize::Small)
438 .tooltip(move |_window, cx| {
439 ui::Tooltip::for_action("Previous Signature", &crate::SignatureHelpPrevious, cx)
440 })
441 .on_click(cx.listener(|editor, _, window, cx| {
442 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
443 }));
444
445 let next_button = IconButton::new("signature_help_next", IconName::ChevronDown)
446 .shape(IconButtonShape::Square)
447 .style(ButtonStyle::Subtle)
448 .icon_size(IconSize::Small)
449 .tooltip(move |_window, cx| {
450 ui::Tooltip::for_action("Next Signature", &crate::SignatureHelpNext, cx)
451 })
452 .on_click(cx.listener(|editor, _, window, cx| {
453 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
454 }));
455
456 let page = Label::new(format!(
457 "{}/{}",
458 self.current_signature + 1,
459 self.signatures.len()
460 ))
461 .size(LabelSize::Small);
462
463 Some(
464 div()
465 .flex()
466 .flex_col()
467 .items_center()
468 .gap_0p5()
469 .px_0p5()
470 .py_0p5()
471 .children([
472 prev_button.into_any_element(),
473 div().child(page).into_any_element(),
474 next_button.into_any_element(),
475 ])
476 .into_any_element(),
477 )
478 } else {
479 None
480 };
481 div()
482 .elevation_2(cx)
483 .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
484 .on_mouse_move(|_, _, cx| cx.stop_propagation())
485 .flex()
486 .flex_row()
487 .when_some(controls, |this, controls| {
488 this.children(vec![
489 div().flex().items_end().child(controls),
490 div().w_px().bg(cx.theme().colors().border_variant),
491 ])
492 })
493 .child(main_content)
494 .into_any_element()
495 }
496}