Conversation
Add an 'Open Preview in Browser' code action that renders the current MJML document to standalone HTML via mrml and opens it in the system browser. No new dependencies (mrml already provides render), no unsafe, in the default feature set. Zed has no webview and no workspace/executeCommand, so the action carries no command: it inserts a nonce marker comment, the server reacts to the resulting didChange (the only selection signal), renders, opens the browser, then strips the marker via workspace/applyEdit. A handled-set makes it fire exactly once per selection. Gated behind the mjml.preview.enabled initialization option (default on). Invalid MJML opens an error page instead of failing silently. preview.rs: render, error-page, browser-open, temp-file, and marker-pulse helpers (pure, unit-tested). main.rs: wire the action into the code-action response and handle the marker in didChange.
The unit-tests job ran only 'cargo test' (the root wasm crate), so mjml-lsp tests - including the new preview tests - never ran in CI. Add 'cargo test --manifest-path crates/mjml-lsp/Cargo.toml'.
Add a Preview section: how to trigger it, the single-shot behavior, the reason for the code-action + marker (Zed has no webview/executeCommand), error-page handling, and the mjml.preview.enabled opt-out.
MRML passes mj-image src and mj-font href values through unchanged, so relative paths like 'img/logo.png' resolved against the temp preview file's location and showed broken images. Inject a <base href> pointing at the source document's directory so relative resources load from where the MJML project keeps them. Absolute URLs are unaffected.
MRML parse errors can contain angle brackets (e.g. <mj-foo>), which the browser would parse as a tag and swallow. Escape &, <, > before placing the message in the error page.
If the browser can't be opened (no xdg-open on headless Linux, etc.) or the temp file can't be written, the user previously saw nothing. Send a window/showMessage with the temp file path so they can open it manually.
Serve previews from a localhost-only HTTP server instead of a static temp file. Every didChange re-renders the registered document and pushes a server-sent event; a small injected script swaps the new HTML in place, so the open tab follows the editor with scroll position intact. Relative images and fonts are served from the source directory (path-traversal guarded), replacing the base-href approach for served pages. The server is hand-rolled over std::net — a single-user localhost GET server does not justify a dependency — and the static temp file remains as a fallback when the server cannot start.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #15 — this branch includes #15's commits, so the diff shows both until it lands; only the last two commits (
a4c14c6,4fd0cf9) are new here. I'll rebase once #15 merges so this shrinks to just the live-preview change, or I can fold both into one PR if you'd rather review them together.This picks up the live-preview follow-up mentioned in #15: instead of writing a static temp file, the language server serves the preview from a localhost-only HTTP server and pushes a server-sent event on every
didChange. A small script injected into the served page fetches the fresh render and swapsdocument.documentElementin place, so the open tab follows the editor as you type — scroll position preserved, no visible flicker. Still no new dependencies, nounsafe, default feature set, behind the samemjml.preview.enabledoption.How it works:
http://127.0.0.1:<port>/<stem>/— server bound lazily on the first preview, ephemeral port, 127.0.0.1 only.didChangere-renders documents that have been previewed and bumps a version; subscribers on the SSE endpoint (/<stem>/__mjml-preview-events__) wake up and refetch.mj-image/mj-fontassets are served from the source document's directory by the same server (path-traversal guarded), replacing the<base href>injection for served pages.The server is hand-rolled over
std::net(GET-only): SSE needs no WebSocket dependency, and a single-user localhost preview didn't seem to justify pulling in an HTTP crate. Happy to swap it fortiny_httpor similar if you'd rather not own that code.Changes (on top of #15):
crates/mjml-lsp/src/preview_server.rs(new): shared state, routing, asset resolution, SSE, reload-script injection — unit- and socket-tested.crates/mjml-lsp/src/main.rs: bundle the preview trigger state with the server handle, register/refresh documents ondidChange, live-or-static open.crates/mjml-lsp/src/preview.rs:open_in_browsernow takes a URL-or-path string.README.md: Preview section update.Test plan:
cargo test --manifest-path crates/mjml-lsp/Cargo.toml(147 tests, including real-socket server tests: page serving, asset serving, traversal rejection, SSE event delivery, non-GET rejection)cargo clippy --manifest-path crates/mjml-lsp/Cargo.toml -- -D warningsdidChange→applyEditstrip → fetch the served page → edit → served page updated → broken MJML serves the error page and recovers.