mirror of
https://github.com/typst/typst
synced 2025-05-14 04:56:26 +08:00
Move a few things into separate files
This commit is contained in:
parent
366348b196
commit
6c22ba1cbd
@ -19,10 +19,8 @@ use typst::text::RawElem;
|
|||||||
use typst::visualize::Color;
|
use typst::visualize::Color;
|
||||||
use unscanny::Scanner;
|
use unscanny::Scanner;
|
||||||
|
|
||||||
use crate::{
|
use crate::utils::{plain_docs_sentence, summarize_font_family};
|
||||||
analyze_expr, analyze_import, analyze_labels, named_items, plain_docs_sentence,
|
use crate::{analyze_expr, analyze_import, analyze_labels, named_items, IdeWorld};
|
||||||
summarize_font_family, IdeWorld,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Autocomplete a cursor position in a source file.
|
/// Autocomplete a cursor position in a source file.
|
||||||
///
|
///
|
||||||
|
@ -6,6 +6,7 @@ mod definition;
|
|||||||
mod jump;
|
mod jump;
|
||||||
mod matchers;
|
mod matchers;
|
||||||
mod tooltip;
|
mod tooltip;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
pub use self::analyze::{analyze_expr, analyze_import, analyze_labels};
|
pub use self::analyze::{analyze_expr, analyze_import, analyze_labels};
|
||||||
pub use self::complete::{autocomplete, Completion, CompletionKind};
|
pub use self::complete::{autocomplete, Completion, CompletionKind};
|
||||||
@ -14,20 +15,17 @@ pub use self::jump::{jump_from_click, jump_from_cursor, Jump};
|
|||||||
pub use self::matchers::{deref_target, named_items, DerefTarget, NamedItem};
|
pub use self::matchers::{deref_target, named_items, DerefTarget, NamedItem};
|
||||||
pub use self::tooltip::{tooltip, Tooltip};
|
pub use self::tooltip::{tooltip, Tooltip};
|
||||||
|
|
||||||
use std::fmt::Write;
|
use ecow::EcoString;
|
||||||
|
|
||||||
use ecow::{eco_format, EcoString};
|
|
||||||
use typst::syntax::package::PackageSpec;
|
use typst::syntax::package::PackageSpec;
|
||||||
use typst::syntax::FileId;
|
use typst::syntax::FileId;
|
||||||
use typst::text::{FontInfo, FontStyle};
|
|
||||||
use typst::World;
|
use typst::World;
|
||||||
|
|
||||||
/// Extends the `World` for IDE functionality.
|
/// Extends the `World` for IDE functionality.
|
||||||
pub trait IdeWorld: World {
|
pub trait IdeWorld: World {
|
||||||
/// Turn into a normal [`World`].
|
/// Turn this into a normal [`World`].
|
||||||
///
|
///
|
||||||
/// This is necessary because trait upcasting is experimental in Rust.
|
/// This is necessary because trait upcasting is experimental in Rust.
|
||||||
/// See: https://github.com/rust-lang/rust/issues/65991
|
/// See <https://github.com/rust-lang/rust/issues/65991>.
|
||||||
///
|
///
|
||||||
/// Implementors can simply return `self`.
|
/// Implementors can simply return `self`.
|
||||||
fn upcast(&self) -> &dyn World;
|
fn upcast(&self) -> &dyn World;
|
||||||
@ -51,266 +49,5 @@ pub trait IdeWorld: World {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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<Item = &'a FontInfo>) -> 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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests;
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use ecow::EcoString;
|
|
||||||
use typst::diag::{FileError, FileResult};
|
|
||||||
use typst::foundations::{Bytes, Datetime, Smart};
|
|
||||||
use typst::layout::{Abs, Margin, PageElem};
|
|
||||||
use typst::syntax::package::{PackageSpec, PackageVersion};
|
|
||||||
use typst::syntax::{FileId, Source, VirtualPath};
|
|
||||||
use typst::text::{Font, FontBook, TextElem, TextSize};
|
|
||||||
use typst::utils::{singleton, LazyHash};
|
|
||||||
use typst::{Library, World};
|
|
||||||
|
|
||||||
use crate::IdeWorld;
|
|
||||||
|
|
||||||
/// A world for IDE testing.
|
|
||||||
pub struct TestWorld {
|
|
||||||
pub main: Source,
|
|
||||||
assets: HashMap<FileId, Bytes>,
|
|
||||||
sources: HashMap<FileId, 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::new(Self::main_id(), text.into());
|
|
||||||
Self {
|
|
||||||
main,
|
|
||||||
assets: HashMap::new(),
|
|
||||||
sources: HashMap::new(),
|
|
||||||
base: singleton!(TestBase, TestBase::default()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add an additional source file to the test world.
|
|
||||||
pub fn with_source(mut self, path: &str, text: &str) -> Self {
|
|
||||||
let id = FileId::new(None, VirtualPath::new(path));
|
|
||||||
let source = Source::new(id, text.into());
|
|
||||||
self.sources.insert(id, source);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add an additional asset file to the test world.
|
|
||||||
#[track_caller]
|
|
||||||
pub fn with_asset(self, filename: &str) -> Self {
|
|
||||||
self.with_asset_at(filename, filename)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add an additional asset file to the test world.
|
|
||||||
#[track_caller]
|
|
||||||
pub fn with_asset_at(mut self, path: &str, filename: &str) -> Self {
|
|
||||||
let id = FileId::new(None, VirtualPath::new(path));
|
|
||||||
let data = typst_dev_assets::get_by_name(filename).unwrap();
|
|
||||||
let bytes = Bytes::from_static(data);
|
|
||||||
self.assets.insert(id, bytes);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The ID of the main file in a `TestWorld`.
|
|
||||||
pub fn main_id() -> FileId {
|
|
||||||
*singleton!(FileId, FileId::new(None, VirtualPath::new("main.typ")))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl World for TestWorld {
|
|
||||||
fn library(&self) -> &LazyHash<Library> {
|
|
||||||
&self.base.library
|
|
||||||
}
|
|
||||||
|
|
||||||
fn book(&self) -> &LazyHash<FontBook> {
|
|
||||||
&self.base.book
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main(&self) -> FileId {
|
|
||||||
self.main.id()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn source(&self, id: FileId) -> FileResult<Source> {
|
|
||||||
if id == self.main.id() {
|
|
||||||
Ok(self.main.clone())
|
|
||||||
} else if let Some(source) = self.sources.get(&id) {
|
|
||||||
Ok(source.clone())
|
|
||||||
} else {
|
|
||||||
Err(FileError::NotFound(id.vpath().as_rootless_path().into()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn file(&self, id: FileId) -> FileResult<Bytes> {
|
|
||||||
match self.assets.get(&id) {
|
|
||||||
Some(bytes) => Ok(bytes.clone()),
|
|
||||||
None => Err(FileError::NotFound(id.vpath().as_rootless_path().into())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn font(&self, index: usize) -> Option<Font> {
|
|
||||||
Some(self.base.fonts[index].clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn today(&self, _: Option<i64>) -> Option<Datetime> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IdeWorld for TestWorld {
|
|
||||||
fn upcast(&self) -> &dyn World {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn files(&self) -> Vec<FileId> {
|
|
||||||
std::iter::once(self.main.id())
|
|
||||||
.chain(self.sources.keys().copied())
|
|
||||||
.chain(self.assets.keys().copied())
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn packages(&self) -> &[(PackageSpec, Option<EcoString>)] {
|
|
||||||
const LIST: &[(PackageSpec, Option<EcoString>)] = &[(
|
|
||||||
PackageSpec {
|
|
||||||
namespace: EcoString::inline("preview"),
|
|
||||||
name: EcoString::inline("example"),
|
|
||||||
version: PackageVersion { major: 0, minor: 1, patch: 0 },
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)];
|
|
||||||
LIST
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extra methods for [`Source`].
|
|
||||||
pub trait SourceExt {
|
|
||||||
/// Negative cursors index from the back.
|
|
||||||
fn cursor(&self, cursor: isize) -> usize;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SourceExt for Source {
|
|
||||||
fn cursor(&self, cursor: isize) -> usize {
|
|
||||||
if cursor < 0 {
|
|
||||||
self.len_bytes().checked_add_signed(cursor).unwrap()
|
|
||||||
} else {
|
|
||||||
cursor as usize
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Shared foundation of all test worlds.
|
|
||||||
struct TestBase {
|
|
||||||
library: LazyHash<Library>,
|
|
||||||
book: LazyHash<FontBook>,
|
|
||||||
fonts: Vec<Font>,
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
184
crates/typst-ide/src/tests.rs
Normal file
184
crates/typst-ide/src/tests.rs
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use ecow::EcoString;
|
||||||
|
use typst::diag::{FileError, FileResult};
|
||||||
|
use typst::foundations::{Bytes, Datetime, Smart};
|
||||||
|
use typst::layout::{Abs, Margin, PageElem};
|
||||||
|
use typst::syntax::package::{PackageSpec, PackageVersion};
|
||||||
|
use typst::syntax::{FileId, Source, VirtualPath};
|
||||||
|
use typst::text::{Font, FontBook, TextElem, TextSize};
|
||||||
|
use typst::utils::{singleton, LazyHash};
|
||||||
|
use typst::{Library, World};
|
||||||
|
|
||||||
|
use crate::IdeWorld;
|
||||||
|
|
||||||
|
/// A world for IDE testing.
|
||||||
|
pub struct TestWorld {
|
||||||
|
pub main: Source,
|
||||||
|
assets: HashMap<FileId, Bytes>,
|
||||||
|
sources: HashMap<FileId, 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::new(Self::main_id(), text.into());
|
||||||
|
Self {
|
||||||
|
main,
|
||||||
|
assets: HashMap::new(),
|
||||||
|
sources: HashMap::new(),
|
||||||
|
base: singleton!(TestBase, TestBase::default()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add an additional source file to the test world.
|
||||||
|
pub fn with_source(mut self, path: &str, text: &str) -> Self {
|
||||||
|
let id = FileId::new(None, VirtualPath::new(path));
|
||||||
|
let source = Source::new(id, text.into());
|
||||||
|
self.sources.insert(id, source);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add an additional asset file to the test world.
|
||||||
|
#[track_caller]
|
||||||
|
pub fn with_asset(self, filename: &str) -> Self {
|
||||||
|
self.with_asset_at(filename, filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add an additional asset file to the test world.
|
||||||
|
#[track_caller]
|
||||||
|
pub fn with_asset_at(mut self, path: &str, filename: &str) -> Self {
|
||||||
|
let id = FileId::new(None, VirtualPath::new(path));
|
||||||
|
let data = typst_dev_assets::get_by_name(filename).unwrap();
|
||||||
|
let bytes = Bytes::from_static(data);
|
||||||
|
self.assets.insert(id, bytes);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The ID of the main file in a `TestWorld`.
|
||||||
|
pub fn main_id() -> FileId {
|
||||||
|
*singleton!(FileId, FileId::new(None, VirtualPath::new("main.typ")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl World for TestWorld {
|
||||||
|
fn library(&self) -> &LazyHash<Library> {
|
||||||
|
&self.base.library
|
||||||
|
}
|
||||||
|
|
||||||
|
fn book(&self) -> &LazyHash<FontBook> {
|
||||||
|
&self.base.book
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main(&self) -> FileId {
|
||||||
|
self.main.id()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn source(&self, id: FileId) -> FileResult<Source> {
|
||||||
|
if id == self.main.id() {
|
||||||
|
Ok(self.main.clone())
|
||||||
|
} else if let Some(source) = self.sources.get(&id) {
|
||||||
|
Ok(source.clone())
|
||||||
|
} else {
|
||||||
|
Err(FileError::NotFound(id.vpath().as_rootless_path().into()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn file(&self, id: FileId) -> FileResult<Bytes> {
|
||||||
|
match self.assets.get(&id) {
|
||||||
|
Some(bytes) => Ok(bytes.clone()),
|
||||||
|
None => Err(FileError::NotFound(id.vpath().as_rootless_path().into())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn font(&self, index: usize) -> Option<Font> {
|
||||||
|
Some(self.base.fonts[index].clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn today(&self, _: Option<i64>) -> Option<Datetime> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IdeWorld for TestWorld {
|
||||||
|
fn upcast(&self) -> &dyn World {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn files(&self) -> Vec<FileId> {
|
||||||
|
std::iter::once(self.main.id())
|
||||||
|
.chain(self.sources.keys().copied())
|
||||||
|
.chain(self.assets.keys().copied())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn packages(&self) -> &[(PackageSpec, Option<EcoString>)] {
|
||||||
|
const LIST: &[(PackageSpec, Option<EcoString>)] = &[(
|
||||||
|
PackageSpec {
|
||||||
|
namespace: EcoString::inline("preview"),
|
||||||
|
name: EcoString::inline("example"),
|
||||||
|
version: PackageVersion { major: 0, minor: 1, patch: 0 },
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)];
|
||||||
|
LIST
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extra methods for [`Source`].
|
||||||
|
pub trait SourceExt {
|
||||||
|
/// Negative cursors index from the back.
|
||||||
|
fn cursor(&self, cursor: isize) -> usize;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SourceExt for Source {
|
||||||
|
fn cursor(&self, cursor: isize) -> usize {
|
||||||
|
if cursor < 0 {
|
||||||
|
self.len_bytes().checked_add_signed(cursor).unwrap()
|
||||||
|
} else {
|
||||||
|
cursor as usize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shared foundation of all test worlds.
|
||||||
|
struct TestBase {
|
||||||
|
library: LazyHash<Library>,
|
||||||
|
book: LazyHash<FontBook>,
|
||||||
|
fonts: Vec<Font>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
@ -11,10 +11,8 @@ use typst::syntax::{ast, LinkedNode, Side, Source, SyntaxKind};
|
|||||||
use typst::utils::{round_with_precision, Numeric};
|
use typst::utils::{round_with_precision, Numeric};
|
||||||
use typst_eval::CapturesVisitor;
|
use typst_eval::CapturesVisitor;
|
||||||
|
|
||||||
use crate::{
|
use crate::utils::{plain_docs_sentence, summarize_font_family};
|
||||||
analyze_expr, analyze_import, analyze_labels, plain_docs_sentence,
|
use crate::{analyze_expr, analyze_import, analyze_labels, IdeWorld};
|
||||||
summarize_font_family, IdeWorld,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Describe the item under the cursor.
|
/// Describe the item under the cursor.
|
||||||
///
|
///
|
||||||
|
82
crates/typst-ide/src/utils.rs
Normal file
82
crates/typst-ide/src/utils.rs
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
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.
|
||||||
|
pub 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.
|
||||||
|
pub 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);
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user