diff --git a/.github/workflows/build-vscode-others.yml b/.github/workflows/build-vscode-others.yml index 5e093768..54dbaf02 100644 --- a/.github/workflows/build-vscode-others.yml +++ b/.github/workflows/build-vscode-others.yml @@ -162,12 +162,12 @@ jobs: run: | npm pack > package-name mv $(cat package-name) tinymist-${{ env.target }}.tar.gz - working-directory: ./crates/tinymist-core + working-directory: ./crates/tinymist - name: Upload tinymist npm library uses: actions/upload-artifact@v4 with: 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 uses: actions/download-artifact@v4 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 621f7cfc..e90a6985 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,12 +62,12 @@ jobs: - name: Generate completions run: | 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 -p tinymist -- completion bash > completions/bash/tinymist - cargo run -p tinymist -- completion fish > completions/fish/vendor_completions.d/tinymist.fish - cargo run -p tinymist -- completion elvish > completions/elvish/lib/tinymist.elv - cargo run -p tinymist -- completion nushell > completions/nushell/vendor/autoload/tinymist.nu - cargo run -p tinymist -- completion powershell > completions/powershell/tinymist.ps1 + cargo run --bin tinymist -- completion zsh > completions/zsh/_tinymist + cargo run --bin tinymist -- completion bash > completions/bash/tinymist + cargo run --bin tinymist -- completion fish > completions/fish/vendor_completions.d/tinymist.fish + cargo run --bin tinymist -- completion elvish > completions/elvish/lib/tinymist.elv + cargo run --bin tinymist -- completion nushell > completions/nushell/vendor/autoload/tinymist.nu + cargo run --bin tinymist -- completion powershell > completions/powershell/tinymist.ps1 tar -czvf tinymist-completions.tar.gz completions - name: upload completions uses: actions/upload-artifact@v4 diff --git a/.github/workflows/release-crates.yml b/.github/workflows/release-crates.yml index 986b0eb7..c37b58a2 100644 --- a/.github/workflows/release-crates.yml +++ b/.github/workflows/release-crates.yml @@ -51,9 +51,9 @@ jobs: cargo publish --no-verify -p tinymist-lint || true cargo publish --no-verify -p tinymist-query || 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 || true + cargo publish --no-verify -p tinymist-cli || true - name: Verifies crate health (Optional) run: | cargo publish --dry-run -p sync-ls diff --git a/Cargo.lock b/Cargo.lock index de814c7f..dff9253f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4133,6 +4133,7 @@ dependencies = [ "clap_mangen", "codespan-reporting", "comemo", + "console_error_panic_hook", "crossbeam-channel", "dapts", "dhat", @@ -4144,6 +4145,7 @@ dependencies = [ "hyper-tungstenite", "hyper-util", "itertools 0.13.0", + "js-sys", "log", "lsp-types", "open", @@ -4161,7 +4163,6 @@ dependencies = [ "sync-ls", "temp-env", "tinymist-assets 0.13.22 (registry+https://github.com/rust-lang/crates.io-index)", - "tinymist-core", "tinymist-debug", "tinymist-l10n", "tinymist-preview", @@ -4188,6 +4189,7 @@ dependencies = [ "unicode-script", "vergen", "walkdir", + "wasm-bindgen", ] [[package]] @@ -4242,18 +4244,77 @@ dependencies = [ ] [[package]] -name = "tinymist-core" +name = "tinymist-cli" version = "0.13.22" dependencies = [ "anyhow", + "async-trait", + "base64", "cargo_metadata", - "console_error_panic_hook", - "js-sys", + "chrono", + "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-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-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", - "wasm-bindgen", + "walkdir", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index d45383bf..9eb111c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,6 +69,7 @@ reqwest = { version = "^0.12", default-features = false, features = [ "blocking", "multipart", ] } +http-body-util = "0.1.2" # Algorithms base64 = "0.22" @@ -201,9 +202,9 @@ typst-shim = { path = "./crates/typst-shim", version = "0.13.16" } tinymist-tests = { path = "./crates/tinymist-tests/" } 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-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-lint = { path = "./crates/tinymist-lint/", version = "0.13.22" } tinymist-query = { path = "./crates/tinymist-query/", version = "0.13.22" } diff --git a/crates/sync-lsp/Cargo.toml b/crates/sync-lsp/Cargo.toml index b3d68434..8685636b 100644 --- a/crates/sync-lsp/Cargo.toml +++ b/crates/sync-lsp/Cargo.toml @@ -14,33 +14,27 @@ rust-version.workspace = true [dependencies] anyhow.workspace = true +crossbeam-channel.workspace = true dapts = { workspace = true, optional = true } +futures.workspace = true log.workspace = true lsp-types = { workspace = true, optional = true } +parking_lot.workspace = true serde.workspace = true serde_json.workspace = true clap = { workspace = true, optional = true } -crossbeam-channel = { workspace = true, optional = true } -futures = { workspace = true, optional = true } -parking_lot = { workspace = true, optional = true } -tokio = { workspace = true, features = ["rt", "time"], optional = true } +tokio = { workspace = true, features = ["rt"], optional = true } tokio-util = { workspace = true, optional = true } [features] dap = ["dapts"] lsp = ["lsp-types"] -server = [ - "crossbeam-channel", - "futures", - "tokio", - "tokio-util", - "clap", - "parking_lot", -] +server = ["tokio"] +system = ["tokio", "tokio/time", "tokio-util", "clap"] [package.metadata.docs.rs] -features = ["dap", "lsp", "server"] +features = ["dap", "lsp", "system", "server"] [lints] workspace = true diff --git a/crates/sync-lsp/src/lib.rs b/crates/sync-lsp/src/lib.rs index d734a93e..fa97c2bf 100644 --- a/crates/sync-lsp/src/lib.rs +++ b/crates/sync-lsp/src/lib.rs @@ -17,7 +17,7 @@ pub use server::*; pub mod req_queue; #[cfg(feature = "server")] mod server; -#[cfg(feature = "server")] +#[cfg(all(feature = "server", feature = "system"))] pub mod transport; use std::any::Any; diff --git a/crates/sync-lsp/src/lsp.rs b/crates/sync-lsp/src/lsp.rs index cd253fb7..ba6dae6a 100644 --- a/crates/sync-lsp/src/lsp.rs +++ b/crates/sync-lsp/src/lsp.rs @@ -195,7 +195,7 @@ impl Notification { } } - #[cfg(feature = "server")] + #[cfg(all(feature = "server", feature = "system"))] pub(crate) fn is_exit(&self) -> bool { self.method == "exit" } diff --git a/crates/sync-lsp/src/server.rs b/crates/sync-lsp/src/server.rs index 6f69be21..f325be63 100644 --- a/crates/sync-lsp/src/server.rs +++ b/crates/sync-lsp/src/server.rs @@ -69,7 +69,7 @@ pub struct TConnectionRx { impl> TConnectionRx { /// Receives a message or an event. - pub(crate) fn recv(&self) -> anyhow::Result> { + pub fn recv(&self) -> anyhow::Result> { crossbeam_channel::select_biased! { recv(self.lsp) -> msg => Ok(EventOrMessage::Msg(msg?.try_into()?)), recv(self.event) -> event => Ok(event.map(EventOrMessage::Evt)?), @@ -78,8 +78,10 @@ impl> TConnectionRx { } /// This is a helper enum to handle both events and messages. -pub(crate) enum EventOrMessage { +pub enum EventOrMessage { + /// An event received. Evt(Event), + /// A message received. Msg(M), } @@ -380,7 +382,7 @@ impl LspClient { /// Finally sends the response if it is not sent before. /// 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 { // Already responded Ok(Some(())) => {} diff --git a/crates/sync-lsp/src/server/lsp_srv.rs b/crates/sync-lsp/src/server/lsp_srv.rs index b16dd47e..51f1a894 100644 --- a/crates/sync-lsp/src/server/lsp_srv.rs +++ b/crates/sync-lsp/src/server/lsp_srv.rs @@ -169,6 +169,7 @@ where /// /// See [`transport::MirrorArgs`] for information about the record-replay /// feature. + #[cfg(feature = "system")] pub fn start( &mut self, inbox: TConnectionRx, @@ -202,6 +203,7 @@ where } /// Starts the language server on the given connection. + #[cfg(feature = "system")] pub fn start_(&mut self, inbox: TConnectionRx) -> anyhow::Result<()> { use EventOrMessage::*; // todo: follow what rust analyzer does @@ -273,7 +275,7 @@ where /// Registers and handles a request. This should only be called once per /// 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 .register_request(&req.method, &req.id, request_received); @@ -353,7 +355,11 @@ where } /// 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(¬.method); let handle = |s, Notification { method, params }: Notification| { let Some(handler) = self.notifications.get(method.as_str()) else { diff --git a/crates/tinymist-cli/Cargo.toml b/crates/tinymist-cli/Cargo.toml new file mode 100644 index 00000000..fb85d894 --- /dev/null +++ b/crates/tinymist-cli/Cargo.toml @@ -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 diff --git a/crates/tinymist/README.md b/crates/tinymist-cli/README.md similarity index 100% rename from crates/tinymist/README.md rename to crates/tinymist-cli/README.md diff --git a/crates/tinymist/src/args.rs b/crates/tinymist-cli/src/args.rs similarity index 84% rename from crates/tinymist/src/args.rs rename to crates/tinymist-cli/src/args.rs index dbd36924..c9c702c2 100644 --- a/crates/tinymist/src/args.rs +++ b/crates/tinymist-cli/src/args.rs @@ -2,10 +2,19 @@ use std::path::Path; use sync_ls::transport::MirrorArgs; use tinymist::project::DocCommands; -use tinymist::tool::project::{CompileArgs, GenerateScriptArgs, TaskCommands}; -use tinymist::tool::testing::TestArgs; +use tinymist::LONG_VERSION; 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)] #[clap(name = "tinymist", author, version, about, long_version(LONG_VERSION.as_str()))] @@ -197,3 +206,33 @@ pub enum QueryDocsFormat { Json, 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, + + /// When to run the task + #[arg(long = "when")] + pub when: Option, + + /// Preview arguments + #[clap(flatten)] + pub preview: PreviewArgs, +} diff --git a/crates/tinymist-cli/src/compile.rs b/crates/tinymist-cli/src/compile.rs new file mode 100644 index 00000000..6b27f754 --- /dev/null +++ b/crates/tinymist-cli/src/compile.rs @@ -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, +} + +/// 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(()) +} diff --git a/crates/tinymist-cli/src/generate_script.rs b/crates/tinymist-cli/src/generate_script.rs new file mode 100644 index 00000000..97dfc61d --- /dev/null +++ b/crates/tinymist-cli/src/generate_script.rs @@ -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, + /// The path to the output script. + #[clap(short, long)] + pub output: Option, +} + +/// 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 { + 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>); + + impl CmdBuilder { + fn new() -> Self { + Self(vec![]) + } + + fn extend(&mut self, args: impl IntoIterator>>) { + for arg in args { + self.0.push(arg.into()); + } + } + + fn push(&mut self, arg: impl Into>) { + 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) +} diff --git a/crates/tinymist-cli/src/lib.rs b/crates/tinymist-cli/src/lib.rs new file mode 100644 index 00000000..385e9904 --- /dev/null +++ b/crates/tinymist-cli/src/lib.rs @@ -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::*; diff --git a/crates/tinymist/src/main.rs b/crates/tinymist-cli/src/main.rs similarity index 81% rename from crates/tinymist/src/main.rs rename to crates/tinymist-cli/src/main.rs index 30522ca1..844e58a8 100644 --- a/crates/tinymist/src/main.rs +++ b/crates/tinymist-cli/src/main.rs @@ -1,6 +1,13 @@ #![doc = include_str!("../README.md")] mod args; +#[cfg(feature = "export")] +mod compile; +mod generate_script; +#[cfg(feature = "preview")] +mod preview; +mod testing; +mod utils; use core::fmt; use std::collections::HashMap; @@ -21,22 +28,31 @@ use sync_ls::{ internal_error, DapBuilder, DapMessage, GetMessageKind, LsHook, LspBuilder, LspClientRoot, 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::{Config, DapRegularInit, RegularInit, ServerState, SuperInit, UserActionTask}; -use tinymist_core::LONG_VERSION; +use tinymist::LONG_VERSION; +use tinymist::{Config, RegularInit, ServerState, SuperInit, UserActionTask}; use tinymist_project::EntryResolver; use tinymist_query::package::PackageInfo; use tinymist_std::hash::{FxBuildHasher, FxHashMap}; use tinymist_std::{bail, error::prelude::*}; +use typst::ecow::EcoString; #[cfg(feature = "l10n")] 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::*; +#[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")] #[global_allocator] static ALLOC: dhat::Alloc = dhat::Alloc; @@ -103,19 +119,16 @@ fn main() -> Result<()> { Commands::Completion(args) => completion(args), Commands::Cov(args) => coverage_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::GenerateScript(args) => generate_script_main(args), Commands::Query(query_cmds) => query_main(query_cmds), Commands::Lsp(args) => lsp_main(args), + #[cfg(feature = "dap")] Commands::Dap(args) => dap_main(args), Commands::TraceLsp(args) => trace_lsp_main(args), #[cfg(feature = "preview")] - Commands::Preview(args) => { - #[cfg(feature = "preview")] - use tinymist::tool::preview::preview_main; - - RUNTIMES.tokio_runtime.block_on(preview_main(args)) - } + Commands::Preview(args) => RUNTIMES.tokio_runtime.block_on(preview_main(args)), Commands::Doc(args) => project_main(args), Commands::Task(args) => task_main(args), Commands::Probe => Ok(()), @@ -163,6 +176,7 @@ pub fn lsp_main(args: LspArgs) -> Result<()> { } /// The main entry point for the language server. +#[cfg(feature = "dap")] pub fn dap_main(args: DapArgs) -> Result<()> { let pairs = LONG_VERSION.trim().split('\n'); let pairs = pairs @@ -175,7 +189,7 @@ pub fn dap_main(args: DapArgs) -> Result<()> { with_stdio_transport::(args.mirror.clone(), |conn| { let client = client_root(conn.sender); ServerState::install_dap(DapBuilder::new( - DapRegularInit { + tinymist::DapRegularInit { client: client.weak().to_typed(), font_opts: args.font, }, @@ -342,6 +356,85 @@ pub fn query_main(cmds: QueryCommands) -> Result<()> { Ok(()) } +#[cfg(feature = "preview")] +trait LockFileExt { + fn preview(&mut self, doc_id: Id, args: &TaskPreviewArgs) -> Result; +} + +#[cfg(feature = "preview")] +impl LockFileExt for LockFile { + fn preview(&mut self, doc_id: Id, args: &TaskPreviewArgs) -> Result { + 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. fn client_root + GetMessageKind>( sender: TConnectionTx, diff --git a/crates/tinymist-cli/src/preview.rs b/crates/tinymist-cli/src/preview.rs new file mode 100644 index 00000000..f71448e2 --- /dev/null +++ b/crates/tinymist-cli/src/preview.rs @@ -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 = 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::(&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(()) +} diff --git a/crates/tinymist/src/tool/testing-log.typ b/crates/tinymist-cli/src/testing-log.typ similarity index 100% rename from crates/tinymist/src/tool/testing-log.typ rename to crates/tinymist-cli/src/testing-log.typ diff --git a/crates/tinymist/src/tool/testing.rs b/crates/tinymist-cli/src/testing.rs similarity index 99% rename from crates/tinymist/src/tool/testing.rs rename to crates/tinymist-cli/src/testing.rs index 538e7c16..7443b8f3 100644 --- a/crates/tinymist/src/tool/testing.rs +++ b/crates/tinymist-cli/src/testing.rs @@ -24,9 +24,11 @@ use typst::syntax::{ast, LinkedNode, Source, Span}; use typst::{utils::PicoStr, World}; use typst_shim::eval::TypstEngine; -use super::project::{start_project, StartProjectResult}; -use crate::world::{with_main, SourceWorld}; -use crate::{project::*, utils::exit_on_ctrl_c}; +use tinymist::project::*; +use tinymist::tool::project::{start_project, StartProjectResult}; +use tinymist::world::{with_main, SourceWorld}; + +use crate::utils::exit_on_ctrl_c; const TEST_EVICT_MAX_AGE: usize = 30; const PREFIX_LEN: usize = 7; diff --git a/crates/tinymist-cli/src/utils.rs b/crates/tinymist-cli/src/utils.rs new file mode 100644 index 00000000..fe10206a --- /dev/null +++ b/crates/tinymist-cli/src/utils.rs @@ -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); + }); +} diff --git a/crates/tinymist-core/Cargo.toml b/crates/tinymist-core/Cargo.toml deleted file mode 100644 index 6d3f230f..00000000 --- a/crates/tinymist-core/Cargo.toml +++ /dev/null @@ -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 diff --git a/crates/tinymist-core/src/lib.rs b/crates/tinymist-core/src/lib.rs deleted file mode 100644 index ed5821fc..00000000 --- a/crates/tinymist-core/src/lib.rs +++ /dev/null @@ -1,30 +0,0 @@ -//! Tinymist Core Library - -use std::sync::LazyLock; - -/// The long version description of the library -pub static LONG_VERSION: LazyLock = 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; diff --git a/crates/tinymist-project/src/lsp.rs b/crates/tinymist-project/src/lsp.rs index 065c62c3..6307d531 100644 --- a/crates/tinymist-project/src/lsp.rs +++ b/crates/tinymist-project/src/lsp.rs @@ -282,6 +282,28 @@ impl LspUniverseBuilder { Ok(searcher.build()) } + /// Resolve fonts from given options. + #[cfg(all(not(feature = "system"), feature = "web"))] + pub fn resolve_fonts(args: CompileFontArgs) -> Result { + 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 { + let mut searcher = tinymist_world::font::memory::MemoryFontSearcher::default(); + searcher.add_memory_fonts(typst_assets::fonts().map(Bytes::new).collect::>()); + Ok(searcher.build()) + } + /// Resolves package registry from given options. #[cfg(feature = "system")] pub fn resolve_package( diff --git a/crates/tinymist-query/Cargo.toml b/crates/tinymist-query/Cargo.toml index 6e74e479..97865f7a 100644 --- a/crates/tinymist-query/Cargo.toml +++ b/crates/tinymist-query/Cargo.toml @@ -69,6 +69,7 @@ sha2 = { version = "0.10" } hex = { version = "0.4" } [features] +local-registry = ["tinymist-world/system"] [lints] workspace = true diff --git a/crates/tinymist-query/src/analysis/completion/typst_specific.rs b/crates/tinymist-query/src/analysis/completion/typst_specific.rs index e76777cb..92f6c5eb 100644 --- a/crates/tinymist-query/src/analysis/completion/typst_specific.rs +++ b/crates/tinymist-query/src/analysis/completion/typst_specific.rs @@ -34,13 +34,16 @@ impl CompletionPair<'_, '_, '_> { .iter() .map(|(spec, desc)| (spec, desc.clone())) .collect(); - // local_packages to references and add them to the packages - let local_packages_refs = self.worker.ctx.local_packages(); - packages.extend( - local_packages_refs - .iter() - .map(|spec| (spec, Some(eco_format!("{} v{}", spec.name, spec.version)))), - ); + #[cfg(feature = "http-registry")] + { + // local_packages to references and add them to the packages + let local_packages_refs = self.worker.ctx.local_packages(); + packages.extend( + local_packages_refs + .iter() + .map(|spec| (spec, Some(eco_format!("{} v{}", spec.name, spec.version)))), + ); + } packages.sort_by_key(|(spec, _)| (&spec.namespace, &spec.name, Reverse(spec.version))); if !all_versions { diff --git a/crates/tinymist-query/src/analysis/global.rs b/crates/tinymist-query/src/analysis/global.rs index c5272e19..c85b2f19 100644 --- a/crates/tinymist-query/src/analysis/global.rs +++ b/crates/tinymist-query/src/analysis/global.rs @@ -672,7 +672,7 @@ impl SharedContext { } /// Get the local packages and their descriptions. - #[cfg(not(target_arch = "wasm32"))] + #[cfg(feature = "local-registry")] pub fn local_packages(&self) -> EcoVec { crate::package::list_package_by_namespace(&self.world.registry, eco_format!("local")) .into_iter() @@ -681,7 +681,7 @@ impl SharedContext { } /// Get the local packages and their descriptions. - #[cfg(target_arch = "wasm32")] + #[cfg(not(feature = "local-registry"))] pub fn local_packages(&self) -> EcoVec { eco_vec![] } diff --git a/crates/tinymist-query/src/lsp_typst_boundary.rs b/crates/tinymist-query/src/lsp_typst_boundary.rs index 96a72f35..c24438ab 100644 --- a/crates/tinymist-query/src/lsp_typst_boundary.rs +++ b/crates/tinymist-query/src/lsp_typst_boundary.rs @@ -43,14 +43,14 @@ pub fn path_res_to_url(path: PathResolution) -> anyhow::Result { } /// 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" { // typst converts an empty path to `Path::new("/")`, which is undesirable. if !uri.has_host() && uri.path() == "/" { return PathBuf::from("/untitled/nEoViM-BuG"); } - return url_to_file_path(&uri); + return url_to_file_path(uri); } 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(); } - url_to_file_path(&uri) + url_to_file_path(uri) } #[cfg(not(target_arch = "wasm32"))] @@ -114,7 +114,7 @@ mod test { assert_eq!(uri.scheme(), "untitled"); 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()); } @@ -122,7 +122,7 @@ mod test { fn unnamed_buffer() { // https://github.com/neovim/nvim-lspconfig/pull/2226 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")); let uri2 = path_to_url(&path).unwrap(); diff --git a/crates/tinymist-query/src/package.rs b/crates/tinymist-query/src/package.rs index 38d64ad1..36583943 100644 --- a/crates/tinymist-query/src/package.rs +++ b/crates/tinymist-query/src/package.rs @@ -73,7 +73,7 @@ pub fn check_package(ctx: &mut LocalContext, spec: &PackageInfo) -> StrResult<() Ok(()) } -#[cfg(not(target_arch = "wasm32"))] +#[cfg(feature = "local-registry")] /// Get the packages in namespaces and their descriptions. pub fn list_package_by_namespace( registry: &tinymist_world::package::registry::HttpRegistry, diff --git a/crates/tinymist-world/src/font/memory.rs b/crates/tinymist-world/src/font/memory.rs index 5fd80a13..4780851b 100644 --- a/crates/tinymist-world/src/font/memory.rs +++ b/crates/tinymist-world/src/font/memory.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use rayon::iter::ParallelIterator; +use rayon::iter::{IntoParallelIterator, ParallelIterator}; use typst::foundations::Bytes; use typst::text::{FontBook, FontInfo}; @@ -25,17 +25,41 @@ impl MemoryFontSearcher { 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. pub fn add_memory_font(&mut self, data: Bytes) { self.add_memory_fonts(rayon::iter::once(data)); } /// Adds in-memory fonts. - pub fn add_memory_fonts(&mut self, data: impl ParallelIterator) { + pub fn add_memory_fonts(&mut self, data: impl IntoParallelIterator) { let source = DataSource::Memory(MemoryDataSource { name: "".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. diff --git a/crates/tinymist-world/src/font/web/mod.rs b/crates/tinymist-world/src/font/web/mod.rs index e6931799..1571cabf 100644 --- a/crates/tinymist-world/src/font/web/mod.rs +++ b/crates/tinymist-world/src/font/web/mod.rs @@ -1,14 +1,19 @@ +use std::borrow::Cow; + use js_sys::ArrayBuffer; +use rayon::iter::{IntoParallelIterator, ParallelIterator}; use tinymist_std::error::prelude::*; use typst::foundations::Bytes; 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 super::{BufferFontLoader, FontLoader, FontResolverImpl, FontSlot}; +use crate::config::CompileFontOpts; use crate::font::cache::FontInfoCache; use crate::font::info::typst_typographic_family; +use crate::font::memory::MemoryFontSearcher; /// Destructures a JS `[key, value]` pair into a tuple of [`Deserializer`]s. pub(crate) fn convert_pair(pair: JsValue) -> (JsValue, JsValue) { @@ -365,71 +370,47 @@ impl FontLoader for WebFontLoader { /// Searches for fonts. pub struct BrowserFontSearcher { - pub fonts: Vec<(FontInfo, FontSlot)>, + /// The base font searcher. + base: MemoryFontSearcher, } impl BrowserFontSearcher { - /// Create a new, empty browser searcher. + /// Creates a new, empty browser searcher. 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 { - 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 } + let base = MemoryFontSearcher::from_resolver(resolver); + Self { base } } - /// Create a new browser searcher with fonts cloned from 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. + /// Builds a FontResolverImpl. pub fn build(self) -> FontResolverImpl { - let (info, slots): (Vec, Vec) = self.fonts.into_iter().unzip(); - - let book = FontBook::from_infos(info); - - FontResolverImpl::new(vec![], book, slots) + self.base.build() } } 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")] + #[deprecated(note = "use `typst_assets::fonts` directly")] pub fn add_embedded(&mut self) { for font_data in typst_assets::fonts() { 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) { + self.base.add_memory_fonts(data); + } + pub async fn add_web_fonts(&mut self, fonts: js_sys::Array) -> Result<()> { 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)?; for (i, info) in font_info.into_iter().enumerate() { - let index = self.fonts.len(); - self.fonts.push(( + let index = self.base.fonts.len(); + self.base.fonts.push(( info.clone(), FontSlot::new(WebFontLoader { font: WebFont { @@ -470,7 +456,7 @@ impl BrowserFontSearcher { pub fn add_font_data(&mut self, buffer: Bytes) { for (i, info) in FontInfo::iter(buffer.as_slice()).enumerate() { let buffer = buffer.clone(); - self.fonts.push(( + self.base.fonts.push(( info, FontSlot::new(BufferFontLoader { buffer: Some(buffer), @@ -481,7 +467,7 @@ impl BrowserFontSearcher { } pub fn with_fonts_mut(&mut self, func: impl FnOnce(&mut Vec<(FontInfo, FontSlot)>)) { - func(&mut self.fonts); + func(&mut self.base.fonts); } } diff --git a/crates/tinymist-core/.gitignore b/crates/tinymist/.gitignore similarity index 100% rename from crates/tinymist-core/.gitignore rename to crates/tinymist/.gitignore diff --git a/crates/tinymist/Cargo.toml b/crates/tinymist/Cargo.toml index 4bb7109d..3863174d 100644 --- a/crates/tinymist/Cargo.toml +++ b/crates/tinymist/Cargo.toml @@ -2,7 +2,7 @@ name = "tinymist" description = "An integrated language service for Typst." categories = ["compilers", "command-line-utilities"] -keywords = ["cli", "lsp", "language", "typst"] +keywords = ["api", "language", "typst"] authors.workspace = true version.workspace = true license.workspace = true @@ -11,6 +11,9 @@ homepage.workspace = true repository.workspace = true rust-version.workspace = true +[lib] +crate-type = ["cdylib", "rlib"] + [dependencies] anyhow.workspace = true async-trait.workspace = true @@ -25,29 +28,20 @@ clap_mangen.workspace = true crossbeam-channel.workspace = true codespan-reporting.workspace = true comemo.workspace = true +dapts.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 +open = { workspace = true, optional = true } parking_lot.workspace = true paste.workspace = true rayon.workspace = true reflexo.workspace = true -reflexo-typst = { workspace = true, features = ["system", "svg"] } +reflexo-typst = { workspace = true, features = ["svg"] } reflexo-vec2svg.workspace = true rpds.workspace = true serde.workspace = true @@ -56,12 +50,15 @@ serde_yaml.workspace = true strum.workspace = true sync-ls = { workspace = true, features = ["lsp", "server"] } tinymist-assets = { workspace = true } +tinymist-debug = { workspace = true, optional = true } +tinymist-l10n.workspace = true tinymist-query.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-render.workspace = true -tokio = { workspace = true, features = ["rt-multi-thread", "io-std"] } +tinymist-task.workspace = true +tokio = { workspace = true } tokio-util.workspace = true toml.workspace = true ttf-parser.workspace = true @@ -71,78 +68,73 @@ typst-svg.workspace = true typst-pdf.workspace = true typst-render.workspace = true typst-timing.workspace = true -typst-html = { workspace = true, optional = 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 +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] -default = [ - "cli", - "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"] +console_error_panic_hook = { version = "0.1.2", optional = true } +js-sys = { version = "0.3.77", optional = true } +wasm-bindgen = { version = "0.2.100", optional = true } [dev-dependencies] temp-env.workspace = true [build-dependencies] anyhow.workspace = true -cargo_metadata = "0.18.0" 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] workspace = true diff --git a/crates/tinymist-core/build.rs b/crates/tinymist/build.rs similarity index 100% rename from crates/tinymist-core/build.rs rename to crates/tinymist/build.rs diff --git a/crates/tinymist-core/package.json b/crates/tinymist/package.json similarity index 70% rename from crates/tinymist-core/package.json rename to crates/tinymist/package.json index d3926490..2bfda616 100644 --- a/crates/tinymist-core/package.json +++ b/crates/tinymist/package.json @@ -9,19 +9,19 @@ "Typst" ], "type": "module", - "module": "pkg/tinymist_core.js", - "types": "pkg/tinymist_core.d.ts", + "module": "pkg/tinymist.js", + "types": "pkg/tinymist.d.ts", "files": [ - "pkg/tinymist_core_bg.wasm", - "pkg/tinymist_core_bg.wasm.d.ts", - "pkg/tinymist_core_bg.js", - "pkg/tinymist_core.js", - "pkg/tinymist_core.d.ts" + "pkg/tinymist_bg.wasm", + "pkg/tinymist_bg.wasm.d.ts", + "pkg/tinymist_bg.js", + "pkg/tinymist.js", + "pkg/tinymist.d.ts" ], "scripts": { - "build:dev": "wasm-pack build --target web --dev -- --no-default-features --features web", - "build:node": "wasm-pack build --target nodejs -- --no-default-features --features web", - "build": "wasm-pack build --target web -- --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,no-content-hint", + "build": "wasm-pack build --target web -- --no-default-features --features web,no-content-hint", "publish:dry": "npm publish --dry-run", "publish:lib": "npm publish || exit 0", "test:chrome": "wasm-pack test --chrome --headless --release", diff --git a/crates/tinymist/src/cmd.rs b/crates/tinymist/src/cmd.rs index f3a56e2a..deef2871 100644 --- a/crates/tinymist/src/cmd.rs +++ b/crates/tinymist/src/cmd.rs @@ -1,12 +1,13 @@ //! Tinymist LSP commands -use std::ops::{Deref, Range}; +use std::ops::Range; use std::path::PathBuf; use lsp_types::TextDocumentIdentifier; -use serde::{Deserialize, Serialize}; +use serde::Deserialize; use serde_json::Value as JsonValue; use sync_ls::RequestId; +#[cfg(feature = "trace")] use task::TraceParams; use tinymist_assets::TYPST_PREVIEW_HTML; use tinymist_project::{ @@ -17,14 +18,20 @@ use tinymist_query::package::PackageInfo; use tinymist_query::{LocalContextGuard, LspRange}; use tinymist_std::error::prelude::*; use tinymist_task::ExportMarkdownTask; -use typst::diag::{eco_format, EcoString, StrResult}; -use typst::syntax::package::{PackageSpec, VersionlessPackageSpec}; +use typst::diag::{eco_format, StrResult}; use typst::syntax::{LinkedNode, Source}; use world::TaskInputs; use super::*; use crate::lsp::query::{run_query, LspClientExt}; 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; /// See [`ProjectTask`]. @@ -416,10 +423,11 @@ impl ServerState { } /// Initialize a new template. + #[cfg(feature = "system")] pub fn init_template(&mut self, mut args: Vec) -> AnySchedulableResponse { use crate::tool::package::{self, TemplateSource}; - #[derive(Debug, Serialize)] + #[derive(Debug, serde::Serialize)] #[serde(rename_all = "camelCase")] struct InitResult { entry_path: PathBuf, @@ -466,6 +474,7 @@ impl ServerState { } /// Get the entry of a template. + #[cfg(feature = "system")] pub fn get_template_entry(&mut self, mut args: Vec) -> AnySchedulableResponse { use crate::tool::package::{self, TemplateSource}; @@ -528,7 +537,9 @@ impl ServerState { } /// Get the trace data of the document. + #[cfg(feature = "trace")] pub fn get_document_trace(&mut self, mut args: Vec) -> AnySchedulableResponse { + use std::ops::Deref; let path = get_arg!(args[0] as PathBuf).into(); // get path to self program @@ -573,6 +584,7 @@ impl ServerState { } /// Start to get the trace data of the server. + #[cfg(feature = "trace")] pub fn start_server_trace(&mut self, _args: Vec) -> AnySchedulableResponse { let task_cell = &mut self.server_trace; if task_cell @@ -595,6 +607,7 @@ impl ServerState { } /// Stop getting the trace data of the server. + #[cfg(feature = "trace")] pub fn stop_server_trace(&mut self, _args: Vec) -> AnySchedulableResponse { let task_cell = &mut self.server_trace; if task_cell @@ -671,6 +684,7 @@ impl ServerState { } /// Get directory of packages + #[cfg(feature = "system")] pub fn resource_package_dirs(&mut self, _arguments: Vec) -> AnySchedulableResponse { let snap = self.snapshot().map_err(internal_error)?; just_future(async move { @@ -681,6 +695,7 @@ impl ServerState { } /// Get writable directory of packages + #[cfg(feature = "system")] pub fn resource_local_package_dir( &mut self, _arguments: Vec, @@ -694,6 +709,7 @@ impl ServerState { } /// Get writable directory of packages + #[cfg(feature = "system")] pub fn resource_package_by_ns( &mut self, mut arguments: Vec, diff --git a/crates/tinymist/src/config.rs b/crates/tinymist/src/config.rs index 6859f022..f52fa2dc 100644 --- a/crates/tinymist/src/config.rs +++ b/crates/tinymist/src/config.rs @@ -11,12 +11,11 @@ use reflexo_typst::{ImmutPath, TypstDict}; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value as JsonValue}; use strum::IntoEnumIterator; -use task::{ExportUserConfig, FormatUserConfig, FormatterConfig}; +use task::{FormatUserConfig, FormatterConfig}; use tinymist_l10n::DebugL10n; -use tinymist_preview::{PreviewConfig, PreviewInvertColors}; use tinymist_project::{DynAccessModel, LspAccessModel}; 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_std::error::prelude::*; use tinymist_task::ExportTarget; @@ -26,11 +25,18 @@ use typst_shim::utils::LazyHash; use super::*; use crate::project::{ - EntryResolver, ExportPdfTask, ExportTask, ImmutDict, PathPattern, ProjectResolutionKind, - ProjectTask, TaskWhen, + EntryResolver, ExportTask, ImmutDict, PathPattern, ProjectResolutionKind, TaskWhen, }; 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 const CONFIG_ITEMS: &[&str] = &[ "tinymist", @@ -172,13 +178,13 @@ impl Config { let roots = match params.workspace_folders.as_ref() { Some(roots) => roots .iter() - .filter_map(|root| root.uri.to_file_path().ok().map(ImmutPath::from)) + .map(|root| ImmutPath::from(url_to_path(&root.uri))) .collect(), #[allow(deprecated)] // `params.root_path` is marked as deprecated None => params .root_uri .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(¶ms.root_path.as_ref()?).into())) .into_iter() .collect(), @@ -510,6 +516,7 @@ impl Config { } /// Gets the preview configuration. + #[cfg(feature = "preview")] pub fn preview(&self) -> PreviewConfig { PreviewConfig { enable_partial_rendering: self.preview.partial_rendering, @@ -529,6 +536,7 @@ impl Config { } /// Gets the export configuration. + #[cfg(feature = "export")] pub(crate) fn export(&self) -> ExportUserConfig { let export = self.export_task(); ExportUserConfig { @@ -675,7 +683,7 @@ impl Config { ) } - #[cfg(target_arch = "wasm32")] + #[cfg(not(feature = "system"))] fn create_physical_access_model( &self, client: &TypedLspClient, @@ -683,7 +691,7 @@ impl Config { self.create_delegate_access_model(client) } - #[cfg(not(target_arch = "wasm32"))] + #[cfg(feature = "system")] fn create_physical_access_model( &self, _client: &TypedLspClient, @@ -874,6 +882,7 @@ pub struct PreviewFeat { #[serde(default, deserialize_with = "deserialize_null_default")] pub partial_rendering: bool, /// Invert colors for the preview. + #[cfg(feature = "preview")] #[serde(default, deserialize_with = "deserialize_null_default")] pub invert_colors: PreviewInvertColors, } @@ -971,6 +980,7 @@ where mod tests { use super::*; use serde_json::json; + #[cfg(feature = "preview")] use tinymist_preview::{PreviewInvertColor, PreviewInvertColorObject}; 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.refresh"); test_good_config("preview.partialRendering"); + #[cfg(feature = "preview")] let c = test_good_config("preview.invertColors"); + #[cfg(feature = "preview")] assert_eq!( c.preview.invert_colors, PreviewInvertColors::Enum(PreviewInvertColor::Never) @@ -1377,6 +1389,7 @@ mod tests { } #[test] + #[cfg(feature = "preview")] fn test_default_preview_config() { let config = Config::default().preview(); assert!(!config.enable_partial_rendering); @@ -1385,6 +1398,7 @@ mod tests { } #[test] + #[cfg(feature = "preview")] fn test_preview_config() { let config = Config { preview: PreviewFeat { @@ -1434,6 +1448,7 @@ mod tests { } #[test] + #[cfg(feature = "preview")] fn test_invert_colors_validation() { fn test(s: &str) -> anyhow::Result { Ok(serde_json::from_str(s)?) diff --git a/crates/tinymist/src/input.rs b/crates/tinymist/src/input.rs index f0e75b40..b660257f 100644 --- a/crates/tinymist/src/input.rs +++ b/crates/tinymist/src/input.rs @@ -5,8 +5,7 @@ use tinymist_std::error::prelude::*; use tinymist_std::ImmutPath; use typst::{diag::FileResult, syntax::Source}; -use crate::project::{Interrupt, ProjectResolutionKind}; -use crate::route::ProjectResolution; +use crate::project::Interrupt; use crate::world::vfs::{notify::MemoryEvent, FileChangeSet}; use crate::world::TaskInputs; use crate::*; @@ -14,6 +13,11 @@ use crate::*; mod client; pub use client::ClientAccessModel; +#[cfg(feature = "lock")] +use crate::project::ProjectResolutionKind; +#[cfg(feature = "lock")] +use crate::route::ProjectResolution; + /// In memory source file management. impl ServerState { /// Updates a set of source files. @@ -232,6 +236,12 @@ impl ServerState { .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 { let proj_input = matches!( self.entry_resolver().project_resolution, diff --git a/crates/tinymist/src/lib.rs b/crates/tinymist/src/lib.rs index d13bb95f..e1c63045 100644 --- a/crates/tinymist/src/lib.rs +++ b/crates/tinymist/src/lib.rs @@ -1,52 +1,75 @@ -//! # 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). - -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; +//! Tinymist Core Library pub use config::*; -pub use dap::RegularInit as DapRegularInit; -pub use dap::SuperInit as DapSuperInit; pub use lsp::init::*; pub use server::*; pub use sync_ls::LspClient; -pub use task::export2 as export; -pub use task::UserActionTask; pub use tinymist_project::world; pub use tinymist_query as query; 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 serde_json::from_value; use sync_ls::*; use utils::*; use world::*; + +/// The long version description of the library +pub static LONG_VERSION: LazyLock = 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; diff --git a/crates/tinymist/src/lsp.rs b/crates/tinymist/src/lsp.rs index bf9c4190..07bf8175 100644 --- a/crates/tinymist/src/lsp.rs +++ b/crates/tinymist/src/lsp.rs @@ -86,7 +86,7 @@ impl ServerState { impl ServerState { pub(crate) fn did_open(&mut self, params: DidOpenTextDocumentParams) -> LspResult<()> { 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_(¶ms.text_document.uri).as_path().into(); let text = params.text_document.text; self.create_source(path.clone(), text) @@ -98,14 +98,14 @@ impl ServerState { } 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)?; Ok(()) } 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_(¶ms.text_document.uri).as_path().into(); let changes = params.content_changes; 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<()> { - 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)?; Ok(()) @@ -139,9 +139,12 @@ impl ServerState { } } - let new_export_config = self.config.export(); - if old_config.export() != new_export_config { - self.change_export_config(new_export_config); + #[cfg(feature = "export")] + { + let new_export_config = self.config.export(); + if old_config.export() != new_export_config { + self.change_export_config(new_export_config); + } } if old_config.notify_status != self.config.notify_status { diff --git a/crates/tinymist/src/lsp/query.rs b/crates/tinymist/src/lsp/query.rs index 8100ba3f..748f5035 100644 --- a/crates/tinymist/src/lsp/query.rs +++ b/crates/tinymist/src/lsp/query.rs @@ -292,8 +292,8 @@ impl ServerState { .iter() .map(|f| { Some(( - as_path_(Url::parse(&f.old_uri).ok()?), - as_path_(Url::parse(&f.new_uri).ok()?), + as_path_(&Url::parse(&f.old_uri).ok()?), + as_path_(&Url::parse(&f.new_uri).ok()?), )) }) .collect::>>() @@ -326,7 +326,12 @@ impl ServerState { DocumentSymbol(req) => query_source!(self, DocumentSymbol, req)?, OnEnter(req) => query_source!(self, OnEnter, req)?, ColorPresentation(req) => CompilerQueryResponse::ColorPresentation(req.request()), + #[cfg(feature = "export")] 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(), // todo: query on dedicate projects _ => return self.query_on(query), @@ -360,6 +365,8 @@ impl ServerState { just_future(async move { stat.snap(); + // todo: preload in web + #[cfg(feature = "system")] if matches!(query, Completion(..)) { // Prefetch the package index for completion. if snap.registry().cached_index().is_none() { diff --git a/crates/tinymist/src/project.rs b/crates/tinymist/src/project.rs index 1255a11c..b457121c 100644 --- a/crates/tinymist/src/project.rs +++ b/crates/tinymist/src/project.rs @@ -19,7 +19,7 @@ #![allow(missing_docs)] -use reflexo_typst::{diag::print_diagnostics, TypstDocument}; +use reflexo_typst::TypstDocument; use serde::{Deserialize, Serialize}; pub use tinymist_project::*; @@ -42,8 +42,11 @@ use typst::{diag::FileResult, foundations::Bytes, layout::Position as TypstPosit use super::ServerState; use crate::actor::editor::{EditorRequest, ProjVersion}; use crate::stats::{CompilerQueryStats, QueryStatGuard}; +#[cfg(feature = "export")] use crate::task::ExportUserConfig; -use crate::{Config, ServerEvent}; +use crate::Config; +#[cfg(feature = "preview")] +use crate::ServerEvent; type EditorSender = mpsc::UnboundedSender; @@ -53,6 +56,7 @@ pub type LspProjectCompiler = ProjectCompiler, pub stats: CompilerQueryStats, + #[cfg(feature = "preview")] + pub preview: ProjectPreviewState, + #[cfg(feature = "export")] pub export: crate::task::ExportTask, } @@ -414,6 +433,7 @@ pub struct CompileHandlerImpl { /// language server). pub is_standalone: bool, + #[cfg(feature = "export")] pub(crate) export: crate::task::ExportTask, pub(crate) editor_tx: EditorSender, pub(crate) client: Arc, @@ -422,9 +442,11 @@ pub struct CompileHandlerImpl { pub(crate) notified_revision: Mutex>, } -pub(crate) trait ProjectClient: Send + Sync + 'static { +pub trait ProjectClient: Send + Sync + 'static { fn interrupt(&self, event: LspInterrupt); + #[cfg(feature = "preview")] fn server_event(&self, event: ServerEvent); + #[cfg(feature = "export")] fn dev_event(&self, event: DevEvent); } @@ -433,10 +455,12 @@ impl ProjectClient for LspClient { self.send_event(event); } + #[cfg(feature = "preview")] fn server_event(&self, event: ServerEvent) { self.send_event(event); } + #[cfg(feature = "export")] fn dev_event(&self, event: DevEvent) { self.send_notification::(&event); } @@ -447,10 +471,12 @@ impl ProjectClient for mpsc::UnboundedSender { self.send(event).log_error("failed to send interrupt"); } + #[cfg(feature = "preview")] fn server_event(&self, _event: ServerEvent) { log::warn!("ProjectClient: server_event is not implemented for mpsc::UnboundedSender"); } + #[cfg(feature = "export")] fn dev_event(&self, _event: DevEvent) { log::warn!( "ProjectClient: dev_event is not implemented for mpsc::UnboundedSender" @@ -696,8 +722,9 @@ impl CompileHandler for CompileHandlerImpl // Prints the diagnostics when we are running the compilation in standalone // CLI. + #[cfg(feature = "system")] if self.is_standalone { - print_diagnostics( + crate::project::system::print_diagnostics( art.world(), art.diagnostics(), reflexo_typst::DiagnosticFormat::Human, @@ -705,6 +732,7 @@ impl CompileHandler for CompileHandlerImpl .log_error("failed to print diagnostics"); } + #[cfg(feature = "export")] self.export.signal(art, &self.client); #[cfg(feature = "preview")] @@ -723,7 +751,7 @@ pub type QuerySnapWithStat = (LspQuerySnapshot, QueryStatGuard); #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub(crate) struct DevExportEvent { +pub struct DevExportEvent { pub id: String, pub when: TaskWhen, pub need_export: bool, @@ -733,7 +761,7 @@ pub(crate) struct DevExportEvent { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase", tag = "type")] -pub(crate) enum DevEvent { +pub enum DevEvent { Export(DevExportEvent), } diff --git a/crates/tinymist/src/server.rs b/crates/tinymist/src/server.rs index 21c471e3..2d93c9f3 100644 --- a/crates/tinymist/src/server.rs +++ b/crates/tinymist/src/server.rs @@ -7,31 +7,30 @@ use lsp_types::request::ShowMessageRequest; use lsp_types::*; use reflexo::debug_loc::LspPosition; use sync_ls::*; -use tinymist_query::{OnExportRequest, ServerInfoResponse}; +use tinymist_query::ServerInfoResponse; use tinymist_std::error::prelude::*; use tinymist_std::ImmutPath; -use tinymist_task::ProjectTask; use tokio::sync::mpsc; use typst::syntax::Source; use crate::actor::editor::{EditorActor, EditorRequest}; use crate::lsp::query::OnEnter; -use crate::project::{ - update_lock, CompiledArtifact, EntryResolver, LspComputeGraph, LspInterrupt, ProjectInsId, - ProjectState, PROJECT_ROUTE_USER_ACTION_PRIORITY, -}; -use crate::route::ProjectRouteState; -use crate::task::{ExportTask, FormatTask, ServerTraceTask, UserActionTask}; -use crate::world::TaskInputs; +use crate::project::{EntryResolver, LspInterrupt, ProjectInsId, ProjectState}; +use crate::task::FormatTask; 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) 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) } @@ -45,10 +44,11 @@ pub struct ServerState { pub client: TypedLspClient, // State - /// The project route state. - pub route: ProjectRouteState, /// The project state. pub project: ProjectState, + /// The project route state. + #[cfg(feature = "lock")] + pub route: ProjectRouteState, /// The preview state. #[cfg(feature = "preview")] pub preview: tool::preview::PreviewState, @@ -59,6 +59,7 @@ pub struct ServerState { pub formatter: FormatTask, /// The user action tasks running in backend, which will be scheduled by /// async runtime. + #[cfg(feature = "trace")] pub user_action: UserActionTask, // State to synchronize with the client. @@ -83,6 +84,7 @@ pub struct ServerState { /// The client ever sent manual focusing request. pub ever_manual_focusing: bool, /// The running server trace. + #[cfg(feature = "trace")] pub server_trace: Option, // Configurations @@ -115,33 +117,36 @@ impl ServerState { ); Self { - client: client.clone(), + #[cfg(feature = "dap")] + debug: crate::dap::DebugState::default(), + #[cfg(feature = "lock")] route: ProjectRouteState::default(), - project: handle, - editor_tx, - memory_changes: HashMap::new(), #[cfg(feature = "preview")] preview: tool::preview::PreviewState::new( &config, watchers, client.cast(|s| &mut s.preview), ), - #[cfg(feature = "dap")] - debug: crate::dap::DebugState::default(), + #[cfg(feature = "trace")] + 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_manual_focusing: false, sema_tokens_registered: false, formatter_registered: false, - server_trace: None, config, - pinning_by_user: false, pinning_by_preview: false, pinning_by_browsing_preview: false, focusing: None, implicit_position: None, formatter, - user_action: UserActionTask, } } @@ -216,6 +221,20 @@ impl ServerState { .with_command("tinymist.doStartBrowsingPreview", State::browse_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::(handlers::handle_cancel)? let mut provider = provider .with_request::(State::shutdown) @@ -277,12 +296,7 @@ impl ServerState { .with_command("tinymist.doClearCache", State::clear_cache) .with_command("tinymist.pinMain", State::pin_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.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.getWorkspaceLabels", State::get_workspace_labels) .with_command_("tinymist.getServerInfo", State::get_server_info) @@ -291,11 +305,8 @@ impl ServerState { .with_resource("/symbols", State::resource_symbols) .with_resource("/preview/index.html", State::resource_preview_html) .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/docs", State::resource_package_docs) - .with_resource("/dir/package", State::resource_package_dirs) - .with_resource("/dir/package/local", State::resource_local_package_dir); + .with_resource("/package/docs", State::resource_package_docs); // todo: generalize me provider.args.add_commands( @@ -310,6 +321,7 @@ impl ServerState { } /// Installs DAP handlers to the language server. + #[cfg(feature = "dap")] pub fn install_dap + 'static>( provider: DapBuilder, ) -> DapBuilder { @@ -445,62 +457,6 @@ impl ServerState { 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::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] @@ -509,11 +465,11 @@ fn test_as_path() { use std::path::Path; 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(); assert_eq!( - as_path_(uri), + as_path_(&uri), Path::new("/untitled/path/to/file with space").clean() ); } diff --git a/crates/tinymist/src/task/export.rs b/crates/tinymist/src/task/export.rs index 56946345..2af72a63 100644 --- a/crates/tinymist/src/task/export.rs +++ b/crates/tinymist/src/task/export.rs @@ -4,10 +4,13 @@ use std::path::PathBuf; use std::str::FromStr; use std::sync::atomic::AtomicUsize; use std::sync::{Arc, OnceLock}; +use std::{ops::DerefMut, pin::Pin}; use reflexo::ImmutPath; 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::fs::paths::write_atomic; use tinymist_std::path::PathClean; @@ -18,24 +21,102 @@ use typlite::{Format, Typlite}; use typst::foundations::IntoValue; 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::{ - ApplyProjectTask, CompiledArtifact, DevEvent, DevExportEvent, EntryReader, ExportHtmlTask, - ExportPdfTask, ExportPngTask, ExportSvgTask, ExportTask as ProjectExportTask, ExportTeXTask, - ExportTextTask, LspCompiledArtifact, ProjectClient, ProjectTask, QueryTask, TaskWhen, + update_lock, ApplyProjectTask, CompiledArtifact, DevEvent, DevExportEvent, EntryReader, + ExportHtmlTask, ExportPdfTask, ExportPngTask, ExportSvgTask, ExportTask as ProjectExportTask, + 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}; +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::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)] pub struct ExportTask { + /// The handle running the task. pub handle: tokio::runtime::Handle, + /// The editor request sender. pub editor_tx: Option>, + /// The task factory for export. pub factory: SyncTaskFactory, export_folder: FutureFolder, count_word_folder: FutureFolder, } impl ExportTask { + /// Creates a new export task. pub fn new( handle: tokio::runtime::Handle, editor_tx: Option>, @@ -50,6 +131,7 @@ impl ExportTask { } } + /// Changes the export configuration. pub fn change_config(&self, config: ExportUserConfig) { self.factory.mutate(|data| *data = config); } @@ -162,6 +244,7 @@ impl ExportTask { Some(()) } + /// Exports a document. pub async fn do_export( task: ProjectTask, artifact: LspCompiledArtifact, @@ -489,6 +572,75 @@ fn serialize(data: &impl serde::Serialize, format: &str, pretty: bool) -> Result }) } +type FoldFuture = Pin> + Send>>; + +#[derive(Default)] +struct FoldingState { + running: bool, + task: Option<(usize, FoldFuture)>, +} + +#[derive(Clone, Default)] +struct FutureFolder { + state: Arc>, +} + +impl FutureFolder { + async fn compute<'scope, OP, R: Send + 'static>(op: OP) -> Result + 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 + 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)] mod tests { use clap::Parser; diff --git a/crates/tinymist/src/task/mod.rs b/crates/tinymist/src/task/mod.rs index 0c9cf8c8..1d2ecc74 100644 --- a/crates/tinymist/src/task/mod.rs +++ b/crates/tinymist/src/task/mod.rs @@ -2,21 +2,22 @@ //! [`SyncTaskFactory`] can hold *mutable* configuration but the mutations don't //! blocking the computation, i.e. the mutations are non-blocking. +#[cfg(feature = "export")] mod export; +#[cfg(feature = "export")] pub use export::*; +#[cfg(feature = "export")] pub mod export2; mod format; pub use format::*; +#[cfg(feature = "trace")] mod user_action; +#[cfg(feature = "trace")] 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 tinymist_std::error::prelude::*; /// Please uses this if you believe all mutations are fast #[derive(Clone, Default)] @@ -40,72 +41,3 @@ impl SyncTaskFactory { self.0.read().unwrap().clone() } } - -type FoldFuture = Pin> + Send>>; - -#[derive(Default)] -struct FoldingState { - running: bool, - task: Option<(usize, FoldFuture)>, -} - -#[derive(Clone, Default)] -struct FutureFolder { - state: Arc>, -} - -impl FutureFolder { - async fn compute<'scope, OP, R: Send + 'static>(op: OP) -> Result - 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 + 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; - } - }) - } -} diff --git a/crates/tinymist/src/tool/mod.rs b/crates/tinymist/src/tool/mod.rs index 98a3fec0..0a48bed2 100644 --- a/crates/tinymist/src/tool/mod.rs +++ b/crates/tinymist/src/tool/mod.rs @@ -3,7 +3,6 @@ pub mod ast; pub mod package; pub mod project; -pub mod testing; pub mod word_count; #[cfg(feature = "preview")] diff --git a/crates/tinymist/src/tool/preview.rs b/crates/tinymist/src/tool/preview.rs index ad8afe86..9c90eb47 100644 --- a/crates/tinymist/src/tool/preview.rs +++ b/crates/tinymist/src/tool/preview.rs @@ -9,7 +9,7 @@ mod http; use std::{collections::HashMap, path::Path, sync::Arc}; use clap::{Parser, ValueEnum}; -use futures::{SinkExt, StreamExt, TryStreamExt}; +use futures::{SinkExt, TryStreamExt}; use hyper_tungstenite::{tungstenite::Message, HyperWebsocket, HyperWebsocketStream}; use lsp_types::notification::Notification; use lsp_types::Url; @@ -27,8 +27,7 @@ use tinymist_std::error::IgnoreLogging; use tokio::sync::{mpsc, oneshot}; use crate::actor::preview::{PreviewActor, PreviewRequest, PreviewTab}; -use crate::project::{ProjectInsId, ProjectPreviewState, WorldProvider}; -use crate::tool::project::{start_project, ProjectOpts, StartProjectResult}; +use crate::project::{ProjectInsId, ProjectPreviewState}; use crate::*; /// The kind of the preview. @@ -489,6 +488,7 @@ impl PreviewState { is_primary, }; + #[cfg(feature = "open")] if open_in_browser { open::that_detached(format!("http://127.0.0.1:{}", addr.port())) .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 = 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::(&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; impl Notification for ScrollSource { @@ -750,7 +602,11 @@ fn send_show_document(client: &TypedLspClient, s: &DocToSrcJumpInf ); } -fn bind_streams(previewer: &mut Previewer, websocket_rx: mpsc::UnboundedReceiver) { +/// Bind the hyper websocket streams to the previewer. +pub fn bind_streams( + previewer: &mut Previewer, + websocket_rx: mpsc::UnboundedReceiver, +) { previewer.start_data_plane( websocket_rx, |conn: Result| { diff --git a/crates/tinymist/src/tool/preview/compile.rs b/crates/tinymist/src/tool/preview/compile.rs index 3419f78f..62eead26 100644 --- a/crates/tinymist/src/tool/preview/compile.rs +++ b/crates/tinymist/src/tool/preview/compile.rs @@ -23,7 +23,7 @@ pub struct ProjectPreviewHandler { /// The project id. pub project_id: ProjectInsId, /// The connection to the compiler compiling projects (language server). - pub(crate) client: Box, + pub client: Box, } impl ProjectPreviewHandler { diff --git a/crates/tinymist/src/tool/project.rs b/crates/tinymist/src/tool/project.rs index 504ab662..a81420c1 100644 --- a/crates/tinymist/src/tool/project.rs +++ b/crates/tinymist/src/tool/project.rs @@ -1,424 +1,17 @@ //! Project management tools. -use std::{ - borrow::Cow, - path::{Path, PathBuf}, - sync::Arc, -}; +use std::sync::Arc; -use clap_complete::Shell; use parking_lot::Mutex; -use reflexo::{path::unix_slash, ImmutPath}; -use reflexo_typst::WorldComputeGraph; use tinymist_query::analysis::Analysis; -use tinymist_std::{bail, error::prelude::*}; use tokio::sync::mpsc; -use crate::{actor::editor::EditorRequest, world::system::print_diagnostics, Config}; -use crate::{project::*, task::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, -} - -/// 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, - /// The path to the output script. - #[clap(short, long)] - pub output: Option, -} - -#[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, - - /// When to run the task - #[arg(long = "when")] - pub when: Option, - - /// Preview arguments - #[clap(flatten)] - pub preview: PreviewArgs, -} - -#[cfg(feature = "preview")] -trait LockFileExt { - fn preview(&mut self, doc_id: Id, args: &TaskPreviewArgs) -> Result; -} - -#[cfg(feature = "preview")] -impl LockFileExt for LockFile { - fn preview(&mut self, doc_id: Id, args: &TaskPreviewArgs) -> Result { - 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 { - 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>); - - impl CmdBuilder { - fn new() -> Self { - Self(vec![]) - } - - fn extend(&mut self, args: impl IntoIterator>>) { - for arg in args { - self.0.push(arg.into()); - } - } - - fn push(&mut self, arg: impl Into>) { - 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(()) - } - } - }) -} +use crate::project::*; +use crate::{actor::editor::EditorRequest, Config}; +/// Options for starting a project. #[derive(Default)] -pub(crate) struct ProjectOpts { +pub struct ProjectOpts { /// The tokio runtime handle. pub handle: Option, /// The shared preview state. @@ -426,21 +19,26 @@ pub(crate) struct ProjectOpts { /// The shared config. pub config: Config, /// The shared preview state. + #[cfg(feature = "preview")] pub preview: ProjectPreviewState, /// The export target. pub export_target: ExportTarget, } -pub(crate) struct StartProjectResult { +/// Result of starting a project. +pub struct StartProjectResult { + /// A future service that runs the project. pub service: WatchService, + /// The interrupt sender. pub intr_tx: mpsc::UnboundedSender, + /// The editor request receiver. pub editor_rx: mpsc::UnboundedReceiver, } // todo: This is only extracted from the `tinymist preview` command, and we need // to abstract it in future. -/// Start a project with the given universe. -pub(crate) fn start_project( +/// Starts a project with the given universe. +pub fn start_project( verse: LspUniverse, opts: Option, intr_handler: F, @@ -453,23 +51,37 @@ where ), { let opts = opts.unwrap_or_default(); + #[cfg(any(feature = "export", feature = "system"))] let handle = opts.handle.unwrap_or_else(tokio::runtime::Handle::current); + let _ = opts.config; + // type EditorSender = mpsc::UnboundedSender; let (editor_tx, editor_rx) = mpsc::unbounded_channel(); let (intr_tx, intr_rx) = tokio::sync::mpsc::unbounded_channel(); // todo: unify filesystem watcher - let (dep_tx, dep_rx) = tokio::sync::mpsc::unbounded_channel(); - let fs_intr_tx = intr_tx.clone(); - handle.spawn(watch_deps(dep_rx, move |event| { - fs_intr_tx.interrupt(LspInterrupt::Fs(event)); - })); + let (dep_tx, dep_rx) = mpsc::unbounded_channel(); + // todo: notify feature? + #[cfg(feature = "system")] + { + let fs_intr_tx = intr_tx.clone(); + handle.spawn(watch_deps(dep_rx, move |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 let compile_handle = Arc::new(CompileHandlerImpl { + #[cfg(feature = "preview")] preview: opts.preview, is_standalone: true, + #[cfg(feature = "export")] export: crate::task::ExportTask::new(handle, Some(editor_tx.clone()), opts.config.export()), editor_tx, client: Arc::new(intr_tx.clone()), @@ -502,7 +114,9 @@ where } } -pub(crate) struct WatchService { +/// A service that watches for project changes and compiles them. +pub struct WatchService { + /// The project compiler. pub compiler: LspProjectCompiler, intr_rx: tokio::sync::mpsc::UnboundedReceiver, intr_handler: F, @@ -517,6 +131,7 @@ where ) + Send + 'static, { + /// Runs the project service. pub async fn run(self) { let Self { mut compiler, diff --git a/crates/tinymist/src/utils.rs b/crates/tinymist/src/utils.rs index 0bc10532..ee6ed407 100644 --- a/crates/tinymist/src/utils.rs +++ b/crates/tinymist/src/utils.rs @@ -1,12 +1,19 @@ use core::fmt; +#[cfg(feature = "system")] use std::pin::Pin; +#[cfg(feature = "system")] use std::sync::atomic::AtomicU64; +#[cfg(feature = "system")] use std::sync::Arc; +#[cfg(feature = "system")] use std::task::{Context, Poll}; - +#[cfg(feature = "system")] use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; +#[cfg(feature = "system")] use tokio::net::TcpStream; +#[cfg(feature = "system")] +use tokio_util::sync::CancellationToken; #[derive(Clone)] pub struct Derived(pub T); @@ -52,7 +59,6 @@ macro_rules! get_arg_or_default { }}; } pub(crate) use get_arg_or_default; -use tokio_util::sync::CancellationToken; pub fn try_(f: impl FnOnce() -> Option) -> Option { f() @@ -62,17 +68,11 @@ pub fn try_or(f: impl FnOnce() -> Option, default: T) -> T { f().unwrap_or(default) } -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); - }); -} - +#[cfg(feature = "system")] #[derive(Default)] pub(crate) struct AliveLock(Arc); +#[cfg(feature = "system")] impl AliveLock { pub fn hold(cnt: Arc) -> Self { let held = cnt.fetch_add(1, std::sync::atomic::Ordering::SeqCst); @@ -81,6 +81,7 @@ impl AliveLock { } } +#[cfg(feature = "system")] impl Drop for AliveLock { fn drop(&mut self) { 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 { stream: TcpStream, pub cancel: CancellationToken, } +#[cfg(feature = "system")] impl ConnWithCancel { pub fn new(stream: TcpStream) -> Self { Self { @@ -102,12 +105,14 @@ impl ConnWithCancel { } } +#[cfg(feature = "system")] impl Drop for ConnWithCancel { fn drop(&mut self) { self.cancel.cancel() } } +#[cfg(feature = "system")] impl AsyncRead for ConnWithCancel { fn poll_read( self: Pin<&mut Self>, @@ -118,6 +123,7 @@ impl AsyncRead for ConnWithCancel { } } +#[cfg(feature = "system")] impl AsyncWrite for ConnWithCancel { fn poll_write( self: Pin<&mut Self>, diff --git a/crates/tinymist-core/src/web.rs b/crates/tinymist/src/web.rs similarity index 100% rename from crates/tinymist-core/src/web.rs rename to crates/tinymist/src/web.rs diff --git a/crates/tinymist-core/tests/simple.mjs b/crates/tinymist/tests/simple.mjs similarity index 52% rename from crates/tinymist-core/tests/simple.mjs rename to crates/tinymist/tests/simple.mjs index 100f9e04..1b9c4485 100644 --- a/crates/tinymist-core/tests/simple.mjs +++ b/crates/tinymist/tests/simple.mjs @@ -1,8 +1,8 @@ -import tinymist_init from "../pkg/tinymist_core.js"; -import * as tinymist from "../pkg/tinymist_core.js"; +import tinymist_init from "../pkg/tinymist.js"; +import * as tinymist from "../pkg/tinymist.js"; import fs from "fs"; -const wasmData = fs.readFileSync("pkg/tinymist_core_bg.wasm"); +const wasmData = fs.readFileSync("pkg/tinymist_bg.wasm"); async function main() { await tinymist_init({ diff --git a/editors/vscode/.vscodeignore b/editors/vscode/.vscodeignore index b578f5e5..e8439582 100644 --- a/editors/vscode/.vscodeignore +++ b/editors/vscode/.vscodeignore @@ -5,7 +5,7 @@ !out/tinymist-docs.pdf !out/extension.js !out/extension.web.js -!out/tinymist_core_bg.wasm +!out/tinymist_bg.wasm !out/tinymist !out/tinymist.exe !out/typst.tmLanguage.json diff --git a/package.json b/package.json index 90dc6c52..7198c003 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "type": "module", "license": "Apache-2.0", "workspaces": [ - "crates/tinymist-core", + "crates/tinymist", "editors/vscode", "contrib/typst-preview/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:l10n": "yarn extract:l10n && node scripts/build-l10n.mjs", "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: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", diff --git a/scripts/feature-testing.sh b/scripts/feature-testing.sh index cb6a68df..f61d5014 100755 --- a/scripts/feature-testing.sh +++ b/scripts/feature-testing.sh @@ -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,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 diff --git a/scripts/nightly-utils.mjs b/scripts/nightly-utils.mjs index 4df3c42f..9e0e0526 100644 --- a/scripts/nightly-utils.mjs +++ b/scripts/nightly-utils.mjs @@ -207,8 +207,8 @@ class NightlyUtils { 'typlite', 'typst-shim', 'sync-lsp', + 'tinymist-cli', 'tinymist-analysis', - 'tinymist-core', 'tinymist-debug', 'tinymist-l10n', 'tinymist-package', @@ -249,7 +249,7 @@ class NightlyUtils { await this.ensureInit(); 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' ]; await this.updateDependencies(nonWorldCrates, newVersion); @@ -265,7 +265,7 @@ class NightlyUtils { async updateVersionFiles(newVersion) { const jsonFiles = [ 'contrib/html/editors/vscode/package.json', - 'crates/tinymist-core/package.json', + 'crates/tinymist/package.json', 'editors/vscode/package.json', 'syntaxes/textmate/package.json' ];