E - The editor · · 5 min read

Building e: a code editor for the rest of us

On why I wrote my own editor, and how it came together — one small, stubborn piece at a time.

Building e: a code editor for the rest of us

There's a particular kind of afternoon where you're deep in a Laravel project, three terminals open, an AI assistant in a fourth window, your database client in a fifth — and you realize you're spending more energy arranging tools than writing code. That afternoon is where e started.

I didn't set out to build "the next VS Code." I set out to build the editor I actually wanted: fast, native, quiet, and unusually good at the specific stack I live in every day. This is the story of why, and how — with examples you can try.

Why build another editor?

Three itches, honestly.

  1. Native speed, without the ceremony. Electron editors are wonderful and I owe them years of good work. But I wanted something that opens instantly, sips battery, and feels like it's part of the machine. So e is pure Rust, GPU-accelerated, drawing its own UI. Cold start is measured in a blink, not a spinner.

  2. PHP and Laravel as first-class citizens, not plugins-on-top. Most editors treat Laravel as "PHP, plus some extensions you configure." I wanted the framework baked in — routes, views, config, translations, Blade components — understood natively.

  3. AI that actually cooperates with the editor. Not a chat box bolted onto the side. An agent that can see your cursor, reuse your language server, query your database through your existing connection, and — crucially — propose edits you review, rather than silently rewriting files.

None of those are impossible in existing editors. I just wanted them to be the center of gravity, not the periphery.

How it's built: grow it, don't scaffold it

The guiding principle was incremental honesty. Every feature had to compile, pass a test, and survive a real editing session before the next one started. No giant "framework" commit that does nothing. The project is a small Rust workspace:

e-core   → buffers, syntax (tree-sitter), git, diffing, the undo tree
e-lsp    → Language Server Protocol client
e-term   → PTY-backed terminal
e-db     → MySQL / Postgres / SQLite / ClickHouse drivers
e-app    → the UI, wiring it all together

Reactive UI, so state flows one way and the screen follows. Here's the flavour — a settings toggle that applies live and persists:

toggle_row(
    "Inlay hints",
    "Inline type & parameter hints",
    move || s.settings.get().inlay_hints,
    move |v| {
        s.settings.update(|st| st.inlay_hints = v);
        config::set_bool("inlay_hints", v);
    },
);

That's the whole pattern: read a signal, write a signal, save to disk. The screen re-renders itself.

Example 1: Laravel that the editor actually understands

Open a Blade file and type a route helper. e doesn't guess — it ran php artisan route:list for you and knows your routes:

return redirect()->route('invoices.show', $invoice);
//                        └── completes real route names,
//                            hover shows the URI + controller,
//                            ⌘-click jumps to the controller method

And here's the one that still makes me smile. Type $user-> on a model instance:

$user = User::find($id);
$user->    // ← suggests created_at, email, remember_token…
           //   real columns, read from your live database schema

That last suggestion isn't from a static stub file. e connected to your database (from .env), read the actual users table, and merged those columns in next to your language server's methods. It's the kind of thing you don't notice until you switch to an editor that can't do it.

Example 2: a branching undo tree

Every editor has undo. Almost none let you get back a branch you abandoned. In e, undo is a tree: undo a few steps, type something new, and the old path isn't thrown away — it becomes a sibling you can return to.

● now  (+40)
├─ ⑂  "the refactor I kept"   (2m ago)
└─ ⑂  "the experiment I undid" (5m ago)   ← still here, one click away

Press ⌘⌥U, click any node, and the buffer time-travels there. The whole tree is saved per file, so it survives closing the app. I've recovered "lost" work with this more times than I'd like to admit.

Example 3: search by meaning, locally

Sometimes you don't know the function name — you know what it does. Press ⌘⌥K and describe it:

│ where is the invoice email sent

e ranks locations by meaning. If you have a local embedding model running it uses real semantic search; if not, it falls back to a fast lexical index. Either way, nothing leaves your machine. Privacy isn't a setting here; it's the architecture.

Example 4: an AI agent that asks before it writes

The agent gets a local socket to cooperate with the editor. When it wants to change a file, it doesn't just write it — it proposes it, and you review hunk by hunk:

- $user->notify(new InvoiceMail($invoice));
+ Mail::to($user)->queue(new InvoiceMail($invoice));   [ accept ] [ reject ]

Accept the changes you like, reject the rest. There's an autonomous test loop too (⌘⇧T): run the suite, let the agent iterate on failures — proposing edits you approve — until it goes green. It's collaboration with a seatbelt.

The unglamorous part: shipping

A tool you can't install nicely isn't finished. So the last stretch was all the boring, important stuff: a universal .dmg (Apple Silicon and Intel), signed with a Developer ID and notarized by Apple so it opens without scary warnings. That whole pipeline now runs itself in CI on every tag:

xcrun notarytool submit e-x.y.z-universal.dmg --keychain-profile e-notary --wait
# → status: Accepted
xcrun stapler staple e-x.y.z-universal.dmg
# → The staple and validate action worked!

The app-specific password lives only in the keychain and in encrypted CI secrets — never in the repo. Details matter, especially the invisible ones.

What I've learned

  • Small commits are a feature. Every step compiling and tested meant I was never more than one git revert from a working editor.

  • Reuse beats cleverness. The command palette's fuzzy ranking is the same function as the file finder's. One good idea, two places.

  • Constraints are a gift. "Native, local, private, fast" ruled out a lot of options — and made every remaining decision easier.

e isn't trying to be everything. It's trying to be right for a particular way of working: PHP and Laravel, a terminal at hand, an AI agent that helps without taking the wheel, and a UI that gets out of the way.

The editor for the rest of us. Come build with it. ☕