summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/eval/library.rs9
-rw-r--r--src/ide/analyze.rs37
-rw-r--r--src/ide/complete.rs32
-rw-r--r--src/ide/mod.rs11
-rw-r--r--src/ide/tooltip.rs43
-rw-r--r--src/model/typeset.rs21
6 files changed, 126 insertions, 27 deletions
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>) -> Content,
+ /// The keys contained in the bibliography and short descriptions of them.
+ pub bibliography_keys: fn(
+ world: Tracked<dyn World>,
+ introspector: Tracked<Introspector>,
+ ) -> Vec<(EcoString, Option<EcoString>)>,
/// 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<EcoString>)>, 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<Completion>)> {
- 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<Item = &'a FontInfo>) -> String {
+fn summarize_font_family<'a>(variants: impl Iterator<Item = &'a FontInfo>) -> 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<Item = &'a FontInfo>) -> 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<Tooltip> {
@@ -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<Tool
if let &Value::Length(length) = value {
if let Some(tooltip) = length_tooltip(length) {
- return Some(Tooltip::Code(tooltip));
+ return Some(tooltip);
}
}
}
@@ -85,22 +88,42 @@ fn expr_tooltip(world: &(dyn World + 'static), leaf: &LinkedNode) -> Option<Tool
}
let tooltip = pretty_comma_list(&pieces, false);
- (!tooltip.is_empty()).then(|| Tooltip::Code(tooltip))
+ (!tooltip.is_empty()).then(|| Tooltip::Code(tooltip.into()))
}
/// Tooltip text for a hovered length.
-fn length_tooltip(length: Length) -> Option<String> {
+fn length_tooltip(length: Length) -> Option<Tooltip> {
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<Tooltip> {
+ 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<dyn World>, content: &Content) -> SourceResult<Doc
document = (library.items.layout)(&mut vt, content, styles)?;
iter += 1;
- if iter >= 5 || introspector.update(&document) {
+ if iter >= 5 || introspector.update(&document.pages) {
break;
}
}
@@ -49,13 +49,10 @@ pub fn typeset(world: Tracked<dyn World>, content: &Content) -> SourceResult<Doc
/// [Vm](crate::eval::Vm) for typesetting.
pub struct Vt<'a> {
/// 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<Item = &Content> {
+ 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) {