Token is a text editor written in Rust. Multi-cursor editing, tree-sitter syntax highlighting, split views, CSV spreadsheet mode, configurable keybindings—around 15,000 lines of code across 333 commits. Most of it was written through 116 conversations with Amp Code agents.
The project started December 3rd and the core features were done by December 19th. This documents how the AI collaboration was structured.
The Approach
AI agents work well for focused, well-defined tasks. The human job is providing structure: clear phases, written specs, and explicit invariants. The more complex the project, the more this structure matters.
Text editors are deceptively complex: cursor choreography with selections, keyboard modifier edge cases across platforms, grapheme cluster handling, viewport scrolling. Getting these consistent across dozens of AI sessions requires documentation that both humans and agents can reference.
Three Work Modes
Each session starts with an explicit mode declaration:
| Mode | Purpose | Inputs | Example |
|---|---|---|---|
| Build | New behavior that didn't exist | Feature spec, reference docs | "Implement split view (Phase 3)" |
| Improve | Better architecture without changing behavior | Organization docs, roadmap | "Extract modules from main.rs" |
| Sweep | Fix a cluster of related bugs | Bug tracker, gap doc | "Multi-cursor selection bugs" |
This prevents scope creep and keeps AI contributions coherent.
Documentation Structure
Three types of docs drive the work:
Reference Documentation — A source of truth for cross-cutting concerns. EDITOR_UI_REFERENCE.md defines viewport math, coordinate systems, cursor behavior, and scrolling semantics. Before implementation, the Oracle reviewed this doc and found 15+ issues: off-by-one errors in viewport calculations, division-by-zero edge cases, semantic inconsistencies between documented and actual selection models.
Feature Specifications — Written before implementation. SELECTION_MULTICURSOR.md defined data structures, invariants, keyboard shortcuts, message enums, and a phased implementation plan.
Gap Documents — For features at 60-90% completion. MULTI_CURSOR_SELECTION_GAPS.md listed what's implemented vs. missing, design decisions needed, and success criteria. Turns vague incompleteness into concrete tasks.
Agent Configuration
AGENTS.md tells agents how to work in your codebase: build commands, architecture patterns, conventions. Specifying
make test instead of letting agents invent cargo test --all-features --no-fail-fast saves a lot of friction.
Development Timeline
| Phase | Dates | Focus |
|---|---|---|
| Foundation | Dec 3-5 | Setup, reference docs, architecture |
| Feature Development | Dec 6 | Split view, undo/redo, multi-cursor selection |
| Codebase Refactor | Dec 6 | Extract modules from main.rs (3100→20 lines) |
| Research & Polish | Dec 7 | Zed research, cursor API fixes, test extraction |
| Keymapping System | Dec 15 | Configurable YAML keybindings, 74 default bindings |
| Syntax Highlighting | Dec 15 | Tree-sitter integration, 17 languages |
| CSV Viewer/Editor | Dec 16 | Spreadsheet view with cell editing |
| Workspace Management | Dec 17 | Sidebar file tree, focus system, global shortcuts |
| Unified Text Editing | Dec 19 | EditableState system for modals and CSV cells |
Example: Multi-Cursor Implementation
Adding multi-cursor to a single-cursor editor touches nearly every file. Here's how it was structured:
1. Invariants upfront:
// MUST maintain: cursors.len() == selections.len()
// MUST maintain: cursors[i].to_position() == selections[i].head
2. Migration helpers:
impl AppModel {
pub fn cursor(&self) -> &Cursor { &self.editor.cursors[0] }
}
Old code keeps working via the accessor while new code uses explicit indexing.
3. Phased implementation:
- Phase 0: Per-cursor primitives (
move_cursor_left_at(idx)) - Phase 1: All-cursor wrappers (
move_all_cursors_left()) - Phase 2-4: Update handlers, add tests
- Phase 5: Bug sweep
The issue was straightforward: all cursor movement handlers used .cursor_mut() which only returned cursors[0]. The
fix was adding per-index primitives, then wrapping them in all-cursor helpers that call deduplicate_cursors() after
each movement. Test count went from 351 to 383.
Threads: T-d4c75d42, T-6c1b5841, T-e751be48
Example: Split View Implementation
Split view was implemented in 7 phases across a single thread (T-29b1dd08):
| Phase | Description |
|---|---|
| 1 | Core data structures: ID types, EditorArea, Tab, EditorGroup, LayoutNode |
| 2 | Layout system: compute_layout(), group_at_point(), splitter hit testing |
| 3 | Update AppModel: Replace Document/EditorState with EditorArea, add accessors |
| 4 | Messages: LayoutMsg enum, split/close/focus operations, 17 tests |
| 5 | Rendering: Multi-group rendering, tab bars, splitters, focus indicators |
| 6 | Document sync: Shared document architecture (edits affect all views) |
| 7 | Keyboard shortcuts: Cmd+\, Cmd+W, Cmd+1/2/3/4, Ctrl+Tab |
Key architectural decision: documents are shared (HashMap<DocumentId, Document>), editors are view-specific
(HashMap<EditorId, EditorState>). Multiple editors can view the same document with independent cursors and viewports.
Example: Module Extraction
By December 6th, main.rs had grown to 3,100 lines. A series of extraction sessions (T-ce688bab through T-072af2cb) broke it apart:
update_layoutand helpers →update/layout.rsupdate_documentand undo/redo →update/document.rsupdate_editor→update/editor.rsRenderer→view.rsPerfStats→perf.rshandle_key→input.rsAppandApplicationHandler→app.rs
After: main.rs was 20 lines. All 669 tests passing.
Example: Keymapping System
Research phase (T-35b11d40) compared how VSCode, Helix, Zed, and Neovim handle configurable keymaps:
| Editor | Pattern | Key Insight |
|---|---|---|
| VSCode | Flat vector + context matching | User overrides via insertion order |
| Helix | Trie-based | Efficient prefix matching for modal editing |
| Zed | Flat with depth indexing | Context depth + insertion order for precedence |
| Neovim | Lua scripting | Full programmability |
Implementation (T-019b217e) resulted in:
- 74 default bindings in
keymap.yaml, embedded at compile time - Platform-aware
cmdmodifier (Cmd on macOS, Ctrl elsewhere) - Context conditions:
has_selection,has_multiple_cursors,modal_active - Chord support with
KeyAction::{Execute, AwaitMore, NoMatch} command: Unboundpattern to disable defaults- User config layering: embedded defaults → project-local → user config
The refactor reduced input.rs from 477 to 220 lines.
Cross-Platform Bug Example
Thread T-519a8c9d: Cmd+Z was inserting 'z' instead of undoing on macOS.
Root cause: key handler only checked control_key(), not super_key() (macOS Command key).
// Before (broken on macOS)
if modifiers.control_key() && key == "z" { ... }
// After (cross-platform)
if (modifiers.control_key() || modifiers.super_key()) && key == "z" { ... }
Thread Reference
All 116 conversation threads are public at ampcode.com/@helgesverre. The full thread list with summaries is in docs/BUILDING_WITH_AI.md.
Notable threads:
- T-7b92a860 — UI Reference doc creation and Oracle review (found 15+ issues before implementation)
- T-35b11d40 — Keymap system research
- T-29b1dd08 — Split view (all 7 phases)
- T-d4c75d42 — Multi-cursor movement
- T-019b22cc — Syntax highlighting with tree-sitter
- T-019b2783 — CSV viewer with cell editing
- T-019b3643 — Unified text editing system
Token is MIT licensed at github.com/HelgeSverre/token.
