summaryrefslogtreecommitdiff
path: root/crates/typst-ide/src/utils.rs
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2024-11-13 12:01:38 +0100
committerLaurenz <laurmaedje@gmail.com>2024-11-13 12:03:47 +0100
commit8dbbe68527e0855b87910fe367ef29f96a670408 (patch)
treeb80403da30012f623fe1d2d298dbd1def19f90a8 /crates/typst-ide/src/utils.rs
parent737895d769188f6fc154523e67a9102bc24c872e (diff)
Backport IDE improvements0.12.0-with-extras
Diffstat (limited to 'crates/typst-ide/src/utils.rs')
-rw-r--r--crates/typst-ide/src/utils.rs168
1 files changed, 168 insertions, 0 deletions
diff --git a/crates/typst-ide/src/utils.rs b/crates/typst-ide/src/utils.rs
new file mode 100644
index 00000000..9ea058b9
--- /dev/null
+++ b/crates/typst-ide/src/utils.rs
@@ -0,0 +1,168 @@
+use std::fmt::Write;
+use std::ops::ControlFlow;
+
+use ecow::{eco_format, EcoString};
+use typst::foundations::{Scope, Value};
+use typst::syntax::{LinkedNode, SyntaxKind};
+use typst::text::{FontInfo, FontStyle};
+
+use crate::IdeWorld;
+
+/// 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
+}
+
+/// The global definitions at the given node.
+pub fn globals<'a>(world: &'a dyn IdeWorld, leaf: &LinkedNode) -> &'a Scope {
+ let in_math = matches!(
+ leaf.parent_kind(),
+ Some(SyntaxKind::Equation)
+ | Some(SyntaxKind::Math)
+ | Some(SyntaxKind::MathFrac)
+ | Some(SyntaxKind::MathAttach)
+ );
+
+ let library = world.library();
+ if in_math {
+ library.math.scope()
+ } else {
+ library.global.scope()
+ }
+}
+
+/// Checks whether the given value or any of its constituent parts satisfy the
+/// predicate.
+pub fn check_value_recursively(
+ value: &Value,
+ predicate: impl Fn(&Value) -> bool,
+) -> bool {
+ let mut searcher = Searcher { steps: 0, predicate, max_steps: 1000 };
+ match searcher.find(value) {
+ ControlFlow::Break(matching) => matching,
+ ControlFlow::Continue(_) => false,
+ }
+}
+
+/// Recursively searches for a value that passes the filter, but without
+/// exceeding a maximum number of search steps.
+struct Searcher<F> {
+ max_steps: usize,
+ steps: usize,
+ predicate: F,
+}
+
+impl<F> Searcher<F>
+where
+ F: Fn(&Value) -> bool,
+{
+ fn find(&mut self, value: &Value) -> ControlFlow<bool> {
+ if (self.predicate)(value) {
+ return ControlFlow::Break(true);
+ }
+
+ if self.steps > self.max_steps {
+ return ControlFlow::Break(false);
+ }
+
+ self.steps += 1;
+
+ match value {
+ Value::Dict(dict) => {
+ self.find_iter(dict.iter().map(|(_, v)| v))?;
+ }
+ Value::Content(content) => {
+ self.find_iter(content.fields().iter().map(|(_, v)| v))?;
+ }
+ Value::Module(module) => {
+ self.find_iter(module.scope().iter().map(|(_, v, _)| v))?;
+ }
+ _ => {}
+ }
+
+ ControlFlow::Continue(())
+ }
+
+ fn find_iter<'a>(
+ &mut self,
+ iter: impl Iterator<Item = &'a Value>,
+ ) -> ControlFlow<bool> {
+ for item in iter {
+ self.find(item)?;
+ }
+ ControlFlow::Continue(())
+ }
+}