feat: complete parameters on user functions (#148)

* fix: skip tabs that have no uris for reopening pdf

* dev: lift call analysis

* feat: complete parameters on user functions
This commit is contained in:
Myriad-Dreamin 2024-04-02 17:12:20 +08:00 committed by GitHub
parent 631f6e288c
commit d708bdfe2d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 605 additions and 611 deletions

View file

@ -1,5 +1,7 @@
//! Semantic static and dynamic analysis of the source code.
pub mod call;
pub use call::*;
pub mod def_use;
pub use def_use::*;
pub mod track_values;

View file

@ -0,0 +1,436 @@
//! Hybrid analysis for function calls.
use core::fmt;
use std::borrow::Cow;
use ecow::{eco_format, eco_vec};
use typst::{
foundations::{Args, CastInfo, Closure},
syntax::SyntaxNode,
util::LazyHash,
};
use crate::prelude::*;
/// Describes kind of a parameter.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ParamKind {
/// A positional parameter.
Positional,
/// A named parameter.
Named,
/// A rest (spread) parameter.
Rest,
}
/// Describes a function call parameter.
#[derive(Debug, Clone)]
pub struct CallParamInfo {
/// The parameter's kind.
pub kind: ParamKind,
/// Whether the parameter is a content block.
pub is_content_block: bool,
/// The parameter's specification.
pub param: Arc<ParamSpec>,
// types: EcoVec<()>,
}
/// Describes a function call.
#[derive(Debug, Clone)]
pub struct CallInfo {
/// The called function's signature.
pub signature: Arc<Signature>,
/// The mapping of arguments syntax nodes to their respective parameter
/// info.
pub arg_mapping: HashMap<SyntaxNode, CallParamInfo>,
}
/// Analyzes a function call.
#[comemo::memoize]
pub fn analyze_call(func: Func, args: ast::Args<'_>) -> Option<Arc<CallInfo>> {
Some(Arc::new(analyze_call_no_cache(func, args)?))
}
/// Analyzes a function call without caching the result.
pub fn analyze_call_no_cache(func: Func, args: ast::Args<'_>) -> Option<CallInfo> {
#[derive(Debug, Clone)]
enum ArgValue<'a> {
Instance(Args),
Instantiating(ast::Args<'a>),
}
let mut with_args = eco_vec![ArgValue::Instantiating(args)];
use typst::foundations::func::Repr;
let mut func = func;
while let Repr::With(f) = func.inner() {
with_args.push(ArgValue::Instance(f.1.clone()));
func = f.0.clone();
}
let signature = analyze_signature(func);
trace!("got signature {signature:?}");
let mut info = CallInfo {
arg_mapping: HashMap::new(),
signature: signature.clone(),
};
enum PosState {
Init,
Pos(usize),
Variadic,
Final,
}
struct PosBuilder {
state: PosState,
signature: Arc<Signature>,
}
impl PosBuilder {
fn advance(&mut self, info: &mut CallInfo, arg: Option<SyntaxNode>) {
let (kind, param) = match self.state {
PosState::Init => {
if !self.signature.pos.is_empty() {
self.state = PosState::Pos(0);
} else if self.signature.rest.is_some() {
self.state = PosState::Variadic;
} else {
self.state = PosState::Final;
}
return;
}
PosState::Pos(i) => {
if i + 1 < self.signature.pos.len() {
self.state = PosState::Pos(i + 1);
} else if self.signature.rest.is_some() {
self.state = PosState::Variadic;
} else {
self.state = PosState::Final;
}
(ParamKind::Positional, &self.signature.pos[i])
}
PosState::Variadic => (ParamKind::Rest, self.signature.rest.as_ref().unwrap()),
PosState::Final => return,
};
if let Some(arg) = arg {
// todo: process desugar
let is_content_block = arg.kind() == SyntaxKind::ContentBlock;
info.arg_mapping.insert(
arg,
CallParamInfo {
kind,
is_content_block,
param: param.clone(),
// types: eco_vec![],
},
);
}
}
fn advance_rest(&mut self, info: &mut CallInfo, arg: Option<SyntaxNode>) {
match self.state {
PosState::Init => unreachable!(),
// todo: not precise
PosState::Pos(..) => {
if self.signature.rest.is_some() {
self.state = PosState::Variadic;
} else {
self.state = PosState::Final;
}
}
PosState::Variadic => {}
PosState::Final => return,
};
let Some(rest) = self.signature.rest.as_ref() else {
return;
};
if let Some(arg) = arg {
// todo: process desugar
let is_content_block = arg.kind() == SyntaxKind::ContentBlock;
info.arg_mapping.insert(
arg,
CallParamInfo {
kind: ParamKind::Rest,
is_content_block,
param: rest.clone(),
// types: eco_vec![],
},
);
}
}
}
let mut pos_builder = PosBuilder {
state: PosState::Init,
signature: signature.clone(),
};
pos_builder.advance(&mut info, None);
for arg in with_args.iter().rev() {
match arg {
ArgValue::Instance(args) => {
for _ in args.items.iter().filter(|arg| arg.name.is_none()) {
pos_builder.advance(&mut info, None);
}
}
ArgValue::Instantiating(args) => {
for arg in args.items() {
let arg_tag = arg.to_untyped().clone();
match arg {
ast::Arg::Named(named) => {
let n = named.name().as_str();
if let Some(param) = signature.named.get(n) {
info.arg_mapping.insert(
arg_tag,
CallParamInfo {
kind: ParamKind::Named,
is_content_block: false,
param: param.clone(),
// types: eco_vec![],
},
);
}
}
ast::Arg::Pos(..) => {
pos_builder.advance(&mut info, Some(arg_tag));
}
ast::Arg::Spread(..) => pos_builder.advance_rest(&mut info, Some(arg_tag)),
}
}
}
}
}
Some(info)
}
/// Describes a function parameter.
#[derive(Debug, Clone)]
pub struct ParamSpec {
/// The parameter's name.
pub name: Cow<'static, str>,
/// Documentation for the parameter.
pub docs: Cow<'static, str>,
/// Describe what values this parameter accepts.
pub input: CastInfo,
/// The parameter's default name.
pub expr: Option<EcoString>,
/// Creates an instance of the parameter's default value.
pub default: Option<fn() -> Value>,
/// Is the parameter positional?
pub positional: bool,
/// Is the parameter named?
///
/// Can be true even if `positional` is true if the parameter can be given
/// in both variants.
pub named: bool,
/// Can the parameter be given any number of times?
pub variadic: bool,
/// Is the parameter settable with a set rule?
pub settable: bool,
}
impl ParamSpec {
fn from_static(s: &ParamInfo) -> Arc<Self> {
Arc::new(Self {
name: Cow::Borrowed(s.name),
docs: Cow::Borrowed(s.docs),
input: s.input.clone(),
expr: Some(eco_format!("{}", TypeExpr(&s.input))),
default: s.default,
positional: s.positional,
named: s.named,
variadic: s.variadic,
settable: s.settable,
})
}
}
/// Describes a function signature.
#[derive(Debug, Clone)]
pub struct Signature {
/// The positional parameters.
pub pos: Vec<Arc<ParamSpec>>,
/// The named parameters.
pub named: HashMap<Cow<'static, str>, Arc<ParamSpec>>,
/// Whether the function has fill, stroke, or size parameters.
pub has_fill_or_size_or_stroke: bool,
/// The rest parameter.
pub rest: Option<Arc<ParamSpec>>,
_broken: bool,
}
#[comemo::memoize]
pub(crate) fn analyze_signature(func: Func) -> Arc<Signature> {
use typst::foundations::func::Repr;
let params = match func.inner() {
Repr::With(..) => unreachable!(),
Repr::Closure(c) => analyze_closure_signature(c.clone()),
Repr::Element(..) | Repr::Native(..) => {
let params = func.params().unwrap();
params.iter().map(ParamSpec::from_static).collect()
}
};
let mut pos = vec![];
let mut named = HashMap::new();
let mut rest = None;
let mut broken = false;
let mut has_fill = false;
let mut has_stroke = false;
let mut has_size = false;
for param in params.into_iter() {
if param.named {
match param.name.as_ref() {
"fill" => {
has_fill = true;
}
"stroke" => {
has_stroke = true;
}
"size" => {
has_size = true;
}
_ => {}
}
named.insert(param.name.clone(), param.clone());
}
if param.variadic {
if rest.is_some() {
broken = true;
} else {
rest = Some(param.clone());
}
}
if param.positional {
pos.push(param);
}
}
Arc::new(Signature {
pos,
named,
rest,
has_fill_or_size_or_stroke: has_fill || has_stroke || has_size,
_broken: broken,
})
}
fn analyze_closure_signature(c: Arc<LazyHash<Closure>>) -> Vec<Arc<ParamSpec>> {
let mut params = vec![];
trace!("closure signature for: {:?}", c.node.kind());
let closure = &c.node;
let closure_ast = match closure.kind() {
SyntaxKind::Closure => closure.cast::<ast::Closure>().unwrap(),
_ => return params,
};
for param in closure_ast.params().children() {
match param {
ast::Param::Pos(ast::Pattern::Placeholder(..)) => {
params.push(Arc::new(ParamSpec {
name: Cow::Borrowed("_"),
input: CastInfo::Any,
expr: None,
default: None,
positional: true,
named: false,
variadic: false,
settable: false,
docs: Cow::Borrowed(""),
}));
}
ast::Param::Pos(e) => {
// todo: destructing
let name = e.bindings();
if name.len() != 1 {
continue;
}
let name = name[0].as_str();
params.push(Arc::new(ParamSpec {
name: Cow::Owned(name.to_owned()),
input: CastInfo::Any,
expr: None,
default: None,
positional: true,
named: false,
variadic: false,
settable: false,
docs: Cow::Borrowed(""),
}));
}
// todo: pattern
ast::Param::Named(n) => {
let expr = unwrap_expr(n.expr()).to_untyped().clone().into_text();
params.push(Arc::new(ParamSpec {
name: Cow::Owned(n.name().as_str().to_owned()),
input: CastInfo::Any,
expr: Some(expr.clone()),
default: None,
positional: false,
named: true,
variadic: false,
settable: true,
docs: Cow::Owned("Default value: ".to_owned() + expr.as_str()),
}));
}
ast::Param::Spread(n) => {
let ident = n.sink_ident().map(|e| e.as_str());
params.push(Arc::new(ParamSpec {
name: Cow::Owned(ident.unwrap_or_default().to_owned()),
input: CastInfo::Any,
expr: None,
default: None,
positional: false,
named: true,
variadic: false,
settable: false,
docs: Cow::Borrowed(""),
}));
}
}
}
params
}
fn unwrap_expr(mut e: ast::Expr) -> ast::Expr {
while let ast::Expr::Parenthesized(p) = e {
e = p.expr();
}
e
}
struct TypeExpr<'a>(&'a CastInfo);
impl<'a> fmt::Display for TypeExpr<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self.0 {
CastInfo::Any => "any",
CastInfo::Value(.., v) => v,
CastInfo::Type(v) => {
f.write_str(v.short_name())?;
return Ok(());
}
CastInfo::Union(v) => {
let mut values = v.iter().map(|e| TypeExpr(e).to_string());
f.write_str(&values.join(" | "))?;
return Ok(());
}
})
}
}

