The editor for
the rest of us
.
A fast, native code editor written in Rust — with first-class PHP/Laravel support, LSP, a built-in terminal and database panel, and an integrated AI agent panel running right beside your code.
Download for macOS
Grab the universal disk image, or a platform archive. New releases are detected by the built-in auto-updater.
Fast and native,
built for the web stack.
Tree-sitter highlighting, LSP, fuzzy navigation, and framework-aware completion for PHP, Laravel, Blade, Vue, Svelte and Tailwind — wrapped in a GPU-accelerated UI that stays out of your way.
GPU-accelerated & native
Written from scratch in Rust with a reactive, GPU-accelerated UI. Responsive on big files, light on resources.
Native agent chat panel
A native chat panel (⌘L) drives Elyra headless over its structured RPC (the e-agent crate) — or run Claude Code, Codex or any CLI agent beside your code. Deep co-op: reviewable propose_edit diffs, an autonomous TDD loop (⌘⇧T), and an activity timeline (⌘⌥A). ⌘⌥⇧T runs just the test the caret is in through the same panel. And since 0.9.16 an agent can wait for diagnostics: wait_diagnostics on the editor socket reloads the file the agent wrote, waits for every running language server to publish again, and answers with the problems — the write, check, fix loop without the agent guessing how long to sleep. And since 0.9.21 there is Elyra Cascade: Elyra as a chat panel instead of the terminal, drawn the way Devin Local and Windsurf's Cascade draw theirs — a session tab with this project's earlier sessions to resume, a streaming markdown transcript with tool-call cards, and a composer that attaches the active file, the selection or all open files, switches Code / Ask, and carries a model chip listing every model Elyra offers by provider with its thinking level. Underneath, Elyra runs headless over its RPC protocol, so the conversation is native views and stays fast however long it gets. API keys for Anthropic, OpenAI, Gemini and Grok go in Settings → Agents — into the macOS Keychain, never config.json, shown masked once saved — and are handed to Elyra as environment variables when Cascade starts it; a variable already exported in your shell still wins. Since 0.13.0 Cascade speaks the Agent Client Protocol, so the ⋯ menu's Agent section runs Claude Code, Codex, Gemini CLI, GitHub Copilot, Cursor, OpenCode, Goose, Qwen Code or Kimi CLI — or any ACP agent, through cascade_custom_command — in the same panel as Elyra: transcript, tool cards, turn receipts with Review and Undo, worktree sessions and the context meter. When the agent asks before acting, its question is a card in the transcript with its own choices. Until then Claude Code and Codex kept the terminal. Since 0.12.0 Cascade shows how full the context is and what the session cost — 42% context · $0.14, amber past 70% and red past 90%, a click compacting the context before the next task — and Enter while Elyra runs steers the running work, ⌥Enter queues a message to run next, and a running tool's card shows its latest output as it arrives. Since 0.11.0 the agent socket answers more: laravel_map gives in one request what an agent otherwise greps for over dozens of calls — the routes, the models with their live columns and relationships, Livewire components, Inertia pages with the props they get, views and Blade components; verify_route replays a route before and after its change and gets the verdict from the same measurement as the verify panel and the ship gate; request_test writes the test that replays a request. And diagnostics, context and wait_diagnostics now carry e's own findings — unknown columns, missing views and keys, PHPStan — where they left them out. And since 0.13.0 the socket drives the editor: commands and command run any named command, code_actions and apply_code_action work the ⌘. menu, dialog confirms or cancels what is showing, cascade does what Cascade's controls do, wait_idle waits for Elyra's turn, and ui_state reads back what is on screen, notifications included. An agent — or a test — can now use the UI end to end, and see the result, without sending keystrokes. Since 0.14.0 it clicks and presses keys as well: click clicks in the editor's window, so a test can press buttons and gutter marks, and key presses keys and text types, each a real key event, through the keyboard layout and e's key bindings. An agent or a test can now work palettes, inputs and shortcuts without Accessibility permission or System Events.
Review what the agent changed
An agent can touch forty files in a minute, and you did not write any of them. ⌘⌥V opens the whole session changeset risk-ranked — migrations, .env, config, routes, auth and dependency manifests first; lockfiles, tests and docs last — with a sign-off counter, Ask why for a single file, and Revert for just that one. No pushing a PR to review your own machine. Since 0.12.0, when Elyra finishes a turn in Cascade, a card under the reply lists the files it changed with their line counts and any review flags: Review opens the session review on just that turn, and Undo puts the turn's files back, leaving alone any changed again since or open with unsaved edits. And New session in a worktree runs Elyra on a new branch beside your checkout, with its own .env, its own copy of the database and its own Grove site (grove try), so its migrations and edits stay off your files and your database; turn receipts and the session review measure the worktree. Since 0.15.0 that no longer needs Grove: any git project gets a plain git worktree, with .env and an SQLite database copied over, where a project Grove doesn't serve was refused; Grove still gives a worktree its own site and database.
It reads the diff too
e flags what agents tend to leave behind: a forgotten dd() or console.log, a hardcoded secret, interpolated raw SQL, a dropColumn in a migration, a changed .env value, a deleted authorize() call, a skipped test, rejectUnauthorized: false. Each finding has an Ask button that sends it to the agent that wrote it. The matching is call-aware, so add( is never mistaken for dd(. Since 0.11.0 it flags unchecked lookups (IDOR): a controller method behind auth that turns a route parameter into a model someone owns — Order $order, Order::findOrFail($id) — with nothing checking it is this user's gets a warning on its line, and a flag in session review when the change touches it. Authorization, FormRequests that authorize, can:, role and signed middleware, scoping to the user, global scopes and the controller's own check helpers all count; run over 81 real projects it raised 11 warnings in 3. And guest routes no longer count as authenticated: the guest middleware has auth in its name, so the map showed a lock on guest routes and never warned that a guest write route was open. Since 0.16.0 a call to env() in a controller, service, route file or template is underlined — it returns null once the config is cached — and ⌘. reads it through config() instead: the key a config file already has for it, or a new entry in config/services.php, with the variable added to .env.example when it's missing there. The agent review flags the same call, and a config file reading a variable .env.example doesn't have. And since 0.17.0 a project writes its own rules in .e/rules.toml — what a rule says, the files it covers, and a pattern it forbids or requires in each file or each public method. e underlines a file that breaks a rule as you type, Session Review flags a change that breaks one where it changed, and an error rule blocks the ship gate. A new Cascade session's first message starts with the rules, and agents can read them, and where the project breaks them, over the socket. Project Rules: Check Project lists every place the project breaks a rule now.
A gate, then a measured pull request
A verdict bar says Ready, Notes or Needs attention — and why: files still unreviewed, tests not run, danger flags. Run tests reports 12 passed · 2 failed · 1 skipped, each failure listed by its first assertion line and clickable straight to it, because “the suite failed” and “two of ninety tests failed” are different news. Measure routes replays the routes a changeset reaches, with the change and again with your work stashed, and puts a before/after table of status, time, queries and N+1 in the PR body — cautiously: write routes are never replayed, and without Clockwork or Grove's SQL capture the query columns read not visible rather than zero. Then Commit & PR commits in logical groups in dependency order with Conventional Commits subjects and opens the PR through gh. Since 0.11.0 timing noise is no longer a verdict: two replays of an unchanged route differ by 10% easily, and a 5% margin called that improved or regressed, so a change now has to be over 15% and 10 ms. When Grove's SQL capture caught only the connection's use and SET NAMES, the verify panel and the gate reported zero queries and no N+1 as if measured; they now say queries weren't visible. And different queries are no longer counted as one N+1.
Step debugger, containers included
Breakpoints, a live call stack, variable inspection and step controls over the Debug Adapter Protocol, so one UI drives PHP via Xdebug, Node, and Rust/C/C++ via codelldb. Under Laravel Sail it did not work at all until 0.9.12: the launch config sent empty path mappings, so Xdebug's container paths matched nothing on disk and breakpoints never bound, silently. e now reads your docker-compose.yml and maps the project root's bind mount, so a stock Sail setup needs no configuration. Since 0.14.0 breakpoints sit left of the line numbers; click there to set or clear one, and since 0.14.1 a breakpoint moves with its line — through typing, pasting, undo and formatting — and goes away when its line is deleted, where it stayed on its line number. And each test shows how it last ended — a tick, a cross, a dash for skipped — beside its declaration, kept across runs and following edits, and since 0.14.1 kept per project, so it comes back with it after a restart; click it to run that test again. Since 0.15.0, with Settings → Laravel → Run affected tests on save on, saving a PHP file runs, in the background, the test files that use its class, are named after it or call its controller's routes — a test file runs itself — and the gutter's marks update, with the status bar saying how it went. Since 0.16.0 saving a Blade template, a Livewire or Volt component's view or an Inertia page runs the tests of the controllers and components that render it, and the tests that name it.
Refactors you can read first
Rename (F2) asks the language server which occurrences are actually the symbol, across the whole workspace, and lists every site as it reads now and as it will read — nothing is written until you confirm. Through 0.9.12 it replaced whole-word matches in the active buffer and wrote them straight in, so it renamed inside strings and comments, missed every other file, and showed you nothing first. Refactor: Move Class does the same for a class and everything pointing at it, with PSR-4 choosing the destination and a namespace Composer could never autoload refused rather than written. Matching is whole-name, so moving App\Models\Order leaves App\Models\OrderItem alone. Where there is no language server the old textual rename is still the fallback. And since 0.11.0 a field can be carried through the stack: ⌘. on a form field — form.phone on an Inertia page, name="phone" in a Blade form, wire:model="phone" in a Livewire view — follows the form to its handler and previews every edit the field needs before writing any: the form's state and its TypeScript interface, the validation rule, the model's $fillable and cast, and a migration when the live schema lacks the column. Steps already done are left alone, and since 0.11.2 a migration that adds the column counts, with a note to run it, where it proposed another migration for the same column.
LSP & tree-sitter
Diagnostics, completion, hover and go-to-definition across 12+ languages, with file-type icons in the explorer. A file can be served by several language servers at once — document sync reaches all of them, completions and code actions are merged, and formatting stays with the primary — except in a PHP project that ships Pint, where Pint takes it. A Laravel project's formatting is whatever Pint says it is, and letting the language server format to its own taste only produces a diff for Pint to undo. PHPStan runs on save alongside, each finding carrying its rule identifier; when PHPStan itself fails the error is shown rather than swallowed, because a run that never looked at your code must not read as a clean one. 0.9.14 is the release that made the language servers behave like part of the editor. Squiggles, completions and renames land on the right column in Norwegian text: the editor counts columns in UTF-8 bytes, servers count them in UTF-16 units, and nothing converted between the two — so on any line with an æ, ø, å or an emoji before the caret, diagnostics were drawn a few characters to the right and rename wrote at the wrong offset. Every position now crosses that boundary through a converter that knows the line's text. Opening the first PHP file no longer freezes the window for up to thirty seconds waiting on a handshake that now runs in the background. And a server that crashes is restarted and handed every open document again, up to three times, with the status bar showing each server for the active file — running, indexing with its own progress, not installed, or crashed — and a click starting whatever is down. Since 0.11.0 Vue components are coloured throughout: the script block is JavaScript, or TypeScript with lang="ts", the style block is CSS, and the template's interpolations and directive values are coloured as expressions, the directives as keywords; script and style blocks in Blade and HTML are coloured too, and a bound :class is no longer read as Tailwind classes.
The official Laravel language server, too
In a Laravel project e runs laravel/lsp alongside Intelephense and merges their answers: Intelephense for general PHP, laravel/lsp for framework awareness — routes, views, translations, config, env, middleware, Inertia, Livewire, policies, container bindings and validation rules, with diagnostics and quick fixes. An unknown route or a missing view is a squiggle rather than a runtime surprise, and Blade files get a language server for the first time. Install it once with composer global require laravel/lsp; without it nothing breaks and e's own Laravel intelligence carries on — and since 0.9.14 its absence is not an errno. Opening a Blade file without it used to print could not start laravel-lsp (No such file or directory) for something that is not an error and has a one-line fix; e now names the missing binary, says the server is optional, and shows the command that installs it, once per session rather than once per file. Or click the status bar: the install command runs in a terminal tab, and the server starts the moment the binary appears on PATH, with no restart.
Laravel intelligence, built in
Completion for route(), view(), config(), env(), __(), x-… Blade components and Eloquent ($model->) from your live schema — plus log tail, schema diff and a Tinker scratchpad (⌘⌥T). None of it depends on an external server being installed. The schema follows migrations since 0.10.0: it was read once at startup, so after migrate the new columns were missing from completion and the column lint flagged them, and a local database is now checked every ten seconds and re-read when its migrations table changes, whether the migration ran from the Migrations panel, a terminal or Grove. Since 0.9.15 it also checks what Intelephense cannot: view('orders.missing'), route('nope'), route('users.show') without the {user} it needs, and __('auth.nope') with no such key are underlined as you type, from the project data e already has — and that data now refreshes itself, where a new route used to be not found until you remembered Refresh Project Data. 0.9.16 lets the picker fix them: create the missing Blade file, or add the key to lang/<locale>/file.php. Packages teach e their string arguments through the ide.json the ecosystem already writes for PhpStorm — one file per package, instead of one feature per package. And a Migrations panel lists every migration with its batch and status, runs what is pending behind a Grove snapshot when it can, and rolls the last batch back. 0.9.19 adds the generating half: New Eloquent Model (⌘⇧N) takes a spec — fields with types, foreignId Customer for a key plus its belongsTo, relations, options — shows a live preview of the files it implies, and ⌘↩ writes them all: model, migration, factory with fakes from the column types, seeder, form requests with rules from the fields, controller, resource and policy. Existing files are left alone. Route Search (⌘⇧R) opens the action behind any URI, unnamed routes included since 0.10.0, because an unnamed POST /webhooks/stripe is exactly the route worth seeing, Find Unused Views lists the Blade nothing references, and ⌘⇧, is the whole Laravel menu in one list. Since 0.9.20 those chords work on a Norwegian keyboard: shortcuts matched only the character a key produced — ⇧, is ; on a Norwegian layout and < on a US one — so a chord that matches nothing by character is now matched by the physical key, and ⌥-dead-keys no longer start an accent composition while ⌘ is held. Since 0.18.0, in a Laravel project, ⌘↵ outside a SQL string evaluates the PHP statement at the caret (or the selection) in the booted app, in the file's namespace with the file's imports; the value shows at the end of the line, with the queries it ran and its time, and ⌘. shows it all in the Tinker panel. Nothing it does sticks: writes are rolled back, and mail, notifications and queued jobs are faked. It refuses to run against an app that boots as production, or one whose database isn't on this machine.
Architecture map & request-replay
An interactive route → controller → view map (⌘⌥M); replay a request against your running app and see the response, SQL and N+1 warnings. Since 0.9.15 the Runtime panel reads Grove, and needs nothing installed in the app: with Grove serving the project, requests come from Grove's own timeline — it is the proxy — each one expandable into its causal chain, the SQL it issued with sql-capture on, the mail it sent, and the matching error-log entries, and the agent can be handed the whole bundle with credentials redacted. Clockwork remains the source when Grove is not around. Since 0.11.0 the running app's latest request shows at the end of the line of the controller method that served it — ⚡ 42 ms · 14 queries · N+1 orders ×12 · 2m ago — yellow for an N+1 or a slow request, red for a server error, and ⌘. opens it in the Runtime panel or hands it to the agent to fix and measure. And 🧪 on a request writes the feature test that replays it — Pest or PHPUnit, as the project has it — and opens it: the route's models made with their factories and owned by the signed-in user, the status asserted, and with SQL capture on, the rows it wrote asserted for the values it sent. A request that failed becomes a test that is red until it is fixed. Since 0.15.0 a route's controller is found through the project's PSR-4 map, so modular and domain-driven apps get their lenses too, beyond app/. And since 0.15.1 an exception the running app logs to storage/logs/laravel.log shows on the first line of your own code it passed through — in red at the end of the line, in Problems and in the status bar, which takes you there — and stays with its line as you edit; ⌘. hands it to the agent with its stacktrace, or dismisses it, and saving the file clears it. Works with Grove, Herd, artisan serve and Sail. Since 0.16.0 a queued job, listener, notification or mail in the local database's failed_jobs table shows in red on the class that runs it, with the exception, on its handle(); ⌘. retries it, hands it to the agent with its models and stacktrace, or dismisses it, and the mark goes when the job leaves the table.
Semantic search
Describe what you’re looking for (⌘⌥K) — ranked locally with Ollama when available, lexical fallback otherwise. Nothing leaves your machine. Since 0.11.0 it remembers its embeddings, by chunk text, project and model, so opening a project again embeds only what changed rather than every chunk, and a build that Ollama gave up on partway resumes where it stopped.
Visual undo tree
A branching history (⌘⌥U) that keeps edits a linear undo would discard, with click-to-jump time travel persisted across sessions.
Fuzzy finder & palette
One search dialog with tabs on ⌘P — Files, Symbols, Actions and Text, Files by default, Tab to cycle. These were four overlays behind four shortcuts, which meant deciding what you were looking for before you had started typing. Search & replace across the workspace stays on ⌘⇧F.
Integrated terminal
A PTY-backed terminal (⌘T) with ANSI colour, multiple tabs, rename and split — never leave the editor.
Seven themes, one code font everywhere
Besides its own Dark and Light, e ships Tokyo Night, Darcula, Material, Nord and Palenight, picked from cards in Settings → Appearance. A theme is the whole palette — chrome, gutter, cursor, selection, syntax and the error and warning colours every panel uses — and the window re-themes at once; F8 switches between the dark theme in use and Light. JetBrains Mono ships with e and is the default code font, so code looks the same on every machine, and Settings → Editor → Font offers the fonts programmers use, greying the ones not installed, for the editor, the terminal and code in every panel alike.
Markdown, written beside its preview
Since 0.9.23 ⌘⇧M opens the preview beside the editor rather than over it, live and scroll-synced, with tables, task lists, strikethrough, local images and links that go where they point — a URL to the browser, a relative .md into the editor. Writing gets the same care: Enter continues a list, a task or a quote and counts numbers on, an empty item ends the list, ⌘B and ⌘I toggle bold and italic, ⌘K makes a link from the URL on the clipboard, and ⌘⇧X ticks a task box. A file opened from the Finder, the Dock or open -a e now opens too, and a folder opens as the project — the same as dropping either on the window, since 0.9.24.
Database panel
Browse and query MySQL, PostgreSQL, SQLite and ClickHouse (⌘3) — connect from .env or manually, edit cells, export CSV. Since 0.11.0 e shows what a migration will do before it runs: open or save a pending migration, and the SQL migrate --pretend says it will run shows beside the line that produces it, each statement checked against the data in the local database — a NOT NULL column without a default on a table with rows, a unique index over duplicates, a foreign key rows break, a drop that takes data with it — marked on the line, with how many rows. On Postgres and SQLite, ⌘. dry-runs up() and down() inside a transaction that is rolled back. Since 0.16.0, when the project opens and after a checkout, pull, merge or reset, a bar above the editor says which migrations on the branch haven't run — with Run migrate — and which the database has from another branch. And since 0.18.0 ⌥⌘↵ on an Eloquent chain runs it (rolled back) and asks the database for the plan of each select it ran, on the app's own connection; a chain that hasn't run yet is explained without running. The end of the line says whether every query used an index, and where one scans a whole table, the line names the composite index its where and order by ask for, and ⌘. writes it as a migration.
Source control
Stage, commit, push/pull, switch branches, view history, stash, blame and resolve merge conflicts (⌘2). Since 0.14.0 Git's marks are in the gutter, beside the text rather than in it: a bar for added and changed lines, and a wedge where lines were removed. Since 0.15.0 a click on a change bar or wedge peeks at that hunk — the lines as they were in the last commit — with Stage, that hunk alone into the index, and Revert, the lines back as one undoable edit — since 0.15.2 also from the keyboard, with Git: Peek / Stage / Revert Change at Caret in the command palette, and since 0.16.0 a peek at a change that is already staged says so, with Unstage to take that hunk alone out of the index again, also as Git: Unstage Change at Caret; and a hunk in a file under a directory now goes into the index at its own path, where it went in at the repository's top. And the marks catch up when HEAD moves, wherever the commit was made; after a commit its lines stayed marked as changed.
Livewire 4 and Volt, understood
A Blade file whose PHP block holds new class extends Component, such as ⚡counter.blade.php or a Volt page, is understood as a component: wire:model and wire:click complete from its properties and actions, taken from the buffer so unsaved edits count, go to definition finds a property's declaration, and renaming a property updates the class and the markup in one edit. Multi-file ⚡counter/counter.php and counter.blade.php pairs work too. The livewire tag and the livewire directive complete every component by the name Livewire uses, pages::app.dashboard or billing.invoice, and go to definition on one, or on Route::livewire(…, 'name'), opens it. Since 0.10.0. A Blade variable knows its model as well: typing $user-> in a view completes the users columns when a controller renders the view with compact('user') and $user was type-hinted or loaded as one model, also through ['user' => …], ->with(…) and auth()->user(), and a Livewire view gets its component's typed properties. Go to definition on a class-based x- component opens its class in app/View/Components when there is no anonymous Blade file.
Find Usages and Rename for the strings no language server follows
On route('users.show'), view('users.index') or __('auth.failed'), Find Usages lists every use across PHP, Blade and, for routes, Ziggy's JS, with the declarations first: the ->name(…), the view file, the key's line in each language. In a Blade view it answers who renders this view. Rename rewrites them all through the usual preview, and moves the view's file. It refuses what it could not do completely, rather than doing half: a route name built from a group's prefix, or a translation key moving to another file. Since 0.10.0.
It does not lose your work
Unsaved edits are backed up with the session while you type and put back at the next launch, so quitting, restarting after an update or a crash no longer loses them; if the file changed on disk while e was closed, the conflict banner comes up instead of the old edits being written over it. Autosave used to write over a git checkout, Pint or an agent's edit that landed while a buffer had unsaved edits; it now leaves those files for the conflict banner, and every save checks the disk first. Saves are atomic: a temporary file renamed into place, keeping permissions, symlinks and hard links, so a crash or a full disk mid-save cannot leave a truncated file. A failed save is reported rather than only logged, saving keeps the encoding (a UTF-8 BOM is written back, UTF-16 stays UTF-16, and a legacy encoding that cannot hold a character you typed refuses to save), and binary files and files over 64 MB are not opened as text, since opening database.sqlite and typing one key was enough for autosave to corrupt it. The Save changes dialog closes the tab only after the save, where it used to close first and lose the edits. Discarding a file's changes in Source Control asks first, because it cannot be undone and the button sits next to Stage. Since 0.10.0.
Typing got several times cheaper
Syntax highlighting is incremental for PHP, Rust, Python, Go, C, JSON and CSS files up to 128 KB: the parse tree is kept between keystrokes and only the lines whose syntax changed are re-coloured, where the whole file was parsed and coloured again, PHP twice. Typing a line into a Laravel controller went from 3.5 ms to 0.5 ms a keystroke at 18 KB, and from 13 ms to 1.9 ms at 70 KB, checked against the old highlighter on 41,000 files and thousands of random edits. A Blade view with many echoes used to parse each one again on every keystroke, 34 ms for 600 echoes in a 16 KB view; the results are kept per fragment now, so a keystroke costs 2 ms. The undo history stores edits, not a copy of the file for each of up to 500 steps, and is written in the background: one history went from 2.9 MB to 58 KB. The window opens before the restored tabs are loaded, so only the tab on screen is opened first; the file tree lists folders in the background and builds only the rows in view, where expanding vendor/ built thousands; Quick Open ranks once per keystroke where it ranked four times; and git operations and Pint run in the background, so a push or a save no longer freezes the window. Since 0.10.0.
Built-in auto-updater
Detects new GitHub releases, shows the changelog, and installs the update in place. Until 0.9.18 it asked the GitHub API, whose 60 unauthenticated calls an hour per address a day of restarts uses up — so the check failed with status: 403 for a reason that had nothing to do with you. The version now comes from the releases/latest redirect and the notes from the CHANGELOG at the tag, neither rationed; the download is verified against the published .sha256, and the API is only a fallback whose limit is reported as what it is.
Your coding agent,
beside your code.
Open the agent panel with ⌘L and run Elyra, Claude Code, Codex — or any CLI agent — without leaving the editor. The editor hosts; the agent reasons.
- Any CLI agent runs in a native panel next to your files
- Review the session changeset risk-first, and revert file by file
- Pairs perfectly with the Elyra coding agent
- Terminal, database and source control one keystroke away
- Free, open source, and built in Rust
Open a project,
start writing.
Download e, point it at your code, and get a native editor with an agent panel built in. Free and open source.