text

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: 12 Imported by: 0

Documentation

Overview

Package text implements nem's buffer model: lines, positions, the two mutation primitives every edit composes from, and the undo log.

It has no dependency on a terminal or on any other nem package, so all of it is testable with plain table-driven tests.

Three coordinate spaces meet in this package and must never be conflated:

  • RuneIdx an index into a line's []rune. Storage and editing.
  • grapheme a cluster boundary; the only place a cursor may rest.
  • ColIdx a display column on screen. Tabs jump to the next tab stop, CJK glyphs occupy two cells, combining marks occupy none.

RuneIdx and ColIdx are distinct types so the compiler rejects mixing them.

Index

Constants

This section is empty.

Variables

View Source
var ErrNoPath = errors.New("text: buffer has no file path")

ErrNoPath is returned by Save when the buffer has no associated file.

View Source
var ErrOutOfRange = errors.New("text: position out of range")

ErrOutOfRange is returned when a Pos does not name a location in the buffer.

View Source
var ErrReadOnly = errors.New("buffer is read-only")

ErrReadOnly is returned by an edit to a read-only buffer.

Functions

func RowOf added in v0.11.0

func RowOf(starts []RuneIdx, i RuneIdx) int

RowOf is which of the rows WrapRows gives the rune index i falls in. The end of the line is on the last row.

Types

type Buffer

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

Buffer is a sequence of lines plus the editing state that belongs to the text itself rather than to any view of it.

Point deliberately does not live here: several windows may show one buffer, each with its own cursor. Buffer keeps only savePt, the point restored when a window next visits it.

func LoadFile

func LoadFile(path string) (*Buffer, error)

LoadFile reads path into a new buffer. A file that does not exist yields an empty buffer bound to that path rather than an error: that is find-file on a new file, not a failure. Invalid UTF-8 is replaced with U+FFFD.

func NewBuffer

func NewBuffer() *Buffer

NewBuffer returns an empty buffer holding a single empty line.

func (*Buffer) ActivateMark added in v0.1.2

func (b *Buffer) ActivateMark()

ActivateMark makes the region live. With no mark set there is no region, so it does nothing.

func (*Buffer) BeginUndoGroup

func (b *Buffer) BeginUndoGroup()

BeginUndoGroup starts a group: every Insert and Delete until the matching EndUndoGroup undoes and redoes as one unit.

A command that composes several primitives needs this. Moving a line is a delete plus an insert, so without grouping five presses of the move key would cost ten presses of undo.

Groups nest by depth, so a helper may group its own edits without splitting its caller's group. Pair it with defer, or with EndUndoGroup on every path.

func (*Buffer) BreakUndo

func (b *Buffer) BreakUndo()

BreakUndo ends the current undo unit, so subsequent typing starts a new one. The editor calls it on movement, on any non-inserting command, and on save.

It also abandons an undo group left open by a command that bailed out. That bounds the damage to the one command: without it an unclosed group would keep absorbing later edits until a single undo reverted the whole session.

func (*Buffer) ClampPos

func (b *Buffer) ClampPos(p Pos) Pos

ClampPos returns the position in p clamped into the buffer.

func (*Buffer) ClearMark

func (b *Buffer) ClearMark()

ClearMark forgets the mark entirely, which also deactivates the region. The ring of earlier marks is kept: forgetting the current selection is not forgetting where you have been.

func (*Buffer) Contents added in v0.1.8

func (b *Buffer) Contents() []byte

Contents is the buffer's text as UTF-8, lines joined by "\n" with no trailing newline: String as bytes, without the string in between. The autosave writes it every thirty seconds, so for a large file the copies it saves are a stall it saves.

func (*Buffer) DeactivateMark added in v0.1.2

func (b *Buffer) DeactivateMark()

DeactivateMark ends the selection but keeps the mark, so C-x C-x can still return to it. This is what C-g does, and what any buffer-changing command does afterwards.

func (*Buffer) Delete

func (b *Buffer) Delete(from, to Pos) error

