refactor: move expr and ty defs to analysis crate (#1633)

This commit is contained in:
Myriad-Dreamin 2025-04-08 05:50:55 +08:00 committed by GitHub
parent 72e33e461d
commit ac506dcc31
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
58 changed files with 968 additions and 890 deletions

View file

@ -1,303 +1,11 @@
use core::fmt;
use std::collections::BTreeMap;
use std::sync::OnceLock;
use ecow::{eco_format, EcoString};
use serde::{Deserialize, Serialize};
use tinymist_analysis::docs::{format_ty, ParamDocs, SignatureDocs, VarDocs};
use tinymist_analysis::ty::DocSource;
use tinymist_analysis::Signature;
use typst::syntax::Span;
use super::tidy::*;
use crate::analysis::{ParamAttrs, ParamTy, Signature};
use crate::prelude::*;
use crate::ty::Ty;
use crate::ty::{DocSource, Interned};
use crate::upstream::plain_docs_sentence;
type TypeRepr = Option<(
/* short */ EcoString,
/* long */ EcoString,
/* value */ EcoString,
)>;
/// Documentation about a definition (without type information).
pub type UntypedDefDocs = DefDocsT<()>;
/// Documentation about a definition.
pub type DefDocs = DefDocsT<TypeRepr>;
/// Documentation about a definition.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind")]
pub enum DefDocsT<T> {
/// Documentation about a function.
#[serde(rename = "func")]
Function(Box<SignatureDocsT<T>>),
/// Documentation about a variable.
#[serde(rename = "var")]
Variable(VarDocsT<T>),
/// 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<T> DefDocsT<T> {
/// Get the markdown representation of the documentation.
pub fn docs(&self) -> &EcoString {
match self {
Self::Function(docs) => &docs.docs,
Self::Variable(docs) => &docs.docs,
Self::Module(docs) => &docs.docs,
Self::Plain { docs } => docs,
}
}
}
impl DefDocs {
/// Get full documentation for the signature.
pub fn hover_docs(&self) -> EcoString {
match self {
DefDocs::Function(docs) => docs.hover_docs().clone(),
_ => plain_docs_sentence(self.docs()),
}
}
}
/// Describes a primary function signature.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SignatureDocsT<T> {
/// Documentation for the function.
pub docs: EcoString,
/// The positional parameters.
pub pos: Vec<ParamDocsT<T>>,
/// The named parameters.
pub named: BTreeMap<Interned<str>, ParamDocsT<T>>,
/// The rest parameter.
pub rest: Option<ParamDocsT<T>>,
/// The return type.
pub ret_ty: T,
/// The full documentation for the signature.
#[serde(skip)]
pub hover_docs: OnceLock<EcoString>,
}
impl SignatureDocsT<TypeRepr> {
/// Get full documentation for the signature.
pub fn hover_docs(&self) -> &EcoString {
self.hover_docs
.get_or_init(|| plain_docs_sentence(&format!("{}", SigHoverDocs(self))))
}
}
struct SigHoverDocs<'a>(&'a SignatureDocs);
impl fmt::Display for SigHoverDocs<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let docs = self.0;
let base_docs = docs.docs.trim();
if !base_docs.is_empty() {
f.write_str(base_docs)?;
}
fn write_param_docs(
f: &mut fmt::Formatter<'_>,
docs: &ParamDocsT<TypeRepr>,
kind: &str,
is_first: &mut bool,
) -> fmt::Result {
if *is_first {
*is_first = false;
write!(f, "\n\n## {}\n\n", docs.name)?;
} else {
write!(f, "\n\n## {} ({kind})\n\n", docs.name)?;
}
// p.cano_type.0
if let Some(t) = &docs.cano_type {
write!(f, "```typc\ntype: {}\n```\n\n", t.2)?;
}
f.write_str(docs.docs.trim())?;
Ok(())
}
if !docs.pos.is_empty() {
f.write_str("\n\n# Positional Parameters")?;
let mut is_first = true;
for pos_docs in &docs.pos {
write_param_docs(f, pos_docs, "positional", &mut is_first)?;
}
}
if docs.rest.is_some() {
f.write_str("\n\n# Rest Parameters")?;
let mut is_first = true;
if let Some(rest) = &docs.rest {
write_param_docs(f, rest, "spread right", &mut is_first)?;
}
}
if !docs.named.is_empty() {
f.write_str("\n\n# Named Parameters")?;
let mut is_first = true;
for named_docs in docs.named.values() {
write_param_docs(f, named_docs, "named", &mut is_first)?;
}
}
Ok(())
}
}
/// Documentation about a signature.
pub type UntypedSignatureDocs = SignatureDocsT<()>;
/// Documentation about a signature.
pub type SignatureDocs = SignatureDocsT<TypeRepr>;
impl SignatureDocs {
/// Get the markdown representation of the documentation.
pub fn print(&self, f: &mut impl std::fmt::Write) -> fmt::Result {
let mut is_first = true;
let mut write_sep = |f: &mut dyn std::fmt::Write| {
if is_first {
is_first = false;
return f.write_str("\n ");
}
f.write_str(",\n ")
};
f.write_char('(')?;
for pos_docs in &self.pos {
write_sep(f)?;
f.write_str(&pos_docs.name)?;
if let Some(t) = &pos_docs.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.default.clone()))
}
name_prints.sort();
for (name, ty, val) in name_prints {
write_sep(f)?;
let val = val.as_deref().unwrap_or("any");
let mut default = val.trim();
if default.starts_with('{') && default.ends_with('}') && default.len() > 30 {
default = "{ .. }"
}
if default.starts_with('`') && default.ends_with('`') && default.len() > 30 {
default = "raw"
}
if default.starts_with('[') && default.ends_with(']') && default.len() > 30 {
default = "content"
}
f.write_str(&name)?;
if let Some(ty) = ty {
write!(f, ": {ty}")?;
}
if default.contains('\n') {
write!(f, " = {}", default.replace("\n", "\n "))?;
} else {
write!(f, " = {default}")?;
}
}
}
if !is_first {
f.write_str(",\n")?;
}
f.write_char(')')?;
Ok(())
}
}
/// Documentation about a variable (without type information).
pub type UntypedVarDocs = VarDocsT<()>;
/// Documentation about a variable.
pub type VarDocs = VarDocsT<Option<(EcoString, EcoString, EcoString)>>;
/// Describes a primary pattern binding.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VarDocsT<T> {
/// Documentation for the pattern binding.
pub docs: EcoString,
/// The inferred type of the pattern binding source.
pub return_ty: T,
/// Cached documentation for the definition.
#[serde(skip)]
pub def_docs: OnceLock<String>,
}
impl VarDocs {
/// Get the markdown representation of the documentation.
pub fn def_docs(&self) -> &String {
self.def_docs
.get_or_init(|| plain_docs_sentence(&self.docs).into())
}
}
/// Documentation about a parameter (without type information).
pub type TypelessParamDocs = ParamDocsT<()>;
/// Documentation about a parameter.
pub type ParamDocs = ParamDocsT<TypeRepr>;
/// Describes a function parameter.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ParamDocsT<T> {
/// The parameter's name.
pub name: Interned<str>,
/// Documentation for the parameter.
pub docs: EcoString,
/// Inferred type of the parameter.
pub cano_type: T,
/// The parameter's default name as value.
pub default: Option<EcoString>,
/// The attribute of the parameter.
#[serde(flatten)]
pub attrs: ParamAttrs,
}
impl ParamDocs {
fn new(param: &ParamTy, ty: Option<&Ty>) -> Self {
Self {
name: param.name.as_ref().into(),
docs: param.docs.clone().unwrap_or_default(),
cano_type: format_ty(ty.or(Some(&param.ty))),
default: param.default.clone(),
attrs: param.attrs,
}
}
}
fn format_ty(ty: Option<&Ty>) -> TypeRepr {
let ty = ty?;
let short = ty.repr().unwrap_or_else(|| "any".into());
let long = eco_format!("{ty:?}");
let value = ty.value_repr().unwrap_or_else(|| "".into());
Some((short, long, value))
}
use crate::LocalContext;
pub(crate) fn var_docs(ctx: &mut LocalContext, pos: Span) -> Option<VarDocs> {
let source = ctx.source_by_id(pos.id()?).ok()?;

View file

@ -4,16 +4,15 @@ mod convert;
mod def;
mod module;
mod package;
mod tidy;
use tinymist_std::path::unix_slash;
use typst::syntax::FileId;
pub(crate) use convert::convert_docs;
pub use def::*;
pub(crate) use def::*;
pub use module::*;
pub use package::*;
pub(crate) use tidy::*;
pub use tinymist_analysis::docs::*;
fn file_id_repr(fid: FileId) -> String {
if let Some(spec) = fid.package() {

View file

@ -10,10 +10,10 @@ use typst::diag::StrResult;
use typst::syntax::package::PackageSpec;
use typst::syntax::FileId;
use crate::adt::interner::Interned;
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::DefDocs;

View file

@ -1,317 +0,0 @@
use ecow::EcoString;
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use typst::diag::StrResult;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TidyParamDocs {
pub name: EcoString,
pub docs: EcoString,
pub types: EcoString,
pub default: Option<EcoString>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TidyPatDocs {
pub docs: EcoString,
pub return_ty: Option<EcoString>,
pub params: Vec<TidyParamDocs>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TidyModuleDocs {
pub docs: EcoString,
}
pub fn identify_pat_docs(converted: &str) -> StrResult<TidyPatDocs> {
let lines = converted.lines().collect::<Vec<_>>();
let mut matching_return_ty = true;
let mut buf = vec![];
let mut params = vec![];
let mut return_ty = None;
let mut break_line = None;
let mut line_width = lines.len();
'search: loop {
if line_width == 0 {
break;
}
line_width -= 1;
let line = lines[line_width];
if line.is_empty() {
continue;
}
loop {
if matching_return_ty {
matching_return_ty = false;
let Some(w) = line.trim_start().strip_prefix("->") else {
// break_line = Some(i);
continue;
};
break_line = Some(line_width);
return_ty = Some(w.trim().into());
break;
}
let Some(mut line) = line
.trim_end()
.strip_suffix("<!-- typlite:end:list-item 0 -->")
else {
break_line = Some(line_width + 1);
break 'search;
};
let mut current_line_no = line_width;
loop {
// <!-- typlite:begin:list-item -->
let t = line
.trim_start()
.strip_prefix("- ")
.and_then(|t| t.trim().strip_prefix("<!-- typlite:begin:list-item 0 -->"));
let line_content = match t {
Some(t) => {
buf.push(t);
break;
}
None => line,
};
buf.push(line_content);
if current_line_no == 0 {
break_line = Some(line_width + 1);
break 'search;
}
current_line_no -= 1;
line = lines[current_line_no];
}
let mut buf = std::mem::take(&mut buf);
buf.reverse();
let Some(first_line) = buf.first_mut() else {
break_line = Some(line_width + 1);
break 'search;
};
*first_line = first_line.trim();
let Some(param_line) = None.or_else(|| {
let (param_name, rest) = first_line.split_once(" ")?;
let (type_content, rest) = match_brace(rest.trim_start().strip_prefix("(")?)?;
let (_, rest) = rest.split_once(":")?;
*first_line = rest.trim();
Some((param_name.into(), type_content.into()))
}) else {
break_line = Some(line_width + 1);
break 'search;
};
line_width = current_line_no;
params.push(TidyParamDocs {
name: param_line.0,
types: param_line.1,
default: None,
docs: buf.into_iter().join("\n").into(),
});
break;
}
}
let docs = match break_line {
Some(line_no) => (lines[..line_no]).iter().copied().join("\n").into(),
None => converted.into(),
};
params.reverse();
Ok(TidyPatDocs {
docs,
return_ty,
params,
})
}
pub fn identify_tidy_module_docs(docs: EcoString) -> StrResult<TidyModuleDocs> {
Ok(TidyModuleDocs { docs })
}
fn match_brace(trim_start: &str) -> Option<(&str, &str)> {
let mut brace_count = 1;
let mut end = 0;
for (idx, ch) in trim_start.char_indices() {
match ch {
'(' => brace_count += 1,
')' => brace_count -= 1,
_ => {}
}
if brace_count == 0 {
end = idx;
break;
}
}
if brace_count != 0 {
return None;
}
let (type_content, rest) = trim_start.split_at(end);
Some((type_content, rest))
}
#[cfg(test)]
mod tests {
use std::fmt::Write;
use super::TidyParamDocs;
fn func(s: &str) -> String {
let docs = super::identify_pat_docs(s).unwrap();
let mut res = format!(">> docs:\n{}\n<< docs", docs.docs);
if let Some(t) = docs.return_ty {
res.push_str(&format!("\n>>return\n{t}\n<<return"));
}
for TidyParamDocs {
name,
types,
docs,
default: _,
} in docs.params
{
let _ = write!(res, "\n>>arg {name}: {types}\n{docs}\n<< arg");
}
res
}
fn var(s: &str) -> String {
let docs = super::identify_pat_docs(s).unwrap();
let mut res = format!(">> docs:\n{}\n<< docs", docs.docs);
if let Some(t) = docs.return_ty {
res.push_str(&format!("\n>>return\n{t}\n<<return"));
}
res
}
#[test]
fn test_identify_tidy_docs() {
insta::assert_snapshot!(func(r###"These again are dictionaries with the keys
- <!-- typlite:begin:list-item 0 -->`description` (optional): The description for the argument.<!-- typlite:end:list-item 0 -->
- <!-- typlite:begin:list-item 0 -->`types` (optional): A list of accepted argument types.<!-- typlite:end:list-item 0 -->
- <!-- typlite:begin:list-item 0 -->`default` (optional): Default value for this argument.<!-- typlite:end:list-item 0 -->
See @@show-module() for outputting the results of this function.
- <!-- typlite:begin:list-item 0 -->content (string): Content of `.typ` file to analyze for docstrings.<!-- typlite:end:list-item 0 -->
- <!-- typlite:begin:list-item 0 -->name (string): The name for the module.<!-- typlite:end:list-item 0 -->
- <!-- typlite:begin:list-item 0 -->label-prefix (auto, string): The label-prefix for internal function
references. If `auto`, the label-prefix name will be the module name.<!-- typlite:end:list-item 0 -->
- <!-- typlite:begin:list-item 0 -->require-all-parameters (boolean): Require that all parameters of a
functions are documented and fail if some are not.<!-- typlite:end:list-item 0 -->
- <!-- typlite:begin:list-item 0 -->scope (dictionary): A dictionary of definitions that are then available
in all function and parameter descriptions.<!-- typlite:end:list-item 0 -->
- <!-- typlite:begin:list-item 0 -->preamble (string): Code to prepend to all code snippets shown with `#example()`.
This can for instance be used to import something from the scope.<!-- typlite:end:list-item 0 -->
-> string"###), @r"
>> docs:
These again are dictionaries with the keys
- <!-- typlite:begin:list-item 0 -->`description` (optional): The description for the argument.<!-- typlite:end:list-item 0 -->
- <!-- typlite:begin:list-item 0 -->`types` (optional): A list of accepted argument types.<!-- typlite:end:list-item 0 -->
- <!-- typlite:begin:list-item 0 -->`default` (optional): Default value for this argument.<!-- typlite:end:list-item 0 -->
See @@show-module() for outputting the results of this function.
<< docs
>>return
string
<<return
>>arg content: string
Content of `.typ` file to analyze for docstrings.
<< arg
>>arg name: string
The name for the module.
<< arg
>>arg label-prefix: auto, string
The label-prefix for internal function
references. If `auto`, the label-prefix name will be the module name.
<< arg
>>arg require-all-parameters: boolean
Require that all parameters of a
functions are documented and fail if some are not.
<< arg
>>arg scope: dictionary
A dictionary of definitions that are then available
in all function and parameter descriptions.
<< arg
>>arg preamble: string
Code to prepend to all code snippets shown with `#example()`.
This can for instance be used to import something from the scope.
<< arg
");
}
#[test]
fn test_identify_tidy_docs_nested() {
insta::assert_snapshot!(func(r###"These again are dictionaries with the keys
- <!-- typlite:begin:list-item 0 -->`description` (optional): The description for the argument.<!-- typlite:end:list-item 0 -->
See @@show-module() for outputting the results of this function.
- <!-- typlite:begin:list-item 0 -->name (string): The name for the module.<!-- typlite:end:list-item 0 -->
- <!-- typlite:begin:list-item 0 -->label-prefix (auto, string): The label-prefix for internal function
references. If `auto`, the label-prefix name will be the module name.
- <!-- typlite:begin:list-item 1 -->nested something<!-- typlite:end:list-item 1 -->
- <!-- typlite:begin:list-item 1 -->nested something 2<!-- typlite:end:list-item 1 --><!-- typlite:end:list-item 0 -->
-> string"###), @r"
>> docs:
These again are dictionaries with the keys
- <!-- typlite:begin:list-item 0 -->`description` (optional): The description for the argument.<!-- typlite:end:list-item 0 -->
See @@show-module() for outputting the results of this function.
<< docs
>>return
string
<<return
>>arg name: string
The name for the module.
<< arg
>>arg label-prefix: auto, string
The label-prefix for internal function
references. If `auto`, the label-prefix name will be the module name.
- <!-- typlite:begin:list-item 1 -->nested something<!-- typlite:end:list-item 1 -->
- <!-- typlite:begin:list-item 1 -->nested something 2<!-- typlite:end:list-item 1 -->
<< arg
");
}
#[test]
fn test_identify_tidy_docs3() {
insta::assert_snapshot!(var(r###"See @@show-module() for outputting the results of this function.
-> string"###), @r"
>> docs:
See @@show-module() for outputting the results of this function.
<< docs
>>return
string
<<return
");
}
#[test]
fn test_identify_tidy_docs4() {
insta::assert_snapshot!(var(r###"
- <!-- typlite:begin:list-item 0 -->name (string): The name for the module.<!-- typlite:end:list-item 0 -->
-> string"###), @r"
>> docs:
- <!-- typlite:begin:list-item 0 -->name (string): The name for the module.<!-- typlite:end:list-item 0 -->
<< docs
>>return
string
<<return
");
}
}