mirror of
https://github.com/Myriad-Dreamin/tinymist.git
synced 2025-07-24 05:05:00 +00:00
feat: provide package view and local documentation (#596)
* feat: move featured components * feat: provide package view and local documentation * stage * fix: compile error by merged commits
This commit is contained in:
parent
b06447ffe2
commit
78f3893185
70 changed files with 3422 additions and 593 deletions
24
Cargo.lock
generated
24
Cargo.lock
generated
|
@ -3945,6 +3945,23 @@ dependencies = [
|
||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tinymist-analysis"
|
||||||
|
version = "0.11.20"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
|
"comemo 0.4.0",
|
||||||
|
"ecow 0.2.2",
|
||||||
|
"insta",
|
||||||
|
"log",
|
||||||
|
"regex",
|
||||||
|
"tinymist-world",
|
||||||
|
"toml 0.8.14",
|
||||||
|
"typst",
|
||||||
|
"typst-svg",
|
||||||
|
"typst-syntax 0.11.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tinymist-assets"
|
name = "tinymist-assets"
|
||||||
version = "0.11.20"
|
version = "0.11.20"
|
||||||
|
@ -3960,6 +3977,7 @@ name = "tinymist-query"
|
||||||
version = "0.11.20"
|
version = "0.11.20"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"base64 0.22.1",
|
||||||
"biblatex",
|
"biblatex",
|
||||||
"chrono",
|
"chrono",
|
||||||
"comemo 0.4.0",
|
"comemo 0.4.0",
|
||||||
|
@ -3991,9 +4009,12 @@ dependencies = [
|
||||||
"sha2",
|
"sha2",
|
||||||
"siphasher 1.0.1",
|
"siphasher 1.0.1",
|
||||||
"strum 0.26.3",
|
"strum 0.26.3",
|
||||||
|
"tinymist-analysis",
|
||||||
|
"tinymist-world",
|
||||||
"toml 0.8.14",
|
"toml 0.8.14",
|
||||||
"triomphe",
|
"triomphe",
|
||||||
"ttf-parser",
|
"ttf-parser",
|
||||||
|
"typlite",
|
||||||
"typst",
|
"typst",
|
||||||
"typst-assets",
|
"typst-assets",
|
||||||
"typst-shim",
|
"typst-shim",
|
||||||
|
@ -4300,7 +4321,7 @@ dependencies = [
|
||||||
"ecow 0.2.2",
|
"ecow 0.2.2",
|
||||||
"insta",
|
"insta",
|
||||||
"regex",
|
"regex",
|
||||||
"tinymist-query",
|
"tinymist-analysis",
|
||||||
"tinymist-world",
|
"tinymist-world",
|
||||||
"typst",
|
"typst",
|
||||||
"typst-svg",
|
"typst-svg",
|
||||||
|
@ -4312,6 +4333,7 @@ name = "typlite-cli"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
|
"ecow 0.2.2",
|
||||||
"tinymist-world",
|
"tinymist-world",
|
||||||
"typlite",
|
"typlite",
|
||||||
]
|
]
|
||||||
|
|
|
@ -139,6 +139,7 @@ insta = { version = "1.39", features = ["glob"] }
|
||||||
typst-preview = { path = "./crates/typst-preview/" }
|
typst-preview = { path = "./crates/typst-preview/" }
|
||||||
tinymist-assets = { version = "0.11.20" }
|
tinymist-assets = { version = "0.11.20" }
|
||||||
tinymist = { path = "./crates/tinymist/" }
|
tinymist = { path = "./crates/tinymist/" }
|
||||||
|
tinymist-analysis = { path = "./crates/tinymist-analysis/" }
|
||||||
tinymist-query = { path = "./crates/tinymist-query/" }
|
tinymist-query = { path = "./crates/tinymist-query/" }
|
||||||
tinymist-world = { path = "./crates/tinymist-world/" }
|
tinymist-world = { path = "./crates/tinymist-world/" }
|
||||||
tinymist-render = { path = "./crates/tinymist-render/" }
|
tinymist-render = { path = "./crates/tinymist-render/" }
|
||||||
|
|
|
@ -12,3 +12,4 @@ path = "src/main.rs"
|
||||||
typlite.workspace = true
|
typlite.workspace = true
|
||||||
clap.workspace = true
|
clap.workspace = true
|
||||||
tinymist-world.workspace = true
|
tinymist-world.workspace = true
|
||||||
|
ecow.workspace = true
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
#![doc = include_str!("../README.md")]
|
#![doc = include_str!("../README.md")]
|
||||||
|
|
||||||
extern crate clap;
|
extern crate clap;
|
||||||
|
extern crate ecow;
|
||||||
extern crate tinymist_world;
|
extern crate tinymist_world;
|
||||||
extern crate typlite;
|
extern crate typlite;
|
||||||
|
|
||||||
|
@ -10,6 +11,8 @@ use std::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
use ecow::{eco_format, EcoString};
|
||||||
|
use typlite::value::*;
|
||||||
use typlite::{CompileOnceArgs, Typlite};
|
use typlite::{CompileOnceArgs, Typlite};
|
||||||
|
|
||||||
/// Common arguments of compile, watch, and query.
|
/// Common arguments of compile, watch, and query.
|
||||||
|
@ -41,7 +44,8 @@ fn main() -> typlite::Result<()> {
|
||||||
let universe = args.compile.resolve().map_err(|e| format!("{e:?}"))?;
|
let universe = args.compile.resolve().map_err(|e| format!("{e:?}"))?;
|
||||||
let world = universe.snapshot();
|
let world = universe.snapshot();
|
||||||
|
|
||||||
let conv = Typlite::new(Arc::new(world)).convert();
|
let converter = Typlite::new(Arc::new(world)).with_library(lib());
|
||||||
|
let conv = converter.convert();
|
||||||
|
|
||||||
match (conv, output) {
|
match (conv, output) {
|
||||||
(Ok(conv), None) => println!("{}", conv),
|
(Ok(conv), None) => println!("{}", conv),
|
||||||
|
@ -54,3 +58,25 @@ fn main() -> typlite::Result<()> {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn lib() -> Arc<typlite::scopes::Scopes<Value>> {
|
||||||
|
let mut scopes = typlite::library::library();
|
||||||
|
|
||||||
|
// todo: how to import this function correctly?
|
||||||
|
scopes.define("cross-link", cross_link as RawFunc);
|
||||||
|
|
||||||
|
Arc::new(scopes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluate a `cross-link`.
|
||||||
|
pub fn cross_link(mut args: Args) -> typlite::Result<Value> {
|
||||||
|
let dest = get_pos_named!(args, dest: EcoString);
|
||||||
|
let body = get_pos_named!(args, body: Content);
|
||||||
|
|
||||||
|
let dest = std::path::Path::new(dest.as_str()).with_extension("html");
|
||||||
|
|
||||||
|
Ok(Value::Content(eco_format!(
|
||||||
|
"[{body}](https://myriad-dreamin.github.io/tinymist/{dest})",
|
||||||
|
dest = dest.to_string_lossy()
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
30
crates/tinymist-analysis/Cargo.toml
Normal file
30
crates/tinymist-analysis/Cargo.toml
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
[package]
|
||||||
|
name = "tinymist-analysis"
|
||||||
|
description = "Typst Static Analyzers for Tinymist."
|
||||||
|
categories = ["compilers"]
|
||||||
|
keywords = ["language", "typst"]
|
||||||
|
authors.workspace = true
|
||||||
|
version.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
homepage.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
typst-syntax.workspace = true
|
||||||
|
tinymist-world.workspace = true
|
||||||
|
ecow.workspace = true
|
||||||
|
comemo.workspace = true
|
||||||
|
log.workspace = true
|
||||||
|
toml.workspace = true
|
||||||
|
|
||||||
|
typst.workspace = true
|
||||||
|
typst-svg.workspace = true
|
||||||
|
base64.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
insta.workspace = true
|
||||||
|
regex.workspace = true
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
|
@ -1,3 +1,5 @@
|
||||||
|
//! Import resolution utilities.
|
||||||
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
/// Resolve a file id by its import path.
|
/// Resolve a file id by its import path.
|
4
crates/tinymist-analysis/src/lib.rs
Normal file
4
crates/tinymist-analysis/src/lib.rs
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
//! Tinymist Analysis
|
||||||
|
|
||||||
|
pub mod import;
|
||||||
|
mod prelude;
|
10
crates/tinymist-analysis/src/prelude.rs
Normal file
10
crates/tinymist-analysis/src/prelude.rs
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
pub use std::path::Path;
|
||||||
|
|
||||||
|
pub use typst::diag::FileError;
|
||||||
|
pub use typst::syntax::FileId as TypstFileId;
|
||||||
|
pub use typst::syntax::{
|
||||||
|
ast::{self},
|
||||||
|
package::{PackageManifest, PackageSpec},
|
||||||
|
Source, VirtualPath,
|
||||||
|
};
|
||||||
|
pub use typst::World;
|
|
@ -51,6 +51,10 @@ dashmap.workspace = true
|
||||||
rustc-hash.workspace = true
|
rustc-hash.workspace = true
|
||||||
hashbrown.workspace = true
|
hashbrown.workspace = true
|
||||||
triomphe.workspace = true
|
triomphe.workspace = true
|
||||||
|
base64.workspace = true
|
||||||
|
typlite.workspace = true
|
||||||
|
tinymist-world.workspace = true
|
||||||
|
tinymist-analysis.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
once_cell.workspace = true
|
once_cell.workspace = true
|
||||||
|
|
|
@ -333,7 +333,7 @@ mod lexical_hierarchy_tests {
|
||||||
undefined_refs.sort();
|
undefined_refs.sort();
|
||||||
let entry = DefUseEntry {
|
let entry = DefUseEntry {
|
||||||
def: &IdentDef {
|
def: &IdentDef {
|
||||||
name: "<nil>".to_string(),
|
name: "<nil>".into(),
|
||||||
kind: LexicalKind::Block,
|
kind: LexicalKind::Block,
|
||||||
range: 0..0,
|
range: 0..0,
|
||||||
},
|
},
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
use std::{collections::HashMap, ops::Range, sync::Arc};
|
use std::{collections::HashMap, ops::Range, sync::Arc};
|
||||||
|
|
||||||
use ecow::EcoVec;
|
use ecow::{EcoString, EcoVec};
|
||||||
use reflexo::hash::hash128;
|
use reflexo::hash::hash128;
|
||||||
|
|
||||||
use super::{prelude::*, ImportInfo};
|
use super::{prelude::*, ImportInfo};
|
||||||
|
@ -18,7 +18,7 @@ enum Ns {
|
||||||
Value,
|
Value,
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExternalRefMap = HashMap<(TypstFileId, Option<String>), Vec<(Option<DefId>, IdentRef)>>;
|
type ExternalRefMap = HashMap<(TypstFileId, Option<EcoString>), Vec<(Option<DefId>, IdentRef)>>;
|
||||||
|
|
||||||
/// The def-use information of a source file.
|
/// The def-use information of a source file.
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
@ -29,11 +29,11 @@ pub struct DefUseInfo {
|
||||||
/// The references to defined symbols.
|
/// The references to defined symbols.
|
||||||
pub ident_refs: HashMap<IdentRef, DefId>,
|
pub ident_refs: HashMap<IdentRef, DefId>,
|
||||||
/// The references of labels.
|
/// The references of labels.
|
||||||
pub label_refs: HashMap<String, Vec<Range<usize>>>,
|
pub label_refs: HashMap<EcoString, Vec<Range<usize>>>,
|
||||||
/// The references to undefined symbols.
|
/// The references to undefined symbols.
|
||||||
pub undefined_refs: Vec<IdentRef>,
|
pub undefined_refs: Vec<IdentRef>,
|
||||||
exports_refs: Vec<DefId>,
|
exports_refs: Vec<DefId>,
|
||||||
exports_defs: HashMap<String, DefId>,
|
exports_defs: HashMap<EcoString, DefId>,
|
||||||
|
|
||||||
self_id: Option<TypstFileId>,
|
self_id: Option<TypstFileId>,
|
||||||
self_hash: u128,
|
self_hash: u128,
|
||||||
|
@ -93,7 +93,7 @@ impl DefUseInfo {
|
||||||
pub fn get_external_refs(
|
pub fn get_external_refs(
|
||||||
&self,
|
&self,
|
||||||
ext_id: TypstFileId,
|
ext_id: TypstFileId,
|
||||||
ext_name: Option<String>,
|
ext_name: Option<EcoString>,
|
||||||
) -> impl Iterator<Item = &(Option<DefId>, IdentRef)> {
|
) -> impl Iterator<Item = &(Option<DefId>, IdentRef)> {
|
||||||
self.external_refs
|
self.external_refs
|
||||||
.get(&(ext_id, ext_name))
|
.get(&(ext_id, ext_name))
|
||||||
|
@ -160,8 +160,8 @@ pub(super) fn get_def_use_inner(
|
||||||
struct DefUseCollector<'a, 'b, 'w> {
|
struct DefUseCollector<'a, 'b, 'w> {
|
||||||
ctx: &'a mut SearchCtx<'b, 'w>,
|
ctx: &'a mut SearchCtx<'b, 'w>,
|
||||||
info: DefUseInfo,
|
info: DefUseInfo,
|
||||||
label_scope: SnapshotMap<String, DefId>,
|
label_scope: SnapshotMap<EcoString, DefId>,
|
||||||
id_scope: SnapshotMap<String, DefId>,
|
id_scope: SnapshotMap<EcoString, DefId>,
|
||||||
import: Arc<ImportInfo>,
|
import: Arc<ImportInfo>,
|
||||||
|
|
||||||
current_id: TypstFileId,
|
current_id: TypstFileId,
|
||||||
|
@ -317,7 +317,7 @@ impl<'a, 'b, 'w> DefUseCollector<'a, 'b, 'w> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn insert_extern(&mut self, name: String, range: Range<usize>, redefine_id: Option<DefId>) {
|
fn insert_extern(&mut self, name: EcoString, range: Range<usize>, redefine_id: Option<DefId>) {
|
||||||
if let Some(src) = &self.ext_src {
|
if let Some(src) = &self.ext_src {
|
||||||
self.info.external_refs.insert(
|
self.info.external_refs.insert(
|
||||||
(src.id(), Some(name.clone())),
|
(src.id(), Some(name.clone())),
|
||||||
|
|
|
@ -174,6 +174,11 @@ pub trait AnalysisResources {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the local packages and their descriptions.
|
||||||
|
fn local_packages(&self) -> EcoVec<PackageSpec> {
|
||||||
|
EcoVec::new()
|
||||||
|
}
|
||||||
|
|
||||||
/// Resolve telescope image at the given position.
|
/// Resolve telescope image at the given position.
|
||||||
fn periscope_at(
|
fn periscope_at(
|
||||||
&self,
|
&self,
|
||||||
|
@ -596,32 +601,7 @@ impl<'w> AnalysisContext<'w> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn with_vm<T>(&self, f: impl FnOnce(&mut typst::eval::Vm) -> T) -> T {
|
pub(crate) fn with_vm<T>(&self, f: impl FnOnce(&mut typst::eval::Vm) -> T) -> T {
|
||||||
use comemo::Track;
|
crate::upstream::with_vm(self.world(), f)
|
||||||
use typst::engine::*;
|
|
||||||
use typst::eval::*;
|
|
||||||
use typst::foundations::*;
|
|
||||||
use typst::introspection::*;
|
|
||||||
|
|
||||||
let mut locator = Locator::default();
|
|
||||||
let introspector = Introspector::default();
|
|
||||||
let mut tracer = Tracer::new();
|
|
||||||
let engine = Engine {
|
|
||||||
world: self.world().track(),
|
|
||||||
route: Route::default(),
|
|
||||||
introspector: introspector.track(),
|
|
||||||
locator: &mut locator,
|
|
||||||
tracer: tracer.track_mut(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let context = Context::none();
|
|
||||||
let mut vm = Vm::new(
|
|
||||||
engine,
|
|
||||||
context.track(),
|
|
||||||
Scopes::new(Some(self.world().library())),
|
|
||||||
Span::detached(),
|
|
||||||
);
|
|
||||||
|
|
||||||
f(&mut vm)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn const_eval(&self, rr: ast::Expr<'_>) -> Option<Value> {
|
pub(crate) fn const_eval(&self, rr: ast::Expr<'_>) -> Option<Value> {
|
||||||
|
@ -651,17 +631,26 @@ impl<'w> AnalysisContext<'w> {
|
||||||
Some(analyze_dyn_signature(self, func.clone()).type_sig())
|
Some(analyze_dyn_signature(self, func.clone()).type_sig())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn user_type_of_def(&mut self, source: &Source, def: &DefinitionLink) -> Option<Ty> {
|
pub(crate) fn user_type_of_ident(
|
||||||
let def_at = def.def_at.clone()?;
|
&mut self,
|
||||||
|
source: &Source,
|
||||||
|
def_fid: TypstFileId,
|
||||||
|
def_ident: &IdentRef,
|
||||||
|
) -> Option<Ty> {
|
||||||
let ty_chk = self.type_check(source.clone())?;
|
let ty_chk = self.type_check(source.clone())?;
|
||||||
let def_use = self.def_use(source.clone())?;
|
let def_use = self.def_use(source.clone())?;
|
||||||
|
|
||||||
|
let (def_id, _) = def_use.get_def(def_fid, def_ident)?;
|
||||||
|
ty_chk.type_of_def(def_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn user_type_of_def(&mut self, source: &Source, def: &DefinitionLink) -> Option<Ty> {
|
||||||
|
let def_at = def.def_at.clone()?;
|
||||||
let def_ident = IdentRef {
|
let def_ident = IdentRef {
|
||||||
name: def.name.clone(),
|
name: def.name.clone(),
|
||||||
range: def_at.1,
|
range: def_at.1,
|
||||||
};
|
};
|
||||||
let (def_id, _) = def_use.get_def(def_at.0, &def_ident)?;
|
self.user_type_of_ident(source, def_at.0, &def_ident)
|
||||||
ty_chk.type_of_def(def_id)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn type_of_span(&mut self, s: Span) -> Option<Ty> {
|
pub(crate) fn type_of_span(&mut self, s: Span) -> Option<Ty> {
|
||||||
|
|
|
@ -26,7 +26,7 @@ pub struct DefinitionLink {
|
||||||
/// A possible instance of the definition.
|
/// A possible instance of the definition.
|
||||||
pub value: Option<Value>,
|
pub value: Option<Value>,
|
||||||
/// The name of the definition.
|
/// The name of the definition.
|
||||||
pub name: String,
|
pub name: EcoString,
|
||||||
/// The location of the definition.
|
/// The location of the definition.
|
||||||
pub def_at: Option<(TypstFileId, Range<usize>)>,
|
pub def_at: Option<(TypstFileId, Range<usize>)>,
|
||||||
/// The range of the name of the definition.
|
/// The range of the name of the definition.
|
||||||
|
@ -54,7 +54,7 @@ pub fn find_definition(
|
||||||
let source = find_source_by_expr(ctx.world(), def_fid, import_node.source())?;
|
let source = find_source_by_expr(ctx.world(), def_fid, import_node.source())?;
|
||||||
return Some(DefinitionLink {
|
return Some(DefinitionLink {
|
||||||
kind: LexicalKind::Mod(LexicalModKind::PathVar),
|
kind: LexicalKind::Mod(LexicalModKind::PathVar),
|
||||||
name: String::new(),
|
name: EcoString::new(),
|
||||||
value: None,
|
value: None,
|
||||||
def_at: Some((source.id(), LinkedNode::new(source.root()).range())),
|
def_at: Some((source.id(), LinkedNode::new(source.root()).range())),
|
||||||
name_range: None,
|
name_range: None,
|
||||||
|
@ -67,7 +67,7 @@ pub fn find_definition(
|
||||||
let source = find_source_by_expr(ctx.world(), def_fid, include_node.source())?;
|
let source = find_source_by_expr(ctx.world(), def_fid, include_node.source())?;
|
||||||
return Some(DefinitionLink {
|
return Some(DefinitionLink {
|
||||||
kind: LexicalKind::Mod(LexicalModKind::PathInclude),
|
kind: LexicalKind::Mod(LexicalModKind::PathInclude),
|
||||||
name: String::new(),
|
name: EcoString::new(),
|
||||||
value: None,
|
value: None,
|
||||||
def_at: Some((source.id(), (LinkedNode::new(source.root())).range())),
|
def_at: Some((source.id(), (LinkedNode::new(source.root())).range())),
|
||||||
name_range: None,
|
name_range: None,
|
||||||
|
@ -125,7 +125,7 @@ pub fn find_definition(
|
||||||
|
|
||||||
Some(DefinitionLink {
|
Some(DefinitionLink {
|
||||||
kind: LexicalKind::Var(LexicalVarKind::Label),
|
kind: LexicalKind::Var(LexicalVarKind::Label),
|
||||||
name: ref_node.to_owned(),
|
name: ref_node.into(),
|
||||||
value: Some(Value::Content(elem)),
|
value: Some(Value::Content(elem)),
|
||||||
def_at,
|
def_at,
|
||||||
name_range,
|
name_range,
|
||||||
|
@ -140,11 +140,11 @@ pub fn find_definition(
|
||||||
// Lexical reference
|
// Lexical reference
|
||||||
let ident_ref = match use_site.cast::<ast::Expr>()? {
|
let ident_ref = match use_site.cast::<ast::Expr>()? {
|
||||||
ast::Expr::Ident(e) => Some(IdentRef {
|
ast::Expr::Ident(e) => Some(IdentRef {
|
||||||
name: e.get().to_string(),
|
name: e.get().clone(),
|
||||||
range: use_site.range(),
|
range: use_site.range(),
|
||||||
}),
|
}),
|
||||||
ast::Expr::MathIdent(e) => Some(IdentRef {
|
ast::Expr::MathIdent(e) => Some(IdentRef {
|
||||||
name: e.get().to_string(),
|
name: e.get().clone(),
|
||||||
range: use_site.range(),
|
range: use_site.range(),
|
||||||
}),
|
}),
|
||||||
ast::Expr::FieldAccess(..) => {
|
ast::Expr::FieldAccess(..) => {
|
||||||
|
@ -173,12 +173,7 @@ pub fn find_definition(
|
||||||
// Global definition
|
// Global definition
|
||||||
let Some((def_fid, def)) = def_info else {
|
let Some((def_fid, def)) = def_info else {
|
||||||
return resolve_global_value(ctx, use_site.clone(), false).and_then(move |f| {
|
return resolve_global_value(ctx, use_site.clone(), false).and_then(move |f| {
|
||||||
value_to_def(
|
value_to_def(ctx, f, || Some(use_site.get().clone().into_text()), None)
|
||||||
ctx,
|
|
||||||
f,
|
|
||||||
|| Some(use_site.get().clone().into_text().to_string()),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -237,7 +232,7 @@ fn find_bib_definition(bib_elem: Arc<BibInfo>, key: &str) -> Option<DefinitionLi
|
||||||
let entry = entry?;
|
let entry = entry?;
|
||||||
Some(DefinitionLink {
|
Some(DefinitionLink {
|
||||||
kind: LexicalKind::Var(LexicalVarKind::BibKey),
|
kind: LexicalKind::Var(LexicalVarKind::BibKey),
|
||||||
name: key.to_string(),
|
name: key.into(),
|
||||||
value: None,
|
value: None,
|
||||||
def_at: Some((entry.file_id, entry.span.clone())),
|
def_at: Some((entry.file_id, entry.span.clone())),
|
||||||
// todo: rename with regard to string format: yaml-key/bib etc.
|
// todo: rename with regard to string format: yaml-key/bib etc.
|
||||||
|
@ -448,7 +443,7 @@ pub(crate) fn resolve_global_value(
|
||||||
fn value_to_def(
|
fn value_to_def(
|
||||||
ctx: &mut AnalysisContext,
|
ctx: &mut AnalysisContext,
|
||||||
value: Value,
|
value: Value,
|
||||||
name: impl FnOnce() -> Option<String>,
|
name: impl FnOnce() -> Option<EcoString>,
|
||||||
name_range: Option<Range<usize>>,
|
name_range: Option<Range<usize>>,
|
||||||
) -> Option<DefinitionLink> {
|
) -> Option<DefinitionLink> {
|
||||||
let mut def_at = |span: Span| {
|
let mut def_at = |span: Span| {
|
||||||
|
@ -460,7 +455,7 @@ fn value_to_def(
|
||||||
|
|
||||||
Some(match value {
|
Some(match value {
|
||||||
Value::Func(func) => {
|
Value::Func(func) => {
|
||||||
let name = func.name().map(|e| e.to_owned()).or_else(name)?;
|
let name = func.name().map(|e| e.into()).or_else(name)?;
|
||||||
let span = func.span();
|
let span = func.span();
|
||||||
DefinitionLink {
|
DefinitionLink {
|
||||||
kind: LexicalKind::Var(LexicalVarKind::Function),
|
kind: LexicalKind::Var(LexicalVarKind::Function),
|
||||||
|
@ -471,7 +466,7 @@ fn value_to_def(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Value::Module(module) => {
|
Value::Module(module) => {
|
||||||
let name = module.name().to_string();
|
let name = module.name().clone();
|
||||||
DefinitionLink {
|
DefinitionLink {
|
||||||
kind: LexicalKind::Var(LexicalVarKind::Variable),
|
kind: LexicalKind::Var(LexicalVarKind::Variable),
|
||||||
name,
|
name,
|
||||||
|
|
|
@ -441,7 +441,7 @@ impl<'a, 'w> TypeChecker<'a, 'w> {
|
||||||
|
|
||||||
fn to_ident_ref(root: &LinkedNode, c: ast::Ident) -> Option<IdentRef> {
|
fn to_ident_ref(root: &LinkedNode, c: ast::Ident) -> Option<IdentRef> {
|
||||||
Some(IdentRef {
|
Some(IdentRef {
|
||||||
name: c.get().to_string(),
|
name: c.get().clone(),
|
||||||
range: root.find(c.span())?.range(),
|
range: root.find(c.span())?.range(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -182,7 +182,7 @@ impl<'a, 'w> TypeChecker<'a, 'w> {
|
||||||
fn check_ident(&mut self, root: LinkedNode<'_>, mode: InterpretMode) -> Option<Ty> {
|
fn check_ident(&mut self, root: LinkedNode<'_>, mode: InterpretMode) -> Option<Ty> {
|
||||||
let ident: ast::Ident = root.cast()?;
|
let ident: ast::Ident = root.cast()?;
|
||||||
let ident_ref = IdentRef {
|
let ident_ref = IdentRef {
|
||||||
name: ident.get().to_string(),
|
name: ident.get().clone(),
|
||||||
range: root.range(),
|
range: root.range(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
24
crates/tinymist-query/src/docs/library.rs
Normal file
24
crates/tinymist-query/src/docs/library.rs
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use ecow::eco_format;
|
||||||
|
use typlite::value::*;
|
||||||
|
|
||||||
|
pub(super) fn lib() -> Arc<typlite::scopes::Scopes<Value>> {
|
||||||
|
let mut scopes = typlite::library::library();
|
||||||
|
|
||||||
|
// todo: how to import this function correctly?
|
||||||
|
scopes.define("example", example as RawFunc);
|
||||||
|
|
||||||
|
Arc::new(scopes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluate a `example`.
|
||||||
|
pub fn example(mut args: Args) -> typlite::Result<Value> {
|
||||||
|
let body = get_pos_named!(args, body: Content).0;
|
||||||
|
let body = body.trim();
|
||||||
|
let ticks = body.chars().take_while(|t| *t == '`').collect::<String>();
|
||||||
|
let body = &body[ticks.len()..];
|
||||||
|
let body = eco_format!("{ticks}typ{body}");
|
||||||
|
|
||||||
|
Ok(Value::Content(body))
|
||||||
|
}
|
816
crates/tinymist-query/src/docs/mod.rs
Normal file
816
crates/tinymist-query/src/docs/mod.rs
Normal file
|
@ -0,0 +1,816 @@
|
||||||
|
//! Package management tools.
|
||||||
|
|
||||||
|
mod library;
|
||||||
|
mod tidy;
|
||||||
|
|
||||||
|
use core::fmt::{self, Write};
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use std::ops::Range;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use comemo::Track;
|
||||||
|
use ecow::{eco_vec, EcoString, EcoVec};
|
||||||
|
use indexmap::IndexSet;
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tinymist_world::base::{EntryState, ShadowApi, TaskInputs};
|
||||||
|
use tinymist_world::LspWorld;
|
||||||
|
use typst::diag::{eco_format, StrResult};
|
||||||
|
use typst::engine::Route;
|
||||||
|
use typst::eval::Tracer;
|
||||||
|
use typst::foundations::{Bytes, Value};
|
||||||
|
use typst::syntax::package::{PackageManifest, PackageSpec};
|
||||||
|
use typst::syntax::{FileId, Span, VirtualPath};
|
||||||
|
use typst::World;
|
||||||
|
|
||||||
|
use self::tidy::*;
|
||||||
|
use crate::analysis::analyze_dyn_signature;
|
||||||
|
use crate::syntax::{find_docs_of, get_non_strict_def_target, IdentRef};
|
||||||
|
use crate::ty::Ty;
|
||||||
|
use crate::upstream::truncated_doc_repr;
|
||||||
|
use crate::AnalysisContext;
|
||||||
|
|
||||||
|
/// Information about a package.
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct PackageInfo {
|
||||||
|
/// The path to the package if any.
|
||||||
|
pub path: PathBuf,
|
||||||
|
/// The namespace the package lives in.
|
||||||
|
pub namespace: EcoString,
|
||||||
|
/// The name of the package within its namespace.
|
||||||
|
pub name: EcoString,
|
||||||
|
/// The package's version.
|
||||||
|
pub version: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<(PathBuf, PackageSpec)> for PackageInfo {
|
||||||
|
fn from((path, spec): (PathBuf, PackageSpec)) -> Self {
|
||||||
|
Self {
|
||||||
|
path,
|
||||||
|
namespace: spec.namespace,
|
||||||
|
name: spec.name,
|
||||||
|
version: spec.version.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Docs about a symbol.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "kind")]
|
||||||
|
pub enum Docs {
|
||||||
|
/// Docs about a function.
|
||||||
|
#[serde(rename = "func")]
|
||||||
|
Function(TidyFuncDocs),
|
||||||
|
/// Docs about a variable.
|
||||||
|
#[serde(rename = "var")]
|
||||||
|
Variable(TidyVarDocs),
|
||||||
|
/// Docs about a module.
|
||||||
|
#[serde(rename = "module")]
|
||||||
|
Module(TidyModuleDocs),
|
||||||
|
/// Other kinds of docs.
|
||||||
|
#[serde(rename = "plain")]
|
||||||
|
Plain(EcoString),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Docs {
|
||||||
|
/// Get the markdown representation of the docs.
|
||||||
|
pub fn docs(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Self::Function(docs) => docs.docs.as_str(),
|
||||||
|
Self::Variable(docs) => docs.docs.as_str(),
|
||||||
|
Self::Module(docs) => docs.docs.as_str(),
|
||||||
|
Self::Plain(docs) => docs.as_str(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Describes a primary function signature.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct DocSignature {
|
||||||
|
/// The positional parameters.
|
||||||
|
pub pos: Vec<DocParamSpec>,
|
||||||
|
/// The named parameters.
|
||||||
|
pub named: HashMap<String, DocParamSpec>,
|
||||||
|
/// The rest parameter.
|
||||||
|
pub rest: Option<DocParamSpec>,
|
||||||
|
/// The return type.
|
||||||
|
pub ret_ty: Option<(String, String)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Describes a function parameter.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct DocParamSpec {
|
||||||
|
/// The parameter's name.
|
||||||
|
pub name: String,
|
||||||
|
/// Documentation for the parameter.
|
||||||
|
pub docs: String,
|
||||||
|
/// Inferred type of the parameter.
|
||||||
|
pub cano_type: Option<(String, String)>,
|
||||||
|
/// The parameter's default name as type.
|
||||||
|
pub type_repr: Option<EcoString>,
|
||||||
|
/// The parameter's default name as value.
|
||||||
|
pub expr: Option<EcoString>,
|
||||||
|
/// Is the parameter positional?
|
||||||
|
pub positional: bool,
|
||||||
|
/// Is the parameter named?
|
||||||
|
///
|
||||||
|
/// Can be true even if `positional` is true if the parameter can be given
|
||||||
|
/// in both variants.
|
||||||
|
pub named: bool,
|
||||||
|
/// Can the parameter be given any number of times?
|
||||||
|
pub variadic: bool,
|
||||||
|
/// Is the parameter settable with a set rule?
|
||||||
|
pub settable: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Information about a symbol.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct SymbolInfoHead {
|
||||||
|
/// The name of the symbol.
|
||||||
|
pub name: EcoString,
|
||||||
|
/// The kind of the symbol.
|
||||||
|
pub kind: EcoString,
|
||||||
|
/// The location (file, start, end) of the symbol.
|
||||||
|
pub loc: Option<(usize, usize, usize)>,
|
||||||
|
/// The raw documentation of the symbol.
|
||||||
|
pub docs: Option<String>,
|
||||||
|
/// The signature of the symbol.
|
||||||
|
pub signature: Option<DocSignature>,
|
||||||
|
/// The parsed documentation of the symbol.
|
||||||
|
pub parsed_docs: Option<Docs>,
|
||||||
|
/// The value of the symbol.
|
||||||
|
#[serde(skip)]
|
||||||
|
pub constant: Option<EcoString>,
|
||||||
|
/// The span of the symbol.
|
||||||
|
#[serde(skip)]
|
||||||
|
pub span: Option<Span>,
|
||||||
|
/// The name range of the symbol.
|
||||||
|
#[serde(skip)]
|
||||||
|
pub name_range: Option<Range<usize>>,
|
||||||
|
/// The value of the symbol.
|
||||||
|
#[serde(skip)]
|
||||||
|
pub value: Option<Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Information about a symbol.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct SymbolInfo {
|
||||||
|
/// The primary information about the symbol.
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub head: SymbolInfoHead,
|
||||||
|
/// The children of the symbol.
|
||||||
|
pub children: EcoVec<SymbolInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Information about a package.
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct PackageMeta {
|
||||||
|
/// The namespace the package lives in.
|
||||||
|
pub namespace: EcoString,
|
||||||
|
/// The name of the package within its namespace.
|
||||||
|
pub name: EcoString,
|
||||||
|
/// The package's version.
|
||||||
|
pub version: String,
|
||||||
|
/// The package's manifest information.
|
||||||
|
pub manifest: Option<PackageManifest>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Information about a package.
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct PackageMetaEnd {
|
||||||
|
packages: Vec<PackageMeta>,
|
||||||
|
files: Vec<FileMeta>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Information about a package.
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct FileMeta {
|
||||||
|
package: Option<usize>,
|
||||||
|
path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses the manifest of the package located at `package_path`.
|
||||||
|
pub fn get_manifest(world: &LspWorld, toml_id: FileId) -> StrResult<PackageManifest> {
|
||||||
|
let toml_data = world
|
||||||
|
.file(toml_id)
|
||||||
|
.map_err(|err| eco_format!("failed to read package manifest ({})", err))?;
|
||||||
|
|
||||||
|
let string = std::str::from_utf8(&toml_data)
|
||||||
|
.map_err(|err| eco_format!("package manifest is not valid UTF-8 ({})", err))?;
|
||||||
|
|
||||||
|
toml::from_str(string)
|
||||||
|
.map_err(|err| eco_format!("package manifest is malformed ({})", err.message()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List all symbols in a package.
|
||||||
|
pub fn list_symbols(world: &LspWorld, spec: &PackageInfo) -> StrResult<SymbolInfo> {
|
||||||
|
let toml_id = FileId::new(
|
||||||
|
Some(PackageSpec {
|
||||||
|
namespace: spec.namespace.clone(),
|
||||||
|
name: spec.name.clone(),
|
||||||
|
version: spec.version.parse()?,
|
||||||
|
}),
|
||||||
|
VirtualPath::new("typst.toml"),
|
||||||
|
);
|
||||||
|
let manifest = get_manifest(world, toml_id)?;
|
||||||
|
|
||||||
|
let entry_point = toml_id.join(&manifest.package.entrypoint);
|
||||||
|
let source = world.source(entry_point).map_err(|e| eco_format!("{e}"))?;
|
||||||
|
let route = Route::default();
|
||||||
|
let mut tracer = Tracer::default();
|
||||||
|
let w: &dyn typst::World = world;
|
||||||
|
|
||||||
|
let src = typst::eval::eval(w.track(), route.track(), tracer.track_mut(), &source)
|
||||||
|
.map_err(|e| eco_format!("{e:?}"))?;
|
||||||
|
|
||||||
|
let for_spec = PackageSpec {
|
||||||
|
namespace: spec.namespace.clone(),
|
||||||
|
name: spec.name.clone(),
|
||||||
|
version: spec.version.parse()?,
|
||||||
|
};
|
||||||
|
Ok(symbol(world, Some(&for_spec), "root", &Value::Module(src)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn jbase64<T: Serialize>(s: &T) -> String {
|
||||||
|
use base64::Engine;
|
||||||
|
let content = serde_json::to_string(s).unwrap();
|
||||||
|
base64::engine::general_purpose::STANDARD.encode(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unfortunately, we have only 65536 possible file ids and we cannot revoke
|
||||||
|
// them. So we share a global file id for all docs conversion.
|
||||||
|
static DOCS_CONVERT_ID: std::sync::LazyLock<Mutex<FileId>> = std::sync::LazyLock::new(|| {
|
||||||
|
Mutex::new(FileId::new(None, VirtualPath::new("__tinymist_docs__.typ")))
|
||||||
|
});
|
||||||
|
|
||||||
|
fn convert_docs(world: &LspWorld, content: &str) -> StrResult<EcoString> {
|
||||||
|
static DOCS_LIB: std::sync::LazyLock<Arc<typlite::scopes::Scopes<typlite::value::Value>>> =
|
||||||
|
std::sync::LazyLock::new(library::lib);
|
||||||
|
|
||||||
|
let conv_id = DOCS_CONVERT_ID.lock();
|
||||||
|
let entry = EntryState::new_rootless(conv_id.vpath().as_rooted_path().into()).unwrap();
|
||||||
|
let entry = entry.select_in_workspace(*conv_id);
|
||||||
|
|
||||||
|
let mut w = world.task(TaskInputs {
|
||||||
|
entry: Some(entry),
|
||||||
|
inputs: None,
|
||||||
|
});
|
||||||
|
w.map_shadow_by_id(*conv_id, Bytes::from(content.as_bytes().to_owned()))?;
|
||||||
|
// todo: bad performance
|
||||||
|
w.source_db.take_state();
|
||||||
|
|
||||||
|
let conv = typlite::Typlite::new(Arc::new(w))
|
||||||
|
.with_library(DOCS_LIB.clone())
|
||||||
|
.annotate_elements(true)
|
||||||
|
.convert()
|
||||||
|
.map_err(|e| eco_format!("failed to convert to markdown: {e}"))?;
|
||||||
|
|
||||||
|
Ok(conv)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
struct ConvertResult {
|
||||||
|
errors: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate full documents in markdown format
|
||||||
|
pub fn generate_md_docs(
|
||||||
|
ctx: &mut AnalysisContext,
|
||||||
|
world: &LspWorld,
|
||||||
|
spec: &PackageInfo,
|
||||||
|
) -> StrResult<String> {
|
||||||
|
log::info!("generate_md_docs {spec:?}");
|
||||||
|
let toml_id = FileId::new(
|
||||||
|
Some(PackageSpec {
|
||||||
|
namespace: spec.namespace.clone(),
|
||||||
|
name: spec.name.clone(),
|
||||||
|
version: spec.version.parse()?,
|
||||||
|
}),
|
||||||
|
VirtualPath::new("typst.toml"),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut md = String::new();
|
||||||
|
let sym = list_symbols(world, spec)?;
|
||||||
|
|
||||||
|
let title = format!("@{}/{}:{}", spec.namespace, spec.name, spec.version);
|
||||||
|
|
||||||
|
let mut errors = vec![];
|
||||||
|
|
||||||
|
writeln!(md, "# {title}").unwrap();
|
||||||
|
md.push('\n');
|
||||||
|
writeln!(md, "This documentation is generated locally. Please submit issues to [tinymist](https://github.com/Myriad-Dreamin/tinymist/issues) if you see **incorrect** information in it.").unwrap();
|
||||||
|
md.push('\n');
|
||||||
|
md.push('\n');
|
||||||
|
|
||||||
|
let manifest = get_manifest(world, toml_id)?;
|
||||||
|
|
||||||
|
let meta = PackageMeta {
|
||||||
|
namespace: spec.namespace.clone(),
|
||||||
|
name: spec.name.clone(),
|
||||||
|
version: spec.version.to_string(),
|
||||||
|
manifest: Some(manifest),
|
||||||
|
};
|
||||||
|
let package_meta = jbase64(&meta);
|
||||||
|
let _ = writeln!(md, "<!-- begin:package {package_meta} -->");
|
||||||
|
|
||||||
|
let mut key = 0;
|
||||||
|
|
||||||
|
let mut modules_to_generate = vec![(EcoString::new(), sym.head.name.clone(), sym)];
|
||||||
|
let mut generated_modules = HashSet::new();
|
||||||
|
let mut file_ids = IndexSet::new();
|
||||||
|
|
||||||
|
while !modules_to_generate.is_empty() {
|
||||||
|
for (prefix, parent_ident, sym) in std::mem::take(&mut modules_to_generate) {
|
||||||
|
// parent_ident, symbols
|
||||||
|
let symbols = sym.children;
|
||||||
|
if !prefix.is_empty() {
|
||||||
|
let _ = writeln!(md, "---\n## Module: {prefix}");
|
||||||
|
}
|
||||||
|
|
||||||
|
let module_val = sym.head.value.as_ref().unwrap();
|
||||||
|
let module = match module_val {
|
||||||
|
Value::Module(m) => m,
|
||||||
|
_ => todo!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let fid = module.file_id();
|
||||||
|
let type_info = None.or_else(|| {
|
||||||
|
let file_id = fid?;
|
||||||
|
let src = world.source(file_id).ok()?;
|
||||||
|
let def_use = ctx.def_use(src.clone())?;
|
||||||
|
let ty_chck = ctx.type_check(src)?;
|
||||||
|
Some((def_use, ty_chck))
|
||||||
|
});
|
||||||
|
let type_info = type_info.as_ref();
|
||||||
|
|
||||||
|
let persist_fid = fid.map(|f| file_ids.insert_full(f).0);
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct ModuleInfo {
|
||||||
|
prefix: EcoString,
|
||||||
|
name: EcoString,
|
||||||
|
loc: Option<usize>,
|
||||||
|
parent_ident: EcoString,
|
||||||
|
}
|
||||||
|
let m = jbase64(&ModuleInfo {
|
||||||
|
prefix: prefix.clone(),
|
||||||
|
name: sym.head.name.clone(),
|
||||||
|
loc: persist_fid,
|
||||||
|
parent_ident: parent_ident.clone(),
|
||||||
|
});
|
||||||
|
let _ = writeln!(md, "<!-- begin:module {parent_ident} {m} -->");
|
||||||
|
|
||||||
|
for mut sym in symbols {
|
||||||
|
let span = sym.head.span.and_then(|v| {
|
||||||
|
v.id().and_then(|e| {
|
||||||
|
let fid = file_ids.insert_full(e).0;
|
||||||
|
let src = world.source(e).ok()?;
|
||||||
|
let rng = src.range(v)?;
|
||||||
|
Some((fid, rng.start, rng.end))
|
||||||
|
})
|
||||||
|
});
|
||||||
|
sym.head.loc = span;
|
||||||
|
|
||||||
|
let mut convert_err = None;
|
||||||
|
if let Some(docs) = &sym.head.docs {
|
||||||
|
match convert_docs(world, docs) {
|
||||||
|
Ok(content) => {
|
||||||
|
let docs = match sym.head.kind.as_str() {
|
||||||
|
"function" => {
|
||||||
|
let t = identify_tidy_func_docs(&content).ok();
|
||||||
|
t.map(Docs::Function).unwrap_or(Docs::Plain(content))
|
||||||
|
}
|
||||||
|
"variable" => {
|
||||||
|
let t = identify_tidy_var_docs(&content).ok();
|
||||||
|
t.map(Docs::Variable).unwrap_or(Docs::Plain(content))
|
||||||
|
}
|
||||||
|
"module" => {
|
||||||
|
let t = identify_tidy_module_docs(&content).ok();
|
||||||
|
t.map(Docs::Module).unwrap_or(Docs::Plain(content))
|
||||||
|
}
|
||||||
|
_ => Docs::Plain(content),
|
||||||
|
};
|
||||||
|
|
||||||
|
sym.head.parsed_docs = Some(docs.clone());
|
||||||
|
sym.head.docs = None;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
let err = format!("failed to convert docs in {title}: {e}").replace(
|
||||||
|
"-->", "—>", // avoid markdown comment
|
||||||
|
);
|
||||||
|
log::error!("{err}");
|
||||||
|
convert_err = Some(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let signature =
|
||||||
|
match &sym.head.parsed_docs {
|
||||||
|
Some(Docs::Function(TidyFuncDocs {
|
||||||
|
params, return_ty, ..
|
||||||
|
})) => sym.head.value.clone().and_then(|e| {
|
||||||
|
let func = match e {
|
||||||
|
Value::Func(f) => f,
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
let sig = analyze_dyn_signature(ctx, func.clone());
|
||||||
|
let type_sig = type_info.and_then(|(def_use, ty_chk)| {
|
||||||
|
let def_fid = func.span().id()?;
|
||||||
|
let def_ident = IdentRef {
|
||||||
|
name: sym.head.name.clone(),
|
||||||
|
range: sym.head.name_range.clone()?,
|
||||||
|
};
|
||||||
|
let (def_id, _) = def_use.get_def(def_fid, &def_ident)?;
|
||||||
|
ty_chk.type_of_def(def_id)
|
||||||
|
});
|
||||||
|
let type_sig = type_sig.and_then(|type_sig| type_sig.sig_repr(true));
|
||||||
|
|
||||||
|
let pos_in = sig.primary().pos.iter().enumerate().map(|(i, pos)| {
|
||||||
|
(pos, type_sig.as_ref().and_then(|sig| sig.pos(i)))
|
||||||
|
});
|
||||||
|
let named_in = sig
|
||||||
|
.primary()
|
||||||
|
.named
|
||||||
|
.iter()
|
||||||
|
.map(|x| (x, type_sig.as_ref().and_then(|sig| sig.named(x.0))));
|
||||||
|
let rest_in =
|
||||||
|
sig.primary().rest.as_ref().map(|x| {
|
||||||
|
(x, type_sig.as_ref().and_then(|sig| sig.rest_param()))
|
||||||
|
});
|
||||||
|
|
||||||
|
let ret_in = type_sig
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|sig| sig.body.as_ref())
|
||||||
|
.or_else(|| sig.primary().ret_ty.as_ref());
|
||||||
|
|
||||||
|
let doc_ty = |ty: Option<&Ty>| {
|
||||||
|
ty.and_then(|ty| ty.describe().map(|e| (e, format!("{ty:?}"))))
|
||||||
|
};
|
||||||
|
|
||||||
|
let _ = params;
|
||||||
|
let _ = return_ty;
|
||||||
|
|
||||||
|
let pos = pos_in
|
||||||
|
.map(|(param, ty)| DocParamSpec {
|
||||||
|
name: param.name.as_ref().to_owned(),
|
||||||
|
docs: param.docs.as_ref().to_owned(),
|
||||||
|
cano_type: doc_ty(ty),
|
||||||
|
type_repr: param.type_repr.clone(),
|
||||||
|
expr: param.expr.clone(),
|
||||||
|
positional: param.positional,
|
||||||
|
named: param.named,
|
||||||
|
variadic: param.variadic,
|
||||||
|
settable: param.settable,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let named = named_in
|
||||||
|
.map(|((name, param), ty)| {
|
||||||
|
(
|
||||||
|
name.as_ref().to_owned(),
|
||||||
|
DocParamSpec {
|
||||||
|
name: param.name.as_ref().to_owned(),
|
||||||
|
docs: param.docs.as_ref().to_owned(),
|
||||||
|
cano_type: doc_ty(ty),
|
||||||
|
type_repr: param.type_repr.clone(),
|
||||||
|
expr: param.expr.clone(),
|
||||||
|
positional: param.positional,
|
||||||
|
named: param.named,
|
||||||
|
variadic: param.variadic,
|
||||||
|
settable: param.settable,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let rest = rest_in.map(|(param, ty)| DocParamSpec {
|
||||||
|
name: param.name.as_ref().to_owned(),
|
||||||
|
docs: param.docs.as_ref().to_owned(),
|
||||||
|
cano_type: doc_ty(ty),
|
||||||
|
type_repr: param.type_repr.clone(),
|
||||||
|
expr: param.expr.clone(),
|
||||||
|
positional: param.positional,
|
||||||
|
named: param.named,
|
||||||
|
variadic: param.variadic,
|
||||||
|
settable: param.settable,
|
||||||
|
});
|
||||||
|
|
||||||
|
let ret_ty = doc_ty(ret_in);
|
||||||
|
|
||||||
|
Some(DocSignature {
|
||||||
|
pos,
|
||||||
|
named,
|
||||||
|
rest,
|
||||||
|
ret_ty,
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
sym.head.signature = signature;
|
||||||
|
|
||||||
|
let _ = writeln!(md, "### {}: {}", sym.head.kind, sym.head.name);
|
||||||
|
|
||||||
|
let ident = eco_format!("symbol-{}-{}-{key}", sym.head.kind, sym.head.name);
|
||||||
|
key += 1;
|
||||||
|
let head = jbase64(&sym.head);
|
||||||
|
let _ = writeln!(md, "<!-- begin:symbol {ident} {head} -->");
|
||||||
|
|
||||||
|
if let Some(sig) = &sym.head.signature {
|
||||||
|
let _ = writeln!(md, "<!-- begin:sig -->");
|
||||||
|
let _ = writeln!(md, "```typc");
|
||||||
|
let _ = writeln!(
|
||||||
|
md,
|
||||||
|
"let {name}({params});",
|
||||||
|
name = sym.head.name,
|
||||||
|
params = ParamTooltip(sig)
|
||||||
|
);
|
||||||
|
let _ = writeln!(md, "```");
|
||||||
|
let _ = writeln!(md, "<!-- end:sig -->");
|
||||||
|
}
|
||||||
|
|
||||||
|
match (&sym.head.parsed_docs, convert_err) {
|
||||||
|
(_, Some(err)) => {
|
||||||
|
let err = format!("failed to convert docs in {title}: {err}").replace(
|
||||||
|
"-->", "—>", // avoid markdown comment
|
||||||
|
);
|
||||||
|
let _ = writeln!(md, "<!-- convert-error: {err} -->");
|
||||||
|
errors.push(err);
|
||||||
|
}
|
||||||
|
(Some(docs), _) => {
|
||||||
|
let _ = writeln!(md, "{}", remove_list_annotations(docs.docs()));
|
||||||
|
if let Docs::Function(f) = docs {
|
||||||
|
for param in &f.params {
|
||||||
|
let _ = writeln!(md, "<!-- begin:param {} -->", param.name);
|
||||||
|
let _ = writeln!(
|
||||||
|
md,
|
||||||
|
"#### {} ({})\n<!-- begin:param-doc {} -->\n{}\n<!-- end:param-doc {} -->",
|
||||||
|
param.name, param.types, param.name, param.docs, param.name
|
||||||
|
);
|
||||||
|
let _ = writeln!(md, "<!-- end:param -->");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(None, None) => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(docs) = &sym.head.docs {
|
||||||
|
let contains_code = docs.contains("```");
|
||||||
|
if contains_code {
|
||||||
|
let _ = writeln!(md, "`````typ");
|
||||||
|
}
|
||||||
|
let _ = writeln!(md, "{docs}");
|
||||||
|
if contains_code {
|
||||||
|
let _ = writeln!(md, "`````");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !sym.children.is_empty() {
|
||||||
|
let mut full_path = prefix.clone();
|
||||||
|
if !full_path.is_empty() {
|
||||||
|
full_path.push_str(".");
|
||||||
|
}
|
||||||
|
full_path.push_str(&sym.head.name);
|
||||||
|
let link = format!("Module-{full_path}").replace(".", "-");
|
||||||
|
let _ = writeln!(md, "[Module Docs](#{link})\n");
|
||||||
|
|
||||||
|
if generated_modules.insert(full_path.clone()) {
|
||||||
|
modules_to_generate.push((full_path, ident.clone(), sym));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = writeln!(md, "<!-- end:symbol {ident} -->");
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = writeln!(md, "<!-- end:module {parent_ident} -->");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = ConvertResult { errors };
|
||||||
|
let err = jbase64(&res);
|
||||||
|
let _ = writeln!(md, "<!-- begin:errors {err} -->");
|
||||||
|
let _ = writeln!(md, "## Errors");
|
||||||
|
for e in res.errors {
|
||||||
|
let _ = writeln!(md, "- {e}");
|
||||||
|
}
|
||||||
|
let _ = writeln!(md, "<!-- end:errors -->");
|
||||||
|
|
||||||
|
let mut packages = IndexSet::new();
|
||||||
|
|
||||||
|
let files = file_ids
|
||||||
|
.into_iter()
|
||||||
|
.map(|e| {
|
||||||
|
let pkg = e.package().map(|e| packages.insert_full(e.clone()).0);
|
||||||
|
|
||||||
|
FileMeta {
|
||||||
|
package: pkg,
|
||||||
|
path: e.vpath().as_rootless_path().to_owned(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let packages = packages
|
||||||
|
.into_iter()
|
||||||
|
.map(|e| PackageMeta {
|
||||||
|
namespace: e.namespace.clone(),
|
||||||
|
name: e.name.clone(),
|
||||||
|
version: e.version.to_string(),
|
||||||
|
manifest: None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let meta = PackageMetaEnd { packages, files };
|
||||||
|
let package_meta = jbase64(&meta);
|
||||||
|
let _ = writeln!(md, "<!-- end:package {package_meta} -->");
|
||||||
|
|
||||||
|
Ok(md)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn kind_of(val: &Value) -> EcoString {
|
||||||
|
match val {
|
||||||
|
Value::Module(_) => "module",
|
||||||
|
Value::Type(_) => "struct",
|
||||||
|
Value::Func(_) => "function",
|
||||||
|
Value::Label(_) => "reference",
|
||||||
|
_ => "constant",
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn symbol(world: &LspWorld, for_spec: Option<&PackageSpec>, key: &str, val: &Value) -> SymbolInfo {
|
||||||
|
let children = match val {
|
||||||
|
Value::Module(module) => {
|
||||||
|
// only generate docs for the same package
|
||||||
|
if module.file_id().map_or(true, |e| e.package() != for_spec) {
|
||||||
|
eco_vec![]
|
||||||
|
} else {
|
||||||
|
let symbols = module.scope().iter();
|
||||||
|
symbols
|
||||||
|
.map(|(k, v)| symbol(world, for_spec, k, v))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => eco_vec![],
|
||||||
|
};
|
||||||
|
SymbolInfo {
|
||||||
|
head: create_head(world, key, val),
|
||||||
|
children,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_head(world: &LspWorld, k: &str, v: &Value) -> SymbolInfoHead {
|
||||||
|
let kind = kind_of(v);
|
||||||
|
let (docs, name_range, span) = match v {
|
||||||
|
Value::Func(f) => {
|
||||||
|
let mut span = None;
|
||||||
|
let mut name_range = None;
|
||||||
|
let docs = None.or_else(|| {
|
||||||
|
let source = world.source(f.span().id()?).ok()?;
|
||||||
|
let node = source.find(f.span())?;
|
||||||
|
log::debug!("node: {k} -> {:?}", node.parent());
|
||||||
|
// use parent of params, todo: reliable way to get the def target
|
||||||
|
let def = get_non_strict_def_target(node.parent()?.clone())?;
|
||||||
|
span = Some(def.node().span());
|
||||||
|
name_range = def.name_range();
|
||||||
|
|
||||||
|
find_docs_of(&source, def)
|
||||||
|
});
|
||||||
|
|
||||||
|
(docs, name_range, span.or(Some(f.span())))
|
||||||
|
}
|
||||||
|
_ => (None, None, None),
|
||||||
|
};
|
||||||
|
|
||||||
|
SymbolInfoHead {
|
||||||
|
name: k.to_string().into(),
|
||||||
|
kind,
|
||||||
|
loc: None,
|
||||||
|
constant: None.or_else(|| match v {
|
||||||
|
Value::Func(_) => None,
|
||||||
|
t => Some(truncated_doc_repr(t)),
|
||||||
|
}),
|
||||||
|
signature: None,
|
||||||
|
parsed_docs: None,
|
||||||
|
docs,
|
||||||
|
name_range,
|
||||||
|
span,
|
||||||
|
value: Some(v.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo: hover with `with_stack`
|
||||||
|
struct ParamTooltip<'a>(&'a DocSignature);
|
||||||
|
|
||||||
|
impl<'a> fmt::Display for ParamTooltip<'a> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let mut is_first = true;
|
||||||
|
let mut write_sep = |f: &mut fmt::Formatter<'_>| {
|
||||||
|
if is_first {
|
||||||
|
is_first = false;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
f.write_str(", ")
|
||||||
|
};
|
||||||
|
|
||||||
|
let primary_sig = self.0;
|
||||||
|
|
||||||
|
for p in &primary_sig.pos {
|
||||||
|
write_sep(f)?;
|
||||||
|
write!(f, "{}", p.name)?;
|
||||||
|
}
|
||||||
|
if let Some(rest) = &primary_sig.rest {
|
||||||
|
write_sep(f)?;
|
||||||
|
write!(f, "{}", rest.name)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !primary_sig.named.is_empty() {
|
||||||
|
let mut name_prints = vec![];
|
||||||
|
for v in primary_sig.named.values() {
|
||||||
|
name_prints.push((v.name.clone(), v.type_repr.clone()))
|
||||||
|
}
|
||||||
|
name_prints.sort();
|
||||||
|
for (k, v) in name_prints {
|
||||||
|
write_sep(f)?;
|
||||||
|
let v = v.as_deref().unwrap_or("any");
|
||||||
|
let mut v = v.trim();
|
||||||
|
if v.starts_with('{') && v.ends_with('}') && v.len() > 30 {
|
||||||
|
v = "{ ... }"
|
||||||
|
}
|
||||||
|
if v.starts_with('`') && v.ends_with('`') && v.len() > 30 {
|
||||||
|
v = "raw"
|
||||||
|
}
|
||||||
|
if v.starts_with('[') && v.ends_with(']') && v.len() > 30 {
|
||||||
|
v = "content"
|
||||||
|
}
|
||||||
|
write!(f, "{k}: {v}")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_list_annotations(s: &str) -> String {
|
||||||
|
let s = s.to_string();
|
||||||
|
static REG: std::sync::LazyLock<regex::Regex> = std::sync::LazyLock::new(|| {
|
||||||
|
regex::Regex::new(r"<!-- typlite:(?:begin|end):[\w\-]+ \d+ -->").unwrap()
|
||||||
|
});
|
||||||
|
REG.replace_all(&s, "").to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use reflexo_typst::package::{PackageRegistry, PackageSpec};
|
||||||
|
|
||||||
|
use super::{generate_md_docs, PackageInfo};
|
||||||
|
use crate::tests::*;
|
||||||
|
|
||||||
|
fn test(pkg: PackageSpec) {
|
||||||
|
run_with_sources("", |verse: &mut LspUniverse, p| {
|
||||||
|
let w = verse.snapshot();
|
||||||
|
let path = verse.registry.resolve(&pkg).unwrap();
|
||||||
|
let pi = PackageInfo {
|
||||||
|
path: path.as_ref().to_owned(),
|
||||||
|
namespace: pkg.namespace,
|
||||||
|
name: pkg.name,
|
||||||
|
version: pkg.version.to_string(),
|
||||||
|
};
|
||||||
|
run_with_ctx(verse, p, &|a, _p| {
|
||||||
|
let d = generate_md_docs(a, &w, &pi).unwrap();
|
||||||
|
let dest = format!(
|
||||||
|
"../../target/{}-{}-{}.md",
|
||||||
|
pi.namespace, pi.name, pi.version
|
||||||
|
);
|
||||||
|
std::fs::write(dest, d).unwrap();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tidy() {
|
||||||
|
test(PackageSpec {
|
||||||
|
namespace: "preview".into(),
|
||||||
|
name: "tidy".into(),
|
||||||
|
version: "0.3.0".parse().unwrap(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn touying() {
|
||||||
|
test(PackageSpec {
|
||||||
|
namespace: "preview".into(),
|
||||||
|
name: "touying".into(),
|
||||||
|
version: "0.5.2".parse().unwrap(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cetz() {
|
||||||
|
test(PackageSpec {
|
||||||
|
namespace: "preview".into(),
|
||||||
|
name: "cetz".into(),
|
||||||
|
version: "0.2.2".parse().unwrap(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
359
crates/tinymist-query/src/docs/tidy.rs
Normal file
359
crates/tinymist-query/src/docs/tidy.rs
Normal file
|
@ -0,0 +1,359 @@
|
||||||
|
use itertools::Itertools;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use typst::diag::StrResult;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct TidyParamDocs {
|
||||||
|
pub name: String,
|
||||||
|
pub docs: String,
|
||||||
|
pub types: String,
|
||||||
|
pub default: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct TidyFuncDocs {
|
||||||
|
pub docs: String,
|
||||||
|
pub return_ty: Option<String>,
|
||||||
|
pub params: Vec<TidyParamDocs>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct TidyVarDocs {
|
||||||
|
pub docs: String,
|
||||||
|
pub return_ty: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct TidyModuleDocs {
|
||||||
|
pub docs: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn identify_tidy_func_docs(converted: &str) -> StrResult<TidyFuncDocs> {
|
||||||
|
let lines = converted.lines().collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let mut matching_return_ty = true;
|
||||||
|
let mut buf = vec![];
|
||||||
|
let mut params = vec![];
|
||||||
|
let mut return_ty = None;
|
||||||
|
let mut break_line = None;
|
||||||
|
|
||||||
|
let mut i = lines.len();
|
||||||
|
'search: loop {
|
||||||
|
if i == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
i -= 1;
|
||||||
|
|
||||||
|
let line = lines[i];
|
||||||
|
if line.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if matching_return_ty {
|
||||||
|
matching_return_ty = false;
|
||||||
|
let Some(w) = line.trim_start().strip_prefix("->") else {
|
||||||
|
// break_line = Some(i);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
return_ty = Some(w.trim().to_string());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(mut line) = line
|
||||||
|
.trim_end()
|
||||||
|
.strip_suffix("<!-- typlite:end:list-item 0 -->")
|
||||||
|
else {
|
||||||
|
break_line = Some(i + 1);
|
||||||
|
break 'search;
|
||||||
|
};
|
||||||
|
let mut current_line_no = i;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// <!-- typlite:begin:list-item -->
|
||||||
|
let t = line
|
||||||
|
.trim_start()
|
||||||
|
.strip_prefix("- ")
|
||||||
|
.and_then(|t| t.trim().strip_prefix("<!-- typlite:begin:list-item 0 -->"));
|
||||||
|
|
||||||
|
let line_content = match t {
|
||||||
|
Some(t) => {
|
||||||
|
buf.push(t);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
None => line,
|
||||||
|
};
|
||||||
|
|
||||||
|
buf.push(line_content);
|
||||||
|
|
||||||
|
if current_line_no == 0 {
|
||||||
|
break_line = Some(i + 1);
|
||||||
|
break 'search;
|
||||||
|
}
|
||||||
|
current_line_no -= 1;
|
||||||
|
line = lines[current_line_no];
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut buf = std::mem::take(&mut buf);
|
||||||
|
buf.reverse();
|
||||||
|
|
||||||
|
let Some(first_line) = buf.first_mut() else {
|
||||||
|
break_line = Some(i + 1);
|
||||||
|
break 'search;
|
||||||
|
};
|
||||||
|
*first_line = first_line.trim();
|
||||||
|
|
||||||
|
let Some(param_line) = None.or_else(|| {
|
||||||
|
let (param_name, rest) = first_line.split_once(" ")?;
|
||||||
|
let (type_content, rest) = match_brace(rest.trim_start().strip_prefix("(")?)?;
|
||||||
|
let (_, rest) = rest.split_once(":")?;
|
||||||
|
*first_line = rest.trim();
|
||||||
|
Some((param_name.into(), type_content.into()))
|
||||||
|
}) else {
|
||||||
|
break_line = Some(i + 1);
|
||||||
|
break 'search;
|
||||||
|
};
|
||||||
|
|
||||||
|
i = current_line_no;
|
||||||
|
params.push(TidyParamDocs {
|
||||||
|
name: param_line.0,
|
||||||
|
types: param_line.1,
|
||||||
|
default: None,
|
||||||
|
docs: buf.into_iter().join("\n"),
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let docs = match break_line {
|
||||||
|
Some(line_no) => (lines[..line_no]).iter().copied().join("\n"),
|
||||||
|
None => converted.to_owned(),
|
||||||
|
};
|
||||||
|
|
||||||
|
params.reverse();
|
||||||
|
Ok(TidyFuncDocs {
|
||||||
|
docs,
|
||||||
|
return_ty,
|
||||||
|
params,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn identify_tidy_var_docs(converted: &str) -> StrResult<TidyVarDocs> {
|
||||||
|
let lines = converted.lines().collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let mut return_ty = None;
|
||||||
|
let mut break_line = None;
|
||||||
|
|
||||||
|
let mut i = lines.len();
|
||||||
|
loop {
|
||||||
|
if i == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
i -= 1;
|
||||||
|
|
||||||
|
let line = lines[i];
|
||||||
|
if line.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(w) = line.trim_start().strip_prefix("->") else {
|
||||||
|
break_line = Some(i + 1);
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
|
||||||
|
return_ty = Some(w.trim().to_string());
|
||||||
|
break_line = Some(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let docs = match break_line {
|
||||||
|
Some(line_no) => (lines[..line_no]).iter().copied().join("\n"),
|
||||||
|
None => converted.to_owned(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(TidyVarDocs { docs, return_ty })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn identify_tidy_module_docs(converted: &str) -> StrResult<TidyModuleDocs> {
|
||||||
|
Ok(TidyModuleDocs {
|
||||||
|
docs: converted.to_owned(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn match_brace(trim_start: &str) -> Option<(&str, &str)> {
|
||||||
|
let mut brace_count = 1;
|
||||||
|
let mut end = 0;
|
||||||
|
for (i, c) in trim_start.char_indices() {
|
||||||
|
match c {
|
||||||
|
'(' => brace_count += 1,
|
||||||
|
')' => brace_count -= 1,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if brace_count == 0 {
|
||||||
|
end = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if brace_count != 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (type_content, rest) = trim_start.split_at(end);
|
||||||
|
Some((type_content, rest))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::fmt::Write;
|
||||||
|
|
||||||
|
use super::TidyParamDocs;
|
||||||
|
|
||||||
|
fn func(s: &str) -> String {
|
||||||
|
let f = super::identify_tidy_func_docs(s).unwrap();
|
||||||
|
let mut res = format!(">> docs:\n{}\n<< docs", f.docs);
|
||||||
|
if let Some(t) = f.return_ty {
|
||||||
|
res.push_str(&format!("\n>>return\n{t}\n<<return"));
|
||||||
|
}
|
||||||
|
for TidyParamDocs {
|
||||||
|
name,
|
||||||
|
types,
|
||||||
|
docs,
|
||||||
|
default: _,
|
||||||
|
} in f.params
|
||||||
|
{
|
||||||
|
let _ = write!(res, "\n>>arg {name}: {types}\n{docs}\n<< arg");
|
||||||
|
}
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
fn var(s: &str) -> String {
|
||||||
|
let f = super::identify_tidy_var_docs(s).unwrap();
|
||||||
|
let mut res = format!(">> docs:\n{}\n<< docs", f.docs);
|
||||||
|
if let Some(t) = f.return_ty {
|
||||||
|
res.push_str(&format!("\n>>return\n{t}\n<<return"));
|
||||||
|
}
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_identify_tidy_docs() {
|
||||||
|
insta::assert_snapshot!(func(r###"These again are dictionaries with the keys
|
||||||
|
- <!-- typlite:begin:list-item 0 -->`description` (optional): The description for the argument.<!-- typlite:end:list-item 0 -->
|
||||||
|
- <!-- typlite:begin:list-item 0 -->`types` (optional): A list of accepted argument types.<!-- typlite:end:list-item 0 -->
|
||||||
|
- <!-- typlite:begin:list-item 0 -->`default` (optional): Default value for this argument.<!-- typlite:end:list-item 0 -->
|
||||||
|
|
||||||
|
See @@show-module() for outputting the results of this function.
|
||||||
|
|
||||||
|
- <!-- typlite:begin:list-item 0 -->content (string): Content of `.typ` file to analyze for docstrings.<!-- typlite:end:list-item 0 -->
|
||||||
|
- <!-- typlite:begin:list-item 0 -->name (string): The name for the module.<!-- typlite:end:list-item 0 -->
|
||||||
|
- <!-- typlite:begin:list-item 0 -->label-prefix (auto, string): The label-prefix for internal function
|
||||||
|
references. If `auto`, the label-prefix name will be the module name.<!-- typlite:end:list-item 0 -->
|
||||||
|
- <!-- typlite:begin:list-item 0 -->require-all-parameters (boolean): Require that all parameters of a
|
||||||
|
functions are documented and fail if some are not.<!-- typlite:end:list-item 0 -->
|
||||||
|
- <!-- typlite:begin:list-item 0 -->scope (dictionary): A dictionary of definitions that are then available
|
||||||
|
in all function and parameter descriptions.<!-- typlite:end:list-item 0 -->
|
||||||
|
- <!-- typlite:begin:list-item 0 -->preamble (string): Code to prepend to all code snippets shown with `#example()`.
|
||||||
|
This can for instance be used to import something from the scope.<!-- typlite:end:list-item 0 -->
|
||||||
|
-> string"###), @r###"
|
||||||
|
>> docs:
|
||||||
|
These again are dictionaries with the keys
|
||||||
|
- <!-- typlite:begin:list-item 0 -->`description` (optional): The description for the argument.<!-- typlite:end:list-item 0 -->
|
||||||
|
- <!-- typlite:begin:list-item 0 -->`types` (optional): A list of accepted argument types.<!-- typlite:end:list-item 0 -->
|
||||||
|
- <!-- typlite:begin:list-item 0 -->`default` (optional): Default value for this argument.<!-- typlite:end:list-item 0 -->
|
||||||
|
|
||||||
|
See @@show-module() for outputting the results of this function.
|
||||||
|
<< docs
|
||||||
|
>>return
|
||||||
|
string
|
||||||
|
<<return
|
||||||
|
>>arg content: string
|
||||||
|
Content of `.typ` file to analyze for docstrings.
|
||||||
|
<< arg
|
||||||
|
>>arg name: string
|
||||||
|
The name for the module.
|
||||||
|
<< arg
|
||||||
|
>>arg label-prefix: auto, string
|
||||||
|
The label-prefix for internal function
|
||||||
|
references. If `auto`, the label-prefix name will be the module name.
|
||||||
|
<< arg
|
||||||
|
>>arg require-all-parameters: boolean
|
||||||
|
Require that all parameters of a
|
||||||
|
functions are documented and fail if some are not.
|
||||||
|
<< arg
|
||||||
|
>>arg scope: dictionary
|
||||||
|
A dictionary of definitions that are then available
|
||||||
|
in all function and parameter descriptions.
|
||||||
|
<< arg
|
||||||
|
>>arg preamble: string
|
||||||
|
Code to prepend to all code snippets shown with `#example()`.
|
||||||
|
This can for instance be used to import something from the scope.
|
||||||
|
<< arg
|
||||||
|
"###);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_identify_tidy_docs_nested() {
|
||||||
|
insta::assert_snapshot!(func(r###"These again are dictionaries with the keys
|
||||||
|
- <!-- typlite:begin:list-item 0 -->`description` (optional): The description for the argument.<!-- typlite:end:list-item 0 -->
|
||||||
|
|
||||||
|
See @@show-module() for outputting the results of this function.
|
||||||
|
|
||||||
|
- <!-- typlite:begin:list-item 0 -->name (string): The name for the module.<!-- typlite:end:list-item 0 -->
|
||||||
|
- <!-- typlite:begin:list-item 0 -->label-prefix (auto, string): The label-prefix for internal function
|
||||||
|
references. If `auto`, the label-prefix name will be the module name.
|
||||||
|
- <!-- typlite:begin:list-item 1 -->nested something<!-- typlite:end:list-item 1 -->
|
||||||
|
- <!-- typlite:begin:list-item 1 -->nested something 2<!-- typlite:end:list-item 1 --><!-- typlite:end:list-item 0 -->
|
||||||
|
-> string"###), @r###"
|
||||||
|
>> docs:
|
||||||
|
These again are dictionaries with the keys
|
||||||
|
- <!-- typlite:begin:list-item 0 -->`description` (optional): The description for the argument.<!-- typlite:end:list-item 0 -->
|
||||||
|
|
||||||
|
See @@show-module() for outputting the results of this function.
|
||||||
|
<< docs
|
||||||
|
>>return
|
||||||
|
string
|
||||||
|
<<return
|
||||||
|
>>arg name: string
|
||||||
|
The name for the module.
|
||||||
|
<< arg
|
||||||
|
>>arg label-prefix: auto, string
|
||||||
|
The label-prefix for internal function
|
||||||
|
references. If `auto`, the label-prefix name will be the module name.
|
||||||
|
- <!-- typlite:begin:list-item 1 -->nested something<!-- typlite:end:list-item 1 -->
|
||||||
|
- <!-- typlite:begin:list-item 1 -->nested something 2<!-- typlite:end:list-item 1 -->
|
||||||
|
<< arg
|
||||||
|
"###);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_identify_tidy_docs3() {
|
||||||
|
insta::assert_snapshot!(var(r###"See @@show-module() for outputting the results of this function.
|
||||||
|
-> string"###), @r###"
|
||||||
|
>> docs:
|
||||||
|
See @@show-module() for outputting the results of this function.
|
||||||
|
<< docs
|
||||||
|
>>return
|
||||||
|
string
|
||||||
|
<<return
|
||||||
|
"###);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_identify_tidy_docs4() {
|
||||||
|
insta::assert_snapshot!(var(r###"
|
||||||
|
- <!-- typlite:begin:list-item 0 -->name (string): The name for the module.<!-- typlite:end:list-item 0 -->
|
||||||
|
-> string"###), @r###"
|
||||||
|
>> docs:
|
||||||
|
|
||||||
|
- <!-- typlite:begin:list-item 0 -->name (string): The name for the module.<!-- typlite:end:list-item 0 -->
|
||||||
|
<< docs
|
||||||
|
>>return
|
||||||
|
string
|
||||||
|
<<return
|
||||||
|
"###);
|
||||||
|
}
|
||||||
|
}
|
|
@ -49,7 +49,7 @@ fn filter_document_symbols(
|
||||||
let rng = typst_to_lsp::range(e.info.range.clone(), source, position_encoding);
|
let rng = typst_to_lsp::range(e.info.range.clone(), source, position_encoding);
|
||||||
|
|
||||||
DocumentSymbol {
|
DocumentSymbol {
|
||||||
name: e.info.name.clone(),
|
name: e.info.name.to_string(),
|
||||||
detail: None,
|
detail: None,
|
||||||
kind: e.info.kind.clone().try_into().unwrap(),
|
kind: e.info.kind.clone().try_into().unwrap(),
|
||||||
tags: None,
|
tags: None,
|
||||||
|
|
|
@ -113,7 +113,7 @@ fn calc_folding_range(
|
||||||
end_line: rng.end.line,
|
end_line: rng.end.line,
|
||||||
end_character: Some(rng.end.character),
|
end_character: Some(rng.end.character),
|
||||||
kind: None,
|
kind: None,
|
||||||
collapsed_text: Some(e.info.name.clone()),
|
collapsed_text: Some(e.info.name.to_string()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let next_start = if is_not_last_range {
|
let next_start = if is_not_last_range {
|
||||||
|
|
|
@ -9,14 +9,13 @@
|
||||||
|
|
||||||
mod adt;
|
mod adt;
|
||||||
pub mod analysis;
|
pub mod analysis;
|
||||||
|
pub mod docs;
|
||||||
pub mod syntax;
|
pub mod syntax;
|
||||||
pub mod ty;
|
pub mod ty;
|
||||||
mod upstream;
|
mod upstream;
|
||||||
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
pub use analysis::AnalysisContext;
|
pub use analysis::AnalysisContext;
|
||||||
use typst::{model::Document as TypstDocument, syntax::Source};
|
pub use upstream::with_vm;
|
||||||
|
|
||||||
mod diagnostics;
|
mod diagnostics;
|
||||||
pub use diagnostics::*;
|
pub use diagnostics::*;
|
||||||
|
@ -80,6 +79,10 @@ pub use lsp_features::*;
|
||||||
|
|
||||||
mod prelude;
|
mod prelude;
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use typst::{model::Document as TypstDocument, syntax::Source};
|
||||||
|
|
||||||
/// The physical position in a document.
|
/// The physical position in a document.
|
||||||
pub type FramePosition = typst::layout::Position;
|
pub type FramePosition = typst::layout::Position;
|
||||||
|
|
||||||
|
|
|
@ -20,13 +20,12 @@ pub use lsp_types::{
|
||||||
};
|
};
|
||||||
pub use reflexo::vector::ir::DefId;
|
pub use reflexo::vector::ir::DefId;
|
||||||
pub use serde_json::Value as JsonValue;
|
pub use serde_json::Value as JsonValue;
|
||||||
pub use typst::diag::{EcoString, FileError, FileResult, Tracepoint};
|
pub use typst::diag::{EcoString, FileResult, Tracepoint};
|
||||||
pub use typst::foundations::{Func, Value};
|
pub use typst::foundations::{Func, Value};
|
||||||
pub use typst::syntax::FileId as TypstFileId;
|
pub use typst::syntax::FileId as TypstFileId;
|
||||||
pub use typst::syntax::{
|
pub use typst::syntax::{
|
||||||
ast::{self, AstNode},
|
ast::{self, AstNode},
|
||||||
package::{PackageManifest, PackageSpec},
|
LinkedNode, Source, Spanned, SyntaxKind,
|
||||||
LinkedNode, Source, Spanned, SyntaxKind, VirtualPath,
|
|
||||||
};
|
};
|
||||||
pub use typst::World;
|
pub use typst::World;
|
||||||
|
|
||||||
|
|
|
@ -47,7 +47,7 @@ impl StatefulRequest for PrepareRenameRequest {
|
||||||
debug!("prepare_rename: {}", lnk.name);
|
debug!("prepare_rename: {}", lnk.name);
|
||||||
Some(PrepareRenameResponse::RangeWithPlaceholder {
|
Some(PrepareRenameResponse::RangeWithPlaceholder {
|
||||||
range: origin_selection_range,
|
range: origin_selection_range,
|
||||||
placeholder: lnk.name,
|
placeholder: lnk.name.to_string(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -159,7 +159,7 @@ impl SemanticRequest for SignatureHelpRequest {
|
||||||
|
|
||||||
Some(SignatureHelp {
|
Some(SignatureHelp {
|
||||||
signatures: vec![SignatureInformation {
|
signatures: vec![SignatureInformation {
|
||||||
label,
|
label: label.to_string(),
|
||||||
documentation,
|
documentation,
|
||||||
parameters: Some(params),
|
parameters: Some(params),
|
||||||
active_parameter: active_parameter.map(|x| x as u32),
|
active_parameter: active_parameter.map(|x| x as u32),
|
||||||
|
|
|
@ -85,7 +85,7 @@ fn filter_document_symbols(
|
||||||
let rng = typst_to_lsp::range(e.info.range.clone(), source, position_encoding);
|
let rng = typst_to_lsp::range(e.info.range.clone(), source, position_encoding);
|
||||||
|
|
||||||
Some(SymbolInformation {
|
Some(SymbolInformation {
|
||||||
name: e.info.name.clone(),
|
name: e.info.name.to_string(),
|
||||||
kind: e.info.kind.clone().try_into().unwrap(),
|
kind: e.info.kind.clone().try_into().unwrap(),
|
||||||
tags: None,
|
tags: None,
|
||||||
deprecated: None,
|
deprecated: None,
|
||||||
|
|
|
@ -5,6 +5,8 @@ use typst_shim::syntax::LinkedNodeExt;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::syntax::get_def_target;
|
use crate::syntax::get_def_target;
|
||||||
|
|
||||||
|
use super::DefTarget;
|
||||||
|
|
||||||
fn extract_document_between(
|
fn extract_document_between(
|
||||||
node: &LinkedNode,
|
node: &LinkedNode,
|
||||||
rng: Range<usize>,
|
rng: Range<usize>,
|
||||||
|
@ -93,6 +95,11 @@ pub fn find_docs_before(src: &Source, cursor: usize) -> Option<String> {
|
||||||
let root = LinkedNode::new(src.root());
|
let root = LinkedNode::new(src.root());
|
||||||
let leaf = root.leaf_at_compat(cursor)?;
|
let leaf = root.leaf_at_compat(cursor)?;
|
||||||
let def_target = get_def_target(leaf.clone())?;
|
let def_target = get_def_target(leaf.clone())?;
|
||||||
|
find_docs_of(src, def_target)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_docs_of(src: &Source, def_target: DefTarget) -> Option<String> {
|
||||||
|
let root = LinkedNode::new(src.root());
|
||||||
log::debug!("found docs target: {:?}", def_target.node().kind());
|
log::debug!("found docs target: {:?}", def_target.node().kind());
|
||||||
// todo: import node
|
// todo: import node
|
||||||
let target = def_target.node().clone();
|
let target = def_target.node().clone();
|
||||||
|
|
|
@ -4,7 +4,7 @@ use std::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::{anyhow, Context};
|
use anyhow::{anyhow, Context};
|
||||||
use ecow::{eco_vec, EcoVec};
|
use ecow::{eco_vec, EcoString, EcoVec};
|
||||||
use log::info;
|
use log::info;
|
||||||
use lsp_types::SymbolKind;
|
use lsp_types::SymbolKind;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
@ -30,7 +30,7 @@ pub(crate) fn get_lexical_hierarchy(
|
||||||
};
|
};
|
||||||
worker.stack.push((
|
worker.stack.push((
|
||||||
LexicalInfo {
|
LexicalInfo {
|
||||||
name: "deadbeef".to_string(),
|
name: "deadbeef".into(),
|
||||||
kind: LexicalKind::Heading(-1),
|
kind: LexicalKind::Heading(-1),
|
||||||
range: 0..0,
|
range: 0..0,
|
||||||
},
|
},
|
||||||
|
@ -223,7 +223,7 @@ impl LexicalScopeKind {
|
||||||
|
|
||||||
#[derive(Debug, Clone, Hash)]
|
#[derive(Debug, Clone, Hash)]
|
||||||
pub(crate) struct LexicalInfo {
|
pub(crate) struct LexicalInfo {
|
||||||
pub name: String,
|
pub name: EcoString,
|
||||||
pub kind: LexicalKind,
|
pub kind: LexicalKind,
|
||||||
pub range: Range<usize>,
|
pub range: Range<usize>,
|
||||||
}
|
}
|
||||||
|
@ -451,14 +451,14 @@ impl LexicalHierarchyWorker {
|
||||||
let symbol = if self.g == LexicalScopeKind::DefUse {
|
let symbol = if self.g == LexicalScopeKind::DefUse {
|
||||||
// DefUse mode does not nest symbols inside of functions
|
// DefUse mode does not nest symbols inside of functions
|
||||||
LexicalInfo {
|
LexicalInfo {
|
||||||
name: String::new(),
|
name: EcoString::new(),
|
||||||
kind: LexicalKind::Block,
|
kind: LexicalKind::Block,
|
||||||
range: body.range(),
|
range: body.range(),
|
||||||
}
|
}
|
||||||
} else if current == self.stack.last().unwrap().1.len() {
|
} else if current == self.stack.last().unwrap().1.len() {
|
||||||
// Closure has no updated symbol stack
|
// Closure has no updated symbol stack
|
||||||
LexicalInfo {
|
LexicalInfo {
|
||||||
name: "<anonymous>".to_string(),
|
name: "<anonymous>".into(),
|
||||||
kind: LexicalKind::function(),
|
kind: LexicalKind::function(),
|
||||||
range: node.range(),
|
range: node.range(),
|
||||||
}
|
}
|
||||||
|
@ -495,9 +495,9 @@ impl LexicalHierarchyWorker {
|
||||||
let target_name_node = node.find(target_name.span()).context("no pos")?;
|
let target_name_node = node.find(target_name.span()).context("no pos")?;
|
||||||
|
|
||||||
self.push_leaf(LexicalInfo {
|
self.push_leaf(LexicalInfo {
|
||||||
name: origin_name.get().to_string(),
|
name: origin_name.get().clone(),
|
||||||
kind: LexicalKind::module_import_alias(IdentRef {
|
kind: LexicalKind::module_import_alias(IdentRef {
|
||||||
name: target_name.get().to_string(),
|
name: target_name.get().clone(),
|
||||||
range: target_name_node.range(),
|
range: target_name_node.range(),
|
||||||
}),
|
}),
|
||||||
range: origin_name_node.range(),
|
range: origin_name_node.range(),
|
||||||
|
@ -590,19 +590,19 @@ impl LexicalHierarchyWorker {
|
||||||
let ast_node = node
|
let ast_node = node
|
||||||
.cast::<ast::Label>()
|
.cast::<ast::Label>()
|
||||||
.ok_or_else(|| anyhow!("cast to ast node failed: {:?}", node))?;
|
.ok_or_else(|| anyhow!("cast to ast node failed: {:?}", node))?;
|
||||||
let name = ast_node.get().to_string();
|
let name = ast_node.get().into();
|
||||||
|
|
||||||
(name, LexicalKind::label())
|
(name, LexicalKind::label())
|
||||||
}
|
}
|
||||||
SyntaxKind::RefMarker if self.g.affect_ref() => {
|
SyntaxKind::RefMarker if self.g.affect_ref() => {
|
||||||
let name = node.text().trim_start_matches('@').to_owned();
|
let name = node.text().trim_start_matches('@').into();
|
||||||
(name, LexicalKind::label_ref())
|
(name, LexicalKind::label_ref())
|
||||||
}
|
}
|
||||||
SyntaxKind::Ident if self.g.affect_symbol() => {
|
SyntaxKind::Ident if self.g.affect_symbol() => {
|
||||||
let ast_node = node
|
let ast_node = node
|
||||||
.cast::<ast::Ident>()
|
.cast::<ast::Ident>()
|
||||||
.ok_or_else(|| anyhow!("cast to ast node failed: {:?}", node))?;
|
.ok_or_else(|| anyhow!("cast to ast node failed: {:?}", node))?;
|
||||||
let name = ast_node.get().to_string();
|
let name = ast_node.get().clone();
|
||||||
let kind = match self.ident_context {
|
let kind = match self.ident_context {
|
||||||
IdentContext::Ref if self.g.affect_ref() => LexicalKind::val_ref(),
|
IdentContext::Ref if self.g.affect_ref() => LexicalKind::val_ref(),
|
||||||
IdentContext::Func => LexicalKind::function(),
|
IdentContext::Func => LexicalKind::function(),
|
||||||
|
@ -616,10 +616,10 @@ impl LexicalHierarchyWorker {
|
||||||
SyntaxKind::Equation | SyntaxKind::Raw | SyntaxKind::BlockComment
|
SyntaxKind::Equation | SyntaxKind::Raw | SyntaxKind::BlockComment
|
||||||
if self.g.affect_markup() =>
|
if self.g.affect_markup() =>
|
||||||
{
|
{
|
||||||
(String::new(), LexicalKind::Block)
|
(EcoString::new(), LexicalKind::Block)
|
||||||
}
|
}
|
||||||
SyntaxKind::CodeBlock | SyntaxKind::ContentBlock if self.g.affect_block() => {
|
SyntaxKind::CodeBlock | SyntaxKind::ContentBlock if self.g.affect_block() => {
|
||||||
(String::new(), LexicalKind::Block)
|
(EcoString::new(), LexicalKind::Block)
|
||||||
}
|
}
|
||||||
SyntaxKind::Parenthesized
|
SyntaxKind::Parenthesized
|
||||||
| SyntaxKind::Destructuring
|
| SyntaxKind::Destructuring
|
||||||
|
@ -628,7 +628,7 @@ impl LexicalHierarchyWorker {
|
||||||
| SyntaxKind::Dict
|
| SyntaxKind::Dict
|
||||||
if self.g.affect_expr() =>
|
if self.g.affect_expr() =>
|
||||||
{
|
{
|
||||||
(String::new(), LexicalKind::Block)
|
(EcoString::new(), LexicalKind::Block)
|
||||||
}
|
}
|
||||||
SyntaxKind::ModuleImport if self.g.affect_import() => {
|
SyntaxKind::ModuleImport if self.g.affect_import() => {
|
||||||
let src = node
|
let src = node
|
||||||
|
@ -639,23 +639,23 @@ impl LexicalHierarchyWorker {
|
||||||
match src {
|
match src {
|
||||||
ast::Expr::Str(e) => {
|
ast::Expr::Str(e) => {
|
||||||
let e = e.get();
|
let e = e.get();
|
||||||
(String::new(), LexicalKind::module(e.as_ref().into()))
|
(EcoString::new(), LexicalKind::module(e.as_ref().into()))
|
||||||
}
|
}
|
||||||
src => {
|
src => {
|
||||||
let e = node
|
let e = node
|
||||||
.find(src.span())
|
.find(src.span())
|
||||||
.ok_or_else(|| anyhow!("find expression failed: {:?}", src))?;
|
.ok_or_else(|| anyhow!("find expression failed: {:?}", src))?;
|
||||||
let e = IdentRef {
|
let e = IdentRef {
|
||||||
name: String::new(),
|
name: EcoString::new(),
|
||||||
range: e.range(),
|
range: e.range(),
|
||||||
};
|
};
|
||||||
|
|
||||||
(String::new(), LexicalKind::module_expr(e.into()))
|
(EcoString::new(), LexicalKind::module_expr(e.into()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SyntaxKind::Markup => {
|
SyntaxKind::Markup => {
|
||||||
let name = node.get().to_owned().into_text().to_string();
|
let name = node.get().to_owned().into_text();
|
||||||
if name.is_empty() {
|
if name.is_empty() {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
@ -703,7 +703,7 @@ impl LexicalHierarchyWorker {
|
||||||
// ^^^
|
// ^^^
|
||||||
let import_node = node.find(name.span()).context("no pos")?;
|
let import_node = node.find(name.span()).context("no pos")?;
|
||||||
self.push_leaf(LexicalInfo {
|
self.push_leaf(LexicalInfo {
|
||||||
name: name.get().to_string(),
|
name: name.get().clone(),
|
||||||
kind: LexicalKind::module_as(),
|
kind: LexicalKind::module_as(),
|
||||||
range: import_node.range(),
|
range: import_node.range(),
|
||||||
});
|
});
|
||||||
|
@ -721,14 +721,14 @@ impl LexicalHierarchyWorker {
|
||||||
let spec = e
|
let spec = e
|
||||||
.parse::<PackageSpec>()
|
.parse::<PackageSpec>()
|
||||||
.map_err(|e| anyhow!("parse package spec failed: {:?}", e))?;
|
.map_err(|e| anyhow!("parse package spec failed: {:?}", e))?;
|
||||||
spec.name.to_string()
|
spec.name.clone()
|
||||||
} else {
|
} else {
|
||||||
let e = Path::new(e.as_ref())
|
let e = Path::new(e.as_ref())
|
||||||
.file_name()
|
.file_name()
|
||||||
.context("no file name")?
|
.context("no file name")?
|
||||||
.to_string_lossy();
|
.to_string_lossy();
|
||||||
let e = e.as_ref();
|
let e = e.as_ref();
|
||||||
e.strip_suffix(".typ").context("no suffix")?.to_owned()
|
e.strip_suffix(".typ").context("no suffix")?.into()
|
||||||
};
|
};
|
||||||
|
|
||||||
// return (e == name).then_some(ImportRef::Path(v));
|
// return (e == name).then_some(ImportRef::Path(v));
|
||||||
|
@ -757,7 +757,7 @@ impl LexicalHierarchyWorker {
|
||||||
.context("no star")?;
|
.context("no star")?;
|
||||||
let v = node.find(wildcard.span()).context("no pos")?;
|
let v = node.find(wildcard.span()).context("no pos")?;
|
||||||
self.push_leaf(LexicalInfo {
|
self.push_leaf(LexicalInfo {
|
||||||
name: "*".to_string(),
|
name: "*".into(),
|
||||||
kind: LexicalKind::module_star(),
|
kind: LexicalKind::module_star(),
|
||||||
range: v.range(),
|
range: v.range(),
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
use std::ops::Range;
|
||||||
|
|
||||||
use ecow::EcoVec;
|
use ecow::EcoVec;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use typst::{
|
use typst::{
|
||||||
|
@ -211,6 +213,29 @@ impl<'a> DefTarget<'a> {
|
||||||
DefTarget::Import(node) => node,
|
DefTarget::Import(node) => node,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn name_range(&self) -> Option<Range<usize>> {
|
||||||
|
match self {
|
||||||
|
DefTarget::Let(node) => {
|
||||||
|
let lb: ast::LetBinding<'_> = node.cast()?;
|
||||||
|
let names = match lb.kind() {
|
||||||
|
ast::LetBindingKind::Closure(name) => node.find(name.span())?,
|
||||||
|
ast::LetBindingKind::Normal(ast::Pattern::Normal(name)) => {
|
||||||
|
node.find(name.span())?
|
||||||
|
}
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(names.range())
|
||||||
|
}
|
||||||
|
DefTarget::Import(_node) => {
|
||||||
|
// let ident = node.cast::<ast::ImportItem>()?;
|
||||||
|
// Some(ident.span().into())
|
||||||
|
// todo: implement this
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo: whether we should distinguish between strict and non-strict def targets
|
// todo: whether we should distinguish between strict and non-strict def targets
|
||||||
|
|
|
@ -5,8 +5,8 @@
|
||||||
// todo: remove this
|
// todo: remove this
|
||||||
#![allow(missing_docs)]
|
#![allow(missing_docs)]
|
||||||
|
|
||||||
pub(crate) mod import;
|
use ecow::EcoString;
|
||||||
pub use import::*;
|
pub use tinymist_analysis::import::*;
|
||||||
pub(crate) mod lexical_hierarchy;
|
pub(crate) mod lexical_hierarchy;
|
||||||
pub use lexical_hierarchy::*;
|
pub use lexical_hierarchy::*;
|
||||||
pub(crate) mod matcher;
|
pub(crate) mod matcher;
|
||||||
|
@ -28,7 +28,7 @@ use serde::{Deserialize, Serialize};
|
||||||
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
|
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
|
||||||
pub struct IdentRef {
|
pub struct IdentRef {
|
||||||
/// The name of the symbol.
|
/// The name of the symbol.
|
||||||
pub name: String,
|
pub name: EcoString,
|
||||||
/// The byte range of the symbol in the source file.
|
/// The byte range of the symbol in the source file.
|
||||||
pub range: Range<usize>,
|
pub range: Range<usize>,
|
||||||
}
|
}
|
||||||
|
@ -89,7 +89,7 @@ impl<'de> Deserialize<'de> for IdentRef {
|
||||||
(name, st_ed[0]..st_ed[1])
|
(name, st_ed[0]..st_ed[1])
|
||||||
};
|
};
|
||||||
Ok(IdentRef {
|
Ok(IdentRef {
|
||||||
name: name.to_string(),
|
name: name.into(),
|
||||||
range,
|
range,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -101,7 +101,7 @@ impl<'de> Deserialize<'de> for IdentRef {
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
pub struct IdentDef {
|
pub struct IdentDef {
|
||||||
/// The name of the symbol.
|
/// The name of the symbol.
|
||||||
pub name: String,
|
pub name: EcoString,
|
||||||
/// The kind of the symbol.
|
/// The kind of the symbol.
|
||||||
pub kind: LexicalKind,
|
pub kind: LexicalKind,
|
||||||
/// The byte range of the symbol in the source file.
|
/// The byte range of the symbol in the source file.
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
use core::fmt;
|
use core::fmt;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
|
||||||
collections::{HashMap, HashSet},
|
collections::{HashMap, HashSet},
|
||||||
ops::Range,
|
ops::Range,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
|
@ -9,21 +8,19 @@ use std::{
|
||||||
|
|
||||||
use ecow::EcoVec;
|
use ecow::EcoVec;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use reflexo_typst::config::CompileOpts;
|
|
||||||
use reflexo_typst::package::{PackageRegistry, PackageSpec};
|
use reflexo_typst::package::{PackageRegistry, PackageSpec};
|
||||||
use reflexo_typst::world::{EntryOpts, EntryState};
|
use reflexo_typst::world::EntryState;
|
||||||
use reflexo_typst::{
|
use reflexo_typst::{CompileDriverImpl, EntryManager, EntryReader, ShadowApi, WorldDeps};
|
||||||
CompileDriver, EntryManager, EntryReader, ShadowApi, TypstSystemUniverse, WorldDeps,
|
|
||||||
};
|
|
||||||
use serde_json::{ser::PrettyFormatter, Serializer, Value};
|
use serde_json::{ser::PrettyFormatter, Serializer, Value};
|
||||||
|
use tinymist_world::CompileFontArgs;
|
||||||
use typst::syntax::ast::{self, AstNode};
|
use typst::syntax::ast::{self, AstNode};
|
||||||
use typst::syntax::{FileId as TypstFileId, LinkedNode, Source, SyntaxKind, VirtualPath};
|
use typst::syntax::{FileId as TypstFileId, LinkedNode, Source, SyntaxKind, VirtualPath};
|
||||||
use typst::{diag::PackageError, foundations::Bytes};
|
use typst::{diag::PackageError, foundations::Bytes};
|
||||||
|
|
||||||
pub use insta::assert_snapshot;
|
pub use insta::assert_snapshot;
|
||||||
pub use reflexo_typst::TypstSystemWorld;
|
|
||||||
pub use serde::Serialize;
|
pub use serde::Serialize;
|
||||||
pub use serde_json::json;
|
pub use serde_json::json;
|
||||||
|
pub use tinymist_world::{LspUniverse, LspUniverseBuilder, LspWorld};
|
||||||
use typst_shim::syntax::LinkedNodeExt;
|
use typst_shim::syntax::LinkedNodeExt;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -32,7 +29,9 @@ use crate::{
|
||||||
typst_to_lsp, LspPosition, PositionEncoding, VersionedDocument,
|
typst_to_lsp, LspPosition, PositionEncoding, VersionedDocument,
|
||||||
};
|
};
|
||||||
|
|
||||||
struct WrapWorld<'a>(&'a mut TypstSystemWorld);
|
type CompileDriver<C> = CompileDriverImpl<C, tinymist_world::LspCompilerFeat>;
|
||||||
|
|
||||||
|
struct WrapWorld<'a>(&'a mut LspWorld);
|
||||||
|
|
||||||
impl<'a> AnalysisResources for WrapWorld<'a> {
|
impl<'a> AnalysisResources for WrapWorld<'a> {
|
||||||
fn world(&self) -> &dyn typst::World {
|
fn world(&self) -> &dyn typst::World {
|
||||||
|
@ -64,14 +63,23 @@ pub fn snapshot_testing(name: &str, f: &impl Fn(&mut AnalysisContext, PathBuf))
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
let contents = contents.replace("\r\n", "\n");
|
let contents = contents.replace("\r\n", "\n");
|
||||||
|
|
||||||
run_with_sources(&contents, |w: &mut TypstSystemUniverse, p| {
|
run_with_sources(&contents, |w, p| {
|
||||||
|
run_with_ctx(w, p, f);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_with_ctx<T>(
|
||||||
|
w: &mut LspUniverse,
|
||||||
|
p: PathBuf,
|
||||||
|
f: &impl Fn(&mut AnalysisContext, PathBuf) -> T,
|
||||||
|
) -> T {
|
||||||
let root = w.workspace_root().unwrap();
|
let root = w.workspace_root().unwrap();
|
||||||
let paths = w
|
let paths = w
|
||||||
.shadow_paths()
|
.shadow_paths()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|p| {
|
.map(|p| TypstFileId::new(None, VirtualPath::new(p.strip_prefix(&root).unwrap())))
|
||||||
TypstFileId::new(None, VirtualPath::new(p.strip_prefix(&root).unwrap()))
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let mut w = w.snapshot();
|
let mut w = w.snapshot();
|
||||||
let w = WrapWorld(&mut w);
|
let w = WrapWorld(&mut w);
|
||||||
|
@ -79,10 +87,7 @@ pub fn snapshot_testing(name: &str, f: &impl Fn(&mut AnalysisContext, PathBuf))
|
||||||
let mut ctx = AnalysisContext::new(root, &w, &a);
|
let mut ctx = AnalysisContext::new(root, &w, &a);
|
||||||
ctx.test_completion_files(Vec::new);
|
ctx.test_completion_files(Vec::new);
|
||||||
ctx.test_files(|| paths);
|
ctx.test_files(|| paths);
|
||||||
f(&mut ctx, p);
|
f(&mut ctx, p)
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_test_properties(s: &str) -> HashMap<&'_ str, &'_ str> {
|
pub fn get_test_properties(s: &str) -> HashMap<&'_ str, &'_ str> {
|
||||||
|
@ -116,21 +121,24 @@ pub fn compile_doc_for_test(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run_with_sources<T>(
|
pub fn run_with_sources<T>(source: &str, f: impl FnOnce(&mut LspUniverse, PathBuf) -> T) -> T {
|
||||||
source: &str,
|
|
||||||
f: impl FnOnce(&mut TypstSystemUniverse, PathBuf) -> T,
|
|
||||||
) -> T {
|
|
||||||
let root = if cfg!(windows) {
|
let root = if cfg!(windows) {
|
||||||
PathBuf::from("C:\\")
|
PathBuf::from("C:\\")
|
||||||
} else {
|
} else {
|
||||||
PathBuf::from("/")
|
PathBuf::from("/")
|
||||||
};
|
};
|
||||||
let mut world = TypstSystemUniverse::new(CompileOpts {
|
let mut world = LspUniverseBuilder::build(
|
||||||
entry: EntryOpts::new_rooted(root.as_path().into(), None),
|
EntryState::new_rooted(root.as_path().into(), None),
|
||||||
with_embedded_fonts: typst_assets::fonts().map(Cow::Borrowed).collect(),
|
Arc::new(
|
||||||
no_system_fonts: true,
|
LspUniverseBuilder::resolve_fonts(CompileFontArgs {
|
||||||
|
ignore_system_fonts: true,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
Default::default(),
|
||||||
|
None,
|
||||||
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let sources = source.split("-----");
|
let sources = source.split("-----");
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,6 @@ use serde::{Deserialize, Serialize};
|
||||||
use typst::foundations::{fields_on, format_str, repr, Repr, StyleChain, Styles, Value};
|
use typst::foundations::{fields_on, format_str, repr, Repr, StyleChain, Styles, Value};
|
||||||
use typst::model::Document;
|
use typst::model::Document;
|
||||||
use typst::syntax::ast::AstNode;
|
use typst::syntax::ast::AstNode;
|
||||||
use typst::syntax::package::PackageSpec;
|
|
||||||
use typst::syntax::{ast, is_id_continue, is_id_start, is_ident, LinkedNode, Source, SyntaxKind};
|
use typst::syntax::{ast, is_id_continue, is_id_start, is_ident, LinkedNode, Source, SyntaxKind};
|
||||||
use typst::text::RawElem;
|
use typst::text::RawElem;
|
||||||
use typst_shim::syntax::LinkedNodeExt;
|
use typst_shim::syntax::LinkedNodeExt;
|
||||||
|
@ -1048,74 +1047,21 @@ impl<'a, 'w> CompletionContext<'a, 'w> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get local packages
|
|
||||||
fn local_packages(&mut self) -> Vec<(PackageSpec, Option<EcoString>)> {
|
|
||||||
// search packages locally. We only search in the data
|
|
||||||
// directory and not the cache directory, because the latter is not
|
|
||||||
// intended for storage of local packages.
|
|
||||||
let mut packages = vec![];
|
|
||||||
let Some(data_dir) = dirs::data_dir() else {
|
|
||||||
return packages;
|
|
||||||
};
|
|
||||||
let local_path = data_dir.join("typst/packages");
|
|
||||||
if !local_path.exists() {
|
|
||||||
return packages;
|
|
||||||
}
|
|
||||||
// namespace/package_name/version
|
|
||||||
// 1. namespace
|
|
||||||
let namespaces = std::fs::read_dir(local_path).unwrap();
|
|
||||||
for namespace in namespaces {
|
|
||||||
let namespace = namespace.unwrap();
|
|
||||||
if !namespace.file_type().unwrap().is_dir() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// start with . are hidden directories
|
|
||||||
if namespace.file_name().to_string_lossy().starts_with('.') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// 2. package_name
|
|
||||||
let package_names = std::fs::read_dir(namespace.path()).unwrap();
|
|
||||||
for package in package_names {
|
|
||||||
let package = package.unwrap();
|
|
||||||
if !package.file_type().unwrap().is_dir() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if package.file_name().to_string_lossy().starts_with('.') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// 3. version
|
|
||||||
let versions = std::fs::read_dir(package.path()).unwrap();
|
|
||||||
for version in versions {
|
|
||||||
let version = version.unwrap();
|
|
||||||
if !version.file_type().unwrap().is_dir() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if version.file_name().to_string_lossy().starts_with('.') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let version = version.file_name().to_string_lossy().parse().unwrap();
|
|
||||||
let spec = PackageSpec {
|
|
||||||
namespace: namespace.file_name().to_string_lossy().into(),
|
|
||||||
name: package.file_name().to_string_lossy().into(),
|
|
||||||
version,
|
|
||||||
};
|
|
||||||
let description = eco_format!("{} v{}", spec.name, spec.version);
|
|
||||||
let package = (spec, Some(description));
|
|
||||||
packages.push(package);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
packages
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add completions for all available packages.
|
/// Add completions for all available packages.
|
||||||
fn package_completions(&mut self, all_versions: bool) {
|
fn package_completions(&mut self, all_versions: bool) {
|
||||||
let mut packages: Vec<_> = self.world().packages().iter().collect();
|
let mut packages: Vec<_> = self
|
||||||
|
.world()
|
||||||
|
.packages()
|
||||||
|
.iter()
|
||||||
|
.map(|e| (&e.0, e.1.clone()))
|
||||||
|
.collect();
|
||||||
// local_packages to references and add them to the packages
|
// local_packages to references and add them to the packages
|
||||||
let local_packages = self.local_packages();
|
let local_packages_refs = self.ctx.resources.local_packages();
|
||||||
let local_packages_refs: Vec<&(PackageSpec, Option<EcoString>)> =
|
packages.extend(
|
||||||
local_packages.iter().collect();
|
local_packages_refs
|
||||||
packages.extend(local_packages_refs);
|
.iter()
|
||||||
|
.map(|spec| (spec, Some(eco_format!("{} v{}", spec.name, spec.version)))),
|
||||||
|
);
|
||||||
|
|
||||||
packages.sort_by_key(|(spec, _)| (&spec.namespace, &spec.name, Reverse(spec.version)));
|
packages.sort_by_key(|(spec, _)| (&spec.namespace, &spec.name, Reverse(spec.version)));
|
||||||
if !all_versions {
|
if !all_versions {
|
||||||
|
|
|
@ -8,6 +8,7 @@ use typst::{
|
||||||
diag::{bail, StrResult},
|
diag::{bail, StrResult},
|
||||||
foundations::{Content, Func, Module, Type, Value},
|
foundations::{Content, Func, Module, Type, Value},
|
||||||
introspection::MetadataElem,
|
introspection::MetadataElem,
|
||||||
|
syntax::Span,
|
||||||
text::{FontInfo, FontStyle},
|
text::{FontInfo, FontStyle},
|
||||||
Library,
|
Library,
|
||||||
};
|
};
|
||||||
|
@ -397,8 +398,7 @@ fn summarize_font_family<'a>(variants: impl Iterator<Item = &'a FontInfo>) -> Ec
|
||||||
detail
|
detail
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn truncated_repr(value: &Value) -> EcoString {
|
pub fn truncated_repr_<const SZ_LIMIT: usize>(value: &Value) -> EcoString {
|
||||||
const _10MB: usize = 100 * 1024 * 1024;
|
|
||||||
use typst::foundations::Repr;
|
use typst::foundations::Repr;
|
||||||
|
|
||||||
let data: Option<Content> = value.clone().cast().ok();
|
let data: Option<Content> = value.clone().cast().ok();
|
||||||
|
@ -411,13 +411,53 @@ pub fn truncated_repr(value: &Value) -> EcoString {
|
||||||
value.repr()
|
value.repr()
|
||||||
};
|
};
|
||||||
|
|
||||||
if repr.len() > _10MB {
|
if repr.len() > SZ_LIMIT {
|
||||||
eco_format!("[truncated-repr: {} bytes]", repr.len())
|
eco_format!("[truncated-repr: {} bytes]", repr.len())
|
||||||
} else {
|
} else {
|
||||||
repr
|
repr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn truncated_repr(value: &Value) -> EcoString {
|
||||||
|
const _10MB: usize = 100 * 1024 * 1024;
|
||||||
|
truncated_repr_::<_10MB>(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn truncated_doc_repr(value: &Value) -> EcoString {
|
||||||
|
const _128B: usize = 128;
|
||||||
|
truncated_repr_::<_128B>(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run a function with a VM instance in the world
|
||||||
|
pub fn with_vm<T>(world: &dyn typst::World, f: impl FnOnce(&mut typst::eval::Vm) -> T) -> T {
|
||||||
|
use comemo::Track;
|
||||||
|
use typst::engine::*;
|
||||||
|
use typst::eval::*;
|
||||||
|
use typst::foundations::*;
|
||||||
|
use typst::introspection::*;
|
||||||
|
|
||||||
|
let mut locator = Locator::default();
|
||||||
|
let introspector = Introspector::default();
|
||||||
|
let mut tracer = Tracer::new();
|
||||||
|
let engine = Engine {
|
||||||
|
world: world.track(),
|
||||||
|
route: Route::default(),
|
||||||
|
introspector: introspector.track(),
|
||||||
|
locator: &mut locator,
|
||||||
|
tracer: tracer.track_mut(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let context = Context::none();
|
||||||
|
let mut vm = Vm::new(
|
||||||
|
engine,
|
||||||
|
context.track(),
|
||||||
|
Scopes::new(Some(world.library())),
|
||||||
|
Span::detached(),
|
||||||
|
);
|
||||||
|
|
||||||
|
f(&mut vm)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -67,7 +67,7 @@ fn filter_document_labels(
|
||||||
let rng = typst_to_lsp::range(e.info.range.clone(), source, position_encoding);
|
let rng = typst_to_lsp::range(e.info.range.clone(), source, position_encoding);
|
||||||
|
|
||||||
Some(SymbolInformation {
|
Some(SymbolInformation {
|
||||||
name: e.info.name.clone(),
|
name: e.info.name.to_string(),
|
||||||
kind: e.info.kind.clone().try_into().unwrap(),
|
kind: e.info.kind.clone().try_into().unwrap(),
|
||||||
tags: None,
|
tags: None,
|
||||||
deprecated: None,
|
deprecated: None,
|
||||||
|
|
|
@ -31,5 +31,8 @@ flate2 = "1"
|
||||||
tar = "0.4"
|
tar = "0.4"
|
||||||
reqwest = "^0.11"
|
reqwest = "^0.11"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
no-content-hint = ["reflexo-typst/no-content-hint"]
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
//! Https registry for tinymist.
|
||||||
|
|
||||||
pub use reflexo_typst::font::FontResolverImpl;
|
pub use reflexo_typst::font::FontResolverImpl;
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
@ -6,21 +8,18 @@ use std::{path::PathBuf, sync::Arc};
|
||||||
use reflexo_typst::vfs::system::SystemAccessModel;
|
use reflexo_typst::vfs::system::SystemAccessModel;
|
||||||
use reflexo_typst::{CompilerFeat, CompilerUniverse, CompilerWorld};
|
use reflexo_typst::{CompilerFeat, CompilerUniverse, CompilerWorld};
|
||||||
|
|
||||||
use std::sync::OnceLock;
|
use log::error;
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
use reflexo_typst::package::{DummyNotifier, Notifier, PackageError, PackageRegistry, PackageSpec};
|
||||||
use reflexo_typst::typst::{
|
use reflexo_typst::typst::{
|
||||||
diag::{eco_format, EcoString},
|
diag::{eco_format, EcoString},
|
||||||
syntax::package::PackageVersion,
|
syntax::package::PackageVersion,
|
||||||
};
|
};
|
||||||
use reqwest::{
|
use reqwest::{blocking::Response, Certificate};
|
||||||
blocking::Response,
|
use std::sync::OnceLock;
|
||||||
Certificate
|
|
||||||
};
|
|
||||||
use log::error;
|
|
||||||
use parking_lot::Mutex;
|
|
||||||
use reflexo_typst::package::{DummyNotifier, Notifier, PackageError, PackageSpec, PackageRegistry};
|
|
||||||
|
|
||||||
/// Compiler feature for LSP universe and worlds without typst.ts to implement more for tinymist.
|
/// Compiler feature for LSP universe and worlds without typst.ts to implement
|
||||||
/// type trait of [`TypstSystemWorld`].
|
/// more for tinymist. type trait of [`TypstSystemWorld`].
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub struct SystemCompilerFeatExtend;
|
pub struct SystemCompilerFeatExtend;
|
||||||
|
|
||||||
|
@ -62,6 +61,7 @@ impl Default for HttpsRegistry {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HttpsRegistry {
|
impl HttpsRegistry {
|
||||||
|
/// Create a new registry.
|
||||||
pub fn new(cert_path: Option<PathBuf>) -> Self {
|
pub fn new(cert_path: Option<PathBuf>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
notifier: Arc::new(Mutex::<DummyNotifier>::default()),
|
notifier: Arc::new(Mutex::<DummyNotifier>::default()),
|
||||||
|
@ -101,7 +101,6 @@ impl HttpsRegistry {
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Make a package available in the on-disk cache.
|
/// Make a package available in the on-disk cache.
|
||||||
pub fn prepare_package(&self, spec: &PackageSpec) -> Result<Arc<Path>, PackageError> {
|
pub fn prepare_package(&self, spec: &PackageSpec) -> Result<Arc<Path>, PackageError> {
|
||||||
let subdir = format!(
|
let subdir = format!(
|
||||||
|
@ -224,7 +223,9 @@ fn threaded_http<T: Send + Sync>(
|
||||||
let client_builder = reqwest::blocking::Client::builder();
|
let client_builder = reqwest::blocking::Client::builder();
|
||||||
|
|
||||||
let client = if let Some(cert_path) = cert_path {
|
let client = if let Some(cert_path) = cert_path {
|
||||||
let cert = std::fs::read(cert_path).ok().and_then(|buf| Certificate::from_pem(&buf).ok());
|
let cert = std::fs::read(cert_path)
|
||||||
|
.ok()
|
||||||
|
.and_then(|buf| Certificate::from_pem(&buf).ok());
|
||||||
if let Some(cert) = cert {
|
if let Some(cert) = cert {
|
||||||
client_builder.add_root_certificate(cert).build().unwrap()
|
client_builder.add_root_certificate(cert).build().unwrap()
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -20,8 +20,10 @@ use reflexo_typst::vfs::{system::SystemAccessModel, Vfs};
|
||||||
use reflexo_typst::TypstDict;
|
use reflexo_typst::TypstDict;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
mod https;
|
pub mod https;
|
||||||
use https::{SystemCompilerFeatExtend, TypstSystemUniverseExtend, TypstSystemWorldExtend, HttpsRegistry};
|
use https::{
|
||||||
|
HttpsRegistry, SystemCompilerFeatExtend, TypstSystemUniverseExtend, TypstSystemWorldExtend,
|
||||||
|
};
|
||||||
|
|
||||||
const ENV_PATH_SEP: char = if cfg!(windows) { ';' } else { ':' };
|
const ENV_PATH_SEP: char = if cfg!(windows) { ';' } else { ':' };
|
||||||
|
|
||||||
|
@ -81,12 +83,9 @@ pub struct CompileOnceArgs {
|
||||||
)]
|
)]
|
||||||
pub creation_timestamp: Option<DateTime<Utc>>,
|
pub creation_timestamp: Option<DateTime<Utc>>,
|
||||||
|
|
||||||
/// Path to CA certificate file for network access, especially for downloading typst packages.
|
/// Path to CA certificate file for network access, especially for
|
||||||
#[clap(
|
/// downloading typst packages.
|
||||||
long = "cert",
|
#[clap(long = "cert", env = "TYPST_CERT", value_name = "CERT_PATH")]
|
||||||
env = "TYPST_CERT",
|
|
||||||
value_name = "CERT_PATH"
|
|
||||||
)]
|
|
||||||
pub certification: Option<PathBuf>,
|
pub certification: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,7 +101,12 @@ impl CompileOnceArgs {
|
||||||
.collect();
|
.collect();
|
||||||
let cert_path = self.certification.clone();
|
let cert_path = self.certification.clone();
|
||||||
|
|
||||||
LspUniverseBuilder::build(entry, Arc::new(fonts), Arc::new(Prehashed::new(inputs)), cert_path)
|
LspUniverseBuilder::build(
|
||||||
|
entry,
|
||||||
|
Arc::new(fonts),
|
||||||
|
Arc::new(Prehashed::new(inputs)),
|
||||||
|
cert_path,
|
||||||
|
)
|
||||||
.context("failed to create universe")
|
.context("failed to create universe")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,7 +32,7 @@ use std::{
|
||||||
use anyhow::{anyhow, bail};
|
use anyhow::{anyhow, bail};
|
||||||
use log::{error, info, trace};
|
use log::{error, info, trace};
|
||||||
use reflexo_typst::{
|
use reflexo_typst::{
|
||||||
debug_loc::DataSource, error::prelude::*, typst::prelude::EcoVec, vfs::notify::MemoryEvent,
|
debug_loc::DataSource, error::prelude::*, typst::prelude::*, vfs::notify::MemoryEvent,
|
||||||
world::EntryState, CompileReport, EntryReader, Error, ImmutPath, TaskInputs, TypstFont,
|
world::EntryState, CompileReport, EntryReader, Error, ImmutPath, TaskInputs, TypstFont,
|
||||||
};
|
};
|
||||||
use sync_lsp::{just_future, QueryFuture};
|
use sync_lsp::{just_future, QueryFuture};
|
||||||
|
@ -230,6 +230,17 @@ impl CompileHandler {
|
||||||
self.0.font_resolver.describe_font(&font)
|
self.0.font_resolver.describe_font(&font)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the local packages and their descriptions.
|
||||||
|
fn local_packages(&self) -> EcoVec<PackageSpec> {
|
||||||
|
crate::tool::package::list_package_by_namespace(
|
||||||
|
&self.0.registry,
|
||||||
|
eco_format!("local"),
|
||||||
|
)
|
||||||
|
.into_iter()
|
||||||
|
.map(|(_, spec)| spec)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
/// Resolve periscope image at the given position.
|
/// Resolve periscope image at the given position.
|
||||||
fn periscope_at(
|
fn periscope_at(
|
||||||
&self,
|
&self,
|
||||||
|
|
|
@ -10,8 +10,9 @@ use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value as JsonValue;
|
use serde_json::Value as JsonValue;
|
||||||
use task::TraceParams;
|
use task::TraceParams;
|
||||||
use tinymist_assets::TYPST_PREVIEW_HTML;
|
use tinymist_assets::TYPST_PREVIEW_HTML;
|
||||||
|
use tinymist_query::docs::PackageInfo;
|
||||||
use tinymist_query::{ExportKind, PageSelection};
|
use tinymist_query::{ExportKind, PageSelection};
|
||||||
use typst::diag::StrResult;
|
use typst::diag::{EcoString, StrResult};
|
||||||
use typst::syntax::package::{PackageSpec, VersionlessPackageSpec};
|
use typst::syntax::package::{PackageSpec, VersionlessPackageSpec};
|
||||||
|
|
||||||
use super::server::*;
|
use super::server::*;
|
||||||
|
@ -498,19 +499,94 @@ impl LanguageState {
|
||||||
Err(method_not_found())
|
Err(method_not_found())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get directory of local pacakges
|
/// Get directory of pacakges
|
||||||
pub fn resource_local_packages(
|
pub fn resource_package_dirs(&mut self, _arguments: Vec<JsonValue>) -> AnySchedulableResponse {
|
||||||
|
let snap = self.primary().snapshot().map_err(z_internal_error)?;
|
||||||
|
just_future(async move {
|
||||||
|
let snap = snap.receive().await.map_err(z_internal_error)?;
|
||||||
|
let paths = snap.world.registry.paths();
|
||||||
|
serde_json::to_value(paths).map_err(|e| internal_error(e.to_string()))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get writable directory of pacakges
|
||||||
|
pub fn resource_local_package_dir(
|
||||||
&mut self,
|
&mut self,
|
||||||
_arguments: Vec<JsonValue>,
|
_arguments: Vec<JsonValue>,
|
||||||
) -> AnySchedulableResponse {
|
) -> AnySchedulableResponse {
|
||||||
let Some(data_dir) = dirs::data_dir() else {
|
let snap = self.primary().snapshot().map_err(z_internal_error)?;
|
||||||
return just_ok(JsonValue::Null);
|
just_future(async move {
|
||||||
};
|
let snap = snap.receive().await.map_err(z_internal_error)?;
|
||||||
let local_path = data_dir.join("typst/packages");
|
let paths = snap
|
||||||
if !local_path.exists() {
|
.world
|
||||||
return just_ok(JsonValue::Null);
|
.registry
|
||||||
|
.local_path()
|
||||||
|
.into_iter()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
serde_json::to_value(paths).map_err(|e| internal_error(e.to_string()))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
let local_path = local_path.to_string_lossy().to_string();
|
|
||||||
just_ok(JsonValue::String(local_path))
|
/// Get writable directory of pacakges
|
||||||
|
pub fn resource_package_by_ns(
|
||||||
|
&mut self,
|
||||||
|
mut arguments: Vec<JsonValue>,
|
||||||
|
) -> AnySchedulableResponse {
|
||||||
|
let ns = get_arg!(arguments[1] as EcoString);
|
||||||
|
|
||||||
|
let snap = self.primary().snapshot().map_err(z_internal_error)?;
|
||||||
|
just_future(async move {
|
||||||
|
let snap = snap.receive().await.map_err(z_internal_error)?;
|
||||||
|
let packages = tool::package::list_package_by_namespace(&snap.world.registry, ns)
|
||||||
|
.into_iter()
|
||||||
|
.map(PackageInfo::from)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
serde_json::to_value(packages).map_err(|e| internal_error(e.to_string()))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the all valid symbols
|
||||||
|
pub fn resource_package_symbols(
|
||||||
|
&mut self,
|
||||||
|
mut arguments: Vec<JsonValue>,
|
||||||
|
) -> AnySchedulableResponse {
|
||||||
|
let info = get_arg!(arguments[1] as PackageInfo);
|
||||||
|
|
||||||
|
let snap = self.primary().snapshot().map_err(z_internal_error)?;
|
||||||
|
just_future(async move {
|
||||||
|
let snap = snap.receive().await.map_err(z_internal_error)?;
|
||||||
|
let w = snap.world.as_ref();
|
||||||
|
let symbols = tinymist_query::docs::list_symbols(w, &info)
|
||||||
|
.map_err(map_string_err("failed to list symbols"))
|
||||||
|
.map_err(z_internal_error)?;
|
||||||
|
|
||||||
|
serde_json::to_value(symbols).map_err(|e| internal_error(e.to_string()))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the all symbol docs
|
||||||
|
pub fn resource_package_docs(
|
||||||
|
&mut self,
|
||||||
|
mut arguments: Vec<JsonValue>,
|
||||||
|
) -> AnySchedulableResponse {
|
||||||
|
let info = get_arg!(arguments[1] as PackageInfo);
|
||||||
|
|
||||||
|
let handle = self.primary().handle.clone();
|
||||||
|
let snap = handle.snapshot().map_err(z_internal_error)?;
|
||||||
|
just_future(async move {
|
||||||
|
let snap = snap.receive().await.map_err(z_internal_error)?;
|
||||||
|
let w = snap.world.as_ref();
|
||||||
|
|
||||||
|
let res = handle.run_analysis(w, |a| {
|
||||||
|
let symbols = tinymist_query::docs::generate_md_docs(a, w, &info)
|
||||||
|
.map_err(map_string_err("failed to list symbols"))
|
||||||
|
.map_err(z_internal_error)?;
|
||||||
|
|
||||||
|
serde_json::to_value(symbols).map_err(|e| internal_error(e.to_string()))
|
||||||
|
});
|
||||||
|
|
||||||
|
res.map_err(|e| internal_error(e.to_string()))?
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -262,7 +262,11 @@ impl LanguageState {
|
||||||
.with_resource("/symbols", State::resource_symbols)
|
.with_resource("/symbols", State::resource_symbols)
|
||||||
.with_resource("/preview/index.html", State::resource_preview_html)
|
.with_resource("/preview/index.html", State::resource_preview_html)
|
||||||
.with_resource("/tutorial", State::resource_tutoral)
|
.with_resource("/tutorial", State::resource_tutoral)
|
||||||
.with_resource("/dirs/local-packages", State::resource_local_packages);
|
.with_resource("/package/by-namespace", State::resource_package_by_ns)
|
||||||
|
.with_resource("/package/symbol", State::resource_package_symbols)
|
||||||
|
.with_resource("/package/docs", State::resource_package_docs)
|
||||||
|
.with_resource("/dir/package", State::resource_package_dirs)
|
||||||
|
.with_resource("/dir/package/local", State::resource_local_package_dir);
|
||||||
|
|
||||||
// todo: generalize me
|
// todo: generalize me
|
||||||
provider.args.add_commands(
|
provider.args.add_commands(
|
||||||
|
|
|
@ -4,8 +4,9 @@ use std::io::Write;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use reflexo_typst::{Bytes, ImmutPath, TypstFileId};
|
use reflexo_typst::{Bytes, ImmutPath, TypstFileId};
|
||||||
|
use tinymist_query::docs::get_manifest;
|
||||||
use typst::diag::{bail, eco_format, FileError, FileResult, StrResult};
|
use typst::diag::{bail, eco_format, FileError, FileResult, StrResult};
|
||||||
use typst::syntax::package::{PackageManifest, PackageSpec, TemplateInfo};
|
use typst::syntax::package::{PackageSpec, TemplateInfo};
|
||||||
use typst::syntax::VirtualPath;
|
use typst::syntax::VirtualPath;
|
||||||
use typst::World;
|
use typst::World;
|
||||||
|
|
||||||
|
@ -31,10 +32,7 @@ pub fn get_entry(world: &LspWorld, tmpl: TemplateSource) -> StrResult<Bytes> {
|
||||||
let TemplateSource::Package(spec) = tmpl;
|
let TemplateSource::Package(spec) = tmpl;
|
||||||
|
|
||||||
let toml_id = TypstFileId::new(Some(spec.clone()), VirtualPath::new("typst.toml"));
|
let toml_id = TypstFileId::new(Some(spec.clone()), VirtualPath::new("typst.toml"));
|
||||||
|
let manifest = get_manifest(world, toml_id)?;
|
||||||
// Parse the manifest.
|
|
||||||
let manifest = parse_manifest(world, toml_id)?;
|
|
||||||
manifest.validate(&spec)?;
|
|
||||||
|
|
||||||
// Ensure that it is indeed a template.
|
// Ensure that it is indeed a template.
|
||||||
let Some(tmpl_info) = &manifest.template else {
|
let Some(tmpl_info) = &manifest.template else {
|
||||||
|
@ -56,10 +54,7 @@ pub fn init(world: &LspWorld, task: InitTask) -> StrResult<PathBuf> {
|
||||||
.unwrap_or_else(|| Path::new(spec.name.as_str()).into());
|
.unwrap_or_else(|| Path::new(spec.name.as_str()).into());
|
||||||
|
|
||||||
let toml_id = TypstFileId::new(Some(spec.clone()), VirtualPath::new("typst.toml"));
|
let toml_id = TypstFileId::new(Some(spec.clone()), VirtualPath::new("typst.toml"));
|
||||||
|
let manifest = get_manifest(world, toml_id)?;
|
||||||
// Parse the manifest.
|
|
||||||
let manifest = parse_manifest(world, toml_id)?;
|
|
||||||
manifest.validate(&spec)?;
|
|
||||||
|
|
||||||
// Ensure that it is indeed a template.
|
// Ensure that it is indeed a template.
|
||||||
let Some(template) = &manifest.template else {
|
let Some(template) = &manifest.template else {
|
||||||
|
@ -78,19 +73,6 @@ pub fn init(world: &LspWorld, task: InitTask) -> StrResult<PathBuf> {
|
||||||
Ok(entry_point)
|
Ok(entry_point)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parses the manifest of the package located at `package_path`.
|
|
||||||
fn parse_manifest(world: &LspWorld, toml_id: TypstFileId) -> StrResult<PackageManifest> {
|
|
||||||
let toml_data = world
|
|
||||||
.file(toml_id)
|
|
||||||
.map_err(|err| eco_format!("failed to read package manifest ({})", err))?;
|
|
||||||
|
|
||||||
let string = std::str::from_utf8(&toml_data)
|
|
||||||
.map_err(|err| eco_format!("package manifest is not valid UTF-8 ({})", err))?;
|
|
||||||
|
|
||||||
toml::from_str(string)
|
|
||||||
.map_err(|err| eco_format!("package manifest is malformed ({})", err.message()))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates the project directory with the template's contents and returns the
|
/// Creates the project directory with the template's contents and returns the
|
||||||
/// path at which it was created.
|
/// path at which it was created.
|
||||||
fn scaffold_project(
|
fn scaffold_project(
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
//! Package management tools.
|
//! Package management tools.
|
||||||
|
|
||||||
use reflexo_typst::package::PackageRegistry;
|
use std::path::PathBuf;
|
||||||
use typst::diag::{eco_format, StrResult};
|
|
||||||
|
use reflexo_typst::package::{PackageRegistry, PackageSpec};
|
||||||
|
use reflexo_typst::typst::prelude::*;
|
||||||
|
use tinymist_world::https::HttpsRegistry;
|
||||||
|
use typst::diag::{eco_format, EcoString, StrResult};
|
||||||
use typst::syntax::package::{PackageVersion, VersionlessPackageSpec};
|
use typst::syntax::package::{PackageVersion, VersionlessPackageSpec};
|
||||||
|
|
||||||
use crate::LspWorld;
|
use crate::LspWorld;
|
||||||
|
@ -40,3 +44,58 @@ pub fn determine_latest_version(
|
||||||
.ok_or_else(|| eco_format!("please specify the desired version"))
|
.ok_or_else(|| eco_format!("please specify the desired version"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the packages in namespaces and their descriptions.
|
||||||
|
pub fn list_package_by_namespace(
|
||||||
|
registry: &HttpsRegistry,
|
||||||
|
ns: EcoString,
|
||||||
|
) -> EcoVec<(PathBuf, PackageSpec)> {
|
||||||
|
// search packages locally. We only search in the data
|
||||||
|
// directory and not the cache directory, because the latter is not
|
||||||
|
// intended for storage of local packages.
|
||||||
|
let mut packages = eco_vec![];
|
||||||
|
|
||||||
|
log::info!(
|
||||||
|
"searching for packages in namespace {ns} in paths {:?}",
|
||||||
|
registry.paths()
|
||||||
|
);
|
||||||
|
for dir in registry.paths() {
|
||||||
|
let local_path = dir.join(ns.as_str());
|
||||||
|
if !local_path.exists() || !local_path.is_dir() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// namespace/package_name/version
|
||||||
|
// 2. package_name
|
||||||
|
let package_names = std::fs::read_dir(local_path).unwrap();
|
||||||
|
for package in package_names {
|
||||||
|
let package = package.unwrap();
|
||||||
|
if !package.file_type().unwrap().is_dir() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if package.file_name().to_string_lossy().starts_with('.') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 3. version
|
||||||
|
let versions = std::fs::read_dir(package.path()).unwrap();
|
||||||
|
for version in versions {
|
||||||
|
let version = version.unwrap();
|
||||||
|
if !version.file_type().unwrap().is_dir() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if version.file_name().to_string_lossy().starts_with('.') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let path = version.path();
|
||||||
|
let version = version.file_name().to_string_lossy().parse().unwrap();
|
||||||
|
let spec = PackageSpec {
|
||||||
|
namespace: ns.clone(),
|
||||||
|
name: package.file_name().to_string_lossy().into(),
|
||||||
|
version,
|
||||||
|
};
|
||||||
|
packages.push((path, spec));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
packages
|
||||||
|
}
|
||||||
|
|
|
@ -12,8 +12,8 @@ repository.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
typst-syntax.workspace = true
|
typst-syntax.workspace = true
|
||||||
tinymist-query.workspace = true
|
tinymist-analysis.workspace = true
|
||||||
tinymist-world.workspace = true
|
tinymist-world = { workspace = true, features = ["no-content-hint"] }
|
||||||
ecow.workspace = true
|
ecow.workspace = true
|
||||||
comemo.workspace = true
|
comemo.workspace = true
|
||||||
|
|
||||||
|
@ -25,5 +25,5 @@ base64.workspace = true
|
||||||
insta.workspace = true
|
insta.workspace = true
|
||||||
regex.workspace = true
|
regex.workspace = true
|
||||||
|
|
||||||
[lints]
|
# [lints]
|
||||||
workspace = true
|
# workspace = true
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
//! # Typlite
|
//! # Typlite
|
||||||
|
|
||||||
mod error;
|
mod error;
|
||||||
mod library;
|
pub mod library;
|
||||||
pub mod scopes;
|
pub mod scopes;
|
||||||
mod value;
|
pub mod value;
|
||||||
|
|
||||||
|
use std::fmt::Write;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub use error::*;
|
pub use error::*;
|
||||||
|
@ -30,6 +31,10 @@ pub use tinymist_world::CompileOnceArgs;
|
||||||
pub struct Typlite {
|
pub struct Typlite {
|
||||||
/// The universe to use for the conversion.
|
/// The universe to use for the conversion.
|
||||||
world: Arc<LspWorld>,
|
world: Arc<LspWorld>,
|
||||||
|
/// library to use for the conversion.
|
||||||
|
library: Option<Arc<Scopes<Value>>>,
|
||||||
|
/// Documentation style to use for annotating the document.
|
||||||
|
do_annotate: bool,
|
||||||
/// Whether to enable GFM (GitHub Flavored Markdown) features.
|
/// Whether to enable GFM (GitHub Flavored Markdown) features.
|
||||||
gfm: bool,
|
gfm: bool,
|
||||||
}
|
}
|
||||||
|
@ -40,11 +45,31 @@ impl Typlite {
|
||||||
/// This is useful when you have a [`Source`] instance and you can avoid
|
/// This is useful when you have a [`Source`] instance and you can avoid
|
||||||
/// reparsing the content.
|
/// reparsing the content.
|
||||||
pub fn new(world: Arc<LspWorld>) -> Self {
|
pub fn new(world: Arc<LspWorld>) -> Self {
|
||||||
Self { world, gfm: false }
|
Self {
|
||||||
|
world,
|
||||||
|
library: None,
|
||||||
|
do_annotate: false,
|
||||||
|
gfm: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set library to use for the conversion.
|
||||||
|
pub fn with_library(mut self, library: Arc<Scopes<Value>>) -> Self {
|
||||||
|
self.library = Some(library);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Annotate the elements for identification.
|
||||||
|
pub fn annotate_elements(mut self, do_annotate: bool) -> Self {
|
||||||
|
self.do_annotate = do_annotate;
|
||||||
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert the content to a markdown string.
|
/// Convert the content to a markdown string.
|
||||||
pub fn convert(self) -> Result<EcoString> {
|
pub fn convert(self) -> Result<EcoString> {
|
||||||
|
static DEFAULT_LIB: std::sync::LazyLock<Arc<Scopes<Value>>> =
|
||||||
|
std::sync::LazyLock::new(|| Arc::new(library::library()));
|
||||||
|
|
||||||
let main = self.world.entry_state().main();
|
let main = self.world.entry_state().main();
|
||||||
let current = main.ok_or("no main file in workspace")?;
|
let current = main.ok_or("no main file in workspace")?;
|
||||||
let world = self.world;
|
let world = self.world;
|
||||||
|
@ -56,7 +81,13 @@ impl Typlite {
|
||||||
let worker = TypliteWorker {
|
let worker = TypliteWorker {
|
||||||
current,
|
current,
|
||||||
gfm: self.gfm,
|
gfm: self.gfm,
|
||||||
scopes: Arc::new(library::library()),
|
do_annotate: self.do_annotate,
|
||||||
|
list_depth: 0,
|
||||||
|
scopes: self
|
||||||
|
.library
|
||||||
|
.as_ref()
|
||||||
|
.unwrap_or_else(|| &*DEFAULT_LIB)
|
||||||
|
.clone(),
|
||||||
world,
|
world,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -64,12 +95,15 @@ impl Typlite {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Typlite worker
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct TypliteWorker {
|
pub struct TypliteWorker {
|
||||||
current: FileId,
|
current: FileId,
|
||||||
gfm: bool,
|
gfm: bool,
|
||||||
|
do_annotate: bool,
|
||||||
scopes: Arc<Scopes<Value>>,
|
scopes: Arc<Scopes<Value>>,
|
||||||
world: Arc<LspWorld>,
|
world: Arc<LspWorld>,
|
||||||
|
list_depth: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TypliteWorker {
|
impl TypliteWorker {
|
||||||
|
@ -110,7 +144,8 @@ impl TypliteWorker {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Text nodes
|
// Text nodes
|
||||||
Text | Space | Linebreak | Parbreak => Self::str(node),
|
Text | Space | Parbreak => Self::str(node),
|
||||||
|
Linebreak => Self::char('\n'),
|
||||||
|
|
||||||
// Semantic nodes
|
// Semantic nodes
|
||||||
Escape => Self::escape(node),
|
Escape => Self::escape(node),
|
||||||
|
@ -394,7 +429,22 @@ impl TypliteWorker {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn list_item(&mut self, node: &SyntaxNode) -> Result<Value> {
|
fn list_item(&mut self, node: &SyntaxNode) -> Result<Value> {
|
||||||
self.reduce(node)
|
let mut s = EcoString::new();
|
||||||
|
|
||||||
|
let list_item = node.cast::<ast::ListItem>().unwrap();
|
||||||
|
|
||||||
|
s.push_str("- ");
|
||||||
|
if self.do_annotate {
|
||||||
|
let _ = write!(s, "<!-- typlite:begin:list-item {} -->", self.list_depth);
|
||||||
|
self.list_depth += 1;
|
||||||
|
}
|
||||||
|
s.push_str(&Self::value(self.eval(list_item.body().to_untyped())?));
|
||||||
|
if self.do_annotate {
|
||||||
|
self.list_depth -= 1;
|
||||||
|
let _ = write!(s, "<!-- typlite:end:list-item {} -->", self.list_depth);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Value::Content(s))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn enum_item(&mut self, node: &SyntaxNode) -> Result<Value> {
|
fn enum_item(&mut self, node: &SyntaxNode) -> Result<Value> {
|
||||||
|
@ -460,7 +510,7 @@ impl TypliteWorker {
|
||||||
|
|
||||||
let path = include.source();
|
let path = include.source();
|
||||||
let src =
|
let src =
|
||||||
tinymist_query::syntax::find_source_by_expr(self.world.as_ref(), self.current, path)
|
tinymist_analysis::import::find_source_by_expr(self.world.as_ref(), self.current, path)
|
||||||
.ok_or_else(|| format!("failed to find source on path {path:?}"))?;
|
.ok_or_else(|| format!("failed to find source on path {path:?}"))?;
|
||||||
|
|
||||||
self.clone().sub_file(src).map(Value::Content)
|
self.clone().sub_file(src).map(Value::Content)
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
//! # Typlite Library
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use ecow::eco_format;
|
use ecow::eco_format;
|
||||||
use value::*;
|
use value::*;
|
||||||
|
@ -6,11 +8,11 @@ pub fn library() -> Scopes<Value> {
|
||||||
let mut scopes = Scopes::new();
|
let mut scopes = Scopes::new();
|
||||||
scopes.define("link", link as RawFunc);
|
scopes.define("link", link as RawFunc);
|
||||||
scopes.define("kbd", kbd as RawFunc);
|
scopes.define("kbd", kbd as RawFunc);
|
||||||
// todo: how to import this function correctly?
|
|
||||||
scopes.define("cross-link", cross_link as RawFunc);
|
|
||||||
scopes.define("md-alter", md_alter as RawFunc);
|
scopes.define("md-alter", md_alter as RawFunc);
|
||||||
scopes.define("image", image as RawFunc);
|
scopes.define("image", image as RawFunc);
|
||||||
scopes.define("figure", figure as RawFunc);
|
scopes.define("figure", figure as RawFunc);
|
||||||
|
scopes.define("raw", raw as RawFunc);
|
||||||
|
scopes.define("pad", pad as RawFunc);
|
||||||
scopes.define("note-box", note as RawFunc);
|
scopes.define("note-box", note as RawFunc);
|
||||||
scopes.define("tip-box", tip as RawFunc);
|
scopes.define("tip-box", tip as RawFunc);
|
||||||
scopes.define("important-box", important_box as RawFunc);
|
scopes.define("important-box", important_box as RawFunc);
|
||||||
|
@ -52,6 +54,18 @@ pub fn figure(mut args: Args) -> Result<Value> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Evaluate a raw.
|
||||||
|
pub fn raw(mut args: Args) -> Result<Value> {
|
||||||
|
let content = get_pos_named!(args, content: EcoString);
|
||||||
|
|
||||||
|
Ok(Value::Content(eco_format!("```` {content} ````")))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluate a padded content.
|
||||||
|
pub fn pad(mut args: Args) -> Result<Value> {
|
||||||
|
Ok(get_pos_named!(args, path: Value))
|
||||||
|
}
|
||||||
|
|
||||||
/// Evaluate a `kbd` element.
|
/// Evaluate a `kbd` element.
|
||||||
pub fn kbd(mut args: Args) -> Result<Value> {
|
pub fn kbd(mut args: Args) -> Result<Value> {
|
||||||
let key = get_pos_named!(args, key: EcoString);
|
let key = get_pos_named!(args, key: EcoString);
|
||||||
|
@ -59,19 +73,6 @@ pub fn kbd(mut args: Args) -> Result<Value> {
|
||||||
Ok(Value::Content(eco_format!("<kbd>{key}</kbd>")))
|
Ok(Value::Content(eco_format!("<kbd>{key}</kbd>")))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Evaluate a `cross-link`.
|
|
||||||
pub fn cross_link(mut args: Args) -> Result<Value> {
|
|
||||||
let dest = get_pos_named!(args, dest: EcoString);
|
|
||||||
let body = get_pos_named!(args, body: Content);
|
|
||||||
|
|
||||||
let dest = std::path::Path::new(dest.as_str()).with_extension("html");
|
|
||||||
|
|
||||||
Ok(Value::Content(eco_format!(
|
|
||||||
"[{body}](https://myriad-dreamin.github.io/tinymist/{dest})",
|
|
||||||
dest = dest.to_string_lossy()
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Evaluate a markdown alteration.
|
/// Evaluate a markdown alteration.
|
||||||
pub fn md_alter(mut args: Args) -> Result<Value> {
|
pub fn md_alter(mut args: Args) -> Result<Value> {
|
||||||
let _: () = get_pos_named!(args, left: ());
|
let _: () = get_pos_named!(args, left: ());
|
||||||
|
|
|
@ -9,7 +9,7 @@ use typst_syntax::Source;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
fn conv(s: &str) -> EcoString {
|
fn conv_(s: &str, for_docs: bool) -> EcoString {
|
||||||
static FONT_RESOLVER: LazyLock<Result<Arc<FontResolverImpl>>> = LazyLock::new(|| {
|
static FONT_RESOLVER: LazyLock<Result<Arc<FontResolverImpl>>> = LazyLock::new(|| {
|
||||||
Ok(Arc::new(
|
Ok(Arc::new(
|
||||||
LspUniverseBuilder::resolve_fonts(CompileFontArgs::default())
|
LspUniverseBuilder::resolve_fonts(CompileFontArgs::default())
|
||||||
|
@ -32,7 +32,8 @@ fn conv(s: &str) -> EcoString {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let world = universe.snapshot();
|
let world = universe.snapshot();
|
||||||
|
|
||||||
let res = Typlite::new(Arc::new(world)).convert().unwrap();
|
let converter = Typlite::new(Arc::new(world)).annotate_elements(for_docs);
|
||||||
|
let res = converter.convert().unwrap();
|
||||||
static REG: OnceLock<Regex> = OnceLock::new();
|
static REG: OnceLock<Regex> = OnceLock::new();
|
||||||
let reg = REG.get_or_init(|| Regex::new(r#"data:image/svg\+xml;base64,([^"]+)"#).unwrap());
|
let reg = REG.get_or_init(|| Regex::new(r#"data:image/svg\+xml;base64,([^"]+)"#).unwrap());
|
||||||
let res = reg.replace(&res, |_captures: ®ex::Captures| {
|
let res = reg.replace(&res, |_captures: ®ex::Captures| {
|
||||||
|
@ -47,6 +48,14 @@ fn conv(s: &str) -> EcoString {
|
||||||
res.into()
|
res.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn conv(s: &str) -> EcoString {
|
||||||
|
conv_(s, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn conv_docs(s: &str) -> EcoString {
|
||||||
|
conv_(s, true)
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_converted() {
|
fn test_converted() {
|
||||||
insta::assert_snapshot!(conv(r###"
|
insta::assert_snapshot!(conv(r###"
|
||||||
|
@ -86,3 +95,75 @@ $
|
||||||
$
|
$
|
||||||
"###), @r###"<p align="center"><img src="data:image-hash/svg+xml;base64,redacted" alt="typst-block" /></p>"###);
|
"###), @r###"<p align="center"><img src="data:image-hash/svg+xml;base64,redacted" alt="typst-block" /></p>"###);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_converted_docs() {
|
||||||
|
insta::assert_snapshot!(conv_docs(r###"
|
||||||
|
These again are dictionaries with the keys
|
||||||
|
- `description` (optional): The description for the argument.
|
||||||
|
- `types` (optional): A list of accepted argument types.
|
||||||
|
- `default` (optional): Default value for this argument.
|
||||||
|
|
||||||
|
See @@show-module() for outputting the results of this function.
|
||||||
|
|
||||||
|
- content (string): Content of `.typ` file to analyze for docstrings.
|
||||||
|
- name (string): The name for the module.
|
||||||
|
- label-prefix (auto, string): The label-prefix for internal function
|
||||||
|
references. If `auto`, the label-prefix name will be the module name.
|
||||||
|
- require-all-parameters (boolean): Require that all parameters of a
|
||||||
|
functions are documented and fail if some are not.
|
||||||
|
- scope (dictionary): A dictionary of definitions that are then available
|
||||||
|
in all function and parameter descriptions.
|
||||||
|
- preamble (string): Code to prepend to all code snippets shown with `#example()`.
|
||||||
|
This can for instance be used to import something from the scope.
|
||||||
|
-> string
|
||||||
|
"###), @r###"
|
||||||
|
|
||||||
|
These again are dictionaries with the keys
|
||||||
|
- <!-- typlite:begin:list-item 0 -->`description` (optional): The description for the argument.<!-- typlite:end:list-item 0 -->
|
||||||
|
- <!-- typlite:begin:list-item 0 -->`types` (optional): A list of accepted argument types.<!-- typlite:end:list-item 0 -->
|
||||||
|
- <!-- typlite:begin:list-item 0 -->`default` (optional): Default value for this argument.<!-- typlite:end:list-item 0 -->
|
||||||
|
|
||||||
|
See @@show-module() for outputting the results of this function.
|
||||||
|
|
||||||
|
- <!-- typlite:begin:list-item 0 -->content (string): Content of `.typ` file to analyze for docstrings.<!-- typlite:end:list-item 0 -->
|
||||||
|
- <!-- typlite:begin:list-item 0 -->name (string): The name for the module.<!-- typlite:end:list-item 0 -->
|
||||||
|
- <!-- typlite:begin:list-item 0 -->label-prefix (auto, string): The label-prefix for internal function
|
||||||
|
references. If `auto`, the label-prefix name will be the module name.<!-- typlite:end:list-item 0 -->
|
||||||
|
- <!-- typlite:begin:list-item 0 -->require-all-parameters (boolean): Require that all parameters of a
|
||||||
|
functions are documented and fail if some are not.<!-- typlite:end:list-item 0 -->
|
||||||
|
- <!-- typlite:begin:list-item 0 -->scope (dictionary): A dictionary of definitions that are then available
|
||||||
|
in all function and parameter descriptions.<!-- typlite:end:list-item 0 -->
|
||||||
|
- <!-- typlite:begin:list-item 0 -->preamble (string): Code to prepend to all code snippets shown with `#example()`.
|
||||||
|
This can for instance be used to import something from the scope.<!-- typlite:end:list-item 0 -->
|
||||||
|
-> string
|
||||||
|
|
||||||
|
"###);
|
||||||
|
insta::assert_snapshot!(conv_docs(r###"
|
||||||
|
These again are dictionaries with the keys
|
||||||
|
- `description` (optional): The description for the argument.
|
||||||
|
|
||||||
|
See @@show-module() for outputting the results of this function.
|
||||||
|
|
||||||
|
- name (string): The name for the module.
|
||||||
|
- label-prefix (auto, string): The label-prefix for internal function
|
||||||
|
references. If `auto`, the label-prefix name will be the module name.
|
||||||
|
- nested something
|
||||||
|
- nested something 2
|
||||||
|
-> string
|
||||||
|
"###), @r###"
|
||||||
|
|
||||||
|
These again are dictionaries with the keys
|
||||||
|
- <!-- typlite:begin:list-item 0 -->`description` (optional): The description for the argument.<!-- typlite:end:list-item 0 -->
|
||||||
|
|
||||||
|
See @@show-module() for outputting the results of this function.
|
||||||
|
|
||||||
|
- <!-- typlite:begin:list-item 0 -->name (string): The name for the module.<!-- typlite:end:list-item 0 -->
|
||||||
|
- <!-- typlite:begin:list-item 0 -->label-prefix (auto, string): The label-prefix for internal function
|
||||||
|
references. If `auto`, the label-prefix name will be the module name.
|
||||||
|
- <!-- typlite:begin:list-item 1 -->nested something<!-- typlite:end:list-item 1 -->
|
||||||
|
- <!-- typlite:begin:list-item 1 -->nested something 2<!-- typlite:end:list-item 1 --><!-- typlite:end:list-item 0 -->
|
||||||
|
-> string
|
||||||
|
|
||||||
|
"###);
|
||||||
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
//! # Typlite Values
|
||||||
|
|
||||||
use core::fmt;
|
use core::fmt;
|
||||||
|
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
@ -88,6 +90,7 @@ impl<'a> Args<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
macro_rules! get_pos_named {
|
macro_rules! get_pos_named {
|
||||||
(
|
(
|
||||||
$args:expr,
|
$args:expr,
|
||||||
|
@ -97,8 +100,9 @@ macro_rules! get_pos_named {
|
||||||
$args.parse::<$ty>(raw)?
|
$args.parse::<$ty>(raw)?
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
pub(crate) use get_pos_named;
|
pub use get_pos_named;
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
macro_rules! get_named {
|
macro_rules! get_named {
|
||||||
(
|
(
|
||||||
$args:expr,
|
$args:expr,
|
||||||
|
@ -128,7 +132,7 @@ macro_rules! get_named {
|
||||||
}
|
}
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
pub(crate) use get_named;
|
pub use get_named;
|
||||||
|
|
||||||
/// Evaluate an expression.
|
/// Evaluate an expression.
|
||||||
pub trait Eval<'a>: Sized {
|
pub trait Eval<'a>: Sized {
|
||||||
|
|
|
@ -46,6 +46,11 @@
|
||||||
"name": "DevKit View",
|
"name": "DevKit View",
|
||||||
"when": "ext.tinymistActivated && ext.tinymistDevKit"
|
"when": "ext.tinymistActivated && ext.tinymistDevKit"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "tinymist.package-view",
|
||||||
|
"name": "Package",
|
||||||
|
"when": "ext.tinymistActivated"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "tinymist.preview.content-preview",
|
"id": "tinymist.preview.content-preview",
|
||||||
"type": "webview",
|
"type": "webview",
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import * as vscode from "vscode";
|
import * as vscode from "vscode";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import { readFile, writeFile } from "fs/promises";
|
import { readFile, writeFile } from "fs/promises";
|
||||||
import { getFocusingFile, getLastFocusingDoc } from "./extension";
|
|
||||||
import { tinymist } from "./lsp";
|
import { tinymist } from "./lsp";
|
||||||
|
import { extensionState, ExtensionContext } from "./state";
|
||||||
|
|
||||||
async function loadHTMLFile(context: vscode.ExtensionContext, relativePath: string) {
|
async function loadHTMLFile(context: ExtensionContext, relativePath: string) {
|
||||||
const filePath = path.resolve(context.extensionPath, relativePath);
|
const filePath = path.resolve(context.extensionPath, relativePath);
|
||||||
const fileContents = await readFile(filePath, "utf8");
|
const fileContents = await readFile(filePath, "utf8");
|
||||||
return fileContents;
|
return fileContents;
|
||||||
|
@ -25,7 +25,7 @@ export interface PackageData {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getUserPackageData(context: vscode.ExtensionContext) {
|
export function getUserPackageData(context: ExtensionContext) {
|
||||||
const defaultPackageData: Versioned<PackageData> = {
|
const defaultPackageData: Versioned<PackageData> = {
|
||||||
version: USER_PACKAGE_VERSION,
|
version: USER_PACKAGE_VERSION,
|
||||||
data: {},
|
data: {},
|
||||||
|
@ -106,7 +106,7 @@ export const fontsExportDefaultConfigure: fontsExportConfigure = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getFontsExportConfigure(context: vscode.ExtensionContext) {
|
export function getFontsExportConfigure(context: ExtensionContext) {
|
||||||
const defaultConfigure: Versioned<fontsExportConfigure> = {
|
const defaultConfigure: Versioned<fontsExportConfigure> = {
|
||||||
version: FONTS_EXPORT_CONFIGURE_VERSION,
|
version: FONTS_EXPORT_CONFIGURE_VERSION,
|
||||||
data: fontsExportDefaultConfigure,
|
data: fontsExportDefaultConfigure,
|
||||||
|
@ -120,14 +120,19 @@ export function getFontsExportConfigure(context: vscode.ExtensionContext) {
|
||||||
return configure;
|
return configure;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type EditorToolName = "template-gallery" | "tracing" | "summary" | "symbol-view";
|
const Standalone: Partial<Record<EditorToolName, boolean>> = {
|
||||||
export async function activateEditorTool(context: vscode.ExtensionContext, tool: EditorToolName) {
|
"symbol-view": true,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type EditorToolName = "template-gallery" | "tracing" | "summary" | "symbol-view" | "docs";
|
||||||
|
export async function editorTool(context: ExtensionContext, tool: EditorToolName, opts?: any) {
|
||||||
// Create and show a new WebView
|
// Create and show a new WebView
|
||||||
const title = {
|
const title = {
|
||||||
"template-gallery": "Template Gallery",
|
"template-gallery": "Template Gallery",
|
||||||
"symbol-view": "Symbol View",
|
"symbol-view": "Symbol View",
|
||||||
tracing: "Tracing",
|
tracing: "Tracing",
|
||||||
summary: "Summary",
|
summary: "Summary",
|
||||||
|
docs: `@${opts?.pkg?.namespace}/${opts?.pkg?.name}:${opts?.pkg?.version} (Docs)`,
|
||||||
}[tool];
|
}[tool];
|
||||||
const panel = vscode.window.createWebviewPanel(
|
const panel = vscode.window.createWebviewPanel(
|
||||||
`tinymist-${tool}`,
|
`tinymist-${tool}`,
|
||||||
|
@ -139,16 +144,17 @@ export async function activateEditorTool(context: vscode.ExtensionContext, tool:
|
||||||
{
|
{
|
||||||
enableScripts: true,
|
enableScripts: true,
|
||||||
retainContextWhenHidden: true,
|
retainContextWhenHidden: true,
|
||||||
|
enableFindWidget: tool === "docs",
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
await activateEditorToolAt(context, tool, panel);
|
await editorToolAt(context, tool, panel, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SymbolViewProvider implements vscode.WebviewViewProvider {
|
export class SymbolViewProvider implements vscode.WebviewViewProvider {
|
||||||
static readonly Name = "tinymist.side-symbol-view";
|
static readonly Name = "tinymist.side-symbol-view";
|
||||||
|
|
||||||
constructor(private context: vscode.ExtensionContext) {}
|
constructor(private context: ExtensionContext) {}
|
||||||
|
|
||||||
public resolveWebviewView(
|
public resolveWebviewView(
|
||||||
webviewView: vscode.WebviewView,
|
webviewView: vscode.WebviewView,
|
||||||
|
@ -160,14 +166,15 @@ export class SymbolViewProvider implements vscode.WebviewViewProvider {
|
||||||
enableScripts: true,
|
enableScripts: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
activateEditorToolAt(this.context, "symbol-view", webviewView);
|
editorToolAt(this.context, "symbol-view", webviewView);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function activateEditorToolAt(
|
async function editorToolAt(
|
||||||
context: vscode.ExtensionContext,
|
context: ExtensionContext,
|
||||||
tool: "template-gallery" | "tracing" | "summary" | "symbol-view",
|
tool: EditorToolName,
|
||||||
panel: vscode.WebviewView | vscode.WebviewPanel,
|
panel: vscode.WebviewView | vscode.WebviewPanel,
|
||||||
|
opts?: any,
|
||||||
) {
|
) {
|
||||||
const dispose = () => {
|
const dispose = () => {
|
||||||
// if has dispose method
|
// if has dispose method
|
||||||
|
@ -220,7 +227,7 @@ async function activateEditorToolAt(
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "editText": {
|
case "editText": {
|
||||||
const activeDocument = getLastFocusingDoc();
|
const activeDocument = extensionState.getFocusingDoc();
|
||||||
if (!activeDocument) {
|
if (!activeDocument) {
|
||||||
await vscode.window.showErrorMessage("No focusing document");
|
await vscode.window.showErrorMessage("No focusing document");
|
||||||
return;
|
return;
|
||||||
|
@ -333,7 +340,8 @@ async function activateEditorToolAt(
|
||||||
disposed = true;
|
disposed = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
let html = await loadHTMLFile(context, "./out/editor-tools/index.html");
|
const appDir = Standalone[tool] ? tool : "default";
|
||||||
|
let html = await loadHTMLFile(context, `./out/editor-tools/${appDir}/index.html`);
|
||||||
// packageData
|
// packageData
|
||||||
|
|
||||||
html = html.replace(
|
html = html.replace(
|
||||||
|
@ -350,7 +358,7 @@ async function activateEditorToolAt(
|
||||||
html = html.replace(":[[preview:FavoritePlaceholder]]:", btoa(packageData));
|
html = html.replace(":[[preview:FavoritePlaceholder]]:", btoa(packageData));
|
||||||
break;
|
break;
|
||||||
case "tracing": {
|
case "tracing": {
|
||||||
const focusingFile = getFocusingFile();
|
const focusingFile = extensionState.getFocusingFile();
|
||||||
if (focusingFile === undefined) {
|
if (focusingFile === undefined) {
|
||||||
await vscode.window.showErrorMessage("No focusing typst file");
|
await vscode.window.showErrorMessage("No focusing typst file");
|
||||||
return;
|
return;
|
||||||
|
@ -406,6 +414,10 @@ async function activateEditorToolAt(
|
||||||
html = html.replace(":[[preview:SymbolInformation]]:", btoa(symbolInfo));
|
html = html.replace(":[[preview:SymbolInformation]]:", btoa(symbolInfo));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "docs": {
|
||||||
|
html = html.replace(":[[preview:DocContent]]:", btoa(encodeURIComponent(opts.content)));
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
panel.webview.html = html;
|
panel.webview.html = html;
|
||||||
|
@ -420,7 +432,7 @@ async function fetchSummaryInfo(): Promise<[any | undefined, any | undefined]> {
|
||||||
let res: [any | undefined, any | undefined] = [undefined, undefined];
|
let res: [any | undefined, any | undefined] = [undefined, undefined];
|
||||||
|
|
||||||
for (const to of waitTimeList) {
|
for (const to of waitTimeList) {
|
||||||
const focusingFile = getFocusingFile();
|
const focusingFile = extensionState.getFocusingFile();
|
||||||
if (focusingFile === undefined) {
|
if (focusingFile === undefined) {
|
||||||
await vscode.window.showErrorMessage("No focusing typst file");
|
await vscode.window.showErrorMessage("No focusing typst file");
|
||||||
return res;
|
return res;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// The module 'vscode' contains the VS Code extensibility API
|
// The module 'vscode' contains the VS Code extensibility API
|
||||||
// Import the module and reference it with the alias vscode in your code below
|
// Import the module and reference it with the alias vscode in your code below
|
||||||
import * as vscode from "vscode";
|
import * as vscode from "vscode";
|
||||||
import { previewActivate, previewDeactivate } from "./preview";
|
import { previewActivate, previewDeactivate } from "./features/preview";
|
||||||
|
|
||||||
// This method is called when your extension is activated
|
// This method is called when your extension is activated
|
||||||
// Your extension is activated the very first time the command is executed
|
// Your extension is activated the very first time the command is executed
|
||||||
|
|
|
@ -20,25 +20,26 @@ import { loadTinymistConfig, substVscodeVarsInConfig } from "./config";
|
||||||
import {
|
import {
|
||||||
EditorToolName,
|
EditorToolName,
|
||||||
SymbolViewProvider as SymbolViewProvider,
|
SymbolViewProvider as SymbolViewProvider,
|
||||||
activateEditorTool,
|
editorTool,
|
||||||
getUserPackageData,
|
getUserPackageData,
|
||||||
} from "./editor-tools";
|
} from "./editor-tools";
|
||||||
import { triggerStatusBar, wordCountItemProcess } from "./ui-extends";
|
import { triggerStatusBar, wordCountItemProcess } from "./ui-extends";
|
||||||
import { setIsTinymist as previewSetIsTinymist } from "./preview-compat";
|
import { setIsTinymist as previewSetIsTinymist } from "./features/preview-compat";
|
||||||
import {
|
import {
|
||||||
previewActivate,
|
previewActivate,
|
||||||
previewDeactivate,
|
previewDeactivate,
|
||||||
previewPreload,
|
previewPreload,
|
||||||
previewProcessOutline,
|
previewProcessOutline,
|
||||||
} from "./preview";
|
} from "./features/preview";
|
||||||
import { commandCreateLocalPackage, commandOpenLocalPackage } from "./package-manager";
|
import { commandCreateLocalPackage, commandOpenLocalPackage } from "./package-manager";
|
||||||
import { activeTypstEditor, DisposeList, getSensibleTextEditorColumn } from "./util";
|
import { activeTypstEditor, DisposeList, getSensibleTextEditorColumn } from "./util";
|
||||||
import { client, getClient, setClient, tinymist } from "./lsp";
|
import { client, getClient, setClient, tinymist } from "./lsp";
|
||||||
import { taskActivate } from "./tasks";
|
import { taskActivate } from "./features/tasks";
|
||||||
import { onEnterHandler } from "./lsp.on-enter";
|
import { onEnterHandler } from "./lsp.on-enter";
|
||||||
import { extensionState } from "./state";
|
import { extensionState } from "./state";
|
||||||
import { devKitActivate } from "./dev-kit";
|
import { devKitFeatureActivate } from "./features/dev-kit";
|
||||||
import { labelViewActivate } from "./label";
|
import { labelFeatureActivate } from "./features/label";
|
||||||
|
import { packageFeatureActivate } from "./features/package";
|
||||||
|
|
||||||
export async function activate(context: ExtensionContext): Promise<void> {
|
export async function activate(context: ExtensionContext): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
@ -68,12 +69,13 @@ export async function doActivate(context: ExtensionContext): Promise<void> {
|
||||||
const client = initClient(context, config);
|
const client = initClient(context, config);
|
||||||
setClient(client);
|
setClient(client);
|
||||||
// Activates features
|
// Activates features
|
||||||
labelViewActivate(context);
|
labelFeatureActivate(context);
|
||||||
|
packageFeatureActivate(context);
|
||||||
if (extensionState.features.task) {
|
if (extensionState.features.task) {
|
||||||
taskActivate(context);
|
taskActivate(context);
|
||||||
}
|
}
|
||||||
if (extensionState.features.devKit) {
|
if (extensionState.features.devKit) {
|
||||||
devKitActivate(context);
|
devKitFeatureActivate(context);
|
||||||
}
|
}
|
||||||
if (extensionState.features.preview) {
|
if (extensionState.features.preview) {
|
||||||
const typstPreviewExtension = vscode.extensions.getExtension("mgt19937.typst-preview");
|
const typstPreviewExtension = vscode.extensions.getExtension("mgt19937.typst-preview");
|
||||||
|
@ -224,15 +226,15 @@ async function startClient(client: LanguageClient, context: ExtensionContext): P
|
||||||
);
|
);
|
||||||
context.subscriptions.push(
|
context.subscriptions.push(
|
||||||
vscode.workspace.onDidCloseTextDocument((doc: vscode.TextDocument) => {
|
vscode.workspace.onDidCloseTextDocument((doc: vscode.TextDocument) => {
|
||||||
if (focusingDoc === doc) {
|
if (extensionState.mut.focusingDoc === doc) {
|
||||||
focusingDoc = undefined;
|
extensionState.mut.focusingDoc = undefined;
|
||||||
commandActivateDoc(undefined);
|
commandActivateDoc(undefined);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const editorToolCommand = (tool: EditorToolName) => async () => {
|
const editorToolCommand = (tool: EditorToolName) => async () => {
|
||||||
await activateEditorTool(context, tool);
|
await editorTool(context, tool);
|
||||||
};
|
};
|
||||||
|
|
||||||
const initTemplateCommand =
|
const initTemplateCommand =
|
||||||
|
@ -651,15 +653,6 @@ async function initTemplate(context: vscode.ExtensionContext, inPlace: boolean,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let focusingFile: string | undefined = undefined;
|
|
||||||
let focusingDoc: vscode.TextDocument | undefined = undefined;
|
|
||||||
export function getFocusingFile() {
|
|
||||||
return focusingFile;
|
|
||||||
}
|
|
||||||
export function getLastFocusingDoc() {
|
|
||||||
return focusingDoc;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function commandActivateDoc(doc: vscode.TextDocument | undefined): Promise<void> {
|
async function commandActivateDoc(doc: vscode.TextDocument | undefined): Promise<void> {
|
||||||
await commandActivateDocPath(doc, doc?.uri.fsPath);
|
await commandActivateDocPath(doc, doc?.uri.fsPath);
|
||||||
}
|
}
|
||||||
|
@ -669,15 +662,15 @@ async function commandActivateDocPath(
|
||||||
fsPath: string | undefined,
|
fsPath: string | undefined,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// console.log("focus main", fsPath, new Error().stack);
|
// console.log("focus main", fsPath, new Error().stack);
|
||||||
focusingFile = fsPath;
|
extensionState.mut.focusingFile = fsPath;
|
||||||
if (fsPath) {
|
if (fsPath) {
|
||||||
focusingDoc = doc;
|
extensionState.mut.focusingDoc = doc;
|
||||||
}
|
}
|
||||||
if (focusingDoc?.isClosed) {
|
if (extensionState.mut.focusingDoc?.isClosed) {
|
||||||
focusingDoc = undefined;
|
extensionState.mut.focusingDoc = undefined;
|
||||||
}
|
}
|
||||||
// remove the status bar until the last focusing file is closed
|
// remove the status bar until the last focusing file is closed
|
||||||
triggerStatusBar(!!(fsPath || focusingDoc?.isClosed === false));
|
triggerStatusBar(!!(fsPath || extensionState.mut.focusingDoc?.isClosed === false));
|
||||||
await client?.sendRequest("workspace/executeCommand", {
|
await client?.sendRequest("workspace/executeCommand", {
|
||||||
command: "tinymist.focusMain",
|
command: "tinymist.focusMain",
|
||||||
arguments: [fsPath],
|
arguments: [fsPath],
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
import * as vscode from "vscode";
|
import * as vscode from "vscode";
|
||||||
|
|
||||||
export function devKitActivate(context: vscode.ExtensionContext) {
|
export function devKitFeatureActivate(context: vscode.ExtensionContext) {
|
||||||
vscode.commands.executeCommand("setContext", "ext.tinymistDevKit", true);
|
vscode.commands.executeCommand("setContext", "ext.tinymistDevKit", true);
|
||||||
|
|
||||||
const devKitProvider = new DevKitProvider();
|
const devKitProvider = new DevKitViewProvider();
|
||||||
context.subscriptions.push(
|
context.subscriptions.push(
|
||||||
vscode.window.registerTreeDataProvider("tinymist.dev-kit", devKitProvider),
|
vscode.window.registerTreeDataProvider("tinymist.dev-kit", devKitProvider),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class DevKitProvider implements vscode.TreeDataProvider<DevKitItem> {
|
class DevKitViewProvider implements vscode.TreeDataProvider<DevKitItem> {
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|
||||||
refresh(): void {}
|
refresh(): void {}
|
|
@ -1,9 +1,8 @@
|
||||||
import * as vscode from "vscode";
|
import * as vscode from "vscode";
|
||||||
import { tinymist } from "./lsp";
|
import { tinymist } from "../lsp";
|
||||||
import { WorkspaceSymbol } from "vscode-languageclient";
|
|
||||||
|
|
||||||
export function labelViewActivate(context: vscode.ExtensionContext) {
|
export function labelFeatureActivate(context: vscode.ExtensionContext) {
|
||||||
const labelViewProvider = new LabelViewProviderProvider();
|
const labelViewProvider = new LabelViewProvider();
|
||||||
context.subscriptions.push(
|
context.subscriptions.push(
|
||||||
vscode.window.registerTreeDataProvider("tinymist.label-view", labelViewProvider),
|
vscode.window.registerTreeDataProvider("tinymist.label-view", labelViewProvider),
|
||||||
// tinymist.syncLabel
|
// tinymist.syncLabel
|
||||||
|
@ -13,7 +12,7 @@ export function labelViewActivate(context: vscode.ExtensionContext) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class LabelViewProviderProvider implements vscode.TreeDataProvider<LabelViewItem> {
|
class LabelViewProvider implements vscode.TreeDataProvider<LabelViewItem> {
|
||||||
changeTreeDataEvent = new vscode.EventEmitter<LabelViewItem | undefined>();
|
changeTreeDataEvent = new vscode.EventEmitter<LabelViewItem | undefined>();
|
||||||
onDidChangeTreeData = this.changeTreeDataEvent.event;
|
onDidChangeTreeData = this.changeTreeDataEvent.event;
|
||||||
|
|
261
editors/vscode/src/features/package.ts
Normal file
261
editors/vscode/src/features/package.ts
Normal file
|
@ -0,0 +1,261 @@
|
||||||
|
import * as vscode from "vscode";
|
||||||
|
import { PackageInfo, SymbolInfo, tinymist } from "../lsp";
|
||||||
|
import { getTargetViewColumn } from "../util";
|
||||||
|
import { editorTool } from "../editor-tools";
|
||||||
|
|
||||||
|
export function packageFeatureActivate(context: vscode.ExtensionContext) {
|
||||||
|
const packageView = new PackageViewProvider();
|
||||||
|
context.subscriptions.push(
|
||||||
|
vscode.window.registerTreeDataProvider("tinymist.package-view", packageView),
|
||||||
|
vscode.commands.registerCommand(
|
||||||
|
"tinymist.showPackageDocsInternal",
|
||||||
|
async (pkg: PackageInfo) => {
|
||||||
|
// console.log("show package docs", pkg);
|
||||||
|
//
|
||||||
|
try {
|
||||||
|
const docs = await tinymist.getResource("/package/docs", pkg);
|
||||||
|
// console.log("docs", docs);
|
||||||
|
|
||||||
|
const content = (await vscode.commands.executeCommand(
|
||||||
|
"markdown.api.render",
|
||||||
|
docs,
|
||||||
|
)) as string;
|
||||||
|
|
||||||
|
await editorTool(context, "docs", { pkg, content });
|
||||||
|
} catch (e) {
|
||||||
|
console.error("show package docs error", e);
|
||||||
|
vscode.window.showErrorMessage(`Failed to show package documentation: ${e}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class PackageViewProvider implements vscode.TreeDataProvider<vscode.TreeItem> {
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
refresh(): void {}
|
||||||
|
|
||||||
|
getTreeItem(element: NamespaceItem): vscode.TreeItem {
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
getChildren(element?: any): Thenable<vscode.TreeItem[]> {
|
||||||
|
if (element && CommandsItem.is(element)) {
|
||||||
|
return this.getCommands();
|
||||||
|
} else if (element && NamespaceItem.is(element)) {
|
||||||
|
return this.getNsPackages(element.namespace);
|
||||||
|
} else if (element && PackageGroupItem.is(element)) {
|
||||||
|
return Promise.resolve(element.packages);
|
||||||
|
} else if (element && PackageItem.is(element)) {
|
||||||
|
return this.getPackageActions(element);
|
||||||
|
} else if (element && SymbolsItem.is(element)) {
|
||||||
|
return this.getPackageSymbols(element);
|
||||||
|
} else if (element && SymbolItem.is(element)) {
|
||||||
|
console.log("symbol item children", element);
|
||||||
|
if (!element.info.children) {
|
||||||
|
return Promise.resolve([]);
|
||||||
|
}
|
||||||
|
return Promise.resolve(createPackageSymbols(element.pkg, element.info.children));
|
||||||
|
} else if (element) {
|
||||||
|
return Promise.resolve([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve([
|
||||||
|
new CommandsItem(),
|
||||||
|
...["preview", "local"].map((ns) => new NamespaceItem(ns)),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getCommands(): Promise<CommandsItem[]> {
|
||||||
|
return [
|
||||||
|
new CommandItem({
|
||||||
|
title: "Create Local Package",
|
||||||
|
command: "tinymist.createLocalPackage",
|
||||||
|
tooltip: `Create a Typst local package.`,
|
||||||
|
}),
|
||||||
|
new CommandItem({
|
||||||
|
title: "Open Local Package",
|
||||||
|
command: "tinymist.openLocalPackage",
|
||||||
|
tooltip: `Open a Typst local package.`,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getNsPackages(ns: string): Promise<NamespaceItem[]> {
|
||||||
|
const packages = await tinymist.getResource("/package/by-namespace", ns);
|
||||||
|
|
||||||
|
// group by name
|
||||||
|
const groups = new Map<string, PackageItem[]>();
|
||||||
|
for (const pkg of packages) {
|
||||||
|
const group = groups.get(pkg.name) || [];
|
||||||
|
group.push(new PackageItem(pkg));
|
||||||
|
groups.set(pkg.name, group);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(groups.entries()).map(([name, packages]) => {
|
||||||
|
return new PackageGroupItem(ns, name, packages);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPackageSymbols(element: SymbolsItem): Promise<vscode.TreeItem[]> {
|
||||||
|
return createPackageSymbols(
|
||||||
|
element.pkg,
|
||||||
|
await tinymist.getResource("/package/symbol", element.pkg.pkg),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getPackageActions(pkg: PackageItem): Promise<vscode.TreeItem[]> {
|
||||||
|
return [
|
||||||
|
new CommandItem({
|
||||||
|
title: "Documentation",
|
||||||
|
command: "tinymist.showPackageDocsInternal",
|
||||||
|
arguments: [pkg.pkg],
|
||||||
|
tooltip: `Open package documentation to side.`,
|
||||||
|
}),
|
||||||
|
new CommandItem({
|
||||||
|
title: "Open",
|
||||||
|
command: "vscode.openFolder",
|
||||||
|
arguments: [vscode.Uri.file(pkg.pkg.path), { forceNewWindow: true }],
|
||||||
|
tooltip: `Open the package directory in editor.`,
|
||||||
|
}),
|
||||||
|
new CommandItem({
|
||||||
|
title: "Reveal in File Explorer",
|
||||||
|
command: "revealFileInOS",
|
||||||
|
arguments: [vscode.Uri.file(pkg.pkg.path)],
|
||||||
|
tooltip: `Reveal the directory of the package in File Explorer.`,
|
||||||
|
}),
|
||||||
|
new SymbolsItem(pkg),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CommandsItem extends vscode.TreeItem {
|
||||||
|
static is(element: vscode.TreeItem): element is CommandsItem {
|
||||||
|
return element.contextValue === "package-commands";
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(public description = "") {
|
||||||
|
super(`commands`, vscode.TreeItemCollapsibleState.Collapsed);
|
||||||
|
this.tooltip = `package commands`;
|
||||||
|
}
|
||||||
|
|
||||||
|
contextValue = "package-commands";
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CommandItem extends vscode.TreeItem {
|
||||||
|
constructor(
|
||||||
|
public readonly command: vscode.Command,
|
||||||
|
public description = "",
|
||||||
|
) {
|
||||||
|
super(command.title, vscode.TreeItemCollapsibleState.None);
|
||||||
|
this.tooltip = this.command.tooltip || ``;
|
||||||
|
}
|
||||||
|
|
||||||
|
iconPath = new vscode.ThemeIcon("tools");
|
||||||
|
|
||||||
|
contextValue = "package-command";
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NamespaceItem extends vscode.TreeItem {
|
||||||
|
static is(element: vscode.TreeItem): element is NamespaceItem {
|
||||||
|
return element.contextValue === "package-namespace-item";
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public readonly namespace: string,
|
||||||
|
public description = "",
|
||||||
|
) {
|
||||||
|
super(`@${namespace}`, vscode.TreeItemCollapsibleState.Collapsed);
|
||||||
|
this.tooltip = `namespace: ${namespace}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
contextValue = "package-namespace-item";
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PackageGroupItem extends vscode.TreeItem {
|
||||||
|
static is(element: vscode.TreeItem): element is PackageGroupItem {
|
||||||
|
return element.contextValue === "package-group-item";
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public readonly namespace: string,
|
||||||
|
public readonly name: string,
|
||||||
|
public readonly packages: PackageItem[],
|
||||||
|
public description = `@${namespace}/${name}`,
|
||||||
|
) {
|
||||||
|
super(`${name}`, vscode.TreeItemCollapsibleState.Collapsed);
|
||||||
|
this.tooltip = `package: @${namespace}/${name}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
contextValue = "package-group-item";
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PackageItem extends vscode.TreeItem {
|
||||||
|
static is(element: vscode.TreeItem): element is PackageItem {
|
||||||
|
return element.contextValue === "package-item";
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public readonly pkg: PackageInfo,
|
||||||
|
public description = "",
|
||||||
|
) {
|
||||||
|
super(`${pkg.version}`, vscode.TreeItemCollapsibleState.Collapsed);
|
||||||
|
this.tooltip = `package: @${pkg.namespace}/${pkg.name}:${pkg.version}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
pkgId() {
|
||||||
|
return `@${this.pkg.namespace}/${this.pkg.name}:${this.pkg.version}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
contextValue = "package-item";
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SymbolsItem extends vscode.TreeItem {
|
||||||
|
static is(element: vscode.TreeItem): element is SymbolsItem {
|
||||||
|
return element.contextValue === "package-symbols-item";
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public readonly pkg: PackageItem,
|
||||||
|
public description = "",
|
||||||
|
) {
|
||||||
|
super(`symbols`, vscode.TreeItemCollapsibleState.Collapsed);
|
||||||
|
this.tooltip = `symbols in package: ${pkg.pkgId()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
contextValue = "package-symbols-item";
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SymbolItem extends vscode.TreeItem {
|
||||||
|
static is(element: vscode.TreeItem): element is SymbolItem {
|
||||||
|
return element.contextValue === "package-symbol-item";
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public readonly pkg: PackageItem,
|
||||||
|
public readonly info: SymbolInfo,
|
||||||
|
public description = "",
|
||||||
|
) {
|
||||||
|
const state =
|
||||||
|
info.children.length > 0
|
||||||
|
? vscode.TreeItemCollapsibleState.Collapsed
|
||||||
|
: vscode.TreeItemCollapsibleState.None;
|
||||||
|
super(`${info.name}`, state);
|
||||||
|
this.tooltip = `a symbol \`${info.name}\` in package: ${pkg.pkgId()}`;
|
||||||
|
this.iconPath = new vscode.ThemeIcon("symbol-" + info.kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
contextValue = "package-symbol-item";
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPackageSymbols(pkgItem: PackageItem, bases: SymbolInfo[]): vscode.TreeItem[] {
|
||||||
|
const symbols = bases.map((info) => new SymbolItem(pkgItem, info));
|
||||||
|
symbols.sort((a, b) => {
|
||||||
|
if (a.info.kind !== b.info.kind) {
|
||||||
|
return a.info.kind.localeCompare(b.info.kind);
|
||||||
|
}
|
||||||
|
return a.info.name.localeCompare(b.info.name);
|
||||||
|
});
|
||||||
|
return symbols;
|
||||||
|
}
|
|
@ -12,8 +12,8 @@ import {
|
||||||
launchPreviewInWebView,
|
launchPreviewInWebView,
|
||||||
previewProcessOutline,
|
previewProcessOutline,
|
||||||
} from "./preview";
|
} from "./preview";
|
||||||
import { tinymist } from "./lsp";
|
import { tinymist } from "../lsp";
|
||||||
import { loadHTMLFile } from "./util";
|
import { loadHTMLFile } from "../util";
|
||||||
|
|
||||||
const vscodeVariables = require("vscode-variables");
|
const vscodeVariables = require("vscode-variables");
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
// Import the module and reference it with the alias vscode in your code below
|
// Import the module and reference it with the alias vscode in your code below
|
||||||
import * as vscode from "vscode";
|
import * as vscode from "vscode";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import { DisposeList, getSensibleTextEditorColumn, getTargetViewColumn } from "./util";
|
import { DisposeList, getSensibleTextEditorColumn, getTargetViewColumn } from "../util";
|
||||||
import {
|
import {
|
||||||
launchPreviewCompat,
|
launchPreviewCompat,
|
||||||
previewActiveCompat as previewPostActivateCompat,
|
previewActiveCompat as previewPostActivateCompat,
|
||||||
|
@ -18,8 +18,8 @@ import {
|
||||||
commandScrollPreview,
|
commandScrollPreview,
|
||||||
commandStartPreview,
|
commandStartPreview,
|
||||||
registerPreviewTaskDispose,
|
registerPreviewTaskDispose,
|
||||||
} from "./extension";
|
} from "../extension";
|
||||||
import { isGitpod, translateGitpodURL } from "./gitpod";
|
import { isGitpod, translateGitpodURL } from "../gitpod";
|
||||||
|
|
||||||
function translateExternalURL(urlstr: string): string {
|
function translateExternalURL(urlstr: string): string {
|
||||||
if (isGitpod()) {
|
if (isGitpod()) {
|
|
@ -1,7 +1,7 @@
|
||||||
import * as vscode from "vscode";
|
import * as vscode from "vscode";
|
||||||
import { tinymist } from "./lsp";
|
import { tinymist } from "../lsp";
|
||||||
import { getFocusingFile } from "./extension";
|
import { VirtualConsole } from "../util";
|
||||||
import { VirtualConsole } from "./util";
|
import { extensionState } from "../state";
|
||||||
|
|
||||||
type ExportFormat = "pdf" | "png" | "svg" | "html" | "markdown" | "text" | "query" | "pdfpc";
|
type ExportFormat = "pdf" | "png" | "svg" | "html" | "markdown" | "text" | "query" | "pdfpc";
|
||||||
|
|
||||||
|
@ -98,7 +98,7 @@ const exportOps = (exportArgs: ExportArgs) => ({
|
||||||
resolveInputPath() {
|
resolveInputPath() {
|
||||||
const inputPath = exportArgs.inputPath;
|
const inputPath = exportArgs.inputPath;
|
||||||
if (inputPath === "$focused" || inputPath === undefined) {
|
if (inputPath === "$focused" || inputPath === undefined) {
|
||||||
return getFocusingFile();
|
return extensionState.getFocusingFile();
|
||||||
}
|
}
|
||||||
|
|
||||||
return inputPath;
|
return inputPath;
|
|
@ -17,10 +17,27 @@ export async function getClient(): Promise<LanguageClient> {
|
||||||
return clientPromise;
|
return clientPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PackageInfo {
|
||||||
|
path: string;
|
||||||
|
namespace: string;
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SymbolInfo {
|
||||||
|
name: string;
|
||||||
|
kind: string;
|
||||||
|
children: SymbolInfo[];
|
||||||
|
}
|
||||||
|
|
||||||
interface ResourceRoutes {
|
interface ResourceRoutes {
|
||||||
"/symbols": any;
|
"/symbols": any;
|
||||||
"/preview/index.html": string;
|
"/preview/index.html": string;
|
||||||
"/dirs/local-packages": string;
|
"/dir/package": string;
|
||||||
|
"/dir/package/local": string;
|
||||||
|
"/package/by-namespace": PackageInfo[];
|
||||||
|
"/package/symbol": SymbolInfo[];
|
||||||
|
"/package/docs": string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const tinymist = {
|
export const tinymist = {
|
||||||
|
@ -43,8 +60,8 @@ export const tinymist = {
|
||||||
arguments: args,
|
arguments: args,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
getResource<T extends keyof ResourceRoutes>(path: T) {
|
getResource<T extends keyof ResourceRoutes>(path: T, ...args: any[]) {
|
||||||
return tinymist.executeCommand<ResourceRoutes[T]>("tinymist.getResources", [path]);
|
return tinymist.executeCommand<ResourceRoutes[T]>("tinymist.getResources", [path, ...args]);
|
||||||
},
|
},
|
||||||
getWorkspaceLabels() {
|
getWorkspaceLabels() {
|
||||||
return tinymist.executeCommand<SymbolInformation[]>("tinymist.getWorkspaceLabels", []);
|
return tinymist.executeCommand<SymbolInformation[]>("tinymist.getWorkspaceLabels", []);
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
import { window, workspace } from 'vscode';
|
import { window, workspace } from "vscode";
|
||||||
import { tinymist } from './lsp';
|
import { tinymist } from "./lsp";
|
||||||
import * as fs from 'fs';
|
import * as fs from "fs";
|
||||||
|
|
||||||
// error message
|
// error message
|
||||||
export const dataDirErrorMessage = 'Can not find package directory.';
|
export const dataDirErrorMessage = "Can not find package directory.";
|
||||||
|
|
||||||
|
// todo: there is a bug since we can have both @local in data dir and cache dir
|
||||||
export async function getLocalPackagesDir() {
|
export async function getLocalPackagesDir() {
|
||||||
const packagesDir = await tinymist.getResource('/dirs/local-packages');
|
const packageDir = (await tinymist.getResource("/dir/package/local"))?.[0];
|
||||||
return packagesDir ? `${packagesDir}/local` : null;
|
return packageDir ? `${packageDir}/local` : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// typst.toml template
|
// typst.toml template
|
||||||
|
@ -17,8 +18,8 @@ const typstTomlTemplate = (name: string, version: string, entrypoint: string) =>
|
||||||
|
|
||||||
// versionCompare
|
// versionCompare
|
||||||
function versionCompare(a: string, b: string) {
|
function versionCompare(a: string, b: string) {
|
||||||
const aArr = a.split('.');
|
const aArr = a.split(".");
|
||||||
const bArr = b.split('.');
|
const bArr = b.split(".");
|
||||||
for (let i = 0; i < 3; i++) {
|
for (let i = 0; i < 3; i++) {
|
||||||
const aNum = Number(aArr[i]);
|
const aNum = Number(aArr[i]);
|
||||||
const bNum = Number(bArr[i]);
|
const bNum = Number(bArr[i]);
|
||||||
|
@ -47,10 +48,10 @@ export async function getLocalPackagesList() {
|
||||||
const localPackagesList = await fs.promises.readdir(localPackagesDir);
|
const localPackagesList = await fs.promises.readdir(localPackagesDir);
|
||||||
// get all version
|
// get all version
|
||||||
const res = [] as {
|
const res = [] as {
|
||||||
package: string,
|
package: string;
|
||||||
namespace: string,
|
namespace: string;
|
||||||
name: string,
|
name: string;
|
||||||
version: string,
|
version: string;
|
||||||
}[];
|
}[];
|
||||||
for (const localPackage of localPackagesList) {
|
for (const localPackage of localPackagesList) {
|
||||||
// if localPackage is not a directory, continue
|
// if localPackage is not a directory, continue
|
||||||
|
@ -59,16 +60,18 @@ export async function getLocalPackagesList() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// filter versions only valid version like '0.1.0'
|
// filter versions only valid version like '0.1.0'
|
||||||
const versions = (await fs.promises.readdir(`${localPackagesDir}/${localPackage}`)).filter(version => {
|
const versions = (await fs.promises.readdir(`${localPackagesDir}/${localPackage}`)).filter(
|
||||||
|
(version) => {
|
||||||
const versionReg = /^\d+\.\d+\.\d+$/;
|
const versionReg = /^\d+\.\d+\.\d+$/;
|
||||||
return versionReg.test(version);
|
return versionReg.test(version);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
// sort versions like ['1.0.0', '0.2.0', '0.1.0', '0.0.2', '0.0.1']
|
// sort versions like ['1.0.0', '0.2.0', '0.1.0', '0.0.2', '0.0.1']
|
||||||
versions.sort(versionCompare);
|
versions.sort(versionCompare);
|
||||||
for (const version of versions) {
|
for (const version of versions) {
|
||||||
res.push({
|
res.push({
|
||||||
package: `@local/${localPackage}:${version}`,
|
package: `@local/${localPackage}:${version}`,
|
||||||
namespace: 'local',
|
namespace: "local",
|
||||||
name: localPackage,
|
name: localPackage,
|
||||||
version,
|
version,
|
||||||
});
|
});
|
||||||
|
@ -88,48 +91,48 @@ export async function commandCreateLocalPackage() {
|
||||||
}
|
}
|
||||||
// 1. input package name
|
// 1. input package name
|
||||||
const packageName = await window.showInputBox({
|
const packageName = await window.showInputBox({
|
||||||
value: '',
|
value: "",
|
||||||
placeHolder: 'Please input package name',
|
placeHolder: "Please input package name",
|
||||||
validateInput: text => {
|
validateInput: (text) => {
|
||||||
return text ? null : 'Please input package name';
|
return text ? null : "Please input package name";
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
if (!packageName) {
|
if (!packageName) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 2. input package version
|
// 2. input package version
|
||||||
const packageVersion = await window.showInputBox({
|
const packageVersion = await window.showInputBox({
|
||||||
value: '0.1.0',
|
value: "0.1.0",
|
||||||
placeHolder: 'Please input package version',
|
placeHolder: "Please input package version",
|
||||||
validateInput: text => {
|
validateInput: (text) => {
|
||||||
if (!text) {
|
if (!text) {
|
||||||
return 'Please input package version';
|
return "Please input package version";
|
||||||
}
|
}
|
||||||
// make sure it is valid version like '0.1.0'
|
// make sure it is valid version like '0.1.0'
|
||||||
const versionReg = /^\d+\.\d+\.\d+$/;
|
const versionReg = /^\d+\.\d+\.\d+$/;
|
||||||
if (!versionReg.test(text)) {
|
if (!versionReg.test(text)) {
|
||||||
return 'Please input valid package version like 0.1.0';
|
return "Please input valid package version like 0.1.0";
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
if (!packageVersion) {
|
if (!packageVersion) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 3. input entrypoint
|
// 3. input entrypoint
|
||||||
const entrypoint = await window.showInputBox({
|
const entrypoint = await window.showInputBox({
|
||||||
value: 'lib.typ',
|
value: "lib.typ",
|
||||||
placeHolder: 'Please input entrypoint',
|
placeHolder: "Please input entrypoint",
|
||||||
validateInput: text => {
|
validateInput: (text) => {
|
||||||
if (!text) {
|
if (!text) {
|
||||||
return 'Please input entrypoint';
|
return "Please input entrypoint";
|
||||||
}
|
}
|
||||||
// make sure it is valid entrypoint end with .typ
|
// make sure it is valid entrypoint end with .typ
|
||||||
if (!text.endsWith('.typ')) {
|
if (!text.endsWith(".typ")) {
|
||||||
return 'Please input valid entrypoint end with .typ';
|
return "Please input valid entrypoint end with .typ";
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
if (!entrypoint) {
|
if (!entrypoint) {
|
||||||
return;
|
return;
|
||||||
|
@ -140,7 +143,7 @@ export async function commandCreateLocalPackage() {
|
||||||
await fs.promises.mkdir(packageDir, { recursive: true });
|
await fs.promises.mkdir(packageDir, { recursive: true });
|
||||||
await fs.promises.writeFile(`${packageDir}/typst.toml`, typstToml);
|
await fs.promises.writeFile(`${packageDir}/typst.toml`, typstToml);
|
||||||
// 5. create localPackagesDir/name/version/entrypoint
|
// 5. create localPackagesDir/name/version/entrypoint
|
||||||
await fs.promises.writeFile(`${packageDir}/${entrypoint}`, '#let add(a, b) = { a + b }');
|
await fs.promises.writeFile(`${packageDir}/${entrypoint}`, "#let add(a, b) = { a + b }");
|
||||||
// 6. open localPackagesDir/name/version/entrypoint
|
// 6. open localPackagesDir/name/version/entrypoint
|
||||||
const document = await workspace.openTextDocument(`${packageDir}/${entrypoint}`);
|
const document = await workspace.openTextDocument(`${packageDir}/${entrypoint}`);
|
||||||
await window.showTextDocument(document);
|
await window.showTextDocument(document);
|
||||||
|
@ -157,25 +160,25 @@ export async function commandOpenLocalPackage() {
|
||||||
}
|
}
|
||||||
// 1. select local package
|
// 1. select local package
|
||||||
const localPackagesList = await getLocalPackagesList();
|
const localPackagesList = await getLocalPackagesList();
|
||||||
const localPackages = localPackagesList.map(pkg => pkg.package);
|
const localPackages = localPackagesList.map((pkg) => pkg.package);
|
||||||
const selected = await window.showQuickPick(localPackages, {
|
const selected = await window.showQuickPick(localPackages, {
|
||||||
placeHolder: 'Please select a local package to open'
|
placeHolder: "Please select a local package to open",
|
||||||
});
|
});
|
||||||
if (!selected) {
|
if (!selected) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 2. read localPackagesDir/name/version/typst.toml
|
// 2. read localPackagesDir/name/version/typst.toml
|
||||||
const name = localPackagesList.filter(pkg => pkg.package === selected)[0].name;
|
const name = localPackagesList.filter((pkg) => pkg.package === selected)[0].name;
|
||||||
const version = localPackagesList.filter(pkg => pkg.package === selected)[0].version;
|
const version = localPackagesList.filter((pkg) => pkg.package === selected)[0].version;
|
||||||
const packageDir = `${localPackagesDir}/${name}/${version}`;
|
const packageDir = `${localPackagesDir}/${name}/${version}`;
|
||||||
// if typst.toml doesn't exist, return
|
// if typst.toml doesn't exist, return
|
||||||
try {
|
try {
|
||||||
await fs.promises.access(`${packageDir}/typst.toml`);
|
await fs.promises.access(`${packageDir}/typst.toml`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
window.showErrorMessage('Can not find typst.toml.');
|
window.showErrorMessage("Can not find typst.toml.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const typstToml = await fs.readFileSync(`${packageDir}/typst.toml`, 'utf-8');
|
const typstToml = await fs.readFileSync(`${packageDir}/typst.toml`, "utf-8");
|
||||||
// parse typst.toml
|
// parse typst.toml
|
||||||
const entrypoint = typstToml.match(/entrypoint\s*=\s*"(.*)"/)?.[1];
|
const entrypoint = typstToml.match(/entrypoint\s*=\s*"(.*)"/)?.[1];
|
||||||
if (!entrypoint) {
|
if (!entrypoint) {
|
||||||
|
|
|
@ -1,8 +1,37 @@
|
||||||
export const extensionState = {
|
import * as vscode from "vscode";
|
||||||
|
|
||||||
|
export type ExtensionContext = vscode.ExtensionContext;
|
||||||
|
|
||||||
|
interface ExtensionState {
|
||||||
|
features: {
|
||||||
|
task: boolean;
|
||||||
|
devKit: boolean;
|
||||||
|
onEnter: boolean;
|
||||||
|
preview: boolean;
|
||||||
|
};
|
||||||
|
mut: {
|
||||||
|
focusingFile: string | undefined;
|
||||||
|
focusingDoc: vscode.TextDocument | undefined;
|
||||||
|
};
|
||||||
|
getFocusingFile(): string | undefined;
|
||||||
|
getFocusingDoc(): vscode.TextDocument | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const extensionState: ExtensionState = {
|
||||||
features: {
|
features: {
|
||||||
task: true,
|
task: true,
|
||||||
devKit: false,
|
devKit: false,
|
||||||
onEnter: false,
|
onEnter: false,
|
||||||
preview: false,
|
preview: false,
|
||||||
},
|
},
|
||||||
|
mut: {
|
||||||
|
focusingFile: undefined,
|
||||||
|
focusingDoc: undefined,
|
||||||
|
},
|
||||||
|
getFocusingFile() {
|
||||||
|
return extensionState.mut.focusingFile;
|
||||||
|
},
|
||||||
|
getFocusingDoc() {
|
||||||
|
return extensionState.mut.focusingDoc;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -154,7 +154,7 @@
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script type="module" src="/src/main.ts"></script>
|
<script type="module" src="%VITE_ENTRY%"></script>
|
||||||
<div id="tinymist-app"></div>
|
<div id="tinymist-app"></div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build -- --component=symbol-view && vite build -- ",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"coverage": "vitest run --coverage"
|
"coverage": "vitest run --coverage"
|
||||||
|
|
1
tools/editor-tools/src/features/.gitignore
vendored
1
tools/editor-tools/src/features/.gitignore
vendored
|
@ -1 +1,2 @@
|
||||||
symbol-view.mock.ts
|
symbol-view.mock.ts
|
||||||
|
docs.mock.ts
|
105
tools/editor-tools/src/features/docs.css
Normal file
105
tools/editor-tools/src/features/docs.css
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
@import url("https://fonts.googleapis.com/css2?family=Merriweather:ital,wght@0,300;0,700;0,900;1,300;1,700&display=swap");
|
||||||
|
|
||||||
|
.tinymist-docs {
|
||||||
|
--mainLight: hsl(250, 68%, 74%);
|
||||||
|
--textDetailAccent: var(--mainLight);
|
||||||
|
font-family:
|
||||||
|
Merriweather,
|
||||||
|
serif,
|
||||||
|
"Inter var experimental",
|
||||||
|
"Inter var",
|
||||||
|
-apple-system,
|
||||||
|
BlinkMacSystemFont,
|
||||||
|
"Segoe UI",
|
||||||
|
Roboto,
|
||||||
|
Oxygen,
|
||||||
|
Ubuntu,
|
||||||
|
Cantarell,
|
||||||
|
"Fira Sans",
|
||||||
|
"Droid Sans",
|
||||||
|
"Helvetica Neue",
|
||||||
|
sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tinymist-docs code {
|
||||||
|
font-family: Menlo, Monaco, "JetBrains Mono", "Fira Code", Consolas,
|
||||||
|
"Courier New", monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tinymist-docs h1 {
|
||||||
|
font-size: 3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tinymist-docs h2 {
|
||||||
|
font-size: 1.7em;
|
||||||
|
margin-block-start: 1em;
|
||||||
|
margin-block-end: 0.83em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tinymist-docs h3 {
|
||||||
|
font-size: 1.17em;
|
||||||
|
margin-block-start: 1.17em;
|
||||||
|
margin-block-end: 0.83em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tinymist-docs .detail-header {
|
||||||
|
margin: 1em 0;
|
||||||
|
margin-top: 1.5em;
|
||||||
|
padding: 0.5em 1em;
|
||||||
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
|
border-left: 3px solid var(--textDetailAccent);
|
||||||
|
font-size: 1em;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tinymist-docs .detail-header h3.doc-symbol-name {
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.tinymist-docs .detail-header .doc-param-title {
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-block-start: 1.17em;
|
||||||
|
margin-block-end: 0.5em;
|
||||||
|
}
|
||||||
|
.tinymist-docs .type-int {
|
||||||
|
color: #e7d9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tinymist-docs .type-float {
|
||||||
|
color: #e7d9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tinymist-docs .type-builtin {
|
||||||
|
color: #d2a8ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tinymist-docs .type-none {
|
||||||
|
color: #ff6d00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tinymist-docs .code-kw,
|
||||||
|
.tinymist-docs .type-auto {
|
||||||
|
color: #ff6d00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tinymist-docs .code-func {
|
||||||
|
color: #79c0ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tinymist-docs .code-op {
|
||||||
|
color: #79c0ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tinymist-docs .type-inferred-as,
|
||||||
|
.tinymist-docs .code-kw.type-inferred {
|
||||||
|
transition: background-color 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tinymist-docs .type-inferred-as:hover,
|
||||||
|
.tinymist-docs .code-kw.type-inferred:hover {
|
||||||
|
background-color: #344134;
|
||||||
|
}
|
804
tools/editor-tools/src/features/docs.ts
Normal file
804
tools/editor-tools/src/features/docs.ts
Normal file
|
@ -0,0 +1,804 @@
|
||||||
|
import "./docs.css";
|
||||||
|
import van, { State, ChildDom } from "vanjs-core";
|
||||||
|
const { div, h1, h2, h3, code, a, p, i, span, strong } = van.tags;
|
||||||
|
|
||||||
|
// import { docsMock } from "./docs.mock";
|
||||||
|
const docsMock = "";
|
||||||
|
|
||||||
|
export const Docs = () => {
|
||||||
|
const parsedDocs: State<DocElement> = van.state({
|
||||||
|
contents: [],
|
||||||
|
children: [],
|
||||||
|
kind: DocKind.None,
|
||||||
|
id: "",
|
||||||
|
data: null,
|
||||||
|
} as DocElement);
|
||||||
|
|
||||||
|
const favoritePlaceholders = `:[[preview:DocContent]]:`;
|
||||||
|
van.derive(async () => {
|
||||||
|
const inp = favoritePlaceholders.startsWith(":")
|
||||||
|
? docsMock
|
||||||
|
: decodeURIComponent(atob(favoritePlaceholders));
|
||||||
|
if (!inp) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedDocs.val = await recoverDocsStructure(inp);
|
||||||
|
});
|
||||||
|
|
||||||
|
return div(
|
||||||
|
{
|
||||||
|
class: "tinymist-docs flex-col",
|
||||||
|
style: "justify-content: center; align-items: center; gap: 10px;",
|
||||||
|
},
|
||||||
|
div(
|
||||||
|
{
|
||||||
|
style: "flex: 1; width: 100%; padding: 10px",
|
||||||
|
},
|
||||||
|
(_dom?: Element) => {
|
||||||
|
const v = parsedDocs.val;
|
||||||
|
console.log("updated", v);
|
||||||
|
return div(MakeDoc(v));
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const enum TokenKind {
|
||||||
|
Text,
|
||||||
|
PackageStart,
|
||||||
|
PackageEnd,
|
||||||
|
ParamDocStart,
|
||||||
|
ParamDocEnd,
|
||||||
|
ErrorStart,
|
||||||
|
ErrorEnd,
|
||||||
|
ModuleStart,
|
||||||
|
ModuleEnd,
|
||||||
|
SymbolStart,
|
||||||
|
SymbolEnd,
|
||||||
|
SigStart,
|
||||||
|
SigEnd,
|
||||||
|
ParamStart,
|
||||||
|
ParamEnd,
|
||||||
|
Comment,
|
||||||
|
}
|
||||||
|
|
||||||
|
const enum DocKind {
|
||||||
|
None,
|
||||||
|
Package,
|
||||||
|
Module,
|
||||||
|
Symbol,
|
||||||
|
Param,
|
||||||
|
SigOrParam,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DocElement {
|
||||||
|
contents: string[];
|
||||||
|
children: DocElement[];
|
||||||
|
kind: DocKind;
|
||||||
|
id: string;
|
||||||
|
data: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function recoverDocsStructure(content: string) {
|
||||||
|
console.log("recoverDocsStructure", { content });
|
||||||
|
// split content by comment
|
||||||
|
let reg = /<!--(.*?)-->/g;
|
||||||
|
let tokenPromises = [];
|
||||||
|
let match;
|
||||||
|
let lastIndex = 0;
|
||||||
|
while ((match = reg.exec(content))) {
|
||||||
|
tokenPromises.push(
|
||||||
|
Promise.resolve([TokenKind.Text, content.slice(lastIndex, match.index)])
|
||||||
|
);
|
||||||
|
tokenPromises.push(identifyCommentToken(match[1]));
|
||||||
|
lastIndex = reg.lastIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenPromises.push(Promise.resolve(content.slice(lastIndex)));
|
||||||
|
const tokens = await Promise.all(tokenPromises);
|
||||||
|
|
||||||
|
let packageStack = [];
|
||||||
|
let structStack = [];
|
||||||
|
let current = {
|
||||||
|
contents: [],
|
||||||
|
children: [],
|
||||||
|
kind: DocKind.None,
|
||||||
|
id: "",
|
||||||
|
data: {},
|
||||||
|
} as DocElement;
|
||||||
|
let currentPkg = current;
|
||||||
|
|
||||||
|
for (const token of tokens) {
|
||||||
|
switch (token[0]) {
|
||||||
|
case TokenKind.PackageStart:
|
||||||
|
structStack.push(current);
|
||||||
|
packageStack.push(currentPkg);
|
||||||
|
current = {
|
||||||
|
contents: [],
|
||||||
|
children: [],
|
||||||
|
kind: DocKind.Package,
|
||||||
|
id: "",
|
||||||
|
data: token[1],
|
||||||
|
};
|
||||||
|
currentPkg = current;
|
||||||
|
break;
|
||||||
|
case TokenKind.PackageEnd:
|
||||||
|
const pkg = current;
|
||||||
|
current = structStack.pop()!;
|
||||||
|
currentPkg = packageStack.pop()!;
|
||||||
|
current.children.push(pkg);
|
||||||
|
break;
|
||||||
|
case TokenKind.ErrorStart:
|
||||||
|
currentPkg.data.error = token[1];
|
||||||
|
break;
|
||||||
|
case TokenKind.ErrorEnd:
|
||||||
|
break;
|
||||||
|
case TokenKind.ParamDocStart: {
|
||||||
|
structStack.push(current);
|
||||||
|
let sym = undefined;
|
||||||
|
for (let i = structStack.length - 1; i >= 0; i--) {
|
||||||
|
if (structStack[i].kind === DocKind.Symbol) {
|
||||||
|
sym = structStack[i];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
current = {
|
||||||
|
contents: [],
|
||||||
|
children: [],
|
||||||
|
kind: DocKind.Param,
|
||||||
|
id: token[1],
|
||||||
|
data: {
|
||||||
|
name: token[1],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if (sym) {
|
||||||
|
current.id = `${sym.id}-param-${token[1]}`;
|
||||||
|
const renderedParams = (sym.data.renderedParams =
|
||||||
|
sym.data.renderedParams || {});
|
||||||
|
renderedParams[current.id] = current;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case TokenKind.ParamDocEnd: {
|
||||||
|
current = structStack.pop()!;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case TokenKind.ModuleStart:
|
||||||
|
structStack.push(current);
|
||||||
|
current = {
|
||||||
|
contents: [],
|
||||||
|
children: [],
|
||||||
|
kind: DocKind.Module,
|
||||||
|
id: token[1],
|
||||||
|
data: token[2],
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case TokenKind.ModuleEnd:
|
||||||
|
const module = current;
|
||||||
|
current = structStack.pop()!;
|
||||||
|
current.children.push(module);
|
||||||
|
break;
|
||||||
|
case TokenKind.SymbolStart:
|
||||||
|
structStack.push(current);
|
||||||
|
current = {
|
||||||
|
contents: [],
|
||||||
|
children: [],
|
||||||
|
kind: DocKind.Symbol,
|
||||||
|
id: token[1],
|
||||||
|
data: token[2],
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case TokenKind.SymbolEnd:
|
||||||
|
const symbol = current;
|
||||||
|
current = structStack.pop()!;
|
||||||
|
current.children.push(symbol);
|
||||||
|
break;
|
||||||
|
case TokenKind.SigStart:
|
||||||
|
case TokenKind.ParamStart:
|
||||||
|
structStack.push(current);
|
||||||
|
current = {
|
||||||
|
contents: [],
|
||||||
|
children: [],
|
||||||
|
kind: DocKind.SigOrParam,
|
||||||
|
id: "",
|
||||||
|
data: {},
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case TokenKind.SigEnd:
|
||||||
|
case TokenKind.ParamEnd:
|
||||||
|
current = structStack.pop()!;
|
||||||
|
break;
|
||||||
|
case TokenKind.Comment:
|
||||||
|
console.log("Comment", token[1]);
|
||||||
|
break;
|
||||||
|
case TokenKind.Text:
|
||||||
|
current.contents.push(token[1]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function identifyCommentToken(comment: string) {
|
||||||
|
const cs = comment.trim().split(" ");
|
||||||
|
switch (cs[0]) {
|
||||||
|
case "begin:package":
|
||||||
|
return [TokenKind.PackageStart, JSON.parse(await base64ToUtf8(cs[1]))];
|
||||||
|
case "end:package":
|
||||||
|
return [TokenKind.PackageEnd, JSON.parse(await base64ToUtf8(cs[1]))];
|
||||||
|
case "begin:param-doc":
|
||||||
|
return [TokenKind.ParamDocStart, cs[1]];
|
||||||
|
case "end:param-doc":
|
||||||
|
return [TokenKind.ParamDocEnd, cs[1]];
|
||||||
|
case "begin:errors":
|
||||||
|
return [TokenKind.ErrorStart, JSON.parse(await base64ToUtf8(cs[1]))];
|
||||||
|
case "end:errors":
|
||||||
|
return [TokenKind.ErrorEnd, cs[1]];
|
||||||
|
case "begin:module":
|
||||||
|
return [
|
||||||
|
TokenKind.ModuleStart,
|
||||||
|
cs[1],
|
||||||
|
JSON.parse(await base64ToUtf8(cs[2])),
|
||||||
|
];
|
||||||
|
case "end:module":
|
||||||
|
return [TokenKind.ModuleEnd, cs[1]];
|
||||||
|
case "begin:symbol":
|
||||||
|
return [
|
||||||
|
TokenKind.SymbolStart,
|
||||||
|
cs[1],
|
||||||
|
JSON.parse(await base64ToUtf8(cs[2])),
|
||||||
|
];
|
||||||
|
case "end:symbol":
|
||||||
|
return [TokenKind.SymbolEnd, cs[1]];
|
||||||
|
case "begin:sig":
|
||||||
|
return [TokenKind.SigStart, cs[1]];
|
||||||
|
case "end:sig":
|
||||||
|
return [TokenKind.SigEnd, cs[1]];
|
||||||
|
case "begin:param":
|
||||||
|
return [TokenKind.ParamStart, cs[1]];
|
||||||
|
case "end:param":
|
||||||
|
return [TokenKind.ParamEnd, cs[1]];
|
||||||
|
case "typlite:begin:list-item":
|
||||||
|
case "typlite:end:list-item":
|
||||||
|
return [TokenKind.Text, ""];
|
||||||
|
default:
|
||||||
|
return [TokenKind.Comment, comment];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function base64ToUtf8(base64: string) {
|
||||||
|
const base64Url = `data:text/plain;base64,${base64}`;
|
||||||
|
const res = await fetch(base64Url);
|
||||||
|
return await res.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getKnownModules(v: DocElement, s: Set<string>) {
|
||||||
|
for (const child of v.children) {
|
||||||
|
if (child.kind === DocKind.Module) {
|
||||||
|
s.add(child.id);
|
||||||
|
}
|
||||||
|
getKnownModules(child, s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function MakeDoc(v: DocElement) {
|
||||||
|
const knownModules = new Set<string>();
|
||||||
|
getKnownModules(v, knownModules);
|
||||||
|
console.log("MakeDoc", v, knownModules);
|
||||||
|
|
||||||
|
function Item(v: DocElement): ChildDom {
|
||||||
|
switch (v.kind) {
|
||||||
|
case DocKind.Package:
|
||||||
|
return PackageItem(v);
|
||||||
|
case DocKind.Module:
|
||||||
|
if (!v.data.prefix) {
|
||||||
|
return ModuleBody(v);
|
||||||
|
}
|
||||||
|
return ModuleItem(v);
|
||||||
|
case DocKind.Symbol:
|
||||||
|
const kind = v.data.kind;
|
||||||
|
|
||||||
|
switch (kind) {
|
||||||
|
case "function":
|
||||||
|
return FuncItem(v);
|
||||||
|
case "constant":
|
||||||
|
return ConstItem(v);
|
||||||
|
case "module":
|
||||||
|
return ModuleRefItem(v);
|
||||||
|
default:
|
||||||
|
return div();
|
||||||
|
}
|
||||||
|
case DocKind.None:
|
||||||
|
return div(...v.children.map(Item));
|
||||||
|
default:
|
||||||
|
return div();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ModuleBody(v: DocElement) {
|
||||||
|
const modules = [];
|
||||||
|
const functions = [];
|
||||||
|
const constants = [];
|
||||||
|
const unknowns = [];
|
||||||
|
for (const child of v.children) {
|
||||||
|
switch (child.kind) {
|
||||||
|
case DocKind.Module:
|
||||||
|
modules.push(child);
|
||||||
|
break;
|
||||||
|
case DocKind.Symbol:
|
||||||
|
switch (child.data.kind) {
|
||||||
|
case "function":
|
||||||
|
functions.push(child);
|
||||||
|
break;
|
||||||
|
case "constant":
|
||||||
|
constants.push(child);
|
||||||
|
break;
|
||||||
|
case "module":
|
||||||
|
modules.push(child);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
unknowns.push(child);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const chs = [];
|
||||||
|
|
||||||
|
if (modules.length > 0) {
|
||||||
|
chs.push(h2("Modules"), div(...modules.map(ModuleRefItem)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (constants.length > 0) {
|
||||||
|
chs.push(h2("Constants"), div(...constants.map(Item)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (functions.length > 0) {
|
||||||
|
chs.push(h2("Functions"), div(...functions.map(Item)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unknowns.length > 0) {
|
||||||
|
chs.push(h2("Unknowns"), div(...unknowns.map(Item)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return div(...chs);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ModuleItem(v: DocElement) {
|
||||||
|
return div(
|
||||||
|
{ class: "tinymist-module" },
|
||||||
|
h1({ id: `module-${v.id}` }, `Module: ${v.data.prefix}`),
|
||||||
|
ModuleBody(v)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PackageItem(v: DocElement) {
|
||||||
|
console.log("PackageItem", v);
|
||||||
|
return div(
|
||||||
|
h1(`@${v.data.namespace}/${v.data.name}:${v.data.version}`),
|
||||||
|
p(
|
||||||
|
span(
|
||||||
|
"This documentation is generated locally. Please submit issues to "
|
||||||
|
),
|
||||||
|
a(
|
||||||
|
{ href: "https://github.com/Myriad-Dreamin/tinymist/issues" },
|
||||||
|
"tinymist"
|
||||||
|
),
|
||||||
|
span(" if you see "),
|
||||||
|
strong(i("incorrect")),
|
||||||
|
span(" information in it.")
|
||||||
|
),
|
||||||
|
// ModuleBody(v)
|
||||||
|
...v.children.map(Item)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ModuleRefItem(v: DocElement) {
|
||||||
|
const isExternal = !knownModules.has(v.id);
|
||||||
|
|
||||||
|
let body;
|
||||||
|
if (isExternal) {
|
||||||
|
body = code("external ", v.data.name);
|
||||||
|
} else {
|
||||||
|
body = code(
|
||||||
|
a(
|
||||||
|
{
|
||||||
|
href: `#module-${v.id}`,
|
||||||
|
},
|
||||||
|
v.data.name
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return div(
|
||||||
|
{
|
||||||
|
class: "tinymist-module-ref",
|
||||||
|
},
|
||||||
|
div(
|
||||||
|
{
|
||||||
|
class: `detail-header doc-symbol-${v.data.kind}`,
|
||||||
|
|
||||||
|
// <a href="https://github.com/elixir-lang/elixir/blob/v1.17.2/lib/elixir/lib/float.ex#L283" class="icon-action" rel="help" title="View Source">
|
||||||
|
// <i class="ri-code-s-slash-line" aria-hidden="true"></i>
|
||||||
|
// <span class="sr-only">View Source</span>
|
||||||
|
// </a>
|
||||||
|
},
|
||||||
|
h3({ class: "doc-symbol-name" }, body)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DocParam {
|
||||||
|
name: string;
|
||||||
|
cano_type: [string, string];
|
||||||
|
expr?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function FuncItem(v: DocElement) {
|
||||||
|
const sig = v.data.signature;
|
||||||
|
let funcTitle = [code(v.data.name), "("];
|
||||||
|
if (sig) {
|
||||||
|
// funcTitle.push(...sig.pos.map((e: DocParam) => code(e.name)));
|
||||||
|
for (let i = 0; i < sig.pos.length; i++) {
|
||||||
|
if (i > 0) {
|
||||||
|
funcTitle.push(code(", "));
|
||||||
|
}
|
||||||
|
funcTitle.push(code(sig.pos[i].name));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sig.rest || Object.keys(sig.named).length > 0) {
|
||||||
|
if (sig.pos.length > 0) {
|
||||||
|
funcTitle.push(code(", "));
|
||||||
|
}
|
||||||
|
funcTitle.push(code(".."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
funcTitle.push(code(")"));
|
||||||
|
if (v.data.parsed_docs?.return_ty) {
|
||||||
|
funcTitle.push(code(" -> "));
|
||||||
|
typeHighlighted(v.data.parsed_docs.return_ty, funcTitle);
|
||||||
|
}
|
||||||
|
|
||||||
|
return div(
|
||||||
|
{
|
||||||
|
class: "tinymist-symbol",
|
||||||
|
},
|
||||||
|
div(
|
||||||
|
{
|
||||||
|
class: `detail-header doc-symbol-${v.data.kind}`,
|
||||||
|
},
|
||||||
|
h3({ class: "doc-symbol-name" }, code(...funcTitle))
|
||||||
|
),
|
||||||
|
...SigPreview(v),
|
||||||
|
div({
|
||||||
|
style: "margin-left: 0.62em",
|
||||||
|
innerHTML: v.contents.join(""),
|
||||||
|
}),
|
||||||
|
...SigDocs(v)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DocSignature {
|
||||||
|
pos: DocParam[];
|
||||||
|
rest: DocParam;
|
||||||
|
named: Record<string, DocParam>;
|
||||||
|
ret_ty?: [string, string];
|
||||||
|
// return;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SigDocs(v: DocElement): ChildDom[] {
|
||||||
|
const sig: DocSignature = v.data.signature;
|
||||||
|
const parsed_docs = v.data.parsed_docs;
|
||||||
|
const res: ChildDom[] = [];
|
||||||
|
|
||||||
|
if (!sig) {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
const docsMapping = new Map<string, any>();
|
||||||
|
// for (const doc of parsed_docs) {
|
||||||
|
// docsMapping.set(doc.name, doc.contents.join(""));
|
||||||
|
// }
|
||||||
|
// return_ty
|
||||||
|
if (parsed_docs?.params) {
|
||||||
|
for (const param of parsed_docs.params) {
|
||||||
|
docsMapping.set(param.name, param);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (v.data.renderedParams) {
|
||||||
|
for (const p of Object.values(v.data.renderedParams)) {
|
||||||
|
const param = p as DocElement;
|
||||||
|
const docs = param.contents.join("");
|
||||||
|
const prev = docsMapping.get(param.data.name) || {};
|
||||||
|
prev.docs = docs;
|
||||||
|
docsMapping.set(param.data.name, prev);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
interface TaggedParam {
|
||||||
|
kind: string;
|
||||||
|
param: DocParam;
|
||||||
|
}
|
||||||
|
|
||||||
|
const paramsAll: TaggedParam[] = [
|
||||||
|
...sig.pos.map((param: DocParam) => ({ kind: "pos", param })),
|
||||||
|
...(sig.rest ? [{ kind: "rest", param: sig.rest }] : []),
|
||||||
|
...Object.entries(sig.named).map(([name, param]) => ({
|
||||||
|
kind: "named",
|
||||||
|
name,
|
||||||
|
param,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (parsed_docs?.return_ty || sig.ret_ty) {
|
||||||
|
let paramTitle = [codeHl("op", "-> ")];
|
||||||
|
sigTypeHighlighted(parsed_docs.return_ty, sig.ret_ty, paramTitle);
|
||||||
|
|
||||||
|
res.push(h3("Resultant"));
|
||||||
|
res.push(
|
||||||
|
div(
|
||||||
|
{
|
||||||
|
style: "margin-left: 0.62em",
|
||||||
|
},
|
||||||
|
div(
|
||||||
|
{
|
||||||
|
style: "margin-left: 0.62em",
|
||||||
|
},
|
||||||
|
div(
|
||||||
|
{
|
||||||
|
class: "doc-param-title",
|
||||||
|
},
|
||||||
|
strong(paramTitle)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (paramsAll.length) {
|
||||||
|
res.push(h3("Parameters"));
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("SigDocs", { paramsAll, docsMapping });
|
||||||
|
|
||||||
|
for (const { kind, param } of paramsAll) {
|
||||||
|
let docs: string[] = [];
|
||||||
|
const docsMeta = docsMapping.get(param.name);
|
||||||
|
if (docsMeta?.docs) {
|
||||||
|
docs = [docsMeta.docs];
|
||||||
|
}
|
||||||
|
|
||||||
|
let paramTitle = [
|
||||||
|
code(
|
||||||
|
{
|
||||||
|
id: `param-${v.id}-${param.name}`,
|
||||||
|
},
|
||||||
|
param.name
|
||||||
|
),
|
||||||
|
];
|
||||||
|
if (docsMeta?.types || param.cano_type) {
|
||||||
|
paramTitle.push(code(": "));
|
||||||
|
// paramTitle += `: ${docsMeta.types}`;
|
||||||
|
sigTypeHighlighted(docsMeta?.types, param.cano_type, paramTitle);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (param.expr) {
|
||||||
|
paramTitle.push(codeHl("op", " = "));
|
||||||
|
paramTitle.push(code(param.expr));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kind == "pos") {
|
||||||
|
paramTitle.push(code(" (positional)"));
|
||||||
|
} else if (kind == "rest") {
|
||||||
|
paramTitle.push(code(" (rest)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const docsAll = docs.join("");
|
||||||
|
|
||||||
|
res.push(
|
||||||
|
div(
|
||||||
|
{
|
||||||
|
style: "margin-left: 0.62em",
|
||||||
|
},
|
||||||
|
div(
|
||||||
|
{
|
||||||
|
class: "doc-param-title",
|
||||||
|
},
|
||||||
|
strong(code(paramTitle))
|
||||||
|
),
|
||||||
|
div({
|
||||||
|
style: "margin-left: 0.62em",
|
||||||
|
innerHTML: docsAll ? docsAll : "<p>-</p>",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SigPreview(v: DocElement): ChildDom[] {
|
||||||
|
const sig = v.data.signature;
|
||||||
|
if (!sig) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const res: ChildDom[] = [];
|
||||||
|
const paramsAll = [
|
||||||
|
...sig.pos.map((param: DocParam) => ({ kind: "pos", param })),
|
||||||
|
...Object.entries(sig.named).map(([name, param]) => ({
|
||||||
|
kind: "named",
|
||||||
|
name,
|
||||||
|
param,
|
||||||
|
})),
|
||||||
|
...(sig.rest ? [{ kind: "rest", param: sig.rest }] : []),
|
||||||
|
];
|
||||||
|
// ...paramsAll.map(({ kind, param }, i) => {
|
||||||
|
// if (i > 0) {
|
||||||
|
// return code(", ");
|
||||||
|
// }
|
||||||
|
// return code(param.name);
|
||||||
|
// }),
|
||||||
|
|
||||||
|
const sigTitle = [
|
||||||
|
code(kwHl("let")),
|
||||||
|
code(" "),
|
||||||
|
code(fnHl(v.data.name)),
|
||||||
|
code("("),
|
||||||
|
];
|
||||||
|
for (let i = 0; i < paramsAll.length; i++) {
|
||||||
|
if (i > 0) {
|
||||||
|
sigTitle.push(code(", "));
|
||||||
|
}
|
||||||
|
let paramTitle = [];
|
||||||
|
if (paramsAll[i].kind == "rest") {
|
||||||
|
paramTitle.push(code(".."));
|
||||||
|
}
|
||||||
|
paramTitle.push(code(paramsAll[i].param.name));
|
||||||
|
if (paramsAll[i].kind == "named") {
|
||||||
|
paramTitle.push(code("?"));
|
||||||
|
}
|
||||||
|
sigTitle.push(
|
||||||
|
a(
|
||||||
|
{
|
||||||
|
href: `#param-${v.id}-${paramsAll[i].param.name}`,
|
||||||
|
},
|
||||||
|
...paramTitle
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
sigTitle.push(code(")"));
|
||||||
|
if (v.data.parsed_docs?.return_ty) {
|
||||||
|
sigTitle.push(code(" -> "));
|
||||||
|
typeHighlighted(v.data.parsed_docs.return_ty, sigTitle);
|
||||||
|
}
|
||||||
|
sigTitle.push(code(";"));
|
||||||
|
|
||||||
|
res.push(
|
||||||
|
div(
|
||||||
|
{ style: "margin-left: 0.62em" },
|
||||||
|
div({
|
||||||
|
style: "font-size: 1.5em; margin: 0.5em 0",
|
||||||
|
}),
|
||||||
|
div(
|
||||||
|
{
|
||||||
|
style: "margin: 0 1em",
|
||||||
|
},
|
||||||
|
code(...sigTitle)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConstItem(v: DocElement) {
|
||||||
|
return div(
|
||||||
|
{
|
||||||
|
class: "tinymist-symbol",
|
||||||
|
},
|
||||||
|
div(
|
||||||
|
{
|
||||||
|
class: `detail-header doc-symbol-${v.data.kind}`,
|
||||||
|
},
|
||||||
|
h3(
|
||||||
|
{ class: "doc-symbol-name" },
|
||||||
|
code(`${v.data.name}`)
|
||||||
|
// code(
|
||||||
|
// {
|
||||||
|
// style: "float: right; line-height: 1em",
|
||||||
|
// },
|
||||||
|
// `${v.data.kind}`
|
||||||
|
// )
|
||||||
|
)
|
||||||
|
),
|
||||||
|
div({
|
||||||
|
style: "margin-left: 0.62em",
|
||||||
|
innerHTML: v.contents.join(""),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Item(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sigTypeHighlighted(
|
||||||
|
types: string | undefined,
|
||||||
|
inferred: [string, string] | undefined,
|
||||||
|
target: ChildDom[]
|
||||||
|
) {
|
||||||
|
console.log("sigTypeHighlighted", { types, inferred });
|
||||||
|
if (types) {
|
||||||
|
typeHighlighted(types, target);
|
||||||
|
} else if (inferred) {
|
||||||
|
const rendered: ChildDom[] = [];
|
||||||
|
typeHighlighted(inferred[0], rendered, "|");
|
||||||
|
const infer = span(
|
||||||
|
{ class: "code-kw type-inferred", title: "inferred by type checker" },
|
||||||
|
"infer"
|
||||||
|
);
|
||||||
|
target.push(
|
||||||
|
code(
|
||||||
|
{ class: "type-inferred" },
|
||||||
|
infer,
|
||||||
|
code(" "),
|
||||||
|
span({ class: "type-inferred-as", title: inferred[1] }, ...rendered)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function typeHighlighted(
|
||||||
|
types: string,
|
||||||
|
target: ChildDom[],
|
||||||
|
by: RegExp | string = /[|,]/g
|
||||||
|
) {
|
||||||
|
const type = types.split(by);
|
||||||
|
for (let i = 0; i < type.length; i++) {
|
||||||
|
if (i > 0) {
|
||||||
|
target.push(code(" | "));
|
||||||
|
}
|
||||||
|
const ty = type[i].trim();
|
||||||
|
switch (ty) {
|
||||||
|
case "int":
|
||||||
|
case "integer":
|
||||||
|
target.push(code({ class: "type-int" }, ty));
|
||||||
|
break;
|
||||||
|
case "float":
|
||||||
|
target.push(code({ class: "type-float" }, ty));
|
||||||
|
break;
|
||||||
|
case "string":
|
||||||
|
case "array":
|
||||||
|
case "dictionary":
|
||||||
|
case "content":
|
||||||
|
case "str":
|
||||||
|
case "bool":
|
||||||
|
case "boolean":
|
||||||
|
target.push(code({ class: "type-builtin" }, ty));
|
||||||
|
break;
|
||||||
|
case "auto":
|
||||||
|
target.push(code({ class: "type-auto" }, ty));
|
||||||
|
break;
|
||||||
|
case "none":
|
||||||
|
target.push(code({ class: "type-none" }, ty));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
target.push(code(type[i]));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function kwHl(kw: string) {
|
||||||
|
return code({ class: "code-kw" }, kw);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fnHl(fn: string) {
|
||||||
|
return code({ class: "code-func" }, fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
function codeHl(cls: string, c: string) {
|
||||||
|
return code({ class: `code-${cls}` }, c);
|
||||||
|
}
|
50
tools/editor-tools/src/main.base.ts
Normal file
50
tools/editor-tools/src/main.base.ts
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import "./style.css";
|
||||||
|
import van, { ChildDom } from "vanjs-core";
|
||||||
|
import { setupVscodeChannel } from "./vscode";
|
||||||
|
|
||||||
|
/// The components that can be rendered by the frontend.
|
||||||
|
/// Typically, each component corresponds to a single tool (Application).
|
||||||
|
type PageComponent =
|
||||||
|
| "template-gallery"
|
||||||
|
| "tracing"
|
||||||
|
| "summary"
|
||||||
|
| "diagnostics"
|
||||||
|
| "symbol-view"
|
||||||
|
| "docs";
|
||||||
|
|
||||||
|
/// The frontend arguments that are passed from the backend.
|
||||||
|
interface Arguments {
|
||||||
|
/// The page to render.
|
||||||
|
page: PageComponent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Placeholders for editor-tools program initializing frontend
|
||||||
|
/// arguments.
|
||||||
|
function retrieveArgs(): Arguments {
|
||||||
|
/// The string `editor-tools-args:{}` is a placeholder
|
||||||
|
/// It will be replaced by the actual arguments.
|
||||||
|
/// ```rs
|
||||||
|
/// let frontend_html = frontend_html.replace(
|
||||||
|
/// "editor-tools-args:{}", ...);
|
||||||
|
/// ```
|
||||||
|
let mode = `editor-tools-args:{"page": "docs"}`;
|
||||||
|
/// Remove the placeholder prefix.
|
||||||
|
mode = mode.replace("editor-tools-args:", "");
|
||||||
|
|
||||||
|
/// Return a `WsArgs` object.
|
||||||
|
return JSON.parse(mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
type Registry = Partial<Record<PageComponent, () => ChildDom>>;
|
||||||
|
export function mainHarness(components: Registry) {
|
||||||
|
setupVscodeChannel();
|
||||||
|
|
||||||
|
const args = retrieveArgs();
|
||||||
|
const appHook = document.querySelector("#tinymist-app")!;
|
||||||
|
|
||||||
|
const Component = components[args.page];
|
||||||
|
if (!Component) {
|
||||||
|
throw new Error(`Unknown page: ${args.page}`);
|
||||||
|
}
|
||||||
|
van.add(appHook, Component());
|
||||||
|
}
|
6
tools/editor-tools/src/main.symbol-view.ts
Normal file
6
tools/editor-tools/src/main.symbol-view.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import { mainHarness } from "./main.base";
|
||||||
|
import { SymbolPicker } from "./features/symbol-view";
|
||||||
|
|
||||||
|
mainHarness({
|
||||||
|
"symbol-view": SymbolPicker,
|
||||||
|
});
|
|
@ -1,69 +1,14 @@
|
||||||
import "./style.css";
|
import { mainHarness } from "./main.base";
|
||||||
import van from "vanjs-core";
|
|
||||||
import { setupVscodeChannel } from "./vscode";
|
|
||||||
import { TemplateGallery } from "./features/template-gallery";
|
import { TemplateGallery } from "./features/template-gallery";
|
||||||
import { Tracing } from "./features/tracing";
|
import { Tracing } from "./features/tracing";
|
||||||
import { Summary } from "./features/summary";
|
import { Summary } from "./features/summary";
|
||||||
import { Diagnostics } from "./features/diagnostics";
|
import { Diagnostics } from "./features/diagnostics";
|
||||||
import { SymbolPicker } from "./features/symbol-view";
|
import { Docs } from "./features/docs";
|
||||||
|
|
||||||
/// The components that can be rendered by the frontend.
|
mainHarness({
|
||||||
/// Typically, each component corresponds to a single tool (Application).
|
"template-gallery": TemplateGallery,
|
||||||
type PageComponent =
|
tracing: Tracing,
|
||||||
| "template-gallery"
|
summary: Summary,
|
||||||
| "tracing"
|
diagnostics: Diagnostics,
|
||||||
| "summary"
|
docs: Docs,
|
||||||
| "diagnostics"
|
});
|
||||||
| "symbol-view";
|
|
||||||
|
|
||||||
/// The frontend arguments that are passed from the backend.
|
|
||||||
interface Arguments {
|
|
||||||
/// The page to render.
|
|
||||||
page: PageComponent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Placeholders for editor-tools program initializing frontend
|
|
||||||
/// arguments.
|
|
||||||
function retrieveArgs(): Arguments {
|
|
||||||
/// The string `editor-tools-args:{}` is a placeholder
|
|
||||||
/// It will be replaced by the actual arguments.
|
|
||||||
/// ```rs
|
|
||||||
/// let frontend_html = frontend_html.replace(
|
|
||||||
/// "editor-tools-args:{}", ...);
|
|
||||||
/// ```
|
|
||||||
let mode = `editor-tools-args:{"page": "symbol-view"}`;
|
|
||||||
/// Remove the placeholder prefix.
|
|
||||||
mode = mode.replace("editor-tools-args:", "");
|
|
||||||
|
|
||||||
/// Return a `WsArgs` object.
|
|
||||||
return JSON.parse(mode);
|
|
||||||
}
|
|
||||||
|
|
||||||
function main() {
|
|
||||||
setupVscodeChannel();
|
|
||||||
|
|
||||||
const args = retrieveArgs();
|
|
||||||
const appHook = document.querySelector("#tinymist-app")!;
|
|
||||||
|
|
||||||
switch (args.page) {
|
|
||||||
case "template-gallery":
|
|
||||||
van.add(appHook, TemplateGallery());
|
|
||||||
break;
|
|
||||||
case "tracing":
|
|
||||||
van.add(appHook, Tracing());
|
|
||||||
break;
|
|
||||||
case "summary":
|
|
||||||
van.add(appHook, Summary());
|
|
||||||
break;
|
|
||||||
case "diagnostics":
|
|
||||||
van.add(appHook, Diagnostics());
|
|
||||||
break;
|
|
||||||
case "symbol-view":
|
|
||||||
van.add(appHook, SymbolPicker());
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new Error(`Unknown page: ${args.page}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main();
|
|
||||||
|
|
|
@ -183,3 +183,8 @@ body.typst-preview-light .tinymist-button.warning.activated {
|
||||||
.hidden {
|
.hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* aria-hidden */
|
||||||
|
span[aria-hidden="true"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
|
@ -1,18 +1,25 @@
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import { viteSingleFile } from "vite-plugin-singlefile";
|
import { viteSingleFile } from "vite-plugin-singlefile";
|
||||||
|
|
||||||
|
// /src/main.ts
|
||||||
|
|
||||||
|
const compPrefix = '--component=';
|
||||||
|
const componentArgs = process.argv.find(arg => arg.startsWith(compPrefix));
|
||||||
|
let output = 'dist/default';
|
||||||
|
if (componentArgs) {
|
||||||
|
const component = componentArgs.substring(compPrefix.length);
|
||||||
|
process.env.VITE_ENTRY = `/src/main.${component}.ts`;
|
||||||
|
output = `dist/${component}`;
|
||||||
|
} else {
|
||||||
|
process.env.VITE_ENTRY = '/src/main.ts';
|
||||||
|
}
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [viteSingleFile()],
|
plugins: [viteSingleFile()],
|
||||||
assetsInclude: ["**/*.onnx"],
|
assetsInclude: ["**/*.onnx"],
|
||||||
build: {
|
build: {
|
||||||
minify: false,
|
minify: false,
|
||||||
rollupOptions: {
|
outDir: output
|
||||||
output: {
|
|
||||||
assetFileNames: `typst-webview-assets/[name]-[hash][extname]`,
|
|
||||||
chunkFileNames: "typst-webview-assets/[name]-[hash].js",
|
|
||||||
entryFileNames: "typst-webview-assets/[name]-[hash].js",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
optimizeDeps: {
|
optimizeDeps: {
|
||||||
esbuildOptions: {
|
esbuildOptions: {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue