pub struct Buffer { /* private fields */ }Expand description
A rope-backed text buffer: the raw text storage under every
Document.
Addressing is by [Position] — a 0-based line plus a 0-based
UTF-8 byte offset within that line (not a char or column index).
Every mutation goes through Buffer::apply_edit, which returns an
AppliedEdit carrying both the inverse (for undo) and the
tree-sitter-shaped [EditDelta] (for incremental reparse).
Buffer knows nothing about versions, undo, dirtiness or cursors — that
bookkeeping is Document’s. Reach for Buffer
directly only when you need text without the document around it.
Cloning is cheap: ropey::Rope shares its chunks by Arc.
§Examples
use lattice_core::Buffer;
use lattice_core::protocol::edit::Edit;
use lattice_core::protocol::position::{Position, Range};
let mut buf = Buffer::from_text("hello\nworld\n");
// Replace "world" (line 1, bytes 0..5) with "there".
let range = Range::new(Position::new(1, 0), Position::new(1, 5));
let applied = buf.apply_edit(&Edit::replace(range, "there"))?;
assert_eq!(buf.as_string(), "hello\nthere\n");
assert_eq!(applied.replaced_text, "world"); // what undo re-inserts
assert_eq!(buf.line(1).as_deref(), Some("there"));
// Two lines as a user counts them; ropey counts the empty tail too.
assert_eq!(buf.content_line_count(), 2);
assert_eq!(buf.rope_line_count(), 3);Implementations§
Source§impl Buffer
impl Buffer
Sourcepub fn from_text(text: &str) -> Self
pub fn from_text(text: &str) -> Self
A buffer holding a copy of text. No line-ending normalisation is
done: \r\n stays two bytes.
Sourcepub fn rope_line_count(&self) -> u32
pub fn rope_line_count(&self) -> u32
ropey’s raw line count: the number of line starts, so a buffer
ending in \n reports one extra (empty) line and an empty buffer
reports 1. Saturates at u32::MAX.
This is the right bound for [Position::line] validity — the empty
line after a trailing newline is addressable (it is where an append
at end-of-file lands). For “how many lines does this file have”, use
Self::content_line_count.
§Examples
use lattice_core::Buffer;
assert_eq!(Buffer::empty().rope_line_count(), 1);
assert_eq!(Buffer::from_text("a\nb").rope_line_count(), 2);
assert_eq!(Buffer::from_text("a\nb\n").rope_line_count(), 3);
assert_eq!(Buffer::from_text("a\nb\n").content_line_count(), 2);Sourcepub fn content_line_count(&self) -> u32
pub fn content_line_count(&self) -> u32
Lines the document actually has, in the sense every editor
and every user means: "a\nb\n" is two lines, and so is
"a\nb". A trailing newline terminates the last line rather
than starting a new one.
This is the count for anything that lays out, addresses or
counts document content — display rows, G, :$, %
progress. Self::rope_line_count is ropey’s raw count and reports
one more for any rope ending in \n; feeding that to a layout
or coverage range is what puts a phantom empty row at the end
of every normal file (CV.2).
Never zero: an empty buffer is one empty line, matching
Self::rope_line_count and vim.
Sourcepub fn as_string(&self) -> String
pub fn as_string(&self) -> String
The whole buffer as one String. O(n) allocation — never call
this on a per-frame or per-keystroke path; use Self::line,
Self::slice or Self::to_rope instead.
Sourcepub fn to_rope(&self) -> Rope
pub fn to_rope(&self) -> Rope
Clone the underlying rope. Rope::clone
is Arc-share of the underlying chunks (no deep copy), so
the cost is one refcount bump per chunk. Used by the
diff subsystem’s BufferTextProvider impl to hand a
snapshot rope to BufferSource
from inside the worker’s spawn_blocking body. Exposes
only Rope (not the internal Rope field), so the
abstraction barrier the pub(crate) rope() method
protects stays in place.
Slice: D.3.a (2026-05-29).
Sourcepub fn line(&self, line: u32) -> Option<String>
pub fn line(&self, line: u32) -> Option<String>
Materialise one logical line (without its trailing \n).
Returns None if line is past the end. Locating the line
is O(log n); copying its bytes is O(line_len).
Why this exists: the renderer’s hot path needs only the
visible window of lines per frame. Calling Self::as_string
followed by split('\n').collect::<Vec<_>>() once per frame
allocates O(buffer size) bytes; a 100MB log blows the §8.2
frame budget on every paint. Iterating one line at a time
keeps the per-frame work proportional to the viewport, not
the document.
Sourcepub fn line_shapes_from<'a>(
&'a self,
start: u32,
unit: &'a IndentUnit,
) -> impl Iterator<Item = LineShape> + 'a
pub fn line_shapes_from<'a>( &'a self, start: u32, unit: &'a IndentUnit, ) -> impl Iterator<Item = LineShape> + 'a
Allocation-free LineShapes from start to the end of the
rope — the indent-guide builder’s read path.
Two properties, and the guide layer needs both:
- Allocation-free.
Self::lineallocates aStringper call, and the guide layer is rebuilt over its whole covered range on every keystroke — belowWINDOW_CAP_LINESthat is the whole document. Streaming the rope slice’s chars instead keeps the pass allocation-free, andLineShape::from_charsstops reading each line as soon as the answer cannot change. - Sequential. Asking for line
icosts aO(log n)B-tree descent; asking fornlines one index at a time costsnof them.Linesis a cursor that walks chunk by chunk, so the whole covered range costs one descent plus a linear walk — which is what the builder’s forward pass wants. Callers that genuinely need one line probe withline_shapes_from(i).next()and pay the single descent knowingly.
start past the end of the rope yields nothing rather than
panicking, so an out-of-range probe reads as “no such line”.
Sourcepub fn line_byte_len(&self, line: u32) -> u32
pub fn line_byte_len(&self, line: u32) -> u32
Byte length of one line excluding the trailing newline. O(log n). Cheap helper for renderers that want to know a line’s width without materialising its text.
Sourcepub fn slice(&self, range: Range) -> CoreResult<String>
pub fn slice(&self, range: Range) -> CoreResult<String>
Copy the text in the half-open range ([start, end)).
Both endpoints are validated like Self::position_to_byte and then
rounded down to a UTF-8 char boundary, so a mid-codepoint offset
never panics. A range may span lines; the newlines between are
included.
§Errors
[ProtocolError::PositionOutOfBounds] if either endpoint is out of
bounds, [ProtocolError::InvalidRange] if end precedes start
(both wrapped in CoreError::Protocol).
§Examples
use lattice_core::Buffer;
use lattice_core::protocol::position::{Position, Range};
let buf = Buffer::from_text("héllo\nworld");
// "é" is two bytes, so "llo" starts at byte 3.
let r = Range::new(Position::new(0, 3), Position::new(1, 2));
assert_eq!(buf.slice(r).ok().as_deref(), Some("llo\nwo"));
// Line 5 does not exist.
let bad = Range::new(Position::new(0, 0), Position::new(5, 0));
assert!(buf.slice(bad).is_err());Sourcepub fn apply_edit(&mut self, edit: &Edit) -> CoreResult<AppliedEdit>
pub fn apply_edit(&mut self, edit: &Edit) -> CoreResult<AppliedEdit>
Apply an edit and return what was applied. The returned
AppliedEdit::replaced_text is exactly what the caller needs to push
onto the undo stack as the inverse.
The range endpoints are validated and snapped down to UTF-8 char
boundaries exactly as in Self::slice. On error the buffer is left
untouched. Note that AppliedEdit::original_range and the delta’s
positions echo the edit’s range as given, before snapping.
§Errors
Same as Self::slice: an out-of-bounds endpoint or end < start.
Sourcepub fn position_to_byte(&self, pos: Position) -> CoreResult<usize>
pub fn position_to_byte(&self, pos: Position) -> CoreResult<usize>
Convert a [Position] to an absolute byte offset from the start of
the buffer.
pos.line must be < rope_line_count(). pos.byte may be anywhere
up to and including the line’s full length with its trailing
\n — so the offset just past the newline is accepted and equals the
next line’s start. The offset is not snapped to a char boundary.
§Errors
[ProtocolError::PositionOutOfBounds] if the line does not exist or
the byte offset is past the end of the line.
§Examples
use lattice_core::Buffer;
use lattice_core::protocol::position::Position;
let buf = Buffer::from_text("ab\ncd");
assert_eq!(buf.position_to_byte(Position::new(1, 1)).ok(), Some(4));
assert!(buf.position_to_byte(Position::new(1, 3)).is_err());
assert_eq!(
buf.byte_to_position(4).ok(),
Some(Position::new(1, 1)),
);Sourcepub fn byte_to_position(&self, byte: usize) -> CoreResult<Position>
pub fn byte_to_position(&self, byte: usize) -> CoreResult<Position>
Convert an absolute byte offset into a [Position] (line + byte
within that line). The inverse of Self::position_to_byte.
§Panics
Despite the Result return type this never returns Err: a byte
greater than Self::byte_len panics inside ropey. Callers pass
offsets derived from the buffer itself.