mirror of
https://github.com/Myriad-Dreamin/tinymist.git
synced 2025-07-19 02:35:00 +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.
|
||||
|
||||
pub mod call;
|
||||
pub use call::*;
|
||||
pub mod def_use;
|
||||
pub use def_use::*;
|
||||
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.
|
||||
pub fn world(&self) -> &dyn World {
|
||||
pub fn world(&self) -> &'w dyn World {
|
||||
self.resources.world()
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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::*;
|
||||
|
|
|
@ -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(¶m.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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(¶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