Skip to main content

latexml/lsp_server/
mod.rs

1//! Persistent server for editor/preview integration (`latexml_oxide --server`).
2//!
3//! This is a JSON-RPC-over-stdio server using LSP message framing. It speaks a
4//! subset of LSP (`initialize`, `textDocument/did{Open,Change,Close}` →
5//! `publishDiagnostics`, `shutdown`, `exit`) **plus** a custom
6//! `latexml/convert` request that returns `{html, log, diagnostics, sources,
7//! status, statusCode}` — the response shape the `ar5iv-editor` client
8//! consumes for its live source↔preview loop (see `docs/performance/SOURCE_PROVENANCE.md`).
9//!
10//! Performance model: the preamble (everything up to and including
11//! `\begin{document}`) is digested once and cached in this (parent) process.
12//! Each body conversion `fork()`s a child that inherits the warm post-preamble
13//! state via copy-on-write, digests only the body, builds the DOM, and writes
14//! the result back over a pipe before exiting. The child is a throwaway, so a
15//! body conversion can never pollute the cache, and a panicking/looping body
16//! can't take down the server.
17//!
18//! Concurrency model: a **single thread** drives everything. While a body
19//! child runs, the parent `poll(2)`s `{stdin, child-pipe}`; a newer
20//! `latexml/convert` for the same document `SIGKILL`s the in-flight child
21//! (a pid we still own — reaped here, so no PID-recycle race) and supersedes
22//! it. Keeping it single-threaded is also what makes the `fork()` safe: there
23//! is no second thread that could hold the allocator lock at fork time.
24
25// The full feature set (fork preamble-cache, dep snapshots, project roots,
26// supersession) is wired only into the unix transport; `generic.rs` drives a
27// simpler blocking subset. The shared modules stay compiled on every
28// platform (their unit tests run everywhere), so on non-unix the
29// unix-only consumers read as dead code — silence exactly that, there.
30mod diagnostics;
31#[cfg_attr(not(unix), allow(dead_code))]
32mod document;
33#[cfg(not(unix))]
34mod generic;
35mod json;
36#[cfg_attr(not(unix), allow(dead_code))]
37mod overlay;
38#[cfg_attr(not(unix), allow(dead_code))]
39mod project;
40#[cfg_attr(not(unix), allow(dead_code))]
41mod protocol;
42#[cfg_attr(not(unix), allow(dead_code))]
43mod server;
44#[cfg(unix)]
45mod unix;
46
47pub(crate) use diagnostics::*;
48pub(crate) use document::*;
49pub(crate) use json::*;
50pub(crate) use overlay::*;
51pub(crate) use project::*;
52pub(crate) use protocol::*;
53pub(crate) use server::*;
54
55/// Run the server. `timeout_secs` is the per-conversion wall-clock budget
56/// (`--timeout`; 0 disables) and `max_memory_mb` the resident-memory ceiling
57/// (`--max-memory`; 0 disables). Both are applied **fresh per conversion** by
58/// the forked body child's shared [`latexml_core::watchdog::Watchdog`] — so a
59/// child never runs against the parent's stale warm-up deadline, and is reaped
60/// if it exceeds the RAM ceiling. The extension surfaces both as VSCode
61/// settings and passes them on the spawn.
62pub fn run_lsp_server(
63  timeout_secs: u64,
64  max_memory_mb: u64,
65) -> Result<(), Box<dyn std::error::Error>> {
66  let max_rss_kb = max_memory_mb.saturating_mul(1024);
67  #[cfg(unix)]
68  {
69    unix::run(timeout_secs, max_rss_kb)
70  }
71  #[cfg(not(unix))]
72  {
73    generic::run(timeout_secs, max_rss_kb)
74  }
75}