Skip to main content

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}