diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 9726758f35509..44d7ad293fbc4 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -100,6 +100,7 @@ "ctrl-k ctrl-r": "editor::RevertSelectedHunks", "ctrl-'": "editor::ToggleHunkDiff", "ctrl-\"": "editor::ExpandAllHunkDiffs", + "ctrl-i": "editor::ShowSignatureHelp", "alt-g b": "editor::ToggleGitBlame" } }, diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index cf33e6c9dcfe7..25f31ef70b7fb 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -126,7 +126,8 @@ "cmd-alt-z": "editor::RevertSelectedHunks", "cmd-'": "editor::ToggleHunkDiff", "cmd-\"": "editor::ExpandAllHunkDiffs", - "cmd-alt-g b": "editor::ToggleGitBlame" + "cmd-alt-g b": "editor::ToggleGitBlame", + "cmd-i": "editor::ShowSignatureHelp" } }, { diff --git a/assets/settings/default.json b/assets/settings/default.json index 00c72e933be61..c4b15c37ec490 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -116,6 +116,11 @@ // The debounce delay before re-querying the language server for completion // documentation when not included in original completion list. "completion_documentation_secondary_query_debounce": 300, + // Show method signatures in the editor, when inside parentheses. + "auto_signature_help": false, + /// Whether to show the signature help after completion or a bracket pair inserted. + /// If `auto_signature_help` is enabled, this setting will be treated as enabled also. + "show_signature_help_after_edits": true, // Whether to show wrap guides (vertical rulers) in the editor. // Setting this to true will show a guide at the 'preferred_line_length' value // if softwrap is set to 'preferred_line_length', and will show any diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 42e5c7e94ff0b..64e0ed29904f2 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -642,7 +642,10 @@ impl Server { app_state.config.openai_api_key.clone(), ) }) - }); + }) + .add_request_handler(user_handler( + forward_read_only_project_request::, + )); Arc::new(server) } diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index d0d7d912ee5ef..192162c594efb 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -286,12 +286,14 @@ gpui::actions!( SelectPageUp, ShowCharacterPalette, ShowInlineCompletion, + ShowSignatureHelp, ShuffleLines, SortLinesCaseInsensitive, SortLinesCaseSensitive, SplitSelectionIntoLines, Tab, TabPrev, + ToggleAutoSignatureHelp, ToggleGitBlame, ToggleGitBlameInline, ToggleSelectionMenu, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 79ac478c29c40..ddeb8e3addd2b 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -39,8 +39,10 @@ pub mod tasks; #[cfg(test)] mod editor_tests; +mod signature_help; #[cfg(any(test, feature = "test-support"))] pub mod test; + use ::git::diff::{DiffHunk, DiffHunkStatus}; use ::git::{parse_git_remote_url, BuildPermalinkParams, GitHostingProviderRegistry}; pub(crate) use actions::*; @@ -154,6 +156,7 @@ use workspace::{ use workspace::{OpenInTerminal, OpenTerminal, TabBarSettings, Toast}; use crate::hover_links::find_url; +use crate::signature_help::{SignatureHelpHiddenBy, SignatureHelpState}; pub const FILE_HEADER_HEIGHT: u8 = 1; pub const MULTI_BUFFER_EXCERPT_HEADER_HEIGHT: u8 = 1; @@ -501,6 +504,8 @@ pub struct Editor { context_menu: RwLock>, mouse_context_menu: Option, completion_tasks: Vec<(CompletionId, Task>)>, + signature_help_state: SignatureHelpState, + auto_signature_help: Option, find_all_references_task_sources: Vec, next_completion_id: CompletionId, completion_documentation_pre_resolve_debounce: DebouncedDelay, @@ -1819,6 +1824,8 @@ impl Editor { context_menu: RwLock::new(None), mouse_context_menu: None, completion_tasks: Default::default(), + signature_help_state: SignatureHelpState::default(), + auto_signature_help: None, find_all_references_task_sources: Vec::new(), next_completion_id: 0, completion_documentation_pre_resolve_debounce: DebouncedDelay::new(), @@ -2411,6 +2418,15 @@ impl Editor { self.request_autoscroll(autoscroll, cx); } self.selections_did_change(true, &old_cursor_position, request_completions, cx); + + if self.should_open_signature_help_automatically( + &old_cursor_position, + self.signature_help_state.backspace_pressed(), + cx, + ) { + self.show_signature_help(&ShowSignatureHelp, cx); + } + self.signature_help_state.set_backspace_pressed(false); } result @@ -2866,6 +2882,10 @@ impl Editor { return true; } + if self.hide_signature_help(cx, SignatureHelpHiddenBy::Escape) { + return true; + } + if self.hide_context_menu(cx).is_some() { return true; } @@ -2942,7 +2962,7 @@ impl Editor { } let selections = self.selections.all_adjusted(cx); - let mut brace_inserted = false; + let mut bracket_inserted = false; let mut edits = Vec::new(); let mut linked_edits = HashMap::<_, Vec<_>>::default(); let mut new_selections = Vec::with_capacity(selections.len()); @@ -3004,6 +3024,7 @@ impl Editor { ), &bracket_pair.start[..prefix_len], )); + if autoclose && bracket_pair.close && following_text_allows_autoclose @@ -3021,7 +3042,7 @@ impl Editor { selection.range(), format!("{}{}", text, bracket_pair.end).into(), )); - brace_inserted = true; + bracket_inserted = true; continue; } } @@ -3067,7 +3088,7 @@ impl Editor { selection.end..selection.end, bracket_pair.end.as_str().into(), )); - brace_inserted = true; + bracket_inserted = true; new_selections.push(( Selection { id: selection.id, @@ -3224,7 +3245,7 @@ impl Editor { s.select(new_selections) }); - if !brace_inserted && EditorSettings::get_global(cx).use_on_type_format { + if !bracket_inserted && EditorSettings::get_global(cx).use_on_type_format { if let Some(on_type_format_task) = this.trigger_on_type_formatting(text.to_string(), cx) { @@ -3232,6 +3253,14 @@ impl Editor { } } + let editor_settings = EditorSettings::get_global(cx); + if bracket_inserted + && (editor_settings.auto_signature_help + || editor_settings.show_signature_help_after_edits) + { + this.show_signature_help(&ShowSignatureHelp, cx); + } + let trigger_in_words = !had_active_inline_completion; this.trigger_completion_on_input(&text, trigger_in_words, cx); linked_editing_ranges::refresh_linked_ranges(this, cx); @@ -4305,6 +4334,14 @@ impl Editor { true, cx, ); + + let editor_settings = EditorSettings::get_global(cx); + if editor_settings.show_signature_help_after_edits || editor_settings.auto_signature_help { + // After the code completion is finished, users often want to know what signatures are needed. + // so we should automatically call signature_help + self.show_signature_help(&ShowSignatureHelp, cx); + } + Some(cx.foreground_executor().spawn(async move { apply_edits.await?; Ok(()) @@ -5328,6 +5365,7 @@ impl Editor { } } + this.signature_help_state.set_backspace_pressed(true); this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections)); this.insert("", cx); let empty_str: Arc = Arc::from(""); diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index fd7a21cd0ba63..9fe91f2ce0be6 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -26,6 +26,8 @@ pub struct EditorSettings { #[serde(default)] pub double_click_in_multibuffer: DoubleClickInMultibuffer, pub search_wrap: bool, + pub auto_signature_help: bool, + pub show_signature_help_after_edits: bool, #[serde(default)] pub jupyter: Jupyter, } @@ -234,6 +236,16 @@ pub struct EditorSettingsContent { /// Default: true pub search_wrap: Option, + /// Whether to automatically show a signature help pop-up or not. + /// + /// Default: false + pub auto_signature_help: Option, + + /// Whether to show the signature help pop-up after completions or bracket pairs inserted. + /// + /// Default: true + pub show_signature_help_after_edits: Option, + /// Jupyter REPL settings. pub jupyter: Option, } diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 87908cbb8cf46..eb4612027e27d 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -21,13 +21,16 @@ use language::{ BracketPairConfig, Capability::ReadWrite, FakeLspAdapter, IndentGuide, LanguageConfig, LanguageConfigOverride, LanguageMatcher, Override, - Point, + ParsedMarkdown, Point, }; use language_settings::IndentGuideSettings; use multi_buffer::MultiBufferIndentGuide; use parking_lot::Mutex; -use project::project_settings::{LspSettings, ProjectSettings}; use project::FakeFs; +use project::{ + lsp_command::SIGNATURE_HELP_HIGHLIGHT_CURRENT, + project_settings::{LspSettings, ProjectSettings}, +}; use serde_json::{self, json}; use std::sync::atomic; use std::sync::atomic::AtomicUsize; @@ -6831,6 +6834,626 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext) ); } +#[gpui::test] +async fn test_handle_input_for_show_signature_help_auto_signature_help_true( + cx: &mut gpui::TestAppContext, +) { + init_test(cx, |_| {}); + + cx.update(|cx| { + cx.update_global::(|settings, cx| { + settings.update_user_settings::(cx, |settings| { + settings.auto_signature_help = Some(true); + }); + }); + }); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + signature_help_provider: Some(lsp::SignatureHelpOptions { + ..Default::default() + }), + ..Default::default() + }, + cx, + ) + .await; + + let language = Language::new( + LanguageConfig { + name: "Rust".into(), + brackets: BracketPairConfig { + pairs: vec![ + BracketPair { + start: "{".to_string(), + end: "}".to_string(), + close: true, + surround: true, + newline: true, + }, + BracketPair { + start: "(".to_string(), + end: ")".to_string(), + close: true, + surround: true, + newline: true, + }, + BracketPair { + start: "/*".to_string(), + end: " */".to_string(), + close: true, + surround: true, + newline: true, + }, + BracketPair { + start: "[".to_string(), + end: "]".to_string(), + close: false, + surround: false, + newline: true, + }, + BracketPair { + start: "\"".to_string(), + end: "\"".to_string(), + close: true, + surround: true, + newline: false, + }, + BracketPair { + start: "<".to_string(), + end: ">".to_string(), + close: false, + surround: true, + newline: true, + }, + ], + ..Default::default() + }, + autoclose_before: "})]".to_string(), + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let language = Arc::new(language); + + cx.language_registry().add(language.clone()); + cx.update_buffer(|buffer, cx| { + buffer.set_language(Some(language), cx); + }); + + cx.set_state( + &r#" + fn main() { + sampleˇ + } + "# + .unindent(), + ); + + cx.update_editor(|view, cx| { + view.handle_input("(", cx); + }); + cx.assert_editor_state( + &" + fn main() { + sample(ˇ) + } + " + .unindent(), + ); + + let mocked_response = lsp::SignatureHelp { + signatures: vec![lsp::SignatureInformation { + label: "fn sample(param1: u8, param2: u8)".to_string(), + documentation: None, + parameters: Some(vec![ + lsp::ParameterInformation { + label: lsp::ParameterLabel::Simple("param1: u8".to_string()), + documentation: None, + }, + lsp::ParameterInformation { + label: lsp::ParameterLabel::Simple("param2: u8".to_string()), + documentation: None, + }, + ]), + active_parameter: None, + }], + active_signature: Some(0), + active_parameter: Some(0), + }; + handle_signature_help_request(&mut cx, mocked_response).await; + + cx.condition(|editor, _| editor.signature_help_state.is_shown()) + .await; + + cx.editor(|editor, _| { + let signature_help_state = editor.signature_help_state.popover().cloned(); + assert!(signature_help_state.is_some()); + let ParsedMarkdown { + text, highlights, .. + } = signature_help_state.unwrap().parsed_content; + assert_eq!(text, "param1: u8, param2: u8"); + assert_eq!(highlights, vec![(0..10, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]); + }); +} + +#[gpui::test] +async fn test_handle_input_with_different_show_signature_settings(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + cx.update(|cx| { + cx.update_global::(|settings, cx| { + settings.update_user_settings::(cx, |settings| { + settings.auto_signature_help = Some(false); + settings.show_signature_help_after_edits = Some(false); + }); + }); + }); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + signature_help_provider: Some(lsp::SignatureHelpOptions { + ..Default::default() + }), + ..Default::default() + }, + cx, + ) + .await; + + let language = Language::new( + LanguageConfig { + name: "Rust".into(), + brackets: BracketPairConfig { + pairs: vec![ + BracketPair { + start: "{".to_string(), + end: "}".to_string(), + close: true, + surround: true, + newline: true, + }, + BracketPair { + start: "(".to_string(), + end: ")".to_string(), + close: true, + surround: true, + newline: true, + }, + BracketPair { + start: "/*".to_string(), + end: " */".to_string(), + close: true, + surround: true, + newline: true, + }, + BracketPair { + start: "[".to_string(), + end: "]".to_string(), + close: false, + surround: false, + newline: true, + }, + BracketPair { + start: "\"".to_string(), + end: "\"".to_string(), + close: true, + surround: true, + newline: false, + }, + BracketPair { + start: "<".to_string(), + end: ">".to_string(), + close: false, + surround: true, + newline: true, + }, + ], + ..Default::default() + }, + autoclose_before: "})]".to_string(), + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let language = Arc::new(language); + + cx.language_registry().add(language.clone()); + cx.update_buffer(|buffer, cx| { + buffer.set_language(Some(language), cx); + }); + + // Ensure that signature_help is not called when no signature help is enabled. + cx.set_state( + &r#" + fn main() { + sampleˇ + } + "# + .unindent(), + ); + cx.update_editor(|view, cx| { + view.handle_input("(", cx); + }); + cx.assert_editor_state( + &" + fn main() { + sample(ˇ) + } + " + .unindent(), + ); + cx.editor(|editor, _| { + assert!(editor.signature_help_state.task().is_none()); + }); + + let mocked_response = lsp::SignatureHelp { + signatures: vec![lsp::SignatureInformation { + label: "fn sample(param1: u8, param2: u8)".to_string(), + documentation: None, + parameters: Some(vec![ + lsp::ParameterInformation { + label: lsp::ParameterLabel::Simple("param1: u8".to_string()), + documentation: None, + }, + lsp::ParameterInformation { + label: lsp::ParameterLabel::Simple("param2: u8".to_string()), + documentation: None, + }, + ]), + active_parameter: None, + }], + active_signature: Some(0), + active_parameter: Some(0), + }; + + // Ensure that signature_help is called when enabled afte edits + cx.update(|cx| { + cx.update_global::(|settings, cx| { + settings.update_user_settings::(cx, |settings| { + settings.auto_signature_help = Some(false); + settings.show_signature_help_after_edits = Some(true); + }); + }); + }); + cx.set_state( + &r#" + fn main() { + sampleˇ + } + "# + .unindent(), + ); + cx.update_editor(|view, cx| { + view.handle_input("(", cx); + }); + cx.assert_editor_state( + &" + fn main() { + sample(ˇ) + } + " + .unindent(), + ); + handle_signature_help_request(&mut cx, mocked_response.clone()).await; + cx.condition(|editor, _| editor.signature_help_state.is_shown()) + .await; + cx.update_editor(|editor, _| { + let signature_help_state = editor.signature_help_state.popover().cloned(); + assert!(signature_help_state.is_some()); + let ParsedMarkdown { + text, highlights, .. + } = signature_help_state.unwrap().parsed_content; + assert_eq!(text, "param1: u8, param2: u8"); + assert_eq!(highlights, vec![(0..10, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]); + editor.signature_help_state = SignatureHelpState::default(); + }); + + // Ensure that signature_help is called when auto signature help override is enabled + cx.update(|cx| { + cx.update_global::(|settings, cx| { + settings.update_user_settings::(cx, |settings| { + settings.auto_signature_help = Some(true); + settings.show_signature_help_after_edits = Some(false); + }); + }); + }); + cx.set_state( + &r#" + fn main() { + sampleˇ + } + "# + .unindent(), + ); + cx.update_editor(|view, cx| { + view.handle_input("(", cx); + }); + cx.assert_editor_state( + &" + fn main() { + sample(ˇ) + } + " + .unindent(), + ); + handle_signature_help_request(&mut cx, mocked_response).await; + cx.condition(|editor, _| editor.signature_help_state.is_shown()) + .await; + cx.editor(|editor, _| { + let signature_help_state = editor.signature_help_state.popover().cloned(); + assert!(signature_help_state.is_some()); + let ParsedMarkdown { + text, highlights, .. + } = signature_help_state.unwrap().parsed_content; + assert_eq!(text, "param1: u8, param2: u8"); + assert_eq!(highlights, vec![(0..10, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]); + }); +} + +#[gpui::test] +async fn test_signature_help(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + cx.update(|cx| { + cx.update_global::(|settings, cx| { + settings.update_user_settings::(cx, |settings| { + settings.auto_signature_help = Some(true); + }); + }); + }); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + signature_help_provider: Some(lsp::SignatureHelpOptions { + ..Default::default() + }), + ..Default::default() + }, + cx, + ) + .await; + + // A test that directly calls `show_signature_help` + cx.update_editor(|editor, cx| { + editor.show_signature_help(&ShowSignatureHelp, cx); + }); + + let mocked_response = lsp::SignatureHelp { + signatures: vec![lsp::SignatureInformation { + label: "fn sample(param1: u8, param2: u8)".to_string(), + documentation: None, + parameters: Some(vec![ + lsp::ParameterInformation { + label: lsp::ParameterLabel::Simple("param1: u8".to_string()), + documentation: None, + }, + lsp::ParameterInformation { + label: lsp::ParameterLabel::Simple("param2: u8".to_string()), + documentation: None, + }, + ]), + active_parameter: None, + }], + active_signature: Some(0), + active_parameter: Some(0), + }; + handle_signature_help_request(&mut cx, mocked_response).await; + + cx.condition(|editor, _| editor.signature_help_state.is_shown()) + .await; + + cx.editor(|editor, _| { + let signature_help_state = editor.signature_help_state.popover().cloned(); + assert!(signature_help_state.is_some()); + let ParsedMarkdown { + text, highlights, .. + } = signature_help_state.unwrap().parsed_content; + assert_eq!(text, "param1: u8, param2: u8"); + assert_eq!(highlights, vec![(0..10, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]); + }); + + // When exiting outside from inside the brackets, `signature_help` is closed. + cx.set_state(indoc! {" + fn main() { + sample(ˇ); + } + + fn sample(param1: u8, param2: u8) {} + "}); + + cx.update_editor(|editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([0..0])); + }); + + let mocked_response = lsp::SignatureHelp { + signatures: Vec::new(), + active_signature: None, + active_parameter: None, + }; + handle_signature_help_request(&mut cx, mocked_response).await; + + cx.condition(|editor, _| !editor.signature_help_state.is_shown()) + .await; + + cx.editor(|editor, _| { + assert!(!editor.signature_help_state.is_shown()); + }); + + // When entering inside the brackets from outside, `show_signature_help` is automatically called. + cx.set_state(indoc! {" + fn main() { + sample(ˇ); + } + + fn sample(param1: u8, param2: u8) {} + "}); + + let mocked_response = lsp::SignatureHelp { + signatures: vec![lsp::SignatureInformation { + label: "fn sample(param1: u8, param2: u8)".to_string(), + documentation: None, + parameters: Some(vec![ + lsp::ParameterInformation { + label: lsp::ParameterLabel::Simple("param1: u8".to_string()), + documentation: None, + }, + lsp::ParameterInformation { + label: lsp::ParameterLabel::Simple("param2: u8".to_string()), + documentation: None, + }, + ]), + active_parameter: None, + }], + active_signature: Some(0), + active_parameter: Some(0), + }; + handle_signature_help_request(&mut cx, mocked_response.clone()).await; + cx.condition(|editor, _| editor.signature_help_state.is_shown()) + .await; + cx.editor(|editor, _| { + assert!(editor.signature_help_state.is_shown()); + }); + + // Restore the popover with more parameter input + cx.set_state(indoc! {" + fn main() { + sample(param1, param2ˇ); + } + + fn sample(param1: u8, param2: u8) {} + "}); + + let mocked_response = lsp::SignatureHelp { + signatures: vec![lsp::SignatureInformation { + label: "fn sample(param1: u8, param2: u8)".to_string(), + documentation: None, + parameters: Some(vec![ + lsp::ParameterInformation { + label: lsp::ParameterLabel::Simple("param1: u8".to_string()), + documentation: None, + }, + lsp::ParameterInformation { + label: lsp::ParameterLabel::Simple("param2: u8".to_string()), + documentation: None, + }, + ]), + active_parameter: None, + }], + active_signature: Some(0), + active_parameter: Some(1), + }; + handle_signature_help_request(&mut cx, mocked_response.clone()).await; + cx.condition(|editor, _| editor.signature_help_state.is_shown()) + .await; + + // When selecting a range, the popover is gone. + // Avoid using `cx.set_state` to not actually edit the document, just change its selections. + cx.update_editor(|editor, cx| { + editor.change_selections(None, cx, |s| { + s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19))); + }) + }); + cx.assert_editor_state(indoc! {" + fn main() { + sample(param1, «ˇparam2»); + } + + fn sample(param1: u8, param2: u8) {} + "}); + cx.editor(|editor, _| { + assert!(!editor.signature_help_state.is_shown()); + }); + + // When unselecting again, the popover is back if within the brackets. + cx.update_editor(|editor, cx| { + editor.change_selections(None, cx, |s| { + s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19))); + }) + }); + cx.assert_editor_state(indoc! {" + fn main() { + sample(param1, ˇparam2); + } + + fn sample(param1: u8, param2: u8) {} + "}); + handle_signature_help_request(&mut cx, mocked_response).await; + cx.condition(|editor, _| editor.signature_help_state.is_shown()) + .await; + cx.editor(|editor, _| { + assert!(editor.signature_help_state.is_shown()); + }); + + // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape. + cx.update_editor(|editor, cx| { + editor.change_selections(None, cx, |s| { + s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0))); + s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19))); + }) + }); + cx.assert_editor_state(indoc! {" + fn main() { + sample(param1, ˇparam2); + } + + fn sample(param1: u8, param2: u8) {} + "}); + + let mocked_response = lsp::SignatureHelp { + signatures: vec![lsp::SignatureInformation { + label: "fn sample(param1: u8, param2: u8)".to_string(), + documentation: None, + parameters: Some(vec![ + lsp::ParameterInformation { + label: lsp::ParameterLabel::Simple("param1: u8".to_string()), + documentation: None, + }, + lsp::ParameterInformation { + label: lsp::ParameterLabel::Simple("param2: u8".to_string()), + documentation: None, + }, + ]), + active_parameter: None, + }], + active_signature: Some(0), + active_parameter: Some(1), + }; + handle_signature_help_request(&mut cx, mocked_response.clone()).await; + cx.condition(|editor, _| editor.signature_help_state.is_shown()) + .await; + cx.update_editor(|editor, cx| { + editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape); + }); + cx.condition(|editor, _| !editor.signature_help_state.is_shown()) + .await; + cx.update_editor(|editor, cx| { + editor.change_selections(None, cx, |s| { + s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19))); + }) + }); + cx.assert_editor_state(indoc! {" + fn main() { + sample(param1, «ˇparam2»); + } + + fn sample(param1: u8, param2: u8) {} + "}); + cx.update_editor(|editor, cx| { + editor.change_selections(None, cx, |s| { + s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19))); + }) + }); + cx.assert_editor_state(indoc! {" + fn main() { + sample(param1, ˇparam2); + } + + fn sample(param1: u8, param2: u8) {} + "}); + cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape + .await; +} + #[gpui::test] async fn test_completion(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); @@ -12450,6 +13073,21 @@ fn assert_selection_ranges(marked_text: &str, view: &mut Editor, cx: &mut ViewCo ); } +pub fn handle_signature_help_request( + cx: &mut EditorLspTestContext, + mocked_response: lsp::SignatureHelp, +) -> impl Future { + let mut request = + cx.handle_request::(move |_, _, _| { + let mocked_response = mocked_response.clone(); + async move { Ok(Some(mocked_response)) } + }); + + async move { + request.next().await; + } +} + /// Handle completion request passing a marked string specifying where the completion /// should be triggered from using '|' character, what range should be replaced, and what completions /// should be returned using '<' and '>' to delimit the range diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 5d26ecf75aaf2..cf9093ab57bfc 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -382,6 +382,7 @@ impl EditorElement { cx.propagate(); } }); + register_action(view, cx, Editor::show_signature_help); register_action(view, cx, Editor::next_inline_completion); register_action(view, cx, Editor::previous_inline_completion); register_action(view, cx, Editor::show_inline_completion); @@ -2635,6 +2636,71 @@ impl EditorElement { } } + #[allow(clippy::too_many_arguments)] + fn layout_signature_help( + &self, + hitbox: &Hitbox, + content_origin: gpui::Point, + scroll_pixel_position: gpui::Point, + display_point: Option, + start_row: DisplayRow, + line_layouts: &[LineWithInvisibles], + line_height: Pixels, + em_width: Pixels, + cx: &mut WindowContext, + ) { + let Some(display_point) = display_point else { + return; + }; + + let Some(cursor_row_layout) = + line_layouts.get(display_point.row().minus(start_row) as usize) + else { + return; + }; + + let start_x = cursor_row_layout.x_for_index(display_point.column() as usize) + - scroll_pixel_position.x + + content_origin.x; + let start_y = + display_point.row().as_f32() * line_height + content_origin.y - scroll_pixel_position.y; + + let max_size = size( + (120. * em_width) // Default size + .min(hitbox.size.width / 2.) // Shrink to half of the editor width + .max(MIN_POPOVER_CHARACTER_WIDTH * em_width), // Apply minimum width of 20 characters + (16. * line_height) // Default size + .min(hitbox.size.height / 2.) // Shrink to half of the editor height + .max(MIN_POPOVER_LINE_HEIGHT * line_height), // Apply minimum height of 4 lines + ); + + let maybe_element = self.editor.update(cx, |editor, cx| { + if let Some(popover) = editor.signature_help_state.popover_mut() { + let element = popover.render( + &self.style, + max_size, + editor.workspace.as_ref().map(|(w, _)| w.clone()), + cx, + ); + Some(element) + } else { + None + } + }); + if let Some(mut element) = maybe_element { + let window_size = cx.viewport_size(); + let size = element.layout_as_root(Size::::default(), cx); + let mut point = point(start_x, start_y - size.height); + + // Adjusting to ensure the popover does not overflow in the X-axis direction. + if point.x + size.width >= window_size.width { + point.x = window_size.width - size.width; + } + + cx.defer_draw(element, point, 1) + } + } + fn paint_background(&self, layout: &EditorLayout, cx: &mut WindowContext) { cx.paint_layer(layout.hitbox.bounds, |cx| { let scroll_top = layout.position_map.snapshot.scroll_position().y; @@ -5072,6 +5138,18 @@ impl Element for EditorElement { vec![] }; + self.layout_signature_help( + &hitbox, + content_origin, + scroll_pixel_position, + newest_selection_head, + start_row, + &line_layouts, + line_height, + em_width, + cx, + ); + if !cx.has_active_drag() { self.layout_hover_popovers( &snapshot, diff --git a/crates/editor/src/signature_help.rs b/crates/editor/src/signature_help.rs new file mode 100644 index 0000000000000..9a19e0e0fdf2a --- /dev/null +++ b/crates/editor/src/signature_help.rs @@ -0,0 +1,225 @@ +mod popover; +mod state; + +use crate::actions::ShowSignatureHelp; +use crate::{Editor, EditorSettings, ToggleAutoSignatureHelp}; +use gpui::{AppContext, ViewContext}; +use language::markdown::parse_markdown; +use multi_buffer::{Anchor, ToOffset}; +use settings::Settings; +use std::ops::Range; + +pub use popover::SignatureHelpPopover; +pub use state::SignatureHelpState; + +// Language-specific settings may define quotes as "brackets", so filter them out separately. +const QUOTE_PAIRS: [(&'static str, &'static str); 3] = [("'", "'"), ("\"", "\""), ("`", "`")]; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum SignatureHelpHiddenBy { + AutoClose, + Escape, + Selection, +} + +impl Editor { + pub fn toggle_auto_signature_help_menu( + &mut self, + _: &ToggleAutoSignatureHelp, + cx: &mut ViewContext, + ) { + self.auto_signature_help = self + .auto_signature_help + .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 => { + self.show_signature_help(&ShowSignatureHelp, cx); + } + Some(_) => { + self.hide_signature_help(cx, SignatureHelpHiddenBy::AutoClose); + } + None => {} + } + cx.notify(); + } + + pub(super) fn hide_signature_help( + &mut self, + cx: &mut ViewContext, + signature_help_hidden_by: SignatureHelpHiddenBy, + ) -> bool { + if self.signature_help_state.is_shown() { + self.signature_help_state.kill_task(); + self.signature_help_state.hide(signature_help_hidden_by); + cx.notify(); + true + } else { + false + } + } + + pub fn auto_signature_help_enabled(&self, cx: &AppContext) -> bool { + if let Some(auto_signature_help) = self.auto_signature_help { + auto_signature_help + } else { + EditorSettings::get_global(cx).auto_signature_help + } + } + + pub(super) fn should_open_signature_help_automatically( + &mut self, + old_cursor_position: &Anchor, + backspace_pressed: bool, + cx: &mut ViewContext, + ) -> bool { + if !(self.signature_help_state.is_shown() || self.auto_signature_help_enabled(cx)) { + return false; + } + let newest_selection = self.selections.newest::(cx); + let head = newest_selection.head(); + + // There are two cases where the head and tail of a selection are different: selecting multiple ranges and using backspace. + // If we don’t exclude the backspace case, signature_help will blink every time backspace is pressed, so we need to prevent this. + if !newest_selection.is_empty() && !backspace_pressed && head != newest_selection.tail() { + self.signature_help_state + .hide(SignatureHelpHiddenBy::Selection); + return false; + } + + let buffer_snapshot = self.buffer().read(cx).snapshot(cx); + let bracket_range = |position: usize| match (position, position + 1) { + (0, b) if b <= buffer_snapshot.len() => 0..b, + (0, b) => 0..b - 1, + (a, b) if b <= buffer_snapshot.len() => a - 1..b, + (a, b) => a - 1..b - 1, + }; + let not_quote_like_brackets = |start: Range, end: Range| { + let text = buffer_snapshot.text(); + let (text_start, text_end) = (text.get(start), text.get(end)); + QUOTE_PAIRS + .into_iter() + .all(|(start, end)| text_start != Some(start) && text_end != Some(end)) + }; + + let previous_position = old_cursor_position.to_offset(&buffer_snapshot); + let previous_brackets_range = bracket_range(previous_position); + let previous_brackets_surround = buffer_snapshot + .innermost_enclosing_bracket_ranges( + previous_brackets_range, + Some(¬_quote_like_brackets), + ) + .filter(|(start_bracket_range, end_bracket_range)| { + start_bracket_range.start != previous_position + && end_bracket_range.end != previous_position + }); + let current_brackets_range = bracket_range(head); + let current_brackets_surround = buffer_snapshot + .innermost_enclosing_bracket_ranges( + current_brackets_range, + Some(¬_quote_like_brackets), + ) + .filter(|(start_bracket_range, end_bracket_range)| { + start_bracket_range.start != head && end_bracket_range.end != head + }); + + match (previous_brackets_surround, current_brackets_surround) { + (None, None) => { + self.signature_help_state + .hide(SignatureHelpHiddenBy::AutoClose); + false + } + (Some(_), None) => { + self.signature_help_state + .hide(SignatureHelpHiddenBy::AutoClose); + false + } + (None, Some(_)) => true, + (Some(previous), Some(current)) => { + let condition = self.signature_help_state.hidden_by_selection() + || previous != current + || (previous == current && self.signature_help_state.is_shown()); + if !condition { + self.signature_help_state + .hide(SignatureHelpHiddenBy::AutoClose); + } + condition + } + } + } + + pub fn show_signature_help(&mut self, _: &ShowSignatureHelp, cx: &mut ViewContext) { + if self.pending_rename.is_some() { + return; + } + + let position = self.selections.newest_anchor().head(); + let Some((buffer, buffer_position)) = + self.buffer.read(cx).text_anchor_for_position(position, cx) + else { + return; + }; + + self.signature_help_state + .set_task(cx.spawn(move |editor, mut cx| async move { + let signature_help = editor + .update(&mut cx, |editor, cx| { + let language = editor.language_at(position, cx); + let project = editor.project.clone()?; + let (markdown, language_registry) = { + project.update(cx, |project, mut cx| { + let language_registry = project.languages().clone(); + ( + project.signature_help(&buffer, buffer_position, &mut cx), + language_registry, + ) + }) + }; + Some((markdown, language_registry, language)) + }) + .ok() + .flatten(); + let signature_help_popover = if let Some(( + signature_help_task, + language_registry, + language, + )) = signature_help + { + // TODO allow multiple signature helps inside the same popover + if let Some(mut signature_help) = signature_help_task.await.into_iter().next() { + let mut parsed_content = parse_markdown( + signature_help.markdown.as_str(), + &language_registry, + language, + ) + .await; + parsed_content + .highlights + .append(&mut signature_help.highlights); + Some(SignatureHelpPopover { parsed_content }) + } else { + None + } + } else { + None + }; + editor + .update(&mut cx, |editor, cx| { + let previous_popover = editor.signature_help_state.popover(); + if previous_popover != signature_help_popover.as_ref() { + if let Some(signature_help_popover) = signature_help_popover { + editor + .signature_help_state + .set_popover(signature_help_popover); + } else { + editor + .signature_help_state + .hide(SignatureHelpHiddenBy::AutoClose); + } + cx.notify(); + } + }) + .ok(); + })); + } +} diff --git a/crates/editor/src/signature_help/popover.rs b/crates/editor/src/signature_help/popover.rs new file mode 100644 index 0000000000000..951cfd5d91ce0 --- /dev/null +++ b/crates/editor/src/signature_help/popover.rs @@ -0,0 +1,48 @@ +use crate::{Editor, EditorStyle}; +use gpui::{ + div, AnyElement, InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels, Size, + StatefulInteractiveElement, Styled, ViewContext, WeakView, +}; +use language::ParsedMarkdown; +use ui::StyledExt; +use workspace::Workspace; + +#[derive(Clone, Debug)] +pub struct SignatureHelpPopover { + pub parsed_content: ParsedMarkdown, +} + +impl PartialEq for SignatureHelpPopover { + fn eq(&self, other: &Self) -> bool { + let str_equality = self.parsed_content.text.as_str() == other.parsed_content.text.as_str(); + let highlight_equality = self.parsed_content.highlights == other.parsed_content.highlights; + str_equality && highlight_equality + } +} + +impl SignatureHelpPopover { + pub fn render( + &mut self, + style: &EditorStyle, + max_size: Size, + workspace: Option>, + cx: &mut ViewContext, + ) -> 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()) + .child(div().p_2().child(crate::render_parsed_markdown( + "signature_help_popover_content", + &self.parsed_content, + style, + workspace, + cx, + ))) + .into_any_element() + } +} diff --git a/crates/editor/src/signature_help/state.rs b/crates/editor/src/signature_help/state.rs new file mode 100644 index 0000000000000..8bb61ebd7167c --- /dev/null +++ b/crates/editor/src/signature_help/state.rs @@ -0,0 +1,65 @@ +use crate::signature_help::popover::SignatureHelpPopover; +use crate::signature_help::SignatureHelpHiddenBy; +use gpui::Task; + +#[derive(Default, Debug)] +pub struct SignatureHelpState { + task: Option>, + popover: Option, + hidden_by: Option, + backspace_pressed: bool, +} + +impl SignatureHelpState { + pub fn set_task(&mut self, task: Task<()>) { + self.task = Some(task); + self.hidden_by = None; + } + + pub fn kill_task(&mut self) { + self.task = None; + } + + pub fn popover(&self) -> Option<&SignatureHelpPopover> { + self.popover.as_ref() + } + + pub fn popover_mut(&mut self) -> Option<&mut SignatureHelpPopover> { + self.popover.as_mut() + } + + pub fn backspace_pressed(&self) -> bool { + self.backspace_pressed + } + + pub fn set_backspace_pressed(&mut self, backspace_pressed: bool) { + self.backspace_pressed = backspace_pressed; + } + + pub fn set_popover(&mut self, popover: SignatureHelpPopover) { + self.popover = Some(popover); + self.hidden_by = None; + } + + pub 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 { + self.hidden_by == Some(SignatureHelpHiddenBy::Selection) + } + + pub fn is_shown(&self) -> bool { + self.popover.is_some() + } +} + +#[cfg(test)] +impl SignatureHelpState { + pub fn task(&self) -> Option<&Task<()>> { + self.task.as_ref() + } +} diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index ef167b9f2280b..ddb87f806f350 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -645,7 +645,20 @@ impl LanguageServer { on_type_formatting: Some(DynamicRegistrationClientCapabilities { dynamic_registration: None, }), - ..Default::default() + signature_help: Some(SignatureHelpClientCapabilities { + signature_information: Some(SignatureInformationSettings { + documentation_format: Some(vec![ + MarkupKind::Markdown, + MarkupKind::PlainText, + ]), + parameter_information: Some(ParameterInformationSettings { + label_offset_support: Some(true), + }), + active_parameter_support: Some(true), + }), + ..SignatureHelpClientCapabilities::default() + }), + ..TextDocumentClientCapabilities::default() }), experimental: Some(json!({ "serverStatusNotification": true, diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index a21c80fb50bbb..a13de4a7d95c2 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1,3 +1,5 @@ +mod signature_help; + use crate::{ CodeAction, CoreCompletion, DocumentHighlight, Hover, HoverBlock, HoverBlockKind, InlayHint, InlayHintLabel, InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, Location, @@ -6,10 +8,12 @@ use crate::{ use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; use client::proto::{self, PeerId}; +use clock::Global; use futures::future; -use gpui::{AppContext, AsyncAppContext, Model}; +use gpui::{AppContext, AsyncAppContext, FontWeight, Model}; use language::{ language_settings::{language_settings, InlayHintKind}, + markdown::{MarkdownHighlight, MarkdownHighlightStyle}, point_from_lsp, point_to_lsp, proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CharKind, @@ -23,6 +27,10 @@ use lsp::{ use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc}; use text::{BufferId, LineEnding}; +pub use signature_help::{ + SignatureHelp, SIGNATURE_HELP_HIGHLIGHT_CURRENT, SIGNATURE_HELP_HIGHLIGHT_OVERLOAD, +}; + pub fn lsp_formatting_options(tab_size: u32) -> lsp::FormattingOptions { lsp::FormattingOptions { tab_size, @@ -121,6 +129,11 @@ pub(crate) struct GetDocumentHighlights { pub position: PointUtf16, } +#[derive(Clone)] +pub(crate) struct GetSignatureHelp { + pub position: PointUtf16, +} + #[derive(Clone)] pub(crate) struct GetHover { pub position: PointUtf16, @@ -1225,6 +1238,164 @@ impl LspCommand for GetDocumentHighlights { } } +#[async_trait(?Send)] +impl LspCommand for GetSignatureHelp { + type Response = Vec; + type LspRequest = lsp::SignatureHelpRequest; + type ProtoRequest = proto::GetSignatureHelp; + + fn check_capabilities(&self, capabilities: &ServerCapabilities) -> bool { + capabilities.signature_help_provider.is_some() + } + + fn to_lsp( + &self, + path: &Path, + _: &Buffer, + _: &Arc, + _cx: &AppContext, + ) -> lsp::SignatureHelpParams { + let url_result = lsp::Url::from_file_path(path); + if url_result.is_err() { + log::error!("an invalid file path has been specified"); + } + + lsp::SignatureHelpParams { + text_document_position_params: lsp::TextDocumentPositionParams { + text_document: lsp::TextDocumentIdentifier { + uri: url_result.expect("invalid file path"), + }, + position: point_to_lsp(self.position), + }, + context: None, + work_done_progress_params: Default::default(), + } + } + + async fn response_from_lsp( + self, + message: Option, + _: Model, + buffer: Model, + _: LanguageServerId, + mut cx: AsyncAppContext, + ) -> Result { + let language = buffer.update(&mut cx, |buffer, _| buffer.language().cloned())?; + Ok(message + .into_iter() + .filter_map(|message| SignatureHelp::new(message, language.clone())) + .collect()) + } + + fn to_proto(&self, project_id: u64, buffer: &Buffer) -> Self::ProtoRequest { + let offset = buffer.point_utf16_to_offset(self.position); + proto::GetSignatureHelp { + project_id, + buffer_id: buffer.remote_id().to_proto(), + position: Some(serialize_anchor(&buffer.anchor_after(offset))), + version: serialize_version(&buffer.version()), + } + } + + async fn from_proto( + payload: Self::ProtoRequest, + _: Model, + buffer: Model, + mut cx: AsyncAppContext, + ) -> Result { + buffer + .update(&mut cx, |buffer, _| { + buffer.wait_for_version(deserialize_version(&payload.version)) + })? + .await + .with_context(|| format!("waiting for version for buffer {}", buffer.entity_id()))?; + let buffer_snapshot = buffer.update(&mut cx, |buffer, _| buffer.snapshot())?; + Ok(Self { + position: payload + .position + .and_then(deserialize_anchor) + .context("invalid position")? + .to_point_utf16(&buffer_snapshot), + }) + } + + fn response_to_proto( + response: Self::Response, + _: &mut Project, + _: PeerId, + _: &Global, + _: &mut AppContext, + ) -> proto::GetSignatureHelpResponse { + proto::GetSignatureHelpResponse { + entries: response + .into_iter() + .map(|signature_help| proto::SignatureHelp { + rendered_text: signature_help.markdown, + highlights: signature_help + .highlights + .into_iter() + .filter_map(|(range, highlight)| { + let MarkdownHighlight::Style(highlight) = highlight else { + return None; + }; + + Some(proto::HighlightedRange { + range: Some(proto::Range { + start: range.start as u64, + end: range.end as u64, + }), + highlight: Some(proto::MarkdownHighlight { + italic: highlight.italic, + underline: highlight.underline, + strikethrough: highlight.strikethrough, + weight: highlight.weight.0, + }), + }) + }) + .collect(), + }) + .collect(), + } + } + + async fn response_from_proto( + self, + response: proto::GetSignatureHelpResponse, + _: Model, + _: Model, + _: AsyncAppContext, + ) -> Result { + Ok(response + .entries + .into_iter() + .map(|proto_entry| SignatureHelp { + markdown: proto_entry.rendered_text, + highlights: proto_entry + .highlights + .into_iter() + .filter_map(|highlight| { + let proto_highlight = highlight.highlight?; + let range = highlight.range?; + Some(( + range.start as usize..range.end as usize, + MarkdownHighlight::Style(MarkdownHighlightStyle { + italic: proto_highlight.italic, + underline: proto_highlight.underline, + strikethrough: proto_highlight.strikethrough, + weight: FontWeight(proto_highlight.weight), + }), + )) + }) + .collect(), + }) + .collect()) + } + + fn buffer_id_from_proto(message: &Self::ProtoRequest) -> Result { + BufferId::new(message.buffer_id) + } +} + #[async_trait(?Send)] impl LspCommand for GetHover { type Response = Option; diff --git a/crates/project/src/lsp_command/signature_help.rs b/crates/project/src/lsp_command/signature_help.rs new file mode 100644 index 0000000000000..2d3daaeadaa49 --- /dev/null +++ b/crates/project/src/lsp_command/signature_help.rs @@ -0,0 +1,533 @@ +use std::{ops::Range, sync::Arc}; + +use gpui::FontWeight; +use language::{ + markdown::{MarkdownHighlight, MarkdownHighlightStyle}, + Language, +}; + +pub const SIGNATURE_HELP_HIGHLIGHT_CURRENT: MarkdownHighlight = + MarkdownHighlight::Style(MarkdownHighlightStyle { + italic: false, + underline: false, + strikethrough: false, + weight: FontWeight::EXTRA_BOLD, + }); + +pub const SIGNATURE_HELP_HIGHLIGHT_OVERLOAD: MarkdownHighlight = + MarkdownHighlight::Style(MarkdownHighlightStyle { + italic: true, + underline: false, + strikethrough: false, + weight: FontWeight::NORMAL, + }); + +#[derive(Debug)] +pub struct SignatureHelp { + pub markdown: String, + pub highlights: Vec<(Range, MarkdownHighlight)>, +} + +impl SignatureHelp { + pub fn new( + lsp::SignatureHelp { + signatures, + active_signature, + active_parameter, + .. + }: lsp::SignatureHelp, + language: Option>, + ) -> Option { + let function_options_count = signatures.len(); + + let signature_information = active_signature + .and_then(|active_signature| signatures.get(active_signature as usize)) + .or_else(|| signatures.first())?; + + let str_for_join = ", "; + let parameter_length = signature_information + .parameters + .as_ref() + .map(|parameters| parameters.len()) + .unwrap_or(0); + let mut highlight_start = 0; + let (markdown, mut highlights): (Vec<_>, Vec<_>) = signature_information + .parameters + .as_ref()? + .iter() + .enumerate() + .filter_map(|(i, parameter_information)| { + let string = 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 string_length = string.len(); + + let result = if let Some(active_parameter) = active_parameter { + if i == active_parameter as usize { + Some(( + string, + Some(( + highlight_start..(highlight_start + string_length), + SIGNATURE_HELP_HIGHLIGHT_CURRENT, + )), + )) + } else { + Some((string, None)) + } + } else { + Some((string, None)) + }; + + if i != parameter_length { + highlight_start += string_length + str_for_join.len(); + } + + result + }) + .unzip(); + + let result = if markdown.is_empty() { + None + } else { + let markdown = markdown.join(str_for_join); + let language_name = language + .map(|n| n.name().to_lowercase()) + .unwrap_or_default(); + + let markdown = if function_options_count >= 2 { + let suffix = format!("(+{} overload)", function_options_count - 1); + let highlight_start = markdown.len() + 1; + highlights.push(Some(( + highlight_start..(highlight_start + suffix.len()), + SIGNATURE_HELP_HIGHLIGHT_OVERLOAD, + ))); + format!("```{language_name}\n{markdown} {suffix}") + } else { + format!("```{language_name}\n{markdown}") + }; + + Some((markdown, highlights.into_iter().flatten().collect())) + }; + + result.map(|(markdown, highlights)| Self { + markdown, + highlights, + }) + } +} + +#[cfg(test)] +mod tests { + use crate::lsp_command::signature_help::{ + SignatureHelp, SIGNATURE_HELP_HIGHLIGHT_CURRENT, SIGNATURE_HELP_HIGHLIGHT_OVERLOAD, + }; + + #[test] + fn test_create_signature_help_markdown_string_1() { + let signature_help = lsp::SignatureHelp { + signatures: vec![lsp::SignatureInformation { + label: "fn test(foo: u8, bar: &str)".to_string(), + documentation: None, + parameters: Some(vec![ + lsp::ParameterInformation { + label: lsp::ParameterLabel::Simple("foo: u8".to_string()), + documentation: None, + }, + lsp::ParameterInformation { + label: lsp::ParameterLabel::Simple("bar: &str".to_string()), + documentation: None, + }, + ]), + active_parameter: None, + }], + active_signature: Some(0), + active_parameter: Some(0), + }; + let maybe_markdown = SignatureHelp::new(signature_help, None); + assert!(maybe_markdown.is_some()); + + let markdown = maybe_markdown.unwrap(); + let markdown = (markdown.markdown, markdown.highlights); + assert_eq!( + markdown, + ( + "```\nfoo: u8, bar: &str".to_string(), + vec![(0..7, SIGNATURE_HELP_HIGHLIGHT_CURRENT)] + ) + ); + } + + #[test] + fn test_create_signature_help_markdown_string_2() { + let signature_help = lsp::SignatureHelp { + signatures: vec![lsp::SignatureInformation { + label: "fn test(foo: u8, bar: &str)".to_string(), + documentation: None, + parameters: Some(vec![ + lsp::ParameterInformation { + label: lsp::ParameterLabel::Simple("foo: u8".to_string()), + documentation: None, + }, + lsp::ParameterInformation { + label: lsp::ParameterLabel::Simple("bar: &str".to_string()), + documentation: None, + }, + ]), + active_parameter: None, + }], + active_signature: Some(0), + active_parameter: Some(1), + }; + let maybe_markdown = SignatureHelp::new(signature_help, None); + assert!(maybe_markdown.is_some()); + + let markdown = maybe_markdown.unwrap(); + let markdown = (markdown.markdown, markdown.highlights); + assert_eq!( + markdown, + ( + "```\nfoo: u8, bar: &str".to_string(), + vec![(9..18, SIGNATURE_HELP_HIGHLIGHT_CURRENT)] + ) + ); + } + + #[test] + fn test_create_signature_help_markdown_string_3() { + let signature_help = lsp::SignatureHelp { + signatures: vec![ + lsp::SignatureInformation { + label: "fn test1(foo: u8, bar: &str)".to_string(), + documentation: None, + parameters: Some(vec![ + lsp::ParameterInformation { + label: lsp::ParameterLabel::Simple("foo: u8".to_string()), + documentation: None, + }, + lsp::ParameterInformation { + label: lsp::ParameterLabel::Simple("bar: &str".to_string()), + documentation: None, + }, + ]), + active_parameter: None, + }, + lsp::SignatureInformation { + label: "fn test2(hoge: String, fuga: bool)".to_string(), + documentation: None, + parameters: Some(vec![ + lsp::ParameterInformation { + label: lsp::ParameterLabel::Simple("hoge: String".to_string()), + documentation: None, + }, + lsp::ParameterInformation { + label: lsp::ParameterLabel::Simple("fuga: bool".to_string()), + documentation: None, + }, + ]), + active_parameter: None, + }, + ], + active_signature: Some(0), + active_parameter: Some(0), + }; + let maybe_markdown = SignatureHelp::new(signature_help, None); + assert!(maybe_markdown.is_some()); + + let markdown = maybe_markdown.unwrap(); + let markdown = (markdown.markdown, markdown.highlights); + assert_eq!( + markdown, + ( + "```\nfoo: u8, bar: &str (+1 overload)".to_string(), + vec![ + (0..7, SIGNATURE_HELP_HIGHLIGHT_CURRENT), + (19..32, SIGNATURE_HELP_HIGHLIGHT_OVERLOAD) + ] + ) + ); + } + + #[test] + fn test_create_signature_help_markdown_string_4() { + let signature_help = lsp::SignatureHelp { + signatures: vec![ + lsp::SignatureInformation { + label: "fn test1(foo: u8, bar: &str)".to_string(), + documentation: None, + parameters: Some(vec![ + lsp::ParameterInformation { + label: lsp::ParameterLabel::Simple("foo: u8".to_string()), + documentation: None, + }, + lsp::ParameterInformation { + label: lsp::ParameterLabel::Simple("bar: &str".to_string()), + documentation: None, + }, + ]), + active_parameter: None, + }, + lsp::SignatureInformation { + label: "fn test2(hoge: String, fuga: bool)".to_string(), + documentation: None, + parameters: Some(vec![ + lsp::ParameterInformation { + label: lsp::ParameterLabel::Simple("hoge: String".to_string()), + documentation: None, + }, + lsp::ParameterInformation { + label: lsp::ParameterLabel::Simple("fuga: bool".to_string()), + documentation: None, + }, + ]), + active_parameter: None, + }, + ], + active_signature: Some(1), + active_parameter: Some(0), + }; + let maybe_markdown = SignatureHelp::new(signature_help, None); + assert!(maybe_markdown.is_some()); + + let markdown = maybe_markdown.unwrap(); + let markdown = (markdown.markdown, markdown.highlights); + assert_eq!( + markdown, + ( + "```\nhoge: String, fuga: bool (+1 overload)".to_string(), + vec![ + (0..12, SIGNATURE_HELP_HIGHLIGHT_CURRENT), + (25..38, SIGNATURE_HELP_HIGHLIGHT_OVERLOAD) + ] + ) + ); + } + + #[test] + fn test_create_signature_help_markdown_string_5() { + let signature_help = lsp::SignatureHelp { + signatures: vec![ + lsp::SignatureInformation { + label: "fn test1(foo: u8, bar: &str)".to_string(), + documentation: None, + parameters: Some(vec![ + lsp::ParameterInformation { + label: lsp::ParameterLabel::Simple("foo: u8".to_string()), + documentation: None, + }, + lsp::ParameterInformation { + label: lsp::ParameterLabel::Simple("bar: &str".to_string()), + documentation: None, + }, + ]), + active_parameter: None, + }, + lsp::SignatureInformation { + label: "fn test2(hoge: String, fuga: bool)".to_string(), + documentation: None, + parameters: Some(vec![ + lsp::ParameterInformation { + label: lsp::ParameterLabel::Simple("hoge: String".to_string()), + documentation: None, + }, + lsp::ParameterInformation { + label: lsp::ParameterLabel::Simple("fuga: bool".to_string()), + documentation: None, + }, + ]), + active_parameter: None, + }, + ], + active_signature: Some(1), + active_parameter: Some(1), + }; + let maybe_markdown = SignatureHelp::new(signature_help, None); + assert!(maybe_markdown.is_some()); + + let markdown = maybe_markdown.unwrap(); + let markdown = (markdown.markdown, markdown.highlights); + assert_eq!( + markdown, + ( + "```\nhoge: String, fuga: bool (+1 overload)".to_string(), + vec![ + (14..24, SIGNATURE_HELP_HIGHLIGHT_CURRENT), + (25..38, SIGNATURE_HELP_HIGHLIGHT_OVERLOAD) + ] + ) + ); + } + + #[test] + fn test_create_signature_help_markdown_string_6() { + let signature_help = lsp::SignatureHelp { + signatures: vec![ + lsp::SignatureInformation { + label: "fn test1(foo: u8, bar: &str)".to_string(), + documentation: None, + parameters: Some(vec![ + lsp::ParameterInformation { + label: lsp::ParameterLabel::Simple("foo: u8".to_string()), + documentation: None, + }, + lsp::ParameterInformation { + label: lsp::ParameterLabel::Simple("bar: &str".to_string()), + documentation: None, + }, + ]), + active_parameter: None, + }, + lsp::SignatureInformation { + label: "fn test2(hoge: String, fuga: bool)".to_string(), + documentation: None, + parameters: Some(vec![ + lsp::ParameterInformation { + label: lsp::ParameterLabel::Simple("hoge: String".to_string()), + documentation: None, + }, + lsp::ParameterInformation { + label: lsp::ParameterLabel::Simple("fuga: bool".to_string()), + documentation: None, + }, + ]), + active_parameter: None, + }, + ], + active_signature: Some(1), + active_parameter: None, + }; + let maybe_markdown = SignatureHelp::new(signature_help, None); + assert!(maybe_markdown.is_some()); + + let markdown = maybe_markdown.unwrap(); + let markdown = (markdown.markdown, markdown.highlights); + assert_eq!( + markdown, + ( + "```\nhoge: String, fuga: bool (+1 overload)".to_string(), + vec![(25..38, SIGNATURE_HELP_HIGHLIGHT_OVERLOAD)] + ) + ); + } + + #[test] + fn test_create_signature_help_markdown_string_7() { + let signature_help = lsp::SignatureHelp { + signatures: vec![ + lsp::SignatureInformation { + label: "fn test1(foo: u8, bar: &str)".to_string(), + documentation: None, + parameters: Some(vec![ + lsp::ParameterInformation { + label: lsp::ParameterLabel::Simple("foo: u8".to_string()), + documentation: None, + }, + lsp::ParameterInformation { + label: lsp::ParameterLabel::Simple("bar: &str".to_string()), + documentation: None, + }, + ]), + active_parameter: None, + }, + lsp::SignatureInformation { + label: "fn test2(hoge: String, fuga: bool)".to_string(), + documentation: None, + parameters: Some(vec![ + lsp::ParameterInformation { + label: lsp::ParameterLabel::Simple("hoge: String".to_string()), + documentation: None, + }, + lsp::ParameterInformation { + label: lsp::ParameterLabel::Simple("fuga: bool".to_string()), + documentation: None, + }, + ]), + active_parameter: None, + }, + lsp::SignatureInformation { + label: "fn test3(one: usize, two: u32)".to_string(), + documentation: None, + parameters: Some(vec![ + lsp::ParameterInformation { + label: lsp::ParameterLabel::Simple("one: usize".to_string()), + documentation: None, + }, + lsp::ParameterInformation { + label: lsp::ParameterLabel::Simple("two: u32".to_string()), + documentation: None, + }, + ]), + active_parameter: None, + }, + ], + active_signature: Some(2), + active_parameter: Some(1), + }; + let maybe_markdown = SignatureHelp::new(signature_help, None); + assert!(maybe_markdown.is_some()); + + let markdown = maybe_markdown.unwrap(); + let markdown = (markdown.markdown, markdown.highlights); + assert_eq!( + markdown, + ( + "```\none: usize, two: u32 (+2 overload)".to_string(), + vec![ + (12..20, SIGNATURE_HELP_HIGHLIGHT_CURRENT), + (21..34, SIGNATURE_HELP_HIGHLIGHT_OVERLOAD) + ] + ) + ); + } + + #[test] + fn test_create_signature_help_markdown_string_8() { + let signature_help = lsp::SignatureHelp { + signatures: vec![], + active_signature: None, + active_parameter: None, + }; + let maybe_markdown = SignatureHelp::new(signature_help, None); + assert!(maybe_markdown.is_none()); + } + + #[test] + fn test_create_signature_help_markdown_string_9() { + let signature_help = lsp::SignatureHelp { + signatures: vec![lsp::SignatureInformation { + label: "fn test(foo: u8, bar: &str)".to_string(), + documentation: None, + parameters: Some(vec![ + lsp::ParameterInformation { + label: lsp::ParameterLabel::LabelOffsets([8, 15]), + documentation: None, + }, + lsp::ParameterInformation { + label: lsp::ParameterLabel::LabelOffsets([17, 26]), + documentation: None, + }, + ]), + active_parameter: None, + }], + active_signature: Some(0), + active_parameter: Some(0), + }; + let maybe_markdown = SignatureHelp::new(signature_help, None); + assert!(maybe_markdown.is_some()); + + let markdown = maybe_markdown.unwrap(); + let markdown = (markdown.markdown, markdown.highlights); + assert_eq!( + markdown, + ( + "```\nfoo: u8, bar: &str".to_string(), + vec![(0..7, SIGNATURE_HELP_HIGHLIGHT_CURRENT)] + ) + ); + } +} diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index a34b5fdad7ea9..372ef786f6694 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -709,6 +709,7 @@ impl Project { client.add_model_request_handler(Self::handle_task_context_for_location); client.add_model_request_handler(Self::handle_task_templates); client.add_model_request_handler(Self::handle_lsp_command::); + client.add_model_request_handler(Self::handle_signature_help); } pub fn local( @@ -5778,6 +5779,63 @@ impl Project { } } + pub fn signature_help( + &self, + buffer: &Model, + position: T, + cx: &mut ModelContext, + ) -> Task> { + let position = position.to_point_utf16(buffer.read(cx)); + if self.is_local() { + let all_actions_task = self.request_multiple_lsp_locally( + buffer, + Some(position), + |server_capabilities| server_capabilities.signature_help_provider.is_some(), + GetSignatureHelp { position }, + cx, + ); + cx.spawn(|_, _| async move { + all_actions_task + .await + .into_iter() + .flatten() + .filter(|help| !help.markdown.is_empty()) + .collect::>() + }) + } else if let Some(project_id) = self.remote_id() { + let position_anchor = buffer + .read(cx) + .anchor_at(buffer.read(cx).point_utf16_to_offset(position), Bias::Right); + let request = self.client.request(proto::GetSignatureHelp { + project_id, + position: Some(serialize_anchor(&position_anchor)), + buffer_id: buffer.read(cx).remote_id().to_proto(), + version: serialize_version(&buffer.read(cx).version()), + }); + let buffer = buffer.clone(); + cx.spawn(move |project, cx| async move { + let Some(response) = request.await.log_err() else { + return Vec::new(); + }; + let Some(project) = project.upgrade() else { + return Vec::new(); + }; + GetSignatureHelp::response_from_proto( + GetSignatureHelp { position }, + response, + project, + buffer, + cx, + ) + .await + .log_err() + .unwrap_or_default() + }) + } else { + Task::ready(Vec::new()) + } + } + fn hover_impl( &self, buffer: &Model, @@ -9851,6 +9909,43 @@ impl Project { Ok(proto::TaskTemplatesResponse { templates }) } + async fn handle_signature_help( + project: Model, + envelope: TypedEnvelope, + mut cx: AsyncAppContext, + ) -> Result { + let sender_id = envelope.original_sender_id()?; + let buffer_id = BufferId::new(envelope.payload.buffer_id)?; + let buffer = project.update(&mut cx, |project, _| { + project + .opened_buffers + .get(&buffer_id) + .and_then(|buffer| buffer.upgrade()) + .with_context(|| format!("unknown buffer id {}", envelope.payload.buffer_id)) + })??; + let response = GetSignatureHelp::from_proto( + envelope.payload.clone(), + project.clone(), + buffer.clone(), + cx.clone(), + ) + .await?; + let help_response = project + .update(&mut cx, |project, cx| { + project.signature_help(&buffer, response.position, cx) + })? + .await; + project.update(&mut cx, |project, cx| { + GetSignatureHelp::response_to_proto( + help_response, + project, + sender_id, + &buffer.read(cx).version(), + cx, + ) + }) + } + async fn try_resolve_code_action( lang_server: &LanguageServer, action: &mut CodeAction, diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index d410da716e1f3..ec23aaccbb2c8 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -262,7 +262,10 @@ message Envelope { OpenContextResponse open_context_response = 213; UpdateContext update_context = 214; SynchronizeContexts synchronize_contexts = 215; - SynchronizeContextsResponse synchronize_contexts_response = 216; // current max + SynchronizeContextsResponse synchronize_contexts_response = 216; + + GetSignatureHelp get_signature_help = 217; + GetSignatureHelpResponse get_signature_help_response = 218; // current max } reserved 158 to 161; @@ -934,6 +937,34 @@ message GetCodeActionsResponse { repeated VectorClockEntry version = 2; } +message GetSignatureHelp { + uint64 project_id = 1; + uint64 buffer_id = 2; + Anchor position = 3; + repeated VectorClockEntry version = 4; +} + +message GetSignatureHelpResponse { + repeated SignatureHelp entries = 1; +} + +message SignatureHelp { + string rendered_text = 1; + repeated HighlightedRange highlights = 2; +} + +message HighlightedRange { + Range range = 1; + MarkdownHighlight highlight = 2; +} + +message MarkdownHighlight { + bool italic = 1; + bool underline = 2; + bool strikethrough = 3; + float weight = 4; +} + message GetHover { uint64 project_id = 1; uint64 buffer_id = 2; diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index 7808aa5bab932..047a072792f1f 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -204,6 +204,8 @@ messages!( (GetProjectSymbolsResponse, Background), (GetReferences, Background), (GetReferencesResponse, Background), + (GetSignatureHelp, Background), + (GetSignatureHelpResponse, Background), (GetSupermavenApiKey, Background), (GetSupermavenApiKeyResponse, Background), (GetTypeDefinition, Background), @@ -382,6 +384,7 @@ request_messages!( (GetPrivateUserInfo, GetPrivateUserInfoResponse), (GetProjectSymbols, GetProjectSymbolsResponse), (GetReferences, GetReferencesResponse), + (GetSignatureHelp, GetSignatureHelpResponse), (GetSupermavenApiKey, GetSupermavenApiKeyResponse), (GetTypeDefinition, GetTypeDefinitionResponse), (LinkedEditingRange, LinkedEditingRangeResponse), @@ -482,6 +485,7 @@ entity_messages!( GetHover, GetProjectSymbols, GetReferences, + GetSignatureHelp, GetTypeDefinition, InlayHints, JoinProject, diff --git a/crates/quick_action_bar/src/quick_action_bar.rs b/crates/quick_action_bar/src/quick_action_bar.rs index 89e57ab8125ff..caa36db8585df 100644 --- a/crates/quick_action_bar/src/quick_action_bar.rs +++ b/crates/quick_action_bar/src/quick_action_bar.rs @@ -102,18 +102,21 @@ impl Render for QuickActionBar { inlay_hints_enabled, supports_inlay_hints, git_blame_inline_enabled, + auto_signature_help_enabled, ) = { let editor = editor.read(cx); let selection_menu_enabled = editor.selection_menu_enabled(cx); let inlay_hints_enabled = editor.inlay_hints_enabled(); let supports_inlay_hints = editor.supports_inlay_hints(cx); let git_blame_inline_enabled = editor.git_blame_inline_enabled(); + let auto_signature_help_enabled = editor.auto_signature_help_enabled(cx); ( selection_menu_enabled, inlay_hints_enabled, supports_inlay_hints, git_blame_inline_enabled, + auto_signature_help_enabled, ) }; @@ -265,6 +268,23 @@ impl Render for QuickActionBar { }, ); + menu = menu.toggleable_entry( + "Auto Signature Help", + auto_signature_help_enabled, + Some(editor::actions::ToggleAutoSignatureHelp.boxed_clone()), + { + let editor = editor.clone(); + move |cx| { + editor.update(cx, |editor, cx| { + editor.toggle_auto_signature_help_menu( + &editor::actions::ToggleAutoSignatureHelp, + cx, + ); + }); + } + }, + ); + menu }); cx.subscribe(&menu, |quick_action_bar, _, _: &DismissEvent, _cx| {