reline

package module
v0.0.0-...-c4ce282 Latest Latest
Warning

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

Go to latest
Published: Jul 17, 2026 License: BSD-3-Clause Imports: 5 Imported by: 0

README

go-ruby-reline/reline

reline — go-ruby-reline

Docs License Go Coverage

A pure-Go (no cgo) reimplementation of Ruby's Reline line-editor core — the deterministic editing state machine of MRI 4.0.5's Reline::LineEditor. It drives a multibyte, rune-aware line buffer through the full emacs + vi command set, history navigation and incremental search, the kill-ring, completion, and the pure rendering computation — without any Ruby runtime and without a real terminal.

It is the line-editor backend for go-embedded-ruby (the prerequisite for a pure-Go IRB), but is a standalone, reusable module — a sibling of go-ruby-regexp (the Onigmo engine), go-ruby-erb (the ERB compiler), and go-ruby-yaml (the Psych port).

What it is — and isn't. The editing semantics — how each keystroke mutates the buffer and cursor, how the keymaps dispatch escape/control sequences to commands, how history and completion behave, and how the buffer is laid out for a terminal of a given width — are fully deterministic and need no interpreter and no tty, so they live here as pure Go. The terminal raw-mode I/O (reading raw key bytes, learning the window size, writing the rendered output) is a small host seam (IO) the embedder wires to the real terminal.

What's implemented

A faithful port of Reline::LineEditor (and KillRing, History, KeyStroke, KeyActor, the Unicode width/word logic), validated key-for-key against the ruby -rreline oracle:

  • Editing commands — insert; backward/forward char and word; beginning/end of line; delete char/word; kill-line, kill-whole-line, unix-line-discard, kill-word, backward-kill-word, kill-region; yank + yank-pop (the kill ring); transpose chars/words; up/down/capitalize word; undo/redo.
  • emacs + vi keymaps — the exact 256-entry EMACS_MAPPING, VI_COMMAND_MAPPING, and VI_INSERT_MAPPING arrays from MRI, the ESC/CSI/SS3 byte-sequence matcher, numeric arguments, and the vi operator grammar (cw, dw, dd, 2cw, df<c>, r/3r, f/t/F/T, p/P, W/E/B, J, …).
  • HistoryReline::HISTORY (size-capped, with the MRI range/index errors), up/down navigation, prefix search, and incremental search (Ctrl-R / Ctrl-S) with the failed/cancel/repeat states.
  • Completion — the completion-proc seam, common-prefix completion, the perfect-match/menu state machine, and the autocompletion journey (cycling).
  • Multiline edit state with a termination-predicate seam, plus undo/redo.
  • Rendering (the pure part) — given the buffer, cursor, prompt(s), and terminal width, Render computes the word-wrapped (prompt, content) rows and the cursor (x, y); writing that to the tty is the host's job.

The terminal-I/O seam

Everything that touches a real terminal lives behind the IO interface, so the editing core stays pure and fully testable by feeding scripted key events:

type IO interface {
    GetC(timeoutMs int) int        // read one raw input byte (-1 = EOF/timeout)
    UngetC(c int)                  // push a byte back
    GetScreenSize() (rows, cols int)
    Write(s string)                // emit rendered output to the tty
    MoveCursorColumn(col int)
    EraseAfterCursor()
    ClearScreen()
}

A NullIO (fixed 24×80, recording) ships for tests and headless use.

Usage

cfg := reline.NewConfig()                  // emacs mode, unlimited history
hist := reline.NewHistory(-1)
le := reline.NewLineEditor(cfg, &reline.NullIO{}, hist)
le.Reset("> ")

// feed scripted key events (the embedder decodes raw bytes via KeyStroke)
for _, ev := range []reline.Key{
    {Char: "h", MethodSymbol: "ed_insert"},
    {Char: "i", MethodSymbol: "ed_insert"},
    {Char: "\x01", MethodSymbol: "ed_move_to_beg"},
    {Char: "X", MethodSymbol: "ed_insert"},
} {
    le.Update(ev)
}
line, _ := le.Line() // "Xhi"

rs := le.Render(80, nil) // wrapped lines + cursor position (pure; no tty)

Tests & coverage

  • 100% statement coverage (including every error branch), enforced in CI.
  • Differential oracle — a battery of scripted editing sessions is replayed through ruby -rreline and compared byte-for-byte against this port (line + cursor + finished/eof). The oracle self-skips on Windows and where a Reline-0.6 ruby is absent; the deterministic, ruby-free tests alone keep coverage at 100% so every lane passes.
  • 6 architectures (amd64/arm64/riscv64/loong64/ppc64le/s390x) and 3 operating systems (Linux/macOS/Windows).

License

BSD-3-Clause — see LICENSE. Copyright the go-ruby-reline/reline authors.

Documentation

Overview

Package reline is a pure-Go (CGO=0) port of MRI 4.0.5's Reline line-editor core: the editing state machine (line buffer, cursor, keymap dispatch, editing commands, history, kill-ring, completion) plus the pure rendering computation. The terminal raw-mode I/O is a host seam (see io.go) wired by the embedder (e.g. rbgo for a pure-Go IRB).

Index

Constants

This section is empty.

Variables

View Source
var (
	CompleterWordBreakCharacters = " \t\n`><=;|&{("
	CompleterQuoteCharacters     = "\"'"
)

CompleterWordBreakCharacters and CompleterQuoteCharacters mirror Reline's defaults; they delimit the word under the cursor for completion.

View Source
var AmbiguousWidth = 1

AmbiguousWidth is the display width used for East-Asian "ambiguous" width characters. MRI resolves this at runtime from the terminal probe; the default is 1 (mirrors Reline.ambiguous_width when STDOUT is not a tty / dumb).

View Source
var LastIncrementalSearch = ""

LastIncrementalSearch holds the last incremental search word (MRI Reline.last_incremental_search), shared across searches.

Functions

func CalculateWidth

func CalculateWidth(str string, allowEscapeCode bool) int

CalculateWidth returns the display width of str. When allowEscapeCode is true, CSI/OSC escape sequences and non-printing markers are excluded from the count (MRI Reline::Unicode.calculate_width).

func CommonPrefix

func CommonPrefix(list []string, ignoreCase bool) string

CommonPrefix returns the longest common grapheme-cluster prefix of list, optionally case-insensitively (MRI Reline::Unicode.common_prefix).

func EscapeForPrint

func EscapeForPrint(str string) string

EscapeForPrint converts control characters to their printable caret escapes, keeping newline and expanding tab to two spaces (MRI escape_for_print).

func SplitByWidth

func SplitByWidth(str string, maxWidth int) ([]string, int)

SplitByWidth wraps str into display lines no wider than maxWidth and returns the lines plus their count (MRI Reline::Unicode.split_by_width, used by IRB).

func SplitLineByWidth

func SplitLineByWidth(str string, maxWidth, offset int) []string

SplitLineByWidth wraps str into display lines no wider than maxWidth, starting at the given column offset, carrying CSI color state across wraps (MRI Reline::Unicode.split_line_by_width).

Types

type CompletionProc

type CompletionProc func(target, pre, post string) []string

