feat: show rendered bibliography in bib hover and improve label hover (#1611)

* feat: show rendered bibliography in citation details

* adjust code and revert changes to completions

* refactor: refactor a bit

* refactor: refactor two bit

* feat: improve hover docs a bit

* test: add hover tests for label and ref

* test: add html tests

---------

Co-authored-by: Myriad-Dreamin <camiyoru@gmail.com>
This commit is contained in:
QuadnucYard 2025-04-11 17:29:03 +08:00 committed by GitHub
parent 4265bc25bc
commit a7a22c0d70
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 312 additions and 50 deletions

View file

@ -21,6 +21,7 @@ dashmap.workspace = true
dirs.workspace = true
ena.workspace = true
ecow.workspace = true
hayagriva.workspace = true
if_chain.workspace = true
itertools.workspace = true
indexmap.workspace = true
@ -55,7 +56,6 @@ unscanny.workspace = true
walkdir.workspace = true
yaml-rust2.workspace = true
[dev-dependencies]
insta.workspace = true
serde.workspace = true

View file

@ -1,29 +1,38 @@
use typst::foundations::Bytes;
use indexmap::IndexMap;
use typst::{foundations::Bytes, model::CslStyle};
use yaml_rust2::{parser::Event, parser::MarkedEventReceiver, scanner::Marker};
use super::prelude::*;
pub(crate) fn bib_info(files: EcoVec<(TypstFileId, Bytes)>) -> Option<Arc<BibInfo>> {
pub(crate) fn bib_info(
csl_style: CslStyle,
files: impl Iterator<Item = (TypstFileId, Bytes)>,
) -> Option<Arc<BibInfo>> {
let mut worker = BibWorker {
info: BibInfo::default(),
info: BibInfo {
csl_style,
entries: IndexMap::new(),
},
};
// We might have multiple bib/yaml files
for (file_id, content) in files.clone() {
for (file_id, content) in files {
worker.analyze_path(file_id, content);
}
let info = Arc::new(worker.info);
crate::log_debug_ct!("bib analysis: {files:?} -> {info:?}");
crate::log_debug_ct!("bib analysis: {info:?}");
Some(info)
}
/// The bibliography information.
#[derive(Debug, Default)]
#[derive(Debug)]
pub struct BibInfo {
/// The using CSL style.
pub csl_style: CslStyle,
/// The bibliography entries.
pub entries: indexmap::IndexMap<String, BibEntry>,
pub entries: IndexMap<String, BibEntry>,
}
#[derive(Debug, Clone)]
@ -31,6 +40,7 @@ pub struct BibEntry {
pub file_id: TypstFileId,
pub name_range: Range<usize>,
pub range: Range<usize>,
pub raw_entry: Option<hayagriva::Entry>,
}
struct BibWorker {
@ -42,15 +52,28 @@ impl BibWorker {
let file_extension = file_id.vpath().as_rooted_path().extension()?.to_str()?;
let content = std::str::from_utf8(&content).ok()?;
match file_extension.to_lowercase().as_str() {
"yml" | "yaml" => self.yaml_bib(file_id, content),
// Reparse the content to get all entries
let bib = match file_extension.to_lowercase().as_str() {
"yml" | "yaml" => {
self.yaml_bib(file_id, content);
hayagriva::io::from_yaml_str(content).ok()?
}
"bib" => {
let bibliography = biblatex::RawBibliography::parse(content).ok()?;
self.tex_bib(file_id, bibliography)
self.tex_bib(file_id, bibliography);
hayagriva::io::from_biblatex_str(content).ok()?
}
_ => return None,
};
for entry in bib {
if let Some(stored_entry) = self.info.entries.get_mut(entry.key()) {
stored_entry.raw_entry = Some(entry);
}
}
Some(())
}
@ -66,6 +89,7 @@ impl BibWorker {
file_id,
name_range: name.span,
range: entry.span,
raw_entry: None,
};
self.info.entries.insert(name.v.to_owned(), entry);
}
@ -172,6 +196,7 @@ impl YamlBib {
file_id,
name_range,
range,
raw_entry: None,
};
Some((name.value, entry))
};
@ -209,8 +234,8 @@ Euclid2:
FileId::new_fake(VirtualPath::new(Path::new("test.yml"))),
);
assert_eq!(bib.entries.len(), 2);
insta::assert_snapshot!(bib_snap(&bib.entries[0]), @r###"("Euclid", BibEntry { file_id: /test.yml, name_range: 1..7, range: 1..63 })"###);
insta::assert_snapshot!(bib_snap(&bib.entries[1]), @r###"("Euclid2", BibEntry { file_id: /test.yml, name_range: 63..70, range: 63..126 })"###);
insta::assert_snapshot!(bib_snap(&bib.entries[0]), @r###"("Euclid", BibEntry { file_id: /test.yml, name_range: 1..7, range: 1..63, raw_entry: None })"###);
insta::assert_snapshot!(bib_snap(&bib.entries[1]), @r###"("Euclid2", BibEntry { file_id: /test.yml, name_range: 63..70, range: 63..126, raw_entry: None })"###);
}
#[test]

View file

@ -1,9 +1,8 @@
//! Linked definition analysis
use tinymist_std::typst::TypstDocument;
use typst::foundations::{IntoValue, Label, Selector, Type};
use typst::foundations::{Label, Selector, Type};
use typst::introspection::Introspector;
use typst::model::BibliographyElem;
use super::{prelude::*, InsTy, SharedContext};
use crate::syntax::{Decl, DeclExpr, Expr, ExprInfo, SyntaxClass, VarClass};
@ -169,13 +168,7 @@ fn bib_definition(
introspector: &Introspector,
key: &str,
) -> Option<Definition> {
let bib_elem = BibliographyElem::find(introspector.track()).ok()?;
let Value::Array(paths) = bib_elem.sources.clone().into_value() else {
return None;
};
let bib_paths = paths.into_iter().flat_map(|path| path.cast().ok());
let bib_info = ctx.analyze_bib(bib_elem.span(), bib_paths)?;
let bib_info = ctx.analyze_bib(introspector)?;
let entry = bib_info.entries.get(key)?;
crate::log_debug_ct!("find_bib_definition: {key} => {entry:?}");

View file

@ -15,11 +15,13 @@ use tinymist_project::{LspComputeGraph, LspWorld};
use tinymist_std::hash::{hash128, FxDashMap};
use tinymist_std::typst::TypstDocument;
use tinymist_world::debug_loc::DataSource;
use tinymist_world::vfs::{FileId, PathResolution, WorkspaceResolver};
use tinymist_world::vfs::{PathResolution, WorkspaceResolver};
use tinymist_world::{EntryReader, DETACHED_ENTRY};
use typst::diag::{eco_format, At, FileError, FileResult, SourceResult, StrResult};
use typst::foundations::{Bytes, Module, Styles};
use typst::foundations::{Bytes, IntoValue, Module, StyleChain, Styles};
use typst::introspection::Introspector;
use typst::layout::Position;
use typst::model::BibliographyElem;
use typst::syntax::package::{PackageManifest, PackageSpec};
use typst::syntax::{Span, VirtualPath};
use typst_shim::eval::{eval_compat, Eval};
@ -884,17 +886,11 @@ impl SharedContext {
}
/// Get bib info of a source file.
pub fn analyze_bib(
&self,
span: Span,
bib_paths: impl Iterator<Item = EcoString>,
) -> Option<Arc<BibInfo>> {
use comemo::Track;
let w = &self.world;
let w = (w as &dyn World).track();
pub fn analyze_bib(&self, introspector: &Introspector) -> Option<Arc<BibInfo>> {
let world = &self.world;
let world = (world as &dyn World).track();
let fid = span.id()?;
analyze_bib(w, bib_paths.collect(), fid)
analyze_bib(world, introspector.track())
}
/// Describe the item under the cursor.
@ -1278,17 +1274,27 @@ fn ceil_char_boundary(text: &str, mut cursor: usize) -> usize {
#[comemo::memoize]
fn analyze_bib(
world: Tracked<dyn World + '_>,
bib_paths: EcoVec<EcoString>,
elem_fid: FileId,
introspector: Tracked<Introspector>,
) -> Option<Arc<BibInfo>> {
let files = bib_paths
.iter()
.flat_map(|bib_path| {
let bib_fid = resolve_id_by_path(world.deref(), elem_fid, bib_path)?;
let bib_elem = BibliographyElem::find(introspector).ok()?;
// todo: it doesn't respect the style chain which can be get from
// `analyze_expr`
let csl_style = bib_elem.style(StyleChain::default()).derived;
let Value::Array(paths) = bib_elem.sources.clone().into_value() else {
return None;
};
let elem_fid = bib_elem.span().id()?;
let files = paths
.into_iter()
.flat_map(|path| path.cast().ok())
.flat_map(|bib_path: EcoString| {
let bib_fid = resolve_id_by_path(world.deref(), elem_fid, &bib_path)?;
Some((bib_fid, world.file(bib_fid).ok()?))
})
.collect::<EcoVec<_>>();
bib_info(files)
});
bib_info(csl_style, files)
}
#[comemo::memoize]

View file

@ -4,7 +4,6 @@ pub use std::ops::Range;
pub use std::path::Path;
pub use std::sync::{Arc, LazyLock};
pub use comemo::Track;
pub use ecow::*;
pub use typst::foundations::{Func, Value};
pub use typst::syntax::ast::{self, AstNode};

View file

@ -0,0 +1,56 @@
use hayagriva::{
BibliographyDriver, BibliographyRequest, BufWriteFormat, CitationItem, CitationRequest,
ElemChildren,
};
use crate::analysis::BibInfo;
pub(crate) struct RenderedBibCitation {
pub citation: String,
pub bib_item: String,
}
/// Render the citation string in the bib with given CSL style.
pub(crate) fn render_citation_string(
bib_info: &BibInfo,
key: &str,
support_html: bool,
) -> Option<RenderedBibCitation> {
let entry = bib_info.entries.get(key)?;
let raw_entry = entry.raw_entry.as_ref()?;
let mut driver = BibliographyDriver::new();
let locales = &[];
driver.citation(CitationRequest::from_items(
vec![CitationItem::with_entry(raw_entry)],
bib_info.csl_style.get(),
locales,
));
let result = driver.finish(BibliographyRequest {
style: bib_info.csl_style.get(),
locale: None, // todo: get locale from CiteElem
locale_files: locales,
});
let rendered_bib = result.bibliography?;
let format_elem = |elem: &ElemChildren| {
let mut buf = String::new();
elem.write_buf(
&mut buf,
if support_html {
BufWriteFormat::Html
} else {
BufWriteFormat::Plain
},
)
.ok()?;
Some(buf)
};
Some(RenderedBibCitation {
citation: format_elem(&result.citations.first()?.citation)?,
bib_item: format_elem(&rendered_bib.items.first()?.content)?,
})
}

View file

@ -0,0 +1,15 @@
/// path: references.bib
@article{Russell:1908,
Author = {Bertand Russell},
Journal = {American Journal of Mathematics},
Pages = {222--262},
Title = {Mathematical logic based on the theory of types},
Volume = 30,
Year = 1908}
-----
/// compile: true
#ref(/* position after */ <Russell:1908>)
#bibliography("references.bib")

View file

@ -0,0 +1,16 @@
/// path: references.bib
@article{Russell:1908,
Author = {Bertand Russell},
Journal = {American Journal of Mathematics},
Pages = {222--262},
Title = {Mathematical logic based on the theory of types},
Volume = 30,
Year = 1908}
-----
/// compile: true
/// html: false
#ref(/* position after */ <Russell:1908>)
#bibliography("references.bib")

View file

@ -0,0 +1,4 @@
/// compile: true
#let test1(body) = figure(body)
#test1([Test1]) /* position after */ <fig:test1>

View file

@ -0,0 +1,5 @@
/// compile: true
#set heading(numbering: "1.1")
= H /* position after */ <head>
@head

View file

@ -0,0 +1,15 @@
/// path: references.bib
@article{Russell:1908,
Author = {Bertand Russell},
Journal = {American Journal of Mathematics},
Pages = {222--262},
Title = {Mathematical logic based on the theory of types},
Volume = 30,
Year = 1908}
-----
/// compile: true
/* position after */ @Russell:1908
#bibliography("references.bib")

View file

@ -0,0 +1,16 @@
/// path: references.bib
@article{Russell:1908,
Author = {Bertand Russell},
Journal = {American Journal of Mathematics},
Pages = {222--262},
Title = {Mathematical logic based on the theory of types},
Volume = 30,
Year = 1908}
-----
/// compile: true
/// html: false
/* position after */ @Russell:1908
#bibliography("references.bib")

View file

@ -0,0 +1,5 @@
/// compile: true
#let test1(body) = figure(body)
#test1([Test1]) <fig:test1>
/* position after */ @fig:test1

View file

@ -0,0 +1,5 @@
/// compile: true
#set heading(numbering: "1.1")
= H /* position after */ <head>
@head

View file

@ -0,0 +1,9 @@
---
source: crates/tinymist-query/src/hover.rs
expression: "JsonRepr::new_redacted(result, &REDACT_LOC)"
input_file: crates/tinymist-query/src/fixtures/hover/label_bib.typ
---
{
"contents": "Bibliography: `Russell:1908` [1]\n\n---\n\nB. Russell, Mathematical logic based on the theory of types, <span style=\"font-style: italic;\">American Journal of Mathematics</span>, 30, 222262, 1908.",
"range": "2:26:2:40"
}

View file

@ -0,0 +1,9 @@
---
source: crates/tinymist-query/src/hover.rs
expression: "JsonRepr::new_redacted(result, &REDACT_LOC)"
input_file: crates/tinymist-query/src/fixtures/hover/label_bib_no_html.typ
---
{
"contents": "Bibliography: `Russell:1908` [1]\n\n---\n\nB. Russell, Mathematical logic based on the theory of types, American Journal of Mathematics, 30, 222262, 1908.",
"range": "3:26:3:40"
}

View file

@ -0,0 +1,9 @@
---
source: crates/tinymist-query/src/hover.rs
expression: "JsonRepr::new_redacted(result, &REDACT_LOC)"
input_file: crates/tinymist-query/src/fixtures/hover/label_figure.typ
---
{
"contents": "Label: `fig:test1`\n",
"range": "3:37:3:48"
}

View file

@ -0,0 +1,9 @@
---
source: crates/tinymist-query/src/hover.rs
expression: "JsonRepr::new_redacted(result, &REDACT_LOC)"
input_file: crates/tinymist-query/src/fixtures/hover/label_heading.typ
---
{
"contents": "Label: `head`\n",
"range": "3:25:3:31"
}

View file

@ -0,0 +1,9 @@
---
source: crates/tinymist-query/src/hover.rs
expression: "JsonRepr::new_redacted(result, &REDACT_LOC)"
input_file: crates/tinymist-query/src/fixtures/hover/ref_bib.typ
---
{
"contents": "Bibliography: `Russell:1908` [1]\n\n---\n\nB. Russell, Mathematical logic based on the theory of types, <span style=\"font-style: italic;\">American Journal of Mathematics</span>, 30, 222262, 1908.",
"range": "2:21:2:34"
}

View file

@ -0,0 +1,9 @@
---
source: crates/tinymist-query/src/hover.rs
expression: "JsonRepr::new_redacted(result, &REDACT_LOC)"
input_file: crates/tinymist-query/src/fixtures/hover/ref_bib_no_html.typ
---
{
"contents": "Bibliography: `Russell:1908` [1]\n\n---\n\nB. Russell, Mathematical logic based on the theory of types, American Journal of Mathematics, 30, 222262, 1908.",
"range": "3:21:3:34"
}

View file

@ -0,0 +1,9 @@
---
source: crates/tinymist-query/src/hover.rs
expression: "JsonRepr::new_redacted(result, &REDACT_LOC)"
input_file: crates/tinymist-query/src/fixtures/hover/ref_figure.typ
---
{
"contents": "Ref: `fig:test1`\n\n\n---\n\n```typc\nfigure(\n body: [Test1],\n placement: none,\n scope: \"column\",\n caption: none,\n kind: image,\n supplement: [Figure],\n numbering: \"1\",\n gap: 0.65em,\n outlined: true,\n counter: counter(figure.where(kind: image)),\n)\n```",
"range": "4:21:4:31"
}

View file

@ -0,0 +1,9 @@
---
source: crates/tinymist-query/src/hover.rs
expression: "JsonRepr::new_redacted(result, &REDACT_LOC)"
input_file: crates/tinymist-query/src/fixtures/hover/ref_heading.typ
---
{
"contents": "Label: `head`\n",
"range": "3:25:3:31"
}

View file

@ -5,6 +5,7 @@ use typst::foundations::repr::separated_list;
use typst_shim::syntax::LinkedNodeExt;
use crate::analysis::get_link_exprs_in;
use crate::bib::{render_citation_string, RenderedBibCitation};
use crate::jump_from_cursor;
use crate::prelude::*;
use crate::upstream::{route_of_value, truncated_repr, Tooltip};
@ -125,14 +126,26 @@ impl HoverWorker<'_> {
use Decl::*;
match def.decl.as_ref() {
Label(..) => {
self.def.push(format!("Label: {}\n", def.name()));
// todo: type repr
if let Some(val) = def.term.as_ref().and_then(|v| v.value()) {
self.def.push(truncated_repr(&val).into());
self.def.push(format!("Ref: `{}`\n", def.name()));
self.def
.push(format!("```typc\n{}\n```", truncated_repr(&val)));
} else {
self.def.push(format!("Label: `{}`\n", def.name()));
}
}
BibEntry(..) => {
self.def.push(format!("Bibliography: @{}", def.name()));
if let Some(details) = try_get_bib_details(&self.doc, self.ctx, def.name()) {
self.def.push(format!(
"Bibliography: `{}` {}",
def.name(),
details.citation
));
self.def.push(details.bib_item);
} else {
// fallback: no additional information
self.def.push(format!("Bibliography: `{}`", def.name()));
}
}
_ => {
let sym_docs = self.ctx.def_docs(&def);
@ -283,6 +296,17 @@ impl HoverWorker<'_> {
}
}
fn try_get_bib_details(
doc: &Option<TypstDocument>,
ctx: &LocalContext,
name: &str,
) -> Option<RenderedBibCitation> {
let doc = doc.as_ref()?;
let support_html = !ctx.shared.analysis.remove_html;
let bib_info = ctx.analyze_bib(doc.introspector())?;
render_citation_string(&bib_info, name, support_html)
}
fn push_result_ty(
name: &str,
ty_repr: Option<&(EcoString, EcoString, EcoString)>,
@ -396,13 +420,16 @@ mod tests {
snapshot_testing("hover", &|ctx, path| {
let source = ctx.source_by_path(&path).unwrap();
let docs = find_module_level_docs(&source).unwrap_or_default();
let properties = get_test_properties(&docs);
let graph = compile_doc_for_test(ctx, &properties);
let request = HoverRequest {
path: path.clone(),
position: find_test_position(&source),
};
let snap = WorldComputeGraph::from_world(ctx.world.clone());
let result = request.request(ctx, snap);
let result = request.request(ctx, graph);
assert_snapshot!(JsonRepr::new_redacted(result, &REDACT_LOC));
});
}

View file

@ -56,6 +56,7 @@ mod adt;
mod lsp_typst_boundary;
mod prelude;
mod bib;
mod check;
mod code_action;
mod code_context;