lsp: Complete overloaded signature help implementation (#33199)

Shuhei Kadowaki , Fernando Tagawa , Claude , Kirill Bulatov , and Kirill Bulatov created

This PR revives zed-industries/zed#27818 and aims to complete the
partially implemented overloaded signature help feature.

The first commit is a rebase of zed-industries/zed#27818, and the
subsequent commit addresses all review feedback from the original PR.

Now the overloaded signature help works like


https://github.com/user-attachments/assets/e253c9a0-e3a5-4bfe-8003-eb75de41f672

Closes #21493

Release Notes:

- Implemented signature help for overloaded items. Additionally, added a
support for rendering signature help documentation.

---------

Co-authored-by: Fernando Tagawa <tagawafernando@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Kirill Bulatov <mail4score@gmail.com>
Co-authored-by: Kirill Bulatov <kirill@zed.dev>

Change summary

Cargo.lock                                       |   1 
assets/keymaps/default-linux.json                |   7 
assets/keymaps/default-macos.json                |   7 
assets/keymaps/linux/emacs.json                  |   7 
assets/keymaps/macos/emacs.json                  |   7 
assets/keymaps/vim.json                          |   7 
crates/editor/src/actions.rs                     |   2 
crates/editor/src/editor.rs                      |  36 +
crates/editor/src/editor_tests.rs                | 146 ++++++
crates/editor/src/element.rs                     |   4 
crates/editor/src/signature_help.rs              | 268 ++++++++++--
crates/markdown/src/markdown.rs                  |   4 
crates/project/Cargo.toml                        |   1 
crates/project/src/lsp_command.rs                |  25 
crates/project/src/lsp_command/signature_help.rs | 398 +++++++++++------
crates/project/src/lsp_store.rs                  |   1 
crates/theme/src/styles/accents.rs               |   2 
crates/theme/src/styles/colors.rs                |   2 
crates/theme/src/styles/players.rs               |   2 
crates/theme/src/styles/system.rs                |   2 
crates/theme/src/theme.rs                        |   2 
21 files changed, 722 insertions(+), 209 deletions(-)

Detailed changes

Cargo.lock πŸ”—

@@ -12258,6 +12258,7 @@ dependencies = [
  "language",
  "log",
  "lsp",
+ "markdown",
  "node_runtime",
  "parking_lot",
  "pathdiff",

assets/keymaps/default-linux.json πŸ”—

@@ -708,6 +708,13 @@
       "pagedown": "editor::ContextMenuLast"
     }
   },
+  {
+    "context": "Editor && showing_signature_help && !showing_completions",
+    "bindings": {
+      "up": "editor::SignatureHelpPrevious",
+      "down": "editor::SignatureHelpNext"
+    }
+  },
   // Custom bindings
   {
     "bindings": {

assets/keymaps/default-macos.json πŸ”—

@@ -773,6 +773,13 @@
       "pagedown": "editor::ContextMenuLast"
     }
   },
+  {
+    "context": "Editor && showing_signature_help && !showing_completions",
+    "bindings": {
+      "up": "editor::SignatureHelpPrevious",
+      "down": "editor::SignatureHelpNext"
+    }
+  },
   // Custom bindings
   {
     "use_key_equivalents": true,

assets/keymaps/linux/emacs.json πŸ”—

@@ -98,6 +98,13 @@
       "ctrl-n": "editor::ContextMenuNext"
     }
   },
+  {
+    "context": "Editor && showing_signature_help && !showing_completions",
+    "bindings": {
+      "ctrl-p": "editor::SignatureHelpPrevious",
+      "ctrl-n": "editor::SignatureHelpNext"
+    }
+  },
   {
     "context": "Workspace",
     "bindings": {

assets/keymaps/macos/emacs.json πŸ”—

@@ -98,6 +98,13 @@
       "ctrl-n": "editor::ContextMenuNext"
     }
   },
+  {
+    "context": "Editor && showing_signature_help && !showing_completions",
+    "bindings": {
+      "ctrl-p": "editor::SignatureHelpPrevious",
+      "ctrl-n": "editor::SignatureHelpNext"
+    }
+  },
   {
     "context": "Workspace",
     "bindings": {

assets/keymaps/vim.json πŸ”—

@@ -477,6 +477,13 @@
       "ctrl-n": "editor::ShowWordCompletions"
     }
   },
