mirror of
https://github.com/zed-industries/zed.git
synced 2024-12-24 06:12:25 +03:00
lsp: Implement textDocument/signatureHelp
for ProjectClientState::Local
environment (#12909)
Closes https://github.com/zed-industries/zed/issues/5155 Closes https://github.com/zed-industries/zed/issues/4879 # Purpose There was no way to know what to put in function signatures or struct fields other than hovering at the moment. Therefore, it was necessary to implement LSP's `textDocument/signatureHelp`. I tried my best to match the surrounding coding style, but since this is my first contribution, I believe there are various aspects that may be lacking. I would greatly appreciate your code review. # Description When the window is displayed, the current argument or field at the cursor's position is automatically bolded. If the cursor moves and there is nothing to display, the window closes automatically. To minimize changes and reduce the burden of review and debugging, the SignatureHelp feature is implemented only when `is_local` is `true`. Some `unimplemented!()` macros are embedded, but rest assured that they are not called in this implementation. # How to try it out Press `cmd + i` (MacOS), `ctrl + i` (Linux). # Enable auto signature help (2 ways) ### Add `"auto_signature_help": true` to `settings.json` <img width="426" alt="image" src="https://github.com/zed-industries/zed/assets/55743826/61310c39-47f9-4586-94b0-ae519dc3b37c"> Or ### Press `Auto Signature Help`. (Default `false`) <img width="226" alt="image" src="https://github.com/zed-industries/zed/assets/55743826/34155215-1eb5-4621-b09b-55df2f1ab6a8"> # Disable to show signature help after completion ### Add `"show_signature_help_after_completion": false` to `settings.json` <img width="438" alt="image" src="https://github.com/zed-industries/zed/assets/55743826/5e5bacac-62e0-4921-9243-17e1e72d5eb6"> # Movie https://github.com/zed-industries/zed/assets/55743826/77c12d51-b0a5-415d-8901-f93ef92098e7 # Screen Shot <img width="628" alt="image" src="https://github.com/zed-industries/zed/assets/55743826/3ebcf4b6-2b94-4dea-97f9-ac4f33e0291e"> <img width="637" alt="image" src="https://github.com/zed-industries/zed/assets/55743826/6dc3eb4d-beee-460b-8dbe-d6eec6379b76"> Release Notes: - Show function signature popovers ([4879](https://github.com/zed-industries/zed/issues/4879), [5155](https://github.com/zed-industries/zed/issues/5155)) --------- Co-authored-by: Kirill Bulatov <kirill@zed.dev>
This commit is contained in:
parent
6a11184ea3
commit
291d64c803
@ -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,
|
||||
|
225
crates/editor/src/signature_help.rs
Normal file
225
crates/editor/src/signature_help.rs
Normal file
@ -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();
|
||||
}));
|
||||
}
|
||||
}
|
48
crates/editor/src/signature_help/popover.rs
Normal file
48
crates/editor/src/signature_help/popover.rs
Normal file
@ -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()
|
||||
}
|
||||
}
|
65
crates/editor/src/signature_help/state.rs
Normal file
65
crates/editor/src/signature_help/state.rs
Normal file
@ -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>;
|
||||
|
533
crates/project/src/lsp_command/signature_help.rs
Normal file
533
crates/project/src/lsp_command/signature_help.rs
Normal file
@ -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| {
|
||||
|
Loading…
Reference in New Issue
Block a user