editor

package
v0.14.1 Latest Latest
Warning

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

Go to latest
Published: Sep 29, 2026 License: MIT Imports: 42 Imported by: 0

Documentation

Overview

Package editor wires nem together and owns the event loop.

It is the only package that knows about every other one: it implements command.Env so commands can act, drives keymap lookup over decoded tcell events, arranges windows through view, and draws through ui. Nothing imports it, which is what lets every other package stay testable without a terminal.

Two responsibilities live here and nowhere else, both because putting them anywhere else means someone eventually forgets one:

  • Cross-command bookkeeping at dispatch. Breaking the kill run, resetting the goal column and recording the last command name all happen in one place, so no command has to remember them. See dispatch.
  • The minibuffer. A prompt is a real text.Buffer in a real view.Window, so C-a, C-e, C-k and the kill ring work inside prompts with no extra code. See minibuffer.go.

Index

Constants

View Source
const DefaultAutosaveIdle = 30 * time.Second

DefaultAutosaveIdle is how long a modified buffer may sit untouched before its contents reach the autosave store.

Variables

View Source
var ErrTooDeep = errors.New("minibuffer recursion too deep")

ErrTooDeep reports that minibuffer recursion hit its limit. It exists so a runaway Lua hook that prompts from inside a prompt fails cleanly instead of growing the Go stack until the process dies.

Functions

func DecodeKey

func DecodeKey(ev *tcell.EventKey, treatCtrlHAsBackspace bool) keymap.Key

DecodeKey translates a tcell key event into a canonical keymap.Key.

This is the only place in nem where tcell meets keymap. The boundary is deliberate: keymap depends on nothing but the standard library and is therefore testable without a terminal, and every terminal-specific irregularity is absorbed here.

The returned key is always already normalized, so it can be appended to a pending sequence and looked up directly.

An event this function does not recognise — tcell's KeyInsert, mouse and paste keys, function keys past F12 — decodes to the zero Key, which keymap.Normalize can never produce. Callers should treat a zero Key as "no binding could name this" and ignore the event.

Escape versus Meta

This function does NOT implement escape-sequence timing, because tcell already owns it. A terminal delivers Meta+x as ESC followed by x, which is indistinguishable from Escape then x except by arrival time. tcell's input parser resolves it with a 50ms expiry and a 60ms timer (input.go: it sets ip.expire to now+50ms and arms time.AfterFunc(60ms, ip.escTimeout)), then rewrites an ESC-prefixed key by adding ModAlt. So by the time an event reaches here the ambiguity is gone. Do not reimplement it.

Ctrl+H, and why treatCtrlHAsBackspace barely matters

tcell merges the Backspace key and Ctrl+H in two independent places: its input parser maps both 0x08 and 0x7F to KeyBackspace ("case '\b', '\x7F'"), and NewEventKey rewrites KeyBackspace2 to KeyBackspace unconditionally. A real terminal therefore cannot deliver C-h as distinct from <backspace>, which means emacs's C-h help prefix is unreachable under tcell's legacy input path regardless of this flag.

The flag is honoured for the one event that can still express the difference: an explicitly constructed KeyCtrlH (tcell numbers it 72, well clear of KeyBackspace at 8). When false that decodes to C-h; when true it folds to <backspace>.

func InstallDefaultBindings

func InstallDefaultBindings(m *keymap.Map) error

InstallDefaultBindings populates m with nem's built-in keymap. It is called before the Lua config loads, so user bindings override these.

Types

type ClipboardMode

type ClipboardMode int

ClipboardMode selects whether kills also reach the system clipboard.

const (
	// ClipboardOSC52 mirrors every kill to the terminal's clipboard, and lets
	// C-y take in text copied elsewhere. The zero value, so this is on unless
	// something turns it off.
	ClipboardOSC52 ClipboardMode = iota
	// ClipboardOff leaves the system clipboard alone, in both directions.
	ClipboardOff
)

type Editor

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

Editor is one editing session: the buffers, the windows onto them, and the machinery that turns keystrokes into commands.

It is not safe for concurrent use, deliberately. Everything — command dispatch, keymap mutation and Lua execution — runs on the goroutine that polls for events, which is why keymap.Map and command.KillRing need no locks.

func New

func New(scr tcell.Screen) (*Editor, error)

New returns an editor with every built-in command registered, the default bindings installed, and a single window on an empty *scratch* buffer.

scr may be nil for tests that drive dispatch without drawing; Run requires a real screen.

func (*Editor) Active

func (e *Editor) Active() *view.Window

Active returns the selected window.

func (*Editor) AfterCommand

func (e *Editor) AfterCommand(name string, fn func())

AfterCommand registers fn to run immediately after name is dispatched, whether or not the command returned an error — an after-save hook still wants to know a save was attempted.

func (*Editor) Arg

func (e *Editor) Arg() (int, bool)

Arg reports the prefix argument for the command being dispatched.

func (*Editor) AutosaveAvailable

func (e *Editor) AutosaveAvailable(path string) (bool, time.Time)

AutosaveAvailable reports whether path has an autosave holding work its saved file does not.

func (*Editor) AutosaveDue

func (e *Editor) AutosaveDue(now time.Time) bool

AutosaveDue reports whether enough idle time has passed, since the last keystroke, to be worth writing recovery files - or enough keystrokes, for someone who never pauses that long.

It is false when nothing has been typed since the previous autosave, so an editor left alone writes once and then stays quiet.

func (*Editor) BackupRoot

func (e *Editor) BackupRoot() string

BackupRoot reports where recovery files are kept, or "" when there is no store. Worth surfacing: a user who is told an autosave exists needs to know where to look.

func (*Editor) BeforeCommand

func (e *Editor) BeforeCommand(name string, fn func())

BeforeCommand registers fn to run immediately before name is dispatched. Several hooks on one name run in registration order.

func (*Editor) Bindings

func (e *Editor) Bindings() map[string]string

Bindings maps key sequences to command names, for describe-bindings. In a buffer with a mode of its own - a dired listing - the mode's keys are included and win, since they are what those keys do there.

func (*Editor) BranchOf

func (e *Editor) BranchOf(b *text.Buffer) string

BranchOf reports the git branch b's file sits on, or empty when it is not in a repository. It is the BranchFunc handed to the renderer.

func (*Editor) Buf

func (e *Editor) Buf() *text.Buffer

Buf returns the active window's buffer.

func (*Editor) BufferByName

func (e *Editor) BufferByName(name string) (*text.Buffer, bool)

BufferByName finds a buffer by display name.

func (*Editor) BufferName

func (e *Editor) BufferName(b *text.Buffer) string

BufferName returns b's display name.

func (*Editor) Buffers

func (e *Editor) Buffers() []*text.Buffer

Buffers lists live buffers, most recently visited first.

func (*Editor) ClipboardMode

func (e *Editor) ClipboardMode() ClipboardMode

ClipboardMode reports the current setting.

func (*Editor) CloseConfig

func (e *Editor) CloseConfig()

CloseConfig releases the Lua interpreter.

func (*Editor) CommandNames

func (e *Editor) CommandNames() []string

CommandNames lists interactive command names, sorted, for M-x completion.

func (*Editor) DeleteOtherWindows

func (e *Editor) DeleteOtherWindows()

DeleteOtherWindows makes the active window fill the frame.

func (*Editor) DeleteSelection

func (e *Editor) DeleteSelection() bool

DeleteSelection reports whether typing replaces an active region.

func (*Editor) DeleteWindow

func (e *Editor) DeleteWindow() error

DeleteWindow removes the active window, refusing to remove the sole one.

func (*Editor) Dired added in v0.1.3

func (e *Editor) Dired(dir string) (*text.Buffer, error)

Dired returns the buffer listing dir, reading it afresh. A directory already listed reuses its buffer, so asking for it twice does not pile up copies.

func (*Editor) DiredKeymap added in v0.1.3

func (e *Editor) DiredKeymap() *keymap.Map

DiredKeymap exposes the dired keymap so the Lua layer can rebind keys in it.

func (*Editor) Echo

func (e *Editor) Echo(format string, a ...any)

Echo shows a message on the bottom row.

func (*Editor) EmergencySave added in v0.10.0

func (e *Editor) EmergencySave() (saved []string, err error)

EmergencySave writes every buffer with unsaved changes where it will be found again, and reports where: a file's to its autosave - opening the file again says so, and M-x recover-file brings it back - and a buffer with no file to a file of its own among the rescued. It is for when nem is about to end, or may be, without anyone to ask what to do with them.

func (*Editor) FileType

func (e *Editor) FileType(b *text.Buffer) string

FileType reports b's file type for the modeline: "go", "lua", "json", "md", or empty for text nem has no grammar for.

It asks the highlight cache rather than looking at the extension itself, so the segment and the colouring cannot disagree - a modeline claiming "go" over uncoloured text would read as the highlighter having failed.

func (*Editor) HandleEvent

func (e *Editor) HandleEvent(ev tcell.Event)

HandleEvent processes one terminal event from the top level, recording it when a keyboard macro is being defined. Exported so tests can drive the editor a keystroke at a time without a real loop.

func (*Editor) HandleKey

func (e *Editor) HandleKey(k keymap.Key)

HandleKey resolves one decoded key and dispatches whatever it names.

func (*Editor) Keymap

func (e *Editor) Keymap() *keymap.Map

Keymap exposes the global keymap so the Lua layer can rebind keys. Mutating it is safe only from the input goroutine, which is where config loading runs.

func (*Editor) KillBackward

func (e *Editor) KillBackward(s string)

func (*Editor) KillBuffer

func (e *Editor) KillBuffer(b *text.Buffer) error

KillBuffer removes b from the live list, refusing to remove the last one.

Any window showing b is moved to another buffer first: leaving a window pointing at a dead buffer would be a nil-buffer panic on the next redraw.

func (*Editor) KillForward

func (e *Editor) KillForward(s string)

KillForward and KillBackward push onto the ring and mirror the resulting entry to the system clipboard, so C-w and M-w reach other applications. See noteKill for why the whole accumulated entry is sent rather than the fragment.

func (*Editor) LanguageOf added in v0.13.0

func (e *Editor) LanguageOf(b *text.Buffer) *syntax.Language

LanguageOf reports the language a buffer is written in, or nil: what M-; asks, to write a comment the way the language does.

func (*Editor) LastCommand

func (e *Editor) LastCommand() string

func (*Editor) LoadConfig

func (e *Editor) LoadConfig(path string) error

LoadConfig loads the Lua config at path, applies its settings, and wires its hooks into dispatch. An empty path uses the default location.

A broken config never takes the editor down. A script error is reported in the echo area and the editor carries on with built-in defaults for whatever failed, because losing a session to a typo in init.lua is a far worse outcome than starting unconfigured. The returned error is informational: callers normally ignore it, having already seen it echoed.

func (*Editor) Loop

func (e *Editor) Loop() error

Loop reads events and dispatches commands until the session ends.

It is not called Run because Env.Run invokes a command by name; this is the event loop.

It selects over a channel of events and a timer rather than blocking in PollEvent, because prefix-key discovery needs to notice that a prefix has sat pending for a while - a blocking read cannot express "or nothing happened for 300ms". tcell fills the channel from its own goroutine, but there is still exactly ONE consumer, which is what lets keymap.Map, command.KillRing and the Lua interpreter stay lock-free. Nothing in here may be moved onto another goroutine without revisiting that.

The timer is rebuilt each iteration and stopped as soon as an event wins the select, so a fluent user who never pauses arms and discards a timer per keystroke and never sees a panel.

func (*Editor) Message

func (e *Editor) Message() string

Message returns the current echo-area text, for tests.

func (*Editor) NewBuffer

func (e *Editor) NewBuffer(name string) *text.Buffer

NewBuffer creates a file-less buffer under name, or returns the existing one if that name is taken — so list-buffers reuses its buffer instead of piling up a new one per invocation.

func (*Editor) NoteInput

func (e *Editor) NoteInput(now time.Time)

NoteInput records that a keystroke arrived, restarting the idle clock. The event loop calls this for every key event.

func (*Editor) OpenFile

func (e *Editor) OpenFile(path string) (*text.Buffer, error)

OpenFile returns the buffer visiting path, reading it if it is not open yet. A path that does not exist yields an empty buffer carrying it, which is how find-file creates a new file. A directory is listed in dired, so find-file and the command line can both be pointed at one.

func (*Editor) OtherWindow

func (e *Editor) OtherWindow(n int)

OtherWindow moves the selection n windows along the cycle, wrapping.

func (*Editor) Quit

func (e *Editor) Quit(force bool) error

Quit ends the session, refusing without force while any buffer is modified.

func (*Editor) Quitting

func (e *Editor) Quitting() bool

Quitting reports whether the session has been asked to end, for tests.

func (*Editor) ReadChar

func (e *Editor) ReadChar(prompt string, valid []rune) (rune, error)

ReadChar prompts for a single keystroke, accepting only the runes in valid.

query-replace's y/n/!/q loop is built from this: a ReadString would force the user to press RET after every answer.

func (*Editor) ReadKey

func (e *Editor) ReadKey(prompt string) (keymap.Key, error)

ReadKey prompts for one raw keystroke, for describe-key.

func (*Editor) ReadString

func (e *Editor) ReadString(opts command.ReadOpts) (string, error)

