Language Server: textDocument/signatureHelp

Implements a pretty barebones function signature help mechanism in
the language server.

Users can use `Analysis::resolve_callback()` to get basic information
about a call site.

Fixes #102
This commit is contained in:
Jeremy A. Kolb 2018-10-09 10:08:17 -04:00
parent 2ba6f18586
commit f8a2b53304
10 changed files with 316 additions and 12 deletions

View file

@ -4,7 +4,8 @@ use std::{
use relative_path::RelativePathBuf;
use ra_syntax::{
SmolStr,
ast::{self, NameOwner},
ast::{self, NameOwner, AstNode, TypeParamsOwner},
text_utils::is_subrange
};
use {
FileId,
@ -218,3 +219,56 @@ fn resolve_submodule(
}
(points_to, problem)
}
#[derive(Debug, Clone)]
pub struct FnDescriptor {
pub name: Option<String>,
pub label : String,
pub ret_type: Option<String>,
pub params: Vec<String>,
}
impl FnDescriptor {
pub fn new(node: ast::FnDef) -> Self {
let name = node.name().map(|name| name.text().to_string());
// Strip the body out for the label.
let label : String = if let Some(body) = node.body() {
let body_range = body.syntax().range();
let label : String = node.syntax().children()
.filter(|child| !is_subrange(body_range, child.range()))
.map(|node| node.text().to_string())
.collect();
label
} else {
node.syntax().text().to_string()
};
let params = FnDescriptor::param_list(node);
let ret_type = node.ret_type().map(|r| r.syntax().text().to_string());
FnDescriptor {
name,
ret_type,
params,
label
}
}
fn param_list(node: ast::FnDef) -> Vec<String> {
let mut res = vec![];
if let Some(param_list) = node.param_list() {
if let Some(self_param) = param_list.self_param() {
res.push(self_param.syntax().text().to_string())
}
// Maybe use param.pat here? See if we can just extract the name?
//res.extend(param_list.params().map(|p| p.syntax().text().to_string()));
res.extend(param_list.params()
.filter_map(|p| p.pat())
.map(|pat| pat.syntax().text().to_string())
);
}
res
}
}

View file

@ -12,19 +12,18 @@ use relative_path::RelativePath;
use rustc_hash::FxHashSet;
use ra_editor::{self, FileSymbol, LineIndex, find_node_at_offset, LocalEdit, resolve_local_name};
use ra_syntax::{
TextUnit, TextRange, SmolStr, File, AstNode,
TextUnit, TextRange, SmolStr, File, AstNode, SyntaxNodeRef,
SyntaxKind::*,
ast::{self, NameOwner},
ast::{self, NameOwner, ArgListOwner, Expr},
};
use {
FileId, FileResolver, Query, Diagnostic, SourceChange, SourceFileEdit, Position, FileSystemEdit,
JobToken, CrateGraph, CrateId,
roots::{SourceRoot, ReadonlySourceRoot, WritableSourceRoot},
descriptors::{ModuleTreeDescriptor, Problem},
descriptors::{FnDescriptor, ModuleTreeDescriptor, Problem},
};
#[derive(Clone, Debug)]
pub(crate) struct FileResolverImp {
inner: Arc<FileResolver>
@ -306,6 +305,70 @@ impl AnalysisImpl {
.collect()
}
pub fn resolve_callable(&self, file_id: FileId, offset: TextUnit, token: &JobToken)
-> Option<(FnDescriptor, Option<usize>)> {
let root = self.root(file_id);
let file = root.syntax(file_id);
let syntax = file.syntax();
// Find the calling expression and it's NameRef
let calling_node = FnCallNode::with_node(syntax, offset)?;
let name_ref = calling_node.name_ref()?;
// Resolve the function's NameRef (NOTE: this isn't entirely accurate).
let file_symbols = self.index_resolve(name_ref, token);
for (_, fs) in file_symbols {
if fs.kind == FN_DEF {
if let Some(fn_def) = find_node_at_offset(syntax, fs.node_range.start()) {
let descriptor = FnDescriptor::new(fn_def);
// If we have a calling expression let's find which argument we are on
let mut current_parameter = None;
let num_params = descriptor.params.len();
let has_self = fn_def.param_list()
.and_then(|l| l.self_param())
.is_some();
if num_params == 1 {
if !has_self {
current_parameter = Some(1);
}
}
else if num_params > 1 {
// Count how many parameters into the call we are.
// TODO: This is best effort for now and should be fixed at some point.
// It may be better to see where we are in the arg_list and then check
// where offset is in that list (or beyond).
// Revisit this after we get documentation comments in.
if let Some(ref arg_list) = calling_node.arg_list() {
let start = arg_list.syntax().range().start();
let range_search = TextRange::from_to(start, offset);
let mut commas : usize = arg_list.syntax().text()
.slice(range_search).to_string()
.matches(",")
.count();
// If we have a method call eat the first param since it's just self.
if has_self {
commas = commas + 1;
}
current_parameter = Some(commas);
}
}
return Some((descriptor, current_parameter));
}
}
}
None
}
fn index_resolve(&self, name_ref: ast::NameRef, token: &JobToken) -> Vec<(FileId, FileSymbol)> {
let name = name_ref.text();
let mut query = Query::new(name.to_string());
@ -355,3 +418,46 @@ impl CrateGraph {
Some(crate_id)
}
}
enum FnCallNode<'a> {
CallExpr(ast::CallExpr<'a>),
MethodCallExpr(ast::MethodCallExpr<'a>)
}
impl<'a> FnCallNode<'a> {
pub fn with_node(syntax: SyntaxNodeRef, offset: TextUnit) -> Option<FnCallNode> {
if let Some(expr) = find_node_at_offset::<ast::CallExpr>(syntax, offset) {
return Some(FnCallNode::CallExpr(expr));
}
if let Some(expr) = find_node_at_offset::<ast::MethodCallExpr>(syntax, offset) {
return Some(FnCallNode::MethodCallExpr(expr));
}
None
}
pub fn name_ref(&self) -> Option<ast::NameRef> {
match *self {
FnCallNode::CallExpr(call_expr) => {
Some(match call_expr.expr()? {
Expr::PathExpr(path_expr) => {
path_expr.path()?.segment()?.name_ref()?
},
_ => return None
})
},
FnCallNode::MethodCallExpr(call_expr) => {
call_expr.syntax().children()
.filter_map(ast::NameRef::cast)
.nth(0)
}
}
}
pub fn arg_list(&self) -> Option<ast::ArgList> {
match *self {
FnCallNode::CallExpr(expr) => expr.arg_list(),
FnCallNode::MethodCallExpr(expr) => expr.arg_list()
}
}
}

View file

@ -38,6 +38,7 @@ pub use ra_editor::{
Fold, FoldKind
};
pub use job::{JobToken, JobHandle};
pub use descriptors::FnDescriptor;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct FileId(pub u32);
@ -236,6 +237,11 @@ impl Analysis {
let file = self.imp.file_syntax(file_id);
ra_editor::folding_ranges(&file)
}
pub fn resolve_callable(&self, file_id: FileId, offset: TextUnit, token: &JobToken)
-> Option<(FnDescriptor, Option<usize>)> {
self.imp.resolve_callable(file_id, offset, token)
}
}
#[derive(Debug)]

View file

@ -1,6 +1,8 @@
extern crate relative_path;
extern crate ra_analysis;
extern crate rustc_hash;
extern crate ra_editor;
extern crate ra_syntax;
extern crate test_utils;
use std::{
@ -9,8 +11,8 @@ use std::{
use rustc_hash::FxHashMap;
use relative_path::{RelativePath, RelativePathBuf};
use ra_analysis::{Analysis, AnalysisHost, FileId, FileResolver, JobHandle, CrateGraph, CrateId};
use test_utils::assert_eq_dbg;
use ra_analysis::{Analysis, AnalysisHost, FileId, FileResolver, JobHandle, CrateGraph, CrateId, FnDescriptor};
use test_utils::{assert_eq_dbg, extract_offset};
#[derive(Debug)]
struct FileMap(Vec<(FileId, RelativePathBuf)>);
@ -39,7 +41,7 @@ impl FileResolver for FileMap {
}
}
fn analysis_host(files: &'static [(&'static str, &'static str)]) -> AnalysisHost {
fn analysis_host(files: &[(&str, &str)]) -> AnalysisHost {
let mut host = AnalysisHost::new();
let mut file_map = Vec::new();
for (id, &(path, contents)) in files.iter().enumerate() {
@ -53,10 +55,20 @@ fn analysis_host(files: &'static [(&'static str, &'static str)]) -> AnalysisHost
host
}
fn analysis(files: &'static [(&'static str, &'static str)]) -> Analysis {
fn analysis(files: &[(&str, &str)]) -> Analysis {
analysis_host(files).analysis()
}
fn get_signature(text: &str) -> (FnDescriptor, Option<usize>) {
let (offset, code) = extract_offset(text);
let code = code.as_str();
let (_handle, token) = JobHandle::new();
let snap = analysis(&[("/lib.rs", code)]);
snap.resolve_callable(FileId(1), offset, &token).unwrap()
}
#[test]
fn test_resolve_module() {
let snap = analysis(&[
@ -145,3 +157,85 @@ fn test_resolve_crate_root() {
vec![CrateId(1)],
);
}
#[test]
fn test_fn_signature_two_args_first() {
let (desc, param) = get_signature(
r#"fn foo(x: u32, y: u32) -> u32 {x + y}
fn bar() { foo(<|>3, ); }"#);
assert_eq!(desc.name, Some("foo".into()));
assert_eq!(desc.params, vec!("x".to_string(),"y".to_string()));
assert_eq!(desc.ret_type, Some("-> u32".into()));
assert_eq!(param, Some(0));
}
#[test]
fn test_fn_signature_two_args_second() {
let (desc, param) = get_signature(
r#"fn foo(x: u32, y: u32) -> u32 {x + y}
fn bar() { foo(3, <|>); }"#);
assert_eq!(desc.name, Some("foo".into()));
assert_eq!(desc.params, vec!("x".to_string(),"y".to_string()));
assert_eq!(desc.ret_type, Some("-> u32".into()));
assert_eq!(param, Some(1));
}
#[test]
fn test_fn_signature_for_impl() {
let (desc, param) = get_signature(
r#"struct F; impl F { pub fn new() { F{}} }
fn bar() {let _ : F = F::new(<|>);}"#);
assert_eq!(desc.name, Some("new".into()));
assert_eq!(desc.params, Vec::<String>::new());
assert_eq!(desc.ret_type, None);
assert_eq!(param, None);
}
#[test]
fn test_fn_signature_for_method_self() {
let (desc, param) = get_signature(
r#"struct F;
impl F {
pub fn new() -> F{
F{}
}
pub fn do_it(&self) {}
}
fn bar() {
let f : F = F::new();
f.do_it(<|>);
}"#);
assert_eq!(desc.name, Some("do_it".into()));
assert_eq!(desc.params, vec!["&self".to_string()]);
assert_eq!(desc.ret_type, None);
assert_eq!(param, None);
}
#[test]
fn test_fn_signature_for_method_with_arg() {
let (desc, param) = get_signature(
r#"struct F;
impl F {
pub fn new() -> F{
F{}
}
pub fn do_it(&self, x: i32) {}
}
fn bar() {
let f : F = F::new();
f.do_it(<|>);
}"#);
assert_eq!(desc.name, Some("do_it".into()));
assert_eq!(desc.params, vec!["&self".to_string(), "x".to_string()]);
assert_eq!(desc.ret_type, None);
assert_eq!(param, Some(1));
}

View file

@ -7,6 +7,7 @@ use languageserver_types::{
TextDocumentSyncKind,
ExecuteCommandOptions,
CompletionOptions,
SignatureHelpOptions,
DocumentOnTypeFormattingOptions,
};
@ -26,7 +27,9 @@ pub fn server_capabilities() -> ServerCapabilities {
resolve_provider: None,
trigger_characters: None,
}),
signature_help_provider: None,
signature_help_provider: Some(SignatureHelpOptions {
trigger_characters: Some(vec!["(".to_string(), ",".to_string()])
}),
definition_provider: Some(true),
type_definition_provider: None,
implementation_provider: None,

View file

@ -411,6 +411,42 @@ pub fn handle_folding_range(
Ok(res)
}
pub fn handle_signature_help(
world: ServerWorld,
params: req::TextDocumentPositionParams,
token: JobToken,
) -> Result<Option<req::SignatureHelp>> {
use languageserver_types::{ParameterInformation, SignatureInformation};
let file_id = params.text_document.try_conv_with(&world)?;
let line_index = world.analysis().file_line_index(file_id);
let offset = params.position.conv_with(&line_index);
if let Some((descriptor, active_param)) = world.analysis().resolve_callable(file_id, offset, &token) {
let parameters : Vec<ParameterInformation> =
descriptor.params.iter().map(|param|
ParameterInformation {
label: param.clone(),
documentation: None
}
).collect();
let sig_info = SignatureInformation {
label: descriptor.label,
documentation: None,
parameters: Some(parameters)
};
Ok(Some(req::SignatureHelp {
signatures: vec![sig_info],
active_signature: Some(0),
active_parameter: active_param.map(|a| a as u64)
}))
} else {
Ok(None)
}
}
pub fn handle_code_action(
world: ServerWorld,
params: req::CodeActionParams,

View file

@ -255,6 +255,7 @@ fn on_request(
.on::<req::Completion>(handlers::handle_completion)?
.on::<req::CodeActionRequest>(handlers::handle_code_action)?
.on::<req::FoldingRangeRequest>(handlers::handle_folding_range)?
.on::<req::SignatureHelpRequest>(handlers::handle_signature_help)?
.finish();
match req {
Ok((id, handle)) => {

View file

@ -14,6 +14,7 @@ pub use languageserver_types::{
CompletionParams, CompletionResponse,
DocumentOnTypeFormattingParams,
TextDocumentEdit,
SignatureHelp, Hover
};
pub enum SyntaxTree {}

View file

@ -1387,7 +1387,10 @@ impl<'a> AstNode<'a> for PathExpr<'a> {
fn syntax(self) -> SyntaxNodeRef<'a> { self.syntax }
}
impl<'a> PathExpr<'a> {}
impl<'a> PathExpr<'a> {pub fn path(self) -> Option<Path<'a>> {
super::child_opt(self)
}
}
// PathPat
#[derive(Debug, Clone, Copy)]

View file

@ -342,7 +342,7 @@ Grammar(
"TupleExpr": (),
"ArrayExpr": (),
"ParenExpr": (),
"PathExpr": (),
"PathExpr": (options: ["Path"]),
"LambdaExpr": (
options: [
"ParamList",