editor

package
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 26, 2026 License: Apache-2.0 Imports: 8 Imported by: 0

Documentation

Overview

Package editor is baifo's embedded text editor component.

Why we wrote our own instead of using bubbles/textarea: the textarea in bubbles v2 is excellent for chat-style input but it does not expose a hook for per-line styling, which we need to render YAML syntax highlighting (PR B.2b) and to overlay autocomplete modals (PR B.2c). Fork or hack-with-ANSI-escapes were the alternatives; both cost more than writing a small editor of our own.

Scope of this iteration (B.2a):

  • rune-by-rune buffer with cursor + selection
  • arrow / home / end / pgup / pgdn / ctrl+home / ctrl+end navigation
  • selection with Shift+ navigation keys
  • copy / cut / paste via the bubbletea v2 OSC52 clipboard helpers
  • line numbers gutter, header title, footer with hotkeys + error bar
  • Ctrl+S triggers a user-supplied validator; errors stay in the footer and the buffer stays open. On success the editor emits SaveMsg and closes.
  • Esc with unsaved changes opens a confirmation modal ("Discard changes? y/N"). Destructive actions always confirm.

What does NOT live here yet (kept as hooks for future PRs):

  • LineStyler is wired but always nil in B.2a (highlighting in B.2b)
  • Triggers map is wired but always nil in B.2a (completions in B.2c)

The editor is content-agnostic. The caller passes the initial text, a title, and an OnSave func; YAML knowledge lives elsewhere.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type CancelMsg

type CancelMsg struct{}

CancelMsg is emitted when the user pressed Esc on a clean buffer, or confirmed the discard prompt. The parent Model closes the editor on receipt; the buffer is not delivered.

type Completion

type Completion struct {
	View   string
	Insert string
}

Completion is a single autocomplete entry. The View will be shown in the picker; Insert is what lands in the buffer when the user picks.

type CompletionContext

type CompletionContext struct {
	Line int
	Col  int

	// Lines is a snapshot of the whole buffer at trigger time, one
	// entry per row. Providers that need sibling context — e.g. a
	// `reasoning:` completer that wants to read the `model:` line
	// above it — scan this. Read-only; do not mutate.
	Lines []string
}

CompletionContext is what the editor passes to a CompletionProvider when its trigger fires. Kept lean on purpose; B.2c may extend it.

type CompletionProvider

type CompletionProvider func(prefix string, ctx CompletionContext) []Completion

CompletionProvider is the hook B.2c will use for autocomplete modals. Declared here so B.2a-callers can already wire empty maps.

type LineStyler

type LineStyler func(lineNum int, content string) []StyledSpan

LineStyler is the per-line tokenisation hook. The styler receives the full raw line and returns the styled spans the editor should paint. By returning spans rather than a pre-styled string, we let the editor compose highlighting with selection and cursor styles cleanly: the editor knows which byte ranges to override and which to delegate to the styler.

In B.2a this hook is nil (no styling). yamlhl plugs in here.

type Model

type Model struct {
	// contains filtered or unexported fields
}

Model is the BubbleTea component. Construct with New, drive with Update, render with View. The host Model owns the lifecycle and reads SaveMsg / CancelMsg to decide when to close the editor.

func New

func New(opts Options) Model

New builds a Model from Options. The cursor starts at (0,0); the buffer comes from Options.InitialValue.

func (*Model) Blur

func (m *Model) Blur()

Blur stops the editor from consuming key events. View() still renders the current state.

func (Model) Dirty

func (m Model) Dirty() bool

Dirty reports whether the buffer was modified since the last save or load. Useful for the host Model to render a "(modified)" marker in its own header if it likes.

func (*Model) Focus

func (m *Model) Focus()

Focus marks the editor as receiving keypresses. The default Model returned by New is already focused; Focus is here for symmetry with the rest of bubbles.

func (Model) Focused

func (m Model) Focused() bool

Focused reports whether the editor consumes keys.

func (Model) Init

func (Model) Init() tea.Cmd

Init implements tea.Model.

func (*Model) SetSize

func (m *Model) SetSize(width, height int)

SetSize updates the editor's outer dimensions. Sub-component sizes (viewport, footer area) are derived from these.

func (*Model) SetTitle

func (m *Model) SetTitle(t string)

SetTitle changes the header text.

func (*Model) SetValue

func (m *Model) SetValue(s string)

SetValue replaces the buffer and resets the cursor to (0, 0). Dirty state and validation errors are cleared too \u2014 SetValue is used to switch documents, not to mutate the current one.

func (Model) Update

func (m Model) Update(msg tea.Msg) (Model, tea.Cmd)

Update implements tea.Model. The editor handles every keystroke itself when focused; an unfocused editor ignores keys but still receives clipboard and resize messages so View renders correctly.

func (Model) Value

func (m Model) Value() string

Value returns the full buffer text joined with '\n' and without a trailing newline.

func (Model) View

func (m Model) View() string

View implements tea.Model. The layout is:

┌─ {title} ─────────────────────────── {modified-marker} ─┐
│  1  first line of the buffer                            │
│  2  second line, with the cursor here →█                │
│ ...                                                     │
│     (rest scrolls in the viewport)                      │
│ ⚠ validation error 1                                   │  (optional)
│ ⚠ validation error 2                                   │
└─ ctrl+s save · esc cancel · ? help ─────────────────────┘

The output is always EXACTLY m.height lines tall. Without this guarantee a paste that grows the buffer beyond the viewport can push the footer below the visible area on some terminals; we pad the body with empty lines to keep the chrome anchored.

type Options

type Options struct {
	// Title appears in the header bar. Defaults to "Editor".
	Title string

	// InitialValue is the buffer text on open. May be empty.
	InitialValue string

	// OnSave validates the buffer when the user hits Ctrl+S. Required;
	// passing nil here defaults to a no-op validator that always
	// allows save (useful for free-form edits like /config edit).
	OnSave Validator

	// LineStyler is the per-line render hook. nil in B.2a.
	LineStyler LineStyler

	// Triggers maps a literal substring (e.g. "${secret:") to its
	// completion provider. nil/empty in B.2a.
	Triggers map[string]CompletionProvider

	// RequireSaveConfirm, when true, makes Ctrl+S open a "Apply
	// changes? [y/N]" prompt before running OnSave. Use this for
	// editors that mutate critical state (config upserts).
	RequireSaveConfirm bool

	// Styles overrides the editor's visual palette. nil uses DefaultStyles().
	Styles *Styles
}

Options configures a Model at construction time. Fields are picked to match what the first real consumer (PR A's /config edit) needs.

type SaveMsg

type SaveMsg struct {
	// Value is the full buffer text, joined with '\n' line separators
	// and WITHOUT a trailing newline. Callers that need POSIX-style
	// "final newline" semantics should add one explicitly.
	Value string
}

SaveMsg is emitted when the user pressed Ctrl+S and OnSave reported no errors. The parent Model uses it to persist the buffer and close the editor overlay.

type StyledSpan

type StyledSpan struct {
	From  int
	To    int
	Style lipgloss.Style
}

StyledSpan is one (from, to) byte range with a lipgloss style. The byte indexing matches the raw string the styler received; the editor converts to rune positions when overlaying selection/cursor.

type Styles

type Styles struct {
	// Header and footer band (background + foreground + padding).
	Header lipgloss.Style
	// Gutter is the line-number column style.
	Gutter lipgloss.Style
	// ErrorLine is the validation-error footer style.
	ErrorLine lipgloss.Style
	// Selection is the highlighted text style.
	Selection lipgloss.Style
	// Cursor is the block cursor style.
	Cursor lipgloss.Style
	// SearchMatch styles a non-current search hit.
	SearchMatch lipgloss.Style
	// SearchCurrentMatch styles the active search hit.
	SearchCurrentMatch lipgloss.Style
	// Modal chrome colors for in-editor prompts (discard/save/help).
	ModalBorder  color.Color
	ModalTitleBg color.Color
	ModalTitleFg color.Color
	ModalText    color.Color
	ModalDim     color.Color
	// Completer popup colors.
	CompleterBg     color.Color
	CompleterSelBg  color.Color
	CompleterFg     color.Color
	CompleterDim    color.Color
	CompleterAccent color.Color
	CompleterBorder color.Color
}

Styles holds every visual property the editor uses internally. Build one via DefaultStyles() and override fields; nil Options.Styles falls back to DefaultStyles().

func DefaultStyles

func DefaultStyles() Styles

DefaultStyles returns the built-in neutral styles (xterm-256 palette). These read well on most dark terminals and are the stand-alone defaults when no host styles are injected via Options.Styles.

type Validator

type Validator func(buffer string) []error

Validator is the OnSave callback. An empty return slice means "save is allowed". Each returned error renders as one line in the footer error bar; the buffer is NOT closed when errors are reported, so the user can fix and retry.

Directories

Path Synopsis
Package mdhl provides a per-line Markdown highlighter for the embedded editor.
Package mdhl provides a per-line Markdown highlighter for the embedded editor.
Package yamlhl provides a single-line YAML syntax highlighter for the embedded editor.
Package yamlhl provides a single-line YAML syntax highlighter for the embedded editor.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL