feat: pretty errors in docstrings (#1876)
Some checks are pending
tinymist::ci / E2E Tests (linux-x64 on ubuntu-latest) (push) Blocked by required conditions
tinymist::ci / Duplicate Actions Detection (push) Waiting to run
tinymist::ci / Check Clippy, Formatting, Completion, Documentation, and Tests (Linux) (push) Waiting to run
tinymist::ci / Check Minimum Rust version and Tests (Windows) (push) Waiting to run
tinymist::ci / E2E Tests (darwin-arm64 on macos-latest) (push) Blocked by required conditions
tinymist::ci / E2E Tests (linux-x64 on ubuntu-22.04) (push) Blocked by required conditions
tinymist::ci / E2E Tests (win32-x64 on windows-2022) (push) Blocked by required conditions
tinymist::ci / E2E Tests (win32-x64 on windows-latest) (push) Blocked by required conditions
tinymist::ci / prepare-build (push) Waiting to run
tinymist::ci / build-binary (push) Blocked by required conditions
tinymist::ci / build-vsc-assets (push) Blocked by required conditions
tinymist::ci / build-vscode (push) Blocked by required conditions
tinymist::ci / build-vscode-others (push) Blocked by required conditions
tinymist::ci / publish-vscode (push) Blocked by required conditions
tinymist::gh_pages / build-gh-pages (push) Waiting to run

* feat: print doc errors

* fix: test errors on windows

* fix: tests on windows again

* fix: tests on windows again 2

* Revert "fix: tests on windows again 2"

This reverts commit 63973dcc1f.

* fix: tests on windows again 3
This commit is contained in:
Myriad-Dreamin 2025-07-06 19:40:02 +08:00 committed by GitHub
parent 9787432029
commit 4426b31ed7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 488 additions and 132 deletions

View file

@ -3,9 +3,12 @@ use std::sync::Arc;
use ecow::{eco_format, EcoString};
use tinymist_std::path::unix_slash;
use tinymist_world::system::print_diagnostics_to_string;
use tinymist_world::vfs::WorkspaceResolver;
use tinymist_world::{EntryReader, EntryState, ShadowApi, TaskInputs};
use typlite::TypliteFeat;
use tinymist_world::{
DiagnosticFormat, EntryReader, EntryState, ShadowApi, SourceWorld, TaskInputs,
};
use typlite::{Format, TypliteFeat};
use typst::diag::StrResult;
use typst::foundations::Bytes;
use typst::syntax::FileId;
@ -19,7 +22,7 @@ pub(crate) fn convert_docs(
source_fid: Option<FileId>,
) -> StrResult<EcoString> {
let mut entry = ctx.world.entry_state();
let (contextual_content, import_context) = if let Some(fid) = source_fid {
let import_context = source_fid.map(|fid| {
let root = ctx
.world
.vfs()
@ -42,16 +45,15 @@ pub(crate) fn convert_docs(
"#import {:?}: *",
unix_slash(fid.vpath().as_rooted_path())
));
let imports = imports.join("\n");
let content_with_import: String = if !imports.is_empty() {
format!("{imports}\n\n{content}")
} else {
content.to_owned()
};
(content_with_import, Some(imports))
} else {
(content.to_owned(), None)
imports.join("; ")
});
let feat = TypliteFeat {
color_theme: Some(ctx.analysis.color_theme),
annotate_elem: true,
soft_error: true,
remove_html: ctx.analysis.remove_html,
import_context,
..Default::default()
};
let entry = entry.select_in_workspace(Path::new("__tinymist_docs__.typ"));
@ -61,21 +63,39 @@ pub(crate) fn convert_docs(
inputs: None,
});
w.map_shadow_by_id(w.main(), Bytes::from_string(contextual_content))?;
// todo: bad performance: content.to_owned()
w.map_shadow_by_id(w.main(), Bytes::from_string(content.to_owned()))?;
// todo: bad performance
w.take_db();
let w = feat
.prepare_world(&w, Format::Md)
.map_err(|e| eco_format!("failed to prepare world: {e}"))?;
let conv = typlite::Typlite::new(Arc::new(w))
.with_feature(TypliteFeat {
color_theme: Some(ctx.analysis.color_theme),
annotate_elem: true,
soft_error: true,
remove_html: ctx.analysis.remove_html,
import_context,
..Default::default()
})
.convert()
.map_err(|err| eco_format!("failed to convert to markdown: {err}"))?;
let w = Arc::new(w);
let res = typlite::Typlite::new(w.clone())
.with_feature(feat)
.convert();
let conv = print_diag_or_error(w.as_ref(), res)?;
Ok(conv.replace("```example", "```typ"))
}
fn print_diag_or_error<T>(
world: &impl SourceWorld,
result: tinymist_std::Result<T>,
) -> StrResult<T> {
match result {
Ok(v) => Ok(v),
Err(err) => {
if let Some(diagnostics) = err.diagnostics() {
return Err(print_diagnostics_to_string(
world,
diagnostics.iter(),
DiagnosticFormat::Human,
)?);
}
Err(eco_format!("failed to convert docs: {err}"))
}
}
}

