← All notes

Saving a page without changing a byte

An editor that respects the file it opened.

Lettrine is a desktop editor I’m building right now. It shows the MDX pages of a Fumadocs project, a documentation framework for Next.js, as Notion blocks. Opening and saving a page without edits changes nothing, and an edit rewrites only the blocks you touched.

In the editor

docs/install.mdx

---title: Install--- Run the installer once. * Open the folder* Pick a page
Nothing saved yet.

That sounds like an implementation detail. It’s the feature. Most visual editors parse your file into a model and print a new one, and your next commit is a diff of reformatted lines that nobody asked for. If people can’t read the diff, they won’t trust the editor with their repository.

How it works

Every block remembers where it came from in the source. Saving doesn’t print a document, it follows a plan: keep this block, rewrite that one. Kept blocks are copied byte for byte, including the whitespace between them.

packages/mdx-core/src/print.ts
/**
 * Prints a document from a plan. Untouched blocks are copied byte for byte from the
 * original source, including the whitespace between consecutive untouched blocks,
 * so a plan that keeps every block reproduces the original file exactly.
 */
export function printDocument(doc: MdxDocument, plan: DocumentPlan = keepAll(doc)): string {

That promise gets tested on real projects, not on tidy examples. The corpus is more than 500 files from at least ten Fumadocs repositories, and on every single one, open and save has to come back identical.

packages/mdx-core/test/roundtrip.test.ts
it('open + save without edits is a zero-byte diff on every file', () => {
  const failures: string[] = [];
  for (const file of corpus) {
    const doc = parseMdx(file.source);
    if (printDocument(doc, keepAll(doc)) !== file.source) failures.push(file.path);
  }
  expect(failures).toEqual([]);
});

What it doesn’t understand, it keeps

Callouts, Cards, Tabs and Steps become real blocks. Anything else, like custom JSX, imports or expressions, becomes an MDX block, kept exactly as written. You can’t edit it visually, but it’s there, you can see it, and it comes out unchanged. If a whole file fails to parse, it falls back to one raw block rather than an error screen.

An editor that says “I don’t know what this is, so I left it alone” earns more trust than one that guesses.

Only what’s in the repository

My favorite rule in the project is about the interface: a block, a property or a setting that has no form in a file doesn’t belong in the editor. So there are no databases, no comments, no block colors, even though it looks like Notion. Everything you can see, you could find in the repository.

The rest follows. Page properties are the frontmatter. The sidebar is meta.json. The changes panel shows the diff and commits with your own git. AI proposals appear as a diff before anything is applied.