use lsp_types::{
ClientCapabilities, Documentation, MarkupContent, MarkupKind, ParameterInformation,
ParameterLabel, ServerCapabilities, SignatureHelp, SignatureHelpOptions, SignatureHelpParams,
SignatureInformation,
};
use tokio_util::sync::CancellationToken;
use super::RegisterCapabilities;
use crate::context::{RequestOutcome, ServerContextSnapshot, snapshot_query};
use crate::util::uri_to_file_path;
use cpp_parser::CppDocComment;
pub async fn on_signature_help(
context: ServerContextSnapshot,
params: SignatureHelpParams,
cancel_token: CancellationToken,
) -> RequestOutcome<SignatureHelp> {
let uri = params.text_document_position_params.text_document.uri;
let position = params.text_document_position_params.position;
if let Some(path) = uri_to_file_path(&uri) {
context.analysis().prepare(&path).await;
crate::handlers::read_the_modules(&context, &path, false).await;
}
snapshot_query(context.analysis(), cancel_token, move |session| {
let path = uri_to_file_path(&uri)?;
let written = session.view_of_the_file(&path)?;
let in_the_file = crate::util::file_offset_at_position(&written, position)?;
let view = session.view(&path)?;
let offset = view.reading_offset_of(
cpp_code_analysis::file::coordinates::FileOffset::new(in_the_file.get()),
)?;
if !session.is_locally_ready(&path) {
log::debug!(
"no signature yet: {} file(s) are queued, and where the callee is declared is not known",
session.pending()
);
return None;
}
let signatures = session.signatures_at(&view, offset);
if signatures.is_empty() {
return None;
}
Some(signature_help_of(&signatures))
})
.await
}
pub fn signature_help_of(
signatures: &[cpp_code_analysis::signature::CallSignature],
) -> SignatureHelp {
let rendered: Vec<SignatureInformation> = signatures
.iter()
.map(|signature| SignatureInformation {
label: signature.label.clone(),
documentation: signature
.documentation
.as_ref()
.and_then(brief_of)
.map(|value| {
Documentation::MarkupContent(MarkupContent {
kind: MarkupKind::Markdown,
value,
})
}),
parameters: Some(
signature
.parameters
.iter()
.map(|(range, written)| ParameterInformation {
label: ParameterLabel::LabelOffsets([range.start as u32, range.end as u32]),
documentation: signature
.documentation
.as_ref()
.and_then(|comment| parameter_documentation(comment, written))
.map(|value| {
Documentation::MarkupContent(MarkupContent {
kind: MarkupKind::Markdown,
value,
})
}),
})
.collect(),
),
active_parameter: signature.active_parameter.map(|at| at as u32),
})
.collect();
SignatureHelp {
active_parameter: rendered.first().and_then(|first| first.active_parameter),
signatures: rendered,
active_signature: Some(0),
}
}
fn brief_of(comment: &CppDocComment) -> Option<String> {
if !comment.is_documentation() {
return None;
}
use cpp_parser::DocCommandKind;
let text = comment.get_comment_text();
let mut kept = String::with_capacity(text.len());
for line in text.lines() {
let content = line.trim_start().trim_start_matches("///").trim_start();
let content = content
.strip_prefix("*")
.map_or(content, |rest| rest.trim_start());
let name = content.strip_prefix(['@', '\\']).map(|rest| {
rest.chars()
.take_while(|character| character.is_ascii_alphanumeric())
.collect::<String>()
});
let is_a_parameter_line =
name.as_deref().and_then(cpp_parser::command_kind) == Some(DocCommandKind::Parameter);
if !is_a_parameter_line {
kept.push_str(line);
kept.push('\n');
}
}
let kept = kept.trim();
(!kept.is_empty()).then(|| kept.to_string())
}
fn parameter_documentation(comment: &CppDocComment, written: &str) -> Option<String> {
use cpp_parser::DocCommandKind;
let name = written.split_whitespace().last()?;
let name = name.trim_matches(['*', '&']);
comment
.get_commands_of_kind(DocCommandKind::Parameter)
.find(|command| command.get_argument_text().as_deref() == Some(name))
.and_then(|command| command.get_body_text())
.map(|body| body.trim().to_string())
.filter(|body| !body.is_empty())
}
pub struct SignatureHelpCapabilities;
impl RegisterCapabilities for SignatureHelpCapabilities {
fn register_capabilities(server_capabilities: &mut ServerCapabilities, _: &ClientCapabilities) {
server_capabilities.signature_help_provider = Some(SignatureHelpOptions {
trigger_characters: Some(vec!["(".to_string(), ",".to_string()]),
retrigger_characters: Some(vec![",".to_string()]),
work_done_progress_options: Default::default(),
});
}
}
#[cfg(test)]
mod tests {
use super::*;
use cpp_code_analysis::signature::CallSignature;
use std::path::PathBuf;
fn a_signature() -> CallSignature {
let label = "scale(int count, double factor)".to_string();
let span = |text: &str| {
let start = label.find(text).expect("the fixture writes it");
start..start + text.len()
};
CallSignature {
parameters: vec![
(span("int count"), "int count".to_string()),
(span("double factor"), "double factor".to_string()),
],
label,
active_parameter: Some(1),
declared_in: PathBuf::from("/p/a.cpp"),
declared_at: 7,
documentation: None,
}
}
#[test]
fn a_doc_comment_is_split_between_the_signature_and_its_parameters() {
use cpp_parser::CppAstNode as _;
let text = "\
/// Scales a value by a factor, keeping the sign.
///
/// @param count how many times to scale
/// @param factor what to multiply by
/// @return the scaled value
double scale(int count, double factor);
";
let tree = cpp_parser::CppParser::parse(text, cpp_parser::ParserConfig::default());
let root = tree.get_red_root();
let comment = root
.descendants()
.find(|node| {
cpp_parser::CppSyntaxKind::from(node.kind())
== cpp_parser::CppSyntaxKind::DocComment
})
.and_then(CppDocComment::cast)
.expect("the fixture writes a doc comment");
let brief = brief_of(&comment).expect("there is prose");
println!("brief: {brief:?}");
assert!(
brief.contains("Scales a value by a factor"),
"the function's own sentence is kept: {brief:?}"
);
assert!(
!brief.contains("@param"),
"and the parameter lines are not repeated above the signature: {brief:?}"
);
assert!(
brief.contains("@return"),
"a command that is prose stays: {brief:?}"
);
assert_eq!(
parameter_documentation(&comment, "int count").as_deref(),
Some("how many times to scale")
);
assert_eq!(
parameter_documentation(&comment, "double factor").as_deref(),
Some("what to multiply by")
);
assert_eq!(
parameter_documentation(&comment, "int missing"),
None,
"a parameter with no `@param` line has no documentation rather than somebody else's"
);
}
#[test]
fn a_parameter_is_a_span_inside_the_label() {
let help = signature_help_of(&[a_signature()]);
assert_eq!(help.signatures.len(), 1);
let label = &help.signatures[0].label;
let spans: Vec<&str> = help.signatures[0]
.parameters
.as_ref()
.expect("the parameters are sent")
.iter()
.map(|parameter| match ¶meter.label {
ParameterLabel::LabelOffsets([start, end]) => {
&label[*start as usize..*end as usize]
}
other => panic!("offsets were expected, not {other:?}"),
})
.collect();
assert_eq!(spans, vec!["int count", "double factor"]);
assert_eq!(help.active_signature, Some(0));
assert_eq!(help.active_parameter, Some(1));
}
#[test]
fn a_signature_without_documentation_sends_none() {
let help = signature_help_of(&[a_signature()]);
assert!(help.signatures[0].documentation.is_none());
}
#[test]
fn every_overload_is_sent_with_its_own_active_parameter() {
let first = a_signature();
let second = CallSignature {
label: "scale(double factor)".to_string(),
parameters: vec![(9..9 + "double factor".len(), "double factor".to_string())],
active_parameter: None,
..first.clone()
};
let help = signature_help_of(&[first, second]);
assert_eq!(help.signatures.len(), 2);
assert_eq!(help.signatures[0].label, "scale(int count, double factor)");
assert_eq!(help.signatures[1].label, "scale(double factor)");
assert_eq!(
help.signatures[0].active_parameter,
Some(1),
"the signature's own count, not the popup's"
);
assert_eq!(help.signatures[1].active_parameter, None);
assert_eq!(
help.active_parameter,
Some(1),
"and the first is the active one"
);
}
}