Minga.Buffer.Document (Minga v0.1.0)

Copy Markdown View Source

A gap buffer implementation for text editing.

The gap buffer stores text as two binaries: before contains all text before the cursor (in natural order), and after contains all text after the cursor (in natural order). The "gap" is the conceptual space between them where insertions happen in O(1).

Moving the cursor shifts characters between the two binaries. This gives O(1) insertions and deletions at the cursor, and O(n) cursor movement (where n is the distance moved). For an interactive editor where the cursor moves incrementally, this is ideal.

Byte-indexed positions

All positions are zero-indexed {line, byte_col} tuples, where byte_col is the byte offset within the line. For ASCII content (the common case), byte offset equals grapheme index. For multi-byte UTF-8 characters, byte offset is larger (e.g., é = 2 bytes, emoji = 4+ bytes).

This representation enables O(1) binary_part slicing throughout the editor and aligns with tree-sitter's byte-offset model.

Use grapheme_col/2 to convert a byte-indexed position to a display column for rendering.

Examples

iex> buf = Minga.Buffer.Document.new("hello\nworld")
iex> Minga.Buffer.Document.cursor(buf)
{0, 0}
iex> buf = Minga.Buffer.Document.insert_text(buf, "H")
iex> Minga.Buffer.Document.content(buf)
"Hhello\nworld"

Summary

Types

A direction for cursor movement.

Cached line-start byte offsets.

t()

A gap buffer instance.

Functions

Returns one byte from the document without materializing full content.

Returns the full text content of the buffer.

Returns the content and cursor position in a single call, avoiding separate content/1 + cursor/1 round-trips.

Returns the content between two positions inclusive on both ends. If the positions are reversed, they are normalised automatically.

Returns the total byte size of the document without materializing full content.

Returns the grapheme count between two positions inclusive on both ends. If the positions are reversed, they are normalised automatically.

Returns the current cursor position as a {line, byte_col} tuple.

Returns the byte offset of the cursor in the full text.

Deletes the character at the cursor (delete forward). Returns the buffer unchanged if the cursor is at the end.

Deletes the character before the cursor (backspace). Returns the buffer unchanged if the cursor is at the beginning.

Deletes lines [start_line, end_line] inclusive from the buffer.

Deletes the text between two positions inclusive on both ends. If the positions are reversed, they are normalised automatically. The cursor is placed at the earlier position.

Returns true if the buffer contains no text.

Inserts a multi-character string at the cursor position in a single binary operation.

Returns the position of the last selectable character on a line.

Returns the total number of lines in the buffer.

Creates a new gap buffer from a string. Cursor starts at {0, 0}.

Returns a byte range from the document without materializing full content.

Types

direction()

@type direction() :: :left | :right | :up | :down

A direction for cursor movement.

line_offsets()

@type line_offsets() :: tuple() | nil

Cached line-start byte offsets.

Clean: a tuple of line-start positions {0, 6, 12, ...}. Pending: {:pending, starts_tuple, adjust_after_line, delta} for deferred shift. nil when the cache needs a full rebuild.

position()

@type position() :: Minga.Buffer.Position.t()

t()

@type t() :: %Minga.Buffer.Document{
  after: String.t(),
  before: String.t(),
  cursor_col: non_neg_integer(),
  cursor_line: non_neg_integer(),
  line_count: pos_integer(),
  line_offsets: line_offsets()
}

A gap buffer instance.

Functions

byte_at(doc, offset)

@spec byte_at(t(), non_neg_integer()) :: byte()

Returns one byte from the document without materializing full content.

clear_line(buf, line_num)

See Minga.Buffer.Selection.clear_line/2.

content(document)

@spec content(t()) :: String.t()

Returns the full text content of the buffer.

content_and_cursor(document)

@spec content_and_cursor(t()) :: {String.t(), position()}

Returns the content and cursor position in a single call, avoiding separate content/1 + cursor/1 round-trips.

content_between_inclusive(buf, from_pos, to_pos)

@spec content_between_inclusive(t(), position(), position()) :: String.t()

Returns the content between two positions inclusive on both ends. If the positions are reversed, they are normalised automatically.

content_byte_size(document)

@spec content_byte_size(t()) :: non_neg_integer()

Returns the total byte size of the document without materializing full content.

content_on_lines(buf, start_line, end_line)

@spec content_on_lines(t(), non_neg_integer(), non_neg_integer()) :: String.t()

See Minga.Buffer.Selection.line_contents/3.

content_range_length(buf, from_pos, to_pos)

@spec content_range_length(t(), position(), position()) :: non_neg_integer()

Returns the grapheme count between two positions inclusive on both ends. If the positions are reversed, they are normalised automatically.

cursor(document)

@spec cursor(t()) :: position()

Returns the current cursor position as a {line, byte_col} tuple.

cursor_offset(document)

@spec cursor_offset(t()) :: non_neg_integer()

Returns the byte offset of the cursor in the full text.

delete_at(buf)

@spec delete_at(t()) :: t()

Deletes the character at the cursor (delete forward). Returns the buffer unchanged if the cursor is at the end.

delete_before(buf)

@spec delete_before(t()) :: t()

Deletes the character before the cursor (backspace). Returns the buffer unchanged if the cursor is at the beginning.

Examples

iex> buf = Minga.Buffer.Document.new("hello")
iex> buf = Minga.Buffer.Document.move_to(buf, {0, 5})
iex> buf = Minga.Buffer.Document.delete_before(buf)
iex> Minga.Buffer.Document.content(buf)
"hell"

delete_lines(buf, start_line, end_line)

@spec delete_lines(t(), non_neg_integer(), non_neg_integer()) :: t()

Deletes lines [start_line, end_line] inclusive from the buffer.

The cursor is placed at the beginning of the line that now occupies the start position (or the last remaining line if fewer lines remain).

delete_range(buf, from_pos, to_pos)

@spec delete_range(t(), position(), position()) :: t()

Deletes the text between two positions inclusive on both ends. If the positions are reversed, they are normalised automatically. The cursor is placed at the earlier position.

empty?(document)

@spec empty?(t()) :: boolean()

Returns true if the buffer contains no text.

Examples

iex> Minga.Buffer.Document.empty?(Minga.Buffer.Document.new(""))
true
iex> Minga.Buffer.Document.empty?(Minga.Buffer.Document.new("hi"))
false

grapheme_col(buf, value)

See Minga.Buffer.Position.display_column/2.

insert_text(buf, text)

@spec insert_text(t(), String.t()) :: t()

Inserts a multi-character string at the cursor position in a single binary operation.

Examples

iex> buf = Minga.Buffer.Document.new("world")
iex> buf = Minga.Buffer.Document.insert_text(buf, "hello ")
iex> Minga.Buffer.Document.content(buf)
"hello world"

iex> buf = Minga.Buffer.Document.new("end")
iex> buf = Minga.Buffer.Document.insert_text(buf, "line1\nline2\n")
iex> Minga.Buffer.Document.content(buf)
"line1\nline2\nend"
iex> Minga.Buffer.Document.cursor(buf)
{2, 0}

last_grapheme_byte_offset(text)

@spec last_grapheme_byte_offset(String.t()) :: non_neg_integer()

Returns the position of the last selectable character on a line.

line_at(buf, line_num)

@spec line_at(t(), non_neg_integer()) :: String.t() | nil

See Minga.Buffer.Lines.fetch/2.

line_count(document)

@spec line_count(t()) :: pos_integer()

Returns the total number of lines in the buffer.

An empty buffer counts as one line.

Examples

iex> buf = Minga.Buffer.Document.new("one\ntwo\nthree")
iex> Minga.Buffer.Document.line_count(buf)
3
iex> Minga.Buffer.Document.line_count(Minga.Buffer.Document.new(""))
1

lines(buf, start, count)

@spec lines(t(), non_neg_integer(), non_neg_integer()) :: [String.t()]

See Minga.Buffer.Lines.slice/3.

move(buf, direction)

See Minga.Buffer.Cursor.move/2.

move_to(buf, target)

See Minga.Buffer.Cursor.place/2.

new(text \\ "")

@spec new(String.t()) :: t()

Creates a new gap buffer from a string. Cursor starts at {0, 0}.

Examples

iex> buf = Minga.Buffer.Document.new("hello")
iex> Minga.Buffer.Document.content(buf)
"hello"
iex> Minga.Buffer.Document.cursor(buf)
{0, 0}

offset_to_position(doc, offset)

See Minga.Buffer.Position.from_point/2.

position_to_offset(buf, target)

See Minga.Buffer.Position.point_for/2.

slice_byte_range(doc, start_byte, byte_count)

@spec slice_byte_range(t(), non_neg_integer(), non_neg_integer()) :: binary()

Returns a byte range from the document without materializing full content.