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