Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T01:34:25.278Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

signature_help.rs

524 lines · 20.8 KB · rust
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(&not_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(&not_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 editor = cx.weak_entity();
386        let main_content = div()
387            .occlude()
388            .p_2()
389            .child(
390                div()
391                    .id("signature_help_container")
392                    .overflow_y_scroll()
393                    .max_w(max_size.width)
394                    .max_h(max_size.height)
395                    .track_scroll(&self.scroll_handle)
396                    .child(
397                        StyledText::new(signature.label.clone()).with_default_highlights(
398                            &self.style,
399                            signature.highlights.iter().cloned(),
400                        ),
401                    )
402                    .when_some(
403                        signature.parameter_documentation.clone(),
404                        |this, param_doc| {
405                            this.child(div().h_px().bg(cx.theme().colors().border_variant).my_1())
406                                .child(
407                                    MarkdownElement::new(
408                                        param_doc,
409                                        hover_markdown_style(window, cx),
410                                    )
411                                    .code_block_renderer(markdown::CodeBlockRenderer::Default {
412                                        copy_button_visibility: CopyButtonVisibility::Hidden,
413                                        wrap_button_visibility:
414                                            markdown::WrapButtonVisibility::Hidden,
415                                        border: false,
416                                    })
417                                    .on_url_click({
418                                        let editor = editor.clone();
419                                        move |link, window, cx| {
420                                            open_markdown_url(
421                                                editor
422                                                    .read_with(cx, |editor, _| editor.workspace())
423                                                    .ok()
424                                                    .flatten(),
425                                                link,
426                                                window,
427                                                cx,
428                                            )
429                                        }
430                                    }),
431                                )
432                        },
433                    )
434                    .when_some(signature.documentation.clone(), |this, description| {
435                        this.child(div().h_px().bg(cx.theme().colors().border_variant).my_1())
436                            .child(
437                                MarkdownElement::new(description, hover_markdown_style(window, cx))
438                                    .code_block_renderer(markdown::CodeBlockRenderer::Default {
439                                        copy_button_visibility: CopyButtonVisibility::Hidden,
440                                        wrap_button_visibility:
441                                            markdown::WrapButtonVisibility::Hidden,
442                                        border: false,
443                                    })
444                                    .on_url_click(move |link, window, cx| {
445                                        open_markdown_url(
446                                            editor
447                                                .read_with(cx, |editor, _| editor.workspace())
448                                                .ok()
449                                                .flatten(),
450                                            link,
451                                            window,
452                                            cx,
453                                        )
454                                    }),
455                            )
456                    }),
457            )
458            .vertical_scrollbar_for(&self.scroll_handle, window, cx);
459
460        let controls = if self.signatures.len() > 1 {
461            let prev_button = IconButton::new("signature_help_prev", IconName::ChevronUp)
462                .shape(IconButtonShape::Square)
463                .style(ButtonStyle::Subtle)
464                .icon_size(IconSize::Small)
465                .tooltip(move |_window, cx| {
466                    ui::Tooltip::for_action("Previous Signature", &crate::SignatureHelpPrevious, cx)
467                })
468                .on_click(cx.listener(|editor, _, window, cx| {
469                    editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
470                }));
471
472            let next_button = IconButton::new("signature_help_next", IconName::ChevronDown)
473                .shape(IconButtonShape::Square)
474                .style(ButtonStyle::Subtle)
475                .icon_size(IconSize::Small)
476                .tooltip(move |_window, cx| {
477                    ui::Tooltip::for_action("Next Signature", &crate::SignatureHelpNext, cx)
478                })
479                .on_click(cx.listener(|editor, _, window, cx| {
480                    editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
481                }));
482
483            let page = Label::new(format!(
484                "{}/{}",
485                self.current_signature + 1,
486                self.signatures.len()
487            ))
488            .size(LabelSize::Small);
489
490            Some(
491                div()
492                    .flex()
493                    .flex_col()
494                    .items_center()
495                    .gap_0p5()
496                    .px_0p5()
497                    .py_0p5()
498                    .children([
499                        prev_button.into_any_element(),
500                        div().child(page).into_any_element(),
501                        next_button.into_any_element(),
502                    ])
503                    .into_any_element(),
504            )
505        } else {
506            None
507        };
508        div()
509            .elevation_2(cx)
510            .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
511            .on_mouse_move(|_, _, cx| cx.stop_propagation())
512            .flex()
513            .flex_row()
514            .when_some(controls, |this, controls| {
515                this.children(vec![
516                    div().flex().items_end().child(controls),
517                    div().w_px().bg(cx.theme().colors().border_variant),
518                ])
519            })
520            .child(main_content)
521            .into_any_element()
522    }
523}
524
Served at tenant.openagents/omega Member data and write actions are omitted.