Delete removes the runes between from and to, joining lines as needed. The endpoints may be given in either order. It is the other primitive.

func (*Buffer) End

func (b *Buffer) End() Pos

End returns the position just past the last rune in the buffer.

func (*Buffer) EndUndoGroup

func (b *Buffer) EndUndoGroup()

EndUndoGroup closes the outermost open group. Calling it with no group open is a no-op, so a stray call cannot corrupt the history.

func (*Buffer) FinalNewline added in v0.8.0

func (b *Buffer) FinalNewline() bool

FinalNewline reports whether saving ends the file with a newline after its last line.

That newline is not in the buffer as an empty last line: a file read with one is shown without it, as every editor shows it, and the buffer only remembers to write it back. So a buffer whose last line is empty ends its file with a newline even when this is false - the one that ends the line before.

func (*Buffer) HasMark

func (b *Buffer) HasMark() bool

HasMark reports whether a mark has been set in this buffer.

The zero Pos is a legitimate mark position, so a flag is the only way to tell "mark at the buffer start" from "no mark at all". Region commands must check this: emacs refuses to act on a region in a buffer with no mark, and without the distinction C-w would silently kill from the buffer start to point.

func (*Buffer) Insert

func (b *Buffer) Insert(at Pos, rs []rune) error

Insert inserts rs at at, splitting the line at any newline in rs. It is one of the two primitives every edit composes from.

func (*Buffer) Line

func (b *Buffer) Line(i int) *Line

Line returns the line at index i.

func (*Buffer) Mark

func (b *Buffer) Mark() Pos

Mark returns the buffer's mark, the far end of the region.

func (*Buffer) MarkActive added in v0.1.2

func (b *Buffer) MarkActive() bool

MarkActive reports whether the region is live: drawn as a selection, and replaced by typing, Backspace or a yank.

A mark can exist without the region being active, and the difference matters. When the two were conflated, every command that set a mark for navigation silently created a selection: M-> M-< then typing a character deleted the whole file, and typing after C-y deleted what had just been pasted. This is emacs's transient-mark-mode distinction between the mark and mark-active.

Region commands that consume the region themselves - C-w, M-w, C-x C-x - use HasMark instead, so C-y then C-w still kills what was yanked, as emacs does by default.

func (*Buffer) Modified

func (b *Buffer) Modified() bool

Modified reports whether the buffer differs from its last saved state.

func (*Buffer) NumLines

func (b *Buffer) NumLines() int

NumLines returns the number of lines. It is always at least 1.

func (*Buffer) Path

func (b *Buffer) Path() string

Path returns the file this buffer is associated with, empty if none.

func (*Buffer) PopMark added in v0.4.0

func (b *Buffer) PopMark() (Pos, bool)

PopMark returns the mark, for point to jump to, and makes the most recent earlier mark the mark, sending the current one to the far end of the ring - so popping again and again visits every remembered position in turn, as emacs's C-u C-SPC does. It reports false when there is no mark at all.

func (*Buffer) ReadOnly added in v0.1.3

func (b *Buffer) ReadOnly() bool

ReadOnly reports whether the buffer refuses edits.

func (*Buffer) Redo

func (b *Buffer) Redo() (Pos, bool)

Redo reapplies the most recently undone edit, returning where point lands.

func (*Buffer) Regenerate added in v0.1.3

func (b *Buffer) Regenerate(rs []rune)

Regenerate replaces the whole text of a buffer whose contents are generated, such as a directory listing. It works on a read-only buffer, leaves it unmodified, and discards the undo history: undoing back to an old listing would show files that are no longer there.

func (*Buffer) RegenerateLine added in v0.1.8

func (b *Buffer) RegenerateLine(i int, rs []rune)

RegenerateLine replaces the text of one line of a generated buffer, as Regenerate does the whole: past read-only, with no undo record, leaving the buffer unmodified. A listing uses it when one entry changes, so marking a file costs one line rather than the whole directory.

func (*Buffer) RegenerateTail added in v0.7.0

