mirror of
https://github.com/Myriad-Dreamin/tinymist.git
synced 2025-07-24 13:13:43 +00:00
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:
parent
631f6e288c
commit
d708bdfe2d
8 changed files with 605 additions and 611 deletions
|
@ -1,5 +1,7 @@
|
||||||
//! Semantic static and dynamic analysis of the source code.
|
//! Semantic static and dynamic analysis of the source code.
|
||||||
|
|
||||||
|
pub mod call;
|
||||||
|
pub use call::*;
|
||||||
pub mod def_use;
|
pub mod def_use;
|
||||||
pub use def_use::*;
|
pub use def_use::*;
|
||||||
pub mod track_values;
|
pub mod track_values;
|
||||||
|
|
436
crates/tinymist-query/src/analysis/call.rs
Normal file
436
crates/tinymist-query/src/analysis/call.rs
Normal 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(());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -115,7 +115,7 @@ impl<'w> AnalysisContext<'w> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the world surface for Typst compiler.
|
/// Get the world surface for Typst compiler.
|
||||||
pub fn world(&self) -> &dyn World {
|
pub fn world(&self) -> &'w dyn World {
|
||||||
self.resources.world()
|
self.resources.world()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ use crate::{
|
||||||
prelude::*,
|
prelude::*,
|
||||||
syntax::{get_deref_target, DerefTarget},
|
syntax::{get_deref_target, DerefTarget},
|
||||||
typst_to_lsp::completion_kind,
|
typst_to_lsp::completion_kind,
|
||||||
upstream::{autocomplete_, Completion, CompletionContext, CompletionKind},
|
upstream::{autocomplete, Completion, CompletionContext, CompletionKind},
|
||||||
LspCompletion, StatefulRequest,
|
LspCompletion, StatefulRequest,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -111,8 +111,8 @@ impl StatefulRequest for CompletionRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
let items = completion_result.or_else(|| {
|
let items = completion_result.or_else(|| {
|
||||||
let cc_ctx = CompletionContext::new(ctx.world(), doc, &source, cursor, explicit)?;
|
let cc_ctx = CompletionContext::new(ctx, doc, &source, cursor, explicit)?;
|
||||||
let (offset, mut completions) = autocomplete_(cc_ctx)?;
|
let (offset, mut completions) = autocomplete(cc_ctx)?;
|
||||||
|
|
||||||
let replace_range;
|
let replace_range;
|
||||||
if match_ident.as_ref().is_some_and(|i| i.offset() == offset) {
|
if match_ident.as_ref().is_some_and(|i| i.offset() == offset) {
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
use core::fmt;
|
use core::fmt;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
analyze_signature, find_definition,
|
analysis::analyze_signature,
|
||||||
|
find_definition,
|
||||||
prelude::*,
|
prelude::*,
|
||||||
syntax::{find_document_before, get_deref_target, LexicalKind, LexicalVarKind},
|
syntax::{find_document_before, get_deref_target, LexicalKind, LexicalVarKind},
|
||||||
upstream::{expr_tooltip, tooltip, Tooltip},
|
upstream::{expr_tooltip, tooltip, Tooltip},
|
||||||
|
|
|
@ -1,16 +1,13 @@
|
||||||
use core::fmt;
|
use std::ops::Range;
|
||||||
use std::{borrow::Cow, ops::Range};
|
|
||||||
|
|
||||||
use ecow::{eco_format, eco_vec};
|
|
||||||
use log::debug;
|
use log::debug;
|
||||||
use lsp_types::{InlayHintKind, InlayHintLabel};
|
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.
|
/// Configuration for inlay hints.
|
||||||
pub struct InlayHintConfig {
|
pub struct InlayHintConfig {
|
||||||
|
@ -314,361 +311,6 @@ fn inlay_hint(
|
||||||
Ok(worker.hints)
|
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 {
|
fn is_one_line(src: &Source, arg_node: &LinkedNode<'_>) -> bool {
|
||||||
is_one_line_(src, arg_node).unwrap_or(true)
|
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)
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
|
@ -1,24 +1,25 @@
|
||||||
use std::cmp::Reverse;
|
use std::cmp::Reverse;
|
||||||
use std::collections::{BTreeSet, HashSet};
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use ecow::{eco_format, EcoString};
|
use ecow::{eco_format, EcoString};
|
||||||
use if_chain::if_chain;
|
use if_chain::if_chain;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use typst::foundations::{
|
use typst::foundations::{
|
||||||
fields_on, format_str, mutable_methods_on, repr, AutoValue, CastInfo, Func, Label, NoneValue,
|
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::model::Document;
|
||||||
use typst::syntax::{ast, is_id_continue, is_id_start, is_ident, LinkedNode, Source, SyntaxKind};
|
use typst::syntax::{ast, is_id_continue, is_id_start, is_ident, LinkedNode, Source, SyntaxKind};
|
||||||
use typst::text::RawElem;
|
use typst::text::RawElem;
|
||||||
use typst::visualize::Color;
|
use typst::visualize::Color;
|
||||||
use typst::World;
|
|
||||||
use unscanny::Scanner;
|
use unscanny::Scanner;
|
||||||
|
|
||||||
use super::{plain_docs_sentence, summarize_font_family};
|
use super::{plain_docs_sentence, summarize_font_family};
|
||||||
use crate::analysis::{analyze_expr, analyze_import, analyze_labels};
|
use crate::analysis::{analyze_expr, analyze_import, analyze_labels};
|
||||||
|
use crate::AnalysisContext;
|
||||||
|
|
||||||
mod ext;
|
mod ext;
|
||||||
|
use ext::*;
|
||||||
|
|
||||||
/// Autocomplete a cursor position in a source file.
|
/// 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
|
/// Passing a `document` (from a previous compilation) is optional, but enhances
|
||||||
/// the autocompletions. Label completions, for instance, are only generated
|
/// the autocompletions. Label completions, for instance, are only generated
|
||||||
/// when the document is available.
|
/// when the document is available.
|
||||||
pub fn autocomplete(
|
pub fn autocomplete(mut ctx: CompletionContext) -> Option<(usize, Vec<Completion>)> {
|
||||||
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;
|
|
||||||
let _ = complete_comments(&mut ctx)
|
let _ = complete_comments(&mut ctx)
|
||||||
|| complete_field_accesses(&mut ctx)
|
|| complete_field_accesses(&mut ctx)
|
||||||
|| complete_open_labels(&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.is::<ast::Expr>();
|
||||||
if prev.parent_kind() != Some(SyntaxKind::Markup) ||
|
if prev.parent_kind() != Some(SyntaxKind::Markup) ||
|
||||||
prev.prev_sibling_kind() == Some(SyntaxKind::Hash);
|
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 {
|
then {
|
||||||
ctx.from = ctx.cursor;
|
ctx.from = ctx.cursor;
|
||||||
field_access_completions(ctx, &value, &styles);
|
field_access_completions(ctx, &value, &styles);
|
||||||
|
@ -383,7 +361,7 @@ fn complete_field_accesses(ctx: &mut CompletionContext) -> bool {
|
||||||
if prev.kind() == SyntaxKind::Dot;
|
if prev.kind() == SyntaxKind::Dot;
|
||||||
if let Some(prev_prev) = prev.prev_sibling();
|
if let Some(prev_prev) = prev.prev_sibling();
|
||||||
if prev_prev.is::<ast::Expr>();
|
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 {
|
then {
|
||||||
ctx.from = ctx.leaf.offset();
|
ctx.from = ctx.leaf.offset();
|
||||||
field_access_completions(ctx, &value, &styles);
|
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.
|
/// Add completions for all exports of a module.
|
||||||
fn import_item_completions<'a>(
|
fn import_item_completions<'a>(
|
||||||
ctx: &mut CompletionContext<'a>,
|
ctx: &mut CompletionContext<'a, '_>,
|
||||||
existing: ast::ImportItems<'a>,
|
existing: ast::ImportItems<'a>,
|
||||||
source: &LinkedNode,
|
source: &LinkedNode,
|
||||||
) {
|
) {
|
||||||
let Some(value) = analyze_import(ctx.world, source) else {
|
let Some(value) = analyze_import(ctx.world(), source) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let Some(scope) = value.scope() else { return };
|
let Some(scope) = value.scope() else { return };
|
||||||
|
@ -742,58 +720,9 @@ fn complete_params(ctx: &mut CompletionContext) -> bool {
|
||||||
false
|
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(¶m.input);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ctx.before.ends_with(',') {
|
|
||||||
ctx.enrich(" ", "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add completions for the values of a named function parameter.
|
/// Add completions for the values of a named function parameter.
|
||||||
fn named_param_value_completions<'a>(
|
fn named_param_value_completions<'a>(
|
||||||
ctx: &mut CompletionContext<'a>,
|
ctx: &mut CompletionContext<'a, '_>,
|
||||||
callee: ast::Expr<'a>,
|
callee: ast::Expr<'a>,
|
||||||
name: &str,
|
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.
|
/// Complete in code mode.
|
||||||
fn complete_code(ctx: &mut CompletionContext) -> bool {
|
fn complete_code(ctx: &mut CompletionContext) -> bool {
|
||||||
if matches!(
|
if matches!(
|
||||||
|
@ -1040,14 +945,13 @@ fn code_completions(ctx: &mut CompletionContext, hash: bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Context for autocompletion.
|
/// Context for autocompletion.
|
||||||
pub struct CompletionContext<'a> {
|
pub struct CompletionContext<'a, 'w> {
|
||||||
pub world: &'a (dyn World + 'a),
|
pub ctx: &'a mut AnalysisContext<'w>,
|
||||||
pub document: Option<&'a Document>,
|
pub document: Option<&'a Document>,
|
||||||
pub global: &'a Scope,
|
|
||||||
pub math: &'a Scope,
|
|
||||||
pub text: &'a str,
|
pub text: &'a str,
|
||||||
pub before: &'a str,
|
pub before: &'a str,
|
||||||
pub after: &'a str,
|
pub after: &'a str,
|
||||||
|
pub root: LinkedNode<'a>,
|
||||||
pub leaf: LinkedNode<'a>,
|
pub leaf: LinkedNode<'a>,
|
||||||
pub cursor: usize,
|
pub cursor: usize,
|
||||||
pub explicit: bool,
|
pub explicit: bool,
|
||||||
|
@ -1056,26 +960,25 @@ pub struct CompletionContext<'a> {
|
||||||
pub seen_casts: HashSet<u128>,
|
pub seen_casts: HashSet<u128>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> CompletionContext<'a> {
|
impl<'a, 'w> CompletionContext<'a, 'w> {
|
||||||
/// Create a new autocompletion context.
|
/// Create a new autocompletion context.
|
||||||
pub fn new(
|
pub fn new(
|
||||||
world: &'a (dyn World + 'a),
|
ctx: &'a mut AnalysisContext<'w>,
|
||||||
document: Option<&'a Document>,
|
document: Option<&'a Document>,
|
||||||
source: &'a Source,
|
source: &'a Source,
|
||||||
cursor: usize,
|
cursor: usize,
|
||||||
explicit: bool,
|
explicit: bool,
|
||||||
) -> Option<Self> {
|
) -> Option<Self> {
|
||||||
let text = source.text();
|
let text = source.text();
|
||||||
let library = world.library();
|
let root = LinkedNode::new(source.root());
|
||||||
let leaf = LinkedNode::new(source.root()).leaf_at(cursor)?;
|
let leaf = root.leaf_at(cursor)?;
|
||||||
Some(Self {
|
Some(Self {
|
||||||
world,
|
ctx,
|
||||||
document,
|
document,
|
||||||
global: library.global.scope(),
|
|
||||||
math: library.math.scope(),
|
|
||||||
text,
|
text,
|
||||||
before: &text[..cursor],
|
before: &text[..cursor],
|
||||||
after: &text[cursor..],
|
after: &text[cursor..],
|
||||||
|
root,
|
||||||
leaf,
|
leaf,
|
||||||
cursor,
|
cursor,
|
||||||
explicit,
|
explicit,
|
||||||
|
@ -1116,7 +1019,7 @@ impl<'a> CompletionContext<'a> {
|
||||||
/// Add completions for all font families.
|
/// Add completions for all font families.
|
||||||
fn font_completions(&mut self) {
|
fn font_completions(&mut self) {
|
||||||
let equation = self.before_window(25).contains("equation");
|
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);
|
let detail = summarize_font_family(iter);
|
||||||
if !equation || family.contains("Math") {
|
if !equation || family.contains("Math") {
|
||||||
self.value_completion(
|
self.value_completion(
|
||||||
|
@ -1131,7 +1034,7 @@ impl<'a> CompletionContext<'a> {
|
||||||
|
|
||||||
/// Add completions for all available packages.
|
/// Add completions for all available packages.
|
||||||
fn package_completions(&mut self, all_versions: bool) {
|
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)));
|
packages.sort_by_key(|(spec, _)| (&spec.namespace, &spec.name, Reverse(spec.version)));
|
||||||
if !all_versions {
|
if !all_versions {
|
||||||
packages.dedup_by_key(|(spec, _)| (&spec.namespace, &spec.name));
|
packages.dedup_by_key(|(spec, _)| (&spec.namespace, &spec.name));
|
||||||
|
@ -1261,7 +1164,7 @@ impl<'a> CompletionContext<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add completions for a castable.
|
/// 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.
|
// Prevent duplicate completions from appearing.
|
||||||
if !self.seen_casts.insert(typst::util::hash128(cast)) {
|
if !self.seen_casts.insert(typst::util::hash128(cast)) {
|
||||||
return;
|
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,22 @@
|
||||||
use super::{Completion, CompletionContext, CompletionKind};
|
use super::{Completion, CompletionContext, CompletionKind};
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use ecow::EcoString;
|
use ecow::{eco_format, EcoString};
|
||||||
use typst::foundations::Value;
|
use typst::foundations::{Func, Value};
|
||||||
|
use typst::syntax::ast::AstNode;
|
||||||
use typst::syntax::{ast, SyntaxKind};
|
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.
|
/// Add completions for definitions that are available at the cursor.
|
||||||
///
|
///
|
||||||
/// Filters the global/math scope with the given filter.
|
/// 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 anaylyze = node.children().find(|child| child.is::<ast::Expr>());
|
||||||
let analyzed = anaylyze
|
let analyzed = anaylyze
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|source| analyze_import(self.world, source));
|
.and_then(|source| analyze_import(self.world(), source));
|
||||||
if analyzed.is_none() {
|
if analyzed.is_none() {
|
||||||
log::info!("failed to analyze import: {:?}", anaylyze);
|
log::info!("failed to analyze import: {:?}", anaylyze);
|
||||||
}
|
}
|
||||||
|
@ -113,7 +122,10 @@ impl<'a> CompletionContext<'a> {
|
||||||
| Some(SyntaxKind::MathAttach)
|
| 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() {
|
for (name, value) in scope.iter() {
|
||||||
if filter(value) && !defined.contains_key(name) {
|
if filter(value) && !defined.contains_key(name) {
|
||||||
self.value_completion(Some(name.clone()), value, parens, None);
|
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(¶m.docs)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if param.positional {
|
||||||
|
ctx.cast_completions(¶m.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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue