e is a customizable self-aware Emacs-like editor written in Chez Scheme

Aug 29, 2026 01:35 PM - 1 hour ago 3

A mini console matter editor written in Chez Scheme. It is Emacs-like in spirit—buffers, windows, a termination ring, incremental search, and a Scheme top level—but it is not an Emacs clone. It borrows the ideas that stay small and stops there.

  • The editor is simply a Scheme system. Everything is an R6RS library: a minimal halfway pinch a published, immutable API, and hold modules built connected it (the syntax modes, bracket matching, the editing helpers, M-x itself). The halfway enforces its boundaries pinch the language alternatively than pinch discipline: internals are invisible, exports cannot beryllium reassigned.
  • M-x evaluates Scheme against the editor's unrecorded apical level, with symbol completion, parameter hints arsenic you type, history, and a transcript buffer.
  • Modules hot-reload. Saving a module's root wrong the editor reloads it into the moving session; sources edited elsewhere are picked up pinch reload-module!. Registrations are replaced, dependent modules recompile, buffers enactment put. e is developed from inside itself.
  • No limitations beyond Chez Scheme and a Unix-like terminal. There is nary build aliases installation step: a checkout runs successful place. The first commencement compiles the libraries; later starts return astir 100 ms.
  • Undo reports what it undoes: Undo insert "hello", Undo (replace-all! "xx" "yy"). Typed characters coalesce into runs, a paste is simply a azygous step, an M-x bid is 1 branded step.

As your mundane editor, cloned arsenic ~/.e:

git clone https://github.com/paveluv/e ~/.e ~/.e/e file.txt # adhd ~/.e to PATH for plain `e`

Or vendored wrong a project, truthful anyone who clones the task gets a working editor pinch it:

git clone https://github.com/paveluv/e ~/git/your_project/.e rm -rf ~/git/your_project/.e/.git # make it mean files of your repo ~/git/your_project/.e/e file.txt

Every installation is self-contained: the loader uses strictly the lib/ adjacent to the book itself, compiled objects enactment section successful eo/, and sources recompile automatically erstwhile they (or thing they import) change. Project-specific modules dropped into a vendored checkout's lib/ enactment section to that project.

The terminal size is detected via ioctl and tracked crossed resizes; if that is unavailable, group LINES and COLUMNS. On FreeBSD, whose Chez larboard renames the scheme-script interpreter, alteration the shebang to #!/usr/bin/env -S chez-scheme --script aliases run chez-scheme --script e (details successful the loader's header comment).

Key Action
C-x C-s Save (prompts for a sanction successful an unnamed buffer)
C-x C-w Save as: punctual for a path, the buffer visits it
C-x C-f Visit a record successful its ain buffer (creating if needed)
C-x C-c Quit (asks only if immoderate buffer differs from its file)
Key Action
C-x b Switch buffer (default: astir caller other; caller sanction creates a buffer)
C-x C-b Open *buffers*: move pinch Up/Down, Enter to select; click selects immediately
M-Up Switch to the erstwhile buffer alphabetically
M-Down Switch to the adjacent buffer alphabetically
C-x k Kill a buffer (default: current; asks if modified)
C-x 2 Split the existent model successful 2 (stacked)
C-x 3 Split the existent model successful 2 (side by side)
C-x 1 Delete each different windows
C-x 0 Delete the existent window
C-x o Move to the adjacent window
C-x t Toggle soft-wrapping of agelong lines successful this window

Each model has its ain position line, point, and scroll position; the same buffer tin beryllium shown successful respective windows astatine once. Buffers person per-buffer history and marks; windows person independent point, scrolling, wrapping, and status. A right-side scrollbar is shown by default; C-x l toggles a buffer-owned line-number gutter. See Buffers for switching, *buffers*, record integrity, target windows, rodent behavior, scrollbars, wrapping, splits, and the nationalist API.

Key Action
C-b / C-f / arrows Left / correct 1 character
C-p / C-n / arrows Up / down 1 line
C-a / C-e, Home/End Beginning / extremity of line
C-v / M-v, PgDn/PgUp Page down / up
M-< / M-> Beginning / extremity of buffer
Key Action
C-d, Delete Delete the characteristic aft point
Backspace Delete the characteristic earlier point
C-o Open a statement below, leaving constituent successful place
C-k Kill to extremity of statement (repeats accumulate)
C-@ (C-Space) Set the mark
C-w Kill the region betwixt people and point
C-y Yank the past kill
C-_ Undo
C-M-_ Redo
TAB Indent the statement by its mode's rules
M-% Query switch from point: y/SPC replaces, n/DEL skips, q/RET/C-g stops
M-. Describe the awesome astatine constituent (Scheme buffers; spot below)
C-x C-e Evaluate the existent buffer arsenic Scheme
C-l Repaint the surface and re-read its size
C-g Cancel (prompt, search, mark, moving evaluation)
C-h k Describe a cardinal and wherever its bindings came from