func (b *Buffer) RegenerateTail(from int, rs []rune)

RegenerateTail replaces everything from the start of line from to the end of the buffer with rs, as Regenerate replaces the whole: past read-only and an edit guard, with no undo record, leaving the buffer unmodified.

It is for a generated buffer that grows - a program's output, arriving a piece at a time - which rewrites only its last, unfinished line and what comes after it, rather than all of itself on every piece.

func (*Buffer) Revert added in v0.7.0

func (b *Buffer) Revert() error

Revert replaces the buffer's text with its file's as it is on disk now, and leaves the buffer unmodified: what the file says is what the buffer says again, line endings and final newline included.

It is one undoable edit, as emacs's revert is, so text thrown away by a revert that was not meant is a C-/ away. A file that already matches changes nothing and leaves nothing to undo.

func (*Buffer) Revision

func (b *Buffer) Revision() uint64

Revision returns a counter that advances once per mutation.

It never resets, and a mutation that changes nothing does not advance it.

func (*Buffer) Save

func (b *Buffer) Save() error

Save writes the buffer back to its file, preserving the line ending style and trailing-newline convention it was loaded with. The file is replaced whole or not at all; see writeFile.

func (*Buffer) SaveAs

func (b *Buffer) SaveAs(path string) error

SaveAs writes the buffer to path and adopts it as the buffer's file.

func (*Buffer) SavePoint

func (b *Buffer) SavePoint() Pos

SavePoint returns the point stored for when a window next visits this buffer.

func (*Buffer) SaveTop added in v0.1.5

func (b *Buffer) SaveTop() int

SaveTop returns the first line shown when a window last left this buffer.

func (*Buffer) SetEditGuard added in v0.5.0

func (b *Buffer) SetEditGuard(g EditGuard)

SetEditGuard makes the buffer accept only the edits g allows, or every edit again with nil: a buffer read-only in parts, such as a directory listing whose file names are being edited.

Undo and redo are not vetted. They replay edits, and every edit made since the guard went in was one it allowed, so a guard should go in on a buffer whose history is clear - one just regenerated.

func (*Buffer) SetFinalNewline added in v0.8.0

func (b *Buffer) SetFinalNewline(on bool)

SetFinalNewline sets whether saving ends the file with a newline after its last line. It is not an edit: nothing on screen changes, nor does the undo history, as nothing does when a file is read with or without one.

func (*Buffer) SetMark

func (b *Buffer) SetMark(p Pos)

SetMark sets the buffer's mark and records that a mark now exists.

It does NOT activate the region. Setting a mark and selecting text are different acts: yank sets the mark so C-x C-x can select what was yanked, and the buffer-edge jumps set it so C-x C-x can return, but in neither case is the text between mark and point a selection. Commands that mean to select call ActivateMark as well.

func (*Buffer) SetModified

func (b *Buffer) SetModified(m bool)

SetModified marks the buffer clean or dirty. Marking it clean records the current undo position, so undoing back to it clears the flag again.

func (*Buffer) SetPath

func (b *Buffer) SetPath(p string)

SetPath associates the buffer with a file path.

func (*Buffer) SetReadOnly added in v0.1.3

func (b *Buffer) SetReadOnly(ro bool)

SetReadOnly makes the buffer refuse or accept edits. Undo is refused too while it is set, since undoing is editing.

func (*Buffer) SetSavePoint

func (b *Buffer) SetSavePoint(p Pos)

SetSavePoint stores the point for when a window next visits this buffer.

func (*Buffer) SetSaveTop added in v0.1.5

func (b *Buffer) SetSaveTop(line int)

SetSaveTop stores the first line shown, for when a window next visits.

func (*Buffer) String

func (b *Buffer) String() string

String returns the whole buffer as text, without a trailing newline.

func (*Buffer) TakeDirty

func (b *Buffer) TakeDirty() (from, delta int, rev uint64)

TakeDirty reports what has changed since the last call and clears the record.

It returns the lowest line touched, the net change in line count, and the current revision. When nothing has changed, from is >= NumLines and delta is zero.