View file

@ -0,0 +1,6 @@
/// *
#let my-fun(mode: "typ", setting: it => it, note) = {
touying-fn-wrapper(utils.my-fun, mode: mode, setting: setting, note)
}
#(/* ident after */ my-fun);

View file

@ -0,0 +1,9 @@
/// Good doc
#let my-fun(mode: "typ", setting: it => it, note) = {
touying-fn-wrapper(utils.my-fun, mode: mode, setting: setting, note)
}
#(/* ident after */ my-fun);
#my-f

View file

@ -0,0 +1,14 @@
/// Good doc
///
/// #example(```
/// my-fun()
/// ```)
///
#let my-fun(mode: "typ", setting: it => it, note) = {
touying-fn-wrapper(utils.my-fun, mode: mode, setting: setting, note)
}
#(/* ident after */ my-fun);
#my-f

View file

@ -0,0 +1,58 @@
---
source: crates/tinymist-query/src/hover.rs
expression: content
input_file: crates/tinymist-query/src/fixtures/hover/error_in_doc.typ
---
Range: 5:20:5:26
```typc
let my-fun(
note: any,
mode: str = "typ",
setting: (any) => any = Closure(..),
) = none;
```
======
```
failed to parse docs: error: unclosed delimiter
┌─ /dummy-root/__wrap_md_main.typ:2:0
2 │ *
│ ^
```
```typ
*
```
# Positional Parameters
## note
```typc
type:
```
# Named Parameters
## mode
```typc
type: "typ"
```
## setting (named)
```typc
type: (any) => any
```

View file

@ -0,0 +1,46 @@
---
source: crates/tinymist-query/src/hover.rs
expression: content
input_file: crates/tinymist-query/src/fixtures/hover/error_in_module.typ
---
Range: 5:20:5:26
```typc
let my-fun(
note: any,
mode: str = "typ",
setting: (any) => any = Closure(..),
) = none;
```
======
Good doc
# Positional Parameters
## note
```typc
type:
```
# Named Parameters
## mode
```typc
type: "typ"
```
## setting (named)
```typc
type: (any) => any
```

View file