All keyboard commands tin beryllium rebound successful config.e. User bindings override built-in and module defaults, and C-h k describes a key, its source, and its contextual meanings:

(bind-key! "C-c s" save!!) (unbind-key! "C-v")

See Key binding configuration for cardinal syntax, context maps, disposable actions, precedence, unbinding, and module defaults.

C-s starts an incremental search: type to widen the needle, C-s again jumps to the adjacent lucifer (wrapping around), Backspace shortens the needle, RET/ESC accepts silently, C-g cancels and returns point to wherever the hunt began. In a new, quiet search, C-s recalls and searches for the erstwhile needle. Point rides conscionable past the current match, truthful a people group earlier searching leaves the recovered matter wrong the region. The needle's matches successful the existent model are highlighted while searching. Matching folds lawsuit the smart way, arsenic successful Emacs: it ignores lawsuit only while the needle is each lowercase, and 1 typed capital makes it nonstop (the punctual past sounds I-search (exact):). M-c toggles the existent hunt either way, and (search-fold-case #f) successful config.e makes each hunt exact. Everything else—including M-% query replace—always matches exactly.

Indentation and formatting

Indentation is the 1 point enforced—spacing wrong a statement is the author's (tables, alignment, and manus layout pass structure), and lines are ne'er joined aliases split. The Scheme rules are Emacs-like: assemblage forms (define, lambda, the fto family, when, ...) indent their bodies 2 columns past the opener, a lone closer sits nether its opener, and an application's continuation lines have two stops: 2 past the opener, aliases aligned nether the first argument erstwhile it shares the opener's line. The writer picks:

(very-long-function-name param1 param2 param3) (very-long-function-name param1 param2 param3)

TAB indents the existent line, cycling done its stops -- the nearest extremity to the right, wrapping astir -- and puts constituent connected the indentation (a blank statement pads retired to it); modes opt successful erstwhile they register an indenter, and scheme-mode does. indent-region! and indent-buffer! indent statement by statement from the apical arsenic 1 undo step, each statement settling connected the extremity nearest its existent indentation: connected a stop it stays, earlier the first it takes the first, past the past the last. format-region! and format-buffer! additionally widen tabs outside strings to spaces ((scheme-tab-width 2); #f keeps tabs), trim trailing whitespace, driblet trailing blank lines truthful the record ends with precisely 1 newline, and use the bracket conventions -- [ ] for the bindings of the fto family, do, parameterize, and with-syntax, and for the clauses of cond, case, case-lambda, guard, syntax-rules, and syntax-case; ( ) everyplace other -- the scheme-format-brackets parameter turns that off. A region format only rewrites bracket pairs it wholly contains, truthful it can never unbalance the buffer; drawstring interiors and separator comments are never touched. Modules supply the rules per mode with register-indenter! and register-formatter!. The Scheme motor is the axenic (scheme-format) library, which besides drives tools/scheme-format: the aforesaid curen from the ammunition with scheme-format [-i] [file ...], aliases stdin to stdout without arguments. With (scheme-format-on-save #t) (on by default) each prevention of a Scheme buffer formats it first done a pre-save hook; modules adhd their ain with add-pre-save-hook! and add-post-save-hook! (the module reload is a post-save hook).

Prompt input is line-editable pinch the accustomed bindings (C-a/C-e, arrows, Home/End, C-k/C-y done the shared termination ring), and completes pinch TAB: record names successful the record prompts, buffer names successful the buffer prompts. TAB extends to the longest communal prefix; erstwhile nothing extends, a 2nd TAB pops up a *completions* window, which disappears erstwhile the punctual finishes. Input longer than the surface is wide wraps onto continuation lines, marked pinch a trailing \ and indented nether the punctual text, the windows supra shrinking to make room; astatine 8 lines the punctual area scrolls instead. Long echo-area messages wrap the aforesaid way. In a wrapped punctual the arrows move between visual lines, turning into history navigation astatine the apical and bottom edges.

M-x evaluates Scheme successful the editor's unrecorded apical level, wherever the published API and loaded modules are available. C-x C-e evaluates the existent buffer. Results look successful the echo area and system log:

M-x (buffer-name (current-buffer)) eval: (buffer-name (current-buffer)) => "sa.txt"

See Evaluation for multiline entry, completion, eval! targets, output capture, consequence copying, interruption, history, and configuration.

The echo area is simply a transient tail of the log: each connection a command logs stacks connected its ain line, prefixed pinch its constituent successful grey, truthful a save that reloads a module and past reports a merge shows each three lines astatine once. The area grows arsenic messages arrive, the windows above shrinking down to their minimum, past which the oldest lines springiness way; the adjacent keyboard aliases rodent arena settles it backmost to a azygous line. A connection logged under (parameterize ([message-progress #t]) ...) is progress: it supersedes its component's newest statement alternatively of stacking -- never another component's -- truthful a download reports fourteen steps arsenic one line redrawn successful place, while *log* still records each step.

Nothing is mislaid erstwhile it settles: each connection that passes done the echo area lands successful the log -- the editor's syslog, system records of clip (nanosecond precision), component, and text. The *log* buffer is simply a view -- a non-interactive app rendered from records connected the alert -- ever in the buffer database (C-x b *log*), read-only, refreshed while visible, wearing [] successful its position barroom wherever mean buffers show --. With the cursor astatine the extremity (M->) it tails the log; anyplace else the viewport holds still while records arrive. (log-view 'eval) makes a filtered position -- *log eval* -- connected the fly. Entries are structured information pinch registered presentation: a module registers a formatter (and optionally a styler) for its component, used identically successful the echo area and the position -- eval logs the pair (query . result), formatted query => consequence pinch Scheme highlighting, while its history sounds conscionable the queries. Messages set pinch a message-source of #f are indicators -- shown for illustration a CapsLock light, ne'er logged. log-entries returns the records themselves for slicing from M-x, log-history derives a command history from immoderate constituent (find-file and save-as browse their past paths this way), and log! adds your own.

Generic editing helpers unrecorded successful the (edit) module and return an optional wherever argument. Omitted, it intends the selected region aliases the whole existent buffer; different options are a buffer aliases its name, a predicate complete buffers, a region, aliases a database of immoderate of these:

M-x (replace-all! "xx" "yy") ; existent buffer M-x (replace-all! "xx" "yy" buffer-file) ; each record buffer M-x (count-matches "xx" "notes.md") ; count, don't edit

Each targeted buffer gets 1 undo measurement branded pinch the command itself, and constituent stays wherever it was. The interactive replace!! (M-%) queries occurrence by occurrence instead, highlighting each, and prompts for whichever arguments it is not given.

The Scheme manual is disposable wrong the editor:

M-x (describe eq-hashtable-ref)

C-h f (describe!!) prompts for a documented usability sanction with completion. Its input uses the aforesaid lucifer styling arsenic different prompts: a partial name is italic, a complete Scheme sanction is upright, and an e-specific sanction is highlighted. It opens a *describe* buffer pinch the introduction from the reference documentation: forms, what it returns, libraries, source, and the full prose. When the described sanction is simply a command, the page besides lists each key currently bound to it. *describe* is simply a unrecorded view, truthful rebinding aliases unbinding a cardinal updates an already visible page connected the adjacent redraw. The corpus covers R6RS (from TSPL4) and Chez extensions (from the Chez Scheme User's Guide). Run M-x (fetch-describe-data!) once to download and extract it: astir 1,400 entries, kept retired of Git in data/describe/ adjacent to lib/. The download uses curl; describe.e does the rest. The database is system and queryable from M-x: doc-lookup returns the entries for a name, doc-entries each of them (optionally filtered by a predicate), and the doc-* accessors take entries apart, truthful the full corpus tin beryllium sliced by source, chapter, library, aliases thing else. In a Scheme buffer, M-. describes the symbol the cursor is on.

Modules whitethorn adhd pages for their published commands with register-descriptions!. Each introduction has the aforesaid fields arsenic a corpus entry: (names forms returns libraries root section url description). Registrations are owned by the calling module and are replaced pinch its different hooks on reload. A URL whitethorn beryllium #f. The (edit) module uses this hook for replace!! and replace-all!, truthful those commands are documented without the downloaded Scheme corpus.

M-x (pretty-scheme-clusters!) toggles the existent buffer into a mode in which each construct's parentheses look arsenic the Unicode pair assigned to its cluster, while the buffer and record clasp plain ASCII:

「define (twice x) ⟨let ([y (* x 2)]) ⦅if (odd? y) y ⟦begin x⟧⦆⟩」

Definitions deterioration 「 」, lambdas ⸦ ⸧, the fto family ⟨ ⟩, conditionals ⦅ ⦆, power ⟦ ⟧, loop ⟅ ⟆, syntax ⧼ ⧽, modules ⸨ ⸩, quoting ‹ ›, set! ⧘ ⧙, hold ⌊ ⌋. Applications and clause brackets enactment plain, truthful the glyphs mark structure without drowning it. The show follows the codification arsenic you type: the glyph appears erstwhile the usability completes and flips if you edit specify into cond. Typing ) aliases ] closes the innermost open conception pinch immoderate characteristic the root opened it with, as the REPL does, and the position statement shows the root characteristic under the cursor. The cluster array lives astatine the apical of pretty-scheme.e as literal glyphs adjacent to a palette of spare pairs; pinch auto-reload on, editing it re-skins each beautiful buffer connected save.

Two related variants waste and acquisition the cluster norm for nesting depth: M-x (pretty-scheme-depth!) rotates the glyph pairs arsenic the character deepens (cycling erstwhile the 12 tally out), and M-x (pretty-scheme-rainbow!) keeps the plain characters but colors each extent done a seven-color rainbow, closers mirroring their openers.

Two style rules thrust on successful each Scheme buffer: symbols the standard connection does not cognize render italic (a local, a program-defined name, aliases a typo), and successful eval contexts, which tally in the editor's ain apical level, the editor's names return their ain style -- truthful astatine the M-x prompt, plain is modular Scheme, purple is the editor, italic is nobody's.

The editor compares record contents earlier editing and saving, canonicalizes visited paths, and ne'er silently overwrites an outer change. It offers overwrite, reread, aliases three-way merge arsenic due and records merge reports. See Buffers: File buffers for the complete lifecycle and conflict workflow.

config.e adjacent to the loader book is plain Scheme—no library, no shebang: each look evaluates successful the editor's apical level, the M-x environment, pinch the full nationalist API successful scope. It loads at startup erstwhile the modules are up, and loads again aft each module reload truthful its settings reapply connected apical of caller registrations -- write it to tolerate being loaded immoderate number of times. Saving it inside the editor applies it connected the spot, arsenic does M-x (load-config!); an error reports successful the echo area and leaves the editor running.

config.e itself is not successful type power -- git ignores it, so personal settings enactment wrong the directory without ever showing up in a diff. What ships is config.template.e: each action arsenic a commented statement showing its default (the editor behaves identically with aliases without it). Copy it to get started:

cp config.template.e config.e

then uncomment a statement and alteration its worth to disagree pinch a default -- for example, (indent-on-tab! "scheme" #f) turns TAB's auto-indent disconnected for Scheme buffers.

Architecture and hold modules

Everything successful lib/ is simply a room pinch the .e extension, named after its file: core.e is (core), eval.e is (eval). The halfway is a generic kernel (buffers, windows, editing, rendering, prompts). sys.e is the system-specific furniture (libc, termios, ioctl, signals and file-descriptor plumbing), and the only room containing overseas procedures. Features specified arsenic eval constitute halfway accommodation and import the narrowly scoped system mechanisms they request directly; their argumentation does not unrecorded successful core. The core's exports are the editor's published API: internals, including each mutable state, are invisible extracurricular it and unfastened to compiler optimization, while exports are immutable. The connection enforces some guarantees (set! connected an exported sanction raises an exception). The API comprises the command procedures, read-only authorities accessors (current-buffer, point, buffer-line, ...), the editing primitives modules constitute commands from (goto-point!; call-with-buffer, which runs codification pinch another buffer temporarily current; call-as-one-edit!, which bundles edits into 1 undo step), and the hold hooks (bind-key!, bind-default-key!, unbind-key!, register-mode!, add-highlighter!, prompt!, read-key-event, reload-module!, ...). M-x ( followed by Shift-TAB lists the full catalog.

Names travel a contract. A process ending successful !! waits for input from the user: a prompt, a confirmation, a cardinal query. It takes no required arguments, reports done the echo area, and returns nothing; on occurrence it returns void, truthful evaluating 1 from M-x leaves its display and connection successful place, and failures raise, reported successful the echo area (and the log) arsenic errors. A single-! verb acts instantly connected what it is given (explicit arguments, a useful return value), moreover erstwhile it displays something: list-buffers! pops up the buffer database but asks nothing, so one bang. Two bangs inquire you things. The !! commands are bladed wrappers over primitives (find-file!! prompts and calls visit-file!), and keys hindrance to whichever fits.

An hold module is simply a room that imports (core) and exports an init! performing its registrations:

;; lib/my-mode.e (library (my-mode) (export init!) (import (chezscheme) (core)) (define (my-styles s) ...) (define (init!) (register-mode! "my" '(".my") '() my-styles)))

Bundled modules usage bind-default-key!; third-party defaults should do the same. bind-key! is reserved for deliberate personification aliases session overrides, truthful a module reload cannot displace a prime from config.e.

At startup the halfway loads each lib/*.e room and calls its init!; the loader book is axenic bootstrap. Features are built this way connected apical of the kernel: M-x itself lives successful eval.e, the editing helpers successful edit.e, bracket matching successful paren.e. Modules usage one another by mean import, and the room strategy orders initialization and recompilation accordingly. A module that fails reports itself successful the connection statement without preventing startup.

Modules reload without restarting. Saving a module root successful the editor (any .e record successful the moving editor's lib/, including a new one) reloads it connected the spot. M-x (modules-reload-on-save #f) turns that off for a session; (modules-reload-on-save #f) successful config.e disables it for an installation. config-reload-on-save controls config.e's own on-save reload successful the aforesaid way. If a reload fails, the editor reports the correction and keeps the aged type running. For sources edited extracurricular e:

M-x (reload-module! "paren")

The halfway redefines the module's room from source, recompiles every loaded module built connected it, and re-runs the init!s. Everything a module registers is tagged pinch the module that registered it, and a reload retracts the aged registrations first, truthful re-registration replaces alternatively than accumulates, for immoderate coming aliases early hook, with nothing for module authors to remember. Buffers, windows, and the M-x top level are untouched; the halfway itself cannot beryllium reloaded.

Syntax highlighting is provided by modes, registered by modules:

(register-mode! "scheme" '(".scm" ".ss" ".sls" ".sps" ".sc" ".e") ; file-name extensions '("scheme" "petite" "chez" "guile" "racket") ; #! interpreters for scheme-styles) ; files without an extension

A buffer gets the first mode whose hold matches its record name, or whose expert sanction appears successful the file's #! line. The styles function maps a statement to a vector of per-column style symbols; brackets styled delimiter return portion successful bracket matching. Three file-detected syntax modes vessel in lib/: scheme-mode.e, c-mode.e (block comments span lines through a memoized whole-buffer scan), and md-mode.e (Markdown; indented code blocks are styled arsenic Scheme, modes composing per line). Stateful modes build their statement supplier pinch memoize-buffer-analysis: the researcher receives a snapshot vector of lines and returns a vector of per-row results, recomputed erstwhile once the buffer contented changes. Context highlighting, markup that depends connected wherever constituent is, is provided by highlighters: add-highlighter! registers a usability consulted at every redraw that returns (row commencement end) aliases (row commencement extremity style) ranges of the existent buffer to people up -- people (the default) underlines, lucifer and match-point are the search's cyan and yellow backgrounds. Bracket matching is specified a module, paren.e, and the incremental hunt itself is another, search.e; local-variable or other semantic highlighting could beryllium added the aforesaid way.

Faces tin beryllium customized declaratively pinch set-style!; see Styles for the DSL, colors, attributes, built-in faces, terminal behavior, and configuration lifecycle.

Dynamic devices are app buffers: views are non-interactive apps, while an app may grip controls and enactment connected the model that was existent earlier entry. *buffers* highlights a navigable statement and opens it successful that target window. See Buffers for its personification interface and App buffers for registration, arena handling, and target semantics.

  • Bracketed paste: the editor enables it itself, truthful a paste from anywhere is 1 edit, and 1 undo step, pinch nary pasted control characters moving commands; successful a prompt, the paste lands whole.
  • Mouse: a click focuses the model nether the pointer and places point at the clicked cell; dragging selects, precisely arsenic though the mark were group astatine the property and constituent moved (so C-w, C-y, and friends apply); a double click selects the connection nether the pointer; dragging a position barroom resizes the windows it separates; the wheel scrolls the model nether the pointer by 1 eighth of its height, and horizontal instrumentality ticks move constituent sideways wrong its line. The terminal's ain rodent action needs Shift held while search is on; M-x (mouse! #f) turns search disconnected (and autochthonal action back on) for the session; a lingering Shift-selection item clears with C-l.
  • Kill ringing entries accumulate crossed consecutive termination commands, so C-k C-k C-k ... C-y reassembles full blocks.
  • Incremental surface updates: only changed rows repaint, statement styling is memoized, and windows without fixed scrollbar aliases sticky-header chrome may use the terminal's autochthonal scroll operation.

The model layout is deliberately simple: horizontal bands whitethorn contain side-by-side columns, but it is not a general-purpose tiling system. Tabs and different power characters are displayed arsenic a azygous abstraction cell. Input is publication arsenic UTF-8, but each characteristic is assumed to inhabit one terminal column.

MIT

More