Scoria

How Scoria works

Useful if you are filing a bug, reading the source, or deciding how much to trust the thing.

The crates

CrateOwns
scoriaThe GPUI application: window, workspace shell, every view
scoria-vaultA vault is a folder — scanning, file kinds, .obsidian config reads, file operations
scoria-markdownObsidian Flavored Markdown: CommonMark + GFM + Obsidian syntax into one spanned tree, and the HTML renderer
scoria-cacheThe metadata cache and link resolution with Obsidian's rules
scoria-formatsOn-disk formats: file kinds, app.json and friends, JSON Canvas 1.0, Bases YAML
scoria-commandsThe 244 command ids, names and default hotkeys
scoria-searchThe search query grammar, the matcher, and fuzzy search
scoria-benchBenchmarks: the synthetic vault generator and the index timer

The one-parser rule

There is exactly one Markdown parser, and every node in its tree carries its byte span. The editor's syntax highlighting, Live Preview's display transform, the reading view, the metadata cache and the link rewriter are all built from that one tree.

This is the single most consequential decision in the project, and it is enforced as a project rule rather than a preference: adding a second tokenizer for any of those is not allowed.

What it buys:

  • What you read and what the cache believes cannot drift. A link the reading view draws is the same link the backlinks pane counted, because they came from the same parse.
  • Rewriting is surgical. A rename knows the byte range of each link that needs changing, so it can replace those bytes and leave the rest of the file alone — which is how frontmatter links get updated without re-serialising your YAML.
  • One place to be right. Obsidian's Markdown has a lot of corners (basic formatting lists a dozen), and each one is implemented once.

The parser follows CommonMark's two-phase design: a line-by-line block pass with a stack of open containers — lazy continuation, tab stops, list-item indentation — then an inline pass per paragraph, heading, callout title and table cell, using a delimiter stack for emphasis and a bracket stack for links. Obsidian's constructs are extra block starts and inline scanners in the same two passes, each behind a flag, so the CommonMark suite can run with them all switched off and still pass 652 of 652.

The metadata cache

scoria-cache is the equivalent of Obsidian's CachedMetadata. Per note it records links, embeds, tags, headings, sections with Obsidian's type names, list items with Obsidian's parent encoding, blocks, footnotes, and frontmatter links with their key paths.

Vault-wide it keeps resolvedLinks and unresolvedLinks counts per source, backlinks per occurrence, tags merged case-insensitively across inline and frontmatter, and aliases.

Link resolution lives here, including the ambiguous-basename order that a probe against a running Obsidian had to settle.

Rendering, and why both views share it

The reading view turns the parse tree into an owned render tree — styled runs with clickable ranges, resolved embeds, footnotes in reference order, frontmatter as properties — and draws it with a lazily measured list, one item per top-level block.

Live Preview is the same editor and buffer as Source mode, with a per-line display transform: from the line's highlight spans it hides markers or substitutes glyphs and returns display text plus a piece map between source and display offsets. The editor shapes the display text and maps clicks, the cursor, the selection and IME bounds back through the pieces. Source mode is the identity transform.

Block widgets — a top-level table, math block, or embed-only paragraph — are rendered by the reading view's renderer, laid out as a child element inside the editor's element. That is why an embedded note looks the same while you are writing around it as it does when you finish.

Threads

  • Scanning and indexing run on a background thread at startup, so the window appears before the vault is finished.
  • Parsing after an edit runs on a background thread, one parse in flight and one queued, with results swapped in when ready. Typing never blocks on the parser.
  • Searching runs on the background executor with rayon across the indexed note text.
  • The graph's force simulation steps on a background thread.
  • File-watcher events are drained every 300 ms, so a burst of writes is one batch.

The clean-room method

No code, CSS or icons come from Obsidian's bundle. The behavior was written down first, as a functional specification derived from observing a real Obsidian, its file formats and its public documentation. The code implements the specification.

Where the specification and Obsidian disagree, the specification is what gets corrected — and the way that happens is a throwaway plugin in a scratch vault that dumps what Obsidian actually does. That is how the command ids, names and default hotkeys were captured, how the settings tabs were enumerated, and how the link-rewriting and link-resolution rules were established. The "same folder first" rule for ambiguous basenames was wrong in the specification, and a probe is what showed it.

The ratchets

Two test suites are not allowed to go down:

  • tests/commonmark.rs — 652 of 652 CommonMark 0.31.2 examples, byte-identical HTML.
  • tests/corpus.rs — a vault of 1,000 generated notes plus nine hand-written edge cases, against the HTML Obsidian 1.13.7 produced for each. Both sides are reduced to a canonical token stream — no copy buttons, icons, MathJax output or dir attributes — before comparison. 1,007 of 1,009 match.

A third, rename_corpus.rs, renames every note in the corpus under every link format, moves its folders, and renames its attachments, asserting each time that every resolved link still resolves to the same file, that no link appears or disappears, and that nothing outside the links changed.

When Obsidian and the parser disagree on something new, the procedure is: add a probe note to the corpus vault's edge folder, re-render it with the plugin in Obsidian, match the observed behavior, and write it down in the specification.

GPUI

The interface is GPUI, the GPU-accelerated toolkit built for the Zed editor. The editor and the reading view are custom GPUI elements that shape and paint only what is visible.

gpui and gpui_platform are git dependencies pinned to a Zed release commit, because the crates.io release is stale and crashes on NVIDIA. The pin is bumped deliberately, one release at a time, and the app is run on a real GPU before the bump is committed.

Where the source is

github.com/ragtux/scoria and git.ragtux.com/ragtux/scoria. The specification and the development notes live in docs/scoria, which is itself an Obsidian vault — dogfooded by Scoria, so anything the two apps disagree about in it is a bug or a specification gap.

Next

Source: sites/scoria/content/docs/reference/architecture.md