//! Capabilities for Typst IDE support. mod analyze; mod complete; mod definition; mod jump; mod matchers; mod tooltip; pub use self::analyze::{analyze_expr, analyze_import, analyze_labels}; pub use self::complete::{autocomplete, Completion, CompletionKind}; pub use self::definition::{definition, Definition, DefinitionKind}; pub use self::jump::{jump_from_click, jump_from_cursor, Jump}; pub use self::matchers::{deref_target, named_items, DerefTarget, NamedItem}; pub use self::tooltip::{tooltip, Tooltip}; use std::fmt::Write; use ecow::{eco_format, EcoString}; use typst::text::{FontInfo, FontStyle}; /// Extract the first sentence of plain text of a piece of documentation. /// /// Removes Markdown formatting. fn plain_docs_sentence(docs: &str) -> EcoString { let mut s = unscanny::Scanner::new(docs); let mut output = EcoString::new(); let mut link = false; while let Some(c) = s.eat() { match c { '`' => { let mut raw = s.eat_until('`'); if (raw.starts_with('{') && raw.ends_with('}')) || (raw.starts_with('[') && raw.ends_with(']')) { raw = &raw[1..raw.len() - 1]; } s.eat(); output.push('`'); output.push_str(raw); output.push('`'); } '[' => link = true, ']' if link => { if s.eat_if('(') { s.eat_until(')'); s.eat(); } else if s.eat_if('[') { s.eat_until(']'); s.eat(); } link = false } '*' | '_' => {} '.' => { output.push('.'); break; } _ => output.push(c), } } output } /// Create a short description of a font family. fn summarize_font_family<'a>(variants: impl Iterator) -> EcoString { let mut infos: Vec<_> = variants.collect(); infos.sort_by_key(|info| info.variant); let mut has_italic = false; let mut min_weight = u16::MAX; let mut max_weight = 0; for info in &infos { let weight = info.variant.weight.to_number(); has_italic |= info.variant.style == FontStyle::Italic; min_weight = min_weight.min(weight); max_weight = min_weight.max(weight); } let count = infos.len(); let mut detail = eco_format!("{count} variant{}.", if count == 1 { "" } else { "s" }); if min_weight == max_weight { write!(detail, " Weight {min_weight}.").unwrap(); } else { write!(detail, " Weights {min_weight}–{max_weight}.").unwrap(); } if has_italic { detail.push_str(" Has italics."); } detail } #[cfg(test)] mod tests { use typst::diag::{FileError, FileResult}; use typst::foundations::{Bytes, Datetime, Smart}; use typst::layout::{Abs, Margin, PageElem}; use typst::syntax::{FileId, Source}; use typst::text::{Font, FontBook, TextElem, TextSize}; use typst::utils::{singleton, LazyHash}; use typst::{Library, World}; /// A world for IDE testing. pub struct TestWorld { pub main: Source, base: &'static TestBase, } impl TestWorld { /// Create a new world for a single test. /// /// This is cheap because the shared base for all test runs is lazily /// initialized just once. pub fn new(text: &str) -> Self { let main = Source::detached(text); Self { main, base: singleton!(TestBase, TestBase::default()), } } /// The ID of the main file in a `TestWorld`. pub fn main_id() -> FileId { *singleton!(FileId, Source::detached("").id()) } } impl World for TestWorld { fn library(&self) -> &LazyHash { &self.base.library } fn book(&self) -> &LazyHash { &self.base.book } fn main(&self) -> FileId { self.main.id() } fn source(&self, id: FileId) -> FileResult { if id == self.main.id() { Ok(self.main.clone()) } else { Err(FileError::NotFound(id.vpath().as_rootless_path().into())) } } fn file(&self, id: FileId) -> FileResult { Err(FileError::NotFound(id.vpath().as_rootless_path().into())) } fn font(&self, index: usize) -> Option { Some(self.base.fonts[index].clone()) } fn today(&self, _: Option) -> Option { None } } /// Shared foundation of all test worlds. struct TestBase { library: LazyHash, book: LazyHash, fonts: Vec, } impl Default for TestBase { fn default() -> Self { let fonts: Vec<_> = typst_assets::fonts() .chain(typst_dev_assets::fonts()) .flat_map(|data| Font::iter(Bytes::from_static(data))) .collect(); Self { library: LazyHash::new(library()), book: LazyHash::new(FontBook::from_fonts(&fonts)), fonts, } } } /// The extended standard library for testing. fn library() -> Library { // Set page width to 120pt with 10pt margins, so that the inner page is // exactly 100pt wide. Page height is unbounded and font size is 10pt so // that it multiplies to nice round numbers. let mut lib = typst::Library::default(); lib.styles .set(PageElem::set_width(Smart::Custom(Abs::pt(120.0).into()))); lib.styles.set(PageElem::set_height(Smart::Auto)); lib.styles.set(PageElem::set_margin(Margin::splat(Some(Smart::Custom( Abs::pt(10.0).into(), ))))); lib.styles.set(TextElem::set_size(TextSize(Abs::pt(10.0).into()))); lib } }