use super::*;
pub(crate) fn signature_help_via_db(
snapshot: &Analysis,
path: &Path,
text: &str,
position: Position,
) -> Option<SignatureHelp> {
let line_index = LineIndex::new(text);
let offset = line_index.position_to_byte(position).min(text.len());
let index = snapshot.library_data().unwrap_or_default();
let cached = salsa::Cancelled::catch(AssertUnwindSafe(|| {
let file = snapshot.lookup_file(path)?;
if snapshot.file_text(file) != text {
return None;
}
let root = snapshot.parsed_tree(file);
Some(signature_help_from_node(&root, offset, &index))
}));
match cached {
Ok(Some(help)) => help,
Ok(None) | Err(_) => {
let root = parse(text).cst;
signature_help_from_node(&root, offset, &index)
}
}
}
pub fn compute_signature_help(
text: &str,
offset: usize,
indexed: &IndexedProvider,
) -> Option<SignatureHelp> {
let root = parse(text).cst;
signature_help_from_node(&root, offset.min(text.len()), indexed)
}
pub(crate) fn signature_help_from_node(
root: &SyntaxNode,
offset: usize,
indexed: &IndexedProvider,
) -> Option<SignatureHelp> {
let offset = TextSize::new(offset as u32);
let call = enclosing_call(root, offset)?;
let callee = call.callee_token()?;
let query = symbol_query_at(root, callee.text_range().start())?;
let (_package, entry, _range) = resolve_query(query, root, indexed)?;
let (label, parameters) = build_signature(entry)?;
let active = active_parameter(call.arg_list().as_ref(), offset, entry, parameters.len());
let info = SignatureInformation {
label,
documentation: signature_documentation(entry),
parameters: (!parameters.is_empty()).then_some(parameters),
active_parameter: active,
};
Some(SignatureHelp {
signatures: vec![info],
active_signature: Some(0),
active_parameter: active,
})
}
fn enclosing_call(root: &SyntaxNode, offset: TextSize) -> Option<CallExpr> {
let token = match root.token_at_offset(offset) {
TokenAtOffset::None => return None,
TokenAtOffset::Single(t) => t,
TokenAtOffset::Between(_left, right) => right,
};
let call = token
.parent_ancestors()
.find(|n| n.kind() == SyntaxKind::CALL_EXPR)
.and_then(CallExpr::cast)?;
if let Some(rparen) = call.arg_list().and_then(|al| {
al.syntax()
.children_with_tokens()
.find(|el| el.kind() == SyntaxKind::RPAREN)
}) && offset > rparen.text_range().start()
{
return None;
}
Some(call)
}
fn build_signature(entry: &SymbolEntry) -> Option<(String, Vec<ParameterInformation>)> {
if let Some(formals) = &entry.formals {
let mut label = String::new();
label.push_str(&entry.name);
label.push('(');
let mut parameters = Vec::with_capacity(formals.len());
for (i, formal) in formals.iter().enumerate() {
if i > 0 {
label.push_str(", ");
}
let start = label.encode_utf16().count() as u32;
label.push_str(&format_formal(formal));
let end = label.encode_utf16().count() as u32;
parameters.push(ParameterInformation {
label: ParameterLabel::LabelOffsets([start, end]),
documentation: parameter_documentation(entry, &formal.name),
});
}
label.push(')');
Some((label, parameters))
} else {
let usage = entry.help.as_ref().and_then(|h| h.usage.as_deref())?;
Some((usage.to_string(), Vec::new()))
}
}
fn active_parameter(
arg_list: Option<&ArgList>,
offset: TextSize,
entry: &SymbolEntry,
param_count: usize,
) -> Option<u32> {
if param_count == 0 {
return None;
}
let Some(arg_list) = arg_list else {
return Some(0);
};
if let Some(name) = active_named_arg(arg_list, offset)
&& let Some(idx) = entry
.formals
.as_ref()
.and_then(|formals| formals.iter().position(|f| f.name == name))
{
return Some(idx as u32);
}
let positional = arg_list
.syntax()
.children_with_tokens()
.filter(|el| el.kind() == SyntaxKind::COMMA && el.text_range().end() <= offset)
.count();
Some(positional.min(param_count - 1) as u32)
}
fn active_named_arg(arg_list: &ArgList, offset: TextSize) -> Option<SmolStr> {
let arg = arg_list
.args()
.find(|a| a.syntax().text_range().contains_inclusive(offset))?;
let mut significant = arg
.syntax()
.children_with_tokens()
.filter(|el| !is_trivia_or_comment(el.kind()));
let name = significant.next()?.into_token()?;
if name.kind() != SyntaxKind::IDENT {
return None;
}
let eq = significant.next()?;
(eq.kind() == SyntaxKind::ASSIGN_EQ).then(|| SmolStr::new(name.text()))
}
fn is_trivia_or_comment(kind: SyntaxKind) -> bool {
matches!(
kind,
SyntaxKind::WHITESPACE | SyntaxKind::NEWLINE | SyntaxKind::COMMENT
)
}
fn parameter_documentation(entry: &SymbolEntry, name: &str) -> Option<Documentation> {
let help = entry.help.as_ref()?;
let arg = help.arguments.iter().find(|a| a.name == name)?;
Some(Documentation::String(arg.description.clone()))
}
fn signature_documentation(entry: &SymbolEntry) -> Option<Documentation> {
let help = entry.help.as_ref()?;
let mut out = String::new();
if let Some(title) = &help.title {
out.push_str(title);
}
if let Some(description) = &help.description {
if !out.is_empty() {
out.push_str("\n\n");
}
out.push_str(description);
}
(!out.is_empty()).then_some(Documentation::MarkupContent(MarkupContent {
kind: MarkupKind::Markdown,
value: out,
}))
}
#[cfg(test)]
mod tests {
use super::*;
fn help_at(src: &str) -> Option<SignatureHelp> {
let offset = src.find('@').expect("cursor marker");
let text = src.replace('@', "");
compute_signature_help(&text, offset, &documented_dplyr())
}
#[test]
fn first_argument_is_active() {
let help = help_at("library(dplyr)\nacross(@)\n").expect("signature");
assert_eq!(help.signatures.len(), 1);
assert_eq!(help.active_parameter, Some(0));
let info = &help.signatures[0];
assert!(info.label.contains(".cols"), "label: {}", info.label);
assert_eq!(info.parameters.as_ref().map(Vec::len), Some(2));
}
#[test]
fn second_argument_active_after_comma() {
let help = help_at("library(dplyr)\nacross(a, @)\n").expect("signature");
assert_eq!(help.active_parameter, Some(1));
}
#[test]
fn named_argument_overrides_position() {
let help = help_at("library(dplyr)\nacross(.fns = 1@)\n").expect("signature");
assert_eq!(help.active_parameter, Some(1));
}
#[test]
fn nested_call_commas_do_not_leak() {
let help = help_at("library(dplyr)\nacross(foo(a, b), @)\n").expect("signature");
assert_eq!(help.active_parameter, Some(1));
}
#[test]
fn namespaced_call_resolves_without_library() {
let help = help_at("dplyr::across(@)\n").expect("signature");
assert_eq!(help.signatures.len(), 1);
assert_eq!(help.active_parameter, Some(0));
}
#[test]
fn none_outside_a_call() {
assert!(help_at("library(dplyr)\nx <- 1@\n").is_none());
}
#[test]
fn none_after_closing_paren() {
assert!(help_at("library(dplyr)\nacross(a)@\n").is_none());
}
#[test]
fn none_for_computed_callee() {
assert!(help_at("x$f(@)\n").is_none());
}
#[test]
fn usage_only_entry_has_label_without_parameters() {
use crate::rindex::schema::{HelpDoc, PackageIndex, SCHEMA_VERSION};
let idx = PackageIndex {
schema_version: SCHEMA_VERSION,
package: "base".into(),
version: "4.5.3".into(),
lib_path: "/lib".into(),
r_version: None,
harvested_at: 0,
symbols: vec![SymbolEntry {
name: "as.matrix".into(),
kind: SymbolKind::Function,
exported: true,
formals: None,
help: Some(HelpDoc {
title: Some("Matrices".into()),
description: None,
usage: Some("as.matrix(x, ...)".into()),
arguments: vec![],
}),
}],
};
let provider = IndexedProvider::from_indices([idx]);
let src = "as.matrix(@x)\n";
let offset = src.find('@').unwrap();
let help = compute_signature_help(&src.replace('@', ""), offset, &provider)
.expect("signature for as.matrix");
assert_eq!(help.signatures[0].label, "as.matrix(x, ...)");
assert!(help.signatures[0].parameters.is_none());
assert_eq!(help.active_parameter, None);
}
#[test]
fn signature_help_via_db_matches_compute() {
use crate::incremental::IncrementalDatabase;
let path = test_path();
let src = "library(dplyr)\nacross(a, mean)\n";
let position = pos(1, 7);
let mut db = IncrementalDatabase::default();
db.set_library_index(documented_dplyr());
db.upsert_file(path, src.to_string());
let help =
signature_help_via_db(&db.snapshot(), path, src, position).expect("signature via db");
assert_eq!(help.signatures.len(), 1);
let mut empty = IncrementalDatabase::default();
empty.set_library_index(documented_dplyr());
assert!(
signature_help_via_db(&empty.snapshot(), path, src, position).is_some(),
"fallback signature help should resolve too"
);
}
}