From 89f44f220de2972452dd816fe59836ba76953d59 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Tue, 14 Mar 2023 22:35:31 +0100 Subject: Bibliography and citations --- src/eval/library.rs | 9 ++++++++- src/ide/analyze.rs | 37 +++++++++++++++++++++++++++++++++++++ src/ide/complete.rs | 32 ++++++++++++++++++++++++++++++-- src/ide/mod.rs | 11 +++++++---- src/ide/tooltip.rs | 43 +++++++++++++++++++++++++++++++++---------- src/model/typeset.rs | 21 +++++++++++---------- 6 files changed, 126 insertions(+), 27 deletions(-) (limited to 'src') diff --git a/src/eval/library.rs b/src/eval/library.rs index d3f7547d..1240d9bb 100644 --- a/src/eval/library.rs +++ b/src/eval/library.rs @@ -2,6 +2,7 @@ use std::fmt::{self, Debug, Formatter}; use std::hash::{Hash, Hasher}; use std::num::NonZeroUsize; +use comemo::Tracked; use ecow::EcoString; use once_cell::sync::OnceCell; @@ -9,8 +10,9 @@ use super::Module; use crate::diag::SourceResult; use crate::doc::Document; use crate::geom::{Abs, Dir}; -use crate::model::{Content, Label, NodeId, StyleChain, StyleMap, Vt}; +use crate::model::{Content, Introspector, Label, NodeId, StyleChain, StyleMap, Vt}; use crate::util::hash128; +use crate::World; /// Definition of Typst's standard library. #[derive(Debug, Clone, Hash)] @@ -61,6 +63,11 @@ pub struct LangItems { pub link: fn(url: EcoString) -> Content, /// A reference: `@target`, `@target[..]`. pub reference: fn(target: Label, supplement: Option) -> Content, + /// The keys contained in the bibliography and short descriptions of them. + pub bibliography_keys: fn( + world: Tracked, + introspector: Tracked, + ) -> Vec<(EcoString, Option)>, /// A section heading: `= Introduction`. pub heading: fn(level: NonZeroUsize, body: Content) -> Content, /// An item in a bullet list: `- ...`. diff --git a/src/ide/analyze.rs b/src/ide/analyze.rs index 3c46cca1..7338ba57 100644 --- a/src/ide/analyze.rs +++ b/src/ide/analyze.rs @@ -1,8 +1,11 @@ use std::path::PathBuf; use comemo::Track; +use ecow::EcoString; +use crate::doc::Frame; use crate::eval::{eval, Module, Route, Tracer, Value}; +use crate::model::{Introspector, Label}; use crate::syntax::{ast, LinkedNode, Source, SyntaxKind}; use crate::util::PathExt; use crate::World; @@ -64,3 +67,37 @@ pub fn analyze_import( let source = world.source(id); eval(world.track(), route.track(), tracer.track_mut(), source).ok() } + +/// Find all labels and details for them. +pub fn analyze_labels( + world: &(dyn World + 'static), + frames: &[Frame], +) -> (Vec<(Label, Option)>, usize) { + let mut output = vec![]; + let mut introspector = Introspector::new(); + let items = &world.library().items; + introspector.update(frames); + + // Labels in the document. + for node in introspector.iter() { + let Some(label) = node.label() else { continue }; + let details = node + .field("caption") + .or_else(|| node.field("body")) + .and_then(|field| match field { + Value::Content(content) => Some(content), + _ => None, + }) + .and_then(|content| (items.text_str)(content)); + output.push((label.clone(), details)); + } + + let split = output.len(); + + // Bibliography keys. + for (key, detail) in (items.bibliography_keys)(world.track(), introspector.track()) { + output.push((Label(key), detail)); + } + + (output, split) +} diff --git a/src/ide/complete.rs b/src/ide/complete.rs index de6f2b73..66590160 100644 --- a/src/ide/complete.rs +++ b/src/ide/complete.rs @@ -4,7 +4,9 @@ use ecow::{eco_format, EcoString}; use if_chain::if_chain; use unscanny::Scanner; +use super::analyze::analyze_labels; use super::{analyze_expr, analyze_import, plain_docs_sentence, summarize_font_family}; +use crate::doc::Frame; use crate::eval::{methods_on, CastInfo, Library, Scope, Value}; use crate::syntax::{ ast, is_id_continue, is_id_start, is_ident, LinkedNode, Source, SyntaxKind, @@ -21,11 +23,12 @@ use crate::World; /// control and space or something similar. pub fn autocomplete( world: &(dyn World + 'static), + frames: &[Frame], source: &Source, cursor: usize, explicit: bool, ) -> Option<(usize, Vec)> { - let mut ctx = CompletionContext::new(world, source, cursor, explicit)?; + let mut ctx = CompletionContext::new(world, frames, source, cursor, explicit)?; let _ = complete_comments(&mut ctx) || complete_field_accesses(&mut ctx) @@ -78,7 +81,10 @@ fn complete_comments(ctx: &mut CompletionContext) -> bool { /// Complete in markup mode. fn complete_markup(ctx: &mut CompletionContext) -> bool { // Bail if we aren't even in markup. - if !matches!(ctx.leaf.parent_kind(), None | Some(SyntaxKind::Markup)) { + if !matches!( + ctx.leaf.parent_kind(), + None | Some(SyntaxKind::Markup) | Some(SyntaxKind::Ref) + ) { return false; } @@ -96,6 +102,13 @@ fn complete_markup(ctx: &mut CompletionContext) -> bool { return true; } + // Start of an reference: "@|" or "@he|". + if ctx.leaf.kind() == SyntaxKind::RefMarker { + ctx.from = ctx.leaf.offset() + 1; + ctx.label_completions(); + return true; + } + // Behind a half-completed binding: "#let x = |". if_chain! { if let Some(prev) = ctx.leaf.prev_leaf(); @@ -850,6 +863,7 @@ fn code_completions(ctx: &mut CompletionContext, hashtag: bool) { /// Context for autocompletion. struct CompletionContext<'a> { world: &'a (dyn World + 'static), + frames: &'a [Frame], library: &'a Library, source: &'a Source, global: &'a Scope, @@ -869,6 +883,7 @@ impl<'a> CompletionContext<'a> { /// Create a new autocompletion context. fn new( world: &'a (dyn World + 'static), + frames: &'a [Frame], source: &'a Source, cursor: usize, explicit: bool, @@ -878,6 +893,7 @@ impl<'a> CompletionContext<'a> { let leaf = LinkedNode::new(source.root()).leaf_at(cursor)?; Some(Self { world, + frames, library, source, global: &library.global.scope(), @@ -955,6 +971,18 @@ impl<'a> CompletionContext<'a> { } } + /// Add completions for all labels. + fn label_completions(&mut self) { + for (label, detail) in analyze_labels(self.world, self.frames).0 { + self.completions.push(Completion { + kind: CompletionKind::Constant, + label: label.0, + apply: None, + detail, + }); + } + } + /// Add a completion for a specific value. fn value_completion( &mut self, diff --git a/src/ide/mod.rs b/src/ide/mod.rs index bee959cd..38bede0b 100644 --- a/src/ide/mod.rs +++ b/src/ide/mod.rs @@ -6,6 +6,7 @@ mod highlight; mod jump; mod tooltip; +pub use self::analyze::analyze_labels; pub use self::complete::*; pub use self::highlight::*; pub use self::jump::*; @@ -13,15 +14,17 @@ pub use self::tooltip::*; use std::fmt::Write; +use ecow::{eco_format, EcoString}; + use self::analyze::*; use crate::font::{FontInfo, FontStyle}; /// Extract the first sentence of plain text of a piece of documentation. /// /// Removes Markdown formatting. -fn plain_docs_sentence(docs: &str) -> String { +fn plain_docs_sentence(docs: &str) -> EcoString { let mut s = unscanny::Scanner::new(docs); - let mut output = String::new(); + let mut output = EcoString::new(); let mut link = false; while let Some(c) = s.eat() { match c { @@ -62,7 +65,7 @@ fn plain_docs_sentence(docs: &str) -> String { } /// Create a short description of a font family. -fn summarize_font_family<'a>(variants: impl Iterator) -> String { +fn summarize_font_family<'a>(variants: impl Iterator) -> EcoString { let mut infos: Vec<_> = variants.collect(); infos.sort_by_key(|info| info.variant); @@ -78,7 +81,7 @@ fn summarize_font_family<'a>(variants: impl Iterator) -> St let count = infos.len(); let s = if count == 1 { "" } else { "s" }; - let mut detail = format!("{count} variant{s}."); + let mut detail = eco_format!("{count} variant{s}."); if min_weight == max_weight { write!(detail, " Weight {min_weight}.").unwrap(); diff --git a/src/ide/tooltip.rs b/src/ide/tooltip.rs index a32dfb0b..0b37b7ca 100644 --- a/src/ide/tooltip.rs +++ b/src/ide/tooltip.rs @@ -1,20 +1,22 @@ use std::fmt::Write; -use ecow::EcoString; +use ecow::{eco_format, EcoString}; use if_chain::if_chain; +use super::analyze::analyze_labels; use super::{analyze_expr, plain_docs_sentence, summarize_font_family}; +use crate::doc::Frame; use crate::eval::{CastInfo, Tracer, Value}; use crate::geom::{round_2, Length, Numeric}; -use crate::syntax::ast; -use crate::syntax::{LinkedNode, Source, SyntaxKind}; +use crate::syntax::{ast, LinkedNode, Source, SyntaxKind}; use crate::util::pretty_comma_list; use crate::World; /// Describe the item under the cursor. pub fn tooltip( world: &(dyn World + 'static), + frames: &[Frame], source: &Source, cursor: usize, ) -> Option { @@ -22,6 +24,7 @@ pub fn tooltip( named_param_tooltip(world, &leaf) .or_else(|| font_tooltip(world, &leaf)) + .or_else(|| ref_tooltip(world, frames, &leaf)) .or_else(|| expr_tooltip(world, &leaf)) } @@ -29,9 +32,9 @@ pub fn tooltip( #[derive(Debug, Clone)] pub enum Tooltip { /// A string of text. - Text(String), + Text(EcoString), /// A string of Typst code. - Code(String), + Code(EcoString), } /// Tooltip for a hovered expression. @@ -55,7 +58,7 @@ fn expr_tooltip(world: &(dyn World + 'static), leaf: &LinkedNode) -> Option Option Option { +fn length_tooltip(length: Length) -> Option { length.em.is_zero().then(|| { - format!( + Tooltip::Code(eco_format!( "{}pt = {}mm = {}cm = {}in", round_2(length.abs.to_pt()), round_2(length.abs.to_mm()), round_2(length.abs.to_cm()), round_2(length.abs.to_inches()) - ) + )) }) } +/// Tooltip for a hovered reference. +fn ref_tooltip( + world: &(dyn World + 'static), + frames: &[Frame], + leaf: &LinkedNode, +) -> Option { + if leaf.kind() != SyntaxKind::RefMarker { + return None; + } + + let target = leaf.text().trim_start_matches('@'); + for (label, detail) in analyze_labels(world, frames).0 { + if label.0 == target { + return Some(Tooltip::Text(detail?.into())); + } + } + + None +} + /// Tooltips for components of a named parameter. fn named_param_tooltip( world: &(dyn World + 'static), diff --git a/src/model/typeset.rs b/src/model/typeset.rs index 8719ea0c..fe433288 100644 --- a/src/model/typeset.rs +++ b/src/model/typeset.rs @@ -35,7 +35,7 @@ pub fn typeset(world: Tracked, content: &Content) -> SourceResult= 5 || introspector.update(&document) { + if iter >= 5 || introspector.update(&document.pages) { break; } } @@ -49,13 +49,10 @@ pub fn typeset(world: Tracked, content: &Content) -> SourceResult { /// The compilation environment. - #[doc(hidden)] pub world: Tracked<'a, dyn World>, /// Provides stable identities to nodes. - #[doc(hidden)] pub provider: TrackedMut<'a, StabilityProvider>, /// Provides access to information about the document. - #[doc(hidden)] pub introspector: Tracked<'a, Introspector>, } @@ -127,7 +124,6 @@ impl StabilityProvider { } /// Provides access to information about the document. -#[doc(hidden)] pub struct Introspector { init: bool, nodes: Vec<(StableId, Content)>, @@ -136,7 +132,7 @@ pub struct Introspector { impl Introspector { /// Create a new introspector. - fn new() -> Self { + pub fn new() -> Self { Self { init: false, nodes: vec![], @@ -146,10 +142,10 @@ impl Introspector { /// Update the information given new frames and return whether we can stop /// layouting. - fn update(&mut self, document: &Document) -> bool { + pub fn update(&mut self, frames: &[Frame]) -> bool { self.nodes.clear(); - for (i, frame) in document.pages.iter().enumerate() { + for (i, frame) in frames.iter().enumerate() { let page = NonZeroUsize::new(1 + i).unwrap(); self.extract(frame, page, Transform::identity()); } @@ -171,6 +167,11 @@ impl Introspector { true } + /// Iterate over all nodes. + pub fn iter(&self) -> impl Iterator { + self.nodes.iter().map(|(_, node)| node) + } + /// Extract metadata from a frame. fn extract(&mut self, frame: &Frame, page: NonZeroUsize, ts: Transform) { for (pos, element) in frame.elements() { @@ -199,12 +200,12 @@ impl Introspector { #[comemo::track] impl Introspector { /// Whether this introspector is not yet initialized. - fn init(&self) -> bool { + pub fn init(&self) -> bool { self.init } /// Locate all metadata matches for the given selector. - fn locate(&self, selector: Selector) -> Vec<(StableId, &Content)> { + pub fn locate(&self, selector: Selector) -> Vec<(StableId, &Content)> { let nodes = self.locate_impl(&selector); let mut queries = self.queries.borrow_mut(); if !queries.iter().any(|(prev, _)| prev == &selector) { -- cgit v1.2.3