Files
lime/PLAN.md
2026-06-21 20:08:56 -05:00

964 lines
18 KiB
Markdown

# Lime Implementation Plan
Lime is a clean, modern terminal/TUI text editor written in Rust. It should feel approachable like Nano, efficient and clean like Vim, and easy to use like VS Code, while using a standard non-modal cursor editing model.
This plan intentionally avoids time estimates so it can be handed directly to an implementation agent.
## 1. Product Direction
Initial product goal:
```bash
lime path/to/file.rs
```
opens a clean single-file terminal editor with:
- Standard typing/editing behavior
- Save and quit shortcuts
- Unsaved-changes confirmation
- Syntax highlighting
- Line numbers
- Status bar and help bar
- File search/open popup with `Ctrl-F`
- Recursive file discovery from the current working directory
- Large-file warning before opening very large files
- Good behavior on macOS and Linux
Lime should not be modal like Vim. It should be keyboard-first, but obvious and friendly.
## 2. Recommended Technical Stack
Use a Rust-native terminal stack:
- `crossterm` for terminal control and input events
- `ratatui` for layout, widgets, status bars, popups, and drawing
- `ropey` for text storage
- `tree-sitter` for syntax highlighting
- `ignore` for recursive file scanning with `.gitignore` support
- `nucleo-matcher` for fuzzy file matching
Do not build a custom terminal renderer at first. `ratatui + crossterm` gives enough control for a polished TUI while keeping implementation manageable.
Suggested dependencies:
```toml
[dependencies]
anyhow = "1"
thiserror = "1"
clap = { version = "4", features = ["derive"] }
ropey = "1"
crossterm = "0.28"
ratatui = "0.29"
unicode-segmentation = "1"
unicode-width = "0.2"
tree-sitter = "0.24"
tree-sitter-rust = "0.23"
tree-sitter-javascript = "0.23"
tree-sitter-typescript = "0.23"
tree-sitter-python = "0.23"
tree-sitter-json = "0.23"
tree-sitter-toml-ng = "0.6"
tree-sitter-md = "0.3"
ignore = "0.4"
nucleo-matcher = "0.3"
notify = "7"
serde = { version = "1", features = ["derive"] }
toml = "0.8"
dirs = "5"
```
Optional later:
```toml
arboard = "3"
similar = "2"
tempfile = "3"
insta = "1"
```
## 3. Repository Structure
Use a Cargo workspace from the beginning:
```txt
lime/
Cargo.toml
README.md
LICENSE
PLAN.md
crates/
lime-core/
Cargo.toml
src/
lib.rs
buffer.rs
cursor.rs
selection.rs
edit.rs
history.rs
movement.rs
search.rs
file.rs
command.rs
lime-syntax/
Cargo.toml
src/
lib.rs
language.rs
highlighter.rs
theme.rs
lime-ui/
Cargo.toml
src/
lib.rs
app.rs
terminal.rs
layout.rs
editor_view.rs
status_bar.rs
command_bar.rs
file_picker.rs
prompt.rs
theme.rs
input.rs
lime-cli/
Cargo.toml
src/
main.rs
```
Root `Cargo.toml`:
```toml
[workspace]
members = [
"crates/lime-core",
"crates/lime-syntax",
"crates/lime-ui",
"crates/lime-cli"
]
resolver = "2"
```
The installed binary should be named `lime`.
## 4. Crate Responsibilities
### `lime-core`
Pure editor logic.
Must not depend on terminal UI, Ratatui, themes, syntax highlighting, or input events.
Responsibilities:
- Text buffer
- Cursor state
- Selections, even if selection editing comes later
- Editing operations
- Undo/redo
- File load/save
- Dirty state
- Search inside the current buffer
- Line/column movement
- Large-file metadata and open policy types
### `lime-syntax`
Syntax parsing and highlighting.
Responsibilities:
- Language detection
- Tree-sitter parser setup
- Highlight spans for visible ranges
- Theme scopes
- Graceful fallback for unknown languages or parser failures
### `lime-ui`
Terminal application.
Responsibilities:
- Raw mode
- Alternate screen
- Event loop
- Keybindings
- Rendering
- Editor viewport
- Popups
- File picker
- Status bar
- Help bar
- Prompts and warnings
### `lime-cli`
CLI entry point.
Responsibilities:
- Parse CLI arguments
- Resolve starting path
- Load config
- Start app
- Ensure terminal cleanup on error or panic
## 5. Core Data Model
Use `ropey::Rope` for buffer storage.
```rust
pub struct TextBuffer {
rope: Rope,
path: Option<PathBuf>,
dirty: bool,
line_ending: LineEnding,
}
```
Track positions as line/column character positions, not byte offsets.
```rust
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Position {
pub line: usize,
pub column: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Cursor {
pub position: Position,
pub preferred_column: Option<usize>,
}
```
Use `preferred_column` for vertical movement so up/down preserve the intended visual column across lines of different lengths.
Also define:
```rust
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TextRange {
pub start: Position,
pub end: Position,
}
pub enum LineEnding {
Lf,
Crlf,
}
```
## 6. Editing Commands
Create a command abstraction in `lime-core`:
```rust
pub enum EditorCommand {
InsertChar(char),
InsertText(String),
Newline,
Backspace,
Delete,
MoveLeft,
MoveRight,
MoveUp,
MoveDown,
MoveLineStart,
MoveLineEnd,
MoveFileStart,
MoveFileEnd,
PageUp,
PageDown,
Save,
Undo,
Redo,
Search(String),
}
```
The UI should convert terminal key events into app actions or editor commands. The core applies commands and returns effects.
```rust
pub enum CommandResult {
None,
Modified,
Saved,
CursorMoved,
NeedsPath,
}
```
## 7. Undo/Redo
Implement undo/redo early.
Represent edits as reversible transactions:
```rust
pub struct EditTransaction {
pub edits: Vec<TextEdit>,
pub before_cursor: Cursor,
pub after_cursor: Cursor,
}
pub enum TextEdit {
Insert {
at: Position,
text: String,
},
Delete {
range: TextRange,
deleted_text: String,
},
}
```
Typing consecutive characters should be grouped into one undo transaction until one of these happens:
- Cursor movement
- Newline
- Backspace/delete
- Save
- Different command type
- Focus changes to a popup/prompt
## 8. Large File Handling
Before reading a file, inspect metadata.
Recommended thresholds:
```txt
<= 5 MB: open normally
5-25 MB: open with warning prompt
> 25 MB: require explicit confirmation
> 100 MB: refuse by default unless --force
```
CLI/TUI warning example:
```txt
This file is 84 MB. Lime may be slower with very large files.
Open anyway? [y/N]
```
Core policy type:
```rust
pub struct FileOpenPolicy {
pub warn_threshold_bytes: u64,
pub confirm_threshold_bytes: u64,
pub hard_threshold_bytes: u64,
pub force: bool,
}
```
The file picker should also use this policy before replacing the current buffer.
## 9. Terminal UI Layout
Default layout:
```txt
┌────────────────────────────────────────────┐
│ 1 │ fn main() { │
│ 2 │ println!("hello"); │
│ 3 │ } │
│ │ │
│ │ │
├────────────────────────────────────────────┤
│ lime file.rs rust Ln 2, Col 5 modified │
│ Ctrl-S Save Ctrl-F Files Ctrl-Q Quit │
└────────────────────────────────────────────┘
```
Main regions:
1. Editor viewport
2. Status bar
3. Help/action bar
4. Optional popup overlay
Use `ratatui::Layout` for the main shell.
## 10. Rendering Strategy
Render only visible lines.
UI viewport state:
```rust
pub struct Viewport {
pub top_line: usize,
pub left_col: usize,
pub height: usize,
pub width: usize,
}
```
Editor rendering should:
- Render visible lines only
- Draw line numbers
- Draw cursor
- Draw selected text later
- Apply syntax spans when available
- Horizontally scroll for long lines
- Vertically scroll when the cursor nears viewport edges
- Avoid allocating the entire file each frame
Gutter width:
```rust
let gutter_width = total_lines.to_string().len() + 2;
```
## 11. Input Model
Recommended shortcuts:
```txt
Ctrl-S Save
Ctrl-Q Quit
Ctrl-F File picker
Ctrl-G Go to line
Ctrl-Z Undo
Ctrl-Y Redo
Ctrl-A Start of line
Ctrl-E End of line
Ctrl-R Search in current file
Esc Close popup/cancel prompt
Arrow keys Move cursor
PageUp Page up
PageDown Page down
Home Start of line
End End of line
Backspace Delete backward
Delete Delete forward
Enter Newline
Tab Insert spaces according to config
```
Use an app-level action enum:
```rust
pub enum AppAction {
Editor(EditorCommand),
Save,
Quit,
OpenFilePicker,
OpenGoToLine,
OpenSearch,
ClosePopup,
Confirm,
Cancel,
}
```
Do not hardcode behavior directly inside the event loop. Route input through a mapping layer.
## 12. File Picker
`Ctrl-F` opens a centered popup listing files recursively from the current directory.
Behavior:
- Scan current working directory or selected workspace root
- Respect `.gitignore`
- Skip hidden files by default
- Skip common bulky directories:
- `.git`
- `target`
- `node_modules`
- `dist`
- `.next`
- `build`
- Fuzzy filter as the user types
- Arrow up/down changes selection
- Enter opens selected file
- Escape closes picker
- Warn if current file has unsaved changes before replacing it
- Warn before opening very large files
Popup layout:
```txt
╭─ Open File ─────────────────────────────╮
│ search: main │
│ │
│ > crates/lime-cli/src/main.rs │
│ crates/lime-core/src/buffer.rs │
│ README.md │
╰─────────────────────────────────────────╯
```
State:
```rust
pub struct FilePickerState {
pub query: String,
pub all_files: Vec<PathBuf>,
pub matches: Vec<FileMatch>,
pub selected: usize,
}
pub struct FileMatch {
pub path: PathBuf,
pub score: u32,
}
```
## 13. Syntax Highlighting
Start with these languages:
- Rust
- JavaScript
- TypeScript
- Python
- JSON
- TOML
- Markdown
Language detection:
```rust
pub enum Language {
Rust,
JavaScript,
TypeScript,
Python,
Json,
Toml,
Markdown,
PlainText,
}
```
Detection should use file extension first.
Highlighting should be resilient:
- Unknown language falls back to plain text
- Parser failure falls back to plain text
- Initial implementation may highlight visible ranges only
- Incremental parsing can come later
Theme model:
```rust
pub struct Theme {
pub background: Color,
pub foreground: Color,
pub gutter: Color,
pub cursor: Color,
pub selection: Color,
pub keyword: Color,
pub string: Color,
pub comment: Color,
pub function: Color,
pub type_name: Color,
pub number: Color,
pub error: Color,
}
```
Use a clean default dark theme with calm contrast.
## 14. Save/Quit Behavior
If there are no unsaved changes, `Ctrl-Q` exits immediately.
If the buffer is dirty:
```txt
Unsaved changes. Save before quitting? [s]ave [d]iscard [c]ancel
```
If saving an unnamed buffer:
```txt
Save as: _
```
Save operation should:
- Write to a temporary file
- Flush data
- Rename over original path
- Preserve line endings where practical
- Clear dirty flag after success
- Show a status message after success or failure
## 15. Config
Keep config simple at first.
Locations:
```txt
macOS: ~/Library/Application Support/lime/config.toml
Linux: ~/.config/lime/config.toml
```
Initial config:
```toml
theme = "lime-dark"
tab_width = 4
insert_spaces = true
show_line_numbers = true
confirm_large_files = true
```
Rust model:
```rust
pub struct Config {
pub theme: String,
pub tab_width: usize,
pub insert_spaces: bool,
pub show_line_numbers: bool,
pub confirm_large_files: bool,
}
```
If config is missing or invalid, use defaults and show a non-blocking warning.
## 16. Error Handling
Use `anyhow` at app/CLI boundaries.
Use `thiserror` for library errors.
Example:
```rust
#[derive(thiserror::Error, Debug)]
pub enum LimeError {
#[error("file is too large: {size} bytes")]
FileTooLarge { size: u64 },
#[error("buffer has no file path")]
MissingPath,
#[error("invalid cursor position")]
InvalidPosition,
}
```
Terminal cleanup must always happen.
Use a guard:
```rust
pub struct TerminalGuard;
impl Drop for TerminalGuard {
fn drop(&mut self) {
// disable raw mode
// leave alternate screen
// show cursor
}
}
```
Never leave the terminal in raw mode after an error or panic.
## 17. CLI Design
Basic usage:
```bash
lime
lime file.rs
lime .
lime --force huge.log
lime --config path/to/config.toml
```
CLI args:
```rust
#[derive(Parser)]
pub struct Cli {
pub path: Option<PathBuf>,
#[arg(long)]
pub force: bool,
#[arg(long)]
pub config: Option<PathBuf>,
}
```
Behavior:
- No path: open empty unnamed buffer
- File path: open file
- Directory path: open empty buffer with file picker rooted at that directory
- Nonexistent file: create a new buffer with that path
- Huge file: warn/confirm unless `--force`
## 18. Testing Strategy
### `lime-core`
Most tests should live here.
Test:
- Insert characters
- Insert text
- Insert newline
- Backspace in middle of line
- Backspace at start of line joins lines
- Delete in middle of line
- Delete at end of line joins lines
- Cursor movement left/right/up/down
- Line start/end
- File start/end
- Undo/redo
- Search
- Save/load roundtrip
- Unicode text
- Tabs
- Long lines
- Empty file behavior
Example test:
```rust
#[test]
fn backspace_joins_lines() {
let mut editor = Editor::from_text("hello\nworld");
editor.set_cursor(Position { line: 1, column: 0 });
editor.backspace();
assert_eq!(editor.text(), "helloworld");
assert_eq!(editor.cursor().position, Position { line: 0, column: 5 });
}
```
### `lime-syntax`
Test:
- Language detection
- Parser setup does not panic
- Highlight spans are valid
- Unknown files fall back to plain text
### `lime-ui`
Test pure/non-terminal pieces:
- Keybinding mapping
- Viewport scrolling
- File picker filtering
- Status string formatting
- Layout calculations
- Prompt state transitions
Avoid depending heavily on full terminal snapshot tests in the first version.
## 19. Implementation Order
### Step 1: Create workspace
Create all workspace crates and make `cargo test` pass.
Deliverables:
- Root `Cargo.toml`
- `lime-core`
- `lime-syntax`
- `lime-ui`
- `lime-cli`
- Minimal compiling binary named `lime`
### Step 2: Implement `lime-core`
Implement:
- `TextBuffer`
- `Position`
- `TextRange`
- `Cursor`
- Load from string
- Load from file
- Save to file
- Insert char/text
- Newline
- Backspace
- Delete
- Cursor movement
- Dirty flag
Add comprehensive tests.
### Step 3: Implement undo/redo
Add:
- `EditTransaction`
- Undo stack
- Redo stack
- Transaction grouping for typing
- Tests for edit operations and grouped undo behavior
### Step 4: Implement basic terminal app
In `lime-ui`, implement:
- Raw mode
- Alternate screen
- Panic-safe cleanup
- Event loop
- Render empty editor
- Render file content
- Status bar
- Help bar
- Cursor rendering
At this point:
```bash
cargo run -p lime-cli -- README.md
```
should open a navigable file.
### Step 5: Wire editing input
Add keyboard handling for:
- Text input
- Enter
- Tab
- Backspace
- Delete
- Arrow movement
- Home/end
- Page up/down
- Save
- Quit
After this step, Lime should be a usable single-file editor.
### Step 6: Add viewport scrolling
Implement:
- Vertical scroll
- Horizontal scroll
- Cursor-follow behavior
- Page up/down
- Long-line handling
Do not render the whole file.
### Step 7: Add file open policy
Before reading files:
- Check size
- Show warning prompt in TUI
- Support `--force`
- Refuse extreme files by default
### Step 8: Add file picker
Implement:
- Recursive file scan
- Fuzzy matching
- Popup rendering
- Query input
- Selection movement
- Opening selected files
- Dirty-buffer confirmation before replacing current file
Bind to `Ctrl-F`.
### Step 9: Add syntax highlighting
Implement `lime-syntax`.
Start with:
- Language detection
- Rust highlighting
- Plain-text fallback
Then add:
- JavaScript
- TypeScript
- Python
- JSON
- TOML
- Markdown
Integrate highlighting into editor rendering.
### Step 10: Add quality-of-life features
Add:
- Go to line popup
- Search in current file
- Unsaved quit prompt
- Save-as prompt
- Status messages
- Error display
- Config loading
- Tab width setting
- Insert spaces setting
## 20. Minimum Definition of Done
The first complete version should support:
```bash
lime file.rs
```
and include:
- Clean terminal UI
- Standard text editing
- Natural cursor movement
- Save with `Ctrl-S`
- Quit with `Ctrl-Q`
- Unsaved changes warning
- Syntax highlighting for known languages
- File picker with `Ctrl-F`
- Recursive current-directory browsing
- Fuzzy file filtering
- Moderately large file handling without freezing
- Warning before opening very large files
- Passing core editing tests
- macOS and Linux support
## 21. Explicit Non-Goals For First Version
Do not implement these yet:
- Plugin system
- Extension marketplace
- LSP
- Multi-pane editor
- Integrated terminal
- Git UI
- Debugger
- AI features
- Remote editing
- Modal Vim emulation
These can be added later only after the core editor feels excellent.