// lesson: the-text-buffer
The Text Buffer
Enough plumbing; time for the data structure at the heart of the editor. How should a file being edited live in memory? This question has a fifty-year literature (Crowley's Data Structures for Text Sequences, in the extended reading, is the best survey), and every serious answer is a different point on the same tradeoff curve:
- One flat
std::string. Reading and saving are trivial; rendering needs a scan to find line starts; and inserting a character at position i moves every byte after i. For a 10 MB file with the cursor at the top, that's 10 MB ofmemmoveper keystroke. Fine for a config-file editor, embarrassing beyond that. - A vector of lines (
std::vector<std::string>). Whatvi, kilo, and a large fraction of real editors use. Inserting a character moves only the tail of one line (~40 bytes, not megabytes); inserting or deleting a whole line shifts only the vector's pointers-to-lines, not the text. Rendering is natural โ the viewport asks for lines rโ..rโ โ and per-line syntax highlighting falls out for free. Weaknesses: a pathological single-line file (a minified 5 MBbundle.js) degrades to the flat-string case, and line joins/splits churn allocations. - A gap buffer. The Emacs answer; next lesson, in full.
- A rope โ a balanced tree of string chunks: everything is O(log n), 10 GB files open instantly, and the implementation is 10ร the code and every simple question ("what's at line 12?") becomes a tree walk. The choice of xi and helix.
- A piece table โ the file is never modified; edits are a list of descriptors pointing into the original bytes plus an append-only "add" buffer. Undo is nearly free (the old pieces still exist), which is why VS Code and, going back further, Word use it.
The honest engineering call for a terminal editor aimed at source files: vector of lines, and that's what our editor core uses. The costs it doesn't handle (giant single lines) are real but rare; the costs it avoids (complexity, cache-hostile tree walks for the common case) are paid on every keystroke. Data structure choice is about which operation you make cheap, and an editor's hot operations are: insert/delete a char near the cursor, split/join a line, and read a screenful of consecutive lines.
Positions, and the operations that edit
A position in the buffer is a row/column pair โ a tiny value type,
rule-of-zero, with C++20's = default comparison:
struct Pos {
std::size_t row = 0;
std::size_t col = 0;
bool operator==(const Pos&) const = default;
};
col counts characters into the line's string โ the chars index,
cx in kilo's terminology. (How that differs from the screen column
once tabs enter the picture is the whole next-next lesson.) The buffer's
editing API is three operations, and each returns the cursor position
the editor should move to โ pinning down, in the type signature, a
question every editor must answer ("after Enter, where is the cursor?"):
insert_char(p, c)โ cursor after the inserted char:{row, col+1}.insert_newline(p)โ split the line atp: the current line keeps[0, col), a new line below receives[col, end). Cursor: start of the new line. Pressing Enter at the end of a line inserts an empty line below; at column 0 it pushes the whole line down.backspace(p)โ delete the character beforep. Atcol == 0the line joins: the current line's text is appended to the previous line, and the cursor lands at the join seam. Backspace at{0, 0}does nothing. This join is why Backspace at the start of a line pulls it up โ one operation, two behaviors, both fall out of "delete the boundary before the cursor".
One invariant makes every downstream component simpler: the buffer
always contains at least one line (possibly empty). "Empty buffer" is
{""}, never {} โ so there is always a line for the cursor to sit on,
and no code ever checks "is there a line 0?".
โบ A Vector-of-Lines Buffer
20 ptsImplement TextBuffer per the operations above. Details the tests pin
down:
- The default constructor yields one empty line; constructing from an
empty vector must also normalize to
{""}. line(row)returns the line's text by value; out-of-range rows return"". (Returningconst std::string&would be faster โ and would dangle the moment a caller kept the reference across an edit that reallocates the vector. Return-by-value is the value-semantics default; optimize only the proven hot path.)- Positions passed in are trusted to be valid:
row < line_count(),col <= line(row).size(). (The editor core maintains that invariant; the buffer doesn't re-police it.) to_string()joins lines with'\n'โ{"a", "b"}โ"a\nb", and{""}โ"".
Log in to submit a solution and earn points.