Documentation
¶
Overview ¶
Package command defines nem's command layer: the named commands the editor can run, the registry that holds them, and the Env through which a command touches editor state.
Env is an interface declared here and implemented by the editor package. A command may move point, edit the buffer, kill and yank, prompt in the minibuffer, and manage windows and buffers. It may never reach the tcell screen or the layout tree: there is deliberately no method that exposes either. That boundary is what lets every command be tested headlessly against commandtest.Fake, and it is why this package imports text, keymap and view but neither ui nor editor.
Env is an interface rather than a struct because a struct holding editor state would force this package to import editor, which imports this package.
Three things are deliberately absent from Env. They are recorded here so that their absence is not later mistaken for an oversight:
- Lua hooks. The editor fires hooks around dispatch, keyed on command name, so save-buffer need not know that before-save exists. A RunHook method here would leak the scripting layer into every command; do not add one.
- Paren highlighting. show-paren is computed by ui from point at render time. Nothing highlight-related belongs on Env, because Env cannot reach the screen — that is this boundary working, not a gap in it.
- Buffer naming in text. A text.Buffer has only a Path; display names live in the editor's name-to-buffer map. That is why *scratch* and *Buffer List* are editor concepts, and why BufferName and BufferByName are methods here rather than on the buffer itself.
Index ¶
- Constants
- Variables
- func CompleteDirectory(prefix string) []string
- func FoldCase(pat string) bool
- func IsDirCandidate(s string) bool
- func RegisterAll(r *Registry) error
- func RegisterBuffers(r *Registry) error
- func RegisterEdit(r *Registry) error
- func RegisterLines(r *Registry) error
- func RegisterMotion(r *Registry) error
- func RegisterRegion(r *Registry) error
- func RegisterSearch(r *Registry) error
- func SearchBackward(b *text.Buffer, pat string, from text.Pos, fold bool) (start, end text.Pos, ok bool)
- func SearchForward(b *text.Buffer, pat string, from text.Pos, fold bool) (start, end text.Pos, ok bool)
- type Command
- type CompleteFunc
- type Env
- type Func
- type Isearch
- type KillRing
- type ReadOpts
- type Registry
- type Seq
Constants ¶
const DefaultCapacity = 60
DefaultCapacity is the number of entries the kill ring retains, matching emacs's kill-ring-max.
Variables ¶
var ( // ErrQuit reports that the user pressed C-g. ReadString, ReadChar and // ReadKey return it when a prompt is abandoned, and commands should // propagate it rather than treating it as a failure. ErrQuit = errors.New("quit") // ErrNoMark is returned by commands needing a region when the buffer has // no mark set. ErrNoMark = errors.New("no mark set in this buffer") // ErrUnknownCommand is returned by Env.Run and Registry.Run for a name // that is not registered. ErrUnknownCommand = errors.New("no such command") // ErrOpenedElsewhere is returned by OpenFile when the file went to the // operating system's own app - a PDF to the viewer - instead of into a // buffer. It is not a failure: there is simply no buffer to visit, so a // caller stops, and nothing is reported beyond what OpenFile said itself. ErrOpenedElsewhere = errors.New("opened with the system app") // ErrBeginningOfBuffer and ErrEndOfBuffer report that point could not move // because it already sits at a boundary. // // These are conditions rather than faults: a caller normally reports one // through Echo and carries on rather than treating it as a failure. They // are declared here, exported, rather than privately in whichever file // raises them, so that the dispatcher can tell a harmless boundary from a // genuine error with errors.Is across package boundaries. ErrBeginningOfBuffer = errors.New("beginning of buffer") ErrEndOfBuffer = errors.New("end of buffer") )
var ( // ErrKillRingEmpty is returned by Yank when nothing has been killed. ErrKillRingEmpty = errors.New("kill ring is empty") // ErrNotAfterYank is returned by YankPop when the preceding operation was // not a Yank or YankPop. Emacs reports this as "Previous command was not a // yank". ErrNotAfterYank = errors.New("previous command was not a yank") )
var ( // ErrEmptyName rejects a command with no name. ErrEmptyName = errors.New("command name is empty") // ErrNilFunc rejects a command with no implementation. ErrNilFunc = errors.New("command has no function") // ErrDuplicateCommand rejects a second registration of the same name. ErrDuplicateCommand = errors.New("command already registered") )
Functions ¶
func CompleteDirectory ¶ added in v0.1.3
CompleteDirectory completes a directory name: the directory the input names, then the directories inside it. Dired's prompts use it, for where to list or where to move something to.
func FoldCase ¶
FoldCase reports whether pat should match case-insensitively.
This is emacs's smart case: a pattern is folded while it is entirely lowercase, and becomes case-sensitive as soon as it contains an uppercase letter. So "hello" finds "Hello", but "Hello" does not find "hello".
func IsDirCandidate ¶ added in v0.1.3
IsDirCandidate reports whether a filename candidate is a directory to walk into, which the completers mark with a trailing separator. It is the Descend hook for the filename prompts, so RET on a directory lists it.
The ./ entry is the exception. It names the directory already listed, so walking into it would change nothing; RET takes it as the answer instead. (The directory itself under any other name is equal to the input, and the minibuffer already takes a candidate equal to the input as the answer.)
func RegisterAll ¶
RegisterAll registers every built-in command group into r.
This is the one place the groups are assembled. A group missing from here does not exist as far as the rest of the editor is concerned: M-x will not find its commands, the Lua config cannot bind them, and the default keymap will point at names that resolve to nothing. So adding a group means adding it here, and the cross-check test in the editor package fails if a bound key names a command no group registers.
func RegisterBuffers ¶
RegisterBuffers adds the file, buffer, window and session commands.
func RegisterEdit ¶
RegisterEdit adds the editing commands to r.
func RegisterLines ¶
RegisterLines adds the line-moving commands.
These are nem going past emacs rather than following it: emacs has no native line move, so there is no traditional behaviour to be faithful to and these work the way the equivalent does in any modern editor.
func RegisterMotion ¶
RegisterMotion adds nem's motion commands to r.
Two rules govern this file and are easy to break by accident:
Character motion moves by grapheme cluster, never by rune. A combining sequence or a ZWJ emoji is one cursor stop however many runes it holds, so forward-char and backward-char go through text.Line's grapheme helpers rather than incrementing a rune index.
Vertical motion preserves the goal column and every other command clears it. The goal column is the display column the cursor is trying to keep, so that descending through a short line and out the other side returns to the original column rather than to the short line's end. It is established on the first vertical move of a run, preserved by later ones, and cleared by everything else — which is why a new command added here must call clearGoal unless it is itself vertical motion.
func RegisterRegion ¶
RegisterRegion adds the mark, region, kill-ring and undo commands.
func RegisterSearch ¶
RegisterSearch adds the search, replace and help commands to r.
It takes the registry rather than reading commands through Env because describe-key must report a command's documentation, and Env deliberately exposes no way to retrieve it: Env is what a command may touch at run time, while the registry is the table itself.
func SearchBackward ¶
func SearchBackward(b *text.Buffer, pat string, from text.Pos, fold bool) (start, end text.Pos, ok bool)
SearchBackward finds the last occurrence of pat beginning strictly before from, scanning backward. It reports the match's bounds.
func SearchForward ¶
func SearchForward(b *text.Buffer, pat string, from text.Pos, fold bool) (start, end text.Pos, ok bool)
SearchForward finds the first occurrence of pat starting at or after from. It reports the match's bounds, and ok is false when there is none.
A pattern containing a newline never matches: v1 searches within single lines only.
Types ¶
type Command ¶
type Command struct {
// Name is the command's identity, in kebab-case.
Name string
// Doc is a one-line description shown by describe-key and M-x.
Doc string
// Fn implements the command.
Fn Func
// Interactive reports whether the command appears in M-x completion.
// Commands driven only by the event loop, such as self-insert-command,
// are registered but not interactive.
Interactive bool
}
Command is one named, invocable operation.
The name is the single identity a command has: M-x searches these names, the Lua config binds keys to them, and describe-bindings reports them. Keep them in emacs's kebab-case ("kill-line", not "KillLine") so a user's existing muscle memory for M-x transfers.
type CompleteFunc ¶
CompleteFunc returns the candidates available for what has been typed so far.
It supplies the candidate UNIVERSE, not a filtered result: the minibuffer filters and ranks with fuzzy matching, so a CompleteFunc that pre-filtered by prefix would defeat it. Typing "fwc" to reach forward-char returns no prefix matches at all, and the list handed back would be empty.
The argument is still the current contents, because for some completions it selects which universe applies rather than narrowing one: a filename completion reads the directory the input names, and then offers everything in it rather than only the entries whose base matches.
func CompleteFrom ¶
func CompleteFrom(names []string) CompleteFunc
CompleteFrom returns a CompleteFunc offering every one of names.
It does not filter. The minibuffer ranks candidates with fuzzy matching, so narrowing here would defeat it: typing "fwc" for forward-char has no prefix match, and a pre-filtered list would come back empty.
type Env ¶
type Env interface {
// Win returns the active window: the buffer being edited, where point is,
// and which lines are on screen.
//
// While a prompt is open, Win reports the MINIBUFFER's window, not the
// text window. That is deliberate and is what makes prompts editable for
// free: C-a, C-e, C-k, M-b and the kill ring are ordinary commands, and
// they must act on the prompt the user is typing into.
//
// The exception is ReadOpts.OnChange and ReadOpts.Session, which run
// against the pre-prompt text window — see the note on OnChange. Anything
// else that needs the text window while a prompt is open is asking for
// trouble; there is no accessor for it, by design.
Win() *view.Window
// Buf returns the active window's buffer. Shorthand for Win().Buf.
Buf() *text.Buffer
// TextHeight reports how many rows of buffer text the active window shows,
// excluding its modeline. Needed by every command that moves by screenfuls
// or repositions the viewport: scroll-up-command, scroll-down-command and
// recenter-top-bottom cannot be written without it.
TextHeight() int
// Arg reports the prefix argument. n is 1 when none was given, and
// explicit distinguishes a bare command from C-u 1, which some commands
// treat differently.
Arg() (n int, explicit bool)
// KillForward records text killed forward of point; KillBackward records
// text killed backward of it. The ring handles kill-run accumulation, so a
// command states only the direction.
KillForward(s string)
KillBackward(s string)
// Yank returns the current kill-ring entry without consuming it.
Yank() (string, error)
// YankPop rotates to the next-older entry and returns the full replacement
// text. It is valid only immediately after a yank.
YankPop() (string, error)
// LastCommand names the command that ran immediately before this one, or
// "" at the start of a session. This is emacs's last-command.
LastCommand() string
// Seq returns the sequencing state shared across consecutive commands.
//
// The pointer is stable for the life of the session, so commands mutate
// the struct in place: e.Seq().RecenterCycle++ is the intended idiom.
Seq() *Seq
// ReadString prompts in the minibuffer and returns what the user typed,
// or ErrQuit if they pressed C-g.
ReadString(opts ReadOpts) (string, error)
// ReadChar prompts for a single keystroke, accepting only the runes in
// valid (all runes when valid is empty), and returns it. This is what
// query-replace's y/n/!/q loop and save-some-buffers are built from; a
// ReadString would force the user to press RET after every answer.
ReadChar(prompt string, valid []rune) (rune, error)
// ReadKey prompts for one raw keystroke and returns it undecoded, for
// describe-key.
ReadKey(prompt string) (keymap.Key, error)
// Echo shows a message in the echo area.
Echo(format string, a ...any)
// Buffers lists every live buffer, most recently visited first.
Buffers() []*text.Buffer
// BufferName is the buffer's display name: the basename of its file, or a
// generated name such as "*scratch*" for one with no file.
BufferName(b *text.Buffer) string
// BufferByName finds a buffer by display name, for switch-to-buffer.
BufferByName(name string) (*text.Buffer, bool)
// NewBuffer creates a live, file-less buffer under the given display name.
// list-buffers renders into one of these, as does the startup *scratch*.
NewBuffer(name string) *text.Buffer
// OpenFile returns the buffer visiting path, reading it from disk if it is
// not already open. A path that does not exist yields an empty buffer, as
// find-file does.
OpenFile(path string) (*text.Buffer, error)
// KillBuffer removes a buffer from the live list. It is an error to kill
// the last remaining buffer.
KillBuffer(b *text.Buffer) error
// SaveBuffer writes b to disk. An empty path saves to b's own path; a
// non-empty path saves there and adopts it, which is write-file.
//
// Commands go through Env rather than calling text.Buffer.Save directly so
// that tests can intercept saves and inject failures. That matters more
// here than anywhere else in this interface: saving is the one operation
// where a bug costs the user the work they were trying to protect.
SaveBuffer(b *text.Buffer, path string) error
// SplitWindow splits the active window, side by side when vertical is
// true and stacked otherwise, and makes the new window active.
SplitWindow(vertical bool) error
// OtherWindow moves the selection n windows forward in cycle order,
// wrapping; negative n moves backward.
OtherWindow(n int)
// DeleteWindow removes the active window. It is an error to remove the
// sole window.
DeleteWindow() error
// DeleteOtherWindows makes the active window fill the frame.
DeleteOtherWindows()
// Run executes another command by name, which is how M-x and the Lua
// nem.run bridge invoke commands.
Run(name string) error
// CommandNames lists every interactive command name, sorted, for M-x
// completion.
CommandNames() []string
// Bindings maps every bound key sequence to its command name, for
// describe-bindings.
Bindings() map[string]string
// Where lists the key sequences bound to a command, for describe-key and
// for showing a command's binding in M-x.
Where(command string) []string
// Quit ends the editing session. Without force it fails if any buffer has
// unsaved changes.
Quit(force bool) error
}
Env is the whole of the editor a command may touch.
type Func ¶
Func is the signature every command implementation has. A returned error is reported to the user in the echo area; returning ErrQuit is not an error condition but an abandoned operation.
type Isearch ¶
type Isearch struct {
// contains filtered or unexported fields
}
Isearch is one incremental search session.
It exists as an exported type because the session outlives any single call: the minibuffer keymap in the editor binds C-s and C-r while the prompt is open, and advancing to the next match is a property of the session, not of the pattern. Update is the ReadOpts.OnChange hook; Advance is what a repeated C-s calls; Abandon is the C-g path.
Every search runs from the position point held when the session opened, so shortening the pattern walks point back toward that origin rather than leaving it stranded at a match the shorter pattern no longer justifies.
func NewIsearch ¶
NewIsearch opens a session searching forward, or backward when backward is set, from the active window's current point.
func (*Isearch) Abandon ¶
func (s *Isearch) Abandon()
Abandon restores point to where the session opened. This is C-g, and it is the behaviour users rely on most.
func (*Isearch) Advance ¶
func (s *Isearch) Advance()
Advance steps to the next match in the search direction, as a repeated C-s does inside the prompt. It stays put when there is no further match.
type KillRing ¶
type KillRing struct {
// contains filtered or unexported fields
}
KillRing holds killed text, newest first, as a fixed-capacity ring.
Two pieces of state make its behaviour emacs-like, and both are owned here rather than by the caller so that command implementations cannot get them wrong:
- A kill run. Consecutive kill commands accumulate into the single newest entry instead of pushing new ones, so C-k C-k C-k then C-y restores all three lines as one block. Any other command calls BreakRun to end it.
- A yank pointer. Yank reads the entry the pointer rests on; YankPop advances it to successively older entries, wrapping around. The pointer persists after a yank run ends, so a later C-y yanks from wherever M-y left it, as emacs does. Pushing or extending an entry resets it.
The run-awareness deliberately lives here rather than in the command layer, and that is why this API is a KillForward/KillBackward pair rather than a direct port of emacs's kill-new/kill-append. Emacs makes each command decide whether it is starting a kill or extending one, which means every new kill command is one forgotten check away from silently breaking C-k C-k C-y. Here a command states only the direction it killed in and cannot get accumulation wrong. Do not "simplify" this back into a push-only Kill plus a separate Append: that reintroduces exactly the bug this shape prevents.
KillRing is not safe for concurrent use; the editor drives it from the input goroutine only.
func NewKillRing ¶
NewKillRing returns an empty ring. A capacity of zero or less is coerced to DefaultCapacity rather than rejected, so a bad config value degrades to the emacs default instead of breaking the editor.
func (*KillRing) BreakRun ¶
func (k *KillRing) BreakRun()
BreakRun ends any kill run and invalidates YankPop. Every command that is neither a kill nor a yank calls this, which is what makes "kill, move, kill" produce two entries rather than one.
func (*KillRing) KillBackward ¶
KillBackward records text killed backward of point — M-DEL, and anything else that consumes text to the left.
During a kill run it extends the newest entry on the left, so killing backward word by word yields text in reading order rather than reversed; otherwise it pushes a new entry. As with KillForward, the caller states only the direction.
func (*KillRing) KillForward ¶
KillForward records text killed forward of point — C-k, M-d, or a direction-neutral kill such as kill-region.
During a kill run it extends the newest entry on the right; otherwise it pushes a new entry. Callers never decide which: they state the direction and the ring handles accumulation.
func (*KillRing) Yank ¶
Yank returns the entry the yank pointer rests on, without consuming it. It ends any kill run, so a kill after a yank starts a fresh entry, and it makes YankPop valid.
func (*KillRing) YankPop ¶
YankPop advances the yank pointer to the next-older entry, wrapping around to the newest, and returns that entry. The result is the full replacement text: the caller removes what it last yanked and inserts this instead.
It is valid only immediately after Yank or another YankPop.
type ReadOpts ¶
type ReadOpts struct {
// Prompt is shown at the start of the minibuffer line, e.g. "Find file: ".
Prompt string
// Initial pre-fills the minibuffer with editable text.
Initial string
// Complete supplies completion candidates. Nil means no completion at all:
// no candidate panel is built and TAB does nothing, which is what an
// incremental search prompt wants.
Complete CompleteFunc
// RequireMatch governs what RET does when no candidate is highlighted.
//
// Whenever one is highlighted, RET takes it, whatever this says: that is
// what the highlight is for, and a prompt that returned the fragment typed
// instead would open a new file called "rea" with readme.txt highlighted
// right under it. A candidate typed out in full is always ranked first, so
// typing main.go never opens domain.go.
//
// The flag decides only the case where the typed text matches nothing. When
// false, RET returns it as typed, so find-file and switch-to-buffer can name
// something that does not exist yet. When true, RET refuses it with a
// message; execute-extended-command sets this, because inventing a command
// name is meaningless.
//
// M-RET returns the typed text whatever is highlighted, which is how a new
// name is given when it happens to fuzzy-match an existing one. It is
// refused where RequireMatch holds.
RequireMatch bool
// Descend, when non-nil, reports whether candidate is somewhere to walk
// into rather than an answer. RET on such a candidate puts it in the prompt
// and keeps the prompt open, so the list shows what is inside it.
//
// find-file sets it for directories, so RET on a highlighted src/ lists
// src/ instead of trying to visit a directory. It is a hook rather than a
// rule about trailing slashes because the minibuffer cannot know what its
// candidates are: a buffer name may end in a slash and mean nothing by it.
// M-RET ignores it, as it ignores the highlight.
Descend func(candidate string) bool
// OnChange, when non-nil, is called with the full contents after every
// edit of the minibuffer — typed, backspaced, killed or yanked alike.
//
// This is the hook that makes search-as-you-type fall out of the ordinary
// prompt mechanism rather than needing one of its own.
//
// It runs with Win reporting the PRE-PROMPT TEXT WINDOW, not the
// minibuffer's, because a hook that reacts to the pattern needs to move
// point in the buffer being searched. Write an OnChange that assumes the
// minibuffer window and it will move point in the prompt instead, which
// reads as a rendering bug rather than as the mistake it is.
OnChange func(string)
// Session, when non-nil, is the incremental-search session this prompt
// drives. The minibuffer calls its Update after every edit and its Advance
// when the search key is pressed again inside the prompt, so the prompt and
// the search share one session.
//
// It exists because Advance cannot be expressed as a callback: a repeated
// C-s must step to the next match, which is a property of the session
// rather than of the pattern, and is not recoverable from an OnChange
// closure. Passing the session explicitly is what keeps the editor from
// having to guess which prompts are searches from their command names.
//
// Like OnChange, it runs against the pre-prompt text window. Session and
// OnChange are independent: set both and both are called.
Session *Isearch
}
ReadOpts configures a minibuffer prompt.
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry holds every command the editor knows.
It is the single table with three consumers: M-x searches it, the Lua config binds keys against it, and the universal argument feeds it. Lua-defined commands register here alongside the built-ins and are indistinguishable from them at the call site.
Registry is not safe for concurrent use. The editor registers at startup and runs commands from the input goroutine only, and Lua config reloads happen on that same goroutine.
func NewDefaultRegistry ¶
NewDefaultRegistry returns a Registry holding every built-in command.
func (*Registry) All ¶
All returns every registered command, interactive or not, in unspecified order. describe-bindings uses this to resolve names to documentation.
func (*Registry) Names ¶
Names lists the interactive command names, sorted.
Sorted because it feeds M-x completion, where a non-deterministic order would reshuffle the candidate list between invocations.
type Seq ¶
type Seq struct {
// LastYankFrom and LastYankTo bound the text the most recent yank
// inserted; HasLastYank reports whether they are meaningful.
//
// yank-pop cannot work without them: it must delete what the preceding
// yank inserted before putting the rotated entry in its place.
LastYankFrom, LastYankTo text.Pos
HasLastYank bool
// RecenterCycle is recenter-top-bottom's position in its centre, top,
// bottom cycle across successive C-l presses.
RecenterCycle int
// LastRune is the rune of the key that triggered the current command. The
// event loop sets it immediately before dispatch.
//
// It is meaningful only for a command invoked by a self-inserting key, and
// self-insert-command is the one command that needs it: it must insert the
// character that was typed, and nothing else in Env reports which key ran
// the command. ReadKey would prompt the user, which is not the same thing
// at all.
//
// Carrying this here rather than as an Env method is what lets
// self-insert-command be an ordinary registered command, reachable from
// M-x and bindable from Lua, instead of a stub that only the event loop
// can call.
LastRune rune
}
Seq holds state that spans consecutive commands.
Commands read and mutate it in place; the editor resets whichever fields need resetting at dispatch. It is a concrete struct rather than an untyped scratch slot so that every field is type-checked — a failed type assertion inside a command is a panic, and a panic costs the user unsaved work.
A future sequencing need is a new field here, not a new method on Env.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package commandtest provides a headless implementation of command.Env.
|
Package commandtest provides a headless implementation of command.Env. |