turbo-editors/turbo-corepublic Fork 0
v0.9.0
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

README.md · 92 lines · 5.8 KBmarkdown Blame HistoryRaw
🛟 Updated. 28d5985 k33g 18h ago1# buffer
2
3The text of one edited file: its lines, the cursor, the selection and the undo history.
4
5This package knows nothing about terminals, rendering or syntax colouring. Everything here is plain data manipulation, which is why it is fully testable on its own and why every other package can depend on it without dragging a terminal in.
6
7## Model
8
9- Text is a slice of lines, each a slice of runes. A buffer always holds **at least one line**, so an empty buffer is one empty line rather than none.
10- Positions are counted in **runes**, never in bytes, so an accented letter is one column like any other character.
11- `Range` is **half-open**: `Start` is included, `End` is excluded. An empty range is an insertion point.
12- Line endings and the trailing newline of the file that was read are remembered, so saving an untouched buffer reproduces the file byte for byte.
13
14## Reloading is refused over unsaved work
15
16`Reload` re-reads the buffer's file, and returns `ErrModified` instead when the buffer has unsaved changes. That restriction is the whole safety of the operation: something rewriting a file under an *unmodified* buffer costs nothing, and under a *modified* one costs the user's work.
17
18The cursor is kept where it was, clamped into whatever the file now holds — a formatter moves lines about, and putting the cursor back at the top would lose the reader's place. The undo history is discarded, because undoing back past a reload would restore text the file no longer has.
19
20## Public API
21
22### Construction and content
23
24| Function | What it does |
25| --- | --- |
26| `New() *Buffer` | An empty buffer: one empty line |
27| `NewFromString(text string) *Buffer` | A buffer holding `text`, split on line feeds |
28| `Open(path string) (*Buffer, error)` | Reads a file; a missing file yields an empty buffer bound to that path |
29| `(*Buffer) SetText(text string)` | Replaces the whole content |
30| `(*Buffer) Text() string` | The whole content, with the original line endings |
31| `(*Buffer) Line(i int) string` | Line `i`, or `""` if out of range |
32| `(*Buffer) LineRunes(i int) []rune` | A **copy** of line `i` |
33| `(*Buffer) LineLen(i int) int` | Rune count of line `i` |
34| `(*Buffer) LineCount() int` | Number of lines, at least one |
35
36### File
37
38| Function | What it does |
39| --- | --- |
40| `(*Buffer) Save() error` | Writes back to `Path()`; returns `ErrNoPath` if there is none |
41| `(*Buffer) SaveAs(path string) error` | Writes to `path` and adopts it |
42| `(*Buffer) Reload() (bool, error)` | Re-reads the file, reporting whether it changed; `ErrModified` rather than discarding unsaved work |
43| `(*Buffer) Path() string` / `SetPath(string)` | The file this buffer belongs to |
44| `(*Buffer) Modified() bool` | Whether there are unsaved changes |
45
46Saving goes through a temporary file in the same directory followed by a rename, so an interrupted save cannot leave a half-written source file behind. The original file's permissions are preserved.
47
48### Cursor
49
50`Cursor()`, `SetCursor(Position)`, and the movements: `MoveLeft`, `MoveRight`, `MoveUp`, `MoveDown`, `MoveLineStart`, `MoveLineEnd`, `MoveBufferStart`, `MoveBufferEnd`, `MoveWordLeft`, `MoveWordRight`.
51
52`DisplayColumn(line, col)` and `RuneColumn(line, screenCol)` convert between rune columns and screen columns, which differ as soon as a line contains tabs. `TabWidth()` / `SetTabWidth(int)` control the expansion.
53
54### Selection
55
56`StartSelection`, `ClearSelection`, `SelectAll`, `Selection() (Range, bool)`, `SelectedText() string`, `DeleteSelection() bool`.
57
58`Extend(move func())` runs any movement while keeping the anchor — that is what holding Shift does, and it is why there is no Shift-flavoured twin of every movement method:
59
60```go
61b.Extend(b.MoveWordRight) // Shift-Ctrl-Right
62b.Extend(b.MoveDown) // Shift-Down
63```
64
65`SetCursorKeepingSelection(Position)` is the same thing for an absolute position, which a mouse drag needs.
66
67### Editing
68
69`Insert(string)`, `InsertRune(rune)`, `InsertNewlineAndIndent()`, `Backspace()`, `Delete()`, `DeleteRange(Range)`, `ReplaceRange(Range, string) Position`.
70
71Every change goes through `ReplaceRange`, which is the single place where the undo history, the modified flag and the cursor are maintained. Out-of-range and reversed ranges are accepted: they are clamped and ordered.
72
73### Undo
74
75`Undo() bool`, `Redo() bool`, `CanUndo() bool`, `CanRedo() bool`.
76
77A run of typed characters, or a run of backspaces, collapses into a **single** undo step, so undo removes a word rather than a letter. Moving the cursor ends the run. The history is capped at 1000 entries.
78
79## Tests
80
81```sh
82make test # the whole repository
83go test ./buffer/
84```
85
86## Whole-line edits, and one idea of a word
87
88`InsertLineAbove` and `DeleteLine` are Turbo C's Ctrl-N and Ctrl-Y. Both go through `ReplaceRange` — the single mutation path — so each is one entry in the undo history rather than a splice nobody recorded.
89
90Two edges are worth knowing. The **last line** of a buffer has no newline after it to take, so deleting it takes the newline *before* it; taking nothing would leave a blank line behind. And a buffer of **one line** is emptied rather than left with no lines at all, because every other operation here assumes there is always a line to be on. That second case is written out although clamping would reach the same answer without it — the range it would build starts on line −1, and leaning on `clamp` to pull that back to 0 is something a reader has to work out from two files away.
91
92`SelectWord` selects the run of runes `IsWordRune` accepts, which is the same word `MoveWordLeft` moves by and the same one the completion popup uses to work out what is being typed. One editor, one idea of a word. A position that is not on a word rune selects nothing and only moves the cursor: editors disagree about what a run of punctuation means, and nothing is at least predictable.