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
19 changed files with 1994 additions and 11 deletions
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)]
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue