Skip to content

ooyeku/stem

Repository files navigation

Stem

A modal text editor for the terminal. Built in Zig, with tree-sitter syntax highlighting and built-in LSP integration for 20+ languages.

Stem aims to keep modal editing approachable: Vim-style modes, a Space leader, and a discoverable command palette. ZLS is embedded so Zig works with no setup; other language servers install on request via stem lsp install.

Zig Platforms License

Install

Prebuilt binaries and one-line install scripts will land with the first tagged release. For now, build from source — zig build does everything end-to-end.

From source

Requires Zig 0.16+ and a C compiler. All other dependencies (tree-sitter, language grammars, ZLS) are fetched by zig build.

git clone https://github.com/ooyeku/stem.git
cd stem
zig build run

To build and install:

macOS / Linux (bash, zsh, sh):

./install.sh                  # build ReleaseFast, install, refresh plugins
./install.sh --prefix ~/.local

Windows (PowerShell 5.1+, no admin needed):

.\install.ps1                          # install into %LOCALAPPDATA%\Programs\stem
.\install.ps1 -Prefix C:\tools\stem    # custom prefix
.\install.ps1 -NoPath                  # skip the user-PATH update

If PowerShell blocks the script with an execution-policy error, launch it as powershell -ExecutionPolicy Bypass -File .\install.ps1.

Both installers compile from source (zig build -Doptimize=ReleaseFast), copy the binary, install bundled wasm plugins to the system prefix and refresh the per-user plugin dir at ~/.stem/plugins/ (%USERPROFILE%\.stem\plugins\ on Windows), and add the bin dir to PATH when it isn't already there.

Uninstall

./uninstall.sh             # remove the binary and bundled plugins
./uninstall.sh --purge     # also remove ~/.stem (config, logs, LSP cache)
.\uninstall.ps1            # remove binary, plugins, and PATH entry
.\uninstall.ps1 -Purge     # also remove %APPDATA%\stem + %LOCALAPPDATA%\stem

Features

  • Modal editing — Select, Insert, Visual, View, and Terminal modes
  • Multi-buffer workflow with a tab bar
  • Horizontal and vertical split panes
  • Transactional undo/redo with cursor restoration
  • Multi-cursor editing (Sublime-style Ctrl+D add-next-occurrence)
  • Vim-style text objects (w W p " ( [ { …) for select inside / around
  • Surround commands: wrap selection, change or delete a pair
  • Named bookmarks (m<a-z> set, '<a-z> jump) persisted per project
  • Incremental in-buffer search with / and ?, smart-case, live match count
  • Project-wide search (Space /) with per-match replace confirmation
  • Fuzzy file picker, buffer picker, and command palette
  • Tree-sitter syntax highlighting for 29 languages
  • LSP integration for 23 external language servers plus embedded ZLS for Zig (with optional format-on-save)
  • LSP code actions (Space C), range format (Space F), signature help (auto-popup in Insert mode), and inlay hints (opt-in)
  • Inline diagnostics ("error lens") rendered at end-of-line
  • Word-under-cursor highlight after a short idle
  • Integrated terminal mode
  • Manifest-driven plugin system with wasm and exec runtimes
  • Auto-completion, hover docs, go-to-definition, references, diagnostics, and document symbols (via LSP)
  • Jump to next/previous diagnostic (]d/[d), git hunk (]g/[g), AST sibling (]s/[s), function (]m/[m)
  • Session restore with crash-recovery snapshots
  • Periodic auto-save backups of dirty buffers in ~/.stem/recover/, surfaced at startup if any survived a crash
  • Large-file mode: files past 5 MB / 50k lines auto-degrade — tree-sitter, brackets, LSP, and auto-pair disabled so a multi-MB log stays responsive. [LARGE] badge in the status bar
  • Background workspace file index for instant Find queries
  • CLI search tools (stem --find, --vfind, --scope)

Language coverage

Syntax highlighting works for: Zig, Python, JavaScript, TypeScript, TSX, JSON, Bash, Go, HTML, CSS, Rust, C, C++, Java, Ruby, C#, PHP, Swift, Kotlin, Lua, Dart, Elixir, Haskell, OCaml, Scala, R, Perl, Erlang, Markdown.

Language servers installable via stem lsp install <name>:

Language Server External requirement
Zig ZLS (embedded)
Python Pyright Node
JavaScript / TypeScript typescript-language-server Node
Go gopls Go
Rust rust-analyzer
C / C++ clangd LLVM / Xcode CLT
Ruby ruby-lsp Ruby + gem
C# OmniSharp
Java jdtls Java runtime
Bash bash-language-server Node
Lua lua-language-server
Swift sourcekit-lsp Swift toolchain
R languageserver R
CSS / HTML / JSON vscode-langservers-extracted Node
PHP intelephense Node
Perl perlnavigator Node
Dart dart language-server Dart SDK
Elixir elixir-ls install via brew or releases
Erlang erlang_ls rebar3
Haskell haskell-language-server ghcup
Kotlin kotlin-language-server brew or releases
OCaml ocaml-lsp-server opam
Scala metals coursier

stem lsp install all walks the list and installs every server whose prerequisites are available.

Usage

stem                       # empty buffer
stem myfile.zig            # open a file
stem file1.zig file2.zig   # open multiple files
stem ./src                 # open a directory

# CLI search tools
stem --find "pattern"      # grep-like text search
stem --vfind "pattern"     # interactive visual search
stem --scope file.zig fn   # search within a specific file

stem --help                # all options
stem --version             # version info

Key bindings

Stem leans on a Space leader and a discoverable command palette (Space f) for most actions. The bindings below cover everyday editing; everything else is reachable through the palette.

Press Space ; (or Space ?) at any time to pop up the which-key reference — it shows every available follow-up key. While in a chord like Space l the popup shows that chord's sub-bindings.

Modes

Key Action
i Enter Insert mode
v Enter Visual mode (selection from cursor)
V Visual-select the syntax node under the cursor
t Enter Terminal mode
Esc Return to Select mode

Navigation (Select / Visual)

Key Action
h j k l Move left/down/up/right
Arrow keys Move left/down/up/right
w b e Next / previous / end of word
W B Next / previous WORD (whitespace-separated)
{ } Previous / next paragraph
Home / End Start / end of line
PageUp / PageDown Scroll one page
% Jump to matching bracket
[N] motion Repeat motion N times (5j, 3w)
[ / ] Previous / next buffer (Cmd+Shift on macOS)
]d / [d Next / previous diagnostic
]g / [g Next / previous git hunk
]s / [s Next / previous AST sibling
]m / [m Next / previous function-like node

Search (Select / Visual)

Key Action
/ Incremental forward search with live preview + [i/N] count
? Incremental backward search
n / N Next / previous match after closing the prompt
Esc Cancel search; cursor returns to its starting position

Search uses smart case: any uppercase character in the query makes the search case-sensitive; otherwise it's case-insensitive.

Bookmarks

Key Action
m<a-z> Set bookmark <x> at the cursor
'<a-z> Jump to bookmark <x> (works across files)

Bookmarks persist per project under ~/.stem/cache/bookmarks/. The bookmark.list command opens a [Bookmarks] overview; bookmark.clear_all removes them.

Text objects (Select / Visual)

In select mode:

  • s i <c> — select INSIDE <c>
  • s a <c> — select AROUND <c>

In visual mode, drop the s prefix: i <c> / a <c>.

<c> is one of: w word, W WORD, p paragraph, " ' ` string literals, ( [ { < matching pairs (use either bracket).

Surround

Chord Action
S <c> (visual) Wrap the active selection with <c>
s d <c> (select) Delete the surround pair <c> enclosing the cursor
s r <old> <new> (select) Replace surround <old> with <new>

Multi-cursor

Key Action
Ctrl+D Add the next occurrence of the word / selection as a secondary cursor
Esc (select mode) Clear all secondary cursors

Typing and backspace replicate at every cursor. Newlines and line-altering operations collapse back to the primary cursor.

Save / open / quit

On macOS use Cmd, on Linux/Windows use Ctrl:

Key Action
Cmd/Ctrl+S Save current buffer
Cmd/Ctrl+O Open file picker
Cmd/Ctrl+W Close active buffer
Cmd/Ctrl+Q Quit

Space leader — top level

The highest-frequency actions live as a single key after Space.

Key Action
Space e Open file (tree-shaped explorer — also Cmd/Ctrl+O)
Space b Buffer picker
Space s Save
Space q Quit
Space k Close current pane / buffer
Space n / Space p Next / previous buffer
Space [1-9] Quick switch to buffer N
Space f Command palette (find any command) — Space : alias works on terminals that handle Shift+; cleanly
Space / Project-wide search & replace
Space , / Space . Jump back / forward
Space z Center cursor in viewport
Space u / Space r Undo / redo
Space c / Space x / Space v Copy / cut / paste
Space a Code actions (LSP)
Space - / Space | Horizontal / vertical split
Space ←/→/↑/↓ Focus split pane in that direction
Space h Help view
Space j Background jobs list
Space ; / Space ? Toggle which-key popup
Space Esc Cancel the leader

Space leader — chord groups

Related families live under a chord prefix. Tap Space ; inside any chord to see the contents on screen.

Space l — LSP

Key Action
Space l d Go to definition
Space l r Find references
Space l h Hover (docs)
Space l a Code actions (alias for Space a)
Space l f Format buffer
Space l F Format selection
Space l D Diagnostics list
Space l s Document symbols
Space l S Workspace symbols
Space l t Toggle inline diagnostics
Space l i Toggle inlay hints
Space l = Toggle format-on-save

Signature help auto-pops above the cursor in Insert mode when you type ( or ,. Dismissed by ), Esc, or mode change.

Space g — Git

Key Action
Space g d Git diff (via bundled git plugin)

Space w — Window / splits

Key Action
Space w - Horizontal split
Space w | Vertical split
Space w h/j/k/l Focus pane left / down / up / right
Space w q Close pane

Space t — Toggle

Key Action
Space t d Toggle inline diagnostics
Space t i Toggle inlay hints
Space t = Toggle format-on-save

File explorer (Space e — also Cmd/Ctrl+O)

The single entry point for opening files. Modal, tree-shaped overlay rooted at the project root.

Key Action
↑/↓ or j/k Move selection
/ l Expand directory
/ h Collapse directory (or jump to parent)
g / G Top / bottom of list
Enter / Space Open file (or toggle directory)
H Toggle hidden files
Ctrl+r Rebuild tree
Esc Close explorer

Project-wide search & replace

Space / opens the global search panel. Type into the query field; results populate live across the workspace.

Key Action
Tab Toggle focus between query and replace fields
Enter Open the highlighted match
/ Walk through matches
Ctrl+R Start replace-with-confirmation walk

Inside the replace walk:

Key Action
y Apply replacement at this match, advance
n Skip this match, advance
A Apply this match and every remaining match silently
q / Esc Cancel; summary shown in the status bar

Replacements happen in open buffers (not directly on disk), so you can undo per-file with Space u and only commit by saving.

Split navigation

Key Action
Ctrl+h / Ctrl+l Focus split left / right
Ctrl+j / Ctrl+k Focus split down / up

Configuration

Configuration lives in ~/.stem/:

~/.stem/
├── config.json     # User settings
├── plugins/        # Installed plugins (seeded from bundled on first run)
├── lsp/            # Language servers installed via `stem lsp install`
├── cache/          # Background workspace index, etc.
└── logs/           # Debug logs (stem-*.log)

Manage settings from the CLI:

stem config list
stem config get editor.tab_size
stem config set editor.tab_size 2

Or edit ~/.stem/config.json directly:

{
  "editor": {
    "tab_size": 4,
    "insert_spaces": true,
    "line_numbers": "relative",
    "wrap": false,
    "cursor_line": true,
    "auto_pairs": true,
    "format_on_save": false,
    "inline_diagnostics": true,
    "inlay_hints": false,
    "auto_save_backup": true,
    "auto_save_interval_seconds": 30,
    "large_file_threshold_bytes": 5242880,
    "large_file_threshold_lines": 50000,
    "large_file_hard_limit_bytes": 104857600
  },
  "ui": {
    "show_status_bar": true
  },
  "logging": {
    "level": "info"
  }
}

Runtime toggles (via the command palette Space a, or stem config set ...):

Setting Command palette Effect
editor.format_on_save lsp.toggle_format_on_save Run LSP formatter before each save
editor.inline_diagnostics editor.toggle_inline_diagnostics "Error lens" — diagnostic message after every affected line, not just the cursor's
editor.inlay_hints editor.toggle_inlay_hints LSP type / param-name hints rendered as dim virtual text

Large-file mode

When a buffer exceeds large_file_threshold_bytes (default 5 MB) or large_file_threshold_lines (default 50 000), Stem opens it in large-file mode: tree-sitter syntax highlighting, bracket rainbow, LSP requests, and bracket auto-pair are disabled for that buffer. The status bar shows a yellow [LARGE] badge so the quiet behaviour isn't mysterious. Files past large_file_hard_limit_bytes (default 100 MB) are rejected at open time. All three thresholds are per-buffer at open and sticky for the buffer's life — re-open after stem config set ... to re-classify.

Auto-save backups

While stem is running, every auto_save_interval_seconds (default 30 s) it writes a snapshot of every dirty buffer to ~/.stem/recover/<hash>.bak with a .path sidecar recording the original filename. On the next startup, if any backups survived, the status bar prompts you to run buffer.restore_backups to view them. Disable with stem config set editor.auto_save_backup false.

Platform support

Platform Status Notes
macOS (ARM64) Supported Primary development target
macOS (x86_64) Supported
Linux (x86_64) Supported
Linux (ARM64) Supported
Windows Experimental No integrated terminal

Building from source

zig build                                  # Debug
zig build run                              # Debug + run
zig build -Doptimize=ReleaseFast           # Release
zig build test                             # Tests
zig build -Dtarget=x86_64-linux-gnu -Doptimize=ReleaseFast
zig build -Dtarget=x86_64-windows -Doptimize=ReleaseFast    # experimental
Option Description
-Doptimize=ReleaseFast Optimised build
-Doptimize=ReleaseSafe Optimised with safety checks
-Doptimize=ReleaseSmall Optimised for size

Architecture

src/
├── main.zig           # Entry point and CLI handling
├── cli.zig            # Subcommand dispatch (config, logs, lsp, plugin)
├── kernel/            # Event loop, buffer manager, sessions, commands
├── core/              # Piece-table buffer, editor state, file I/O
├── ui/                # Terminal rendering (vaxis), pickers, themes
├── syntax/            # Tree-sitter integration and language queries
├── services/          # LSP, logging, terminal, global search
├── lsp/               # LSP protocol client and transport
├── plugins/           # Manifest, wasm interpreter, exec runtime
├── config/            # Config schema, keys, persistent storage
├── tools/             # CLI tools (find, vfind, scope, plugin)
└── fuzz/              # Fuzz targets (piece table, state, URIs)

Dependencies

All Zig dependencies are pinned in build.zig.zon:

Plugins

Bundled plugins are installed into ~/.stem/plugins/<name>/ with a plugin.json manifest. Both wasm modules and child-process exec plugins are supported.

Plugin Runtime Description
echo wasm Reference plugin: a single command that pops a notification
git wasm Status / diff / staged-diff plus a live branch indicator
plugin_manager wasm Plugin dashboard and reload commands

See docs/plugins.md for the full author guide and host internals.

Troubleshooting

An LSP isn't working for a language. Run stem lsp install <language> and check stem logs. Bump verbosity with stem config set logging.level debug.

Colours look wrong. Make sure your terminal advertises 24-bit colour: export COLORTERM=truecolor.

./install.sh says "no write access to /usr/local". Either re-run with --prefix $HOME/.local (no sudo needed), or grant sudo access.

Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Make sure zig build and zig build test pass
  4. Cross-check at least one other target: zig build -Dtarget=x86_64-linux-gnu
  5. Open a pull request

Documentation

  • Plugins — author guide + host internals
  • stem.md — long-form reference

License

MIT License

Acknowledgments

About

A modal text editor for the terminal. Built in Zig, with tree-sitter syntax highlighting and built-in LSP integration for 20+ languages.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors