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

15 KiB

name, description, license, compatibility, metadata
name description license compatibility metadata
gim Guidelines for working with the Gim vim-like terminal text editor Proprietary opencode
language framework module
Go 1.25.5 BubbleTea 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.

// 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/)

// 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:

    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():

    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:

    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:

    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:

    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:

    actions: map[string]action.Action{
        "X": action.MyAction{},
    }
    

New Ex Command (:mycommand)

  1. Add handler in internal/command/handlers.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():

    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)

// 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

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

# 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