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