// signature help: resolve what is the closest function expression for the possible call,
// then wait for checker outputs to actually return the signature
use kailua_env::Pos;
use kailua_diag::{Localize, Localized};
use kailua_syntax::lex::{Tok, Punct, NestedToken};
use kailua_types::ty::{TypeContext, Display, Nil, Functions};
use kailua_check::env::Output;
use protocol::*;
use message as m;
use super::{get_prefix_expr_slot, last_non_comment, PrefixExprSlot};
// for the interactivity, the lookbehind is limited to a reasonable number
const LOOKBEHIND_LIMIT: usize = 4096;
// look for the enclosing function call and the argument position of the caret.
// returns the index to the first token of arguments and the argument index, if any.
// the callee would have to seek more to find the end position of the function expression,
// and resolve the function slot from that position to continue.
//
// it seeks for the tokens `(` or `{` preceded by something resembling a function,
// while counting the number of commas *at the nesting opened by that `(` or `{`*.
// the token is considered to be the end of a function if it's `)` or a name;
// otherwise it is a subexpression inside a call and the number of commas is reset to 0.
//
// this should also handle a special case of `) "string"` or `NAME "string"`.
fn enclosing_func_call(tokens: &[NestedToken], pos: Pos) -> Option<(usize, usize)> {
// blank end of line
// | |
// ( N A M E , _ " s t _ r i n g " _ ) _ _ $
// 0 1 1 1 1 2 3 3 3 3 3 3 3 3 3 3 4 4 5 5 5 = idx
let idx = match tokens.binary_search_by(|tok| tok.tok.span.end().cmp(&pos)) {
Ok(i) => i + 1, // tokens[i].end == pos
Err(i) => i, // tokens[i-1].end or -inf < pos < tokens[i].end or inf
};
// no token before the caret or the caret is strictly inside the first token; no call here
if idx == 0 { return None; }
// in the delimited sequence of tokens, the opening token and subsequent tokens are
// in the same nesting, and the closing token is in the outermost nesting.
// (this is because each token first updates the nestings and gets assigned its nesting.)
// therefore the nesting for the caret (between two tokens) should be that of the first.
let mut last_tok;
let mut init_depth;
let mut init_serial;
if let Some(tok) = tokens.get(idx - 1) {
last_tok = tok;
init_depth = tok.depth;
init_serial = tok.serial;
} else {
return None;
}
// a special case for `<func> "str"`, which has no nesting changes
let ptok = if idx > 1 { tokens.get(idx - 2) } else { None };
match (ptok.map(|tok| &tok.tok.base), &last_tok.tok.base) {
(Some(&Tok::Name(_)), &Tok::Str(_)) |
(Some(&Tok::Punct(Punct::RParen)), &Tok::Str(_)) => return Some((idx - 1, 0)),
(_, _) => {}
}
let mut commas = 0;
for (i, tok) in tokens[..idx - 1].iter().enumerate().rev().take(LOOKBEHIND_LIMIT) {
let prev_tok = last_tok;
last_tok = tok;
if tok.depth <= init_depth && tok.serial != init_serial {
// escaped the current nesting, the last token should have been the opening token.
match (&tok.tok.base, &prev_tok.tok.base) {
(&Tok::Name(_), &Tok::Punct(Punct::LParen)) |
(&Tok::Punct(Punct::RParen), &Tok::Punct(Punct::LParen)) => {
// `tok` is likely the last token of the function expression
return Some((i + 1, commas));
}
(&Tok::Name(_), &Tok::Punct(Punct::LBrace)) |
(&Tok::Punct(Punct::RParen), &Tok::Punct(Punct::LBrace)) => {
// same as above, but the call is `<func> {...}` which has a single argument
return Some((i + 1, 0));
}
(_, _) => {
// otherwise we move to the parent nesting and reset the # of commas
init_depth = tok.depth;
init_serial = tok.serial;
commas = 0;
}
}
} else if tok.depth > init_depth {
// ignore more nested tokens (but count them towards the threshold)
continue;
}
if let Tok::Punct(Punct::Comma) = prev_tok.tok.base {
// the number of commas at the current nesting = the eventual argument index
// note that we take acount for prev_tok as `a , | b` will start with prev_tok = `,`.
commas += 1;
}
}
None
}
#[derive(Clone, Debug)]
pub struct Loc {
pub args_token_idx: usize,
pub arg_idx: usize,
}
pub fn locate(tokens: &[NestedToken], pos: Pos) -> Option<Loc> {
enclosing_func_call(tokens, pos).map(|(token_idx, arg_idx)| {
Loc { args_token_idx: token_idx, arg_idx: arg_idx }
})
}
pub fn help<F>(tokens: &[NestedToken], loc: &Loc, output: &Output,
mut localize: F) -> Option<SignatureHelp>
where F: for<'a> FnMut(&'a Localize) -> Localized<'a, Localize>
{
use std::fmt::Write;
let empty_signature = || {
SignatureHelp { signatures: Vec::new(), activeSignature: None, activeParameter: None }
};
// parameters are highlighted in the signature as a string pattern.
// this is darn wrong especially for Kailua
// because it fails to highlight, for example, `function(string, string)` correctly.
// to deal with this, we prefix a series of invisible characters (again) to each parameter.
let write_invisible_num = |s: &mut String, mut n: usize| {
s.push('\u{2060}');
while n > 0 {
s.push(['\u{200b}', '\u{200c}', '\u{200d}'][n % 3]);
n /= 3;
}
};
let res = get_prefix_expr_slot(tokens, loc.args_token_idx, output);
debug!("signature_help: get_prefix_expr_slot returns {:?}", res);
match res {
// fail fast, this is not a prefix expression
None => Some(empty_signature()),
// we may retry for the newer output if there is no slot available
Some(PrefixExprSlot::NotFound) => None,
// check if it's a callable function (otherwise we fail fast)
Some(PrefixExprSlot::Found(end_idx, slot)) => {
let ty = if let Some(ty) = output.resolve_exact_type(&slot.unlift()) {
ty
} else {
return Some(empty_signature());
};
if ty.nil() == Nil::Noisy {
// nilable function is not callable
return Some(empty_signature());
}
let func = if let Some(&Functions::Simple(ref func)) = ty.get_functions() {
func
} else {
return Some(empty_signature());
};
// seek more to determine this is a method call or not.
// tokens[end_idx] is never a comment, so we are sure that
// tokens[end_idx] is a name and preceding non-comment token is `:`
// when this is a method call.
let mut is_method = false;
if let Tok::Name(_) = tokens[end_idx].tok.base {
let prev_tok = last_non_comment(&tokens[..end_idx]).map(|(_, tok)| &tok.tok.base);
if let Some(&Tok::Punct(Punct::Colon)) = prev_tok {
is_method = true;
}
}
// they should be constructed in a lock step,
// as matching params in the label are underlined.
let mut label = format!("{}(", if is_method { "method" } else { "function" });
let mut params = Vec::new();
let types = output.types() as &TypeContext;
let mut first = true;
let mut names = func.argnames.iter();
for t in &func.args.head {
let implicit;
if first {
first = false;
implicit = is_method;
if implicit {
let _ = write!(label, "{} ", localize(&m::OmittedSelfLabel {}));
}
} else {
label.push_str(", ");
implicit = false;
}
let mut param = String::new();
write_invisible_num(&mut param, params.len());
if let Some(name) = names.next() {
if let Some(ref name) = *name {
let _ = write!(param, "{:+}: ", name);
}
}
let _ = write!(param, "{}", localize(&t.display(types)));
label.push_str(¶m);
if !implicit {
// implicit parameter is not listed
params.push(ParameterInformation { label: param, documentation: None });
}
}
if let Some(ref t) = func.args.tail {
if !first {
label.push_str(", ");
}
let mut param = String::new();
write_invisible_num(&mut param, params.len());
let _ = write!(param, "{:#}...", localize(&t.display(types)));
label.push_str(¶m);
params.push(ParameterInformation { label: param, documentation: None });
}
match func.returns {
Some(ref returns) => match (returns.head.len(), returns.tail.is_some()) {
(0, false) => {
label.push_str(")");
}
(1, false) => {
let _ = write!(label, ") --> {}",
localize(&returns.head[0].display(types)));
}
(_, _) => {
let _ = write!(label, ") --> {}", localize(&returns.display(types)));
}
},
None => {
label.push_str(") --> !");
}
}
let param_idx = if loc.arg_idx < params.len() {
Some(loc.arg_idx as u32)
} else if func.args.tail.is_some() {
// clamp to the number of parameters listed,
// mapping the last arguments to the variadic position
assert!(!params.is_empty());
Some((params.len() - 1) as u32)
} else {
// otherwise it's an excess parameter and should not be highlighted
None
};
return Some(SignatureHelp {
signatures: vec![
SignatureInformation { label: label, documentation: None, parameters: params },
],
activeSignature: Some(0),
activeParameter: param_idx,
});
},
}
}