feat: move and compile tinymist crate for wasm32 target (#2027)
Some checks are pending
tinymist::auto_tag / auto-tag (push) Waiting to run
tinymist::ci / Duplicate Actions Detection (push) Waiting to run
tinymist::ci / Check Clippy, Formatting, Completion, Documentation, and Tests (Linux) (push) Waiting to run
tinymist::ci / Check Minimum Rust version and Tests (Windows) (push) Waiting to run
tinymist::ci / prepare-build (push) Waiting to run
tinymist::ci / announce (push) Blocked by required conditions
tinymist::ci / build (push) Blocked by required conditions
tinymist::gh_pages / build-gh-pages (push) Waiting to run

This commit is contained in:
Myriad-Dreamin 2025-08-11 13:14:26 +08:00 committed by GitHub
parent 79f68dc94d
commit ce5ab81760
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
56 changed files with 1558 additions and 1122 deletions

View file

@ -162,12 +162,12 @@ jobs:
run: | run: |
npm pack > package-name npm pack > package-name
mv $(cat package-name) tinymist-${{ env.target }}.tar.gz mv $(cat package-name) tinymist-${{ env.target }}.tar.gz
working-directory: ./crates/tinymist-core working-directory: ./crates/tinymist
- name: Upload tinymist npm library - name: Upload tinymist npm library
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: tinymist-${{ env.target }}-npm name: tinymist-${{ env.target }}-npm
path: crates/tinymist-core/tinymist-${{ env.target }}.tar.gz path: crates/tinymist/tinymist-${{ env.target }}.tar.gz
- name: Download PDF Documentation - name: Download PDF Documentation
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4

View file

@ -62,12 +62,12 @@ jobs:
- name: Generate completions - name: Generate completions
run: | run: |
mkdir -p completions/{zsh,bash,fish/vendor_completions.d,elvish/lib,nushell/vendor/autoload,powershell}/ mkdir -p completions/{zsh,bash,fish/vendor_completions.d,elvish/lib,nushell/vendor/autoload,powershell}/
cargo run -p tinymist -- completion zsh > completions/zsh/_tinymist cargo run --bin tinymist -- completion zsh > completions/zsh/_tinymist
cargo run -p tinymist -- completion bash > completions/bash/tinymist cargo run --bin tinymist -- completion bash > completions/bash/tinymist
cargo run -p tinymist -- completion fish > completions/fish/vendor_completions.d/tinymist.fish cargo run --bin tinymist -- completion fish > completions/fish/vendor_completions.d/tinymist.fish
cargo run -p tinymist -- completion elvish > completions/elvish/lib/tinymist.elv cargo run --bin tinymist -- completion elvish > completions/elvish/lib/tinymist.elv
cargo run -p tinymist -- completion nushell > completions/nushell/vendor/autoload/tinymist.nu cargo run --bin tinymist -- completion nushell > completions/nushell/vendor/autoload/tinymist.nu
cargo run -p tinymist -- completion powershell > completions/powershell/tinymist.ps1 cargo run --bin tinymist -- completion powershell > completions/powershell/tinymist.ps1
tar -czvf tinymist-completions.tar.gz completions tar -czvf tinymist-completions.tar.gz completions
- name: upload completions - name: upload completions
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4

View file

@ -51,9 +51,9 @@ jobs:
cargo publish --no-verify -p tinymist-lint || true cargo publish --no-verify -p tinymist-lint || true
cargo publish --no-verify -p tinymist-query || true cargo publish --no-verify -p tinymist-query || true
cargo publish --no-verify -p tinymist-render || true cargo publish --no-verify -p tinymist-render || true
cargo publish --no-verify -p tinymist-core || true
cargo publish --no-verify -p tinymist-preview || true cargo publish --no-verify -p tinymist-preview || true
cargo publish --no-verify -p tinymist || true cargo publish --no-verify -p tinymist || true
cargo publish --no-verify -p tinymist-cli || true
- name: Verifies crate health (Optional) - name: Verifies crate health (Optional)
run: | run: |
cargo publish --dry-run -p sync-ls cargo publish --dry-run -p sync-ls

71
Cargo.lock generated
View file

@ -4133,6 +4133,7 @@ dependencies = [
"clap_mangen", "clap_mangen",
"codespan-reporting", "codespan-reporting",
"comemo", "comemo",
"console_error_panic_hook",
"crossbeam-channel", "crossbeam-channel",
"dapts", "dapts",
"dhat", "dhat",
@ -4144,6 +4145,7 @@ dependencies = [
"hyper-tungstenite", "hyper-tungstenite",
"hyper-util", "hyper-util",
"itertools 0.13.0", "itertools 0.13.0",
"js-sys",
"log", "log",
"lsp-types", "lsp-types",
"open", "open",
@ -4161,7 +4163,6 @@ dependencies = [
"sync-ls", "sync-ls",
"temp-env", "temp-env",
"tinymist-assets 0.13.22 (registry+https://github.com/rust-lang/crates.io-index)", "tinymist-assets 0.13.22 (registry+https://github.com/rust-lang/crates.io-index)",
"tinymist-core",
"tinymist-debug", "tinymist-debug",
"tinymist-l10n", "tinymist-l10n",
"tinymist-preview", "tinymist-preview",
@ -4188,6 +4189,7 @@ dependencies = [
"unicode-script", "unicode-script",
"vergen", "vergen",
"walkdir", "walkdir",
"wasm-bindgen",
] ]
[[package]] [[package]]
@ -4242,18 +4244,77 @@ dependencies = [
] ]
[[package]] [[package]]
name = "tinymist-core" name = "tinymist-cli"
version = "0.13.22" version = "0.13.22"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait",
"base64",
"cargo_metadata", "cargo_metadata",
"console_error_panic_hook", "chrono",
"js-sys", "clap",
"clap_builder",
"clap_complete",
"clap_complete_fig",
"clap_complete_nushell",
"clap_mangen",
"codespan-reporting",
"comemo",
"crossbeam-channel",
"dapts",
"dhat",
"dirs",
"env_logger",
"futures",
"http-body-util",
"hyper",
"hyper-tungstenite",
"hyper-util",
"itertools 0.13.0",
"log",
"lsp-types",
"open",
"parking_lot",
"paste",
"rayon",
"reflexo",
"reflexo-typst", "reflexo-typst",
"reflexo-vec2svg",
"rpds",
"serde",
"serde_json",
"serde_yaml",
"strum",
"sync-ls",
"temp-env",
"tinymist",
"tinymist-assets 0.13.22 (registry+https://github.com/rust-lang/crates.io-index)",
"tinymist-debug",
"tinymist-l10n",
"tinymist-preview",
"tinymist-project", "tinymist-project",
"tinymist-query", "tinymist-query",
"tinymist-render",
"tinymist-std",
"tinymist-task",
"tokio",
"tokio-util",
"toml",
"ttf-parser",
"typlite",
"typst",
"typst-ansi-hl",
"typst-html",
"typst-pdf",
"typst-render",
"typst-shim",
"typst-svg",
"typst-timing",
"typstfmt",
"typstyle-core",
"unicode-script",
"vergen", "vergen",
"wasm-bindgen", "walkdir",
] ]
[[package]] [[package]]

View file

@ -69,6 +69,7 @@ reqwest = { version = "^0.12", default-features = false, features = [
"blocking", "blocking",
"multipart", "multipart",
] } ] }
http-body-util = "0.1.2"
# Algorithms # Algorithms
base64 = "0.22" base64 = "0.22"
@ -201,9 +202,9 @@ typst-shim = { path = "./crates/typst-shim", version = "0.13.16" }
tinymist-tests = { path = "./crates/tinymist-tests/" } tinymist-tests = { path = "./crates/tinymist-tests/" }
sync-ls = { path = "./crates/sync-lsp", version = "0.13.22" } sync-ls = { path = "./crates/sync-lsp", version = "0.13.22" }
tinymist = { path = "./crates/tinymist/", version = "0.13.22" } tinymist = { path = "./crates/tinymist/", version = "0.13.22", default-features = false }
tinymist-analysis = { path = "./crates/tinymist-analysis/", version = "0.13.22" } tinymist-analysis = { path = "./crates/tinymist-analysis/", version = "0.13.22" }
tinymist-core = { path = "./crates/tinymist-core/", version = "0.13.22", default-features = false } tinymist-cli = { path = "./crates/tinymist-cli/", version = "0.13.22" }
tinymist-debug = { path = "./crates/tinymist-debug/", version = "0.13.22" } tinymist-debug = { path = "./crates/tinymist-debug/", version = "0.13.22" }
tinymist-lint = { path = "./crates/tinymist-lint/", version = "0.13.22" } tinymist-lint = { path = "./crates/tinymist-lint/", version = "0.13.22" }
tinymist-query = { path = "./crates/tinymist-query/", version = "0.13.22" } tinymist-query = { path = "./crates/tinymist-query/", version = "0.13.22" }

View file

@ -14,33 +14,27 @@ rust-version.workspace = true
[dependencies] [dependencies]
anyhow.workspace = true anyhow.workspace = true
crossbeam-channel.workspace = true
dapts = { workspace = true, optional = true } dapts = { workspace = true, optional = true }
futures.workspace = true
log.workspace = true log.workspace = true
lsp-types = { workspace = true, optional = true } lsp-types = { workspace = true, optional = true }
parking_lot.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
clap = { workspace = true, optional = true } clap = { workspace = true, optional = true }
crossbeam-channel = { workspace = true, optional = true } tokio = { workspace = true, features = ["rt"], optional = true }
futures = { workspace = true, optional = true }
parking_lot = { workspace = true, optional = true }
tokio = { workspace = true, features = ["rt", "time"], optional = true }
tokio-util = { workspace = true, optional = true } tokio-util = { workspace = true, optional = true }
[features] [features]
dap = ["dapts"] dap = ["dapts"]
lsp = ["lsp-types"] lsp = ["lsp-types"]
server = [ server = ["tokio"]
"crossbeam-channel", system = ["tokio", "tokio/time", "tokio-util", "clap"]
"futures",
"tokio",
"tokio-util",
"clap",
"parking_lot",
]
[package.metadata.docs.rs] [package.metadata.docs.rs]
features = ["dap", "lsp", "server"] features = ["dap", "lsp", "system", "server"]
[lints] [lints]
workspace = true workspace = true

View file

@ -17,7 +17,7 @@ pub use server::*;
pub mod req_queue; pub mod req_queue;
#[cfg(feature = "server")] #[cfg(feature = "server")]
mod server; mod server;
#[cfg(feature = "server")] #[cfg(all(feature = "server", feature = "system"))]
pub mod transport; pub mod transport;
use std::any::Any; use std::any::Any;

View file

@ -195,7 +195,7 @@ impl Notification {
} }
} }
#[cfg(feature = "server")] #[cfg(all(feature = "server", feature = "system"))]
pub(crate) fn is_exit(&self) -> bool { pub(crate) fn is_exit(&self) -> bool {
self.method == "exit" self.method == "exit"
} }

View file

@ -69,7 +69,7 @@ pub struct TConnectionRx<M> {
impl<M: TryFrom<Message, Error = anyhow::Error>> TConnectionRx<M> { impl<M: TryFrom<Message, Error = anyhow::Error>> TConnectionRx<M> {
/// Receives a message or an event. /// Receives a message or an event.
pub(crate) fn recv(&self) -> anyhow::Result<EventOrMessage<M>> { pub fn recv(&self) -> anyhow::Result<EventOrMessage<M>> {
crossbeam_channel::select_biased! { crossbeam_channel::select_biased! {
recv(self.lsp) -> msg => Ok(EventOrMessage::Msg(msg?.try_into()?)), recv(self.lsp) -> msg => Ok(EventOrMessage::Msg(msg?.try_into()?)),
recv(self.event) -> event => Ok(event.map(EventOrMessage::Evt)?), recv(self.event) -> event => Ok(event.map(EventOrMessage::Evt)?),
@ -78,8 +78,10 @@ impl<M: TryFrom<Message, Error = anyhow::Error>> TConnectionRx<M> {
} }
/// This is a helper enum to handle both events and messages. /// This is a helper enum to handle both events and messages.
pub(crate) enum EventOrMessage<M> { pub enum EventOrMessage<M> {
/// An event received.
Evt(Event), Evt(Event),
/// A message received.
Msg(M), Msg(M),
} }
@ -380,7 +382,7 @@ impl LspClient {
/// Finally sends the response if it is not sent before. /// Finally sends the response if it is not sent before.
/// From the definition, the response is already sent if it is `Some(())`. /// From the definition, the response is already sent if it is `Some(())`.
pub(crate) fn schedule_tail(&self, req_id: RequestId, resp: ScheduledResult) { pub fn schedule_tail(&self, req_id: RequestId, resp: ScheduledResult) {
match resp { match resp {
// Already responded // Already responded
Ok(Some(())) => {} Ok(Some(())) => {}

View file

@ -169,6 +169,7 @@ where
/// ///
/// See [`transport::MirrorArgs`] for information about the record-replay /// See [`transport::MirrorArgs`] for information about the record-replay
/// feature. /// feature.
#[cfg(feature = "system")]
pub fn start( pub fn start(
&mut self, &mut self,
inbox: TConnectionRx<LspMessage>, inbox: TConnectionRx<LspMessage>,
@ -202,6 +203,7 @@ where
} }
/// Starts the language server on the given connection. /// Starts the language server on the given connection.
#[cfg(feature = "system")]
pub fn start_(&mut self, inbox: TConnectionRx<LspMessage>) -> anyhow::Result<()> { pub fn start_(&mut self, inbox: TConnectionRx<LspMessage>) -> anyhow::Result<()> {
use EventOrMessage::*; use EventOrMessage::*;
// todo: follow what rust analyzer does // todo: follow what rust analyzer does
@ -273,7 +275,7 @@ where
/// Registers and handles a request. This should only be called once per /// Registers and handles a request. This should only be called once per
/// incoming request. /// incoming request.
fn on_lsp_request(&mut self, request_received: Instant, req: Request) { pub fn on_lsp_request(&mut self, request_received: Instant, req: Request) {
self.client self.client
.register_request(&req.method, &req.id, request_received); .register_request(&req.method, &req.id, request_received);
@ -353,7 +355,11 @@ where
} }
/// Handles an incoming notification. /// Handles an incoming notification.
fn on_notification(&mut self, received_at: Instant, not: Notification) -> anyhow::Result<()> { pub fn on_notification(
&mut self,
received_at: Instant,
not: Notification,
) -> anyhow::Result<()> {
self.client.hook.start_notification(&not.method); self.client.hook.start_notification(&not.method);
let handle = |s, Notification { method, params }: Notification| { let handle = |s, Notification { method, params }: Notification| {
let Some(handler) = self.notifications.get(method.as_str()) else { let Some(handler) = self.notifications.get(method.as_str()) else {

View file

@ -0,0 +1,150 @@
[package]
name = "tinymist-cli"
description = "An integrated language service for Typst."
categories = ["compilers", "command-line-utilities"]
keywords = ["cli", "lsp", "language", "typst"]
authors.workspace = true
version.workspace = true
license.workspace = true
edition.workspace = true
homepage.workspace = true
repository.workspace = true
rust-version.workspace = true
[[bin]]
name = "tinymist"
path = "src/main.rs"
doc = false
[dependencies]
anyhow.workspace = true
async-trait.workspace = true
base64.workspace = true
chrono.workspace = true
clap.workspace = true
clap_builder.workspace = true
clap_complete.workspace = true
clap_complete_fig.workspace = true
clap_complete_nushell.workspace = true
clap_mangen.workspace = true
crossbeam-channel.workspace = true
codespan-reporting.workspace = true
comemo.workspace = true
dhat = { workspace = true, optional = true }
dirs.workspace = true
env_logger.workspace = true
futures.workspace = true
hyper.workspace = true
hyper-util = { workspace = true, features = [
"server",
"http1",
"http2",
"server-graceful",
"server-auto",
] }
http-body-util = "0.1.2"
hyper-tungstenite = { workspace = true, optional = true }
itertools.workspace = true
lsp-types.workspace = true
log.workspace = true
open.workspace = true
parking_lot.workspace = true
paste.workspace = true
rayon.workspace = true
reflexo.workspace = true
reflexo-typst = { workspace = true, features = ["system", "svg"] }
reflexo-vec2svg.workspace = true
rpds.workspace = true
serde.workspace = true
serde_json.workspace = true
serde_yaml.workspace = true
strum.workspace = true
sync-ls = { workspace = true, features = ["lsp", "server", "system"] }
tinymist-assets = { workspace = true }
tinymist-query.workspace = true
tinymist-std.workspace = true
tinymist = { workspace = true, default-features = false, features = ["system"] }
tinymist-project = { workspace = true, features = ["lsp"] }
tinymist-render.workspace = true
tokio = { workspace = true, features = ["rt-multi-thread", "io-std"] }
tokio-util.workspace = true
toml.workspace = true
ttf-parser.workspace = true
typlite = { workspace = true, default-features = false }
typst.workspace = true
typst-svg.workspace = true
typst-pdf.workspace = true
typst-render.workspace = true
typst-timing.workspace = true
typst-html.workspace = true
typst-shim.workspace = true
tinymist-preview = { workspace = true, optional = true }
typst-ansi-hl.workspace = true
tinymist-task.workspace = true
tinymist-debug.workspace = true
typstfmt.workspace = true
typstyle-core.workspace = true
unicode-script.workspace = true
walkdir.workspace = true
tinymist-l10n.workspace = true
dapts.workspace = true
[features]
default = [
"cli",
"pdf",
"l10n",
"lock",
"export",
"preview",
"trace",
"embed-fonts",
"no-content-hint",
"dap",
]
cli = ["sync-ls/clap", "clap/wrap_help"]
dhat-heap = ["dhat"]
# Embeds Typst's default fonts for
# - text (Linux Libertine),
# - math (New Computer Modern Math), and
# - code (Deja Vu Sans Mono)
# and additionally New Computer Modern for text
# into the binary.
embed-fonts = ["tinymist-project/fonts"]
pdf = ["tinymist-task/pdf"]
# Disable the default content hint.
# This requires modifying typst.
no-content-hint = [
"tinymist-task/no-content-hint",
"tinymist-project/no-content-hint",
"tinymist-preview/no-content-hint",
"typlite/no-content-hint",
"reflexo-typst/no-content-hint",
"reflexo-vec2svg/no-content-hint",
]
export = ["tinymist/export"]
lock = ["tinymist/lock"]
preview = ["tinymist/preview", "hyper-tungstenite"]
trace = ["tinymist/trace"]
dap = ["tinymist/dap"]
l10n = ["tinymist-assets/l10n"]
[dev-dependencies]
temp-env.workspace = true
[build-dependencies]
anyhow.workspace = true
cargo_metadata = "0.18.0"
vergen.workspace = true
[lints]
workspace = true

View file

@ -2,10 +2,19 @@ use std::path::Path;
use sync_ls::transport::MirrorArgs; use sync_ls::transport::MirrorArgs;
use tinymist::project::DocCommands; use tinymist::project::DocCommands;
use tinymist::tool::project::{CompileArgs, GenerateScriptArgs, TaskCommands}; use tinymist::LONG_VERSION;
use tinymist::tool::testing::TestArgs;
use tinymist::{CompileFontArgs, CompileOnceArgs}; use tinymist::{CompileFontArgs, CompileOnceArgs};
use tinymist_core::LONG_VERSION;
#[cfg(feature = "preview")]
use tinymist::tool::preview::PreviewArgs;
#[cfg(feature = "preview")]
use tinymist_project::DocNewArgs;
#[cfg(feature = "preview")]
use tinymist_task::TaskWhen;
use crate::compile::CompileArgs;
use crate::generate_script::GenerateScriptArgs;
use crate::testing::TestArgs;
#[derive(Debug, Clone, clap::Parser)] #[derive(Debug, Clone, clap::Parser)]
#[clap(name = "tinymist", author, version, about, long_version(LONG_VERSION.as_str()))] #[clap(name = "tinymist", author, version, about, long_version(LONG_VERSION.as_str()))]
@ -197,3 +206,33 @@ pub enum QueryDocsFormat {
Json, Json,
Markdown, Markdown,
} }
/// Project task commands.
#[derive(Debug, Clone, clap::Subcommand)]
#[clap(rename_all = "kebab-case")]
pub enum TaskCommands {
/// Declare a preview task.
#[cfg(feature = "preview")]
Preview(TaskPreviewArgs),
}
/// Declare an lsp task.
#[derive(Debug, Clone, clap::Parser)]
#[cfg(feature = "preview")]
pub struct TaskPreviewArgs {
/// Argument to identify a project.
#[clap(flatten)]
pub declare: DocNewArgs,
/// Name a task.
#[clap(long = "task")]
pub task_name: Option<String>,
/// When to run the task
#[arg(long = "when")]
pub when: Option<TaskWhen>,
/// Preview arguments
#[clap(flatten)]
pub preview: PreviewArgs,
}

View file

@ -0,0 +1,88 @@
//! Project management tools.
use std::path::PathBuf;
use reflexo::ImmutPath;
use reflexo_typst::WorldComputeGraph;
use tinymist_std::error::prelude::*;
use tinymist::project::*;
use tinymist::world::system::print_diagnostics;
use tinymist::ExportTask;
/// Arguments for project compilation.
#[derive(Debug, Clone, clap::Parser)]
pub struct CompileArgs {
/// Inherits the compile task arguments.
#[clap(flatten)]
pub compile: TaskCompileArgs,
/// Saves the compilation arguments to the lock file.
#[clap(long)]
pub save_lock: bool,
/// Specifies the path to the lock file. If the path is
/// set, the lock file will be saved.
#[clap(long)]
pub lockfile: Option<PathBuf>,
}
/// Runs project compilation(s)
pub async fn compile_main(args: CompileArgs) -> Result<()> {
let cwd = std::env::current_dir().context("cannot get cwd")?;
// todo: respect the name of the lock file
// Saves the lock file if the flags are set
let save_lock = args.save_lock || args.lockfile.is_some();
let lock_dir: ImmutPath = if let Some(lockfile) = args.lockfile {
let lockfile = if lockfile.is_absolute() {
lockfile
} else {
cwd.join(lockfile)
};
lockfile
.parent()
.context("lock file must have a parent directory")?
.into()
} else {
cwd.as_path().into()
};
// Identifies the input and output
let input = args.compile.declare.to_input((&cwd, &lock_dir));
let output = args.compile.to_task(input.id.clone(), &cwd)?;
if save_lock {
LockFile::update(&lock_dir, |state| {
state.replace_document(input.relative_to(&lock_dir));
state.replace_task(output.clone());
Ok(())
})?;
}
// Prepares for the compilation
let universe = (input, lock_dir.clone()).resolve()?;
let world = universe.snapshot();
let graph = WorldComputeGraph::from_world(world);
// Compiles the project
let is_html = matches!(output.task, ProjectTask::ExportHtml(..));
let compiled = CompiledArtifact::from_graph(graph, is_html);
let diag = compiled.diagnostics();
print_diagnostics(compiled.world(), diag, DiagnosticFormat::Human)
.context_ut("print diagnostics")?;
if compiled.has_errors() {
// todo: we should process case of compile error in fn main function
std::process::exit(1);
}
// Exports the compiled project
let lock_dir = save_lock.then_some(lock_dir);
ExportTask::do_export(output.task, compiled, lock_dir).await?;
Ok(())
}

View file

@ -0,0 +1,225 @@
//! Project management tools.
use std::{borrow::Cow, path::Path};
use clap_complete::Shell;
use reflexo::path::unix_slash;
use tinymist::project::*;
use tinymist_std::{bail, error::prelude::*};
/// Arguments for generating a build script.
#[derive(Debug, Clone, clap::Parser)]
pub struct GenerateScriptArgs {
/// The shell to generate the completion script for. If not provided, it
/// will be inferred from the environment.
#[clap(value_enum)]
pub shell: Option<Shell>,
/// The path to the output script.
#[clap(short, long)]
pub output: Option<String>,
}
/// Generates a build script for compilation
pub fn generate_script_main(args: GenerateScriptArgs) -> Result<()> {
let Some(shell) = args.shell.or_else(Shell::from_env) else {
bail!("could not infer shell");
};
let output = Path::new(args.output.as_deref().unwrap_or("build"));
let output = match shell {
Shell::Bash | Shell::Zsh | Shell::Elvish | Shell::Fish => output.with_extension("sh"),
Shell::PowerShell => output.with_extension("ps1"),
_ => bail!("unsupported shell: {shell:?}"),
};
let script = match shell {
Shell::Bash | Shell::Zsh | Shell::PowerShell => shell_build_script(shell)?,
_ => bail!("unsupported shell: {shell:?}"),
};
std::fs::write(output, script).context("write script")?;
Ok(())
}
/// Generates a build script for shell-like shells
fn shell_build_script(shell: Shell) -> Result<String> {
let mut output = String::new();
match shell {
Shell::Bash => {
output.push_str("#!/usr/bin/env bash\n\n");
}
Shell::Zsh => {
output.push_str("#!/usr/bin/env zsh\n\n");
}
Shell::PowerShell => {}
_ => {}
}
let lock_dir = std::env::current_dir().context("current directory")?;
let lock = LockFile::read(&lock_dir)?;
struct CmdBuilder(Vec<Cow<'static, str>>);
impl CmdBuilder {
fn new() -> Self {
Self(vec![])
}
fn extend(&mut self, args: impl IntoIterator<Item = impl Into<Cow<'static, str>>>) {
for arg in args {
self.0.push(arg.into());
}
}
fn push(&mut self, arg: impl Into<Cow<'static, str>>) {
self.0.push(arg.into());
}
fn build(self) -> String {
self.0.join(" ")
}
}
let quote_escape = |s: &str| s.replace("'", r#"'"'"'"#);
let quote = |s: &str| format!("'{}'", s.replace("'", r#"'"'"'"#));
let path_of = |p: &ResourcePath, loc: &str| {
let Some(path) = p.to_rel_path(&lock_dir) else {
log::error!("could not resolve path for {loc}, path: {p:?}");
return String::default();
};
quote(&unix_slash(&path))
};
let base_cmd: Vec<&str> = vec!["tinymist", "compile", "--save-lock"];
for task in lock.task.iter() {
let Some(input) = lock.get_document(&task.document) else {
log::warn!(
"could not find document for task {:?}, whose document is {:?}",
task.id,
task.doc_id()
);
continue;
};
// todo: preview/query commands
let Some(export) = task.task.as_export() else {
continue;
};
let mut cmd = CmdBuilder::new();
cmd.extend(base_cmd.iter().copied());
cmd.push("--task");
cmd.push(quote(&task.id.to_string()));
cmd.push(path_of(&input.main, "main"));
if let Some(root) = &input.root {
cmd.push("--root");
cmd.push(path_of(root, "root"));
}
for (k, v) in &input.inputs {
cmd.push(format!(
r#"--input='{}={}'"#,
quote_escape(k),
quote_escape(v)
));
}
for p in &input.font_paths {
cmd.push("--font-path");
cmd.push(path_of(p, "font-path"));
}
if !input.system_fonts {
cmd.push("--ignore-system-fonts");
}
if let Some(p) = &input.package_path {
cmd.push("--package-path");
cmd.push(path_of(p, "package-path"));
}
if let Some(p) = &input.package_cache_path {
cmd.push("--package-cache-path");
cmd.push(path_of(p, "package-cache-path"));
}
if let Some(p) = &export.output {
cmd.push("--output");
cmd.push(quote(&p.to_string()));
}
for t in &export.transform {
match t {
ExportTransform::Pretty { .. } => {
cmd.push("--pretty");
}
ExportTransform::Pages { ranges } => {
for r in ranges {
cmd.push("--pages");
cmd.push(r.to_string());
}
}
// todo: export me
ExportTransform::Merge { .. } | ExportTransform::Script { .. } => {}
}
}
match &task.task {
ProjectTask::Preview(..) | ProjectTask::Query(..) => {}
ProjectTask::ExportPdf(task) => {
cmd.push("--format=pdf");
for s in &task.pdf_standards {
cmd.push("--pdf-standard");
let s = serde_json::to_string(s).context("pdf standard")?;
cmd.push(s);
}
if let Some(output) = &task.creation_timestamp {
cmd.push("--creation-timestamp");
cmd.push(output.to_string());
}
}
ProjectTask::ExportSvg(..) => {
cmd.push("--format=svg");
}
ProjectTask::ExportSvgHtml(..) => {
cmd.push("--format=svg_html");
}
ProjectTask::ExportMd(..) => {
cmd.push("--format=md");
}
ProjectTask::ExportTeX(..) => {
cmd.push("--format=tex");
}
ProjectTask::ExportPng(..) => {
cmd.push("--format=png");
}
ProjectTask::ExportText(..) => {
cmd.push("--format=txt");
}
ProjectTask::ExportHtml(..) => {
cmd.push("--format=html");
}
}
let ext = task.task.extension();
output.push_str(&format!(
"# From {} to {} ({ext})\n",
task.doc_id(),
task.id
));
output.push_str(&cmd.build());
output.push('\n');
}
Ok(output)
}

View file

@ -0,0 +1,21 @@
//! # tinymist
//!
//! This crate provides a CLI that starts services for [Typst](https://typst.app/). It provides:
//! + `tinymist lsp`: A language server following the [Language Server Protocol](https://microsoft.github.io/language-server-protocol/).
//! + `tinymist preview`: A preview server for Typst.
//!
//! ## Usage
//!
//! See [Features: Command Line Interface](https://myriad-dreamin.github.io/tinymist/feature/cli.html).
//!
//! ## Documentation
//!
//! See [Crate Docs](https://myriad-dreamin.github.io/tinymist/rs/tinymist/index.html).
//!
//! Also see [Developer Guide: Tinymist LSP](https://myriad-dreamin.github.io/tinymist/module/lsp.html).
//!
//! ## Contributing
//!
//! See [CONTRIBUTING.md](https://github.com/Myriad-Dreamin/tinymist/blob/main/CONTRIBUTING.md).
pub use tinymist::*;

View file

@ -1,6 +1,13 @@
#![doc = include_str!("../README.md")] #![doc = include_str!("../README.md")]
mod args; mod args;
#[cfg(feature = "export")]
mod compile;
mod generate_script;
#[cfg(feature = "preview")]
mod preview;
mod testing;
mod utils;
use core::fmt; use core::fmt;
use std::collections::HashMap; use std::collections::HashMap;
@ -21,22 +28,31 @@ use sync_ls::{
internal_error, DapBuilder, DapMessage, GetMessageKind, LsHook, LspBuilder, LspClientRoot, internal_error, DapBuilder, DapMessage, GetMessageKind, LsHook, LspBuilder, LspClientRoot,
LspMessage, LspResult, Message, RequestId, TConnectionTx, LspMessage, LspResult, Message, RequestId, TConnectionTx,
}; };
use tinymist::tool::project::{compile_main, generate_script_main, project_main, task_main};
use tinymist::tool::testing::{coverage_main, test_main};
use tinymist::world::TaskInputs; use tinymist::world::TaskInputs;
use tinymist::{Config, DapRegularInit, RegularInit, ServerState, SuperInit, UserActionTask}; use tinymist::LONG_VERSION;
use tinymist_core::LONG_VERSION; use tinymist::{Config, RegularInit, ServerState, SuperInit, UserActionTask};
use tinymist_project::EntryResolver; use tinymist_project::EntryResolver;
use tinymist_query::package::PackageInfo; use tinymist_query::package::PackageInfo;
use tinymist_std::hash::{FxBuildHasher, FxHashMap}; use tinymist_std::hash::{FxBuildHasher, FxHashMap};
use tinymist_std::{bail, error::prelude::*}; use tinymist_std::{bail, error::prelude::*};
use typst::ecow::EcoString;
#[cfg(feature = "l10n")] #[cfg(feature = "l10n")]
use tinymist_l10n::{load_translations, set_translations}; use tinymist_l10n::{load_translations, set_translations};
use typst::ecow::EcoString; #[cfg(feature = "preview")]
use tinymist_project::LockFile;
#[cfg(feature = "preview")]
use tinymist_task::Id;
use crate::args::*; use crate::args::*;
#[cfg(feature = "export")]
use crate::compile::compile_main;
use crate::generate_script::generate_script_main;
#[cfg(feature = "preview")]
use crate::preview::preview_main;
use crate::testing::{coverage_main, test_main};
#[cfg(feature = "dhat-heap")] #[cfg(feature = "dhat-heap")]
#[global_allocator] #[global_allocator]
static ALLOC: dhat::Alloc = dhat::Alloc; static ALLOC: dhat::Alloc = dhat::Alloc;
@ -103,19 +119,16 @@ fn main() -> Result<()> {
Commands::Completion(args) => completion(args), Commands::Completion(args) => completion(args),
Commands::Cov(args) => coverage_main(args), Commands::Cov(args) => coverage_main(args),
Commands::Test(args) => RUNTIMES.tokio_runtime.block_on(test_main(args)), Commands::Test(args) => RUNTIMES.tokio_runtime.block_on(test_main(args)),
#[cfg(feature = "export")]
Commands::Compile(args) => RUNTIMES.tokio_runtime.block_on(compile_main(args)), Commands::Compile(args) => RUNTIMES.tokio_runtime.block_on(compile_main(args)),
Commands::GenerateScript(args) => generate_script_main(args), Commands::GenerateScript(args) => generate_script_main(args),
Commands::Query(query_cmds) => query_main(query_cmds), Commands::Query(query_cmds) => query_main(query_cmds),
Commands::Lsp(args) => lsp_main(args), Commands::Lsp(args) => lsp_main(args),
#[cfg(feature = "dap")]
Commands::Dap(args) => dap_main(args), Commands::Dap(args) => dap_main(args),
Commands::TraceLsp(args) => trace_lsp_main(args), Commands::TraceLsp(args) => trace_lsp_main(args),
#[cfg(feature = "preview")] #[cfg(feature = "preview")]
Commands::Preview(args) => { Commands::Preview(args) => RUNTIMES.tokio_runtime.block_on(preview_main(args)),
#[cfg(feature = "preview")]
use tinymist::tool::preview::preview_main;
RUNTIMES.tokio_runtime.block_on(preview_main(args))
}
Commands::Doc(args) => project_main(args), Commands::Doc(args) => project_main(args),
Commands::Task(args) => task_main(args), Commands::Task(args) => task_main(args),
Commands::Probe => Ok(()), Commands::Probe => Ok(()),
@ -163,6 +176,7 @@ pub fn lsp_main(args: LspArgs) -> Result<()> {
} }
/// The main entry point for the language server. /// The main entry point for the language server.
#[cfg(feature = "dap")]
pub fn dap_main(args: DapArgs) -> Result<()> { pub fn dap_main(args: DapArgs) -> Result<()> {
let pairs = LONG_VERSION.trim().split('\n'); let pairs = LONG_VERSION.trim().split('\n');
let pairs = pairs let pairs = pairs
@ -175,7 +189,7 @@ pub fn dap_main(args: DapArgs) -> Result<()> {
with_stdio_transport::<DapMessage>(args.mirror.clone(), |conn| { with_stdio_transport::<DapMessage>(args.mirror.clone(), |conn| {
let client = client_root(conn.sender); let client = client_root(conn.sender);
ServerState::install_dap(DapBuilder::new( ServerState::install_dap(DapBuilder::new(
DapRegularInit { tinymist::DapRegularInit {
client: client.weak().to_typed(), client: client.weak().to_typed(),
font_opts: args.font, font_opts: args.font,
}, },
@ -342,6 +356,85 @@ pub fn query_main(cmds: QueryCommands) -> Result<()> {
Ok(()) Ok(())
} }
#[cfg(feature = "preview")]
trait LockFileExt {
fn preview(&mut self, doc_id: Id, args: &TaskPreviewArgs) -> Result<Id>;
}
#[cfg(feature = "preview")]
impl LockFileExt for LockFile {
fn preview(&mut self, doc_id: Id, args: &TaskPreviewArgs) -> Result<Id> {
use tinymist_task::{ApplyProjectTask, PreviewTask, ProjectTask, TaskWhen};
let task_id = args
.task_name
.as_ref()
.map(|t| Id::new(t.clone()))
.unwrap_or(doc_id.clone());
let when = args.when.clone().unwrap_or(TaskWhen::OnType);
let task = ProjectTask::Preview(PreviewTask { when });
let task = ApplyProjectTask {
id: task_id.clone(),
document: doc_id,
task,
};
self.replace_task(task);
Ok(task_id)
}
}
/// Project document commands' main
#[cfg(feature = "lock")]
pub fn project_main(args: tinymist_project::DocCommands) -> Result<()> {
use tinymist_project::DocCommands;
let cwd = std::env::current_dir().context("cannot get cwd")?;
LockFile::update(&cwd, |state| {
let ctx: (&Path, &Path) = (&cwd, &cwd);
match args {
DocCommands::New(args) => {
state.replace_document(args.to_input(ctx));
}
DocCommands::Configure(args) => {
use tinymist_project::ProjectRoute;
let id: Id = args.id.id(ctx);
state.route.push(ProjectRoute {
id: id.clone(),
priority: args.priority,
});
}
}
Ok(())
})
}
/// Project task commands' main
#[cfg(feature = "lock")]
pub fn task_main(args: TaskCommands) -> Result<()> {
let cwd = std::env::current_dir().context("cannot get cwd")?;
LockFile::update(&cwd, |state| {
let _ = state;
match args {
#[cfg(feature = "preview")]
TaskCommands::Preview(args) => {
let ctx: (&Path, &Path) = (&cwd, &cwd);
let input = args.declare.to_input(ctx);
let id = input.id.clone();
state.replace_document(input);
let _ = state.preview(id, &args);
Ok(())
}
}
})
}
/// Creates a new language server host. /// Creates a new language server host.
fn client_root<M: TryFrom<Message, Error = anyhow::Error> + GetMessageKind>( fn client_root<M: TryFrom<Message, Error = anyhow::Error> + GetMessageKind>(
sender: TConnectionTx<M>, sender: TConnectionTx<M>,

View file

@ -0,0 +1,170 @@
use std::sync::Arc;
use futures::{SinkExt, StreamExt};
use hyper_tungstenite::tungstenite::Message;
use tinymist::{
project::ProjectPreviewState,
tool::{
preview::{bind_streams, make_http_server, PreviewCliArgs, ProjectPreviewHandler},
project::{start_project, ProjectOpts, StartProjectResult},
},
};
use tinymist_assets::TYPST_PREVIEW_HTML;
use tinymist_preview::{
frontend_html, ControlPlaneMessage, ControlPlaneTx, PreviewBuilder, PreviewConfig,
};
use tinymist_project::WorldProvider;
use tinymist_std::error::prelude::*;
use tokio::sync::mpsc;
use crate::utils::exit_on_ctrl_c;
/// Entry point of the preview tool.
pub async fn preview_main(args: PreviewCliArgs) -> Result<()> {
log::info!("Arguments: {args:#?}");
let handle = tokio::runtime::Handle::current();
let config = args.preview.config(&PreviewConfig::default());
#[cfg(feature = "open")]
let open_in_browser = args.open_in_browser(true);
let static_file_host =
if args.static_file_host == args.data_plane_host || !args.static_file_host.is_empty() {
Some(args.static_file_host)
} else {
None
};
exit_on_ctrl_c();
let verse = args.compile.resolve()?;
let previewer = PreviewBuilder::new(config);
let (service, handle) = {
let preview_state = ProjectPreviewState::default();
let opts = ProjectOpts {
handle: Some(handle),
preview: preview_state.clone(),
..ProjectOpts::default()
};
let StartProjectResult {
service,
intr_tx,
mut editor_rx,
} = start_project(verse, Some(opts), |compiler, intr, next| {
next(compiler, intr)
});
// Consume editor_rx
tokio::spawn(async move { while editor_rx.recv().await.is_some() {} });
let id = service.compiler.primary.id.clone();
let registered = preview_state.register(&id, previewer.compile_watcher(args.task_id));
if !registered {
tinymist_std::bail!("failed to register preview");
}
let handle: Arc<ProjectPreviewHandler> = Arc::new(ProjectPreviewHandler {
project_id: id,
client: Box::new(intr_tx),
});
(service, handle)
};
let (lsp_tx, mut lsp_rx) = ControlPlaneTx::new(true);
let control_plane_server_handle = tokio::spawn(async move {
let (control_sock_tx, mut control_sock_rx) = mpsc::unbounded_channel();
let srv =
make_http_server(String::default(), args.control_plane_host, control_sock_tx).await;
log::info!("Control panel server listening on: {}", srv.addr);
let control_websocket = control_sock_rx.recv().await.unwrap();
let ws = control_websocket.await.unwrap();
tokio::pin!(ws);
loop {
tokio::select! {
Some(resp) = lsp_rx.resp_rx.recv() => {
let r = ws
.send(Message::Text(serde_json::to_string(&resp).unwrap()))
.await;
let Err(err) = r else {
continue;
};
log::warn!("failed to send response to editor {err:?}");
break;
}
msg = ws.next() => {
let msg = match msg {
Some(Ok(Message::Text(msg))) => Some(msg),
Some(Ok(msg)) => {
log::error!("unsupported message: {msg:?}");
break;
}
Some(Err(e)) => {
log::error!("failed to receive message: {e}");
break;
}
_ => None,
};
if let Some(msg) = msg {
let Ok(msg) = serde_json::from_str::<ControlPlaneMessage>(&msg) else {
log::warn!("failed to parse control plane request: {msg:?}");
break;
};
lsp_rx.ctl_tx.send(msg).unwrap();
} else {
// todo: inform the editor that the connection is closed.
break;
}
}
}
}
let _ = srv.shutdown_tx.send(());
let _ = srv.join.await;
});
let (websocket_tx, websocket_rx) = mpsc::unbounded_channel();
let mut previewer = previewer.build(lsp_tx, handle.clone()).await;
tokio::spawn(service.run());
bind_streams(&mut previewer, websocket_rx);
let frontend_html = frontend_html(TYPST_PREVIEW_HTML, args.preview.preview_mode, "/");
let static_server = if let Some(static_file_host) = static_file_host {
log::warn!("--static-file-host is deprecated, which will be removed in the future. Use --data-plane-host instead.");
let html = frontend_html.clone();
Some(make_http_server(html, static_file_host, websocket_tx.clone()).await)
} else {
None
};
let srv = make_http_server(frontend_html, args.data_plane_host, websocket_tx).await;
log::info!("Data plane server listening on: {}", srv.addr);
let static_server_addr = static_server.as_ref().map(|s| s.addr).unwrap_or(srv.addr);
log::info!("Static file server listening on: {static_server_addr}");
#[cfg(feature = "open")]
if open_in_browser {
open::that_detached(format!("http://{static_server_addr}"))
.log_error("failed to open browser for preview");
}
let _ = tokio::join!(previewer.join(), srv.join, control_plane_server_handle);
// Assert that the static server's lifetime is longer than the previewer.
let _s = static_server;
Ok(())
}

View file

@ -24,9 +24,11 @@ use typst::syntax::{ast, LinkedNode, Source, Span};
use typst::{utils::PicoStr, World}; use typst::{utils::PicoStr, World};
use typst_shim::eval::TypstEngine; use typst_shim::eval::TypstEngine;
use super::project::{start_project, StartProjectResult}; use tinymist::project::*;
use crate::world::{with_main, SourceWorld}; use tinymist::tool::project::{start_project, StartProjectResult};
use crate::{project::*, utils::exit_on_ctrl_c}; use tinymist::world::{with_main, SourceWorld};
use crate::utils::exit_on_ctrl_c;
const TEST_EVICT_MAX_AGE: usize = 30; const TEST_EVICT_MAX_AGE: usize = 30;
const PREFIX_LEN: usize = 7; const PREFIX_LEN: usize = 7;

View file

@ -0,0 +1,7 @@
pub fn exit_on_ctrl_c() {
tokio::spawn(async move {
let _ = tokio::signal::ctrl_c().await;
log::info!("Ctrl-C received, exiting");
std::process::exit(0);
});
}

View file

@ -1,46 +0,0 @@
[package]
name = "tinymist-core"
description = "Tinymist core library."
categories = ["compilers", "command-line-utilities"]
keywords = ["api", "language", "typst"]
authors.workspace = true
version.workspace = true
license.workspace = true
edition.workspace = true
homepage.workspace = true
repository.workspace = true
rust-version.workspace = true
[lib]
crate-type = ["cdylib", "rlib"]
[features]
default = ["web", "no-content-hint"]
web = [
"wasm-bindgen",
"js-sys",
"console_error_panic_hook",
"no-content-hint",
"tinymist-project/web",
"reflexo-typst/web",
]
no-content-hint = ["reflexo-typst/no-content-hint"]
[dependencies]
wasm-bindgen = { version = "0.2.100", optional = true }
js-sys = { version = "0.3.77", optional = true }
tinymist-project.workspace = true
tinymist-query.workspace = true
reflexo-typst.workspace = true
console_error_panic_hook = { version = "0.1.2", optional = true }
[build-dependencies]
anyhow.workspace = true
vergen.workspace = true
cargo_metadata = "0.18.0"
[lints]
workspace = true

View file

@ -1,30 +0,0 @@
//! Tinymist Core Library
use std::sync::LazyLock;
/// The long version description of the library
pub static LONG_VERSION: LazyLock<String> = LazyLock::new(|| {
format!(
"
Build Timestamp: {}
Build Git Describe: {}
Commit SHA: {}
Commit Date: {}
Commit Branch: {}
Cargo Target Triple: {}
Typst Version: {}
Typst Source: {}
",
env!("VERGEN_BUILD_TIMESTAMP"),
env!("VERGEN_GIT_DESCRIBE"),
option_env!("VERGEN_GIT_SHA").unwrap_or("None"),
option_env!("VERGEN_GIT_COMMIT_TIMESTAMP").unwrap_or("None"),
option_env!("VERGEN_GIT_BRANCH").unwrap_or("None"),
env!("VERGEN_CARGO_TARGET_TRIPLE"),
env!("TYPST_VERSION"),
env!("TYPST_SOURCE"),
)
});
#[cfg(feature = "web")]
pub mod web;

View file

@ -282,6 +282,28 @@ impl LspUniverseBuilder {
Ok(searcher.build()) Ok(searcher.build())
} }
/// Resolve fonts from given options.
#[cfg(all(not(feature = "system"), feature = "web"))]
pub fn resolve_fonts(args: CompileFontArgs) -> Result<FontResolverImpl> {
let mut searcher = tinymist_world::font::web::BrowserFontSearcher::new();
searcher.resolve_opts(tinymist_world::config::CompileFontOpts {
font_paths: args.font_paths,
no_system_fonts: args.ignore_system_fonts,
with_embedded_fonts: typst_assets::fonts()
.map(std::borrow::Cow::Borrowed)
.collect(),
})?;
Ok(searcher.build())
}
/// Resolve fonts from given options.
#[cfg(not(any(feature = "system", feature = "web")))]
pub fn resolve_fonts(_args: CompileFontArgs) -> Result<FontResolverImpl> {
let mut searcher = tinymist_world::font::memory::MemoryFontSearcher::default();
searcher.add_memory_fonts(typst_assets::fonts().map(Bytes::new).collect::<Vec<_>>());
Ok(searcher.build())
}
/// Resolves package registry from given options. /// Resolves package registry from given options.
#[cfg(feature = "system")] #[cfg(feature = "system")]
pub fn resolve_package( pub fn resolve_package(

View file

@ -69,6 +69,7 @@ sha2 = { version = "0.10" }
hex = { version = "0.4" } hex = { version = "0.4" }
[features] [features]
local-registry = ["tinymist-world/system"]
[lints] [lints]
workspace = true workspace = true

View file

@ -34,6 +34,8 @@ impl CompletionPair<'_, '_, '_> {
.iter() .iter()
.map(|(spec, desc)| (spec, desc.clone())) .map(|(spec, desc)| (spec, desc.clone()))
.collect(); .collect();
#[cfg(feature = "http-registry")]
{
// local_packages to references and add them to the packages // local_packages to references and add them to the packages
let local_packages_refs = self.worker.ctx.local_packages(); let local_packages_refs = self.worker.ctx.local_packages();
packages.extend( packages.extend(
@ -41,6 +43,7 @@ impl CompletionPair<'_, '_, '_> {
.iter() .iter()
.map(|spec| (spec, Some(eco_format!("{} v{}", spec.name, spec.version)))), .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 {

View file

@ -672,7 +672,7 @@ impl SharedContext {
} }
/// Get the local packages and their descriptions. /// Get the local packages and their descriptions.
#[cfg(not(target_arch = "wasm32"))] #[cfg(feature = "local-registry")]
pub fn local_packages(&self) -> EcoVec<PackageSpec> { pub fn local_packages(&self) -> EcoVec<PackageSpec> {
crate::package::list_package_by_namespace(&self.world.registry, eco_format!("local")) crate::package::list_package_by_namespace(&self.world.registry, eco_format!("local"))
.into_iter() .into_iter()
@ -681,7 +681,7 @@ impl SharedContext {
} }
/// Get the local packages and their descriptions. /// Get the local packages and their descriptions.
#[cfg(target_arch = "wasm32")] #[cfg(not(feature = "local-registry"))]
pub fn local_packages(&self) -> EcoVec<PackageSpec> { pub fn local_packages(&self) -> EcoVec<PackageSpec> {
eco_vec![] eco_vec![]
} }

View file

@ -43,14 +43,14 @@ pub fn path_res_to_url(path: PathResolution) -> anyhow::Result<Url> {
} }
/// Convert a URL to a path. /// Convert a URL to a path.
pub fn url_to_path(uri: Url) -> PathBuf { pub fn url_to_path(uri: &Url) -> PathBuf {
if uri.scheme() == "file" { if uri.scheme() == "file" {
// typst converts an empty path to `Path::new("/")`, which is undesirable. // typst converts an empty path to `Path::new("/")`, which is undesirable.
if !uri.has_host() && uri.path() == "/" { if !uri.has_host() && uri.path() == "/" {
return PathBuf::from("/untitled/nEoViM-BuG"); return PathBuf::from("/untitled/nEoViM-BuG");
} }
return url_to_file_path(&uri); return url_to_file_path(uri);
} }
if uri.scheme() == "untitled" { if uri.scheme() == "untitled" {
@ -67,7 +67,7 @@ pub fn url_to_path(uri: Url) -> PathBuf {
return Path::new(String::from_utf8_lossy(&bytes).as_ref()).clean(); return Path::new(String::from_utf8_lossy(&bytes).as_ref()).clean();
} }
url_to_file_path(&uri) url_to_file_path(uri)
} }
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
@ -114,7 +114,7 @@ mod test {
assert_eq!(uri.scheme(), "untitled"); assert_eq!(uri.scheme(), "untitled");
assert_eq!(uri.path(), "test"); assert_eq!(uri.path(), "test");
let path = url_to_path(uri); let path = url_to_path(&uri);
assert_eq!(path, Path::new("/untitled/test").clean()); assert_eq!(path, Path::new("/untitled/test").clean());
} }
@ -122,7 +122,7 @@ mod test {
fn unnamed_buffer() { fn unnamed_buffer() {
// https://github.com/neovim/nvim-lspconfig/pull/2226 // https://github.com/neovim/nvim-lspconfig/pull/2226
let uri = EMPTY_URL.clone(); let uri = EMPTY_URL.clone();
let path = url_to_path(uri); let path = url_to_path(&uri);
assert_eq!(path, Path::new("/untitled/nEoViM-BuG")); assert_eq!(path, Path::new("/untitled/nEoViM-BuG"));
let uri2 = path_to_url(&path).unwrap(); let uri2 = path_to_url(&path).unwrap();

View file

@ -73,7 +73,7 @@ pub fn check_package(ctx: &mut LocalContext, spec: &PackageInfo) -> StrResult<()
Ok(()) Ok(())
} }
#[cfg(not(target_arch = "wasm32"))] #[cfg(feature = "local-registry")]
/// Get the packages in namespaces and their descriptions. /// Get the packages in namespaces and their descriptions.
pub fn list_package_by_namespace( pub fn list_package_by_namespace(
registry: &tinymist_world::package::registry::HttpRegistry, registry: &tinymist_world::package::registry::HttpRegistry,

View file

@ -1,6 +1,6 @@
use std::sync::Arc; use std::sync::Arc;
use rayon::iter::ParallelIterator; use rayon::iter::{IntoParallelIterator, ParallelIterator};
use typst::foundations::Bytes; use typst::foundations::Bytes;
use typst::text::{FontBook, FontInfo}; use typst::text::{FontBook, FontInfo};
@ -25,17 +25,41 @@ impl MemoryFontSearcher {
Self { fonts: vec![] } Self { fonts: vec![] }
} }
/// Create a new browser searcher with fonts in a FontResolverImpl.
pub fn from_resolver(resolver: FontResolverImpl) -> Self {
let fonts = resolver
.slots
.into_iter()
.enumerate()
.map(|(idx, slot)| {
(
resolver
.book
.info(idx)
.expect("font should be in font book")
.clone(),
slot,
)
})
.collect();
Self { fonts }
}
/// Adds an in-memory font. /// Adds an in-memory font.
pub fn add_memory_font(&mut self, data: Bytes) { pub fn add_memory_font(&mut self, data: Bytes) {
self.add_memory_fonts(rayon::iter::once(data)); self.add_memory_fonts(rayon::iter::once(data));
} }
/// Adds in-memory fonts. /// Adds in-memory fonts.
pub fn add_memory_fonts(&mut self, data: impl ParallelIterator<Item = Bytes>) { pub fn add_memory_fonts(&mut self, data: impl IntoParallelIterator<Item = Bytes>) {
let source = DataSource::Memory(MemoryDataSource { let source = DataSource::Memory(MemoryDataSource {
name: "<memory>".to_owned(), name: "<memory>".to_owned(),
}); });
self.extend_bytes(data.map(|data| (data, Some(source.clone())))); self.extend_bytes(
data.into_par_iter()
.map(|data| (data, Some(source.clone()))),
);
} }
/// Adds a number of raw font resources. /// Adds a number of raw font resources.

View file

@ -1,14 +1,19 @@
use std::borrow::Cow;
use js_sys::ArrayBuffer; use js_sys::ArrayBuffer;
use rayon::iter::{IntoParallelIterator, ParallelIterator};
use tinymist_std::error::prelude::*; use tinymist_std::error::prelude::*;
use typst::foundations::Bytes; use typst::foundations::Bytes;
use typst::text::{ use typst::text::{
Coverage, Font, FontBook, FontFlags, FontInfo, FontStretch, FontStyle, FontVariant, FontWeight, Coverage, Font, FontFlags, FontInfo, FontStretch, FontStyle, FontVariant, FontWeight,
}; };
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
use super::{BufferFontLoader, FontLoader, FontResolverImpl, FontSlot}; use super::{BufferFontLoader, FontLoader, FontResolverImpl, FontSlot};
use crate::config::CompileFontOpts;
use crate::font::cache::FontInfoCache; use crate::font::cache::FontInfoCache;
use crate::font::info::typst_typographic_family; use crate::font::info::typst_typographic_family;
use crate::font::memory::MemoryFontSearcher;
/// Destructures a JS `[key, value]` pair into a tuple of [`Deserializer`]s. /// Destructures a JS `[key, value]` pair into a tuple of [`Deserializer`]s.
pub(crate) fn convert_pair(pair: JsValue) -> (JsValue, JsValue) { pub(crate) fn convert_pair(pair: JsValue) -> (JsValue, JsValue) {
@ -365,71 +370,47 @@ impl FontLoader for WebFontLoader {
/// Searches for fonts. /// Searches for fonts.
pub struct BrowserFontSearcher { pub struct BrowserFontSearcher {
pub fonts: Vec<(FontInfo, FontSlot)>, /// The base font searcher.
base: MemoryFontSearcher,
} }
impl BrowserFontSearcher { impl BrowserFontSearcher {
/// Create a new, empty browser searcher. /// Creates a new, empty browser searcher.
pub fn new() -> Self { pub fn new() -> Self {
Self { fonts: vec![] } Self {
base: MemoryFontSearcher::default(),
}
} }
/// Create a new browser searcher with fonts in a FontResolverImpl. /// Creates a new browser searcher with fonts in a FontResolverImpl.
pub fn from_resolver(resolver: FontResolverImpl) -> Self { pub fn from_resolver(resolver: FontResolverImpl) -> Self {
let fonts = resolver let base = MemoryFontSearcher::from_resolver(resolver);
.slots Self { base }
.into_iter()
.enumerate()
.map(|(idx, slot)| {
(
resolver
.book
.info(idx)
.expect("font should be in font book")
.clone(),
slot,
)
})
.collect();
Self { fonts }
} }
/// Create a new browser searcher with fonts cloned from a FontResolverImpl. /// Builds a FontResolverImpl.
/// Since FontSlot only holds QueryRef to font data, cloning is cheap.
pub fn new_with_resolver(resolver: &FontResolverImpl) -> Self {
let fonts = resolver
.slots
.iter()
.enumerate()
.map(|(idx, slot)| {
(
resolver
.book
.info(idx)
.expect("font should be in font book")
.clone(),
slot.clone(),
)
})
.collect();
Self { fonts }
}
/// Build a FontResolverImpl.
pub fn build(self) -> FontResolverImpl { pub fn build(self) -> FontResolverImpl {
let (info, slots): (Vec<FontInfo>, Vec<FontSlot>) = self.fonts.into_iter().unzip(); self.base.build()
let book = FontBook::from_infos(info);
FontResolverImpl::new(vec![], book, slots)
} }
} }
impl BrowserFontSearcher { impl BrowserFontSearcher {
/// Add fonts that are embedded in the binary. /// Resolves fonts from given options.
pub fn resolve_opts(&mut self, opts: CompileFontOpts) -> Result<()> {
// Source3: add the fonts in memory.
self.add_memory_fonts(opts.with_embedded_fonts.into_par_iter().map(|font_data| {
match font_data {
Cow::Borrowed(data) => Bytes::new(data),
Cow::Owned(data) => Bytes::new(data),
}
}));
Ok(())
}
/// Adds fonts that are embedded in the binary.
#[cfg(feature = "fonts")] #[cfg(feature = "fonts")]
#[deprecated(note = "use `typst_assets::fonts` directly")]
pub fn add_embedded(&mut self) { pub fn add_embedded(&mut self) {
for font_data in typst_assets::fonts() { for font_data in typst_assets::fonts() {
let buffer = Bytes::new(font_data); let buffer = Bytes::new(font_data);
@ -441,6 +422,11 @@ impl BrowserFontSearcher {
} }
} }
/// Adds in-memory fonts.
pub fn add_memory_fonts(&mut self, data: impl ParallelIterator<Item = Bytes>) {
self.base.add_memory_fonts(data);
}
pub async fn add_web_fonts(&mut self, fonts: js_sys::Array) -> Result<()> { pub async fn add_web_fonts(&mut self, fonts: js_sys::Array) -> Result<()> {
let font_builder = FontBuilder {}; let font_builder = FontBuilder {};
@ -448,8 +434,8 @@ impl BrowserFontSearcher {
let (font_ref, font_blob_loader, font_info) = font_builder.font_web_to_typst(&v)?; let (font_ref, font_blob_loader, font_info) = font_builder.font_web_to_typst(&v)?;
for (i, info) in font_info.into_iter().enumerate() { for (i, info) in font_info.into_iter().enumerate() {
let index = self.fonts.len(); let index = self.base.fonts.len();
self.fonts.push(( self.base.fonts.push((
info.clone(), info.clone(),
FontSlot::new(WebFontLoader { FontSlot::new(WebFontLoader {
font: WebFont { font: WebFont {
@ -470,7 +456,7 @@ impl BrowserFontSearcher {
pub fn add_font_data(&mut self, buffer: Bytes) { pub fn add_font_data(&mut self, buffer: Bytes) {
for (i, info) in FontInfo::iter(buffer.as_slice()).enumerate() { for (i, info) in FontInfo::iter(buffer.as_slice()).enumerate() {
let buffer = buffer.clone(); let buffer = buffer.clone();
self.fonts.push(( self.base.fonts.push((
info, info,
FontSlot::new(BufferFontLoader { FontSlot::new(BufferFontLoader {
buffer: Some(buffer), buffer: Some(buffer),
@ -481,7 +467,7 @@ impl BrowserFontSearcher {
} }
pub fn with_fonts_mut(&mut self, func: impl FnOnce(&mut Vec<(FontInfo, FontSlot)>)) { pub fn with_fonts_mut(&mut self, func: impl FnOnce(&mut Vec<(FontInfo, FontSlot)>)) {
func(&mut self.fonts); func(&mut self.base.fonts);
} }
} }

View file

@ -2,7 +2,7 @@
name = "tinymist" name = "tinymist"
description = "An integrated language service for Typst." description = "An integrated language service for Typst."
categories = ["compilers", "command-line-utilities"] categories = ["compilers", "command-line-utilities"]
keywords = ["cli", "lsp", "language", "typst"] keywords = ["api", "language", "typst"]
authors.workspace = true authors.workspace = true
version.workspace = true version.workspace = true
license.workspace = true license.workspace = true
@ -11,6 +11,9 @@ homepage.workspace = true
repository.workspace = true repository.workspace = true
rust-version.workspace = true rust-version.workspace = true
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies] [dependencies]
anyhow.workspace = true anyhow.workspace = true
async-trait.workspace = true async-trait.workspace = true
@ -25,29 +28,20 @@ clap_mangen.workspace = true
crossbeam-channel.workspace = true crossbeam-channel.workspace = true
codespan-reporting.workspace = true codespan-reporting.workspace = true
comemo.workspace = true comemo.workspace = true
dapts.workspace = true
dhat = { workspace = true, optional = true } dhat = { workspace = true, optional = true }
dirs.workspace = true dirs.workspace = true
env_logger.workspace = true env_logger.workspace = true
futures.workspace = true futures.workspace = true
hyper.workspace = true
hyper-util = { workspace = true, features = [
"server",
"http1",
"http2",
"server-graceful",
"server-auto",
] }
http-body-util = "0.1.2"
hyper-tungstenite = { workspace = true, optional = true }
itertools.workspace = true itertools.workspace = true
lsp-types.workspace = true lsp-types.workspace = true
log.workspace = true log.workspace = true
open.workspace = true open = { workspace = true, optional = true }
parking_lot.workspace = true parking_lot.workspace = true
paste.workspace = true paste.workspace = true
rayon.workspace = true rayon.workspace = true
reflexo.workspace = true reflexo.workspace = true
reflexo-typst = { workspace = true, features = ["system", "svg"] } reflexo-typst = { workspace = true, features = ["svg"] }
reflexo-vec2svg.workspace = true reflexo-vec2svg.workspace = true
rpds.workspace = true rpds.workspace = true
serde.workspace = true serde.workspace = true
@ -56,12 +50,15 @@ serde_yaml.workspace = true
strum.workspace = true strum.workspace = true
sync-ls = { workspace = true, features = ["lsp", "server"] } sync-ls = { workspace = true, features = ["lsp", "server"] }
tinymist-assets = { workspace = true } tinymist-assets = { workspace = true }
tinymist-debug = { workspace = true, optional = true }
tinymist-l10n.workspace = true
tinymist-query.workspace = true tinymist-query.workspace = true
tinymist-std.workspace = true tinymist-std.workspace = true
tinymist-core = { workspace = true, default-features = false, features = [] } tinymist-preview = { workspace = true, optional = true }
tinymist-project = { workspace = true, features = ["lsp"] } tinymist-project = { workspace = true, features = ["lsp"] }
tinymist-render.workspace = true tinymist-render.workspace = true
tokio = { workspace = true, features = ["rt-multi-thread", "io-std"] } tinymist-task.workspace = true
tokio = { workspace = true }
tokio-util.workspace = true tokio-util.workspace = true
toml.workspace = true toml.workspace = true
ttf-parser.workspace = true ttf-parser.workspace = true
@ -71,78 +68,73 @@ typst-svg.workspace = true
typst-pdf.workspace = true typst-pdf.workspace = true
typst-render.workspace = true typst-render.workspace = true
typst-timing.workspace = true typst-timing.workspace = true
typst-html = { workspace = true, optional = true } typst-html.workspace = true
typst-shim.workspace = true typst-shim.workspace = true
tinymist-preview = { workspace = true, optional = true }
typst-ansi-hl.workspace = true typst-ansi-hl.workspace = true
tinymist-task.workspace = true
tinymist-debug.workspace = true
typstfmt.workspace = true typstfmt.workspace = true
typstyle-core.workspace = true typstyle-core.workspace = true
unicode-script.workspace = true unicode-script.workspace = true
walkdir.workspace = true walkdir.workspace = true
tinymist-l10n.workspace = true
dapts.workspace = true http-body-util = { workspace = true, optional = true }
hyper = { workspace = true, optional = true }
hyper-util = { workspace = true, optional = true, features = [
"server",
"http1",
"http2",
"server-graceful",
"server-auto",
] }
hyper-tungstenite = { workspace = true, optional = true }
[features] console_error_panic_hook = { version = "0.1.2", optional = true }
default = [ js-sys = { version = "0.3.77", optional = true }
"cli", wasm-bindgen = { version = "0.2.100", optional = true }
"html",
"pdf",
"l10n",
"preview",
"embed-fonts",
"no-content-hint",
"dap",
]
cli = ["sync-ls/clap", "clap/wrap_help"]
dhat-heap = ["dhat"]
# Embeds Typst's default fonts for
# - text (Linux Libertine),
# - math (New Computer Modern Math), and
# - code (Deja Vu Sans Mono)
# and additionally New Computer Modern for text
# into the binary.
embed-fonts = ["tinymist-project/fonts"]
# Enable the experimental HTML backend.
html = ["dep:typst-html"]
pdf = ["tinymist-task/pdf"]
# Disable the default content hint.
# This requires modifying typst.
no-content-hint = [
"tinymist-task/no-content-hint",
"tinymist-project/no-content-hint",
"tinymist-preview/no-content-hint",
"typlite/no-content-hint",
"reflexo-typst/no-content-hint",
"reflexo-vec2svg/no-content-hint",
]
preview = [
"tinymist-preview",
"tinymist-preview/clap",
"tinymist-assets/typst-preview",
"hyper-tungstenite",
]
dap = ["sync-ls/dap"]
l10n = ["tinymist-assets/l10n"]
[dev-dependencies] [dev-dependencies]
temp-env.workspace = true temp-env.workspace = true
[build-dependencies] [build-dependencies]
anyhow.workspace = true anyhow.workspace = true
cargo_metadata = "0.18.0"
vergen.workspace = true vergen.workspace = true
cargo_metadata = "0.18.0"
[features]
dap = ["sync-ls/dap", "tinymist-debug"]
default = ["web", "no-content-hint"]
preview = [
"open",
"http-body-util",
"hyper",
"hyper-util",
"hyper-tungstenite",
"tinymist-preview",
"tinymist-preview/clap",
"tinymist-assets/typst-preview",
]
lock = []
export = []
trace = []
web = [
"console_error_panic_hook",
"js-sys",
"wasm-bindgen",
"reflexo-typst/web",
"tinymist-project/web",
]
open = ["dep:open"]
system = [
"lock",
"open",
"reflexo-typst/system",
"tinymist-project/system",
"tinymist-query/local-registry",
"sync-ls/system",
"tokio/rt-multi-thread",
"tokio/io-std",
]
no-content-hint = ["reflexo-typst/no-content-hint"]
[lints] [lints]
workspace = true workspace = true

View file

@ -9,19 +9,19 @@
"Typst" "Typst"
], ],
"type": "module", "type": "module",
"module": "pkg/tinymist_core.js", "module": "pkg/tinymist.js",
"types": "pkg/tinymist_core.d.ts", "types": "pkg/tinymist.d.ts",
"files": [ "files": [
"pkg/tinymist_core_bg.wasm", "pkg/tinymist_bg.wasm",
"pkg/tinymist_core_bg.wasm.d.ts", "pkg/tinymist_bg.wasm.d.ts",
"pkg/tinymist_core_bg.js", "pkg/tinymist_bg.js",
"pkg/tinymist_core.js", "pkg/tinymist.js",
"pkg/tinymist_core.d.ts" "pkg/tinymist.d.ts"
], ],
"scripts": { "scripts": {
"build:dev": "wasm-pack build --target web --dev -- --no-default-features --features web", "build:dev": "wasm-pack build --target web --dev -- --no-default-features --features web,no-content-hint",
"build:node": "wasm-pack build --target nodejs -- --no-default-features --features web", "build:node": "wasm-pack build --target nodejs -- --no-default-features --features web,no-content-hint",
"build": "wasm-pack build --target web -- --no-default-features --features web", "build": "wasm-pack build --target web -- --no-default-features --features web,no-content-hint",
"publish:dry": "npm publish --dry-run", "publish:dry": "npm publish --dry-run",
"publish:lib": "npm publish || exit 0", "publish:lib": "npm publish || exit 0",
"test:chrome": "wasm-pack test --chrome --headless --release", "test:chrome": "wasm-pack test --chrome --headless --release",

View file

@ -1,12 +1,13 @@
//! Tinymist LSP commands //! Tinymist LSP commands
use std::ops::{Deref, Range}; use std::ops::Range;
use std::path::PathBuf; use std::path::PathBuf;
use lsp_types::TextDocumentIdentifier; use lsp_types::TextDocumentIdentifier;
use serde::{Deserialize, Serialize}; use serde::Deserialize;
use serde_json::Value as JsonValue; use serde_json::Value as JsonValue;
use sync_ls::RequestId; use sync_ls::RequestId;
#[cfg(feature = "trace")]
use task::TraceParams; use task::TraceParams;
use tinymist_assets::TYPST_PREVIEW_HTML; use tinymist_assets::TYPST_PREVIEW_HTML;
use tinymist_project::{ use tinymist_project::{
@ -17,14 +18,20 @@ use tinymist_query::package::PackageInfo;
use tinymist_query::{LocalContextGuard, LspRange}; use tinymist_query::{LocalContextGuard, LspRange};
use tinymist_std::error::prelude::*; use tinymist_std::error::prelude::*;
use tinymist_task::ExportMarkdownTask; use tinymist_task::ExportMarkdownTask;
use typst::diag::{eco_format, EcoString, StrResult}; use typst::diag::{eco_format, StrResult};
use typst::syntax::package::{PackageSpec, VersionlessPackageSpec};
use typst::syntax::{LinkedNode, Source}; use typst::syntax::{LinkedNode, Source};
use world::TaskInputs; use world::TaskInputs;
use super::*; use super::*;
use crate::lsp::query::{run_query, LspClientExt}; use crate::lsp::query::{run_query, LspClientExt};
use crate::tool::ast::AstRepr; use crate::tool::ast::AstRepr;
#[cfg(feature = "system")]
use typst::diag::EcoString;
#[cfg(feature = "system")]
use typst::syntax::package::{PackageSpec, VersionlessPackageSpec};
#[cfg(feature = "system")]
use crate::tool::package::InitTask; use crate::tool::package::InitTask;
/// See [`ProjectTask`]. /// See [`ProjectTask`].
@ -416,10 +423,11 @@ impl ServerState {
} }
/// Initialize a new template. /// Initialize a new template.
#[cfg(feature = "system")]
pub fn init_template(&mut self, mut args: Vec<JsonValue>) -> AnySchedulableResponse { pub fn init_template(&mut self, mut args: Vec<JsonValue>) -> AnySchedulableResponse {
use crate::tool::package::{self, TemplateSource}; use crate::tool::package::{self, TemplateSource};
#[derive(Debug, Serialize)] #[derive(Debug, serde::Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct InitResult { struct InitResult {
entry_path: PathBuf, entry_path: PathBuf,
@ -466,6 +474,7 @@ impl ServerState {
} }
/// Get the entry of a template. /// Get the entry of a template.
#[cfg(feature = "system")]
pub fn get_template_entry(&mut self, mut args: Vec<JsonValue>) -> AnySchedulableResponse { pub fn get_template_entry(&mut self, mut args: Vec<JsonValue>) -> AnySchedulableResponse {
use crate::tool::package::{self, TemplateSource}; use crate::tool::package::{self, TemplateSource};
@ -528,7 +537,9 @@ impl ServerState {
} }
/// Get the trace data of the document. /// Get the trace data of the document.
#[cfg(feature = "trace")]
pub fn get_document_trace(&mut self, mut args: Vec<JsonValue>) -> AnySchedulableResponse { pub fn get_document_trace(&mut self, mut args: Vec<JsonValue>) -> AnySchedulableResponse {
use std::ops::Deref;
let path = get_arg!(args[0] as PathBuf).into(); let path = get_arg!(args[0] as PathBuf).into();
// get path to self program // get path to self program
@ -573,6 +584,7 @@ impl ServerState {
} }
/// Start to get the trace data of the server. /// Start to get the trace data of the server.
#[cfg(feature = "trace")]
pub fn start_server_trace(&mut self, _args: Vec<JsonValue>) -> AnySchedulableResponse { pub fn start_server_trace(&mut self, _args: Vec<JsonValue>) -> AnySchedulableResponse {
let task_cell = &mut self.server_trace; let task_cell = &mut self.server_trace;
if task_cell if task_cell
@ -595,6 +607,7 @@ impl ServerState {
} }
/// Stop getting the trace data of the server. /// Stop getting the trace data of the server.
#[cfg(feature = "trace")]
pub fn stop_server_trace(&mut self, _args: Vec<JsonValue>) -> AnySchedulableResponse { pub fn stop_server_trace(&mut self, _args: Vec<JsonValue>) -> AnySchedulableResponse {
let task_cell = &mut self.server_trace; let task_cell = &mut self.server_trace;
if task_cell if task_cell
@ -671,6 +684,7 @@ impl ServerState {
} }
/// Get directory of packages /// Get directory of packages
#[cfg(feature = "system")]
pub fn resource_package_dirs(&mut self, _arguments: Vec<JsonValue>) -> AnySchedulableResponse { pub fn resource_package_dirs(&mut self, _arguments: Vec<JsonValue>) -> AnySchedulableResponse {
let snap = self.snapshot().map_err(internal_error)?; let snap = self.snapshot().map_err(internal_error)?;
just_future(async move { just_future(async move {
@ -681,6 +695,7 @@ impl ServerState {
} }
/// Get writable directory of packages /// Get writable directory of packages
#[cfg(feature = "system")]
pub fn resource_local_package_dir( pub fn resource_local_package_dir(
&mut self, &mut self,
_arguments: Vec<JsonValue>, _arguments: Vec<JsonValue>,
@ -694,6 +709,7 @@ impl ServerState {
} }
/// Get writable directory of packages /// Get writable directory of packages
#[cfg(feature = "system")]
pub fn resource_package_by_ns( pub fn resource_package_by_ns(
&mut self, &mut self,
mut arguments: Vec<JsonValue>, mut arguments: Vec<JsonValue>,

View file

@ -11,12 +11,11 @@ use reflexo_typst::{ImmutPath, TypstDict};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::{Map, Value as JsonValue}; use serde_json::{Map, Value as JsonValue};
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
use task::{ExportUserConfig, FormatUserConfig, FormatterConfig}; use task::{FormatUserConfig, FormatterConfig};
use tinymist_l10n::DebugL10n; use tinymist_l10n::DebugL10n;
use tinymist_preview::{PreviewConfig, PreviewInvertColors};
use tinymist_project::{DynAccessModel, LspAccessModel}; use tinymist_project::{DynAccessModel, LspAccessModel};
use tinymist_query::analysis::{Modifier, TokenType}; use tinymist_query::analysis::{Modifier, TokenType};
use tinymist_query::{CompletionFeat, PositionEncoding}; use tinymist_query::{url_to_path, CompletionFeat, PositionEncoding};
use tinymist_render::PeriscopeArgs; use tinymist_render::PeriscopeArgs;
use tinymist_std::error::prelude::*; use tinymist_std::error::prelude::*;
use tinymist_task::ExportTarget; use tinymist_task::ExportTarget;
@ -26,11 +25,18 @@ use typst_shim::utils::LazyHash;
use super::*; use super::*;
use crate::project::{ use crate::project::{
EntryResolver, ExportPdfTask, ExportTask, ImmutDict, PathPattern, ProjectResolutionKind, EntryResolver, ExportTask, ImmutDict, PathPattern, ProjectResolutionKind, TaskWhen,
ProjectTask, TaskWhen,
}; };
use crate::world::font::FontResolverImpl; use crate::world::font::FontResolverImpl;
#[cfg(feature = "export")]
use task::ExportUserConfig;
#[cfg(feature = "preview")]
use tinymist_preview::{PreviewConfig, PreviewInvertColors};
#[cfg(feature = "export")]
use crate::project::{ExportPdfTask, ProjectTask};
// region Configuration Items // region Configuration Items
const CONFIG_ITEMS: &[&str] = &[ const CONFIG_ITEMS: &[&str] = &[
"tinymist", "tinymist",
@ -172,13 +178,13 @@ impl Config {
let roots = match params.workspace_folders.as_ref() { let roots = match params.workspace_folders.as_ref() {
Some(roots) => roots Some(roots) => roots
.iter() .iter()
.filter_map(|root| root.uri.to_file_path().ok().map(ImmutPath::from)) .map(|root| ImmutPath::from(url_to_path(&root.uri)))
.collect(), .collect(),
#[allow(deprecated)] // `params.root_path` is marked as deprecated #[allow(deprecated)] // `params.root_path` is marked as deprecated
None => params None => params
.root_uri .root_uri
.as_ref() .as_ref()
.and_then(|uri| uri.to_file_path().ok().map(ImmutPath::from)) .map(|uri| ImmutPath::from(url_to_path(uri)))
.or_else(|| Some(Path::new(&params.root_path.as_ref()?).into())) .or_else(|| Some(Path::new(&params.root_path.as_ref()?).into()))
.into_iter() .into_iter()
.collect(), .collect(),
@ -510,6 +516,7 @@ impl Config {
} }
/// Gets the preview configuration. /// Gets the preview configuration.
#[cfg(feature = "preview")]
pub fn preview(&self) -> PreviewConfig { pub fn preview(&self) -> PreviewConfig {
PreviewConfig { PreviewConfig {
enable_partial_rendering: self.preview.partial_rendering, enable_partial_rendering: self.preview.partial_rendering,
@ -529,6 +536,7 @@ impl Config {
} }
/// Gets the export configuration. /// Gets the export configuration.
#[cfg(feature = "export")]
pub(crate) fn export(&self) -> ExportUserConfig { pub(crate) fn export(&self) -> ExportUserConfig {
let export = self.export_task(); let export = self.export_task();
ExportUserConfig { ExportUserConfig {
@ -675,7 +683,7 @@ impl Config {
) )
} }
#[cfg(target_arch = "wasm32")] #[cfg(not(feature = "system"))]
fn create_physical_access_model( fn create_physical_access_model(
&self, &self,
client: &TypedLspClient<ServerState>, client: &TypedLspClient<ServerState>,
@ -683,7 +691,7 @@ impl Config {
self.create_delegate_access_model(client) self.create_delegate_access_model(client)
} }
#[cfg(not(target_arch = "wasm32"))] #[cfg(feature = "system")]
fn create_physical_access_model( fn create_physical_access_model(
&self, &self,
_client: &TypedLspClient<ServerState>, _client: &TypedLspClient<ServerState>,
@ -874,6 +882,7 @@ pub struct PreviewFeat {
#[serde(default, deserialize_with = "deserialize_null_default")] #[serde(default, deserialize_with = "deserialize_null_default")]
pub partial_rendering: bool, pub partial_rendering: bool,
/// Invert colors for the preview. /// Invert colors for the preview.
#[cfg(feature = "preview")]
#[serde(default, deserialize_with = "deserialize_null_default")] #[serde(default, deserialize_with = "deserialize_null_default")]
pub invert_colors: PreviewInvertColors, pub invert_colors: PreviewInvertColors,
} }
@ -971,6 +980,7 @@ where
mod tests { mod tests {
use super::*; use super::*;
use serde_json::json; use serde_json::json;
#[cfg(feature = "preview")]
use tinymist_preview::{PreviewInvertColor, PreviewInvertColorObject}; use tinymist_preview::{PreviewInvertColor, PreviewInvertColorObject};
fn update_config(config: &mut Config, update: &JsonValue) -> Result<()> { fn update_config(config: &mut Config, update: &JsonValue) -> Result<()> {
@ -1174,7 +1184,9 @@ mod tests {
test_good_config("preview.background.args"); test_good_config("preview.background.args");
test_good_config("preview.refresh"); test_good_config("preview.refresh");
test_good_config("preview.partialRendering"); test_good_config("preview.partialRendering");
#[cfg(feature = "preview")]
let c = test_good_config("preview.invertColors"); let c = test_good_config("preview.invertColors");
#[cfg(feature = "preview")]
assert_eq!( assert_eq!(
c.preview.invert_colors, c.preview.invert_colors,
PreviewInvertColors::Enum(PreviewInvertColor::Never) PreviewInvertColors::Enum(PreviewInvertColor::Never)
@ -1377,6 +1389,7 @@ mod tests {
} }
#[test] #[test]
#[cfg(feature = "preview")]
fn test_default_preview_config() { fn test_default_preview_config() {
let config = Config::default().preview(); let config = Config::default().preview();
assert!(!config.enable_partial_rendering); assert!(!config.enable_partial_rendering);
@ -1385,6 +1398,7 @@ mod tests {
} }
#[test] #[test]
#[cfg(feature = "preview")]
fn test_preview_config() { fn test_preview_config() {
let config = Config { let config = Config {
preview: PreviewFeat { preview: PreviewFeat {
@ -1434,6 +1448,7 @@ mod tests {
} }
#[test] #[test]
#[cfg(feature = "preview")]
fn test_invert_colors_validation() { fn test_invert_colors_validation() {
fn test(s: &str) -> anyhow::Result<PreviewInvertColors> { fn test(s: &str) -> anyhow::Result<PreviewInvertColors> {
Ok(serde_json::from_str(s)?) Ok(serde_json::from_str(s)?)

View file

@ -5,8 +5,7 @@ use tinymist_std::error::prelude::*;
use tinymist_std::ImmutPath; use tinymist_std::ImmutPath;
use typst::{diag::FileResult, syntax::Source}; use typst::{diag::FileResult, syntax::Source};
use crate::project::{Interrupt, ProjectResolutionKind}; use crate::project::Interrupt;
use crate::route::ProjectResolution;
use crate::world::vfs::{notify::MemoryEvent, FileChangeSet}; use crate::world::vfs::{notify::MemoryEvent, FileChangeSet};
use crate::world::TaskInputs; use crate::world::TaskInputs;
use crate::*; use crate::*;
@ -14,6 +13,11 @@ use crate::*;
mod client; mod client;
pub use client::ClientAccessModel; pub use client::ClientAccessModel;
#[cfg(feature = "lock")]
use crate::project::ProjectResolutionKind;
#[cfg(feature = "lock")]
use crate::route::ProjectResolution;
/// In memory source file management. /// In memory source file management.
impl ServerState { impl ServerState {
/// Updates a set of source files. /// Updates a set of source files.
@ -232,6 +236,12 @@ impl ServerState {
.unwrap_or_else(|| self.resolve_task_without_lock(path)) .unwrap_or_else(|| self.resolve_task_without_lock(path))
} }
#[cfg(not(feature = "lock"))]
pub(crate) fn resolve_task(&mut self, path: ImmutPath) -> TaskInputs {
self.resolve_task_without_lock(Some(path))
}
#[cfg(feature = "lock")]
pub(crate) fn resolve_task(&mut self, path: ImmutPath) -> TaskInputs { pub(crate) fn resolve_task(&mut self, path: ImmutPath) -> TaskInputs {
let proj_input = matches!( let proj_input = matches!(
self.entry_resolver().project_resolution, self.entry_resolver().project_resolution,

View file

@ -1,52 +1,75 @@
//! # tinymist //! Tinymist Core Library
//!
//! This crate provides a CLI that starts services for [Typst](https://typst.app/). It provides:
//! + `tinymist lsp`: A language server following the [Language Server Protocol](https://microsoft.github.io/language-server-protocol/).
//! + `tinymist preview`: A preview server for Typst.
//!
//! ## Usage
//!
//! See [Features: Command Line Interface](https://myriad-dreamin.github.io/tinymist/feature/cli.html).
//!
//! ## Documentation
//!
//! See [Crate Docs](https://myriad-dreamin.github.io/tinymist/rs/tinymist/index.html).
//!
//! Also see [Developer Guide: Tinymist LSP](https://myriad-dreamin.github.io/tinymist/module/lsp.html).
//!
//! ## Contributing
//!
//! See [CONTRIBUTING.md](https://github.com/Myriad-Dreamin/tinymist/blob/main/CONTRIBUTING.md).
mod actor;
mod cmd;
pub(crate) mod config;
pub(crate) mod dap;
pub(crate) mod input;
pub(crate) mod lsp;
pub mod project;
mod resource;
pub(crate) mod route;
mod server;
mod stats;
mod task;
pub mod tool;
mod utils;
pub use config::*; pub use config::*;
pub use dap::RegularInit as DapRegularInit;
pub use dap::SuperInit as DapSuperInit;
pub use lsp::init::*; pub use lsp::init::*;
pub use server::*; pub use server::*;
pub use sync_ls::LspClient; pub use sync_ls::LspClient;
pub use task::export2 as export;
pub use task::UserActionTask;
pub use tinymist_project::world; pub use tinymist_project::world;
pub use tinymist_query as query; pub use tinymist_query as query;
pub use world::{CompileFontArgs, CompileOnceArgs, CompilePackageArgs}; pub use world::{CompileFontArgs, CompileOnceArgs, CompilePackageArgs};
#[cfg(feature = "export")]
pub use task::export2 as export;
#[cfg(feature = "export")]
pub use task::ExportTask;
#[cfg(feature = "trace")]
pub use task::UserActionTask;
#[cfg(feature = "dap")]
pub use dap::RegularInit as DapRegularInit;
#[cfg(feature = "dap")]
pub use dap::SuperInit as DapSuperInit;
pub mod project;
pub mod tool;
pub(crate) mod config;
#[cfg(feature = "dap")]
pub(crate) mod dap;
pub(crate) mod input;
pub(crate) mod lsp;
#[cfg(feature = "lock")]
pub(crate) mod route;
mod actor;
mod cmd;
mod resource;
mod server;
mod stats;
mod task;
mod utils;
use std::sync::LazyLock;
use lsp::query::QueryFuture; use lsp::query::QueryFuture;
use serde_json::from_value; use serde_json::from_value;
use sync_ls::*; use sync_ls::*;
use utils::*; use utils::*;
use world::*; use world::*;
/// The long version description of the library
pub static LONG_VERSION: LazyLock<String> = LazyLock::new(|| {
format!(
"
Build Timestamp: {}
Build Git Describe: {}
Commit SHA: {}
Commit Date: {}
Commit Branch: {}
Cargo Target Triple: {}
Typst Version: {}
Typst Source: {}
",
env!("VERGEN_BUILD_TIMESTAMP"),
env!("VERGEN_GIT_DESCRIBE"),
option_env!("VERGEN_GIT_SHA").unwrap_or("None"),
option_env!("VERGEN_GIT_COMMIT_TIMESTAMP").unwrap_or("None"),
option_env!("VERGEN_GIT_BRANCH").unwrap_or("None"),
env!("VERGEN_CARGO_TARGET_TRIPLE"),
env!("TYPST_VERSION"),
env!("TYPST_SOURCE"),
)
});
#[cfg(feature = "web")]
pub mod web;

View file

@ -86,7 +86,7 @@ impl ServerState {
impl ServerState { impl ServerState {
pub(crate) fn did_open(&mut self, params: DidOpenTextDocumentParams) -> LspResult<()> { pub(crate) fn did_open(&mut self, params: DidOpenTextDocumentParams) -> LspResult<()> {
log::info!("did open {}", params.text_document.uri); log::info!("did open {}", params.text_document.uri);
let path: ImmutPath = as_path_(params.text_document.uri).as_path().into(); let path: ImmutPath = as_path_(&params.text_document.uri).as_path().into();
let text = params.text_document.text; let text = params.text_document.text;
self.create_source(path.clone(), text) self.create_source(path.clone(), text)
@ -98,14 +98,14 @@ impl ServerState {
} }
pub(crate) fn did_close(&mut self, params: DidCloseTextDocumentParams) -> LspResult<()> { pub(crate) fn did_close(&mut self, params: DidCloseTextDocumentParams) -> LspResult<()> {
let path = as_path_(params.text_document.uri).as_path().into(); let path = as_path(params.text_document).as_path().into();
self.remove_source(path).map_err(invalid_params)?; self.remove_source(path).map_err(invalid_params)?;
Ok(()) Ok(())
} }
pub(crate) fn did_change(&mut self, params: DidChangeTextDocumentParams) -> LspResult<()> { pub(crate) fn did_change(&mut self, params: DidChangeTextDocumentParams) -> LspResult<()> {
let path = as_path_(params.text_document.uri).as_path().into(); let path = as_path_(&params.text_document.uri).as_path().into();
let changes = params.content_changes; let changes = params.content_changes;
self.edit_source(path, changes, self.const_config().position_encoding) self.edit_source(path, changes, self.const_config().position_encoding)
@ -114,7 +114,7 @@ impl ServerState {
} }
pub(crate) fn did_save(&mut self, params: DidSaveTextDocumentParams) -> LspResult<()> { pub(crate) fn did_save(&mut self, params: DidSaveTextDocumentParams) -> LspResult<()> {
let path = as_path_(params.text_document.uri).as_path().into(); let path = as_path(params.text_document).as_path().into();
self.save_source(path).map_err(invalid_params)?; self.save_source(path).map_err(invalid_params)?;
Ok(()) Ok(())
@ -139,10 +139,13 @@ impl ServerState {
} }
} }
#[cfg(feature = "export")]
{
let new_export_config = self.config.export(); let new_export_config = self.config.export();
if old_config.export() != new_export_config { if old_config.export() != new_export_config {
self.change_export_config(new_export_config); self.change_export_config(new_export_config);
} }
}
if old_config.notify_status != self.config.notify_status { if old_config.notify_status != self.config.notify_status {
self.editor_tx self.editor_tx

View file

@ -292,8 +292,8 @@ impl ServerState {
.iter() .iter()
.map(|f| { .map(|f| {
Some(( Some((
as_path_(Url::parse(&f.old_uri).ok()?), as_path_(&Url::parse(&f.old_uri).ok()?),
as_path_(Url::parse(&f.new_uri).ok()?), as_path_(&Url::parse(&f.new_uri).ok()?),
)) ))
}) })
.collect::<Option<Vec<_>>>() .collect::<Option<Vec<_>>>()
@ -326,7 +326,12 @@ impl ServerState {
DocumentSymbol(req) => query_source!(self, DocumentSymbol, req)?, DocumentSymbol(req) => query_source!(self, DocumentSymbol, req)?,
OnEnter(req) => query_source!(self, OnEnter, req)?, OnEnter(req) => query_source!(self, OnEnter, req)?,
ColorPresentation(req) => CompilerQueryResponse::ColorPresentation(req.request()), ColorPresentation(req) => CompilerQueryResponse::ColorPresentation(req.request()),
#[cfg(feature = "export")]
OnExport(req) => return self.on_export(req), OnExport(req) => return self.on_export(req),
#[cfg(not(feature = "export"))]
OnExport(_req) => {
return Err(tinymist_std::error_once!("export feature is not enabled"))
}
ServerInfo(_) => return self.collect_server_info(), ServerInfo(_) => return self.collect_server_info(),
// todo: query on dedicate projects // todo: query on dedicate projects
_ => return self.query_on(query), _ => return self.query_on(query),
@ -360,6 +365,8 @@ impl ServerState {
just_future(async move { just_future(async move {
stat.snap(); stat.snap();
// todo: preload in web
#[cfg(feature = "system")]
if matches!(query, Completion(..)) { if matches!(query, Completion(..)) {
// Prefetch the package index for completion. // Prefetch the package index for completion.
if snap.registry().cached_index().is_none() { if snap.registry().cached_index().is_none() {

View file

@ -19,7 +19,7 @@
#![allow(missing_docs)] #![allow(missing_docs)]
use reflexo_typst::{diag::print_diagnostics, TypstDocument}; use reflexo_typst::TypstDocument;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
pub use tinymist_project::*; pub use tinymist_project::*;
@ -42,8 +42,11 @@ use typst::{diag::FileResult, foundations::Bytes, layout::Position as TypstPosit
use super::ServerState; use super::ServerState;
use crate::actor::editor::{EditorRequest, ProjVersion}; use crate::actor::editor::{EditorRequest, ProjVersion};
use crate::stats::{CompilerQueryStats, QueryStatGuard}; use crate::stats::{CompilerQueryStats, QueryStatGuard};
#[cfg(feature = "export")]
use crate::task::ExportUserConfig; use crate::task::ExportUserConfig;
use crate::{Config, ServerEvent}; use crate::Config;
#[cfg(feature = "preview")]
use crate::ServerEvent;
type EditorSender = mpsc::UnboundedSender<EditorRequest>; type EditorSender = mpsc::UnboundedSender<EditorRequest>;
@ -53,6 +56,7 @@ pub type LspProjectCompiler = ProjectCompiler<LspCompilerFeat, ProjectInsStateEx
/// Project access and mutations. /// Project access and mutations.
impl ServerState { impl ServerState {
/// Changes the export configuration. /// Changes the export configuration.
#[cfg(feature = "export")]
pub fn change_export_config(&mut self, config: ExportUserConfig) { pub fn change_export_config(&mut self, config: ExportUserConfig) {
self.project.export.change_config(config); self.project.export.change_config(config);
} }
@ -138,6 +142,7 @@ impl ServerState {
let const_config = &config.const_config; let const_config = &config.const_config;
// Run Export actors before preparing cluster to avoid loss of events // Run Export actors before preparing cluster to avoid loss of events
#[cfg(feature = "export")]
let export = crate::task::ExportTask::new( let export = crate::task::ExportTask::new(
client.handle.clone(), client.handle.clone(),
Some(editor_tx.clone()), Some(editor_tx.clone()),
@ -150,6 +155,7 @@ impl ServerState {
#[cfg(feature = "preview")] #[cfg(feature = "preview")]
preview, preview,
is_standalone: false, is_standalone: false,
#[cfg(feature = "export")]
export: export.clone(), export: export.clone(),
editor_tx: editor_tx.clone(), editor_tx: editor_tx.clone(),
client: Arc::new(client.clone().to_untyped()), client: Arc::new(client.clone().to_untyped()),
@ -207,11 +213,20 @@ impl ServerState {
// todo: unify filesystem watcher // todo: unify filesystem watcher
let (dep_tx, dep_rx) = mpsc::unbounded_channel(); let (dep_tx, dep_rx) = mpsc::unbounded_channel();
// todo: notify feature?
#[cfg(feature = "system")]
{
let fs_client = client.clone().to_untyped(); let fs_client = client.clone().to_untyped();
let async_handle = client.handle.clone(); let async_handle = client.handle.clone();
async_handle.spawn(watch_deps(dep_rx, move |event| { async_handle.spawn(watch_deps(dep_rx, move |event| {
fs_client.send_event(LspInterrupt::Fs(event)); fs_client.send_event(LspInterrupt::Fs(event));
})); }));
}
#[cfg(not(feature = "system"))]
{
let _ = dep_rx;
log::warn!("Project: system watcher is not enabled, file changes will not be watched");
}
// Create the actor // Create the actor
let compile_handle = handle.clone(); let compile_handle = handle.clone();
@ -227,9 +242,11 @@ impl ServerState {
ProjectState { ProjectState {
compiler, compiler,
#[cfg(feature = "preview")]
preview: handle.preview.clone(), preview: handle.preview.clone(),
analysis: handle.analysis.clone(), analysis: handle.analysis.clone(),
stats: CompilerQueryStats::default(), stats: CompilerQueryStats::default(),
#[cfg(feature = "export")]
export: handle.export.clone(), export: handle.export.clone(),
} }
} }
@ -298,9 +315,11 @@ impl ProjectInsStateExt {
pub struct ProjectState { pub struct ProjectState {
pub compiler: LspProjectCompiler, pub compiler: LspProjectCompiler,
pub preview: ProjectPreviewState,
pub analysis: Arc<Analysis>, pub analysis: Arc<Analysis>,
pub stats: CompilerQueryStats, pub stats: CompilerQueryStats,
#[cfg(feature = "preview")]
pub preview: ProjectPreviewState,
#[cfg(feature = "export")]
pub export: crate::task::ExportTask, pub export: crate::task::ExportTask,
} }
@ -414,6 +433,7 @@ pub struct CompileHandlerImpl {
/// language server). /// language server).
pub is_standalone: bool, pub is_standalone: bool,
#[cfg(feature = "export")]
pub(crate) export: crate::task::ExportTask, pub(crate) export: crate::task::ExportTask,
pub(crate) editor_tx: EditorSender, pub(crate) editor_tx: EditorSender,
pub(crate) client: Arc<dyn ProjectClient>, pub(crate) client: Arc<dyn ProjectClient>,
@ -422,9 +442,11 @@ pub struct CompileHandlerImpl {
pub(crate) notified_revision: Mutex<FxHashMap<ProjectInsId, (usize, CompileSignal)>>, pub(crate) notified_revision: Mutex<FxHashMap<ProjectInsId, (usize, CompileSignal)>>,
} }
pub(crate) trait ProjectClient: Send + Sync + 'static { pub trait ProjectClient: Send + Sync + 'static {
fn interrupt(&self, event: LspInterrupt); fn interrupt(&self, event: LspInterrupt);
#[cfg(feature = "preview")]
fn server_event(&self, event: ServerEvent); fn server_event(&self, event: ServerEvent);
#[cfg(feature = "export")]
fn dev_event(&self, event: DevEvent); fn dev_event(&self, event: DevEvent);
} }
@ -433,10 +455,12 @@ impl ProjectClient for LspClient {
self.send_event(event); self.send_event(event);
} }
#[cfg(feature = "preview")]
fn server_event(&self, event: ServerEvent) { fn server_event(&self, event: ServerEvent) {
self.send_event(event); self.send_event(event);
} }
#[cfg(feature = "export")]
fn dev_event(&self, event: DevEvent) { fn dev_event(&self, event: DevEvent) {
self.send_notification::<DevEvent>(&event); self.send_notification::<DevEvent>(&event);
} }
@ -447,10 +471,12 @@ impl ProjectClient for mpsc::UnboundedSender<LspInterrupt> {
self.send(event).log_error("failed to send interrupt"); self.send(event).log_error("failed to send interrupt");
} }
#[cfg(feature = "preview")]
fn server_event(&self, _event: ServerEvent) { fn server_event(&self, _event: ServerEvent) {
log::warn!("ProjectClient: server_event is not implemented for mpsc::UnboundedSender<LspInterrupt>"); log::warn!("ProjectClient: server_event is not implemented for mpsc::UnboundedSender<LspInterrupt>");
} }
#[cfg(feature = "export")]
fn dev_event(&self, _event: DevEvent) { fn dev_event(&self, _event: DevEvent) {
log::warn!( log::warn!(
"ProjectClient: dev_event is not implemented for mpsc::UnboundedSender<LspInterrupt>" "ProjectClient: dev_event is not implemented for mpsc::UnboundedSender<LspInterrupt>"
@ -696,8 +722,9 @@ impl CompileHandler<LspCompilerFeat, ProjectInsStateExt> for CompileHandlerImpl
// Prints the diagnostics when we are running the compilation in standalone // Prints the diagnostics when we are running the compilation in standalone
// CLI. // CLI.
#[cfg(feature = "system")]
if self.is_standalone { if self.is_standalone {
print_diagnostics( crate::project::system::print_diagnostics(
art.world(), art.world(),
art.diagnostics(), art.diagnostics(),
reflexo_typst::DiagnosticFormat::Human, reflexo_typst::DiagnosticFormat::Human,
@ -705,6 +732,7 @@ impl CompileHandler<LspCompilerFeat, ProjectInsStateExt> for CompileHandlerImpl
.log_error("failed to print diagnostics"); .log_error("failed to print diagnostics");
} }
#[cfg(feature = "export")]
self.export.signal(art, &self.client); self.export.signal(art, &self.client);
#[cfg(feature = "preview")] #[cfg(feature = "preview")]
@ -723,7 +751,7 @@ pub type QuerySnapWithStat = (LspQuerySnapshot, QueryStatGuard);
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub(crate) struct DevExportEvent { pub struct DevExportEvent {
pub id: String, pub id: String,
pub when: TaskWhen, pub when: TaskWhen,
pub need_export: bool, pub need_export: bool,
@ -733,7 +761,7 @@ pub(crate) struct DevExportEvent {
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "type")] #[serde(rename_all = "camelCase", tag = "type")]
pub(crate) enum DevEvent { pub enum DevEvent {
Export(DevExportEvent), Export(DevExportEvent),
} }

View file

@ -7,31 +7,30 @@ use lsp_types::request::ShowMessageRequest;
use lsp_types::*; use lsp_types::*;
use reflexo::debug_loc::LspPosition; use reflexo::debug_loc::LspPosition;
use sync_ls::*; use sync_ls::*;
use tinymist_query::{OnExportRequest, ServerInfoResponse}; use tinymist_query::ServerInfoResponse;
use tinymist_std::error::prelude::*; use tinymist_std::error::prelude::*;
use tinymist_std::ImmutPath; use tinymist_std::ImmutPath;
use tinymist_task::ProjectTask;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use typst::syntax::Source; use typst::syntax::Source;
use crate::actor::editor::{EditorActor, EditorRequest}; use crate::actor::editor::{EditorActor, EditorRequest};
use crate::lsp::query::OnEnter; use crate::lsp::query::OnEnter;
use crate::project::{ use crate::project::{EntryResolver, LspInterrupt, ProjectInsId, ProjectState};
update_lock, CompiledArtifact, EntryResolver, LspComputeGraph, LspInterrupt, ProjectInsId, use crate::task::FormatTask;
ProjectState, PROJECT_ROUTE_USER_ACTION_PRIORITY,
};
use crate::route::ProjectRouteState;
use crate::task::{ExportTask, FormatTask, ServerTraceTask, UserActionTask};
use crate::world::TaskInputs;
use crate::{lsp::init::*, *}; use crate::{lsp::init::*, *};
#[cfg(feature = "lock")]
use crate::route::ProjectRouteState;
#[cfg(feature = "trace")]
use crate::task::{ServerTraceTask, UserActionTask};
pub(crate) use futures::Future; pub(crate) use futures::Future;
pub(crate) fn as_path(inp: TextDocumentIdentifier) -> PathBuf { pub(crate) fn as_path(inp: TextDocumentIdentifier) -> PathBuf {
as_path_(inp.uri) as_path_(&inp.uri)
} }
pub(crate) fn as_path_(uri: Url) -> PathBuf { pub(crate) fn as_path_(uri: &Url) -> PathBuf {
tinymist_query::url_to_path(uri) tinymist_query::url_to_path(uri)
} }
@ -45,10 +44,11 @@ pub struct ServerState {
pub client: TypedLspClient<Self>, pub client: TypedLspClient<Self>,
// State // State
/// The project route state.
pub route: ProjectRouteState,
/// The project state. /// The project state.
pub project: ProjectState, pub project: ProjectState,
/// The project route state.
#[cfg(feature = "lock")]
pub route: ProjectRouteState,
/// The preview state. /// The preview state.
#[cfg(feature = "preview")] #[cfg(feature = "preview")]
pub preview: tool::preview::PreviewState, pub preview: tool::preview::PreviewState,
@ -59,6 +59,7 @@ pub struct ServerState {
pub formatter: FormatTask, pub formatter: FormatTask,
/// The user action tasks running in backend, which will be scheduled by /// The user action tasks running in backend, which will be scheduled by
/// async runtime. /// async runtime.
#[cfg(feature = "trace")]
pub user_action: UserActionTask, pub user_action: UserActionTask,
// State to synchronize with the client. // State to synchronize with the client.
@ -83,6 +84,7 @@ pub struct ServerState {
/// The client ever sent manual focusing request. /// The client ever sent manual focusing request.
pub ever_manual_focusing: bool, pub ever_manual_focusing: bool,
/// The running server trace. /// The running server trace.
#[cfg(feature = "trace")]
pub server_trace: Option<ServerTraceTask>, pub server_trace: Option<ServerTraceTask>,
// Configurations // Configurations
@ -115,33 +117,36 @@ impl ServerState {
); );
Self { Self {
client: client.clone(), #[cfg(feature = "dap")]
debug: crate::dap::DebugState::default(),
#[cfg(feature = "lock")]
route: ProjectRouteState::default(), route: ProjectRouteState::default(),
project: handle,
editor_tx,
memory_changes: HashMap::new(),
#[cfg(feature = "preview")] #[cfg(feature = "preview")]
preview: tool::preview::PreviewState::new( preview: tool::preview::PreviewState::new(
&config, &config,
watchers, watchers,
client.cast(|s| &mut s.preview), client.cast(|s| &mut s.preview),
), ),
#[cfg(feature = "dap")] #[cfg(feature = "trace")]
debug: crate::dap::DebugState::default(), server_trace: None,
#[cfg(feature = "trace")]
user_action: UserActionTask,
client: client.clone(),
project: handle,
editor_tx,
memory_changes: HashMap::new(),
ever_focusing_by_activities: false, ever_focusing_by_activities: false,
ever_manual_focusing: false, ever_manual_focusing: false,
sema_tokens_registered: false, sema_tokens_registered: false,
formatter_registered: false, formatter_registered: false,
server_trace: None,
config, config,
pinning_by_user: false, pinning_by_user: false,
pinning_by_preview: false, pinning_by_preview: false,
pinning_by_browsing_preview: false, pinning_by_browsing_preview: false,
focusing: None, focusing: None,
implicit_position: None, implicit_position: None,
formatter, formatter,
user_action: UserActionTask,
} }
} }
@ -216,6 +221,20 @@ impl ServerState {
.with_command("tinymist.doStartBrowsingPreview", State::browse_preview) .with_command("tinymist.doStartBrowsingPreview", State::browse_preview)
.with_command("tinymist.doKillPreview", State::kill_preview); .with_command("tinymist.doKillPreview", State::kill_preview);
#[cfg(feature = "trace")]
let provider = provider
.with_command("tinymist.getDocumentTrace", State::get_document_trace)
.with_command("tinymist.startServerProfiling", State::start_server_trace)
.with_command("tinymist.stopServerProfiling", State::stop_server_trace);
#[cfg(feature = "system")]
let provider = provider
.with_command("tinymist.doInitTemplate", State::init_template)
.with_command("tinymist.doGetTemplateEntry", State::get_template_entry)
.with_resource("/package/by-namespace", State::resource_package_by_ns)
.with_resource("/dir/package", State::resource_package_dirs)
.with_resource("/dir/package/local", State::resource_local_package_dir);
// todo: .on_sync_mut::<notifs::Cancel>(handlers::handle_cancel)? // todo: .on_sync_mut::<notifs::Cancel>(handlers::handle_cancel)?
let mut provider = provider let mut provider = provider
.with_request::<Shutdown>(State::shutdown) .with_request::<Shutdown>(State::shutdown)
@ -277,12 +296,7 @@ impl ServerState {
.with_command("tinymist.doClearCache", State::clear_cache) .with_command("tinymist.doClearCache", State::clear_cache)
.with_command("tinymist.pinMain", State::pin_document) .with_command("tinymist.pinMain", State::pin_document)
.with_command("tinymist.focusMain", State::focus_document) .with_command("tinymist.focusMain", State::focus_document)
.with_command("tinymist.doInitTemplate", State::init_template)
.with_command("tinymist.doGetTemplateEntry", State::get_template_entry)
.with_command_("tinymist.interactCodeContext", State::interact_code_context) .with_command_("tinymist.interactCodeContext", State::interact_code_context)
.with_command("tinymist.getDocumentTrace", State::get_document_trace)
.with_command("tinymist.startServerProfiling", State::start_server_trace)
.with_command("tinymist.stopServerProfiling", State::stop_server_trace)
.with_command_("tinymist.getDocumentMetrics", State::get_document_metrics) .with_command_("tinymist.getDocumentMetrics", State::get_document_metrics)
.with_command_("tinymist.getWorkspaceLabels", State::get_workspace_labels) .with_command_("tinymist.getWorkspaceLabels", State::get_workspace_labels)
.with_command_("tinymist.getServerInfo", State::get_server_info) .with_command_("tinymist.getServerInfo", State::get_server_info)
@ -291,11 +305,8 @@ impl ServerState {
.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("/package/by-namespace", State::resource_package_by_ns)
.with_resource("/package/symbol", State::resource_package_symbols) .with_resource("/package/symbol", State::resource_package_symbols)
.with_resource("/package/docs", State::resource_package_docs) .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(
@ -310,6 +321,7 @@ impl ServerState {
} }
/// Installs DAP handlers to the language server. /// Installs DAP handlers to the language server.
#[cfg(feature = "dap")]
pub fn install_dap<T: Initializer<S = Self> + 'static>( pub fn install_dap<T: Initializer<S = Self> + 'static>(
provider: DapBuilder<T>, provider: DapBuilder<T>,
) -> DapBuilder<T> { ) -> DapBuilder<T> {
@ -445,62 +457,6 @@ impl ServerState {
Ok(tinymist_query::CompilerQueryResponse::ServerInfo(info)) Ok(tinymist_query::CompilerQueryResponse::ServerInfo(info))
}) })
} }
/// Exports the current document.
pub fn on_export(&mut self, req: OnExportRequest) -> QueryFuture {
let OnExportRequest { path, task, open } = req;
let entry = self.entry_resolver().resolve(Some(path.as_path().into()));
let lock_dir = self.entry_resolver().resolve_lock(&entry);
let update_dep = lock_dir.clone().map(|lock_dir| {
|snap: LspComputeGraph| async move {
let mut updater = update_lock(lock_dir.clone());
let world = snap.world();
// todo: rootless.
let root_dir = world.entry_state().root()?;
let doc_id = updater.compiled(world, (&root_dir, &lock_dir))?;
updater.update_materials(doc_id.clone(), world.depended_fs_paths());
updater.route(doc_id, PROJECT_ROUTE_USER_ACTION_PRIORITY);
updater.commit();
Some(())
}
});
let snap = self.snapshot()?;
just_future(async move {
let snap = snap.task(TaskInputs {
entry: Some(entry),
..TaskInputs::default()
});
let is_html = matches!(task, ProjectTask::ExportHtml { .. });
let artifact = CompiledArtifact::from_graph(snap.clone(), is_html);
let res = ExportTask::do_export(task, artifact, lock_dir).await?;
if let Some(update_dep) = update_dep {
tokio::spawn(update_dep(snap));
}
// See https://github.com/Myriad-Dreamin/tinymist/issues/837
// Also see https://github.com/Byron/open-rs/issues/105
#[cfg(not(target_os = "windows"))]
let do_open = ::open::that_detached;
#[cfg(target_os = "windows")]
fn do_open(path: impl AsRef<std::ffi::OsStr>) -> std::io::Result<()> {
::open::with_detached(path, "explorer")
}
if let Some(Some(path)) = open.then_some(res.as_ref()) {
log::trace!("open with system default apps: {path:?}");
do_open(path).log_error("failed to open with system default apps");
}
log::trace!("CompileActor: on export end: {path:?} as {res:?}");
Ok(tinymist_query::CompilerQueryResponse::OnExport(res))
})
}
} }
#[test] #[test]
@ -509,11 +465,11 @@ fn test_as_path() {
use std::path::Path; use std::path::Path;
let uri = Url::parse("untitled:/path/to/file").unwrap(); let uri = Url::parse("untitled:/path/to/file").unwrap();
assert_eq!(as_path_(uri), Path::new("/untitled/path/to/file").clean()); assert_eq!(as_path_(&uri), Path::new("/untitled/path/to/file").clean());
let uri = Url::parse("untitled:/path/to/file%20with%20space").unwrap(); let uri = Url::parse("untitled:/path/to/file%20with%20space").unwrap();
assert_eq!( assert_eq!(
as_path_(uri), as_path_(&uri),
Path::new("/untitled/path/to/file with space").clean() Path::new("/untitled/path/to/file with space").clean()
); );
} }

View file

@ -4,10 +4,13 @@ use std::path::PathBuf;
use std::str::FromStr; use std::str::FromStr;
use std::sync::atomic::AtomicUsize; use std::sync::atomic::AtomicUsize;
use std::sync::{Arc, OnceLock}; use std::sync::{Arc, OnceLock};
use std::{ops::DerefMut, pin::Pin};
use reflexo::ImmutPath; use reflexo::ImmutPath;
use reflexo_typst::{Bytes, CompilationTask, ExportComputation}; use reflexo_typst::{Bytes, CompilationTask, ExportComputation};
use tinymist_project::{LspWorld, PROJECT_ROUTE_USER_ACTION_PRIORITY}; use sync_ls::just_future;
use tinymist_project::LspWorld;
use tinymist_query::OnExportRequest;
use tinymist_std::error::prelude::*; use tinymist_std::error::prelude::*;
use tinymist_std::fs::paths::write_atomic; use tinymist_std::fs::paths::write_atomic;
use tinymist_std::path::PathClean; use tinymist_std::path::PathClean;
@ -18,24 +21,102 @@ use typlite::{Format, Typlite};
use typst::foundations::IntoValue; use typst::foundations::IntoValue;
use typst::visualize::Color; use typst::visualize::Color;
use super::{FutureFolder, SyncTaskFactory}; use futures::Future;
use parking_lot::Mutex;
use rayon::Scope;
use super::SyncTaskFactory;
use crate::lsp::query::QueryFuture;
use crate::project::{ use crate::project::{
ApplyProjectTask, CompiledArtifact, DevEvent, DevExportEvent, EntryReader, ExportHtmlTask, update_lock, ApplyProjectTask, CompiledArtifact, DevEvent, DevExportEvent, EntryReader,
ExportPdfTask, ExportPngTask, ExportSvgTask, ExportTask as ProjectExportTask, ExportTeXTask, ExportHtmlTask, ExportPdfTask, ExportPngTask, ExportSvgTask, ExportTask as ProjectExportTask,
ExportTextTask, LspCompiledArtifact, ProjectClient, ProjectTask, QueryTask, TaskWhen, ExportTeXTask, ExportTextTask, LspCompiledArtifact, LspComputeGraph, ProjectClient,
ProjectTask, QueryTask, TaskWhen, PROJECT_ROUTE_USER_ACTION_PRIORITY,
}; };
use crate::world::TaskInputs;
use crate::ServerState;
use crate::{actor::editor::EditorRequest, tool::word_count}; use crate::{actor::editor::EditorRequest, tool::word_count};
impl ServerState {
/// Exports the current document.
pub fn on_export(&mut self, req: OnExportRequest) -> QueryFuture {
let OnExportRequest { path, task, open } = req;
let entry = self.entry_resolver().resolve(Some(path.as_path().into()));
let lock_dir = self.entry_resolver().resolve_lock(&entry);
let update_dep = lock_dir.clone().map(|lock_dir| {
|snap: LspComputeGraph| async move {
let mut updater = update_lock(lock_dir.clone());
let world = snap.world();
// todo: rootless.
let root_dir = world.entry_state().root()?;
let doc_id = updater.compiled(world, (&root_dir, &lock_dir))?;
updater.update_materials(doc_id.clone(), world.depended_fs_paths());
updater.route(doc_id, PROJECT_ROUTE_USER_ACTION_PRIORITY);
updater.commit();
Some(())
}
});
let snap = self.snapshot()?;
just_future(async move {
let snap = snap.task(TaskInputs {
entry: Some(entry),
..TaskInputs::default()
});
let is_html = matches!(task, ProjectTask::ExportHtml { .. });
let artifact = CompiledArtifact::from_graph(snap.clone(), is_html);
let res = ExportTask::do_export(task, artifact, lock_dir).await?;
if let Some(update_dep) = update_dep {
tokio::spawn(update_dep(snap));
}
#[cfg(not(feature = "open"))]
if open {
log::warn!("open is not supported in this build, ignoring");
}
#[cfg(feature = "open")]
{
// See https://github.com/Myriad-Dreamin/tinymist/issues/837
// Also see https://github.com/Byron/open-rs/issues/105
#[cfg(not(target_os = "windows"))]
let do_open = ::open::that_detached;
#[cfg(target_os = "windows")]
fn do_open(path: impl AsRef<std::ffi::OsStr>) -> std::io::Result<()> {
::open::with_detached(path, "explorer")
}
if let Some(Some(path)) = open.then_some(res.as_ref()) {
log::trace!("open with system default apps: {path:?}");
do_open(path).log_error("failed to open with system default apps");
}
}
log::trace!("CompileActor: on export end: {path:?} as {res:?}");
Ok(tinymist_query::CompilerQueryResponse::OnExport(res))
})
}
}
/// Runs a export document task.
#[derive(Clone)] #[derive(Clone)]
pub struct ExportTask { pub struct ExportTask {
/// The handle running the task.
pub handle: tokio::runtime::Handle, pub handle: tokio::runtime::Handle,
/// The editor request sender.
pub editor_tx: Option<mpsc::UnboundedSender<EditorRequest>>, pub editor_tx: Option<mpsc::UnboundedSender<EditorRequest>>,
/// The task factory for export.
pub factory: SyncTaskFactory<ExportUserConfig>, pub factory: SyncTaskFactory<ExportUserConfig>,
export_folder: FutureFolder, export_folder: FutureFolder,
count_word_folder: FutureFolder, count_word_folder: FutureFolder,
} }
impl ExportTask { impl ExportTask {
/// Creates a new export task.
pub fn new( pub fn new(
handle: tokio::runtime::Handle, handle: tokio::runtime::Handle,
editor_tx: Option<mpsc::UnboundedSender<EditorRequest>>, editor_tx: Option<mpsc::UnboundedSender<EditorRequest>>,
@ -50,6 +131,7 @@ impl ExportTask {
} }
} }
/// Changes the export configuration.
pub fn change_config(&self, config: ExportUserConfig) { pub fn change_config(&self, config: ExportUserConfig) {
self.factory.mutate(|data| *data = config); self.factory.mutate(|data| *data = config);
} }
@ -162,6 +244,7 @@ impl ExportTask {
Some(()) Some(())
} }
/// Exports a document.
pub async fn do_export( pub async fn do_export(
task: ProjectTask, task: ProjectTask,
artifact: LspCompiledArtifact, artifact: LspCompiledArtifact,
@ -489,6 +572,75 @@ fn serialize(data: &impl serde::Serialize, format: &str, pretty: bool) -> Result
}) })
} }
type FoldFuture = Pin<Box<dyn Future<Output = Option<()>> + Send>>;
#[derive(Default)]
struct FoldingState {
running: bool,
task: Option<(usize, FoldFuture)>,
}
#[derive(Clone, Default)]
struct FutureFolder {
state: Arc<Mutex<FoldingState>>,
}
impl FutureFolder {
async fn compute<'scope, OP, R: Send + 'static>(op: OP) -> Result<R>
where
OP: FnOnce(&Scope<'scope>) -> R + Send + 'static,
{
tokio::task::spawn_blocking(move || -> R { rayon::in_place_scope(op) })
.await
.context_ut("compute error")
}
#[must_use]
fn spawn(
&self,
revision: usize,
fut: impl FnOnce() -> FoldFuture,
) -> Option<impl Future<Output = ()> + Send + 'static> {
let mut state = self.state.lock();
let state = state.deref_mut();
match &mut state.task {
Some((prev_revision, prev)) => {
if *prev_revision < revision {
*prev = fut();
*prev_revision = revision;
}
return None;
}
next_update => {
*next_update = Some((revision, fut()));
}
}
if state.running {
return None;
}
state.running = true;
let state = self.state.clone();
Some(async move {
loop {
let fut = {
let mut state = state.lock();
let Some((_, fut)) = state.task.take() else {
state.running = false;
return;
};
fut
};
fut.await;
}
})
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use clap::Parser; use clap::Parser;

View file

@ -2,21 +2,22 @@
//! [`SyncTaskFactory`] can hold *mutable* configuration but the mutations don't //! [`SyncTaskFactory`] can hold *mutable* configuration but the mutations don't
//! blocking the computation, i.e. the mutations are non-blocking. //! blocking the computation, i.e. the mutations are non-blocking.
#[cfg(feature = "export")]
mod export; mod export;
#[cfg(feature = "export")]
pub use export::*; pub use export::*;
#[cfg(feature = "export")]
pub mod export2; pub mod export2;
mod format; mod format;
pub use format::*; pub use format::*;
#[cfg(feature = "trace")]
mod user_action; mod user_action;
#[cfg(feature = "trace")]
pub use user_action::*; pub use user_action::*;
use std::{ops::DerefMut, pin::Pin, sync::Arc}; use std::sync::Arc;
use futures::Future;
use parking_lot::Mutex;
use rayon::Scope;
use reflexo::TakeAs; use reflexo::TakeAs;
use tinymist_std::error::prelude::*;
/// Please uses this if you believe all mutations are fast /// Please uses this if you believe all mutations are fast
#[derive(Clone, Default)] #[derive(Clone, Default)]
@ -40,72 +41,3 @@ impl<T: Clone> SyncTaskFactory<T> {
self.0.read().unwrap().clone() self.0.read().unwrap().clone()
} }
} }
type FoldFuture = Pin<Box<dyn Future<Output = Option<()>> + Send>>;
#[derive(Default)]
struct FoldingState {
running: bool,
task: Option<(usize, FoldFuture)>,
}
#[derive(Clone, Default)]
struct FutureFolder {
state: Arc<Mutex<FoldingState>>,
}
impl FutureFolder {
async fn compute<'scope, OP, R: Send + 'static>(op: OP) -> Result<R>
where
OP: FnOnce(&Scope<'scope>) -> R + Send + 'static,
{
tokio::task::spawn_blocking(move || -> R { rayon::in_place_scope(op) })
.await
.context_ut("compute error")
}
#[must_use]
fn spawn(
&self,
revision: usize,
fut: impl FnOnce() -> FoldFuture,
) -> Option<impl Future<Output = ()> + Send + 'static> {
let mut state = self.state.lock();
let state = state.deref_mut();
match &mut state.task {
Some((prev_revision, prev)) => {
if *prev_revision < revision {
*prev = fut();
*prev_revision = revision;
}
return None;
}
next_update => {
*next_update = Some((revision, fut()));
}
}
if state.running {
return None;
}
state.running = true;
let state = self.state.clone();
Some(async move {
loop {
let fut = {
let mut state = state.lock();
let Some((_, fut)) = state.task.take() else {
state.running = false;
return;
};
fut
};
fut.await;
}
})
}
}

View file

@ -3,7 +3,6 @@
pub mod ast; pub mod ast;
pub mod package; pub mod package;
pub mod project; pub mod project;
pub mod testing;
pub mod word_count; pub mod word_count;
#[cfg(feature = "preview")] #[cfg(feature = "preview")]

View file

@ -9,7 +9,7 @@ mod http;
use std::{collections::HashMap, path::Path, sync::Arc}; use std::{collections::HashMap, path::Path, sync::Arc};
use clap::{Parser, ValueEnum}; use clap::{Parser, ValueEnum};
use futures::{SinkExt, StreamExt, TryStreamExt}; use futures::{SinkExt, TryStreamExt};
use hyper_tungstenite::{tungstenite::Message, HyperWebsocket, HyperWebsocketStream}; use hyper_tungstenite::{tungstenite::Message, HyperWebsocket, HyperWebsocketStream};
use lsp_types::notification::Notification; use lsp_types::notification::Notification;
use lsp_types::Url; use lsp_types::Url;
@ -27,8 +27,7 @@ use tinymist_std::error::IgnoreLogging;
use tokio::sync::{mpsc, oneshot}; use tokio::sync::{mpsc, oneshot};
use crate::actor::preview::{PreviewActor, PreviewRequest, PreviewTab}; use crate::actor::preview::{PreviewActor, PreviewRequest, PreviewTab};
use crate::project::{ProjectInsId, ProjectPreviewState, WorldProvider}; use crate::project::{ProjectInsId, ProjectPreviewState};
use crate::tool::project::{start_project, ProjectOpts, StartProjectResult};
use crate::*; use crate::*;
/// The kind of the preview. /// The kind of the preview.
@ -489,6 +488,7 @@ impl PreviewState {
is_primary, is_primary,
}; };
#[cfg(feature = "open")]
if open_in_browser { if open_in_browser {
open::that_detached(format!("http://127.0.0.1:{}", addr.port())) open::that_detached(format!("http://127.0.0.1:{}", addr.port()))
.log_error("failed to open browser for preview"); .log_error("failed to open browser for preview");
@ -546,154 +546,6 @@ impl PreviewState {
} }
} }
/// Entry point of the preview tool.
pub async fn preview_main(args: PreviewCliArgs) -> Result<()> {
log::info!("Arguments: {args:#?}");
let handle = tokio::runtime::Handle::current();
let config = args.preview.config(&PreviewConfig::default());
let open_in_browser = args.open_in_browser(true);
let static_file_host =
if args.static_file_host == args.data_plane_host || !args.static_file_host.is_empty() {
Some(args.static_file_host)
} else {
None
};
exit_on_ctrl_c();
let verse = args.compile.resolve()?;
let previewer = PreviewBuilder::new(config);
let (service, handle) = {
let preview_state = ProjectPreviewState::default();
let opts = ProjectOpts {
handle: Some(handle),
preview: preview_state.clone(),
..ProjectOpts::default()
};
let StartProjectResult {
service,
intr_tx,
mut editor_rx,
} = start_project(verse, Some(opts), |compiler, intr, next| {
next(compiler, intr)
});
// Consume editor_rx
tokio::spawn(async move { while editor_rx.recv().await.is_some() {} });
let id = service.compiler.primary.id.clone();
let registered = preview_state.register(&id, previewer.compile_watcher(args.task_id));
if !registered {
tinymist_std::bail!("failed to register preview");
}
let handle: Arc<ProjectPreviewHandler> = Arc::new(ProjectPreviewHandler {
project_id: id,
client: Box::new(intr_tx),
});
(service, handle)
};
let (lsp_tx, mut lsp_rx) = ControlPlaneTx::new(true);
let control_plane_server_handle = tokio::spawn(async move {
let (control_sock_tx, mut control_sock_rx) = mpsc::unbounded_channel();
let srv =
make_http_server(String::default(), args.control_plane_host, control_sock_tx).await;
log::info!("Control panel server listening on: {}", srv.addr);
let control_websocket = control_sock_rx.recv().await.unwrap();
let ws = control_websocket.await.unwrap();
tokio::pin!(ws);
loop {
tokio::select! {
Some(resp) = lsp_rx.resp_rx.recv() => {
let r = ws
.send(Message::Text(serde_json::to_string(&resp).unwrap()))
.await;
let Err(err) = r else {
continue;
};
log::warn!("failed to send response to editor {err:?}");
break;
}
msg = ws.next() => {
let msg = match msg {
Some(Ok(Message::Text(msg))) => Some(msg),
Some(Ok(msg)) => {
log::error!("unsupported message: {msg:?}");
break;
}
Some(Err(e)) => {
log::error!("failed to receive message: {e}");
break;
}
_ => None,
};
if let Some(msg) = msg {
let Ok(msg) = serde_json::from_str::<ControlPlaneMessage>(&msg) else {
log::warn!("failed to parse control plane request: {msg:?}");
break;
};
lsp_rx.ctl_tx.send(msg).unwrap();
} else {
// todo: inform the editor that the connection is closed.
break;
}
}
}
}
let _ = srv.shutdown_tx.send(());
let _ = srv.join.await;
});
let (websocket_tx, websocket_rx) = mpsc::unbounded_channel();
let mut previewer = previewer.build(lsp_tx, handle.clone()).await;
tokio::spawn(service.run());
bind_streams(&mut previewer, websocket_rx);
let frontend_html = frontend_html(TYPST_PREVIEW_HTML, args.preview.preview_mode, "/");
let static_server = if let Some(static_file_host) = static_file_host {
log::warn!("--static-file-host is deprecated, which will be removed in the future. Use --data-plane-host instead.");
let html = frontend_html.clone();
Some(make_http_server(html, static_file_host, websocket_tx.clone()).await)
} else {
None
};
let srv = make_http_server(frontend_html, args.data_plane_host, websocket_tx).await;
log::info!("Data plane server listening on: {}", srv.addr);
let static_server_addr = static_server.as_ref().map(|s| s.addr).unwrap_or(srv.addr);
log::info!("Static file server listening on: {static_server_addr}");
if open_in_browser {
open::that_detached(format!("http://{static_server_addr}"))
.log_error("failed to open browser for preview");
}
let _ = tokio::join!(previewer.join(), srv.join, control_plane_server_handle);
// Assert that the static server's lifetime is longer than the previewer.
let _s = static_server;
Ok(())
}
struct ScrollSource; struct ScrollSource;
impl Notification for ScrollSource { impl Notification for ScrollSource {
@ -750,7 +602,11 @@ fn send_show_document(client: &TypedLspClient<PreviewState>, s: &DocToSrcJumpInf
); );
} }
fn bind_streams(previewer: &mut Previewer, websocket_rx: mpsc::UnboundedReceiver<HyperWebsocket>) { /// Bind the hyper websocket streams to the previewer.
pub fn bind_streams(
previewer: &mut Previewer,
websocket_rx: mpsc::UnboundedReceiver<HyperWebsocket>,
) {
previewer.start_data_plane( previewer.start_data_plane(
websocket_rx, websocket_rx,
|conn: Result<HyperWebsocketStream, hyper_tungstenite::tungstenite::Error>| { |conn: Result<HyperWebsocketStream, hyper_tungstenite::tungstenite::Error>| {

View file

@ -23,7 +23,7 @@ pub struct ProjectPreviewHandler {
/// The project id. /// The project id.
pub project_id: ProjectInsId, pub project_id: ProjectInsId,
/// The connection to the compiler compiling projects (language server). /// The connection to the compiler compiling projects (language server).
pub(crate) client: Box<dyn ProjectClient>, pub client: Box<dyn ProjectClient>,
} }
impl ProjectPreviewHandler { impl ProjectPreviewHandler {

View file

@ -1,424 +1,17 @@
//! Project management tools. //! Project management tools.
use std::{ use std::sync::Arc;
borrow::Cow,
path::{Path, PathBuf},
sync::Arc,
};
use clap_complete::Shell;
use parking_lot::Mutex; use parking_lot::Mutex;
use reflexo::{path::unix_slash, ImmutPath};
use reflexo_typst::WorldComputeGraph;
use tinymist_query::analysis::Analysis; use tinymist_query::analysis::Analysis;
use tinymist_std::{bail, error::prelude::*};
use tokio::sync::mpsc; use tokio::sync::mpsc;
use crate::{actor::editor::EditorRequest, world::system::print_diagnostics, Config}; use crate::project::*;
use crate::{project::*, task::ExportTask}; use crate::{actor::editor::EditorRequest, Config};
/// Arguments for project compilation.
#[derive(Debug, Clone, clap::Parser)]
pub struct CompileArgs {
/// Inherits the compile task arguments.
#[clap(flatten)]
pub compile: TaskCompileArgs,
/// Saves the compilation arguments to the lock file.
#[clap(long)]
pub save_lock: bool,
/// Specifies the path to the lock file. If the path is
/// set, the lock file will be saved.
#[clap(long)]
pub lockfile: Option<PathBuf>,
}
/// Arguments for generating a build script.
#[derive(Debug, Clone, clap::Parser)]
pub struct GenerateScriptArgs {
/// The shell to generate the completion script for. If not provided, it
/// will be inferred from the environment.
#[clap(value_enum)]
pub shell: Option<Shell>,
/// The path to the output script.
#[clap(short, long)]
pub output: Option<String>,
}
#[cfg(feature = "preview")]
pub use super::preview::PreviewArgs;
#[cfg(feature = "preview")]
pub use tinymist_preview::PreviewMode;
/// Project task commands.
#[derive(Debug, Clone, clap::Subcommand)]
#[clap(rename_all = "kebab-case")]
pub enum TaskCommands {
/// Declare a preview task.
#[cfg(feature = "preview")]
Preview(TaskPreviewArgs),
}
/// Declare an lsp task.
#[derive(Debug, Clone, clap::Parser)]
#[cfg(feature = "preview")]
pub struct TaskPreviewArgs {
/// Argument to identify a project.
#[clap(flatten)]
pub declare: DocNewArgs,
/// Name a task.
#[clap(long = "task")]
pub task_name: Option<String>,
/// When to run the task
#[arg(long = "when")]
pub when: Option<TaskWhen>,
/// Preview arguments
#[clap(flatten)]
pub preview: PreviewArgs,
}
#[cfg(feature = "preview")]
trait LockFileExt {
fn preview(&mut self, doc_id: Id, args: &TaskPreviewArgs) -> Result<Id>;
}
#[cfg(feature = "preview")]
impl LockFileExt for LockFile {
fn preview(&mut self, doc_id: Id, args: &TaskPreviewArgs) -> Result<Id> {
let task_id = args
.task_name
.as_ref()
.map(|t| Id::new(t.clone()))
.unwrap_or(doc_id.clone());
let when = args.when.clone().unwrap_or(TaskWhen::OnType);
let task = ProjectTask::Preview(PreviewTask { when });
let task = ApplyProjectTask {
id: task_id.clone(),
document: doc_id,
task,
};
self.replace_task(task);
Ok(task_id)
}
}
/// Runs project compilation(s)
pub async fn compile_main(args: CompileArgs) -> Result<()> {
let cwd = std::env::current_dir().context("cannot get cwd")?;
// todo: respect the name of the lock file
// Saves the lock file if the flags are set
let save_lock = args.save_lock || args.lockfile.is_some();
let lock_dir: ImmutPath = if let Some(lockfile) = args.lockfile {
let lockfile = if lockfile.is_absolute() {
lockfile
} else {
cwd.join(lockfile)
};
lockfile
.parent()
.context("lock file must have a parent directory")?
.into()
} else {
cwd.as_path().into()
};
// Identifies the input and output
let input = args.compile.declare.to_input((&cwd, &lock_dir));
let output = args.compile.to_task(input.id.clone(), &cwd)?;
if save_lock {
LockFile::update(&lock_dir, |state| {
state.replace_document(input.relative_to(&lock_dir));
state.replace_task(output.clone());
Ok(())
})?;
}
// Prepares for the compilation
let universe = (input, lock_dir.clone()).resolve()?;
let world = universe.snapshot();
let graph = WorldComputeGraph::from_world(world);
// Compiles the project
let is_html = matches!(output.task, ProjectTask::ExportHtml(..));
let compiled = CompiledArtifact::from_graph(graph, is_html);
let diag = compiled.diagnostics();
print_diagnostics(compiled.world(), diag, DiagnosticFormat::Human)
.context_ut("print diagnostics")?;
if compiled.has_errors() {
// todo: we should process case of compile error in fn main function
std::process::exit(1);
}
// Exports the compiled project
let lock_dir = save_lock.then_some(lock_dir);
ExportTask::do_export(output.task, compiled, lock_dir).await?;
Ok(())
}
/// Generates a build script for compilation
pub fn generate_script_main(args: GenerateScriptArgs) -> Result<()> {
let Some(shell) = args.shell.or_else(Shell::from_env) else {
bail!("could not infer shell");
};
let output = Path::new(args.output.as_deref().unwrap_or("build"));
let output = match shell {
Shell::Bash | Shell::Zsh | Shell::Elvish | Shell::Fish => output.with_extension("sh"),
Shell::PowerShell => output.with_extension("ps1"),
_ => bail!("unsupported shell: {shell:?}"),
};
let script = match shell {
Shell::Bash | Shell::Zsh | Shell::PowerShell => shell_build_script(shell)?,
_ => bail!("unsupported shell: {shell:?}"),
};
std::fs::write(output, script).context("write script")?;
Ok(())
}
/// Generates a build script for shell-like shells
fn shell_build_script(shell: Shell) -> Result<String> {
let mut output = String::new();
match shell {
Shell::Bash => {
output.push_str("#!/usr/bin/env bash\n\n");
}
Shell::Zsh => {
output.push_str("#!/usr/bin/env zsh\n\n");
}
Shell::PowerShell => {}
_ => {}
}
let lock_dir = std::env::current_dir().context("current directory")?;
let lock = LockFile::read(&lock_dir)?;
struct CmdBuilder(Vec<Cow<'static, str>>);
impl CmdBuilder {
fn new() -> Self {
Self(vec![])
}
fn extend(&mut self, args: impl IntoIterator<Item = impl Into<Cow<'static, str>>>) {
for arg in args {
self.0.push(arg.into());
}
}
fn push(&mut self, arg: impl Into<Cow<'static, str>>) {
self.0.push(arg.into());
}
fn build(self) -> String {
self.0.join(" ")
}
}
let quote_escape = |s: &str| s.replace("'", r#"'"'"'"#);
let quote = |s: &str| format!("'{}'", s.replace("'", r#"'"'"'"#));
let path_of = |p: &ResourcePath, loc: &str| {
let Some(path) = p.to_rel_path(&lock_dir) else {
log::error!("could not resolve path for {loc}, path: {p:?}");
return String::default();
};
quote(&unix_slash(&path))
};
let base_cmd: Vec<&str> = vec!["tinymist", "compile", "--save-lock"];
for task in lock.task.iter() {
let Some(input) = lock.get_document(&task.document) else {
log::warn!(
"could not find document for task {:?}, whose document is {:?}",
task.id,
task.doc_id()
);
continue;
};
// todo: preview/query commands
let Some(export) = task.task.as_export() else {
continue;
};
let mut cmd = CmdBuilder::new();
cmd.extend(base_cmd.iter().copied());
cmd.push("--task");
cmd.push(quote(&task.id.to_string()));
cmd.push(path_of(&input.main, "main"));
if let Some(root) = &input.root {
cmd.push("--root");
cmd.push(path_of(root, "root"));
}
for (k, v) in &input.inputs {
cmd.push(format!(
r#"--input='{}={}'"#,
quote_escape(k),
quote_escape(v)
));
}
for p in &input.font_paths {
cmd.push("--font-path");
cmd.push(path_of(p, "font-path"));
}
if !input.system_fonts {
cmd.push("--ignore-system-fonts");
}
if let Some(p) = &input.package_path {
cmd.push("--package-path");
cmd.push(path_of(p, "package-path"));
}
if let Some(p) = &input.package_cache_path {
cmd.push("--package-cache-path");
cmd.push(path_of(p, "package-cache-path"));
}
if let Some(p) = &export.output {
cmd.push("--output");
cmd.push(quote(&p.to_string()));
}
for t in &export.transform {
match t {
ExportTransform::Pretty { .. } => {
cmd.push("--pretty");
}
ExportTransform::Pages { ranges } => {
for r in ranges {
cmd.push("--pages");
cmd.push(r.to_string());
}
}
// todo: export me
ExportTransform::Merge { .. } | ExportTransform::Script { .. } => {}
}
}
match &task.task {
ProjectTask::Preview(..) | ProjectTask::Query(..) => {}
ProjectTask::ExportPdf(task) => {
cmd.push("--format=pdf");
for s in &task.pdf_standards {
cmd.push("--pdf-standard");
let s = serde_json::to_string(s).context("pdf standard")?;
cmd.push(s);
}
if let Some(output) = &task.creation_timestamp {
cmd.push("--creation-timestamp");
cmd.push(output.to_string());
}
}
ProjectTask::ExportSvg(..) => {
cmd.push("--format=svg");
}
ProjectTask::ExportSvgHtml(..) => {
cmd.push("--format=svg_html");
}
ProjectTask::ExportMd(..) => {
cmd.push("--format=md");
}
ProjectTask::ExportTeX(..) => {
cmd.push("--format=tex");
}
ProjectTask::ExportPng(..) => {
cmd.push("--format=png");
}
ProjectTask::ExportText(..) => {
cmd.push("--format=txt");
}
ProjectTask::ExportHtml(..) => {
cmd.push("--format=html");
}
}
let ext = task.task.extension();
output.push_str(&format!(
"# From {} to {} ({ext})\n",
task.doc_id(),
task.id
));
output.push_str(&cmd.build());
output.push('\n');
}
Ok(output)
}
/// Project document commands' main
pub fn project_main(args: DocCommands) -> Result<()> {
let cwd = std::env::current_dir().context("cannot get cwd")?;
LockFile::update(&cwd, |state| {
let ctx: (&Path, &Path) = (&cwd, &cwd);
match args {
DocCommands::New(args) => {
state.replace_document(args.to_input(ctx));
}
DocCommands::Configure(args) => {
let id: Id = args.id.id(ctx);
state.route.push(ProjectRoute {
id: id.clone(),
priority: args.priority,
});
}
}
Ok(())
})
}
/// Project task commands' main
pub fn task_main(args: TaskCommands) -> Result<()> {
let cwd = std::env::current_dir().context("cannot get cwd")?;
LockFile::update(&cwd, |state| {
let ctx: (&Path, &Path) = (&cwd, &cwd);
let _ = state;
match args {
#[cfg(feature = "preview")]
TaskCommands::Preview(args) => {
let input = args.declare.to_input(ctx);
let id = input.id.clone();
state.replace_document(input);
let _ = state.preview(id, &args);
Ok(())
}
}
})
}
/// Options for starting a project.
#[derive(Default)] #[derive(Default)]
pub(crate) struct ProjectOpts { pub struct ProjectOpts {
/// The tokio runtime handle. /// The tokio runtime handle.
pub handle: Option<tokio::runtime::Handle>, pub handle: Option<tokio::runtime::Handle>,
/// The shared preview state. /// The shared preview state.
@ -426,21 +19,26 @@ pub(crate) struct ProjectOpts {
/// The shared config. /// The shared config.
pub config: Config, pub config: Config,
/// The shared preview state. /// The shared preview state.
#[cfg(feature = "preview")]
pub preview: ProjectPreviewState, pub preview: ProjectPreviewState,
/// The export target. /// The export target.
pub export_target: ExportTarget, pub export_target: ExportTarget,
} }
pub(crate) struct StartProjectResult<F> { /// Result of starting a project.
pub struct StartProjectResult<F> {
/// A future service that runs the project.
pub service: WatchService<F>, pub service: WatchService<F>,
/// The interrupt sender.
pub intr_tx: mpsc::UnboundedSender<LspInterrupt>, pub intr_tx: mpsc::UnboundedSender<LspInterrupt>,
/// The editor request receiver.
pub editor_rx: mpsc::UnboundedReceiver<EditorRequest>, pub editor_rx: mpsc::UnboundedReceiver<EditorRequest>,
} }
// todo: This is only extracted from the `tinymist preview` command, and we need // todo: This is only extracted from the `tinymist preview` command, and we need
// to abstract it in future. // to abstract it in future.
/// Start a project with the given universe. /// Starts a project with the given universe.
pub(crate) fn start_project<F>( pub fn start_project<F>(
verse: LspUniverse, verse: LspUniverse,
opts: Option<ProjectOpts>, opts: Option<ProjectOpts>,
intr_handler: F, intr_handler: F,
@ -453,23 +51,37 @@ where
), ),
{ {
let opts = opts.unwrap_or_default(); let opts = opts.unwrap_or_default();
#[cfg(any(feature = "export", feature = "system"))]
let handle = opts.handle.unwrap_or_else(tokio::runtime::Handle::current); let handle = opts.handle.unwrap_or_else(tokio::runtime::Handle::current);
let _ = opts.config;
// type EditorSender = mpsc::UnboundedSender<EditorRequest>; // type EditorSender = mpsc::UnboundedSender<EditorRequest>;
let (editor_tx, editor_rx) = mpsc::unbounded_channel(); let (editor_tx, editor_rx) = mpsc::unbounded_channel();
let (intr_tx, intr_rx) = tokio::sync::mpsc::unbounded_channel(); let (intr_tx, intr_rx) = tokio::sync::mpsc::unbounded_channel();
// todo: unify filesystem watcher // todo: unify filesystem watcher
let (dep_tx, dep_rx) = tokio::sync::mpsc::unbounded_channel(); let (dep_tx, dep_rx) = mpsc::unbounded_channel();
// todo: notify feature?
#[cfg(feature = "system")]
{
let fs_intr_tx = intr_tx.clone(); let fs_intr_tx = intr_tx.clone();
handle.spawn(watch_deps(dep_rx, move |event| { handle.spawn(watch_deps(dep_rx, move |event| {
fs_intr_tx.interrupt(LspInterrupt::Fs(event)); fs_intr_tx.interrupt(LspInterrupt::Fs(event));
})); }));
}
#[cfg(not(feature = "system"))]
{
let _ = dep_rx;
log::warn!("Project: system watcher is not enabled, file changes will not be watched");
}
// Create the actor // Create the actor
let compile_handle = Arc::new(CompileHandlerImpl { let compile_handle = Arc::new(CompileHandlerImpl {
#[cfg(feature = "preview")]
preview: opts.preview, preview: opts.preview,
is_standalone: true, is_standalone: true,
#[cfg(feature = "export")]
export: crate::task::ExportTask::new(handle, Some(editor_tx.clone()), opts.config.export()), export: crate::task::ExportTask::new(handle, Some(editor_tx.clone()), opts.config.export()),
editor_tx, editor_tx,
client: Arc::new(intr_tx.clone()), client: Arc::new(intr_tx.clone()),
@ -502,7 +114,9 @@ where
} }
} }
pub(crate) struct WatchService<F> { /// A service that watches for project changes and compiles them.
pub struct WatchService<F> {
/// The project compiler.
pub compiler: LspProjectCompiler, pub compiler: LspProjectCompiler,
intr_rx: tokio::sync::mpsc::UnboundedReceiver<LspInterrupt>, intr_rx: tokio::sync::mpsc::UnboundedReceiver<LspInterrupt>,
intr_handler: F, intr_handler: F,
@ -517,6 +131,7 @@ where
) + Send ) + Send
+ 'static, + 'static,
{ {
/// Runs the project service.
pub async fn run(self) { pub async fn run(self) {
let Self { let Self {
mut compiler, mut compiler,

View file

@ -1,12 +1,19 @@
use core::fmt; use core::fmt;
#[cfg(feature = "system")]
use std::pin::Pin; use std::pin::Pin;
#[cfg(feature = "system")]
use std::sync::atomic::AtomicU64; use std::sync::atomic::AtomicU64;
#[cfg(feature = "system")]
use std::sync::Arc; use std::sync::Arc;
#[cfg(feature = "system")]
use std::task::{Context, Poll}; use std::task::{Context, Poll};
#[cfg(feature = "system")]
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
#[cfg(feature = "system")]
use tokio::net::TcpStream; use tokio::net::TcpStream;
#[cfg(feature = "system")]
use tokio_util::sync::CancellationToken;
#[derive(Clone)] #[derive(Clone)]
pub struct Derived<T>(pub T); pub struct Derived<T>(pub T);
@ -52,7 +59,6 @@ macro_rules! get_arg_or_default {
}}; }};
} }
pub(crate) use get_arg_or_default; pub(crate) use get_arg_or_default;
use tokio_util::sync::CancellationToken;
pub fn try_<T>(f: impl FnOnce() -> Option<T>) -> Option<T> { pub fn try_<T>(f: impl FnOnce() -> Option<T>) -> Option<T> {
f() f()
@ -62,17 +68,11 @@ pub fn try_or<T>(f: impl FnOnce() -> Option<T>, default: T) -> T {
f().unwrap_or(default) f().unwrap_or(default)
} }
pub fn exit_on_ctrl_c() { #[cfg(feature = "system")]
tokio::spawn(async move {
let _ = tokio::signal::ctrl_c().await;
log::info!("Ctrl-C received, exiting");
std::process::exit(0);
});
}
#[derive(Default)] #[derive(Default)]
pub(crate) struct AliveLock(Arc<AtomicU64>); pub(crate) struct AliveLock(Arc<AtomicU64>);
#[cfg(feature = "system")]
impl AliveLock { impl AliveLock {
pub fn hold(cnt: Arc<AtomicU64>) -> Self { pub fn hold(cnt: Arc<AtomicU64>) -> Self {
let held = cnt.fetch_add(1, std::sync::atomic::Ordering::SeqCst); let held = cnt.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
@ -81,6 +81,7 @@ impl AliveLock {
} }
} }
#[cfg(feature = "system")]
impl Drop for AliveLock { impl Drop for AliveLock {
fn drop(&mut self) { fn drop(&mut self) {
let cnt = self.0.fetch_sub(1, std::sync::atomic::Ordering::SeqCst); let cnt = self.0.fetch_sub(1, std::sync::atomic::Ordering::SeqCst);
@ -88,11 +89,13 @@ impl Drop for AliveLock {
} }
} }
#[cfg(feature = "system")]
pub(crate) struct ConnWithCancel { pub(crate) struct ConnWithCancel {
stream: TcpStream, stream: TcpStream,
pub cancel: CancellationToken, pub cancel: CancellationToken,
} }
#[cfg(feature = "system")]
impl ConnWithCancel { impl ConnWithCancel {
pub fn new(stream: TcpStream) -> Self { pub fn new(stream: TcpStream) -> Self {
Self { Self {
@ -102,12 +105,14 @@ impl ConnWithCancel {
} }
} }
#[cfg(feature = "system")]
impl Drop for ConnWithCancel { impl Drop for ConnWithCancel {
fn drop(&mut self) { fn drop(&mut self) {
self.cancel.cancel() self.cancel.cancel()
} }
} }
#[cfg(feature = "system")]
impl AsyncRead for ConnWithCancel { impl AsyncRead for ConnWithCancel {
fn poll_read( fn poll_read(
self: Pin<&mut Self>, self: Pin<&mut Self>,
@ -118,6 +123,7 @@ impl AsyncRead for ConnWithCancel {
} }
} }
#[cfg(feature = "system")]
impl AsyncWrite for ConnWithCancel { impl AsyncWrite for ConnWithCancel {
fn poll_write( fn poll_write(
self: Pin<&mut Self>, self: Pin<&mut Self>,

View file

@ -1,8 +1,8 @@
import tinymist_init from "../pkg/tinymist_core.js"; import tinymist_init from "../pkg/tinymist.js";
import * as tinymist from "../pkg/tinymist_core.js"; import * as tinymist from "../pkg/tinymist.js";
import fs from "fs"; import fs from "fs";
const wasmData = fs.readFileSync("pkg/tinymist_core_bg.wasm"); const wasmData = fs.readFileSync("pkg/tinymist_bg.wasm");
async function main() { async function main() {
await tinymist_init({ await tinymist_init({

View file

@ -5,7 +5,7 @@
!out/tinymist-docs.pdf !out/tinymist-docs.pdf
!out/extension.js !out/extension.js
!out/extension.web.js !out/extension.web.js
!out/tinymist_core_bg.wasm !out/tinymist_bg.wasm
!out/tinymist !out/tinymist
!out/tinymist.exe !out/tinymist.exe
!out/typst.tmLanguage.json !out/typst.tmLanguage.json

View file

@ -5,7 +5,7 @@
"type": "module", "type": "module",
"license": "Apache-2.0", "license": "Apache-2.0",
"workspaces": [ "workspaces": [
"crates/tinymist-core", "crates/tinymist",
"editors/vscode", "editors/vscode",
"contrib/typst-preview/editors/vscode", "contrib/typst-preview/editors/vscode",
"contrib/html/editors/vscode", "contrib/html/editors/vscode",
@ -20,7 +20,7 @@
"build:preview": "cd tools/typst-preview-frontend && yarn run build && rimraf ../../crates/tinymist-assets/src/typst-preview.html && cpr ./dist/index.html ../../crates/tinymist-assets/src/typst-preview.html", "build:preview": "cd tools/typst-preview-frontend && yarn run build && rimraf ../../crates/tinymist-assets/src/typst-preview.html && cpr ./dist/index.html ../../crates/tinymist-assets/src/typst-preview.html",
"build:l10n": "yarn extract:l10n && node scripts/build-l10n.mjs", "build:l10n": "yarn extract:l10n && node scripts/build-l10n.mjs",
"build:docker": "docker build -t myriaddreamin/tinymist:0.13.22 .", "build:docker": "docker build -t myriaddreamin/tinymist:0.13.22 .",
"build:web": "cd crates/tinymist-core && yarn build && cp pkg/tinymist_core_bg.wasm ../../editors/vscode/out/tinymist_core_bg.wasm", "build:web": "cd crates/tinymist && yarn build && cp pkg/tinymist_bg.wasm ../../editors/vscode/out/tinymist_bg.wasm",
"extract:l10n": "yarn extract:l10n:ts && yarn extract:l10n:rs", "extract:l10n": "yarn extract:l10n:ts && yarn extract:l10n:rs",
"extract:l10n:ts": "cargo run --release --bin tinymist-l10n -- --kind ts --dir ./editors/vscode --output ./locales/tinymist-vscode-rt.toml", "extract:l10n:ts": "cargo run --release --bin tinymist-l10n -- --kind ts --dir ./editors/vscode --output ./locales/tinymist-vscode-rt.toml",
"extract:l10n:rs": "cargo run --release --bin tinymist-l10n -- --kind rs --dir ./crates --output ./locales/tinymist-rt.toml && rimraf ./crates/tinymist-assets/src/tinymist-rt.toml && cpr ./locales/tinymist-rt.toml ./crates/tinymist-assets/src/tinymist-rt.toml", "extract:l10n:rs": "cargo run --release --bin tinymist-l10n -- --kind rs --dir ./crates --output ./locales/tinymist-rt.toml && rimraf ./crates/tinymist-assets/src/tinymist-rt.toml && cpr ./locales/tinymist-rt.toml ./crates/tinymist-assets/src/tinymist-rt.toml",

View file

@ -5,3 +5,10 @@ cargo clippy -p sync-ls --no-default-features --features=lsp,dap
cargo clippy -p typlite --no-default-features --features=cli,no-content-hint cargo clippy -p typlite --no-default-features --features=cli,no-content-hint
cargo clippy -p typlite --no-default-features --features=cli,docx,no-content-hint cargo clippy -p typlite --no-default-features --features=cli,docx,no-content-hint
cargo clippy -p tinymist --no-default-features --features=no-content-hint
cargo clippy -p tinymist --no-default-features --features=no-content-hint,preview
# cargo clippy -p tinymist --no-default-features --features=no-content-hint,export
# cargo clippy -p tinymist --no-default-features --features=no-content-hint,trace
cargo clippy -p tinymist --no-default-features --features=no-content-hint,dap
cargo clippy -p tinymist --no-default-features --features=no-content-hint,web

View file

@ -207,8 +207,8 @@ class NightlyUtils {
'typlite', 'typlite',
'typst-shim', 'typst-shim',
'sync-lsp', 'sync-lsp',
'tinymist-cli',
'tinymist-analysis', 'tinymist-analysis',
'tinymist-core',
'tinymist-debug', 'tinymist-debug',
'tinymist-l10n', 'tinymist-l10n',
'tinymist-package', 'tinymist-package',
@ -249,7 +249,7 @@ class NightlyUtils {
await this.ensureInit(); await this.ensureInit();
const nonWorldCrates = [ const nonWorldCrates = [
'sync-ls', 'tinymist', 'tinymist-analysis', 'tinymist-core', 'tinymist-debug', 'sync-ls', 'tinymist-cli', 'tinymist-analysis', 'tinymist', 'tinymist-debug',
'tinymist-lint', 'tinymist-query', 'tinymist-render', 'tinymist-preview', 'typlite' 'tinymist-lint', 'tinymist-query', 'tinymist-render', 'tinymist-preview', 'typlite'
]; ];
await this.updateDependencies(nonWorldCrates, newVersion); await this.updateDependencies(nonWorldCrates, newVersion);
@ -265,7 +265,7 @@ class NightlyUtils {
async updateVersionFiles(newVersion) { async updateVersionFiles(newVersion) {
const jsonFiles = [ const jsonFiles = [
'contrib/html/editors/vscode/package.json', 'contrib/html/editors/vscode/package.json',
'crates/tinymist-core/package.json', 'crates/tinymist/package.json',
'editors/vscode/package.json', 'editors/vscode/package.json',
'syntaxes/textmate/package.json' 'syntaxes/textmate/package.json'
]; ];