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