text

package
v0.1.7 Latest Latest
Warning

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

Go to latest
Published: Sep 24, 2026 License: MIT Imports: 6 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

This section is empty.

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.

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) 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) 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) 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.

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) 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.

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 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) 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) 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) 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) Width

func (l *Line) Width() ColIdx

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

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