+  {
+    "context": "vim_mode == insert && showing_signature_help && !showing_completions",
+    "bindings": {
+      "ctrl-p": "editor::SignatureHelpPrevious",
+      "ctrl-n": "editor::SignatureHelpNext"
+    }
+  },
   {
     "context": "vim_mode == replace",
     "bindings": {

crates/editor/src/actions.rs πŸ”—

@@ -424,6 +424,8 @@ actions!(
         ShowSignatureHelp,
         ShowWordCompletions,
         ShuffleLines,
+        SignatureHelpNext,
+        SignatureHelpPrevious,
         SortLinesCaseInsensitive,
         SortLinesCaseSensitive,
         SplitSelectionIntoLines,

crates/editor/src/editor.rs πŸ”—

@@ -2362,6 +2362,10 @@ impl Editor {
             None => {}
         }
 
+        if self.signature_help_state.has_multiple_signatures() {
+            key_context.add("showing_signature_help");
+        }
+
         // Disable vim contexts when a sub-editor (e.g. rename/inline assistant) is focused.
         if !self.focus_handle(cx).contains_focused(window, cx)
             || (self.is_focused(window) || self.mouse_menu_is_focused(window, cx))
@@ -12582,6 +12586,38 @@ impl Editor {
         }
     }
 
+    pub fn signature_help_prev(
+        &mut self,
+        _: &SignatureHelpPrevious,
+        _: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        if let Some(popover) = self.signature_help_state.popover_mut() {
+            if popover.current_signature == 0 {
+                popover.current_signature = popover.signatures.len() - 1;
+            } else {
+                popover.current_signature -= 1;
+            }
+            cx.notify();
+        }
+    }
+
+    pub fn signature_help_next(
+        &mut self,
+        _: &SignatureHelpNext,
+        _: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        if let Some(popover) = self.signature_help_state.popover_mut() {
+            if popover.current_signature + 1 == popover.signatures.len() {
+                popover.current_signature = 0;
+            } else {
+                popover.current_signature += 1;
+            }
+            cx.notify();
+        }
+    }
+
     pub fn move_to_previous_word_start(
         &mut self,
         _: &MoveToPreviousWordStart,

crates/editor/src/editor_tests.rs πŸ”—

@@ -10866,9 +10866,10 @@ async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
 
     cx.editor(|editor, _, _| {
         let signature_help_state = editor.signature_help_state.popover().cloned();
+        let signature = signature_help_state.unwrap();
         assert_eq!(
-            signature_help_state.unwrap().label,
-            "param1: u8, param2: u8"
+            signature.signatures[signature.current_signature].label,
+            "fn sample(param1: u8, param2: u8)"
         );
     });
 }
@@ -11037,9 +11038,10 @@ async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestA
     cx.update_editor(|editor, _, _| {
         let signature_help_state = editor.signature_help_state.popover().cloned();
         assert!(signature_help_state.is_some());
+        let signature = signature_help_state.unwrap();
         assert_eq!(
-            signature_help_state.unwrap().label,
-            "param1: u8, param2: u8"
+            signature.signatures[signature.current_signature].label,
+            "fn sample(param1: u8, param2: u8)"
         );
         editor.signature_help_state = SignatureHelpState::default();
     });
@@ -11078,9 +11080,10 @@ async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestA
     cx.editor(|editor, _, _| {
         let signature_help_state = editor.signature_help_state.popover().cloned();
         assert!(signature_help_state.is_some());
+        let signature = signature_help_state.unwrap();
         assert_eq!(
-            signature_help_state.unwrap().label,
-            "param1: u8, param2: u8"
+            signature.signatures[signature.current_signature].label,
+            "fn sample(param1: u8, param2: u8)"
         );
     });
 }
@@ -11139,9 +11142,10 @@ async fn test_signature_help(cx: &mut TestAppContext) {
     cx.editor(|editor, _, _| {
         let signature_help_state = editor.signature_help_state.popover().cloned();
         assert!(signature_help_state.is_some());
+        let signature = signature_help_state.unwrap();
         assert_eq!(
-            signature_help_state.unwrap().label,
-            "param1: u8, param2: u8"
+            signature.signatures[signature.current_signature].label,
+            "fn sample(param1: u8, param2: u8)"
         );
     });
 
@@ -11349,6 +11353,132 @@ async fn test_signature_help(cx: &mut TestAppContext) {
         .await;
 }
 
+#[gpui::test]
+async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut cx = EditorLspTestContext::new_rust(
+        lsp::ServerCapabilities {
+            signature_help_provider: Some(lsp::SignatureHelpOptions {
+                ..Default::default()
+            }),
+            ..Default::default()
+        },
+        cx,
+    )
+    .await;
+
+    cx.set_state(indoc! {"
+        fn main() {
+            overloadedˇ
+        }
+    "});
+
+    cx.update_editor(|editor, window, cx| {
+        editor.handle_input("(", window, cx);
+        editor.show_signature_help(&ShowSignatureHelp, window, cx);
+    });
+
+    // Mock response with 3 signatures
+    let mocked_response = lsp::SignatureHelp {
+        signatures: vec![
+            lsp::SignatureInformation {
+                label: "fn overloaded(x: i32)".to_string(),
+                documentation: None,
+                parameters: Some(vec![lsp::ParameterInformation {
+                    label: lsp::ParameterLabel::Simple("x: i32".to_string()),
+                    documentation: None,
+                }]),
+                active_parameter: None,
+            },
+            lsp::SignatureInformation {
+                label: "fn overloaded(x: i32, y: i32)".to_string(),
+                documentation: None,
+                parameters: Some(vec![
+                    lsp::ParameterInformation {
+                        label: lsp::ParameterLabel::Simple("x: i32".to_string()),
+                        documentation: None,
+                    },
+                    lsp::ParameterInformation {
+                        label: lsp::ParameterLabel::Simple("y: i32".to_string()),
+                        documentation: None,
+                    },
+                ]),
+                active_parameter: None,
+            },
+            lsp::SignatureInformation {
+                label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
+                documentation: None,
+                parameters: Some(vec![
+                    lsp::ParameterInformation {
+                        label: lsp::ParameterLabel::Simple("x: i32".to_string()),
+                        documentation: None,
+                    },
+                    lsp::ParameterInformation {
+                        label: lsp::ParameterLabel::Simple("y: i32".to_string()),
+                        documentation: None,
+                    },
+                    lsp::ParameterInformation {
+                        label: lsp::ParameterLabel::Simple("z: i32".to_string()),
+                        documentation: None,
+                    },
+                ]),
+                active_parameter: None,
+            },
+        ],
+        active_signature: Some(1),
+        active_parameter: Some(0),
+    };
+    handle_signature_help_request(&mut cx, mocked_response).await;
+
+    cx.condition(|editor, _| editor.signature_help_state.is_shown())
+        .await;
+
+    // Verify we have multiple signatures and the right one is selected
+    cx.editor(|editor, _, _| {
+        let popover = editor.signature_help_state.popover().cloned().unwrap();
+        assert_eq!(popover.signatures.len(), 3);
+        // active_signature was 1, so that should be the current
+        assert_eq!(popover.current_signature, 1);
+        assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
+        assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
+        assert_eq!(
+            popover.signatures[2].label,
+            "fn overloaded(x: i32, y: i32, z: i32)"
+        );
+    });
+
+    // Test navigation functionality
+    cx.update_editor(|editor, window, cx| {
+        editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
+    });
+
+    cx.editor(|editor, _, _| {
+        let popover = editor.signature_help_state.popover().cloned().unwrap();
+        assert_eq!(popover.current_signature, 2);
+    });
+
+    // Test wrap around
+    cx.update_editor(|editor, window, cx| {
+        editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
+    });
+
+    cx.editor(|editor, _, _| {
+        let popover = editor.signature_help_state.popover().cloned().unwrap();
+        assert_eq!(popover.current_signature, 0);
+    });
+
+    // Test previous navigation
+    cx.update_editor(|editor, window, cx| {
+        editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
+    });
+
+    cx.editor(|editor, _, _| {
+        let popover = editor.signature_help_state.popover().cloned().unwrap();
+        assert_eq!(popover.current_signature, 2);
+    });
+}
+
 #[gpui::test]
 async fn test_completion_mode(cx: &mut TestAppContext) {
     init_test(cx, |_| {});

crates/editor/src/element.rs πŸ”—

@@ -546,6 +546,8 @@ impl EditorElement {
             }
         });
         register_action(editor, window, Editor::show_signature_help);
+        register_action(editor, window, Editor::signature_help_prev);
+        register_action(editor, window, Editor::signature_help_next);
         register_action(editor, window, Editor::next_edit_prediction);
         register_action(editor, window, Editor::previous_edit_prediction);
         register_action(editor, window, Editor::show_inline_completion);
@@ -4985,7 +4987,7 @@ impl EditorElement {
 
         let maybe_element = self.editor.update(cx, |editor, cx| {
             if let Some(popover) = editor.signature_help_state.popover_mut() {
-                let element = popover.render(max_size, cx);
+                let element = popover.render(max_size, window, cx);
                 Some(element)
             } else {
                 None

crates/editor/src/signature_help.rs πŸ”—

@@ -1,18 +1,22 @@
 use crate::actions::ShowSignatureHelp;
-use crate::{Editor, EditorSettings, ToggleAutoSignatureHelp};
+use crate::hover_popover::open_markdown_url;
+use crate::{Editor, EditorSettings, ToggleAutoSignatureHelp, hover_markdown_style};
 use gpui::{
-    App, Context, HighlightStyle, MouseButton, Size, StyledText, Task, TextStyle, Window,
-    combine_highlights,
+    App, Context, Div, Entity, HighlightStyle, MouseButton, ScrollHandle, Size, Stateful,
+    StyledText, Task, TextStyle, Window, combine_highlights,
 };
 use language::BufferSnapshot;
+use markdown::{Markdown, MarkdownElement};
 use multi_buffer::{Anchor, ToOffset};
 use settings::Settings;
 use std::ops::Range;
 use text::Rope;
 use theme::ThemeSettings;
 use ui::{
-    ActiveTheme, AnyElement, InteractiveElement, IntoElement, ParentElement, Pixels, SharedString,
-    StatefulInteractiveElement, Styled, StyledExt, div, relative,
+    ActiveTheme, AnyElement, ButtonCommon, ButtonStyle, Clickable, FluentBuilder, IconButton,
+    IconButtonShape, IconName, IconSize, InteractiveElement, IntoElement, Label, LabelCommon,
+    LabelSize, ParentElement, Pixels, Scrollbar, ScrollbarState, SharedString,
+    StatefulInteractiveElement, Styled, StyledExt, div, px, relative,
 };
 
 // Language-specific settings may define quotes as "brackets", so filter them out separately.
@@ -37,15 +41,14 @@ impl Editor {
             .map(|auto_signature_help| !auto_signature_help)
             .or_else(|| Some(!EditorSettings::get_global(cx).auto_signature_help));
         match self.auto_signature_help {
-            Some(auto_signature_help) if auto_signature_help => {
+            Some(true) => {
                 self.show_signature_help(&ShowSignatureHelp, window, cx);
             }
-            Some(_) => {
+            Some(false) => {
                 self.hide_signature_help(cx, SignatureHelpHiddenBy::AutoClose);
             }
             None => {}
         }
-        cx.notify();
     }
 
     pub(super) fn hide_signature_help(
@@ -54,7 +57,7 @@ impl Editor {
         signature_help_hidden_by: SignatureHelpHiddenBy,
     ) -> bool {
         if self.signature_help_state.is_shown() {
-            self.signature_help_state.kill_task();
+            self.signature_help_state.task = None;
             self.signature_help_state.hide(signature_help_hidden_by);
             cx.notify();
             true
@@ -187,31 +190,62 @@ impl Editor {
                         };
 
                         if let Some(language) = language {
-                            let text = Rope::from(signature_help.label.clone());
-                            let highlights = language
-                                .highlight_text(&text, 0..signature_help.label.len())
-                                .into_iter()
-                                .flat_map(|(range, highlight_id)| {
-                                    Some((range, highlight_id.style(&cx.theme().syntax())?))
-                                });
-                            signature_help.highlights =
-                                combine_highlights(signature_help.highlights, highlights).collect()
+                            for signature in &mut signature_help.signatures {
+                                let text = Rope::from(signature.label.to_string());
+                                let highlights = language
+                                    .highlight_text(&text, 0..signature.label.len())
+                                    .into_iter()
+                                    .flat_map(|(range, highlight_id)| {
+                                        Some((range, highlight_id.style(&cx.theme().syntax())?))
+                                    });
+                                signature.highlights =
+                                    combine_highlights(signature.highlights.clone(), highlights)
+                                        .collect();
+                            }
                         }
                         let settings = ThemeSettings::get_global(cx);
-                        let text_style = TextStyle {
+                        let style = TextStyle {
                             color: cx.theme().colors().text,
                             font_family: settings.buffer_font.family.clone(),
                             font_fallbacks: settings.buffer_font.fallbacks.clone(),
                             font_size: settings.buffer_font_size(cx).into(),
                             font_weight: settings.buffer_font.weight,
                             line_height: relative(settings.buffer_line_height.value()),
-                            ..Default::default()
+                            ..TextStyle::default()
                         };
+                        let scroll_handle = ScrollHandle::new();
+                        let signatures = signature_help
+                            .signatures
+                            .into_iter()
+                            .map(|s| SignatureHelp {
+                                label: s.label,
+                                documentation: s.documentation,
+                                highlights: s.highlights,
+                                active_parameter: s.active_parameter,
+                                parameter_documentation: s
+                                    .active_parameter
+                                    .and_then(|idx| s.parameters.get(idx))
+                                    .and_then(|param| param.documentation.clone()),
+                            })
+                            .collect::<Vec<_>>();
+
+                        if signatures.is_empty() {
+                            editor
+                                .signature_help_state
+                                .hide(SignatureHelpHiddenBy::AutoClose);
+                            return;
+                        }
+
+                        let current_signature = signature_help
+                            .active_signature
+                            .min(signatures.len().saturating_sub(1));
 
                         let signature_help_popover = SignatureHelpPopover {
-                            label: signature_help.label.into(),
-                            highlights: signature_help.highlights,
-                            style: text_style,
+                            scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
+                            style,
+                            signatures,
+                            current_signature,
+                            scroll_handle,
                         };
                         editor
                             .signature_help_state
@@ -231,15 +265,11 @@ pub struct SignatureHelpState {
 }
 
 impl SignatureHelpState {
-    pub fn set_task(&mut self, task: Task<()>) {
+    fn set_task(&mut self, task: Task<()>) {
         self.task = Some(task);
         self.hidden_by = None;
     }
 
-    pub fn kill_task(&mut self) {
-        self.task = None;
-    }
-
     #[cfg(test)]
     pub fn popover(&self) -> Option<&SignatureHelpPopover> {
         self.popover.as_ref()
@@ -249,25 +279,31 @@ impl SignatureHelpState {
         self.popover.as_mut()
     }
 
-    pub fn set_popover(&mut self, popover: SignatureHelpPopover) {
+    fn set_popover(&mut self, popover: SignatureHelpPopover) {
         self.popover = Some(popover);
         self.hidden_by = None;
     }
 
-    pub fn hide(&mut self, hidden_by: SignatureHelpHiddenBy) {
+    fn hide(&mut self, hidden_by: SignatureHelpHiddenBy) {
         if self.hidden_by.is_none() {
             self.popover = None;
             self.hidden_by = Some(hidden_by);
         }
     }
 
-    pub fn hidden_by_selection(&self) -> bool {
+    fn hidden_by_selection(&self) -> bool {
         self.hidden_by == Some(SignatureHelpHiddenBy::Selection)
     }
 
     pub fn is_shown(&self) -> bool {
         self.popover.is_some()
     }
+
+    pub fn has_multiple_signatures(&self) -> bool {
+        self.popover
+            .as_ref()
+            .is_some_and(|popover| popover.signatures.len() > 1)
+    }
 }
 
 #[cfg(test)]
@@ -278,28 +314,170 @@ impl SignatureHelpState {
 }
 
 #[derive(Clone, Debug, PartialEq)]
+pub struct SignatureHelp {
+    pub(crate) label: SharedString,
+    documentation: Option<Entity<Markdown>>,
+    highlights: Vec<(Range<usize>, HighlightStyle)>,
+    active_parameter: Option<usize>,
+    parameter_documentation: Option<Entity<Markdown>>,
+}
+
+#[derive(Clone, Debug)]
 pub struct SignatureHelpPopover {
-    pub label: SharedString,
     pub style: TextStyle,
-    pub highlights: Vec<(Range<usize>, HighlightStyle)>,
+    pub signatures: Vec<SignatureHelp>,
+    pub current_signature: usize,
+    scroll_handle: ScrollHandle,
+    scrollbar_state: ScrollbarState,
 }
 
 impl SignatureHelpPopover {
-    pub fn render(&mut self, max_size: Size<Pixels>, cx: &mut Context<Editor>) -> AnyElement {
+    pub fn render(
+        &mut self,
+        max_size: Size<Pixels>,
+        window: &mut Window,
+        cx: &mut Context<Editor>,
+    ) -> AnyElement {
+        let Some(signature) = self.signatures.get(self.current_signature) else {
+            return div().into_any_element();
+        };
+
+        let main_content = div()
+            .occlude()
+            .p_2()
+            .child(
+                div()
+                    .id("signature_help_container")
+                    .overflow_y_scroll()
+                    .max_w(max_size.width)
+                    .max_h(max_size.height)
+                    .track_scroll(&self.scroll_handle)
+                    .child(
+                        StyledText::new(signature.label.clone()).with_default_highlights(
+                            &self.style,
+                            signature.highlights.iter().cloned(),
+                        ),
+                    )
+                    .when_some(
+                        signature.parameter_documentation.clone(),
+                        |this, param_doc| {
+                            this.child(div().h_px().bg(cx.theme().colors().border_variant).my_1())
+                                .child(
+                                    MarkdownElement::new(
+                                        param_doc,
+                                        hover_markdown_style(window, cx),
+                                    )
+                                    .code_block_renderer(markdown::CodeBlockRenderer::Default {
+                                        copy_button: false,
+                                        border: false,
+                                        copy_button_on_hover: false,
+                                    })
+                                    .on_url_click(open_markdown_url),
+                                )
+                        },
+                    )
+                    .when_some(signature.documentation.clone(), |this, description| {
+                        this.child(div().h_px().bg(cx.theme().colors().border_variant).my_1())
+                            .child(
+                                MarkdownElement::new(description, hover_markdown_style(window, cx))
+                                    .code_block_renderer(markdown::CodeBlockRenderer::Default {
+                                        copy_button: false,
+                                        border: false,
+                                        copy_button_on_hover: false,
+                                    })
+                                    .on_url_click(open_markdown_url),
+                            )
+                    }),
+            )
+            .child(self.render_vertical_scrollbar(cx));
+        let controls = if self.signatures.len() > 1 {
+            let prev_button = IconButton::new("signature_help_prev", IconName::ChevronUp)
+                .shape(IconButtonShape::Square)
+                .style(ButtonStyle::Subtle)
+                .icon_size(IconSize::Small)
+                .tooltip(move |window, cx| {
+                    ui::Tooltip::for_action(
+                        "Previous Signature",
+                        &crate::SignatureHelpPrevious,
+                        window,
+                        cx,
+                    )
+                })
+                .on_click(cx.listener(|editor, _, window, cx| {
+                    editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
+                }));
+
+            let next_button = IconButton::new("signature_help_next", IconName::ChevronDown)
+                .shape(IconButtonShape::Square)
+                .style(ButtonStyle::Subtle)
+                .icon_size(IconSize::Small)
+                .tooltip(move |window, cx| {
+                    ui::Tooltip::for_action("Next Signature", &crate::SignatureHelpNext, window, cx)
+                })
+                .on_click(cx.listener(|editor, _, window, cx| {
+                    editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
+                }));
+
+            let page = Label::new(format!(
+                "{}/{}",
+                self.current_signature + 1,
+                self.signatures.len()
+            ))
+            .size(LabelSize::Small);
+
+            Some(
+                div()
+                    .flex()
+                    .flex_col()
+                    .items_center()
+                    .gap_0p5()
+                    .px_0p5()
+                    .py_0p5()
+                    .children([
+                        prev_button.into_any_element(),
+                        div().child(page).into_any_element(),
+                        next_button.into_any_element(),
+                    ])
+                    .into_any_element(),
+            )
+        } else {
+            None
+        };
         div()
-            .id("signature_help_popover")
             .elevation_2(cx)
-            .overflow_y_scroll()
-            .max_w(max_size.width)
-            .max_h(max_size.height)
-            .on_mouse_move(|_, _, cx| cx.stop_propagation())
             .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
-            .child(
-                div().px_2().py_0p5().child(
-                    StyledText::new(self.label.clone())
-                        .with_default_highlights(&self.style, self.highlights.iter().cloned()),
-                ),
-            )
+            .on_mouse_move(|_, _, cx| cx.stop_propagation())
+            .flex()
+            .flex_row()
+            .when_some(controls, |this, controls| {
+                this.children(vec![
+                    div().flex().items_end().child(controls),
+                    div().w_px().bg(cx.theme().colors().border_variant),
+                ])
+            })
+            .child(main_content)
             .into_any_element()
     }
+
+    fn render_vertical_scrollbar(&self, cx: &mut Context<Editor>) -> Stateful<Div> {
+        div()
+            .occlude()
+            .id("signature_help_scrollbar")
+            .on_mouse_move(cx.listener(|_, _, _, cx| {
+                cx.notify();
+                cx.stop_propagation()
+            }))
+            .on_hover(|_, _, cx| cx.stop_propagation())
+            .on_any_mouse_down(|_, _, cx| cx.stop_propagation())
+            .on_mouse_up(MouseButton::Left, |_, _, cx| cx.stop_propagation())
+            .on_scroll_wheel(cx.listener(|_, _, _, cx| cx.notify()))
+            .h_full()
+            .absolute()
+            .right_1()
+            .top_1()
+            .bottom_1()
+            .w(px(12.))
+            .cursor_default()
+            .children(Scrollbar::vertical(self.scrollbar_state.clone()))
+    }
 }

crates/markdown/src/markdown.rs πŸ”—

@@ -421,7 +421,7 @@ impl Selection {
     }
 }
 
-#[derive(Clone, Default)]
+#[derive(Debug, Clone, Default)]
 pub struct ParsedMarkdown {
     pub source: SharedString,
     pub events: Arc<[(Range<usize>, MarkdownEvent)]>,
@@ -1672,7 +1672,7 @@ struct RenderedText {
     links: Rc<[RenderedLink]>,
 }
 
-#[derive(Clone, Eq, PartialEq)]
+#[derive(Debug, Clone, Eq, PartialEq)]
 struct RenderedLink {
     source_range: Range<usize>,
     destination_url: SharedString,

crates/project/Cargo.toml πŸ”—

@@ -54,6 +54,7 @@ indexmap.workspace = true
 language.workspace = true
 log.workspace = true
 lsp.workspace = true
+markdown.workspace = true
 node_runtime.workspace = true
 parking_lot.workspace = true
 pathdiff.workspace = true

crates/project/src/lsp_command.rs πŸ”—

@@ -1846,12 +1846,15 @@ impl LspCommand for GetSignatureHelp {
     async fn response_from_lsp(
         self,
         message: Option<lsp::SignatureHelp>,
-        _: Entity<LspStore>,
+        lsp_store: Entity<LspStore>,
         _: Entity<Buffer>,
         _: LanguageServerId,
-        _: AsyncApp,
+        cx: AsyncApp,
     ) -> Result<Self::Response> {
-        Ok(message.and_then(SignatureHelp::new))
+        let Some(message) = message else {
+            return Ok(None);
+        };
+        cx.update(|cx| SignatureHelp::new(message, Some(lsp_store.read(cx).languages.clone()), cx))
     }
 
     fn to_proto(&self, project_id: u64, buffer: &Buffer) -> Self::ProtoRequest {
@@ -1902,14 +1905,18 @@ impl LspCommand for GetSignatureHelp {
     async fn response_from_proto(
         self,
         response: proto::GetSignatureHelpResponse,
-        _: Entity<LspStore>,
+        lsp_store: Entity<LspStore>,
         _: Entity<Buffer>,
-        _: AsyncApp,
+        cx: AsyncApp,
     ) -> Result<Self::Response> {
-        Ok(response
-            .signature_help
-            .map(proto_to_lsp_signature)
-            .and_then(SignatureHelp::new))
+        cx.update(|cx| {
+            response
+                .signature_help
+                .map(proto_to_lsp_signature)
+                .and_then(|signature| {
+                    SignatureHelp::new(signature, Some(lsp_store.read(cx).languages.clone()), cx)
+                })
+        })
     }
 
     fn buffer_id_from_proto(message: &Self::ProtoRequest) -> Result<BufferId> {

crates/project/src/lsp_command/signature_help.rs πŸ”—

@@ -1,94 +1,143 @@
-use std::ops::Range;
+use std::{ops::Range, sync::Arc};
 
-use gpui::{FontStyle, FontWeight, HighlightStyle};
+use gpui::{App, AppContext, Entity, FontWeight, HighlightStyle, SharedString};
+use language::LanguageRegistry;
+use markdown::Markdown;
 use rpc::proto::{self, documentation};
 
 #[derive(Debug)]
 pub struct SignatureHelp {
-    pub label: String,
-    pub highlights: Vec<(Range<usize>, HighlightStyle)>,
+    pub active_signature: usize,
+    pub signatures: Vec<SignatureHelpData>,
     pub(super) original_data: lsp::SignatureHelp,
 }
 
-impl SignatureHelp {
-    pub fn new(help: lsp::SignatureHelp) -> Option<Self> {
-        let function_options_count = help.signatures.len();
-
-        let signature_information = help
-            .active_signature
-            .and_then(|active_signature| help.signatures.get(active_signature as usize))
-            .or_else(|| help.signatures.first())?;
-
-        let str_for_join = ", ";
-        let parameter_length = signature_information
-            .parameters
-            .as_ref()
-            .map_or(0, |parameters| parameters.len());
-        let mut highlight_start = 0;
-        let (strings, mut highlights): (Vec<_>, Vec<_>) = signature_information
-            .parameters
-            .as_ref()?
-            .iter()
-            .enumerate()
-            .map(|(i, parameter_information)| {
-                let label = match parameter_information.label.clone() {
-                    lsp::ParameterLabel::Simple(string) => string,
-                    lsp::ParameterLabel::LabelOffsets(offset) => signature_information
-                        .label
-                        .chars()
-                        .skip(offset[0] as usize)
-                        .take((offset[1] - offset[0]) as usize)
-                        .collect::<String>(),
-                };
-                let label_length = label.len();
-
-                let highlights = help.active_parameter.and_then(|active_parameter| {
-                    if i == active_parameter as usize {
-                        Some((
-                            highlight_start..(highlight_start + label_length),
-                            HighlightStyle {
-                                font_weight: Some(FontWeight::EXTRA_BOLD),
-                                ..Default::default()
-                            },
-                        ))
-                    } else {
-                        None
-                    }
-                });
+#[derive(Debug, Clone)]
+pub struct SignatureHelpData {
+    pub label: SharedString,
+    pub documentation: Option<Entity<Markdown>>,
+    pub highlights: Vec<(Range<usize>, HighlightStyle)>,
+    pub active_parameter: Option<usize>,
+    pub parameters: Vec<ParameterInfo>,
+}
+
+#[derive(Debug, Clone)]
+pub struct ParameterInfo {
+    pub label_range: Option<Range<usize>>,
+    pub documentation: Option<Entity<Markdown>>,
+}
 
-                if i != parameter_length {
-                    highlight_start += label_length + str_for_join.len();
+impl SignatureHelp {
+    pub fn new(
+        help: lsp::SignatureHelp,
+        language_registry: Option<Arc<LanguageRegistry>>,
+        cx: &mut App,
+    ) -> Option<Self> {
+        if help.signatures.is_empty() {
+            return None;
+        }
+        let active_signature = help.active_signature.unwrap_or(0) as usize;
+        let mut signatures = Vec::<SignatureHelpData>::with_capacity(help.signatures.capacity());
+        for signature in &help.signatures {
+            let active_parameter = signature
+                .active_parameter
+                .unwrap_or_else(|| help.active_parameter.unwrap_or(0))
+                as usize;
+            let mut highlights = Vec::new();
+            let mut parameter_infos = Vec::new();
+
+            if let Some(parameters) = &signature.parameters {
+                for (index, parameter) in parameters.iter().enumerate() {
+                    let label_range = match &parameter.label {
+                        lsp::ParameterLabel::LabelOffsets(parameter_label_offsets) => {
+                            let range = *parameter_label_offsets.get(0)? as usize
+                                ..*parameter_label_offsets.get(1)? as usize;
+                            if index == active_parameter {
+                                highlights.push((
+                                    range.clone(),
+                                    HighlightStyle {
+                                        font_weight: Some(FontWeight::EXTRA_BOLD),
+                                        ..HighlightStyle::default()
+                                    },
+                                ));
+                            }
+                            Some(range)
+                        }
+                        lsp::ParameterLabel::Simple(parameter_label) => {
+                            if let Some(start) = signature.label.find(parameter_label) {
+                                let range = start..start + parameter_label.len();
+                                if index == active_parameter {
+                                    highlights.push((
+                                        range.clone(),
+                                        HighlightStyle {
+                                            font_weight: Some(FontWeight::EXTRA_BOLD),
+                                            ..HighlightStyle::default()
+                                        },
+                                    ));
+                                }
+                                Some(range)
+                            } else {
+                                None
+                            }
+                        }
+                    };
+
+                    let documentation = parameter
+                        .documentation
+                        .as_ref()
+                        .map(|doc| documentation_to_markdown(doc, language_registry.clone(), cx));
+
+                    parameter_infos.push(ParameterInfo {
+                        label_range,
+                        documentation,
+                    });
                 }
+            }
 
-                (label, highlights)
-            })
-            .unzip();
-
-        if strings.is_empty() {
-            None
-        } else {
-            let mut label = strings.join(str_for_join);
-
-            if function_options_count >= 2 {
-                let suffix = format!("(+{} overload)", function_options_count - 1);
-                let highlight_start = label.len() + 1;
-                highlights.push(Some((
-                    highlight_start..(highlight_start + suffix.len()),
-                    HighlightStyle {
-                        font_style: Some(FontStyle::Italic),
-                        ..Default::default()
-                    },
-                )));
-                label.push(' ');
-                label.push_str(&suffix);
-            };
+            let label = SharedString::from(signature.label.clone());
+            let documentation = signature
+                .documentation
+                .as_ref()
+                .map(|doc| documentation_to_markdown(doc, language_registry.clone(), cx));
 
-            Some(Self {
+            signatures.push(SignatureHelpData {
                 label,
-                highlights: highlights.into_iter().flatten().collect(),
-                original_data: help,
-            })
+                documentation,
+                highlights,
+                active_parameter: Some(active_parameter),
+                parameters: parameter_infos,
+            });
+        }
+        Some(Self {
+            signatures,
+            active_signature,
+            original_data: help,
+        })
+    }
+}
+
+fn documentation_to_markdown(
+    documentation: &lsp::Documentation,
+    language_registry: Option<Arc<LanguageRegistry>>,
+    cx: &mut App,
+) -> Entity<Markdown> {
+    match documentation {
+        lsp::Documentation::String(string) => {
+            cx.new(|cx| Markdown::new_text(SharedString::from(string), cx))
         }
+        lsp::Documentation::MarkupContent(markup) => match markup.kind {
+            lsp::MarkupKind::PlainText => {
+                cx.new(|cx| Markdown::new_text(SharedString::from(&markup.value), cx))
+            }
+            lsp::MarkupKind::Markdown => cx.new(|cx| {
+                Markdown::new(
+                    SharedString::from(&markup.value),
+                    language_registry,
+                    None,
+                    cx,
+                )
+            }),
+        },
     }
 }
 
@@ -206,7 +255,8 @@ fn proto_to_lsp_documentation(documentation: proto::Documentation) -> Option<lsp
 
 #[cfg(test)]
 mod tests {
-    use gpui::{FontStyle, FontWeight, HighlightStyle};
+    use gpui::{FontWeight, HighlightStyle, SharedString, TestAppContext};
+    use lsp::{Documentation, MarkupContent, MarkupKind};
 
     use crate::lsp_command::signature_help::SignatureHelp;
 
@@ -217,19 +267,14 @@ mod tests {
         }
     }
 
-    fn overload() -> HighlightStyle {
-        HighlightStyle {
-            font_style: Some(FontStyle::Italic),
-            ..Default::default()
-        }
-    }
-
-    #[test]
-    fn test_create_signature_help_markdown_string_1() {
+    #[gpui::test]
+    fn test_create_signature_help_markdown_string_1(cx: &mut TestAppContext) {
         let signature_help = lsp::SignatureHelp {
             signatures: vec![lsp::SignatureInformation {
                 label: "fn test(foo: u8, bar: &str)".to_string(),
-                documentation: None,
+                documentation: Some(Documentation::String(
+                    "This is a test documentation".to_string(),
+                )),
                 parameters: Some(vec![
                     lsp::ParameterInformation {
                         label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
@@ -245,26 +290,37 @@ mod tests {
             active_signature: Some(0),
             active_parameter: Some(0),
         };
-        let maybe_markdown = SignatureHelp::new(signature_help);
+        let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, cx));
         assert!(maybe_markdown.is_some());
 
         let markdown = maybe_markdown.unwrap();
-        let markdown = (markdown.label, markdown.highlights);
+        let signature = markdown.signatures[markdown.active_signature].clone();
+        let markdown = (signature.label, signature.highlights);
         assert_eq!(
             markdown,
             (
-                "foo: u8, bar: &str".to_string(),
-                vec![(0..7, current_parameter())]
+                SharedString::new("fn test(foo: u8, bar: &str)"),
+                vec![(8..15, current_parameter())]
             )
         );
+        assert_eq!(
+            signature
+                .documentation
+                .unwrap()
+                .update(cx, |documentation, _| documentation.source().to_owned()),
+            "This is a test documentation",
+        )
     }
 
-    #[test]
-    fn test_create_signature_help_markdown_string_2() {
+    #[gpui::test]
+    fn test_create_signature_help_markdown_string_2(cx: &mut TestAppContext) {
         let signature_help = lsp::SignatureHelp {
             signatures: vec![lsp::SignatureInformation {
                 label: "fn test(foo: u8, bar: &str)".to_string(),
-                documentation: None,
+                documentation: Some(Documentation::MarkupContent(MarkupContent {
+                    kind: MarkupKind::Markdown,
+                    value: "This is a test documentation".to_string(),
+                })),
                 parameters: Some(vec![
                     lsp::ParameterInformation {
                         label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
@@ -280,22 +336,30 @@ mod tests {
             active_signature: Some(0),
             active_parameter: Some(1),
         };
-        let maybe_markdown = SignatureHelp::new(signature_help);
+        let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, cx));
         assert!(maybe_markdown.is_some());
 
         let markdown = maybe_markdown.unwrap();
-        let markdown = (markdown.label, markdown.highlights);
+        let signature = markdown.signatures[markdown.active_signature].clone();
+        let markdown = (signature.label, signature.highlights);
         assert_eq!(
             markdown,
             (
-                "foo: u8, bar: &str".to_string(),
-                vec![(9..18, current_parameter())]
+                SharedString::new("fn test(foo: u8, bar: &str)"),
+                vec![(17..26, current_parameter())]
             )
         );
+        assert_eq!(
+            signature
+                .documentation
+                .unwrap()
+                .update(cx, |documentation, _| documentation.source().to_owned()),
+            "This is a test documentation",
+        )
     }
 
-    #[test]
-    fn test_create_signature_help_markdown_string_3() {
+    #[gpui::test]
+    fn test_create_signature_help_markdown_string_3(cx: &mut TestAppContext) {
         let signature_help = lsp::SignatureHelp {
             signatures: vec![
                 lsp::SignatureInformation {
@@ -332,22 +396,23 @@ mod tests {
             active_signature: Some(0),
             active_parameter: Some(0),
         };
-        let maybe_markdown = SignatureHelp::new(signature_help);
+        let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, cx));
         assert!(maybe_markdown.is_some());
 
         let markdown = maybe_markdown.unwrap();
-        let markdown = (markdown.label, markdown.highlights);
+        let signature = markdown.signatures[markdown.active_signature].clone();
+        let markdown = (signature.label, signature.highlights);
         assert_eq!(
             markdown,
             (
-                "foo: u8, bar: &str (+1 overload)".to_string(),
-                vec![(0..7, current_parameter()), (19..32, overload())]
+                SharedString::new("fn test1(foo: u8, bar: &str)"),
+                vec![(9..16, current_parameter())]
             )
         );
     }
 
-    #[test]
-    fn test_create_signature_help_markdown_string_4() {
+    #[gpui::test]
+    fn test_create_signature_help_markdown_string_4(cx: &mut TestAppContext) {
         let signature_help = lsp::SignatureHelp {
             signatures: vec![
                 lsp::SignatureInformation {
@@ -384,22 +449,23 @@ mod tests {
             active_signature: Some(1),
             active_parameter: Some(0),
         };
-        let maybe_markdown = SignatureHelp::new(signature_help);
+        let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, cx));
         assert!(maybe_markdown.is_some());
 
         let markdown = maybe_markdown.unwrap();
-        let markdown = (markdown.label, markdown.highlights);
+        let signature = markdown.signatures[markdown.active_signature].clone();
+        let markdown = (signature.label, signature.highlights);
         assert_eq!(
             markdown,
             (
-                "hoge: String, fuga: bool (+1 overload)".to_string(),
-                vec![(0..12, current_parameter()), (25..38, overload())]
+                SharedString::new("fn test2(hoge: String, fuga: bool)"),
+                vec![(9..21, current_parameter())]
             )
         );
     }
 
-    #[test]
-    fn test_create_signature_help_markdown_string_5() {
+    #[gpui::test]
+    fn test_create_signature_help_markdown_string_5(cx: &mut TestAppContext) {
         let signature_help = lsp::SignatureHelp {
             signatures: vec![
                 lsp::SignatureInformation {
@@ -436,22 +502,23 @@ mod tests {
             active_signature: Some(1),
             active_parameter: Some(1),
         };
-        let maybe_markdown = SignatureHelp::new(signature_help);
+        let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, cx));
         assert!(maybe_markdown.is_some());
 
         let markdown = maybe_markdown.unwrap();
-        let markdown = (markdown.label, markdown.highlights);
+        let signature = markdown.signatures[markdown.active_signature].clone();
+        let markdown = (signature.label, signature.highlights);
         assert_eq!(
             markdown,
             (
-                "hoge: String, fuga: bool (+1 overload)".to_string(),
-                vec![(14..24, current_parameter()), (25..38, overload())]
+                SharedString::new("fn test2(hoge: String, fuga: bool)"),
+                vec![(23..33, current_parameter())]
             )
         );
     }
 
-    #[test]
-    fn test_create_signature_help_markdown_string_6() {
+    #[gpui::test]
+    fn test_create_signature_help_markdown_string_6(cx: &mut TestAppContext) {
         let signature_help = lsp::SignatureHelp {
             signatures: vec![
                 lsp::SignatureInformation {
@@ -488,22 +555,23 @@ mod tests {
             active_signature: Some(1),
             active_parameter: None,
         };
-        let maybe_markdown = SignatureHelp::new(signature_help);
+        let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, cx));
         assert!(maybe_markdown.is_some());
 
         let markdown = maybe_markdown.unwrap();
-        let markdown = (markdown.label, markdown.highlights);
+        let signature = markdown.signatures[markdown.active_signature].clone();
+        let markdown = (signature.label, signature.highlights);
         assert_eq!(
             markdown,
             (
-                "hoge: String, fuga: bool (+1 overload)".to_string(),
-                vec![(25..38, overload())]
+                SharedString::new("fn test2(hoge: String, fuga: bool)"),
+                vec![(9..21, current_parameter())]
             )
         );
     }
 
-    #[test]
-    fn test_create_signature_help_markdown_string_7() {
+    #[gpui::test]
+    fn test_create_signature_help_markdown_string_7(cx: &mut TestAppContext) {
         let signature_help = lsp::SignatureHelp {
             signatures: vec![
                 lsp::SignatureInformation {
@@ -555,33 +623,34 @@ mod tests {
             active_signature: Some(2),
             active_parameter: Some(1),
         };
-        let maybe_markdown = SignatureHelp::new(signature_help);
+        let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, cx));
         assert!(maybe_markdown.is_some());
 
         let markdown = maybe_markdown.unwrap();
-        let markdown = (markdown.label, markdown.highlights);
+        let signature = markdown.signatures[markdown.active_signature].clone();
+        let markdown = (signature.label, signature.highlights);
         assert_eq!(
             markdown,
             (
-                "one: usize, two: u32 (+2 overload)".to_string(),
-                vec![(12..20, current_parameter()), (21..34, overload())]
+                SharedString::new("fn test3(one: usize, two: u32)"),
+                vec![(21..29, current_parameter())]
             )
         );
     }
 
-    #[test]
-    fn test_create_signature_help_markdown_string_8() {
+    #[gpui::test]
+    fn test_create_signature_help_markdown_string_8(cx: &mut TestAppContext) {
         let signature_help = lsp::SignatureHelp {
             signatures: vec![],
             active_signature: None,
             active_parameter: None,
         };
-        let maybe_markdown = SignatureHelp::new(signature_help);
+        let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, cx));
         assert!(maybe_markdown.is_none());
     }
 
-    #[test]
-    fn test_create_signature_help_markdown_string_9() {
+    #[gpui::test]
+    fn test_create_signature_help_markdown_string_9(cx: &mut TestAppContext) {
         let signature_help = lsp::SignatureHelp {
             signatures: vec![lsp::SignatureInformation {
                 label: "fn test(foo: u8, bar: &str)".to_string(),
@@ -601,17 +670,70 @@ mod tests {
             active_signature: Some(0),
             active_parameter: Some(0),
         };
-        let maybe_markdown = SignatureHelp::new(signature_help);
+        let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, cx));
         assert!(maybe_markdown.is_some());
 
         let markdown = maybe_markdown.unwrap();
-        let markdown = (markdown.label, markdown.highlights);
+        let signature = markdown.signatures[markdown.active_signature].clone();
+        let markdown = (signature.label, signature.highlights);
         assert_eq!(
             markdown,
             (
-                "foo: u8, bar: &str".to_string(),
-                vec![(0..7, current_parameter())]
+                SharedString::new("fn test(foo: u8, bar: &str)"),
+                vec![(8..15, current_parameter())]
             )
         );
     }
+
+    #[gpui::test]
+    fn test_parameter_documentation(cx: &mut TestAppContext) {
+        let signature_help = lsp::SignatureHelp {
+            signatures: vec![lsp::SignatureInformation {
+                label: "fn test(foo: u8, bar: &str)".to_string(),
+                documentation: Some(Documentation::String(
+                    "This is a test documentation".to_string(),
+                )),
+                parameters: Some(vec![
+                    lsp::ParameterInformation {
+                        label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
+                        documentation: Some(Documentation::String("The foo parameter".to_string())),
+                    },
+                    lsp::ParameterInformation {
+                        label: lsp::ParameterLabel::Simple("bar: &str".to_string()),
+                        documentation: Some(Documentation::String("The bar parameter".to_string())),
+                    },
+                ]),
+                active_parameter: None,
+            }],
+            active_signature: Some(0),
+            active_parameter: Some(0),
+        };
+        let maybe_signature_help = cx.update(|cx| SignatureHelp::new(signature_help, None, cx));
+        assert!(maybe_signature_help.is_some());
+
+        let signature_help = maybe_signature_help.unwrap();
+        let signature = &signature_help.signatures[signature_help.active_signature];
+
+        // Check that parameter documentation is extracted
+        assert_eq!(signature.parameters.len(), 2);
+        assert_eq!(
+            signature.parameters[0]
+                .documentation
+                .as_ref()
+                .unwrap()
+                .update(cx, |documentation, _| documentation.source().to_owned()),
+            "The foo parameter",
+        );
+        assert_eq!(
+            signature.parameters[1]
+                .documentation
+                .as_ref()
+                .unwrap()
+                .update(cx, |documentation, _| documentation.source().to_owned()),
+            "The bar parameter",
+        );
+
+        // Check that the active parameter is correct
+        assert_eq!(signature.active_parameter, Some(0));
+    }
 }

crates/project/src/lsp_store.rs πŸ”—

@@ -6504,7 +6504,6 @@ impl LspStore {
                     .await
                     .into_iter()
                     .flat_map(|(_, actions)| actions)
-                    .filter(|help| !help.label.is_empty())
                     .collect::<Vec<_>>()
             })
         }

crates/theme/src/styles/accents.rs πŸ”—

@@ -7,7 +7,7 @@ use crate::{
 };
 
 /// A collection of colors that are used to color indent aware lines in the editor.
-#[derive(Clone, Deserialize, PartialEq)]
+#[derive(Clone, Debug, Deserialize, PartialEq)]
 pub struct AccentColors(pub Vec<Hsla>);
 
 impl Default for AccentColors {

crates/theme/src/styles/colors.rs πŸ”—

@@ -535,7 +535,7 @@ pub fn all_theme_colors(cx: &mut App) -> Vec<(Hsla, SharedString)> {
         .collect()
 }
 
-#[derive(Refineable, Clone, PartialEq)]
+#[derive(Refineable, Clone, Debug, PartialEq)]
 pub struct ThemeStyles {
     /// The background appearance of the window.
     pub window_background_appearance: WindowBackgroundAppearance,

crates/theme/src/styles/players.rs πŸ”—

@@ -20,7 +20,7 @@ pub struct PlayerColor {
 ///
 /// The rest of the default colors crisscross back and forth on the
 /// color wheel so that the colors are as distinct as possible.
-#[derive(Clone, Deserialize, PartialEq)]
+#[derive(Clone, Debug, Deserialize, PartialEq)]
 pub struct PlayerColors(pub Vec<PlayerColor>);
 
 impl Default for PlayerColors {

crates/theme/src/styles/system.rs πŸ”—

@@ -2,7 +2,7 @@
 
 use gpui::{Hsla, hsla};
 
-#[derive(Clone, PartialEq)]
+#[derive(Clone, Debug, PartialEq)]
 pub struct SystemColors {
     pub transparent: Hsla,
     pub mac_os_traffic_light_red: Hsla,

crates/theme/src/theme.rs πŸ”—

@@ -268,7 +268,7 @@ pub fn refine_theme_family(theme_family_content: ThemeFamilyContent) -> ThemeFam
 }
 
 /// A theme is the primary mechanism for defining the appearance of the UI.
-#[derive(Clone, PartialEq)]
+#[derive(Clone, Debug, PartialEq)]
 pub struct Theme {
     /// The unique identifier for the theme.
     pub id: String,