View file

@ -115,7 +115,7 @@ impl<'w> AnalysisContext<'w> {
}
/// Get the world surface for Typst compiler.
pub fn world(&self) -> &dyn World {
pub fn world(&self) -> &'w dyn World {
self.resources.world()
}

View file

@ -6,7 +6,7 @@ use crate::{
prelude::*,
syntax::{get_deref_target, DerefTarget},
typst_to_lsp::completion_kind,
upstream::{autocomplete_, Completion, CompletionContext, CompletionKind},
upstream::{autocomplete, Completion, CompletionContext, CompletionKind},
LspCompletion, StatefulRequest,
};
@ -111,8 +111,8 @@ impl StatefulRequest for CompletionRequest {
}
let items = completion_result.or_else(|| {
let cc_ctx = CompletionContext::new(ctx.world(), doc, &source, cursor, explicit)?;
let (offset, mut completions) = autocomplete_(cc_ctx)?;
let cc_ctx = CompletionContext::new(ctx, doc, &source, cursor, explicit)?;
let (offset, mut completions) = autocomplete(cc_ctx)?;
let replace_range;
if match_ident.as_ref().is_some_and(|i| i.offset() == offset) {

View file

@ -1,7 +1,8 @@
use core::fmt;
use crate::{
analyze_signature, find_definition,
analysis::analyze_signature,
find_definition,
prelude::*,
syntax::{find_document_before, get_deref_target, LexicalKind, LexicalVarKind},
upstream::{expr_tooltip, tooltip, Tooltip},

View file

@ -1,16 +1,13 @@
use core::fmt;
use std::{borrow::Cow, ops::Range};
use std::ops::Range;
use ecow::{eco_format, eco_vec};
use log::debug;
use lsp_types::{InlayHintKind, InlayHintLabel};
use typst::{
foundations::{Args, CastInfo, Closure},
syntax::SyntaxNode,
util::LazyHash,
};
use crate::{prelude::*, SemanticRequest};
use crate::{
analysis::{analyze_call, ParamKind},
prelude::*,
SemanticRequest,
};
/// Configuration for inlay hints.
pub struct InlayHintConfig {
@ -314,361 +311,6 @@ fn inlay_hint(
Ok(worker.hints)
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum ParamKind {
Positional,
Named,
Rest,
}
#[derive(Debug, Clone)]
struct CallParamInfo {
kind: ParamKind,
is_content_block: bool,
param: Arc<ParamSpec>,
// types: EcoVec<()>,
}
#[derive(Debug, Clone)]
struct CallInfo {
signature: Arc<Signature>,
arg_mapping: HashMap<SyntaxNode, CallParamInfo>,
}
#[comemo::memoize]
fn analyze_call(func: Func, args: ast::Args<'_>) -> Option<Arc<CallInfo>> {
Some(Arc::new(analyze_call_no_cache(func, args)?))
}
fn analyze_call_no_cache(func: Func, args: ast::Args<'_>) -> Option<CallInfo> {
#[derive(Debug, Clone)]
enum ArgValue<'a> {
Instance(Args),
Instantiating(ast::Args<'a>),
}
let mut with_args = eco_vec![ArgValue::Instantiating(args)];
use typst::foundations::func::Repr;
let mut func = func;
while let Repr::With(f) = func.inner() {
with_args.push(ArgValue::Instance(f.1.clone()));
func = f.0.clone();
}
let signature = analyze_signature(func);
trace!("got signature {signature:?}");
let mut info = CallInfo {
arg_mapping: HashMap::new(),
signature: signature.clone(),
};
enum PosState {
Init,
Pos(usize),
Variadic,
Final,
}
struct PosBuilder {
state: PosState,
signature: Arc<Signature>,
}
impl PosBuilder {
fn advance(&mut self, info: &mut CallInfo, arg: Option<SyntaxNode>) {
let (kind, param) = match self.state {
PosState::Init => {
if !self.signature.pos.is_empty() {
self.state = PosState::Pos(0);
} else if self.signature.rest.is_some() {
self.state = PosState::Variadic;
} else {
self.state = PosState::Final;
}
return;
}
PosState::Pos(i) => {
if i + 1 < self.signature.pos.len() {
self.state = PosState::Pos(i + 1);
} else if self.signature.rest.is_some() {
self.state = PosState::Variadic;
} else {
self.state = PosState::Final;
}
(ParamKind::Positional, &self.signature.pos[i])
}
PosState::Variadic => (ParamKind::Rest, self.signature.rest.as_ref().unwrap()),
PosState::Final => return,
};
if let Some(arg) = arg {
// todo: process desugar
let is_content_block = arg.kind() == SyntaxKind::ContentBlock;
info.arg_mapping.insert(
arg,
CallParamInfo {
kind,
is_content_block,
param: param.clone(),
// types: eco_vec![],
},
);
}
}
fn advance_rest(&mut self, info: &mut CallInfo, arg: Option<SyntaxNode>) {
match self.state {
PosState::Init => unreachable!(),
// todo: not precise
PosState::Pos(..) => {
if self.signature.rest.is_some() {
self.state = PosState::Variadic;
} else {
self.state = PosState::Final;
}
}
PosState::Variadic => {}
PosState::Final => return,
};
let Some(rest) = self.signature.rest.as_ref() else {
return;
};
if let Some(arg) = arg {
// todo: process desugar
let is_content_block = arg.kind() == SyntaxKind::ContentBlock;
info.arg_mapping.insert(
arg,
CallParamInfo {
kind: ParamKind::Rest,
is_content_block,
param: rest.clone(),
// types: eco_vec![],
},
);
}
}
}
let mut pos_builder = PosBuilder {
state: PosState::Init,
signature: signature.clone(),
};
pos_builder.advance(&mut info, None);
for arg in with_args.iter().rev() {
match arg {
ArgValue::Instance(args) => {
for _ in args.items.iter().filter(|arg| arg.name.is_none()) {
pos_builder.advance(&mut info, None);
}
}
ArgValue::Instantiating(args) => {
for arg in args.items() {
let arg_tag = arg.to_untyped().clone();
match arg {
ast::Arg::Named(named) => {
let n = named.name().as_str();
if let Some(param) = signature.named.get(n) {
info.arg_mapping.insert(
arg_tag,
CallParamInfo {
kind: ParamKind::Named,
is_content_block: false,
param: param.clone(),
// types: eco_vec![],
},
);
}
}
ast::Arg::Pos(..) => {
pos_builder.advance(&mut info, Some(arg_tag));
}
ast::Arg::Spread(..) => pos_builder.advance_rest(&mut info, Some(arg_tag)),
}
}
}
}
}
Some(info)
}
/// Describes a function parameter.
#[derive(Debug, Clone)]
pub struct ParamSpec {
/// The parameter's name.
pub name: Cow<'static, str>,
/// The parameter's default name.
pub expr: Option<EcoString>,
/// Creates an instance of the parameter's default value.
pub default: Option<fn() -> Value>,
/// Is the parameter positional?
pub positional: bool,
/// Is the parameter named?
///
/// Can be true even if `positional` is true if the parameter can be given
/// in both variants.
pub named: bool,
/// Can the parameter be given any number of times?
pub variadic: bool,
}
impl ParamSpec {
fn from_static(s: &ParamInfo) -> Arc<Self> {
Arc::new(Self {
name: Cow::Borrowed(s.name),
expr: Some(eco_format!("{}", TypeExpr(&s.input))),
default: s.default,
positional: s.positional,
named: s.named,
variadic: s.variadic,
})
}
}
#[derive(Debug, Clone)]
pub(crate) struct Signature {
pub pos: Vec<Arc<ParamSpec>>,
pub named: HashMap<Cow<'static, str>, Arc<ParamSpec>>,
has_fill_or_size_or_stroke: bool,
pub rest: Option<Arc<ParamSpec>>,
_broken: bool,
}
#[comemo::memoize]
pub(crate) fn analyze_signature(func: Func) -> Arc<Signature> {
use typst::foundations::func::Repr;
let params = match func.inner() {
Repr::With(..) => unreachable!(),
Repr::Closure(c) => analyze_closure_signature(c.clone()),
Repr::Element(..) | Repr::Native(..) => {
let params = func.params().unwrap();
params.iter().map(ParamSpec::from_static).collect()
}
};
let mut pos = vec![];
let mut named = HashMap::new();
let mut rest = None;
let mut broken = false;
let mut has_fill = false;
let mut has_stroke = false;
let mut has_size = false;
for param in params.into_iter() {
if param.named {
match param.name.as_ref() {
"fill" => {
has_fill = true;
}
"stroke" => {
has_stroke = true;
}
"size" => {
has_size = true;
}
_ => {}
}
named.insert(param.name.clone(), param.clone());
}
if param.variadic {
if rest.is_some() {
broken = true;
} else {
rest = Some(param.clone());
}
}
if param.positional {
pos.push(param);
}
}
Arc::new(Signature {
pos,
named,
rest,
has_fill_or_size_or_stroke: has_fill || has_stroke || has_size,
_broken: broken,
})
}
fn analyze_closure_signature(c: Arc<LazyHash<Closure>>) -> Vec<Arc<ParamSpec>> {
let mut params = vec![];
trace!("closure signature for: {:?}", c.node.kind());
let closure = &c.node;
let closure_ast = match closure.kind() {
SyntaxKind::Closure => closure.cast::<ast::Closure>().unwrap(),
_ => return params,
};
for param in closure_ast.params().children() {
match param {
ast::Param::Pos(ast::Pattern::Placeholder(..)) => {
params.push(Arc::new(ParamSpec {
name: Cow::Borrowed("_"),
expr: None,
default: None,
positional: true,
named: false,
variadic: false,
}));
}
ast::Param::Pos(e) => {
// todo: destructing
let name = e.bindings();
if name.len() != 1 {
continue;
}
let name = name[0].as_str();
params.push(Arc::new(ParamSpec {
name: Cow::Owned(name.to_owned()),
expr: None,
default: None,
positional: true,
named: false,
variadic: false,
}));
}
// todo: pattern
ast::Param::Named(n) => {
params.push(Arc::new(ParamSpec {
name: Cow::Owned(n.name().as_str().to_owned()),
expr: Some(unwrap_expr(n.expr()).to_untyped().clone().into_text()),
default: None,
positional: false,
named: true,
variadic: false,
}));
}
ast::Param::Spread(n) => {
let ident = n.sink_ident().map(|e| e.as_str());
params.push(Arc::new(ParamSpec {
name: Cow::Owned(ident.unwrap_or_default().to_owned()),
expr: None,
default: None,
positional: false,
named: true,
variadic: false,
}));
}
}
}
params
}
fn is_one_line(src: &Source, arg_node: &LinkedNode<'_>) -> bool {
is_one_line_(src, arg_node).unwrap_or(true)
}
@ -681,34 +323,6 @@ fn is_one_line_(src: &Source, arg_node: &LinkedNode<'_>) -> Option<bool> {
Some(ll == rl)
}
fn unwrap_expr(mut e: ast::Expr) -> ast::Expr {
while let ast::Expr::Parenthesized(p) = e {
e = p.expr();
}
e
}
struct TypeExpr<'a>(&'a CastInfo);
impl<'a> fmt::Display for TypeExpr<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self.0 {
CastInfo::Any => "any",
CastInfo::Value(.., v) => v,
CastInfo::Type(v) => {
f.write_str(v.short_name())?;
return Ok(());
}
CastInfo::Union(v) => {
let mut values = v.iter().map(|e| TypeExpr(e).to_string());
f.write_str(&values.join(" | "))?;
return Ok(());
}
})
}
}
#[cfg(test)]
mod tests {
use super::*;

View file

@ -1,24 +1,25 @@
use std::cmp::Reverse;
use std::collections::{BTreeSet, HashSet};
use std::collections::HashSet;
use ecow::{eco_format, EcoString};
use if_chain::if_chain;
use serde::{Deserialize, Serialize};
use typst::foundations::{
fields_on, format_str, mutable_methods_on, repr, AutoValue, CastInfo, Func, Label, NoneValue,
Repr, Scope, StyleChain, Styles, Type, Value,
Repr, StyleChain, Styles, Type, Value,
};
use typst::model::Document;
use typst::syntax::{ast, is_id_continue, is_id_start, is_ident, LinkedNode, Source, SyntaxKind};
use typst::text::RawElem;
use typst::visualize::Color;
use typst::World;
use unscanny::Scanner;
use super::{plain_docs_sentence, summarize_font_family};
use crate::analysis::{analyze_expr, analyze_import, analyze_labels};
use crate::AnalysisContext;
mod ext;
use ext::*;
/// Autocomplete a cursor position in a source file.
///
@ -31,30 +32,7 @@ mod ext;
/// Passing a `document` (from a previous compilation) is optional, but enhances
/// the autocompletions. Label completions, for instance, are only generated
/// when the document is available.
pub fn autocomplete(
world: &dyn World,
document: Option<&Document>,
source: &Source,
cursor: usize,
explicit: bool,
) -> Option<(usize, Vec<Completion>)> {
let mut ctx = CompletionContext::new(world, document, source, cursor, explicit)?;
let _ = complete_comments(&mut ctx)
|| complete_field_accesses(&mut ctx)
|| complete_open_labels(&mut ctx)
|| complete_imports(&mut ctx)
|| complete_rules(&mut ctx)
|| complete_params(&mut ctx)
|| complete_markup(&mut ctx)
|| complete_math(&mut ctx)
|| complete_code(&mut ctx);
Some((ctx.from, ctx.completions))
}
pub fn autocomplete_(mut ctx: CompletionContext) -> Option<(usize, Vec<Completion>)> {
let _ = autocomplete;
pub fn autocomplete(mut ctx: CompletionContext) -> Option<(usize, Vec<Completion>)> {
let _ = complete_comments(&mut ctx)
|| complete_field_accesses(&mut ctx)
|| complete_open_labels(&mut ctx)
@ -368,7 +346,7 @@ fn complete_field_accesses(ctx: &mut CompletionContext) -> bool {
if prev.is::<ast::Expr>();
if prev.parent_kind() != Some(SyntaxKind::Markup) ||
prev.prev_sibling_kind() == Some(SyntaxKind::Hash);
if let Some((value, styles)) = analyze_expr(ctx.world, &prev).into_iter().next();
if let Some((value, styles)) = analyze_expr(ctx.world(), &prev).into_iter().next();
then {
ctx.from = ctx.cursor;
field_access_completions(ctx, &value, &styles);
@ -383,7 +361,7 @@ fn complete_field_accesses(ctx: &mut CompletionContext) -> bool {
if prev.kind() == SyntaxKind::Dot;
if let Some(prev_prev) = prev.prev_sibling();
if prev_prev.is::<ast::Expr>();
if let Some((value, styles)) = analyze_expr(ctx.world, &prev_prev).into_iter().next();
if let Some((value, styles)) = analyze_expr(ctx.world(), &prev_prev).into_iter().next();
then {
ctx.from = ctx.leaf.offset();
field_access_completions(ctx, &value, &styles);
@ -552,11 +530,11 @@ fn complete_imports(ctx: &mut CompletionContext) -> bool {
/// Add completions for all exports of a module.
fn import_item_completions<'a>(
ctx: &mut CompletionContext<'a>,
ctx: &mut CompletionContext<'a, '_>,
existing: ast::ImportItems<'a>,
source: &LinkedNode,
) {
let Some(value) = analyze_import(ctx.world, source) else {
let Some(value) = analyze_import(ctx.world(), source) else {
return;
};
let Some(scope) = value.scope() else { return };
@ -742,58 +720,9 @@ fn complete_params(ctx: &mut CompletionContext) -> bool {
false
}
/// Add completions for the parameters of a function.
fn param_completions<'a>(
ctx: &mut CompletionContext<'a>,
callee: ast::Expr<'a>,
set: bool,
args: ast::Args<'a>,
) {
let Some(func) = resolve_global_callee(ctx, callee) else {
return;
};
let Some(params) = func.params() else { return };
// Exclude named arguments which are already present.
let exclude: Vec<_> = args
.items()
.filter_map(|arg| match arg {
ast::Arg::Named(named) => Some(named.name()),
_ => None,
})
.collect();
for param in params {
if exclude.iter().any(|ident| ident.as_str() == param.name) {
continue;
}
if set && !param.settable {
continue;
}
if param.named {
ctx.completions.push(Completion {
kind: CompletionKind::Param,
label: param.name.into(),
apply: Some(eco_format!("{}: ${{}}", param.name)),
detail: Some(plain_docs_sentence(param.docs)),
});
}
if param.positional {
ctx.cast_completions(&param.input);
}
}
if ctx.before.ends_with(',') {
ctx.enrich(" ", "");
}
}
/// Add completions for the values of a named function parameter.
fn named_param_value_completions<'a>(
ctx: &mut CompletionContext<'a>,
ctx: &mut CompletionContext<'a, '_>,
callee: ast::Expr<'a>,
name: &str,
) {
@ -817,30 +746,6 @@ fn named_param_value_completions<'a>(
}
}
/// Resolve a callee expression to a global function.
fn resolve_global_callee<'a>(
ctx: &CompletionContext<'a>,
callee: ast::Expr<'a>,
) -> Option<&'a Func> {
let value = match callee {
ast::Expr::Ident(ident) => ctx.global.get(&ident)?,
ast::Expr::FieldAccess(access) => match access.target() {
ast::Expr::Ident(target) => match ctx.global.get(&target)? {
Value::Module(module) => module.field(&access.field()).ok()?,
Value::Func(func) => func.field(&access.field()).ok()?,
_ => return None,
},
_ => return None,
},
_ => return None,
};
match value {
Value::Func(func) => Some(func),
_ => None,
}
}
/// Complete in code mode.
fn complete_code(ctx: &mut CompletionContext) -> bool {
if matches!(
@ -1040,14 +945,13 @@ fn code_completions(ctx: &mut CompletionContext, hash: bool) {
}
/// Context for autocompletion.
pub struct CompletionContext<'a> {
pub world: &'a (dyn World + 'a),
pub struct CompletionContext<'a, 'w> {
pub ctx: &'a mut AnalysisContext<'w>,
pub document: Option<&'a Document>,
pub global: &'a Scope,
pub math: &'a Scope,
pub text: &'a str,
pub before: &'a str,
pub after: &'a str,
pub root: LinkedNode<'a>,
pub leaf: LinkedNode<'a>,
pub cursor: usize,
pub explicit: bool,
@ -1056,26 +960,25 @@ pub struct CompletionContext<'a> {
pub seen_casts: HashSet<u128>,
}
impl<'a> CompletionContext<'a> {
impl<'a, 'w> CompletionContext<'a, 'w> {
/// Create a new autocompletion context.
pub fn new(
world: &'a (dyn World + 'a),
ctx: &'a mut AnalysisContext<'w>,
document: Option<&'a Document>,
source: &'a Source,
cursor: usize,
explicit: bool,
) -> Option<Self> {
let text = source.text();
let library = world.library();
let leaf = LinkedNode::new(source.root()).leaf_at(cursor)?;
let root = LinkedNode::new(source.root());
let leaf = root.leaf_at(cursor)?;
Some(Self {
world,
ctx,
document,
global: library.global.scope(),
math: library.math.scope(),
text,
before: &text[..cursor],
after: &text[cursor..],
root,
leaf,
cursor,
explicit,
@ -1116,7 +1019,7 @@ impl<'a> CompletionContext<'a> {
/// Add completions for all font families.
fn font_completions(&mut self) {
let equation = self.before_window(25).contains("equation");
for (family, iter) in self.world.book().families() {
for (family, iter) in self.world().book().families() {
let detail = summarize_font_family(iter);
if !equation || family.contains("Math") {
self.value_completion(
@ -1131,7 +1034,7 @@ impl<'a> CompletionContext<'a> {
/// Add completions for all available packages.
fn package_completions(&mut self, all_versions: bool) {
let mut packages: Vec<_> = self.world.packages().iter().collect();
let mut packages: Vec<_> = self.world().packages().iter().collect();
packages.sort_by_key(|(spec, _)| (&spec.namespace, &spec.name, Reverse(spec.version)));
if !all_versions {
packages.dedup_by_key(|(spec, _)| (&spec.namespace, &spec.name));
@ -1261,7 +1164,7 @@ impl<'a> CompletionContext<'a> {
}
/// Add completions for a castable.
fn cast_completions(&mut self, cast: &'a CastInfo) {
fn cast_completions(&mut self, cast: &CastInfo) {
// Prevent duplicate completions from appearing.
if !self.seen_casts.insert(typst::util::hash128(cast)) {
return;
@ -1343,93 +1246,4 @@ impl<'a> CompletionContext<'a> {
}
}
}
/// Add completions for definitions that are available at the cursor.
///
/// Filters the global/math scope with the given filter.
fn _scope_completions(&mut self, parens: bool, filter: impl Fn(&Value) -> bool) {
let mut defined = BTreeSet::new();
let mut ancestor = Some(self.leaf.clone());
while let Some(node) = &ancestor {
let mut sibling = Some(node.clone());
while let Some(node) = &sibling {
if let Some(v) = node.cast::<ast::LetBinding>() {
for ident in v.kind().bindings() {
defined.insert(ident.get().clone());
}
}
if let Some(v) = node.cast::<ast::ModuleImport>() {
let imports = v.imports();
match imports {
None | Some(ast::Imports::Wildcard) => {
if let Some(value) = node
.children()
.find(|child| child.is::<ast::Expr>())
.and_then(|source| analyze_import(self.world, &source))
{
if imports.is_none() {
defined.extend(value.name().map(Into::into));
} else if let Some(scope) = value.scope() {
for (name, _) in scope.iter() {
defined.insert(name.clone());
}
}
}
}
Some(ast::Imports::Items(items)) => {
for item in items.iter() {
defined.insert(item.bound_name().get().clone());
}
}
}
}
sibling = node.prev_sibling();
}
if let Some(parent) = node.parent() {
if let Some(v) = parent.cast::<ast::ForLoop>() {
if node.prev_sibling_kind() != Some(SyntaxKind::In) {
let pattern = v.pattern();
for ident in pattern.bindings() {
defined.insert(ident.get().clone());
}
}
}
ancestor = Some(parent.clone());
continue;
}
break;
}
let in_math = matches!(
self.leaf.parent_kind(),
Some(SyntaxKind::Equation)
| Some(SyntaxKind::Math)
| Some(SyntaxKind::MathFrac)
| Some(SyntaxKind::MathAttach)
);
let scope = if in_math { self.math } else { self.global };
for (name, value) in scope.iter() {
if filter(value) && !defined.contains(name) {
self.value_completion(Some(name.clone()), value, parens, None);
}
}
for name in defined {
if !name.is_empty() {
self.completions.push(Completion {
kind: CompletionKind::Constant,
label: name,
apply: None,
detail: None,
});
}
}
}
}

View file

@ -1,13 +1,22 @@
use super::{Completion, CompletionContext, CompletionKind};
use std::collections::BTreeMap;
use ecow::EcoString;
use typst::foundations::Value;
use ecow::{eco_format, EcoString};
use typst::foundations::{Func, Value};
use typst::syntax::ast::AstNode;
use typst::syntax::{ast, SyntaxKind};
use crate::analysis::analyze_import;
use crate::analysis::{analyze_import, analyze_signature};
use crate::find_definition;
use crate::prelude::analyze_expr;
use crate::syntax::{get_deref_target, LexicalKind, LexicalVarKind};
use crate::upstream::plain_docs_sentence;
impl<'a, 'w> CompletionContext<'a, 'w> {
pub fn world(&self) -> &'w dyn typst::World {
self.ctx.world()
}
impl<'a> CompletionContext<'a> {
/// Add completions for definitions that are available at the cursor.
///
/// Filters the global/math scope with the given filter.
@ -43,7 +52,7 @@ impl<'a> CompletionContext<'a> {
let anaylyze = node.children().find(|child| child.is::<ast::Expr>());
let analyzed = anaylyze
.as_ref()
.and_then(|source| analyze_import(self.world, source));
.and_then(|source| analyze_import(self.world(), source));
if analyzed.is_none() {
log::info!("failed to analyze import: {:?}", anaylyze);
}
@ -113,7 +122,10 @@ impl<'a> CompletionContext<'a> {
| Some(SyntaxKind::MathAttach)
);
let scope = if in_math { self.math } else { self.global };
let lib = self.world().library();
let scope = if in_math { &lib.math } else { &lib.global }
.scope()
.clone();
for (name, value) in scope.iter() {
if filter(value) && !defined.contains_key(name) {
self.value_completion(Some(name.clone()), value, parens, None);
@ -132,3 +144,118 @@ impl<'a> CompletionContext<'a> {
}
}
}
/// Add completions for the parameters of a function.
pub fn param_completions<'a>(
ctx: &mut CompletionContext<'a, '_>,
callee: ast::Expr<'a>,
set: bool,
args: ast::Args<'a>,
) {
let Some(func) = resolve_global_callee(ctx, callee) else {
return;
};
use typst::foundations::func::Repr;
let mut func = func;
while let Repr::With(f) = func.inner() {
// todo: complete with positional arguments
// with_args.push(ArgValue::Instance(f.1.clone()));
func = f.0.clone();
}
let signature = analyze_signature(func.clone());
// Exclude named arguments which are already present.
let exclude: Vec<_> = args
.items()
.filter_map(|arg| match arg {
ast::Arg::Named(named) => Some(named.name()),
_ => None,
})
.collect();
for (name, param) in &signature.named {
if exclude.iter().any(|ident| ident.as_str() == name) {
continue;
}
if set && !param.settable {
continue;
}
if param.named {
ctx.completions.push(Completion {
kind: CompletionKind::Param,
label: param.name.clone().into(),
apply: Some(eco_format!("{}: ${{}}", param.name)),
detail: Some(plain_docs_sentence(&param.docs)),
});
}
if param.positional {
ctx.cast_completions(&param.input);
}
}
if ctx.before.ends_with(',') {
ctx.enrich(" ", "");
}
}
/// Resolve a callee expression to a function.
// todo: fallback to static analysis if we can't resolve the callee
pub fn resolve_global_callee<'a>(
ctx: &mut CompletionContext<'a, '_>,
callee: ast::Expr<'a>,
) -> Option<Func> {
resolve_global_dyn_callee(ctx, callee)
.or_else(|| {
let source = ctx.ctx.source_by_id(callee.span().id()?).ok()?;
let node = source.find(callee.span())?;
let cursor = node.offset();
let deref_target = get_deref_target(node, cursor)?;
let def = find_definition(ctx.ctx, source.clone(), deref_target)?;
match def.kind {
LexicalKind::Var(LexicalVarKind::Function) => match def.value {
Some(Value::Func(f)) => Some(f),
_ => None,
},
_ => None,
}
})
.or_else(|| {
let lib = ctx.world().library();
let value = match callee {
ast::Expr::Ident(ident) => lib.global.scope().get(&ident)?,
ast::Expr::FieldAccess(access) => match access.target() {
ast::Expr::Ident(target) => match lib.global.scope().get(&target)? {
Value::Module(module) => module.field(&access.field()).ok()?,
Value::Func(func) => func.field(&access.field()).ok()?,
_ => return None,
},
_ => return None,
},
_ => return None,
};
match value {
Value::Func(func) => Some(func.clone()),
_ => None,
}
})
}
/// Resolve a callee expression to a dynamic function.
// todo: fallback to static analysis if we can't resolve the callee
fn resolve_global_dyn_callee<'a>(
ctx: &CompletionContext<'a, '_>,
callee: ast::Expr<'a>,
) -> Option<Func> {
let values = analyze_expr(ctx.world(), &ctx.root.find(callee.span())?);
values.into_iter().find_map(|v| match v.0 {
Value::Func(f) => Some(f),
_ => None,
})
}