diff --git a/Cargo.lock b/Cargo.lock index 1aeb16ca39..d22f1e6795 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12258,6 +12258,7 @@ dependencies = [ "language", "log", "lsp", + "markdown", "node_runtime", "parking_lot", "pathdiff", diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 1d4363d604..f0ed829c1e 100644 --- a/assets/keymaps/default-linux.json +++ b/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": { diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index fa052bfe66..cd986e12e9 100644 --- a/assets/keymaps/default-macos.json +++ b/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, diff --git a/assets/keymaps/linux/emacs.json b/assets/keymaps/linux/emacs.json index 26482f66f5..0c633efabe 100755 --- a/assets/keymaps/linux/emacs.json +++ b/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": { diff --git a/assets/keymaps/macos/emacs.json b/assets/keymaps/macos/emacs.json index 26482f66f5..0c633efabe 100755 --- a/assets/keymaps/macos/emacs.json +++ b/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": { diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 59b21ae345..639f1cefad 100644 --- a/assets/keymaps/vim.json +++ b/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": { diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index 697bd6ef37..b6e7845908 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -424,6 +424,8 @@ actions!( ShowSignatureHelp, ShowWordCompletions, ShuffleLines, + SignatureHelpNext, + SignatureHelpPrevious, SortLinesCaseInsensitive, SortLinesCaseSensitive, SplitSelectionIntoLines, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index baa8e1a21c..aed3988af7 100644 --- a/crates/editor/src/editor.rs +++ b/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, + ) { + 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, + ) { + 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, diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index cefc2a0fc1..8615ff2a97 100644 --- a/crates/editor/src/editor_tests.rs +++ b/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, |_| {}); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 1c55ff2d09..a4b2ceb5de 100644 --- a/crates/editor/src/element.rs +++ b/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 diff --git a/crates/editor/src/signature_help.rs b/crates/editor/src/signature_help.rs index 9d69b10193..3447e66ccd 100644 --- a/crates/editor/src/signature_help.rs +++ b/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::>(); + + 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>, + highlights: Vec<(Range, HighlightStyle)>, + active_parameter: Option, + parameter_documentation: Option>, +} + +#[derive(Clone, Debug)] pub struct SignatureHelpPopover { - pub label: SharedString, pub style: TextStyle, - pub highlights: Vec<(Range, HighlightStyle)>, + pub signatures: Vec, + pub current_signature: usize, + scroll_handle: ScrollHandle, + scrollbar_state: ScrollbarState, } impl SignatureHelpPopover { - pub fn render(&mut self, max_size: Size, cx: &mut Context) -> AnyElement { - 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()) + pub fn render( + &mut self, + max_size: Size, + window: &mut Window, + cx: &mut Context, + ) -> 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().px_2().py_0p5().child( - StyledText::new(self.label.clone()) - .with_default_highlights(&self.style, self.highlights.iter().cloned()), - ), + 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() + .elevation_2(cx) + .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) + .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) -> Stateful
{ + 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())) + } } diff --git a/crates/markdown/src/markdown.rs b/crates/markdown/src/markdown.rs index 9c057baec9..27859f6e08 100644 --- a/crates/markdown/src/markdown.rs +++ b/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, MarkdownEvent)]>, @@ -1672,7 +1672,7 @@ struct RenderedText { links: Rc<[RenderedLink]>, } -#[derive(Clone, Eq, PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq)] struct RenderedLink { source_range: Range, destination_url: SharedString, diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index 0a2c61fd43..729d61aab5 100644 --- a/crates/project/Cargo.toml +++ b/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 diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index cdeb9f71c1..70d8518c48 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1846,12 +1846,15 @@ impl LspCommand for GetSignatureHelp { async fn response_from_lsp( self, message: Option, - _: Entity, + lsp_store: Entity, _: Entity, _: LanguageServerId, - _: AsyncApp, + cx: AsyncApp, ) -> Result { - 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, + lsp_store: Entity, _: Entity, - _: AsyncApp, + cx: AsyncApp, ) -> Result { - 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 { diff --git a/crates/project/src/lsp_command/signature_help.rs b/crates/project/src/lsp_command/signature_help.rs index 37bd43fcce..8adb69ac77 100644 --- a/crates/project/src/lsp_command/signature_help.rs +++ b/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, HighlightStyle)>, + pub active_signature: usize, + pub signatures: Vec, pub(super) original_data: lsp::SignatureHelp, } +#[derive(Debug, Clone)] +pub struct SignatureHelpData { + pub label: SharedString, + pub documentation: Option>, + pub highlights: Vec<(Range, HighlightStyle)>, + pub active_parameter: Option, + pub parameters: Vec, +} + +#[derive(Debug, Clone)] +pub struct ParameterInfo { + pub label_range: Option>, + pub documentation: Option>, +} + impl SignatureHelp { - pub fn new(help: lsp::SignatureHelp) -> Option { - 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::(), - }; - 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 - } - }); - - if i != parameter_length { - highlight_start += label_length + str_for_join.len(); - } - - (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); - }; - - Some(Self { - label, - highlights: highlights.into_iter().flatten().collect(), - original_data: help, - }) + pub fn new( + help: lsp::SignatureHelp, + language_registry: Option>, + cx: &mut App, + ) -> Option { + if help.signatures.is_empty() { + return None; } + let active_signature = help.active_signature.unwrap_or(0) as usize; + let mut signatures = Vec::::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 ¶meter.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, + }); + } + } + + let label = SharedString::from(signature.label.clone()); + let documentation = signature + .documentation + .as_ref() + .map(|doc| documentation_to_markdown(doc, language_registry.clone(), cx)); + + signatures.push(SignatureHelpData { + label, + 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>, + cx: &mut App, +) -> Entity { + 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 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)); + } } diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index dc402be2b6..1047d7a5f0 100644 --- a/crates/project/src/lsp_store.rs +++ b/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::>() }) } diff --git a/crates/theme/src/styles/accents.rs b/crates/theme/src/styles/accents.rs index 54d4be3b63..cda0ef778a 100644 --- a/crates/theme/src/styles/accents.rs +++ b/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); impl Default for AccentColors { diff --git a/crates/theme/src/styles/colors.rs b/crates/theme/src/styles/colors.rs index 76d18c6d65..7c5270e361 100644 --- a/crates/theme/src/styles/colors.rs +++ b/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, diff --git a/crates/theme/src/styles/players.rs b/crates/theme/src/styles/players.rs index 5ac098d3be..4b1f0976b6 100644 --- a/crates/theme/src/styles/players.rs +++ b/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); impl Default for PlayerColors { diff --git a/crates/theme/src/styles/system.rs b/crates/theme/src/styles/system.rs index 64dd964b79..676577bfb4 100644 --- a/crates/theme/src/styles/system.rs +++ b/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, diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index bdb52693c0..f04eeade73 100644 --- a/crates/theme/src/theme.rs +++ b/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,