Gim/.opencode/skills/gim/SKILL.md
2026-04-06 19:24:47 -07:00

466 lines
15 KiB
Markdown

---
name: gim
description: Guidelines for working with the Gim vim-like terminal text editor
license: Proprietary
compatibility: opencode
metadata:
language: Go 1.25.5
framework: BubbleTea
module: git.gophernest.net/azpect/TextEditor
---
# Gim — Agent Skill Guide
Gim is a vim-like terminal text editor written in Go using the BubbleTea TUI framework. This document gives AI agents everything needed to contribute effectively.
---
## Project Identity
| Field | Value |
|-------|-------|
| Module path | `git.gophernest.net/azpect/TextEditor` |
| Go version | 1.25.5 |
| Entry point | `cmd/gim/main.go` |
| TUI framework | BubbleTea v1.3.10 |
| Styling | Lipgloss v1.1.0 |
| Syntax highlighting | Chroma v2.23.1 |
| Testing | teatest (charmbracelet/x/exp/teatest) |
---
## Directory Layout
```
Gim/
├── cmd/gim/
│ └── main.go # Binary entry point
├── internal/
│ ├── action/ # Executable actions (implement Action interface)
│ │ ├── interface.go # Core interfaces: Action, Motion, Operator, etc.
│ │ ├── insert.go # Insert mode entry actions (i, a, o, O, I, A, C, s, S)
│ │ ├── delete.go # Delete actions (x, X, dd, D)
│ │ ├── replace.go # Replace mode actions (r, R)
│ │ ├── find.go # Find char motions (f, F, t, T, ;, ,)
│ │ ├── command.go # Command mode state management
│ │ ├── command_history.go # Command history navigation
│ │ ├── paste.go # Paste (p, P)
│ │ ├── yank.go # Yank (y, yy, Y)
│ │ ├── undo.go # Undo/redo (u, ctrl+r)
│ │ ├── visual.go # Visual mode entry (v, V, ctrl+v)
│ │ ├── scroll.go # Scroll actions (ctrl+d, ctrl+u, etc.)
│ │ └── repeat.go # Dot operator (.)
│ ├── command/ # Ex command mode (:) registry and handlers
│ │ ├── registry.go # Command registration and lookup
│ │ └── handlers.go # Handler functions (:w, :q, :wq, etc.)
│ ├── core/ # Shared data structures and types
│ │ ├── types.go # Mode, MotionType, RegisterType enums
│ │ ├── buffer.go # Buffer struct and operations
│ │ ├── gap_buffer.go # GapBuffer (efficient per-line editing)
│ │ ├── window.go # Window (viewport over a buffer)
│ │ ├── position.go # Position{Line, Col} type
│ │ ├── register.go # Register (vim clipboard) types
│ │ ├── undo.go # UndoStack (UndoStack, Change, ChangeBlock)
│ │ ├── settings.go # EditorSettings (NewDefaultSettings)
│ │ └── command.go # Command parsing helpers
│ ├── editor/ # BubbleTea Model — orchestrates everything
│ │ ├── model.go # Model struct definition
│ │ ├── init.go # tea.Model Init()
│ │ ├── update.go # tea.Model Update()
│ │ ├── view.go # tea.Model View()
│ │ ├── builder.go # ModelBuilder (fluent constructor)
│ │ ├── helpers_test.go # Shared test helpers (all tests use these)
│ │ └── integration_*_test.go # Integration tests by feature area
│ ├── input/ # Key-input state machine
│ │ ├── handler.go # InputState machine, key dispatch
│ │ └── keymap.go # Keybinding registries per mode
│ ├── motion/ # Motion implementations
│ │ ├── basic.go # h, j, k, l
│ │ ├── jump.go # gg, G, 0, ^, $, %, {, }
│ │ └── word.go # w, W, b, B, e, E
│ ├── operator/ # Operator implementations
│ │ ├── delete.go # d{motion}, dd
│ │ ├── yank.go # y{motion}, yy
│ │ └── change.go # c{motion}, cc
│ ├── program/ # BubbleTea program bootstrap
│ │ └── program_builder.go # ProgramBuilder (EmptyProgram, FileProgram)
│ ├── style/ # Visual rendering
│ │ └── style.go # Lipgloss styles, Chroma syntax highlighting
│ ├── textobject/ # Text object ranges
│ │ └── word.go # iw, aw, iW, aW
│ └── theme/ # Color themes
│ └── theme.go # RegisterAll(), kanagawa-wave default
```
---
## Key Interfaces (`internal/action/interface.go`)
All extension points in the editor are interface-based. Implementing the right interface is how new features are added.
```go
// Action — any executable editor command
type Action interface {
Execute(m Model) tea.Cmd
}
// Motion — moves the cursor; returns a range type
type Motion interface {
Action
Type() core.MotionType // CharwiseExclusive | CharwiseInclusive | Linewise
}
// Operator — acts on a range defined by a motion
type Operator interface {
Operate(m Model, start, end core.Position, mtype core.MotionType) tea.Cmd
}
// DoublePresser — supports the doubled form (dd, yy, cc)
type DoublePresser interface {
DoublePress(m Model, count int) tea.Cmd
}
// Repeatable — action that accepts a leading count (3w, 5dd)
type Repeatable interface {
WithCount(n int) Action
}
// CharMotion — motion that needs a char argument (f, F, t, T)
type CharMotion interface {
Motion
WithChar(char string) Motion
}
// Resolvable — motion whose behavior depends on model state at execution time
type Resolvable interface {
Motion
Resolve(m Model) Motion
}
// TextObject — selects a structured range (iw, aw, iW, aW)
type TextObject interface {
GetRange(m Model, cursor core.Position, modifier string) (start, end core.Position, mtype core.MotionType)
}
```
`action.Model` is the main interface that all actions receive — it provides access to windows, buffers, mode, registers, and input dispatch.
---
## Core Types (`internal/core/`)
```go
// Position — line and column (both 0-indexed)
type Position struct{ Line, Col int }
// Mode
type Mode int
const (
NormalMode, InsertMode, CommandMode, CommandOutputMode,
VisualMode, VisualLineMode, VisualBlockMode, ReplaceMode, WaitingMode
)
// MotionType — determines how operator ranges are calculated
type MotionType int
const (
CharwiseExclusive // end col not included (w, b, h, l, 0, ^)
CharwiseInclusive // end col included (e, $, f)
Linewise // whole lines (j, k, G, gg, {, })
)
// RegisterType
type RegisterType int
const (
CharwiseRegister, LinewiseRegister, BlockwiseRegister
)
// Buffer — document held in memory
type Buffer struct {
Id, Type int
Filename string
Filetype string
Lines []*GapBuffer
Modified bool
UndoStack *UndoStack
}
// Window — viewport over a buffer
type Window struct {
Id, Number int
Buffer *Buffer
Cursor Position
Anchor Position // visual mode anchor
ScrollY int
Height int
Width int
}
```
---
## Adding Features — Step-by-Step Patterns
### New Normal-Mode Motion
1. Create `internal/motion/<name>.go`:
```go
package motion
type MyMotion struct{ Count int }
func (m MyMotion) Execute(model action.Model) tea.Cmd {
// Move model.ActiveWindow().Cursor
return nil
}
func (m MyMotion) Type() core.MotionType { return core.CharwiseExclusive }
func (m MyMotion) WithCount(n int) action.Action { return MyMotion{Count: n} }
```
2. Register in `internal/input/keymap.go` inside `NewNormalKeymap()`:
```go
motions: map[string]action.Motion{
"X": motion.MyMotion{},
}
```
3. Write integration tests in `internal/editor/integration_motion_<name>_test.go`.
### New Operator
1. Create `internal/operator/<name>.go`:
```go
package operator
type MyOp struct{}
func (o MyOp) Operate(m action.Model, start, end core.Position, mt core.MotionType) tea.Cmd {
// Act on the range [start, end)
return nil
}
// Double-press support (e.g. "xx" acts on current line)
var _ action.DoublePresser = MyOp{}
func (o MyOp) DoublePress(m action.Model, count int) tea.Cmd { return nil }
```
2. Register in `internal/input/keymap.go`:
```go
operators: map[string]action.Operator{
"X": operator.MyOp{},
}
```
3. Write integration tests in `internal/editor/integration_operator_<name>_test.go`.
### New Standalone Action
1. Create `internal/action/<name>.go`:
```go
package action
type MyAction struct{ Count int }
func (a MyAction) Execute(m Model) tea.Cmd {
// Mutate model state
return nil
}
func (a MyAction) WithCount(n int) Action { return MyAction{Count: n} }
```
2. Register in `internal/input/keymap.go`:
```go
actions: map[string]action.Action{
"X": action.MyAction{},
}
```
### New Ex Command (`:mycommand`)
1. Add handler in `internal/command/handlers.go`:
```go
func handleMyCommand(m action.Model, args []string, force bool) tea.Cmd {
// force = true when called as :mycommand!
return nil
}
```
2. Register in `internal/command/registry.go` inside `registerDefaults()`:
```go
r.Register(Command{
Name: "mycommand",
ShortForm: "myc",
Handler: handleMyCommand,
})
```
---
## Testing
### Framework
All tests use **teatest** — a BubbleTea test driver that runs the full model loop. Integration tests live in `internal/editor/`. Unit tests for isolated logic live alongside their package (`*_test.go` next to the source).
### Shared Test Helpers (`internal/editor/helpers_test.go`)
```go
// Create a default test model
newTestModel(t *testing.T) *teatest.TestModel
// Create a model with specific initial buffer content
newTestModelWithLines(t *testing.T, lines []string) *teatest.TestModel
// Create a model with content and cursor at a specific position
newTestModelWithLinesAndCursorPos(t *testing.T, lines []string, pos core.Position) *teatest.TestModel
// Functional options variant (preferred for complex setups)
newTestModelWithOptions(t *testing.T, opts ...TestModelOption) *teatest.TestModel
// Options:
WithLines(lines []string)
WithCursorPos(pos core.Position)
WithTermSize(width, height int)
WithRegister(name rune, regType core.RegisterType, content []string)
// Send one or more keys to the model
sendKeys(tm *teatest.TestModel, keys ...string)
// Send a string of keys as a sequence
sendKeyString(tm *teatest.TestModel, keyString string)
// Retrieve the final model state for assertions
getFinalModel(t *testing.T, tm *teatest.TestModel) *editor.Model
```
### Test File Naming Convention
| File | Coverage area |
|------|---------------|
| `integration_motion_basic_test.go` | h, j, k, l |
| `integration_motion_jump_test.go` | gg, G, 0, $, %, {, } |
| `integration_motion_word_test.go` | w, W, b, B, e, E |
| `integration_operator_delete_test.go` | d{motion}, dd |
| `integration_operator_change_test.go` | c{motion}, cc |
| `integration_operator_yank_test.go` | y{motion}, yy |
| `integration_insert_test.go` | i, a, o, O, I, A, C, s, S |
| `integration_delete_test.go` | x, X, D, dd |
| `integration_visual_test.go` | v, V, ctrl+v |
| `integration_paste_test.go` | p, P |
| `integration_yank_test.go` | y, yy, Y |
| `integration_repeat_test.go` | . (dot operator) |
| `integration_replace_test.go` | r, R |
| `integration_undo_test.go` | u, ctrl+r |
| `integration_scroll_test.go` | ctrl+d, ctrl+u, etc. |
| `integration_command_test.go` | :w, :q, :wq, etc. |
| `integration_textobject_test.go` | iw, aw, iW, aW |
### Example Integration Test
```go
func TestMyFeature(t *testing.T) {
t.Run("moves forward one word", func(t *testing.T) {
tm := newTestModelWithLines(t, []string{"hello world"})
sendKeys(tm, "w")
m := getFinalModel(t, tm)
pos := m.ActiveWindow().Cursor
if pos.Col != 6 {
t.Errorf("expected col 6, got %d", pos.Col)
}
})
}
```
Run all tests:
```
go test ./...
```
Run a specific package:
```
go test git.gophernest.net/azpect/TextEditor/internal/editor
```
---
## Input State Machine (`internal/input/handler.go`)
The handler dispatches key presses through a state machine. Understanding states is critical when tracing how a key sequence is processed.
| State | Meaning |
|-------|---------|
| `StateReady` | Waiting for first key of a command |
| `StateCount` | Accumulating leading count digits (e.g. `12`) |
| `StateOperatorPending` | Operator received, waiting for motion (e.g. after `d`) |
| `StateMotionCount` | Accumulating count after operator (e.g. `d3`) |
| `StateWaitingForChar` | f/t/F/T received, waiting for target char |
| `StateWaitingForTextObject` | i/a received in operator pending, waiting for object (w/W) |
---
## BubbleTea Elm Architecture
```
tea.KeyMsg
editor.Update()
input.Handler.Handle(key)
action.Action.Execute(model) ← motions, operators, actions all land here
model state mutation
editor.View() ← renders buffer with Lipgloss + Chroma
```
The editor model implements `tea.Model`:
- `Init() tea.Cmd` — returns nil (no startup IO)
- `Update(msg tea.Msg) (tea.Model, tea.Cmd)` — handles key, resize, and mouse messages
- `View() string` — renders current state as a string
---
## Code Conventions
- **Comment format:** `// ReceiverType.MethodName: description` on every exported method
- **Builder pattern** for complex structs — `NewBufferBuilder().WithFilename(f).Build()`
- **Functional options** for test setup — `WithLines(...)`, `WithCursorPos(...)`
- **Interface guards** for compile-time checks: `var _ action.DoublePresser = MyOp{}`
- **No panics** in normal paths — return early or propagate via `tea.Cmd`
- **No global mutable state** — all state lives inside `editor.Model`
- `tea.Cmd` return value is almost always `nil` unless async IO is needed
---
## Build & Run
```bash
# Run the editor (no file)
go run ./cmd/gim
# Run with a file
go run ./cmd/gim myfile.go
# Build binary
go build -o gim ./cmd/gim
# Run all tests
go test ./...
# Run tests with verbose output
go test -v ./internal/editor/...
```
---
## What NOT to Change Without Care
- `internal/core/types.go` — changing `MotionType` or `Mode` constants affects the entire codebase
- `internal/action/interface.go` — changing `Action` or `Model` interfaces requires updating all implementations
- `internal/input/keymap.go` — duplicate key registrations silently shadow each other; verify no conflicts
- `internal/editor/update.go` — the central dispatch loop; changes here affect all modes