Skip to content

Typed gapbuffer indexing - #191

Merged
sminez merged 11 commits into
developfrom
typed-gapbuffer-indexing
Nov 1, 2026
Merged

sminez merged 11 commits into
developfrom
typed-gapbuffer-indexing

Conversation

@sminez

@sminez sminez commented Nov 1, 2026 •

Copy link
Copy Markdown
Owner

This is a big one...

Mixing up character and byte indices when interacting with the GapBuffer has (unsurprisingly) been one of the main sources of logic bugs within ad. The GapBuffer itself is highly defensive about this but previous versions of the public GapBuffer API made use of bare usize parameters for indexing which allowed callers to mess things up (particularly when multi-byte utf-8 characters were involved).

This PR moves everything over to using a set of index types in order to leverage the type system to give stricter guarantees over how indices are constructed and used. Aiming to fully rule out any invalid indexing is probably going to result in an unwieldy API so instead this is about making it easy to do the right thing and easy to audit for potential logic errors based on how indices are constructed.

The diff (so far) is a lot smaller than I was expecting given that the change is so broad, but most of the non-mechanical changes are confined to the buffer::internal module.

Additional changes in this PR

As expected, enforcing this new level of type safety has changed several logic bugs that were possible while everything used usize into type errors. Rather than jump through hoops to keep the current behaviour while making this change, I've opted to include the following bug fixes in this PR.

  • GapBuffer::char_at was reading from inside the gap when the offset was at the gap start, which could return deleted text.
  • Regex-based syntax highlighting mixed char and byte offsets, which gave wrong highlight ranges or panicked on multi-byte text.
  • /re/ and -/re/ addresses passed a char offset as a byte offset, so searches started in the wrong place after multi-byte characters.
  • The cursor position returned after c, < and | edits in Edit programs was incorrect on multi-byte text because the replace delta subtracted a byte span from a char count.
  • Plumbing narrows-to compared the message cursor against byte offsets. cur is now documented and treated as a character offset.
  • Buffer undo grouped using byte lengths, which failed to merge adjacent multi-byte edits and incorrectly merged non-adjacent ones (so undo removed the wrong text).
  • Relative char addresses wrapped to EOF when moving back past the start of the buffer and relative line addresses failed. Both now clamp at the start.

The new index types

This is lifted directly from the doc comment in src/buffer/internal/index.rs

There are four distinct ways to index into a GapBuffer depending on the operation in question. The first three offer different ways of performing arbitrary indexing into the contents of the buffer:

  1. RawIdx - A raw byte offset into the memory allocation held by the GapBuffer. This is only permitted within the buffer::internal module as an implementation detail of the GapBuffer and Slice types. Indexing in this way is never guaranteed to return valid utf-8 data so the caller must ensure that indexes are calculated correctly and that the data read is valid.
  2. ByteIdx - A byte offset within the logical contents of the GapBuffer. This is used primarily by the regex and treesitter APIs where a GapBuffer is being treated as a byte slice. The standard pitfalls of indexing into a utf-8 encoded string all apply (i.e. care must be taken when multi-byte characters are present) and it is not guaranteed that an arbitrary index of this type lies on a character boundary. However, this type does guarantee that indexing in this way always returns bytes from the logical contents of the buffer and not the gap region.
  3. CharIdx - A character offset within the logical contents of the GapBuffer. This is the most expensive index type to work with but it guarantees that indexing in this way always points to a valid utf-8 character boundary. This is used when reading and writing utf-8 data and is efficiently mapped to a RawIdx when inserting / deleting at the start of the gap region.

The fourth index type is LineIdx which is used to quickly jump to the beginning of newline delimited lines within the logical buffer state.

@sminez
sminez force-pushed the typed-gapbuffer-indexing branch from 026dbc9 to e484646 Compare November 1, 2026 11:32
@sminez
sminez merged commit c698d99 into develop Nov 1, 2026
10 checks passed
@sminez
sminez deleted the typed-gapbuffer-indexing branch November 1, 2026 12:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant