dev: collect func.with type information for signature help (#759)

* feat: static analysis on `func.with` bindings

* test: update snapshot
This commit is contained in:
Myriad-Dreamin 2024-11-02 14:52:25 +08:00 committed by GitHub
parent 67367b03bf
commit f1f77065d7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 163 additions and 230 deletions

View file

@ -376,7 +376,6 @@ mod signature_tests {
use core::fmt;
use typst::foundations::Repr;
use typst::syntax::LinkedNode;
use typst_shim::syntax::LinkedNodeExt;
@ -424,11 +423,9 @@ mod signature_tests {
if let Some(name) = &arg.name {
write!(f, "{name}: ")?;
}
write!(
f,
"{}, ",
arg.value.as_ref().map(|v| v.repr()).unwrap_or_default()
)?;
let term = arg.term.as_ref();
let term = term.and_then(|v| v.describe()).unwrap_or_default();
write!(f, "{term}, ")?;
}
f.write_str("\n")?;
}

View file

@ -813,7 +813,7 @@ impl SharedContext {
/// Get the manifest of a package by file id.
pub fn get_manifest(&self, toml_id: TypstFileId) -> StrResult<PackageManifest> {
crate::docs::get_manifest(&self.world, toml_id)
crate::package::get_manifest(&self.world, toml_id)
}
/// Compute the signature of a function.

View file

@ -4,12 +4,14 @@ use itertools::Either;
use tinymist_derive::BindTyCtx;
use typst::foundations::{Closure, ParamInfo};
use super::{prelude::*, BoundChecker, Definition, DocSource, SharedContext, SigTy, TypeVar};
use super::{
prelude::*, BoundChecker, Definition, DocSource, SharedContext, SigTy, SigWithTy, TypeVar,
};
use crate::analysis::PostTypeChecker;
use crate::docs::{UntypedDefDocs, UntypedSignatureDocs, UntypedVarDocs};
use crate::syntax::get_non_strict_def_target;
use crate::ty::TyCtx;
use crate::ty::TypeBounds;
use crate::ty::{InsTy, TyCtx};
use crate::upstream::truncated_repr;
/// Describes a function parameter.
@ -121,6 +123,17 @@ impl Signature {
// todo: with stack
primary
}
pub(crate) fn param_shift(&self, _ctx: &mut LocalContext) -> usize {
match self {
Signature::Primary(_) => 0,
Signature::Partial(sig) => sig
.with_stack
.iter()
.map(|ws| ws.items.len())
.sum::<usize>(),
}
}
}
/// Describes a primary function signature.
@ -200,9 +213,9 @@ impl PrimarySignature {
#[derive(Debug, Clone)]
pub struct ArgInfo {
/// The argument's name.
pub name: Option<EcoString>,
/// The argument's value.
pub value: Option<Value>,
pub name: Option<StrRef>,
/// The argument's term.
pub term: Option<Ty>,
}
/// Describes a function argument list.
@ -289,7 +302,7 @@ fn analyze_type_signature(
};
// todo: this will affect inlay hint: _var_with
let (_var_with, docstring) = match type_info.var_docs.get(&v.def).map(|x| x.as_ref()) {
let (var_with, docstring) = match type_info.var_docs.get(&v.def).map(|x| x.as_ref()) {
Some(UntypedDefDocs::Function(sig)) => (vec![], Either::Left(sig.as_ref())),
Some(UntypedDefDocs::Variable(d)) => find_alias_stack(&mut ty_ctx, &v, d)?,
_ => return None,
@ -297,7 +310,7 @@ fn analyze_type_signature(
let docstring = match docstring {
Either::Left(docstring) => docstring,
Either::Right(f) => return Some(ctx.type_of_func(f)),
Either::Right(f) => return Some(wind_stack(var_with, ctx.type_of_func(f))),
};
let mut param_specs = Vec::new();
@ -356,13 +369,14 @@ fn analyze_type_signature(
});
}
Some(Signature::Primary(Arc::new(PrimarySignature {
let sig = Signature::Primary(Arc::new(PrimarySignature {
docs: Some(docstring.docs.clone()),
param_specs,
has_fill_or_size_or_stroke,
sig_ty,
_broken,
})))
}));
Some(wind_stack(var_with, sig))
}
src @ (DocSource::Builtin(..) | DocSource::Ins(..)) => {
Some(ctx.type_of_func(src.as_func()?))
@ -370,17 +384,54 @@ fn analyze_type_signature(
}
}
fn wind_stack(var_with: Vec<WithElem>, sig: Signature) -> Signature {
if var_with.is_empty() {
return sig;
}
let (primary, mut base_args) = match sig {
Signature::Primary(primary) => (primary, eco_vec![]),
Signature::Partial(partial) => (partial.signature.clone(), partial.with_stack.clone()),
};
let mut accepting = primary.pos().iter().skip(base_args.len());
// Ignoring docs at the moment
for (_d, w) in var_with {
if let Some(w) = w {
let mut items = eco_vec![];
for pos in w.with.positional_params() {
let Some(arg) = accepting.next() else {
break;
};
items.push(ArgInfo {
name: Some(arg.name.clone()),
term: Some(pos.clone()),
});
}
// todo: ignored spread arguments
if !items.is_empty() {
base_args.push(ArgsInfo { items });
}
}
}
Signature::Partial(Arc::new(PartialSignature {
signature: primary,
with_stack: base_args,
}))
}
type WithElem<'a> = (&'a UntypedVarDocs, Option<Interned<SigWithTy>>);
fn find_alias_stack<'a>(
ctx: &'a mut PostTypeChecker,
v: &Interned<TypeVar>,
d: &'a UntypedVarDocs,
) -> Option<(
Vec<&'a UntypedVarDocs>,
Either<&'a UntypedSignatureDocs, Func>,
)> {
) -> Option<(Vec<WithElem<'a>>, Either<&'a UntypedSignatureDocs, Func>)> {
let mut checker = AliasStackChecker {
ctx,
stack: vec![d],
stack: vec![(d, None)],
res: None,
checking_with: true,
};
@ -393,7 +444,7 @@ fn find_alias_stack<'a>(
#[bind(ctx)]
struct AliasStackChecker<'a, 'b> {
ctx: &'a mut PostTypeChecker<'b>,
stack: Vec<&'a UntypedVarDocs>,
stack: Vec<WithElem<'a>>,
res: Option<Either<&'a UntypedSignatureDocs, Func>>,
checking_with: bool,
}
@ -420,7 +471,7 @@ impl<'a, 'b> BoundChecker for AliasStackChecker<'a, 'b> {
}
Some(UntypedDefDocs::Variable(d)) => {
self.checking_with = true;
self.stack.push(d);
self.stack.push((d, None));
self.check_var_rec(u, pol);
self.stack.pop();
self.checking_with = false;
@ -437,6 +488,7 @@ impl<'a, 'b> BoundChecker for AliasStackChecker<'a, 'b> {
match (self.checking_with, ty) {
(true, Ty::With(w)) => {
log::debug!("collecting with {ty:?} {pol:?}");
self.stack.last_mut().unwrap().1 = Some(w.clone());
self.checking_with = false;
w.sig.bounds(pol, self);
self.checking_with = true;
@ -485,7 +537,7 @@ fn analyze_dyn_signature(
.iter()
.map(|arg| ArgInfo {
name: arg.name.clone().map(From::from),
value: Some(arg.value.v.clone()),
term: Some(Ty::Value(InsTy::new(arg.value.v.clone()))),
})
.collect(),
});

View file

@ -11,11 +11,12 @@ use typst::syntax::package::PackageSpec;
use typst::syntax::FileId;
use crate::docs::file_id_repr;
use crate::package::{get_manifest_id, PackageInfo};
use crate::syntax::{Decl, DefKind, Expr, ExprInfo};
use crate::ty::Interned;
use crate::LocalContext;
use super::{get_manifest_id, DefDocs, PackageInfo};
use super::DefDocs;
/// Get documentation of definitions in a package.
pub fn package_module_docs(ctx: &mut LocalContext, pkg: &PackageInfo) -> StrResult<PackageDefInfo> {

View file

@ -6,24 +6,13 @@ use ecow::{EcoString, EcoVec};
use indexmap::IndexSet;
use serde::{Deserialize, Serialize};
use typst::diag::{eco_format, StrResult};
use typst::syntax::package::{PackageManifest, PackageSpec};
use typst::syntax::{FileId, Span, VirtualPath};
use typst::World;
use typst::syntax::package::PackageManifest;
use typst::syntax::{FileId, Span};
use crate::docs::{file_id_repr, module_docs, DefDocs, PackageDefInfo};
use crate::package::{get_manifest_id, PackageInfo};
use crate::LocalContext;
/// Check Package.
pub fn check_package(ctx: &mut LocalContext, spec: &PackageInfo) -> StrResult<()> {
let toml_id = get_manifest_id(spec)?;
let manifest = ctx.get_manifest(toml_id)?;
let entry_point = toml_id.join(&manifest.package.entrypoint);
ctx.shared_().preload_package(entry_point);
Ok(())
}
/// Generate full documents in markdown format
pub fn package_docs(ctx: &mut LocalContext, spec: &PackageInfo) -> StrResult<String> {
log::info!("generate_md_docs {spec:?}");
@ -326,55 +315,6 @@ pub fn package_docs(ctx: &mut LocalContext, spec: &PackageInfo) -> StrResult<Str
Ok(md)
}
/// Parses the manifest of the package located at `package_path`.
pub fn get_manifest_id(spec: &PackageInfo) -> StrResult<FileId> {
Ok(FileId::new(
Some(PackageSpec {
namespace: spec.namespace.clone(),
name: spec.name.clone(),
version: spec.version.parse()?,
}),
VirtualPath::new("typst.toml"),
))
}
/// Parses the manifest of the package located at `package_path`.
pub fn get_manifest(world: &dyn World, toml_id: FileId) -> StrResult<PackageManifest> {
let toml_data = world
.file(toml_id)
.map_err(|err| eco_format!("failed to read package manifest ({})", err))?;
let string = std::str::from_utf8(&toml_data)
.map_err(|err| eco_format!("package manifest is not valid UTF-8 ({})", err))?;
toml::from_str(string)
.map_err(|err| eco_format!("package manifest is malformed ({})", err.message()))
}
/// Information about a package.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PackageInfo {
/// The path to the package if any.
pub path: PathBuf,
/// The namespace the package lives in.
pub namespace: EcoString,
/// The name of the package within its namespace.
pub name: EcoString,
/// The package's version.
pub version: String,
}
impl From<(PathBuf, PackageSpec)> for PackageInfo {
fn from((path, spec): (PathBuf, PackageSpec)) -> Self {
Self {
path,
namespace: spec.namespace,
name: spec.name,
version: spec.version.to_string(),
}
}
}
fn jbase64<T: Serialize>(s: &T) -> String {
use base64::Engine;
let content = serde_json::to_string(s).unwrap();

View file

@ -3,6 +3,7 @@ source: crates/tinymist-query/src/analysis.rs
expression: SignatureSnapshot(result.as_ref())
input_file: crates/tinymist-query/src/fixtures/signature/builtin_with.typ
---
with red: 50%, green: 50%, blue: 50%,
fn(
red,
green,

View file

@ -3,6 +3,7 @@ source: crates/tinymist-query/src/analysis.rs
expression: SignatureSnapshot(result.as_ref())
input_file: crates/tinymist-query/src/fixtures/signature/user_with.typ
---
with u: 1,
fn(
u,
v,

View file

@ -196,12 +196,6 @@ fn def_tooltip(
results.push(MarkedString::String(doc.hover_docs().into()));
}
// if let Some(doc) = sig {
// results.push(MarkedString::String(doc.def_docs().clone()));
// } else if let Some(doc) = DocTooltip::get(ctx, &def) {
// results.push(MarkedString::String(doc));
// }
if let Some(link) = ExternalDocLink::get(&def) {
actions.push(link);
}
@ -294,8 +288,7 @@ fn render_actions(results: &mut Vec<MarkedString>, actions: Vec<CommandLink>) {
return;
}
let g = actions.into_iter().join(" | ");
results.push(MarkedString::String(g));
results.push(MarkedString::String(actions.into_iter().join(" | ")));
}
struct ExternalDocLink;

View file

@ -1,11 +1,8 @@
use std::ops::Range;
use lsp_types::{InlayHintKind, InlayHintLabel};
use crate::{
analysis::{analyze_call, ParamKind},
prelude::*,
SemanticRequest,
};
/// Configuration for inlay hints.

View file

@ -5,10 +5,76 @@ use std::path::PathBuf;
use std::sync::OnceLock;
use parking_lot::Mutex;
use reflexo_typst::package::PackageSpec;
use reflexo_typst::typst::prelude::*;
use reflexo_typst::{package::PackageSpec, TypstFileId};
use serde::{Deserialize, Serialize};
use tinymist_world::https::HttpsRegistry;
use typst::diag::EcoString;
use typst::diag::{EcoString, StrResult};
use typst::syntax::package::PackageManifest;
use typst::syntax::VirtualPath;
use typst::World;
use crate::LocalContext;
/// Information about a package.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PackageInfo {
/// The path to the package if any.
pub path: PathBuf,
/// The namespace the package lives in.
pub namespace: EcoString,
/// The name of the package within its namespace.
pub name: EcoString,
/// The package's version.
pub version: String,
}
impl From<(PathBuf, PackageSpec)> for PackageInfo {
fn from((path, spec): (PathBuf, PackageSpec)) -> Self {
Self {
path,
namespace: spec.namespace,
name: spec.name,
version: spec.version.to_string(),
}
}
}
/// Parses the manifest of the package located at `package_path`.
pub fn get_manifest_id(spec: &PackageInfo) -> StrResult<TypstFileId> {
Ok(TypstFileId::new(
Some(PackageSpec {
namespace: spec.namespace.clone(),
name: spec.name.clone(),
version: spec.version.parse()?,
}),
VirtualPath::new("typst.toml"),
))
}
/// Parses the manifest of the package located at `package_path`.
pub fn get_manifest(world: &dyn World, toml_id: TypstFileId) -> StrResult<PackageManifest> {
let toml_data = world
.file(toml_id)
.map_err(|err| eco_format!("failed to read package manifest ({})", err))?;
let string = std::str::from_utf8(&toml_data)
.map_err(|err| eco_format!("package manifest is not valid UTF-8 ({})", err))?;
toml::from_str(string)
.map_err(|err| eco_format!("package manifest is malformed ({})", err.message()))
}
/// Check Package.
pub fn check_package(ctx: &mut LocalContext, spec: &PackageInfo) -> StrResult<()> {
let toml_id = get_manifest_id(spec)?;
let manifest = ctx.get_manifest(toml_id)?;
let entry_point = toml_id.join(&manifest.package.entrypoint);
ctx.shared_().preload_package(entry_point);
Ok(())
}
/// Get the packages in namespaces and their descriptions.
pub fn list_package_by_namespace(

View file

@ -3,10 +3,8 @@ use typst_shim::syntax::LinkedNodeExt;
use crate::{
adt::interner::Interned,
analysis::Definition,
prelude::*,
syntax::{find_docs_before, get_check_target, get_deref_target, CheckTarget, ParamTarget},
upstream::plain_docs_sentence,
syntax::{get_check_target, get_deref_target, CheckTarget, ParamTarget},
LspParamInfo, SemanticRequest,
};
@ -41,33 +39,14 @@ impl SemanticRequest for SignatureHelpRequest {
};
let deref_target = get_deref_target(callee, cursor)?;
let def_link = ctx.def_of_syntax(&source, None, deref_target)?;
let documentation = DocTooltip::get(ctx, &def_link)
.as_deref()
.map(markdown_docs);
let Some(Value::Func(function)) = def_link.value() else {
return None;
};
log::trace!("got function {function:?}");
let mut function = &function;
use typst::foundations::func::Repr;
let mut param_shift = 0;
while let Repr::With(inner) = function.inner() {
param_shift += inner.1.items.iter().filter(|x| x.name.is_none()).count();
function = &inner.0;
}
let sig = ctx.sig_of_func(function.clone());
let def = ctx.def_of_syntax(&source, None, deref_target)?;
let sig = ctx.sig_of_def(def.clone())?;
log::debug!("got signature {sig:?}");
let param_shift = sig.param_shift(ctx);
let mut active_parameter = None;
let mut label = def_link.name().as_ref().to_owned();
let mut label = def.name().as_ref().to_owned();
let mut params = Vec::new();
label.push('(');
@ -137,7 +116,7 @@ impl SemanticRequest for SignatureHelpRequest {
Some(SignatureHelp {
signatures: vec![SignatureInformation {
label: label.to_string(),
documentation,
documentation: sig.primary().docs.as_deref().map(markdown_docs),
parameters: Some(params),
active_parameter: active_parameter.map(|x| x as u32),
}],
@ -147,54 +126,6 @@ impl SemanticRequest for SignatureHelpRequest {
}
}
pub(crate) struct DocTooltip;
impl DocTooltip {
pub fn get(ctx: &mut LocalContext, def: &Definition) -> Option<String> {
self::DocTooltip::get_inner(ctx, def).map(|s| "\n\n".to_owned() + &s)
}
fn get_inner(ctx: &mut LocalContext, def: &Definition) -> Option<String> {
let value = def.value();
if matches!(value, Some(Value::Func(..))) {
if let Some(builtin) = Self::builtin_func_tooltip(def) {
return Some(plain_docs_sentence(builtin).into());
}
};
let (fid, def_range) = def.def_at(ctx.shared()).clone()?;
let src = ctx.source_by_id(fid).ok()?;
find_docs_before(&src, def_range.start)
}
}
impl DocTooltip {
fn builtin_func_tooltip(def: &Definition) -> Option<&'_ str> {
let value = def.value();
let Some(Value::Func(func)) = &value else {
return None;
};
use typst::foundations::func::Repr;
let mut func = func;
let docs = 'search: loop {
match func.inner() {
Repr::Native(n) => break 'search n.docs,
Repr::Element(e) => break 'search e.docs(),
Repr::With(w) => {
func = &w.0;
}
Repr::Closure(..) => {
return None;
}
}
};
Some(docs)
}
}
fn markdown_docs(docs: &str) -> Documentation {
Documentation::MarkupContent(MarkupContent {
kind: MarkupKind::Markdown,

View file

@ -1,50 +1,4 @@
use std::ops::Range;
use typst::syntax::SyntaxNode;
use typst_shim::syntax::LinkedNodeExt;
use crate::prelude::*;
use crate::syntax::get_def_target;
use super::DefTarget;
pub fn find_docs_before(src: &Source, cursor: usize) -> Option<String> {
log::debug!("finding docs at: {id:?}, {cursor}", id = src.id());
let root = LinkedNode::new(src.root());
let leaf = root.leaf_at_compat(cursor)?;
let def_target = get_def_target(leaf.clone())?;
find_docs_of(src, def_target)
}
pub fn find_docs_of(src: &Source, def_target: DefTarget) -> Option<String> {
let root = LinkedNode::new(src.root());
log::debug!("found docs target: {:?}", def_target.node().kind());
// todo: import node
let target = def_target.node().clone();
let mut node = target.clone();
while let Some(prev) = node.prev_sibling() {
node = prev;
if node.kind() == SyntaxKind::Hash {
continue;
}
let start = node.range().end;
let end = target.range().start;
if end <= start {
return None;
}
return extract_document_between(node.parent()?, start..end, false);
}
if node.parent()?.range() == root.range() && node.prev_sibling().is_none() {
return extract_document_between(node.parent()?, root.offset()..node.range().start, false);
}
None
}
pub fn find_module_level_docs(src: &Source) -> Option<String> {
log::debug!("finding docs at: {id:?}", id = src.id());