Detailed changes
@@ -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"
}
},
@@ -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"
}
},
{
@@ -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
@@ -642,7 +642,10 @@ impl Server {
app_state.config.openai_api_key.clone(),
)
})
- });
+ })
+ .add_request_handler(user_handler(
+ forward_read_only_project_request::<proto::GetSignatureHelp>,
+ ));
Arc::new(server)
}
@@ -286,12 +286,14 @@ gpui::actions!(
SelectPageUp,
ShowCharacterPalette,
ShowInlineCompletion,
+ ShowSignatureHelp,
ShuffleLines,
SortLinesCaseInsensitive,
SortLinesCaseSensitive,
SplitSelectionIntoLines,
Tab,
TabPrev,
+ ToggleAutoSignatureHelp,
ToggleGitBlame,
ToggleGitBlameInline,
ToggleSelectionMenu,
@@ -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<Option<ContextMenu>>,
mouse_context_menu: Option<MouseContextMenu>,
completion_tasks: Vec<(CompletionId, Task<Option<()>>)>,
+ signature_help_state: SignatureHelpState,
+ auto_signature_help: Option<bool>,
find_all_references_task_sources: Vec<Anchor>,
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<str> = Arc::from("");
@@ -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<bool>,
+ /// Whether to automatically show a signature help pop-up or not.
+ ///
+ /// Default: false
+ pub auto_signature_help: Option<bool>,
+
+ /// Whether to show the signature help pop-up after completions or bracket pairs inserted.
+ ///
+ /// Default: true
+ pub show_signature_help_after_edits: Option<bool>,
+
/// Jupyter REPL settings.
pub jupyter: Option<Jupyter>,
}
@@ -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::<SettingsStore, _>(|settings, cx| {
+ settings.update_user_settings::<EditorSettings>(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::<SettingsStore, _>(|settings, cx| {
+ settings.update_user_settings::<EditorSettings>(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::<SettingsStore, _>(|settings, cx| {
+ settings.update_user_settings::<EditorSettings>(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::<SettingsStore, _>(|settings, cx| {
+ settings.update_user_settings::<EditorSettings>(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::<SettingsStore, _>(|settings, cx| {
+ settings.update_user_settings::<EditorSettings>(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<Output = ()> {
+ let mut request =
+ cx.handle_request::<lsp::request::SignatureHelpRequest, _, _>(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
@@ -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<Pixels>,
+ scroll_pixel_position: gpui::Point<Pixels>,
+ display_point: Option<DisplayPoint>,
+ 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::<AvailableSpace>::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,
@@ -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>,
+ ) {
+ 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<Self>,
+ 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<Self>,
+ ) -> bool {
+ if !(self.signature_help_state.is_shown() || self.auto_signature_help_enabled(cx)) {
+ return false;
+ }
+ let newest_selection = self.selections.newest::<usize>(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<usize>, end: Range<usize>| {
+ 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<Self>) {
+ 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();
+ }));
+ }
+}
@@ -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<Pixels>,
+ workspace: Option<WeakView<Workspace>>,
+ cx: &mut ViewContext<Editor>,
+ ) -> 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()
+ }
+}
@@ -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<Task<()>>,
+ popover: Option<SignatureHelpPopover>,
+ hidden_by: Option<SignatureHelpHiddenBy>,
+ 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()
+ }
+}
@@ -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,
@@ -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<SignatureHelp>;
+ 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<LanguageServer>,
+ _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<lsp::SignatureHelp>,
+ _: Model<Project>,
+ buffer: Model<Buffer>,
+ _: LanguageServerId,
+ mut cx: AsyncAppContext,
+ ) -> Result<Self::Response> {
+ 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<Project>,
+ buffer: Model<Buffer>,
+ mut cx: AsyncAppContext,
+ ) -> Result<Self> {
+ 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<Project>,
+ _: Model<Buffer>,
+ _: AsyncAppContext,
+ ) -> Result<Self::Response> {
+ 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> {
+ BufferId::new(message.buffer_id)
+ }
+}
+
#[async_trait(?Send)]
impl LspCommand for GetHover {
type Response = Option<Hover>;
@@ -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<usize>, MarkdownHighlight)>,
+}
+
+impl SignatureHelp {
+ pub fn new(
+ lsp::SignatureHelp {
+ signatures,
+ active_signature,
+ active_parameter,
+ ..
+ }: lsp::SignatureHelp,
+ language: Option<Arc<Language>>,
+ ) -> Option<Self> {
+ 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::<String>(),
+ };
+ 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)]
+ )
+ );
+ }
+}
@@ -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::<LinkedEditingRange>);
+ client.add_model_request_handler(Self::handle_signature_help);
}
pub fn local(
@@ -5778,6 +5779,63 @@ impl Project {
}
}
+ pub fn signature_help<T: ToPointUtf16>(
+ &self,
+ buffer: &Model<Buffer>,
+ position: T,
+ cx: &mut ModelContext<Self>,
+ ) -> Task<Vec<SignatureHelp>> {
+ 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::<Vec<_>>()
+ })
+ } 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<Buffer>,
@@ -9851,6 +9909,43 @@ impl Project {
Ok(proto::TaskTemplatesResponse { templates })
}
+ async fn handle_signature_help(
+ project: Model<Self>,
+ envelope: TypedEnvelope<proto::GetSignatureHelp>,
+ mut cx: AsyncAppContext,
+ ) -> Result<proto::GetSignatureHelpResponse> {
+ 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,
@@ -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;
@@ -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,
@@ -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| {