// lesson: the-gap-buffer

The Gap Buffer

Before committing to vector-of-lines forever, build the classic alternative well enough to respect it. The gap buffer is how Emacs has stored text since the 1970s, and it's founded on one empirical observation: edits cluster. You type a character at position 100, the next lands at 101, then 102. A flat string pays a full memmove for each. The gap buffer prepays one move, then rides the cluster for free.

The idea: store the text in one array with a hole โ€” the gap โ€” at the cursor:

        "the qick fox"  with the cursor after "q", gap size 4:
        the q[____]ick fox
              ^gap_start           text[gap_start..gap_end) is dead space
  • Insert at the cursor: write into the gap, gap_start++. O(1).
  • Delete before the cursor: gap_start--. O(1). Delete after: gap_end++. O(1). Nothing is moved; the dead zone just swallows the character. (This should remind you of the append buffer trick: reserve space once, then cheap operations amortize against it.)
  • Move the cursor to position p: slide the gap there by memmove-ing the characters between the old and new gap position โ€” cost proportional to the distance moved, not the file size. A cursor that moves one line pays ~80 bytes; jumping from top to bottom of a 10 MB file pays 10 MB, once, and then edits are O(1) again.
  • Gap exhausted (gap_start == gap_end): reallocate bigger, like std::vector growth โ€” doubling gives amortized O(1) inserts.

The "logical" text is the array minus the gap, so reading position i must skip it:

char at(size_t i) {                     // logical index -> physical
    return i < gap_start ? buf[i] : buf[i + gap_size()];
}

That translation is the gap buffer's tax: every read pays a branch, and substring extraction must be gap-aware. It's also why "vector of lines where each line is a gap buffer" โ€” plausibly the best of both โ€” is rarely worth it: the constant factors eat the win for ordinary line lengths.

Complexity summary, honestly stated: for a buffer of n characters and an edit at distance d from the previous edit โ€” flat string: O(n) per edit, always. Gap buffer: O(d) to seek + amortized O(1) per edit; the pathological case is alternating edits at opposite ends (O(n) each). Vector of lines: O(line length) per edit regardless of distance. Ropes and piece tables: O(log n), for 10ร— the code. Emacs bets edits cluster (they do), vi bets lines are short (they are). Both bets are 50 years old and still paying out.

โ€บ Build the Gap Buffer

25 pts

Implement GapBuffer with logical-index semantics โ€” from the outside it behaves like a std::string; the gap is invisible except through gap_start(), which the tests use to verify you're really moving a gap and not memmove-ing the world:

  • GapBuffer(std::string_view initial): logical content = initial, and the gap sits at the end, with nonzero capacity (so gap_start() == size() after construction).
  • size(), at(i) (i < size() guaranteed by callers), to_string().
  • insert(pos, c): after it, gap_start() == pos + 1 โ€” the gap was moved to pos, the character written, the gap front advanced.
  • erase(pos, n): remove n logical chars starting at pos (callers guarantee pos + n <= size()) by growing the gap over them; after it, gap_start() == pos. Deletion never moves text โ€” the tests can't see that directly, but gap_start() landing exactly at pos means you absorbed, not shifted.
  • When the gap is empty at insert time, grow capacity (any growth policy; doubling is traditional).

Store the text in a std::string or std::vector<char> โ€” raw new[] buys nothing here but a rule-of-five obligation you'd have to meet yourself. (Composition over allocation: the rule of zero means the default special members are already correct.)

Log in to submit a solution and earn points.