lattice_protocol/edit.rs
1//! Edit primitives.
2//!
3//! An `Edit` is a single atomic change to a buffer. Compound changes are
4//! sequences of edits; the dispatcher groups them into one undo step.
5//!
6//! [`EditDelta`] is the tree-sitter-shaped sibling of `Edit`: the
7//! byte/position deltas a parser needs to know how an edit reshaped
8//! a buffer. Producers (the buffer's `apply_edit`) emit it as a
9//! by-product of the rope mutation; consumers (the syntax worker)
10//! convert it to `tree_sitter::InputEdit` at the parser boundary so
11//! `lattice-protocol` stays parser-agnostic. The type sits here so
12//! all edit-shaped types live together.
13
14use serde::{Deserialize, Serialize};
15
16use crate::position::{Position, Range};
17
18/// A single buffer mutation: replace the bytes in [`Self::range`] with the
19/// text carried by [`Self::kind`].
20///
21/// Coordinates are pre-edit [`Position`]s (line + UTF-8 byte). A compound
22/// change is a sequence of `Edit`s applied in order — each against the buffer
23/// as the previous one left it — and grouped into one undo step by the
24/// dispatcher.
25///
26/// # Examples
27///
28/// ```
29/// use lattice_protocol::{Edit, EditKind, Position, Range};
30///
31/// // Insert "fn " at the start of line 3: an empty range plus text.
32/// let insert = Edit::insert(Position::new(3, 0), "fn ");
33/// assert!(insert.range.is_empty());
34///
35/// // Delete bytes 4..7 of line 0: a range plus empty text.
36/// let delete = Edit::delete(Range::new(Position::new(0, 4), Position::new(0, 7)));
37/// let EditKind::Replace { text } = &delete.kind;
38/// assert!(text.is_empty());
39///
40/// // Replace is the general form both of the above reduce to.
41/// let word = Range::new(Position::new(1, 0), Position::new(1, 5));
42/// assert_eq!(Edit::replace(word, "world").range, word);
43/// ```
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
45pub struct Edit {
46 /// The half-open span to replace, in pre-edit coordinates. Empty for an
47 /// insert.
48 pub range: Range,
49 /// What to put there.
50 pub kind: EditKind,
51}
52
53/// The mutation operation. Replace covers insert (empty range) and delete
54/// (empty replacement); we keep the simple form here and let higher layers
55/// build dot-repeat / macro records on top.
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
57pub enum EditKind {
58 /// Replace the bytes in `range` with `text`. An insert is `range.is_empty()
59 /// && !text.is_empty()`; a delete is `!range.is_empty() && text.is_empty()`.
60 Replace {
61 /// The replacement text; empty for a delete.
62 text: String,
63 },
64}
65
66impl Edit {
67 /// Insert `text` at `at` (an empty-range replace).
68 pub fn insert(at: crate::Position, text: impl Into<String>) -> Self {
69 Self {
70 range: Range::empty(at),
71 kind: EditKind::Replace { text: text.into() },
72 }
73 }
74
75 /// Delete the bytes in `range` (a replace with empty text).
76 pub fn delete(range: Range) -> Self {
77 Self {
78 range,
79 kind: EditKind::Replace {
80 text: String::new(),
81 },
82 }
83 }
84
85 /// Replace the bytes in `range` with `text`.
86 pub fn replace(range: Range, text: impl Into<String>) -> Self {
87 Self {
88 range,
89 kind: EditKind::Replace { text: text.into() },
90 }
91 }
92}
93
94/// Tree-sitter-shaped description of a single applied edit.
95///
96/// Carries the six fields tree-sitter's `InputEdit` needs (three
97/// byte offsets + three line/byte positions) so a syntax worker
98/// can drive incremental reparse without re-querying the buffer.
99/// Produced as a by-product of `Buffer::apply_edit` (zero new
100/// rope reads -- every field is already computed there); rides
101/// on `lattice_core::AppliedEdit`. The actual `tree_sitter::InputEdit`
102/// conversion lives in `lattice-syntax` so this crate stays
103/// parser-agnostic.
104///
105/// Field semantics match tree-sitter:
106/// - `start_byte`: byte offset of the edit's start in the
107/// pre-edit buffer.
108/// - `old_end_byte`: byte offset of the end of the deleted span
109/// in the pre-edit buffer (== `start_byte` for pure inserts).
110/// - `new_end_byte`: byte offset of the end of the inserted span
111/// in the post-edit buffer (== `start_byte` for pure deletes).
112/// - `start_position` / `old_end_position` / `new_end_position`:
113/// the same three points in line-and-byte-within-line form.
114/// `Position.byte` is byte-within-line, which already matches
115/// tree-sitter's `Point.column` semantics (column-as-bytes).
116///
117/// `Copy` so callers pass it through chains by register move,
118/// not by Arc bump. 36 bytes -- fits one cache line.
119///
120/// The producer (`lattice_core::Buffer::apply_edit`) saturates each byte
121/// offset at `u32::MAX` rather than panicking on a buffer over 4 GiB.
122#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
123pub struct EditDelta {
124 /// Absolute byte offset (from the start of the buffer) where the edit
125 /// begins; the same in the pre- and post-edit buffer.
126 pub start_byte: u32,
127 /// Absolute byte offset of the end of the removed span, in the pre-edit
128 /// buffer. Equals `start_byte` for a pure insert.
129 pub old_end_byte: u32,
130 /// Absolute byte offset of the end of the inserted span, in the post-edit
131 /// buffer. Equals `start_byte` for a pure delete.
132 pub new_end_byte: u32,
133 /// `start_byte` as (line, byte-within-line).
134 pub start_position: Position,
135 /// `old_end_byte` as (line, byte-within-line), pre-edit.
136 pub old_end_position: Position,
137 /// `new_end_byte` as (line, byte-within-line), post-edit.
138 pub new_end_position: Position,
139}
140
141#[cfg(test)]
142mod tests {
143 #![allow(clippy::unwrap_used, clippy::panic)]
144 use super::*;
145 use crate::position::Position;
146
147 #[test]
148 fn insert_uses_empty_range_at_position() {
149 let edit = Edit::insert(Position::new(2, 5), "hi");
150 assert!(edit.range.is_empty());
151 assert_eq!(edit.range.start, Position::new(2, 5));
152 let EditKind::Replace { text } = &edit.kind;
153 assert_eq!(text, "hi");
154 }
155
156 #[test]
157 fn delete_uses_empty_replacement_text() {
158 let r = Range::new(Position::new(0, 0), Position::new(0, 3));
159 let edit = Edit::delete(r);
160 assert_eq!(edit.range, r);
161 let EditKind::Replace { text } = &edit.kind;
162 assert!(text.is_empty());
163 }
164
165 #[test]
166 fn replace_carries_both_range_and_text() {
167 let r = Range::new(Position::new(1, 0), Position::new(1, 5));
168 let edit = Edit::replace(r, "world");
169 assert_eq!(edit.range, r);
170 let EditKind::Replace { text } = &edit.kind;
171 assert_eq!(text, "world");
172 }
173
174 #[test]
175 fn edit_is_clone_and_eq() {
176 let edit = Edit::insert(Position::ZERO, "hi");
177 let copy = edit.clone();
178 assert_eq!(edit, copy);
179 }
180
181 #[test]
182 fn edit_is_serializable() {
183 let r = Range::new(Position::new(1, 0), Position::new(1, 5));
184 let edit = Edit::replace(r, "x");
185 let json = serde_json::to_string(&edit).unwrap();
186 let back: Edit = serde_json::from_str(&json).unwrap();
187 assert_eq!(back, edit);
188 }
189}