CompletionProc computes completion candidates for the word `target`, given the text before (`pre`) and after (`post`) it. Returning nil means "no completion" (MRI's completion_proc that may return non-Array).

type Config

type Config struct {
	HistorySize          int  // -1 unlimited (default), 0 drop-all, >0 cap
	CompletionIgnoreCase bool // case-fold completion matching
	ShowAllIfAmbiguous   bool // show menu immediately on ambiguous completion
	DisableCompletion    bool
	Autocompletion       bool
	KeyseqTimeout        int // ms; ESC ambiguity timeout
	IsearchTerminators   string
	// contains filtered or unexported fields
}

Config holds the editor configuration relevant to the pure editing core (MRI Reline::Config, trimmed to the state-machine-relevant fields).

func NewConfig

func NewConfig() *Config

NewConfig returns a Config with MRI's defaults (emacs mode, unlimited history, 500ms keyseq timeout) and the standard emacs/vi keymaps loaded.

func (*Config) EditingMode

func (c *Config) EditingMode() EditingMode

EditingMode returns the current mode.

func (*Config) EditingModeIs

func (c *Config) EditingModeIs(modes ...EditingMode) bool

EditingModeIs reports whether the current mode is any of the given modes.

func (*Config) SetEditingMode

func (c *Config) SetEditingMode(m EditingMode)

SetEditingMode switches the active keymap.

type EditingMode

type EditingMode int

EditingMode selects the active keymap (MRI @editing_mode_label).

const (
	// ModeEmacs is the default emacs keymap.
	ModeEmacs EditingMode = iota
	// ModeViInsert is vi insert mode.
	ModeViInsert
	// ModeViCommand is vi command (normal) mode.
	ModeViCommand
)

type History

type History struct {
	MaxSize int
	// contains filtered or unexported fields
}

History is the line-edit history (MRI Reline::History, a size-capped Array). MaxSize > 0 caps the number of entries (oldest dropped); 0 drops everything; negative means unlimited.

func NewHistory

func NewHistory(maxSize int) *History

NewHistory returns an empty history with the given size cap (-1 = unlimited, the MRI default).

func (*History) Append

func (h *History) Append(val string)

Append adds a single entry (MRI Reline::History#<<).

func (*History) Clear

func (h *History) Clear()

Clear removes all entries.

func (*History) DeleteAt

func (h *History) DeleteAt(index int) (string, error)

DeleteAt removes the entry at index, returning it (MRI #delete_at).

func (*History) Empty

func (h *History) Empty() bool

Empty reports whether the history has no entries.

func (*History) Get

func (h *History) Get(index int) (string, error)

Get returns the entry at index (supporting negative indices), mirroring Reline::History#[].

func (*History) Push

func (h *History) Push(vals ...string)

Push appends one or more entries, enforcing the size cap. Mirrors Reline::History#push (and #concat, which is push applied per element).

func (*History) Set

func (h *History) Set(index int, val string) error

Set replaces the entry at index (MRI Reline::History#[]=).

func (*History) Size

func (h *History) Size() int

Size returns the number of entries.

type HistoryError

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

HistoryError is raised for out-of-range/out-of-bounds history index access, mirroring MRI's RangeError / IndexError from Reline::History#check_index.

func (*HistoryError) Error

func (e *HistoryError) Error() string

type IO

type IO interface {
	// GetC returns the next input byte, or -1 on timeout/EOF. timeoutMs < 0
	// means block indefinitely.
	GetC(timeoutMs int) int
	// UngetC pushes a byte back to be returned by the next GetC.
	UngetC(c int)
	// GetScreenSize returns the terminal size as (rows, cols).
	GetScreenSize() (rows, cols int)
	// Write emits already-rendered output (escape sequences + text) to the tty.
	Write(s string)
	// MoveCursorColumn moves the cursor to an absolute column.
	MoveCursorColumn(col int)
	// EraseAfterCursor clears from the cursor to the end of the line.
	EraseAfterCursor()
	// ClearScreen clears the whole screen.
	ClearScreen()
}

IO is the terminal host seam. Everything that touches a real tty lives behind this interface so the editing core stays pure and fully testable. The embedder (e.g. rbgo) supplies a concrete implementation backed by the host terminal in raw mode; tests supply a scripted/recording fake.

The pure core only needs three capabilities from the host: read a raw key byte, learn the terminal size, and write rendered output. Cursor positioning and screen clears are expressed as writes by the renderer, but are surfaced here too for embedders that drive the screen directly.

type Key

type Key struct {
	Char         string
	MethodSymbol string
	EOF          bool
}

Key is a decoded key event: Char is the input string and MethodSymbol the command it dispatches to (MRI Reline::Key). A nil/empty Char with empty MethodSymbol represents EOF.

type KillRing

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

KillRing is the emacs kill ring (MRI Reline::KillRing): kill commands append text, yank pastes the most recent entry, and yank-pop rotates backward.

func NewKillRing

func NewKillRing(max int) *KillRing

NewKillRing returns an empty kill ring holding up to max entries.

func (*KillRing) Append

func (k *KillRing) Append(s string, beforeP bool)

Append adds killed text. When beforeP is true the text is prepended to the current accumulating entry (used by backward kills).

func (*KillRing) Each

func (k *KillRing) Each(fn func(string))

Each iterates the ring from most-recent to least-recent (MRI KillRing#each).

func (*KillRing) Process

func (k *KillRing) Process()

Process advances the kill-accumulation state machine once per dispatched key, so that two consecutive kills merge but a kill after another command starts a new entry (MRI KillRing#process).

func (*KillRing) Yank

func (k *KillRing) Yank() (string, bool)

Yank returns the most recent kill entry (or "", false if empty) and enters the YANK state so YankPop can rotate.

func (*KillRing) YankPop

func (k *KillRing) YankPop() (yanked, prev string, ok bool)

YankPop rotates to the previous kill entry, returning the new text and the previously yanked text (to be replaced). Valid only right after a yank.

type LineEditor

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

LineEditor is the MRI Reline::LineEditor editing state machine: a multiline rune-aware buffer with a byte cursor, the full emacs+vi command set, history navigation + incremental search, kill-ring, completion, and undo/redo. It is driven purely by Keys (Update) and exposes the buffer/cursor for rendering.

func NewLineEditor

func NewLineEditor(config *Config, io IO, hist *History) *LineEditor

NewLineEditor returns a LineEditor wired to config and the terminal seam io. hist is the shared history (Reline::HISTORY).

func (*LineEditor) BytePointer

func (le *LineEditor) BytePointer() int

BytePointer returns the cursor's byte offset within the current line.

func (*LineEditor) CurrentLine

func (le *LineEditor) CurrentLine() string

CurrentLine returns the line the cursor is on.

func (*LineEditor) DeleteText

func (le *LineEditor) DeleteText(start, length int, hasArgs bool)

DeleteText deletes a range of the current line, or (with hasArgs=false) deletes the current line / merges per MRI delete_text() with no args.

func (*LineEditor) EOF

func (le *LineEditor) EOF() bool

EOF reports whether the read ended via end-of-input (e.g. Ctrl-D on empty).

func (*LineEditor) Finished

func (le *LineEditor) Finished() bool

Finished reports whether the current read has terminated (Enter or EOF).

func (*LineEditor) History

func (le *LineEditor) History() *History

History returns the editor's shared history.

func (*LineEditor) InsertMultilineText

func (le *LineEditor) InsertMultilineText(text string)

InsertMultilineText inserts text that may contain newlines, splitting the current line (MRI insert_multiline_text).

func (*LineEditor) InsertText

func (le *LineEditor) InsertText(text string)

InsertText inserts text at the cursor (MRI insert_text), advancing the cursor.

func (*LineEditor) Line

func (le *LineEditor) Line() (string, bool)

Line returns the submitted line (whole buffer), or "" with ok=false at EOF (MRI LineEditor#line returns nil on eof).

func (*LineEditor) LineIndex

func (le *LineEditor) LineIndex() int

LineIndex returns the index of the current line in a multiline buffer.

func (*LineEditor) MenuInfo

func (le *LineEditor) MenuInfo() ([]string, bool)

MenuInfo returns the pending completion menu candidates (and whether a menu is active). Rendering of the menu is the embedder's concern; the state lives here.

func (*LineEditor) MultilineOff

func (le *LineEditor) MultilineOff()

MultilineOff restricts editing to a single line (Enter finishes).

func (*LineEditor) MultilineOn

func (le *LineEditor) MultilineOn()

MultilineOn enables multiline editing (Enter inserts a newline until the termination predicate accepts).

func (*LineEditor) Render

func (le *LineEditor) Render(width int, promptProc PromptProc) RenderState

Render lays out the buffer for a terminal `width` wide using promptProc (which may be nil), returning the wrapped lines and cursor position. Pure: no IO.

func (*LineEditor) Reset

func (le *LineEditor) Reset(prompt string)

Reset re-initializes the editor for a new read with the given prompt and refreshes the screen size from the IO seam.

func (*LineEditor) ResetLine

func (le *LineEditor) ResetLine()

ResetLine clears the buffer to a single empty line with the cursor at home.

func (*LineEditor) SearchingPrompt

func (le *LineEditor) SearchingPrompt() (string, bool)

SearchingPrompt returns the active incremental-search prompt and whether a search is in progress (exposed for the embedder's renderer).

func (*LineEditor) SetBytePointer

func (le *LineEditor) SetBytePointer(v int)

SetBytePointer sets the cursor's byte offset (MRI byte_pointer=).

func (*LineEditor) SetCompletionAppendCharacter

func (le *LineEditor) SetCompletionAppendCharacter(s string)

SetCompletionAppendCharacter sets the character appended after a unique completion (typically a space).

func (*LineEditor) SetCompletionProc

func (le *LineEditor) SetCompletionProc(p CompletionProc)

SetCompletionProc installs the completion candidate generator.

func (*LineEditor) SetConfirmMultilineTermination

func (le *LineEditor) SetConfirmMultilineTermination(p func(buffer string) bool)

SetConfirmMultilineTermination installs the predicate that decides whether Enter on the last line submits the buffer (true) or inserts a newline. The embedder (e.g. IRB) supplies one that checks for a complete Ruby expression.

func (*LineEditor) SetPastingState

func (le *LineEditor) SetPastingState(inPasting bool)

SetPastingState toggles bracketed-paste batching: text typed while pasting is accumulated and force-inserted on exit (MRI set_pasting_state).

func (*LineEditor) Update

func (le *LineEditor) Update(key Key) bool

Update feeds one key event through the dispatch + post-processing pipeline and returns whether the buffer changed (MRI update/input_key).

func (*LineEditor) WholeBuffer

func (le *LineEditor) WholeBuffer() string

WholeBuffer returns the whole multiline buffer joined by newlines.

func (*LineEditor) WholeLines

func (le *LineEditor) WholeLines() []string

WholeLines returns a copy of every buffer line.

type NullIO

type NullIO struct {
	Rows, Cols int
	Out        []string
	Unget      []int
	Cleared    int
	Col        int
}

NullIO is a recording IO with a fixed default 24x80 screen, suitable as a default when no real terminal is wired (the pure state machine never blocks on it in tests). It captures writes and cursor ops for inspection.

func (*NullIO) ClearScreen

func (n *NullIO) ClearScreen()

ClearScreen records a screen clear.

func (*NullIO) EraseAfterCursor

func (n *NullIO) EraseAfterCursor()

EraseAfterCursor records an erase request as a sentinel write.

func (*NullIO) GetC

func (n *NullIO) GetC(timeoutMs int) int

GetC returns a previously ungot byte if any, else reports EOF.

func (*NullIO) GetScreenSize

func (n *NullIO) GetScreenSize() (int, int)

GetScreenSize returns the configured size (defaulting to 24x80).

func (*NullIO) MoveCursorColumn

func (n *NullIO) MoveCursorColumn(col int)

MoveCursorColumn records the requested cursor column.

func (*NullIO) UngetC

func (n *NullIO) UngetC(c int)

UngetC pushes a byte back to be returned by the next GetC.

func (*NullIO) Write

func (n *NullIO) Write(s string)

Write records output for inspection.

type PromptProc

type PromptProc func(lines []string) []string

PromptProc, when set, maps the buffer lines to per-line prompts (MRI prompt_proc). When nil, a single prompt is used for line 0 and "" for the rest.

type RenderState

type RenderState struct {
	// Lines are wrapped display rows as (prompt, content) pairs.
	Lines [][2]string
	// CursorX is the cursor column within its wrapped row.
	CursorX int
	// CursorY is the cursor row index into Lines.
	CursorY int
}

RenderState is the pure result of laying out the current buffer for a terminal of the given width: the visible (prompt, content) line pairs after word wrapping, and the cursor's (x, y) in that wrapped grid. This is the "rendering-to-output" computation; writing it to the tty is the IO seam.

Jump to

Keyboard shortcuts

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