Architecture Overview
Knot is structured as a Rust workspace with four components:
knot/
├── crates/
│ ├── knot-core/ # Engine: parser, compiler, cache, executors
│ ├── knot-cli/ # knot command-line tool
│ └── knot-lsp/ # Language Server (Tinymist proxy + Knot overlays)
└── editors/
└── vscode/ # VS Code extension (TypeScript)
Component responsibilities
knot-core
The heart of the system. Everything that makes Knot work lives here:
- Parser (
parser/): Winnow combinator parser that turns.knotfiles into an AST ofNodevalues — prose, code chunks, and inline expressions. - Compiler (
compiler/): Three-pass pipeline (plan → execute → assemble). - Cache (
cache/): SHA-256 addressed persistent cache in.knot_cache/. - Executors (
executors/): Persistent R and Python subprocesses. - Backend (
backend.rs): RendersNodes into.typtext. - Project (
project.rs): Top-level API —compile_project_full,compile_project_phase0, etc.
knot-core has no tokio dependency. Concurrency is std::thread::scope.
knot-lsp
An LSP server that wraps Tinymist (the official Typst LSP) and adds Knot-specific capabilities:
- Forwards most LSP requests to a Tinymist subprocess after mapping
.knotcoordinates to virtual.typcoordinates. - Adds chunk-option completion, hover docs, hybrid formatting, and diagnostics.
- Manages streaming preview via
knot/startPreviewandknot/syncForward.
knot-cli
A thin binary over knot-core. Most commands delegate directly to project-level
functions (compile_project_full, etc.).
editors/vscode
A VS Code extension written in TypeScript. It communicates with knot-lsp via the
Language Server Protocol and adds editor UI (status bar, preview lifecycle,
auto-redirect from .typ to .knot).
Data flow: a save event
Here is the full path a didSave event takes from VS Code to a rendered PDF:
[VS Code]
didSave
│
▼
[knot-lsp — server_impl.rs::did_save]
├── increment compile_generation
├── spawn do_compile(generation)
│
└── [do_compile]
├── Phase 0: compile_project_phase0 (instant, orange placeholders)
│ └── apply_update → textDocument/didChange → Tinymist
│
├── Streaming: compile_project_full(path, Some(callback))
│ └── for each chunk executed:
│ apply_update → textDocument/didChange → Tinymist
│
└── Final: apply_update + refresh_diagnostics
→ textDocument/didChange → Tinymist
→ publishDiagnostics → VS Code
[Tinymist subprocess]
textDocument/didChange
→ recompile .typ
→ push updated PDF to browser preview
Key types
In knot-core
#![allow(unused)] fn main() { // A parsed document node enum Node { Prose(String), CodeChunk { language, options, code, … }, Inline { language, expr, … }, } // How much of the chunk's output to include enum Show { Both, Code, Output, None } // What execution work is needed enum ExecutionNeed { Skip, // eval: false CacheHit(ExecutionAttempt), // hash matched cache MustExecute, // must re-run } // The result of running (or attempting to run) a chunk enum ExecutionAttempt { Success(ExecutionOutput), RuntimeError(RuntimeError), } // Visual state used in .typ output enum ChunkExecutionState { Ready, // cache hit or just executed Inert, // suspended: upstream error Pending, // compilation in progress (orange) Modified, // direct edit, pre-save (amber, thick) ModifiedCascade, // hash-cascade, pre-save (amber, thin) } }
In knot-lsp
#![allow(unused)] fn main() { // Whether Tinymist has received didOpen for the virtual .typ enum TinymistOverlay { Inactive, Active { next_version: u64 }, } }