It CONSUMES: the name says so because a second reader calling it would take the notification from the first, which would then never learn of the edit. nem attaches exactly one highlight cache to a buffer. A second reader is not a correctness problem even so - it sees the revision advance without a dirty line, which is a signal it cannot account for, and the cache treats that as "trust nothing" rather than as "nothing changed". The result is slower, not wrong.

func (*Buffer) Text

func (b *Buffer) Text(from, to Pos) []rune

Text returns a copy of the runes between from and to, with '\n' at line boundaries. The endpoints may be given in either order.

func (*Buffer) Undo

func (b *Buffer) Undo() (Pos, bool)

Undo reverts the most recent edit, returning where point should land. A read-only buffer reports nothing to undo; callers that want to say why check ReadOnly first.

func (*Buffer) Vet added in v0.5.0

func (b *Buffer) Vet(from, to Pos, ins []rune) error

Vet reports whether an edit would be made, without making it: the deletion of the text between from and to, or, with from equal to to, the insertion of ins there. It is for a command that should pass over what it may not change rather than stop at it - a replace across a buffer read-only in parts.

type Cluster

type Cluster struct {
	// Runes is lent from the line, not copied. It is valid only until the line
	// is edited, and must not be mutated or retained. Copy it if you need to
	// keep it.
	Runes []rune
	// Start is the rune index where the cluster begins.
	Start RuneIdx
	// Col is the display column where the cluster begins.
	Col ColIdx
	// Width is how many display columns the cluster occupies. Zero for a
	// combining mark that merged into the preceding cluster is impossible here,
	// since such a mark is part of that cluster rather than one of its own.
	Width ColIdx
}

Cluster is one grapheme cluster's placement within a line: the runes it is made of, where they start, and the screen columns they occupy.

A cluster is the smallest unit that occupies a cell, which is why it rather than the rune is what a renderer walks. "é" written as e+U+0301 is two runes in one cell; a ZWJ family emoji is seven runes in two cells; a tab is one rune spanning however many columns reach the next tab stop.

type ColIdx

type ColIdx int

ColIdx is a display column on screen. It is not a rune index.

var TabWidth ColIdx = 8

TabWidth is the number of columns between tab stops. Changing it invalidates every line's cached layout lazily, on next use.

type EditGuard added in v0.5.0

type EditGuard func(from, to Pos, ins []rune) error

EditGuard vets an edit: the deletion of the text between from and to, or, with from equal to to, the insertion of ins there. An error refuses it, and is what the edit returns.

type Line

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

Line is a single line of text plus a cached grapheme/column layout. The cache is built lazily and invalidated on edit.

Guaranteed: DisplayCol(Len()) == Width(). Renderers rely on it to size the last grapheme of a line, computing a cluster's width as the difference between its own start column and the next boundary's - and for the final cluster that next boundary is Len(). Anything changing DisplayCol must keep this true; text/guarantee_test.go pins it across every width class.

func NewLine

func NewLine(rs []rune) Line

NewLine returns a Line holding a copy of rs.

func (*Line) ASCII added in v0.9.3

func (l *Line) ASCII() bool

ASCII reports whether every rune of the line is ASCII: nothing to lay out right to left, nothing wide. It is learnt while the line is measured, so asking costs nothing for a line on screen.

func (*Line) At added in v0.1.8

func (l *Line) At(i RuneIdx) rune

At returns the rune at index i, which must be in range. Reading one rune through Runes copied the whole line, which made stepping along a line by runes cost the square of its length.

func (*Line) Clusters

func (l *Line) Clusters() iter.Seq[Cluster]

Clusters iterates the line's grapheme clusters in order, lending each one's runes.

This exists because the render path walks every visible line on every keystroke. Reconstructing cluster boundaries from NextGrapheme plus two DisplayCol lookups costs a binary search per lookup over the segment cache, and Runes() copies the whole line; iterating the cache directly does neither.

Breaking out of the loop stops the walk, which the render path relies on to abandon a line as soon as a cluster passes the right edge of the window.