ReadString prompts in the minibuffer and returns what was typed, or ErrQuit if the user pressed C-g.

It enters a nested event loop rather than returning a continuation, which is what lets a prompting command read as straight-line code: find-file is ReadString then OpenFile then Visit, not a three-state machine.

Incremental search

A repeated C-s inside the prompt must advance to the next match, which the prompt's keymap owns. The session it advances arrives in ReadOpts.Session, passed by the search command itself, so the editor never has to guess which prompts are searches. Session and OnChange are independent hooks and both fire; both run against the text window, not the prompt's.

func (*Editor) RecoverFile

func (e *Editor) RecoverFile(b *text.Buffer) error

RecoverFile replaces b's contents with its autosave.

The buffer is left modified on purpose: recovered text is not what is on disk, and marking it clean would invite the user to quit believing it had been saved. The whole replacement is one undo group, so a mistaken recovery is a single C-/ away.

func (*Editor) Redraw

func (e *Editor) Redraw()

func (*Editor) Registry

func (e *Editor) Registry() *command.Registry

Registry exposes the command table so the Lua layer can register commands into the same table the built-ins live in.

func (*Editor) RequestClipboard

func (e *Editor) RequestClipboard()

RequestClipboard asks the terminal for its clipboard contents.

The answer is asynchronous and may never come: OSC 52 reads are far less widely implemented than writes, and terminals that do support writing often refuse to read on the grounds that a program should not be able to exfiltrate whatever you last copied. So this cannot be made to look synchronous, and yanking does not wait on it — C-y reads through the desktop's clipboard tools instead (see takeSystemClipboard). When a reply does arrive, SetClipboardReply puts it at the front of the ring so the next C-y picks it up.

func (*Editor) Ring

func (e *Editor) Ring() *command.KillRing

Ring exposes the kill ring for tests and for the Lua layer.

func (*Editor) Run

func (e *Editor) Run(name string) error

Run invokes another command by name. This is M-x's mechanism and Lua's nem.run.

It routes through dispatch so the invoked command gets the same bookkeeping as a keystroke would — otherwise M-x kill-line would leave the kill run open and fuse with the next kill.

func (*Editor) RunAutosave

func (e *Editor) RunAutosave(now time.Time) error

RunAutosave writes every modified buffer that has a file to the autosave store.

Errors are reported through the echo area and returned for tests; the event loop can ignore the return. A failure must be visible, because a user who believes they have recovery files and does not is worse off than one who knows they have none.

func (*Editor) SaveBuffer

func (e *Editor) SaveBuffer(b *text.Buffer, path string) error

SaveBuffer writes b to disk. An empty path saves to b's own path; a non-empty path saves there and adopts it.

On failure the buffer must come out exactly as it went in: still modified, and still pointing at its old path. text.Buffer.SaveAs assigns the path before attempting the write, so a failed write would otherwise leave the buffer claiming a file it was never written to — and the user would then believe a later successful C-x C-s had saved somewhere it had not. Two data-safety steps happen here, in this order and for these reasons. A file something else has changed is not overwritten without asking, because silently discarding an external edit is the worst thing this function could do. And the file's previous contents are copied to the backup store before the write, never after — a backup taken afterwards holds the new contents and preserves nothing. See safety.go.

func (*Editor) SaveMemory added in v0.4.0

func (e *Editor) SaveMemory()

SaveMemory is for the end of a session.

func (*Editor) Seq

func (e *Editor) Seq() *command.Seq

func (*Editor) SetAutosaveIdle

func (e *Editor) SetAutosaveIdle(d time.Duration)

SetAutosaveIdle sets how long a modified buffer may sit untouched before it is autosaved. Zero disables autosave.

func (*Editor) SetBackupEnabled

func (e *Editor) SetBackupEnabled(on bool)

SetBackupEnabled turns backups and autosaves on or off.

func (*Editor) SetBackupRoot

func (e *Editor) SetBackupRoot(root string)

SetBackupRoot points the backup and autosave store at root. Tests use this to keep out of the real state directory.

func (*Editor) SetClipboardMode

func (e *Editor) SetClipboardMode(m ClipboardMode)

SetClipboardMode chooses whether kills reach the system clipboard.

func (*Editor) SetClipboardReply

func (e *Editor) SetClipboardReply(data []byte)

SetClipboardReply accepts the terminal's answer to RequestClipboard. The event loop routes *tcell.EventClipboard here.

The reply becomes the newest kill-ring entry, which is what makes it reachable by the ordinary C-y rather than needing a second yank command. Text nem itself put on the clipboard is ignored: terminals echo it straight back, and adding it again would duplicate an entry the ring already holds.

func (*Editor) SetCompletionRows

func (e *Editor) SetCompletionRows(n int)

SetCompletionRows sets how many candidates a panel shows at once.

func (*Editor) SetCompletionStyle

func (e *Editor) SetCompletionStyle(name string)

SetCompletionStyle selects the emacs-shaped bottom rendering or the popup. The value has already been validated by the config host; an unrecognised one here is a programming error rather than a user's typo, so it is ignored rather than reported to someone who cannot act on it.

func (*Editor) SetDeleteSelection

func (e *Editor) SetDeleteSelection(on bool)

SetDeleteSelection turns delete-selection behaviour on or off. It is on by default; off restores emacs's own behaviour, where a selection is inert.

func (*Editor) SetIcons added in v0.1.5

func (e *Editor) SetIcons(on bool)

SetIcons turns file icons on or off, in the prompts and in every listing, open ones included. They need a Nerd Font, or a terminal that carries its symbols; without one each icon is an empty box, which is what this is for.

func (*Editor) SetLineWrap added in v0.11.0

func (e *Editor) SetLineWrap(on bool)

SetLineWrap turns line wrapping on or off. The goal column each window keeps means a column of the line unwrapped and of the row wrapped, so it is let go of; and a window scrolled sideways or part way down a line starts its view afresh.

func (*Editor) SetOpenBinary added in v0.1.4

func (e *Editor) SetOpenBinary(name string)

SetOpenBinary sets what happens to a file that is not text: "ask", "system" or "text". The config host has validated the value; anything else is ignored.

func (*Editor) SetSignals added in v0.10.0

func (e *Editor) SetSignals(ch <-chan os.Signal)

SetSignals has the event loop end the session when a signal arrives on ch. The caller chooses the signals, with signal.Notify.

func (*Editor) SetWhichKeyDelay

func (e *Editor) SetWhichKeyDelay(d time.Duration)

SetWhichKeyDelay sets how long a prefix sits pending before the panel appears. Zero disables prefix-key discovery entirely.

func (*Editor) ShowStartup

func (e *Editor) ShowStartup()

ShowStartup asks for the welcome panel on the next frame.

The caller decides rather than the editor, because only the caller knows whether a file was named on the command line - the editor sees an empty *scratch* either way, and showing a welcome over a file someone asked for would be an interruption rather than a greeting.

func (*Editor) SplitWindow

func (e *Editor) SplitWindow(vertical bool) error

SplitWindow splits the active window and selects the new half.

func (*Editor) TextHeight

func (e *Editor) TextHeight() int

TextHeight reports the rows of buffer text the active window shows. A prompt is a single row.

func (*Editor) Tree

func (e *Editor) Tree() *view.Tree

Tree exposes the window tree for tests.

func (*Editor) UseStateDir added in v0.4.0

func (e *Editor) UseStateDir(dir string) error

UseStateDir loads what was remembered in dir and keeps remembering there. Until it is called nothing persists, which is what keeps a test from reading or writing anyone's real history.

func (*Editor) Where

func (e *Editor) Where(cmd string) []string

Where lists the sequences bound to a command, for describe-key. A global key the current mode has taken over is left out: in dired, C-n no longer runs next-line.

func (*Editor) Win

func (e *Editor) Win() *view.Window

Win returns the minibuffer's window while a prompt is active, and the active text window otherwise.

This one substitution is what makes editing commands work inside a prompt: C-a, C-e, C-k and yank all act on whatever Win reports, so a prompt needs no parallel implementation of any of them.

func (*Editor) WrapWidth added in v0.11.0

func (e *Editor) WrapWidth() int

WrapWidth reports the width lines are wrapped at in the active window: its text area's, or 0 when lines are not wrapped. A prompt's one line never is.

func (*Editor) Yank

func (e *Editor) Yank() (string, error)

Yank consults the system clipboard first, so text copied in another application is what C-y pastes. See takeSystemClipboard. YankPop does not: M-y walks back through what the ring already holds.

func (*Editor) YankPop

func (e *Editor) YankPop() (string, error)

type SignalError added in v0.10.0

type SignalError struct {
	Signal os.Signal
	// Saved lists where the unsaved buffers were written.
	Saved []string
	// Err says what could not be written, if anything.
	Err error
}

SignalError is what Loop returns when a signal ended the session.

func (*SignalError) Error added in v0.10.0

func (s *SignalError) Error() string

Jump to

Keyboard shortcuts

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