dev: merge signature docs and rest docs (#685)

* dev: deduplicate code

* dev: merge signature docs and rest docs

* dev: change struct of the `DocString`

* dev: improve structure of SymbolDocs
This commit is contained in:
Myriad-Dreamin 2024-10-16 11:16:16 +08:00 committed by GitHub
parent d121e8279d
commit b9da92175e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 522 additions and 455 deletions

View file

@ -130,7 +130,7 @@ impl<'a, 'w> TypeChecker<'a, 'w> {
var
}
fn get_var(&mut self, s: Span, r: IdentRef) -> Option<Ty> {
fn get_var(&mut self, s: Span, r: IdentRef) -> Option<Interned<TypeVar>> {
let def_id = self.get_def_id(s, &r)?;
// todo: false positive of clippy
@ -150,9 +150,9 @@ impl<'a, 'w> TypeChecker<'a, 'w> {
);
}
let var = self.info.vars.get_mut(&def_id).unwrap();
TypeScheme::witness_(s, var.as_type(), &mut self.info.mapping);
Some(var.as_type())
let var = self.info.vars.get(&def_id).unwrap().var.clone();
TypeScheme::witness_(s, Ty::Var(var.clone()), &mut self.info.mapping);
Some(var)
}
fn import_ty(&mut self, def_id: DefId) -> Option<Ty> {

View file

@ -14,7 +14,7 @@ use super::*;
const DOC_VARS: u64 = 0;
impl<'a, 'w> TypeChecker<'a, 'w> {
pub fn check_func_docs(&mut self, root: &LinkedNode) -> Option<DocString> {
pub fn check_func_docs(&mut self, root: &LinkedNode) -> Option<Arc<DocString>> {
let closure = root.cast::<ast::Closure>()?;
let documenting_id = closure
.name()
@ -23,7 +23,7 @@ impl<'a, 'w> TypeChecker<'a, 'w> {
self.check_docstring(root, DocStringKind::Function, documenting_id)
}
pub fn check_var_docs(&mut self, root: &LinkedNode) -> Option<DocString> {
pub fn check_var_docs(&mut self, root: &LinkedNode) -> Option<Arc<DocString>> {
let lb = root.cast::<ast::LetBinding>()?;
let first = lb.kind().bindings();
let documenting_id = first
@ -38,35 +38,40 @@ impl<'a, 'w> TypeChecker<'a, 'w> {
root: &LinkedNode,
kind: DocStringKind,
base_id: DefId,
) -> Option<DocString> {
) -> Option<Arc<DocString>> {
// todo: cache docs capture
// use parent of params, todo: reliable way to get the def target
let def = get_non_strict_def_target(root.clone())?;
let docs = find_docs_of(&self.source, def)?;
let docstring = self.ctx.compute_docstring(root.span().id()?, docs, kind)?;
Some(docstring.take().rename_based_on(base_id, self))
let res = Arc::new(docstring.take().rename_based_on(base_id, self));
self.info.var_docs.insert(base_id, res.clone());
Some(res)
}
}
/// The documentation string of an item
#[derive(Debug, Clone, Default)]
pub(crate) struct DocString {
pub struct DocString {
/// The documentation of the item
pub docs: Option<String>,
pub docs: Option<EcoString>,
/// The typing on definitions
pub var_bounds: HashMap<DefId, TypeVarBounds>,
/// The variable doc associated with the item
pub vars: HashMap<EcoString, VarDoc>,
pub vars: BTreeMap<StrRef, VarDoc>,
/// The type of the resultant type
pub res_ty: Option<Ty>,
}
impl DocString {
pub fn get_var(&self, name: &str) -> Option<&VarDoc> {
/// Get the documentation of a variable associated with the item
pub fn get_var(&self, name: &StrRef) -> Option<&VarDoc> {
self.vars.get(name)
}
pub fn var_ty(&self, name: &str) -> Option<&Ty> {
/// Get the type of a variable associated with the item
pub fn var_ty(&self, name: &StrRef) -> Option<&Ty> {
self.get_var(name).and_then(|v| v.ty.as_ref())
}
@ -105,11 +110,15 @@ impl DocString {
}
}
/// The documentation string of a variable associated with some item.
#[derive(Debug, Clone, Default)]
pub(crate) struct VarDoc {
pub _docs: Option<EcoString>,
pub struct VarDoc {
/// The documentation of the variable
pub docs: Option<EcoString>,
/// The type of the variable
pub ty: Option<Ty>,
pub _default: Option<EcoString>,
/// The default value of the variable
pub default: Option<EcoString>,
}
pub(crate) fn compute_docstring(
@ -129,6 +138,10 @@ pub(crate) fn compute_docstring(
match kind {
DocStringKind::Function => checker.check_func_docs(docs),
DocStringKind::Variable => checker.check_var_docs(docs),
DocStringKind::Module => None,
DocStringKind::Constant => None,
DocStringKind::Struct => None,
DocStringKind::Reference => None,
}
}
@ -148,14 +161,14 @@ impl<'a, 'w> DocsChecker<'a, 'w> {
let converted = identify_func_docs(&converted).ok()?;
let module = self.ctx.module_by_str(docs)?;
let mut params = HashMap::new();
let mut params = BTreeMap::new();
for param in converted.params.into_iter() {
params.insert(
param.name,
param.name.into(),
VarDoc {
_docs: Some(param.docs),
docs: Some(param.docs),
ty: self.check_type_strings(&module, &param.types),
_default: param.default,
default: param.default,
},
);
}
@ -174,7 +187,7 @@ impl<'a, 'w> DocsChecker<'a, 'w> {
pub fn check_var_docs(mut self, docs: String) -> Option<DocString> {
let converted = convert_docs(self.ctx.world(), &docs).ok()?;
let converted = identify_var_docs(&converted).ok()?;
let converted = identify_var_docs(converted).ok()?;
let module = self.ctx.module_by_str(docs)?;
let res_ty = converted
@ -184,7 +197,7 @@ impl<'a, 'w> DocsChecker<'a, 'w> {
Some(DocString {
docs: Some(converted.docs),
var_bounds: self.vars,
vars: HashMap::new(),
vars: BTreeMap::new(),
res_ty,
})
}

View file

@ -189,11 +189,13 @@ impl<'a, 'w> TypeChecker<'a, 'w> {
range: root.range(),
};
self.get_var(root.span(), ident_ref).or_else(|| {
let s = root.span();
let v = resolve_global_value(self.ctx, root, mode == InterpretMode::Math)?;
Some(Ty::Value(InsTy::new_at(v, s)))
})
self.get_var(root.span(), ident_ref)
.map(Ty::Var)
.or_else(|| {
let s = root.span();
let v = resolve_global_value(self.ctx, root, mode == InterpretMode::Math)?;
Some(Ty::Value(InsTy::new_at(v, s)))
})
}
fn check_array(&mut self, root: LinkedNode<'_>) -> Option<Ty> {
@ -373,7 +375,7 @@ impl<'a, 'w> TypeChecker<'a, 'w> {
fn check_closure(&mut self, root: LinkedNode<'_>) -> Option<Ty> {
let docstring = self.check_func_docs(&root);
let docstring = docstring.as_ref().unwrap_or(&EMPTY_DOCSTRING);
let docstring = docstring.as_deref().unwrap_or(&EMPTY_DOCSTRING);
let closure: ast::Closure = root.cast()?;
log::debug!("check closure: {:?} -> {docstring:#?}", closure.name());
@ -389,24 +391,24 @@ impl<'a, 'w> TypeChecker<'a, 'w> {
pos.push(self.check_pattern(pattern, Ty::Any, docstring, root.clone()));
}
ast::Param::Named(e) => {
let name = e.name().get();
let name = e.name().get().into();
let exp = self.check_expr_in(e.expr().span(), root.clone());
let v = self.get_var(e.name().span(), to_ident_ref(&root, e.name())?)?;
if let Some(annotated) = docstring.var_ty(name.as_str()) {
let v = Ty::Var(self.get_var(e.name().span(), to_ident_ref(&root, e.name())?)?);
if let Some(annotated) = docstring.var_ty(&name) {
self.constrain(&v, annotated);
}
// todo: this is less efficient than v.lbs.push(exp), we may have some idea to
// optimize it, so I put a todo here.
self.constrain(&exp, &v);
named.insert(name.into(), v);
defaults.insert(name.into(), exp);
named.insert(name.clone(), v);
defaults.insert(name, exp);
}
// todo: spread left/right
ast::Param::Spread(a) => {
if let Some(e) = a.sink_ident() {
let exp = Ty::Builtin(BuiltinTy::Args);
let v = self.get_var(e.span(), to_ident_ref(&root, e)?)?;
if let Some(annotated) = docstring.var_ty(e.get().as_str()) {
let v = Ty::Var(self.get_var(e.span(), to_ident_ref(&root, e)?)?);
if let Some(annotated) = docstring.var_ty(&e.get().as_str().into()) {
self.constrain(&v, annotated);
}
self.constrain(&exp, &v);
@ -467,13 +469,13 @@ impl<'a, 'w> TypeChecker<'a, 'w> {
.map(|init| self.check_expr_in(init.span(), root.clone()))
.unwrap_or_else(|| Ty::Builtin(BuiltinTy::Infer));
let v = self.get_var(c.span(), to_ident_ref(&root, c)?)?;
let v = Ty::Var(self.get_var(c.span(), to_ident_ref(&root, c)?)?);
self.constrain(&value, &v);
// todo lbs is the lexical signature.
}
ast::LetBindingKind::Normal(pattern) => {
let docstring = self.check_var_docs(&root);
let docstring = docstring.as_ref().unwrap_or(&EMPTY_DOCSTRING);
let docstring = docstring.as_deref().unwrap_or(&EMPTY_DOCSTRING);
let value = let_binding
.init()
@ -617,14 +619,15 @@ impl<'a, 'w> TypeChecker<'a, 'w> {
) -> Option<Ty> {
Some(match pattern {
ast::Pattern::Normal(ast::Expr::Ident(ident)) => {
let v = self.get_var(ident.span(), to_ident_ref(&root, ident)?)?;
let annotated = docs.var_ty(ident.get().as_str());
let var = self.get_var(ident.span(), to_ident_ref(&root, ident)?)?;
let annotated = docs.var_ty(&var.name);
let var = Ty::Var(var);
log::debug!("check pattern: {ident:?} with {value:?} and annotation {annotated:?}");
if let Some(annotated) = annotated {
self.constrain(&v, annotated);
self.constrain(&var, annotated);
}
self.constrain(&value, &v);
v
self.constrain(&value, &var);
var
}
ast::Pattern::Normal(_) => Ty::Any,
ast::Pattern::Placeholder(_) => Ty::Any,

View file

@ -1,95 +0,0 @@
use std::sync::Arc;
use ecow::{eco_format, EcoString};
use parking_lot::Mutex;
use serde::{Deserialize, Serialize};
use tinymist_world::base::{EntryState, ShadowApi, TaskInputs};
use tinymist_world::LspWorld;
use typst::foundations::Bytes;
use typst::{
diag::StrResult,
syntax::{FileId, VirtualPath},
};
use crate::docs::library;
use super::tidy::*;
/// Kind of a docstring.
#[derive(Debug, Clone, Hash, Serialize, Deserialize)]
pub enum DocStringKind {
/// A docstring for a function.
Function,
/// A docstring for a variable.
Variable,
}
/// Docs about a symbol.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind")]
pub enum RawDocs {
/// Docs about a function.
#[serde(rename = "func")]
Function(TidyFuncDocs),
/// Docs about a variable.
#[serde(rename = "var")]
Variable(TidyVarDocs),
/// Docs about a module.
#[serde(rename = "module")]
Module(TidyModuleDocs),
/// Other kinds of docs.
#[serde(rename = "plain")]
Plain(EcoString),
}
impl RawDocs {
/// Get the markdown representation of the docs.
pub fn docs(&self) -> &str {
match self {
Self::Function(docs) => docs.docs.as_str(),
Self::Variable(docs) => docs.docs.as_str(),
Self::Module(docs) => docs.docs.as_str(),
Self::Plain(docs) => docs.as_str(),
}
}
}
// Unfortunately, we have only 65536 possible file ids and we cannot revoke
// them. So we share a global file id for all docs conversion.
static DOCS_CONVERT_ID: std::sync::LazyLock<Mutex<FileId>> = std::sync::LazyLock::new(|| {
Mutex::new(FileId::new(None, VirtualPath::new("__tinymist_docs__.typ")))
});
pub(crate) fn convert_docs(world: &LspWorld, content: &str) -> StrResult<EcoString> {
static DOCS_LIB: std::sync::LazyLock<Arc<typlite::scopes::Scopes<typlite::value::Value>>> =
std::sync::LazyLock::new(library::lib);
let conv_id = DOCS_CONVERT_ID.lock();
let entry = EntryState::new_rootless(conv_id.vpath().as_rooted_path().into()).unwrap();
let entry = entry.select_in_workspace(*conv_id);
let mut w = world.task(TaskInputs {
entry: Some(entry),
inputs: None,
});
w.map_shadow_by_id(*conv_id, Bytes::from(content.as_bytes().to_owned()))?;
// todo: bad performance
w.source_db.take_state();
let conv = typlite::Typlite::new(Arc::new(w))
.with_library(DOCS_LIB.clone())
.annotate_elements(true)
.convert()
.map_err(|e| eco_format!("failed to convert to markdown: {e}"))?;
Ok(conv)
}
pub(crate) fn identify_docs(kind: &str, content: &str) -> StrResult<RawDocs> {
match kind {
"function" => identify_func_docs(content).map(RawDocs::Function),
"variable" => identify_var_docs(content).map(RawDocs::Variable),
"module" => identify_tidy_module_docs(content).map(RawDocs::Module),
_ => Err(eco_format!("unknown kind {kind}")),
}
}

View file

@ -1,20 +1,17 @@
//! Documentation generation utilities.
mod docstring;
mod library;
mod module;
mod package;
mod signature;
mod symbol;
mod tidy;
use ecow::EcoString;
use reflexo::path::unix_slash;
use typst::{foundations::Value, syntax::FileId};
pub use docstring::*;
pub use module::*;
pub use package::*;
pub use signature::*;
pub use symbol::*;
pub(crate) use tidy::*;
fn file_id_repr(k: FileId) -> String {
@ -25,13 +22,12 @@ fn file_id_repr(k: FileId) -> String {
}
}
fn kind_of(val: &Value) -> EcoString {
fn kind_of(val: &Value) -> DocStringKind {
match val {
Value::Module(_) => "module",
Value::Type(_) => "struct",
Value::Func(_) => "function",
Value::Label(_) => "reference",
_ => "constant",
Value::Module(_) => DocStringKind::Module,
Value::Type(_) => DocStringKind::Struct,
Value::Func(_) => DocStringKind::Function,
Value::Label(_) => DocStringKind::Reference,
_ => DocStringKind::Constant,
}
.into()
}

View file

@ -16,7 +16,7 @@ use crate::syntax::{find_docs_of, get_non_strict_def_target};
use crate::upstream::truncated_doc_repr;
use crate::AnalysisContext;
use super::{get_manifest, get_manifest_id, kind_of, PackageInfo, RawDocs, SignatureDocs};
use super::{get_manifest, get_manifest_id, kind_of, DocStringKind, PackageInfo, SymbolDocs};
/// Get documentation of symbols in a package.
pub fn package_module_docs(ctx: &mut AnalysisContext, pkg: &PackageInfo) -> StrResult<SymbolsInfo> {
@ -70,7 +70,7 @@ pub struct SymbolInfoHead {
/// The name of the symbol.
pub name: EcoString,
/// The kind of the symbol.
pub kind: EcoString,
pub kind: DocStringKind,
/// The location (file, start, end) of the symbol.
pub loc: Option<(usize, usize, usize)>,
/// Is the symbol reexport
@ -81,10 +81,8 @@ pub struct SymbolInfoHead {
pub oneliner: Option<String>,
/// The raw documentation of the symbol.
pub docs: Option<String>,
/// The signature of the symbol.
pub signature: Option<SignatureDocs>,
/// The parsed documentation of the symbol.
pub parsed_docs: Option<RawDocs>,
pub parsed_docs: Option<SymbolDocs>,
/// The value of the symbol.
#[serde(skip)]
pub constant: Option<EcoString>,

View file

@ -12,9 +12,7 @@ use typst::syntax::package::{PackageManifest, PackageSpec};
use typst::syntax::{FileId, Span, VirtualPath};
use typst::World;
use crate::docs::{
convert_docs, file_id_repr, identify_docs, module_docs, signature_docs, RawDocs, SymbolsInfo,
};
use crate::docs::{file_id_repr, module_docs, symbol_docs, SymbolDocs, SymbolsInfo};
use crate::syntax::IdentRef;
use crate::ty::Ty;
use crate::AnalysisContext;
@ -159,33 +157,32 @@ pub fn package_docs(
});
sym.head.loc = span;
let sym_value = sym.head.value.clone();
let signature = sym_value.and_then(|e| {
let def_ident = IdentRef {
name: sym.head.name.clone(),
range: sym.head.name_range.clone()?,
};
signature_docs(ctx, type_info, Some(&def_ident), &e, Some(&mut doc_ty))
let def_ident = sym.head.name_range.as_ref().map(|range| IdentRef {
name: sym.head.name.clone(),
range: range.clone(),
});
sym.head.signature = signature;
let docs = symbol_docs(
ctx,
type_info,
sym.head.kind,
def_ident.as_ref(),
sym.head.value.as_ref(),
sym.head.docs.as_deref(),
Some(&mut doc_ty),
);
let mut convert_err = None;
if let Some(docs) = &sym.head.docs {
match convert_docs(world, docs) {
Ok(content) => {
let docs = identify_docs(sym.head.kind.as_str(), &content)
.unwrap_or(RawDocs::Plain(content));
sym.head.parsed_docs = Some(docs.clone());
sym.head.docs = None;
}
Err(e) => {
let err = format!("failed to convert docs in {title}: {e}").replace(
"-->", "—>", // avoid markdown comment
);
log::error!("{err}");
convert_err = Some(err);
}
match &docs {
Ok(docs) => {
sym.head.parsed_docs = Some(docs.clone());
sym.head.docs = None;
}
Err(e) => {
let err = format!("failed to convert docs in {title}: {e}").replace(
"-->", "—>", // avoid markdown comment
);
log::error!("{err}");
convert_err = Some(err);
}
}
@ -227,7 +224,7 @@ pub fn package_docs(
let head = jbase64(&sym.head);
let _ = writeln!(md, "<!-- begin:symbol {ident} {head} -->");
if let Some(sig) = &sym.head.signature {
if let Some(SymbolDocs::Function(sig)) = &sym.head.parsed_docs {
let _ = writeln!(md, "<!-- begin:sig -->");
let _ = writeln!(md, "```typc");
let _ = writeln!(md, "let {name}({sig});", name = sym.head.name);
@ -245,13 +242,18 @@ pub fn package_docs(
}
(Some(docs), _) => {
let _ = writeln!(md, "{}", remove_list_annotations(docs.docs()));
if let RawDocs::Function(f) = docs {
for param in &f.params {
if let SymbolDocs::Function(f) = docs {
for param in f.pos.iter().chain(f.named.values()).chain(f.rest.as_ref())
{
let _ = writeln!(md, "<!-- begin:param {} -->", param.name);
let ty = match &param.cano_type {
Some((short, _)) => short,
None => "unknown",
};
let _ = writeln!(
md,
"#### {} ({})\n<!-- begin:param-doc {} -->\n{}\n<!-- end:param-doc {} -->",
param.name, param.types, param.name, param.docs, param.name
"#### {} ({ty:?})\n<!-- begin:param-doc {} -->\n{}\n<!-- end:param-doc {} -->",
param.name, param.name, param.docs, param.name
);
let _ = writeln!(md, "<!-- end:param -->");
}

View file

@ -1,228 +0,0 @@
use std::fmt;
use std::{collections::HashMap, sync::Arc};
use ecow::EcoString;
use serde::{Deserialize, Serialize};
use typst::foundations::Value;
use crate::analysis::analyze_dyn_signature;
use crate::syntax::IdentRef;
use crate::{ty::Ty, AnalysisContext};
type TypeRepr = Option<(/* short */ String, /* long */ String)>;
type ShowTypeRepr<'a> = &'a mut dyn FnMut(Option<&Ty>) -> TypeRepr;
/// Describes a primary function signature.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SignatureDocs {
/// The positional parameters.
pub pos: Vec<ParamDocs>,
/// The named parameters.
pub named: HashMap<String, ParamDocs>,
/// The rest parameter.
pub rest: Option<ParamDocs>,
/// The return type.
pub ret_ty: TypeRepr,
}
impl fmt::Display for SignatureDocs {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut is_first = true;
let mut write_sep = |f: &mut fmt::Formatter<'_>| {
if is_first {
is_first = false;
return Ok(());
}
f.write_str(", ")
};
for p in &self.pos {
write_sep(f)?;
f.write_str(&p.name)?;
if let Some(t) = &p.cano_type {
write!(f, ": {}", t.0)?;
}
}
if let Some(rest) = &self.rest {
write_sep(f)?;
f.write_str("..")?;
f.write_str(&rest.name)?;
if let Some(t) = &rest.cano_type {
write!(f, ": {}", t.0)?;
}
}
if !self.named.is_empty() {
let mut name_prints = vec![];
for v in self.named.values() {
let ty = v.cano_type.as_ref().map(|t| &t.0);
name_prints.push((v.name.clone(), ty, v.expr.clone()))
}
name_prints.sort();
for (k, t, v) in name_prints {
write_sep(f)?;
let v = v.as_deref().unwrap_or("any");
let mut v = v.trim();
if v.starts_with('{') && v.ends_with('}') && v.len() > 30 {
v = "{ ... }"
}
if v.starts_with('`') && v.ends_with('`') && v.len() > 30 {
v = "raw"
}
if v.starts_with('[') && v.ends_with(']') && v.len() > 30 {
v = "content"
}
f.write_str(&k)?;
if let Some(t) = t {
write!(f, ": {t}")?;
}
write!(f, " = {v}")?;
}
}
Ok(())
}
}
/// Describes a function parameter.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ParamDocs {
/// The parameter's name.
pub name: String,
/// Documentation for the parameter.
pub docs: String,
/// Inferred type of the parameter.
pub cano_type: TypeRepr,
/// The parameter's default name as value.
pub expr: Option<EcoString>,
/// 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,
}
type TypeInfo = (Arc<crate::analysis::DefUseInfo>, Arc<crate::ty::TypeScheme>);
pub(crate) fn signature_docs(
ctx: &mut AnalysisContext,
type_info: Option<&TypeInfo>,
def_ident: Option<&IdentRef>,
runtime_fn: &Value,
doc_ty: Option<ShowTypeRepr>,
) -> Option<SignatureDocs> {
let func = match runtime_fn {
Value::Func(f) => f,
_ => return None,
};
// todo: documenting with bindings
use typst::foundations::func::Repr;
let mut func = func;
loop {
match func.inner() {
Repr::Element(..) | Repr::Native(..) => {
break;
}
Repr::With(w) => {
func = &w.0;
}
Repr::Closure(..) => {
break;
}
}
}
let sig = analyze_dyn_signature(ctx, func.clone());
let type_sig = type_info.and_then(|(def_use, ty_chk)| {
let def_fid = func.span().id()?;
let (def_id, _) = def_use.get_def(def_fid, def_ident?)?;
ty_chk.type_of_def(def_id)
});
let type_sig = type_sig.and_then(|type_sig| type_sig.sig_repr(true));
const F: fn(Option<&Ty>) -> TypeRepr = |ty: Option<&Ty>| {
ty.and_then(|ty| ty.describe())
.map(|short| (short, format!("{ty:?}")))
};
let mut binding = F;
let doc_ty = doc_ty.unwrap_or(&mut binding);
let pos_in = sig
.primary()
.pos
.iter()
.enumerate()
.map(|(i, pos)| (pos, type_sig.as_ref().and_then(|sig| sig.pos(i))));
let named_in = sig
.primary()
.named
.iter()
.map(|x| (x, type_sig.as_ref().and_then(|sig| sig.named(x.0))));
let rest_in = sig
.primary()
.rest
.as_ref()
.map(|x| (x, type_sig.as_ref().and_then(|sig| sig.rest_param())));
let ret_in = type_sig
.as_ref()
.and_then(|sig| sig.body.as_ref())
.or_else(|| sig.primary().ret_ty.as_ref());
let pos = pos_in
.map(|(param, ty)| ParamDocs {
name: param.name.as_ref().to_owned(),
docs: param.docs.as_ref().to_owned(),
cano_type: doc_ty(ty.or(Some(&param.base_type))),
expr: param.expr.clone(),
positional: param.positional,
named: param.named,
variadic: param.variadic,
settable: param.settable,
})
.collect();
let named = named_in
.map(|((name, param), ty)| {
(
name.as_ref().to_owned(),
ParamDocs {
name: param.name.as_ref().to_owned(),
docs: param.docs.as_ref().to_owned(),
cano_type: doc_ty(ty.or(Some(&param.base_type))),
expr: param.expr.clone(),
positional: param.positional,
named: param.named,
variadic: param.variadic,
settable: param.settable,
},
)
})
.collect();
let rest = rest_in.map(|(param, ty)| ParamDocs {
name: param.name.as_ref().to_owned(),
docs: param.docs.as_ref().to_owned(),
cano_type: doc_ty(ty.or(Some(&param.base_type))),
expr: param.expr.clone(),
positional: param.positional,
named: param.named,
variadic: param.variadic,
settable: param.settable,
});
let ret_ty = doc_ty(ret_in);
Some(SignatureDocs {
pos,
named,
rest,
ret_ty,
})
}

View file

@ -0,0 +1,375 @@
use core::fmt;
use std::collections::BTreeMap;
use std::sync::Arc;
use ecow::{eco_format, EcoString};
use parking_lot::Mutex;
use serde::{Deserialize, Serialize};
use tinymist_world::base::{EntryState, ShadowApi, TaskInputs};
use tinymist_world::LspWorld;
use typst::foundations::{Bytes, Value};
use typst::{
diag::StrResult,
syntax::{FileId, VirtualPath},
};
use super::tidy::*;
use crate::analysis::{analyze_dyn_signature, ParamSpec};
use crate::docs::library;
use crate::syntax::IdentRef;
use crate::{ty::Ty, AnalysisContext};
type TypeRepr = Option<(/* short */ String, /* long */ String)>;
type ShowTypeRepr<'a> = &'a mut dyn FnMut(Option<&Ty>) -> TypeRepr;
/// Kind of a docstring.
#[derive(Debug, Default, Clone, Copy, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum DocStringKind {
/// A docstring for a any constant.
#[default]
Constant,
/// A docstring for a function.
Function,
/// A docstring for a variable.
Variable,
/// A docstring for a module.
Module,
/// A docstring for a struct.
Struct,
/// A docstring for a reference.
Reference,
}
impl fmt::Display for DocStringKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Constant => write!(f, "constant"),
Self::Function => write!(f, "function"),
Self::Variable => write!(f, "variable"),
Self::Module => write!(f, "module"),
Self::Struct => write!(f, "struct"),
Self::Reference => write!(f, "reference"),
}
}
}
/// Documentation about a symbol.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind")]
pub enum SymbolDocs {
/// Documentation about a function.
#[serde(rename = "func")]
Function(Box<SignatureDocs>),
/// Documentation about a variable.
#[serde(rename = "var")]
Variable(TidyVarDocs),
/// Documentation about a module.
#[serde(rename = "module")]
Module(TidyModuleDocs),
/// Other kinds of documentation.
#[serde(rename = "plain")]
Plain {
/// The content of the documentation.
docs: EcoString,
},
}
impl SymbolDocs {
/// Get the markdown representation of the documentation.
pub fn docs(&self) -> &str {
match self {
Self::Function(docs) => docs.docs.as_str(),
Self::Variable(docs) => docs.docs.as_str(),
Self::Module(docs) => docs.docs.as_str(),
Self::Plain { docs } => docs.as_str(),
}
}
}
pub(crate) fn symbol_docs(
ctx: &mut AnalysisContext,
type_info: Option<&TypeInfo>,
kind: DocStringKind,
def_ident: Option<&IdentRef>,
sym_value: Option<&Value>,
docs: Option<&str>,
doc_ty: Option<ShowTypeRepr>,
) -> Result<SymbolDocs, String> {
let signature = sym_value.and_then(|e| signature_docs(ctx, type_info, def_ident, e, doc_ty));
if let Some(signature) = signature {
return Ok(SymbolDocs::Function(Box::new(signature)));
}
if let Some(docs) = &docs {
match convert_docs(ctx.world(), docs) {
Ok(content) => {
let docs = identify_docs(kind, content.clone())
.unwrap_or(SymbolDocs::Plain { docs: content });
return Ok(docs);
}
Err(e) => {
let err = format!("failed to convert docs: {e}").replace(
"-->", "—>", // avoid markdown comment
);
log::error!("{err}");
return Err(err);
}
}
}
Ok(SymbolDocs::Plain { docs: "".into() })
}
/// Describes a primary function signature.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SignatureDocs {
/// Documentation for the function.
pub docs: EcoString,
// pub return_ty: Option<EcoString>,
// pub params: Vec<TidyParamDocs>,
/// The positional parameters.
pub pos: Vec<ParamDocs>,
/// The named parameters.
pub named: BTreeMap<String, ParamDocs>,
/// The rest parameter.
pub rest: Option<ParamDocs>,
/// The return type.
pub ret_ty: TypeRepr,
}
impl fmt::Display for SignatureDocs {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut is_first = true;
let mut write_sep = |f: &mut fmt::Formatter<'_>| {
if is_first {
is_first = false;
return Ok(());
}
f.write_str(", ")
};
for p in &self.pos {
write_sep(f)?;
f.write_str(&p.name)?;
if let Some(t) = &p.cano_type {
write!(f, ": {}", t.0)?;
}
}
if let Some(rest) = &self.rest {
write_sep(f)?;
f.write_str("..")?;
f.write_str(&rest.name)?;
if let Some(t) = &rest.cano_type {
write!(f, ": {}", t.0)?;
}
}
if !self.named.is_empty() {
let mut name_prints = vec![];
for v in self.named.values() {
let ty = v.cano_type.as_ref().map(|t| &t.0);
name_prints.push((v.name.clone(), ty, v.expr.clone()))
}
name_prints.sort();
for (k, t, v) in name_prints {
write_sep(f)?;
let v = v.as_deref().unwrap_or("any");
let mut v = v.trim();
if v.starts_with('{') && v.ends_with('}') && v.len() > 30 {
v = "{ ... }"
}
if v.starts_with('`') && v.ends_with('`') && v.len() > 30 {
v = "raw"
}
if v.starts_with('[') && v.ends_with(']') && v.len() > 30 {
v = "content"
}
f.write_str(&k)?;
if let Some(t) = t {
write!(f, ": {t}")?;
}
write!(f, " = {v}")?;
}
}
Ok(())
}
}
/// Describes a function parameter.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ParamDocs {
/// The parameter's name.
pub name: String,
/// Documentation for the parameter.
pub docs: String,
/// Inferred type of the parameter.
pub cano_type: TypeRepr,
/// The parameter's default name as value.
pub expr: Option<EcoString>,
/// 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 ParamDocs {
fn new(param: &ParamSpec, ty: Option<&Ty>, doc_ty: Option<&mut ShowTypeRepr>) -> Self {
Self {
name: param.name.as_ref().to_owned(),
docs: param.docs.as_ref().to_owned(),
cano_type: format_ty(ty.or(Some(&param.base_type)), doc_ty),
expr: param.expr.clone(),
positional: param.positional,
named: param.named,
variadic: param.variadic,
settable: param.settable,
}
}
}
type TypeInfo = (Arc<crate::analysis::DefUseInfo>, Arc<crate::ty::TypeScheme>);
fn format_ty(ty: Option<&Ty>, doc_ty: Option<&mut ShowTypeRepr>) -> TypeRepr {
match doc_ty {
Some(doc_ty) => doc_ty(ty),
None => ty
.and_then(|ty| ty.describe())
.map(|short| (short, format!("{ty:?}"))),
}
}
pub(crate) fn signature_docs(
ctx: &mut AnalysisContext,
type_info: Option<&TypeInfo>,
def_ident: Option<&IdentRef>,
runtime_fn: &Value,
mut doc_ty: Option<ShowTypeRepr>,
) -> Option<SignatureDocs> {
let func = match runtime_fn {
Value::Func(f) => f,
_ => return None,
};
// todo: documenting with bindings
use typst::foundations::func::Repr;
let mut func = func;
loop {
match func.inner() {
Repr::Element(..) | Repr::Native(..) => {
break;
}
Repr::With(w) => {
func = &w.0;
}
Repr::Closure(..) => {
break;
}
}
}
let sig = analyze_dyn_signature(ctx, func.clone());
let def_id = type_info.and_then(|(def_use, _)| {
let def_fid = func.span().id()?;
let (def_id, _) = def_use.get_def(def_fid, def_ident?)?;
Some(def_id)
});
let docstring = type_info.and_then(|(_, ty_chk)| ty_chk.var_docs.get(&def_id?));
let type_sig = type_info.and_then(|(_, ty_chk)| ty_chk.type_of_def(def_id?));
let type_sig = type_sig.and_then(|type_sig| type_sig.sig_repr(true));
let pos_in = sig
.primary()
.pos
.iter()
.enumerate()
.map(|(i, pos)| (pos, type_sig.as_ref().and_then(|sig| sig.pos(i))));
let named_in = sig
.primary()
.named
.iter()
.map(|x| (x, type_sig.as_ref().and_then(|sig| sig.named(x.0))));
let rest_in = sig
.primary()
.rest
.as_ref()
.map(|x| (x, type_sig.as_ref().and_then(|sig| sig.rest_param())));
let ret_in = type_sig
.as_ref()
.and_then(|sig| sig.body.as_ref())
.or_else(|| sig.primary().sig_ty.body.as_ref());
let pos = pos_in
.map(|(param, ty)| ParamDocs::new(param, ty, doc_ty.as_mut()))
.collect();
let named = named_in
.map(|((name, param), ty)| {
(
name.as_ref().to_owned(),
ParamDocs::new(param, ty, doc_ty.as_mut()),
)
})
.collect();
let rest = rest_in.map(|(param, ty)| ParamDocs::new(param, ty, doc_ty.as_mut()));
let ret_ty = format_ty(ret_in, doc_ty.as_mut());
Some(SignatureDocs {
docs: docstring.and_then(|x| x.docs.clone()).unwrap_or_default(),
pos,
named,
rest,
ret_ty,
})
}
// Unfortunately, we have only 65536 possible file ids and we cannot revoke
// them. So we share a global file id for all docs conversion.
static DOCS_CONVERT_ID: std::sync::LazyLock<Mutex<FileId>> = std::sync::LazyLock::new(|| {
Mutex::new(FileId::new(None, VirtualPath::new("__tinymist_docs__.typ")))
});
pub(crate) fn convert_docs(world: &LspWorld, content: &str) -> StrResult<EcoString> {
static DOCS_LIB: std::sync::LazyLock<Arc<typlite::scopes::Scopes<typlite::value::Value>>> =
std::sync::LazyLock::new(library::lib);
let conv_id = DOCS_CONVERT_ID.lock();
let entry = EntryState::new_rootless(conv_id.vpath().as_rooted_path().into()).unwrap();
let entry = entry.select_in_workspace(*conv_id);
let mut w = world.task(TaskInputs {
entry: Some(entry),
inputs: None,
});
w.map_shadow_by_id(*conv_id, Bytes::from(content.as_bytes().to_owned()))?;
// todo: bad performance
w.source_db.take_state();
let conv = typlite::Typlite::new(Arc::new(w))
.with_library(DOCS_LIB.clone())
.annotate_elements(true)
.convert()
.map_err(|e| eco_format!("failed to convert to markdown: {e}"))?;
Ok(conv)
}
pub(crate) fn identify_docs(kind: DocStringKind, docs: EcoString) -> StrResult<SymbolDocs> {
match kind {
DocStringKind::Function => Err(eco_format!("must be already handled")),
DocStringKind::Variable => identify_var_docs(docs).map(SymbolDocs::Variable),
DocStringKind::Constant => identify_var_docs(docs).map(SymbolDocs::Variable),
DocStringKind::Module => identify_tidy_module_docs(docs).map(SymbolDocs::Module),
DocStringKind::Struct => Ok(SymbolDocs::Plain { docs }),
DocStringKind::Reference => Ok(SymbolDocs::Plain { docs }),
}
}

View file

@ -13,20 +13,20 @@ pub struct TidyParamDocs {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TidyFuncDocs {
pub docs: String,
pub docs: EcoString,
pub return_ty: Option<EcoString>,
pub params: Vec<TidyParamDocs>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TidyVarDocs {
pub docs: String,
pub docs: EcoString,
pub return_ty: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TidyModuleDocs {
pub docs: String,
pub docs: EcoString,
}
pub fn identify_func_docs(converted: &str) -> StrResult<TidyFuncDocs> {
@ -129,8 +129,8 @@ pub fn identify_func_docs(converted: &str) -> StrResult<TidyFuncDocs> {
}
let docs = match break_line {
Some(line_no) => (lines[..line_no]).iter().copied().join("\n"),
None => converted.to_owned(),
Some(line_no) => (lines[..line_no]).iter().copied().join("\n").into(),
None => converted.into(),
};
params.reverse();
@ -141,7 +141,7 @@ pub fn identify_func_docs(converted: &str) -> StrResult<TidyFuncDocs> {
})
}
pub fn identify_var_docs(converted: &str) -> StrResult<TidyVarDocs> {
pub fn identify_var_docs(converted: EcoString) -> StrResult<TidyVarDocs> {
let lines = converted.lines().collect::<Vec<_>>();
let mut return_ty = None;
@ -170,17 +170,15 @@ pub fn identify_var_docs(converted: &str) -> StrResult<TidyVarDocs> {
}
let docs = match break_line {
Some(line_no) => (lines[..line_no]).iter().copied().join("\n"),
None => converted.to_owned(),
Some(line_no) => (lines[..line_no]).iter().copied().join("\n").into(),
None => converted,
};
Ok(TidyVarDocs { docs, return_ty })
}
pub fn identify_tidy_module_docs(converted: &str) -> StrResult<TidyModuleDocs> {
Ok(TidyModuleDocs {
docs: converted.to_owned(),
})
pub fn identify_tidy_module_docs(docs: EcoString) -> StrResult<TidyModuleDocs> {
Ok(TidyModuleDocs { docs })
}
fn match_brace(trim_start: &str) -> Option<(&str, &str)> {
@ -232,7 +230,7 @@ mod tests {
}
fn var(s: &str) -> String {
let f = super::identify_var_docs(s).unwrap();
let f = super::identify_var_docs(s.into()).unwrap();
let mut res = format!(">> docs:\n{}\n<< docs", f.docs);
if let Some(t) = f.return_ty {
res.push_str(&format!("\n>>return\n{t}\n<<return"));

View file

@ -21,7 +21,7 @@ use typst::{
use super::PackageId;
use crate::{
adt::{interner::impl_internable, snapshot_map},
analysis::BuiltinTy,
analysis::{BuiltinTy, DocString},
};
pub(crate) use super::{TyCtx, TyCtxMut};
@ -951,6 +951,8 @@ impl IfTy {
pub struct TypeScheme {
/// The typing on definitions
pub vars: HashMap<DefId, TypeVarBounds>,
/// The checked documentation of definitions
pub var_docs: HashMap<DefId, Arc<DocString>>,
/// The local binding of the type variable
pub local_binds: snapshot_map::SnapshotMap<DefId, Ty>,
/// The typing on syntax structures
@ -975,6 +977,7 @@ impl TypeScheme {
pub fn type_of_def(&self, def: DefId) -> Option<Ty> {
Some(self.simplify(self.vars.get(&def).map(|e| e.as_type())?, false))
}
/// Get the type of a syntax structure
pub fn type_of_span(&self, site: Span) -> Option<Ty> {
self.mapping