func (*Line) ClustersFrom added in v0.1.8

func (l *Line) ClustersFrom(col ColIdx) iter.Seq[Cluster]

ClustersFrom is Clusters starting from the cluster that covers display column col, or the first one after it - the first cluster a window scrolled to col can show any of.

A renderer scrolled far along a long line otherwise walks every cluster to its left on each frame just to skip it; the segment cache knows every cluster's column, so a binary search finds the place instead.

func (*Line) DisplayCol

func (l *Line) DisplayCol(i RuneIdx) ColIdx

DisplayCol returns the screen column at which the grapheme containing rune index i begins. Out-of-range indices clamp to the ends of the line, so DisplayCol(Len()) is the line's Width - a guarantee callers depend on to measure the final cluster without a special case.

func (*Line) Forget added in v0.9.4

func (l *Line) Forget()

Forget drops the line's layout, to be measured again when it is next needed. A renderer does this for lines that scroll out of view, so that a file scrolled through from end to end keeps only what is on screen measured, rather than every line it ever showed.

func (*Line) Len

func (l *Line) Len() RuneIdx

Len returns the number of runes in the line.

func (*Line) NextGrapheme

func (l *Line) NextGrapheme(i RuneIdx) RuneIdx

NextGrapheme returns the next grapheme boundary strictly after i, clamped to the end of the line.

func (*Line) PrevGrapheme

func (l *Line) PrevGrapheme(i RuneIdx) RuneIdx

PrevGrapheme returns the previous grapheme boundary strictly before i, clamped to the start of the line.

func (*Line) RuneAt

func (l *Line) RuneAt(c ColIdx) RuneIdx

RuneAt returns the rune index of the grapheme occupying display column c. A column landing inside a wide glyph clamps to that glyph's first rune, so the cursor never sits in the right half of a CJK character or an emoji.

func (*Line) Runes

func (l *Line) Runes() []rune

Runes returns a copy of the line's runes.

func (*Line) String

func (l *Line) String() string

String returns the line's text.

func (*Line) View added in v0.1.8

func (l *Line) View() []rune

View lends the line's runes without copying them, for code that only reads - a search scanning every line of a file, say. Like Cluster.Runes it is valid only until the line is edited, and must not be modified or kept.

func (*Line) Width

func (l *Line) Width() ColIdx

Width returns the line's total display width in columns.

func (*Line) WrapRows added in v0.11.0

func (l *Line) WrapRows(width ColIdx) []RuneIdx

WrapRows is where each row of the line starts when it is folded into rows width columns wide: the rune index of each row's first cluster, the first always 0. A line that fits is one row.

Every row holds at least one cluster, so a glyph wider than the whole row has a row of its own and runs past its edge. The rows are kept until the line changes, so a line drawn frame after frame is folded once.

type Pos

type Pos struct {
	Line int
	Col  RuneIdx
}

Pos is a location in a buffer: a line number and a rune index within it.

func MaxPos

func MaxPos(p, q Pos) Pos

MaxPos returns whichever of p and q sorts later.

func MinPos

func MinPos(p, q Pos) Pos

MinPos returns whichever of p and q sorts earlier.

func OrderPos

func OrderPos(p, q Pos) (lo, hi Pos)

OrderPos returns p and q sorted, for normalizing a region whose endpoints may be in either order (point and mark, typically).

func (Pos) After

func (p Pos) After(q Pos) bool

After reports whether p sorts strictly after q.

func (Pos) Before

func (p Pos) Before(q Pos) bool

Before reports whether p sorts strictly before q.

func (Pos) Equal

func (p Pos) Equal(q Pos) bool

Equal reports whether p and q are the same position.

type RuneIdx

type RuneIdx int

RuneIdx is an index into a line's []rune. It is not a screen column.

type UndoLog

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

UndoLog is a linear undo history. Undoing and then making a fresh edit discards the redo branch, which is the behaviour most editors have and emacs conspicuously does not.

Jump to

Keyboard shortcuts

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