@ -0,0 +1,64 @@
---
source: crates/tinymist-query/src/hover.rs
expression: content
input_file: crates/tinymist-query/src/fixtures/hover/error_in_module_example.typ
---
Range: 10:20:10:26
```typc
let my-fun(
note: any,
mode: str = "typ",
setting: (any) => any = Closure(..),
) = none;
```
======
Good doc
```typ
my-fun()
```
Error compiling idoc: error: unknown variable: my-f
┌─ /dummy-root/s0.typ:14:1
14 │ #my-f
│ ^^^^
= hint: if you meant to use subtraction, try adding spaces around the minus sign: \`my - f\`
help: error occurred while importing this module
┌─ /dummy-root/\_\_main\_\_.typ:1:8
1 │ #import "/s0.typ": \*
│ ^^^^^^^^^
# Positional Parameters
## note
```typc
type:
```
# Named Parameters
## mode
```typc
type: "typ"
```
## setting (named)
```typc
type: (any) => any
```

View file

@ -1,7 +1,7 @@
---
source: crates/tinymist-query/src/hover.rs
expression: content
input_file: crates/tinymist-query/src/fixtures/hover/annotate_docs_error.typ
input_file: crates/tinymist-query/src/fixtures/hover/example.typ
---
Range: 12:20:12:32

View file

@ -1,2 +1,6 @@
#let tmpl2(stroke) = text(stroke: stroke)
#tmpl2(/* position */)
/// *
#let my-fun(mode: "typ", setting: it => it, note) = {
touying-fn-wrapper(utils.my-fun, mode: mode, setting: setting, note)
}
#(/* ident after */ my-fun);

View file

@ -0,0 +1,58 @@
---
source: crates/tinymist-query/src/hover.rs
expression: content
input_file: crates/tinymist-query/src/fixtures/playground/base.typ
---
Range: 5:20:5:26
```typc
let my-fun(
note: any,
mode: str = "typ",
setting: (any) => any = Closure(..),
) = none;
```
======
```
failed to parse docs: error: unclosed delimiter
┌─ /dummy-root/__wrap_md_main.typ:2:0
2 │ *
│ ^
```
```typ
*
```
# Positional Parameters
## note
```typc
type:
```
# Named Parameters
## mode
```typc
type: "typ"
```
## setting (named)
```typc
type: (any) => any
```

View file

@ -8,6 +8,7 @@ use std::{
path::{Path, PathBuf},
};
use regex::{Regex, Replacer};
use serde_json::{ser::PrettyFormatter, Serializer, Value};
use tinymist_project::{LspCompileSnapshot, LspComputeGraph};
use tinymist_std::path::unix_slash;
@ -333,13 +334,14 @@ impl JsonRepr {
}
pub fn md_content(v: &str) -> Cow<'_, str> {
static REG: LazyLock<regex::Regex> =
LazyLock::new(|| regex::Regex::new(r#"data:image/svg\+xml;base64,([^"]+)"#).unwrap());
static REG: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r#"data:image/svg\+xml;base64,([^"]+)"#).unwrap());
static REG2: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r#"C:\\?\\dummy-root\\?\\"#).unwrap());
let v = REG.replace_all(v, |_captures: &regex::Captures| {
"data:image-hash/svg+xml;base64,redacted"
});
v
REG2.replace_all_cow(v, "/dummy-root/")
}
pub fn range(v: impl serde::Serialize) -> String {
@ -360,8 +362,7 @@ impl fmt::Display for JsonRepr {
let res = String::from_utf8(ser.into_inner().into_inner().unwrap()).unwrap();
// replace Span(number) to Span(..)
static REG: LazyLock<regex::Regex> =
LazyLock::new(|| regex::Regex::new(r#"Span\((\d+)\)"#).unwrap());
static REG: LazyLock<Regex> = LazyLock::new(|| Regex::new(r#"Span\((\d+)\)"#).unwrap());
let res = REG.replace_all(&res, "Span(..)");
f.write_str(&res)
}
@ -470,3 +471,18 @@ impl fmt::Display for HashRepr<JsonRepr> {
write!(f, "sha256:{}", hex::encode(hash))
}
}
/// Extension methods for `Regex` that operate on `Cow<str>` instead of `&str`.
pub trait RegexCowExt {
/// [`Regex::replace_all`], but taking text as `Cow<str>` instead of `&str`.
fn replace_all_cow<'t, R: Replacer>(&self, text: Cow<'t, str>, rep: R) -> Cow<'t, str>;
}
impl RegexCowExt for Regex {
fn replace_all_cow<'t, R: Replacer>(&self, text: Cow<'t, str>, rep: R) -> Cow<'t, str> {
match self.replace_all(&text, rep) {
Cow::Owned(result) => Cow::Owned(result),
Cow::Borrowed(_) => text,
}
}
}

View file

@ -1,17 +1,17 @@
use std::io::IsTerminal;
use ecow::EcoString;
use std::io::IsTerminal;
use std::str::FromStr;
use codespan_reporting::term::termcolor::{ColorChoice, NoColor, StandardStream, WriteColor};
use codespan_reporting::{
diagnostic::{Diagnostic, Label},
term::{
self,
termcolor::{ColorChoice, StandardStream},
},
term,
};
use tinymist_std::Result;
use tinymist_vfs::FileId;
use typst::diag::{eco_format, Severity, SourceDiagnostic};
use typst::diag::{eco_format, Severity, SourceDiagnostic, StrResult};
use typst::syntax::Span;
use typst::WorldExt;
use crate::{CodeSpanReportWorld, DiagnosticFormat, SourceWorld};
@ -30,13 +30,41 @@ pub fn print_diagnostics<'d, 'files>(
errors: impl Iterator<Item = &'d SourceDiagnostic>,
diagnostic_format: DiagnosticFormat,
) -> Result<(), codespan_reporting::files::Error> {
let world = CodeSpanReportWorld::new(world);
let mut w = match diagnostic_format {
DiagnosticFormat::Human => color_stream(),
DiagnosticFormat::Short => StandardStream::stderr(ColorChoice::Never),
};
print_diagnostics_to(world, errors, &mut w, diagnostic_format)
}
/// Print diagnostic messages to the terminal.
pub fn print_diagnostics_to_string<'d, 'files>(
world: &'files dyn SourceWorld,
errors: impl Iterator<Item = &'d SourceDiagnostic>,
diagnostic_format: DiagnosticFormat,
) -> StrResult<EcoString> {
let mut w = NoColor::new(vec![]);
print_diagnostics_to(world, errors, &mut w, diagnostic_format)
.map_err(|e| eco_format!("failed to print diagnostics to string: {e}"))?;
let output = EcoString::from_str(
std::str::from_utf8(&w.into_inner())
.map_err(|e| eco_format!("failed to convert diagnostics to string: {e}"))?,
)
.unwrap_or_default();
Ok(output)
}
/// Print diagnostic messages to the terminal.
pub fn print_diagnostics_to<'d, 'files>(
world: &'files dyn SourceWorld,
errors: impl Iterator<Item = &'d SourceDiagnostic>,
w: &mut impl WriteColor,
diagnostic_format: DiagnosticFormat,
) -> Result<(), codespan_reporting::files::Error> {
let world = CodeSpanReportWorld::new(world);
let mut config = term::Config {
tab_width: 2,
..Default::default()
@ -60,7 +88,7 @@ pub fn print_diagnostics<'d, 'files>(
)
.with_labels(label(world.world, diagnostic.span).into_iter().collect());
term::emit(&mut w, &config, &world, &diag)?;
term::emit(w, &config, &world, &diag)?;
// Stacktrace-like helper diagnostics.
for point in &diagnostic.trace {
@ -69,7 +97,7 @@ pub fn print_diagnostics<'d, 'files>(
.with_message(message)
.with_labels(label(world.world, point.span).into_iter().collect());
term::emit(&mut w, &config, &world, &help)?;
term::emit(w, &config, &world, &help)?;
}
}
@ -78,5 +106,5 @@ pub fn print_diagnostics<'d, 'files>(
/// Create a label for a span.
fn label(world: &dyn SourceWorld, span: Span) -> Option<Label<FileId>> {
Some(Label::primary(span.id()?, world.range(span)?))
Some(Label::primary(span.id()?, world.source_range(span)?))
}

View file

@ -17,7 +17,7 @@ use typst::{
syntax::{Source, Span, VirtualPath},
text::{Font, FontBook},
utils::LazyHash,
Features, Library, World,
Features, Library, World, WorldExt,
};
use crate::{
@ -858,6 +858,10 @@ pub trait SourceWorld: World {
self.source(id)
.expect("file id does not point to any source file")
}
fn source_range(&self, span: Span) -> Option<std::ops::Range<usize>> {
self.range(span)
}
}
impl<F: CompilerFeat> SourceWorld for CompilerWorld<F> {

View file

@ -178,6 +178,93 @@ pub struct TypliteFeat {
pub processor: Option<String>,
}
impl TypliteFeat {
pub fn prepare_world(
&self,
world: &LspWorld,
format: Format,
) -> tinymist_std::Result<LspWorld> {
let entry = world.entry_state();
let main = entry.main();
let current = main.context("no main file in workspace")?;
if WorkspaceResolver::is_package_file(current) {
bail!("package file is not supported");
}
let wrap_main_id = current.join("__wrap_md_main.typ");
let (main_id, main_content) = match self.processor.as_ref() {
None => (wrap_main_id, None),
Some(processor) => {
let main_id = current.join("__md_main.typ");
let content = format!(
r#"#import {processor:?}: article
#article(include "__wrap_md_main.typ")"#
);
(main_id, Some(Bytes::from_string(content)))
}
};
let mut dict = TypstDict::new();
dict.insert("x-target".into(), Str("md".into()));
if format == Format::Text || self.remove_html {
dict.insert("x-remove-html".into(), Str("true".into()));
}
let task_inputs = TaskInputs {
entry: Some(entry.select_in_workspace(main_id.vpath().as_rooted_path())),
inputs: Some(Arc::new(LazyHash::new(dict))),
};
let mut world = world.task(task_inputs).html_task().into_owned();
let markdown_id = FileId::new(
Some(
typst_syntax::package::PackageSpec::from_str("@local/_markdown:0.1.0")
.context_ut("failed to import markdown package")?,
),
VirtualPath::new("lib.typ"),
);
world
.map_shadow_by_id(
markdown_id.join("typst.toml"),
Bytes::from_string(include_str!("markdown-typst.toml")),
)
.context_ut("cannot map markdown-typst.toml")?;
world
.map_shadow_by_id(
markdown_id,
Bytes::from_string(include_str!("markdown.typ")),
)
.context_ut("cannot map markdown.typ")?;
world
.map_shadow_by_id(
wrap_main_id,
Bytes::from_string(format!(
r#"#import "@local/_markdown:0.1.0": md-doc, example; #show: md-doc
{}"#,
world
.source(current)
.context_ut("failed to get main file content")?
.text()
)),
)
.context_ut("cannot map source for main file")?;
if let Some(main_content) = main_content {
world
.map_shadow_by_id(main_id, main_content)
.context_ut("cannot map source for main file")?;
}
Ok(world)
}
}
/// Task builder for converting a typst document to Markdown.
pub struct Typlite {
/// The universe to use for the conversion.
@ -231,91 +318,22 @@ impl Typlite {
/// Convert the content to a markdown document.
pub fn convert_doc(self, format: Format) -> tinymist_std::Result<MarkdownDocument> {
let entry = self.world.entry_state();
let main = entry.main();
let current = main.context("no main file in workspace")?;
let world_origin = self.world.clone();
let world = self.world;
if WorkspaceResolver::is_package_file(current) {
bail!("package file is not supported");
}
let wrap_main_id = current.join("__wrap_md_main.typ");
let (main_id, main_content) = match self.feat.processor.as_ref() {
None => (wrap_main_id, None),
Some(processor) => {
let main_id = current.join("__md_main.typ");
let content = format!(
r#"#import {processor:?}: article
#article(include "__wrap_md_main.typ")"#
);
(main_id, Some(Bytes::from_string(content)))
}
};
let mut dict = TypstDict::new();
dict.insert("x-target".into(), Str("md".into()));
if format == Format::Text || self.feat.remove_html {
dict.insert("x-remove-html".into(), Str("true".into()));
}
let task_inputs = TaskInputs {
entry: Some(entry.select_in_workspace(main_id.vpath().as_rooted_path())),
inputs: Some(Arc::new(LazyHash::new(dict))),
};
let mut world = world.task(task_inputs).html_task().into_owned();
let markdown_id = FileId::new(
Some(
typst_syntax::package::PackageSpec::from_str("@local/markdown:0.1.0")
.context_ut("failed to import markdown package")?,
),
VirtualPath::new("lib.typ"),
);
world
.map_shadow_by_id(
markdown_id.join("typst.toml"),
Bytes::from_string(include_str!("markdown-typst.toml")),
)
.context_ut("cannot map markdown-typst.toml")?;
world
.map_shadow_by_id(
markdown_id,
Bytes::from_string(include_str!("markdown.typ")),
)
.context_ut("cannot map markdown.typ")?;
world
.map_shadow_by_id(
wrap_main_id,
Bytes::from_string(format!(
r#"#import "@local/markdown:0.1.0": md-doc, example
#show: md-doc
{}"#,
world
.source(current)
.context_ut("failed to get main file content")?
.text()
)),
)
.context_ut("cannot map source for main file")?;
if let Some(main_content) = main_content {
world
.map_shadow_by_id(main_id, main_content)
.context_ut("cannot map source for main file")?;
}
let world = Arc::new(self.feat.prepare_world(&self.world, format)?);
let feat = self.feat.clone();
Self::convert_doc_prepared(feat, format, world)
}
/// Convert the content to a markdown document.
pub fn convert_doc_prepared(
feat: TypliteFeat,
format: Format,
world: Arc<LspWorld>,
) -> tinymist_std::Result<MarkdownDocument> {
// todo: ignoring warnings
let base = typst::compile(&world).output?;
let mut feat = self.feat;
let mut feat = feat;
feat.target = format;
Ok(MarkdownDocument::new(base, world_origin, feat))
Ok(MarkdownDocument::new(base, world.clone(), feat))
}
}

View file

@ -1,5 +1,5 @@
[package]
name = "markdown"
name = "_markdown"
version = "0.1.0"
entrypoint = "lib.typ"
description = "Markdown support for typst."

View file

@ -7,6 +7,7 @@ use std::sync::{Arc, LazyLock};
use base64::Engine;
use cmark_writer::ast::{HtmlAttribute, HtmlElement as CmarkHtmlElement, Node};
use ecow::{eco_format, EcoString};
use tinymist_project::system::print_diagnostics_to_string;
use tinymist_project::{base::ShadowApi, EntryReader, TaskInputs, MEMORY_MAIN_ENTRY};
use typst::{
foundations::{Bytes, Dict, IntoValue},
@ -277,12 +278,22 @@ impl HtmlToAstParser {
)
.unwrap();
//todo: ignoring warnings
let doc = typst::compile(&world);
let doc = match doc.output {
Ok(doc) => doc,
Err(e) => {
let diag = doc.warnings.iter().chain(e.iter());
let e = print_diagnostics_to_string(
&world,
diag,
tinymist_project::DiagnosticFormat::Human,
)
.unwrap_or_else(|e| e);
if self.feat.soft_error {
return Node::Text(eco_format!("Error compiling idoc: {e:?}"));
return Node::Text(eco_format!("Error compiling idoc: {e}"));
} else {
// Construct error node
return Node::HtmlElement(CmarkHtmlElement {
@ -291,7 +302,7 @@ impl HtmlToAstParser {
name: EcoString::inline("class"),
value: EcoString::inline("error"),
}],
children: vec![Node::Text(eco_format!("Error compiling idoc: {e:?}"))],
children: vec![Node::Text(eco_format!("Error compiling idoc: {e}"))],
self_closing: false,
});
}

View file

@ -256,7 +256,7 @@ impl DocxWriter {
}
// Other inline element types
_ => {
println!("other inline element: {:?}", node);
eprintln!("other inline element: {:?}", node);
}
}